From 1a2d03f529c5bfaff214a8ec3b5e121aa1d61eda Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 15:44:43 +0800 Subject: [PATCH 01/28] feat: sync bot rich messages and inline menus --- cmd/bots/bedolagaformat/README.md | 19 + cmd/bots/bedolagaformat/demo.py | 127 ++++- cmd/bots/bedolagaformat/test_demo.py | 42 ++ internal/app/dialogs/read_model_cache.go | 2 + internal/app/dialogs/service.go | 1 + internal/botapi/projection.go | 8 +- internal/botapi/rich_message.go | 99 ++++ internal/botapi/server.go | 113 +++- internal/botapi/server_test.go | 124 +++++ internal/domain/botapi_rich_message.go | 29 + internal/domain/channel.go | 2 +- internal/domain/message.go | 13 +- internal/rpc/botapi_gateway.go | 158 +++++- internal/rpc/botapi_gateway_test.go | 106 ++++ internal/rpc/botapi_rich_message.go | 305 +++++++++++ internal/rpc/botapi_rich_projection.go | 503 ++++++++++++++++++ internal/rpc/convert_rich_message.go | 57 +- internal/rpc/errors.go | 12 + internal/rpc/messages_bot_no_state.go | 42 +- internal/rpc/messages_edit.go | 12 +- .../rpc/messages_rich_message_rpc_test.go | 111 +++- internal/rpc/messages_send.go | 8 +- internal/rpc/rich_message_limits.go | 215 ++++++++ internal/store/memory/message_helpers.go | 3 +- 24 files changed, 2073 insertions(+), 38 deletions(-) create mode 100644 internal/botapi/rich_message.go create mode 100644 internal/domain/botapi_rich_message.go create mode 100644 internal/rpc/botapi_rich_message.go create mode 100644 internal/rpc/botapi_rich_projection.go create mode 100644 internal/rpc/rich_message_limits.go diff --git a/cmd/bots/bedolagaformat/README.md b/cmd/bots/bedolagaformat/README.md index 32dfccb4..155cff04 100644 --- a/cmd/bots/bedolagaformat/README.md +++ b/cmd/bots/bedolagaformat/README.md @@ -14,6 +14,13 @@ telesrv 发送 `parse_mode=HTML`。`/formatdemo` 依次发送默认 HTML、legac MarkdownV2,用于验证完整的 `aiogram → telesrv Bot API → MTProto message/update → TDesktop` 链路。 +`/richdemo` 进一步复刻 Bedolaga 的 rich menu:调用 `sendRichMessage` 发送 HTML 与 +Markdown `InputRichMessage`,携带 inline callback keyboard,再通过 +`editMessageText.rich_message` 编辑 HTML 菜单。HTML 样例覆盖 heading、divider、 +bordered/striped table、`tg-time`、details、blockquote、code 与 footer。第一次请求 +故意带远程 logo;当前本地 blob backend 返回 `WEBPAGE_MEDIA_EMPTY` 后,demo 按 +Bedolaga 的既有策略自动去掉 logo 重试,正文与按钮不会降级成 classic menu。 + ## 安装 建议使用虚拟环境,token 只通过环境变量传入: @@ -34,6 +41,7 @@ $env:TELESRV_BOT_API_SERVER = "http://127.0.0.1:8081" ```text /start /formatdemo +/richdemo ``` 也可以不启动 polling,直接向指定私聊发送三条格式测试消息: @@ -46,5 +54,16 @@ $env:TELESRV_BOT_API_SERVER = "http://127.0.0.1:8081" --marker BEDOLAGA-LOCAL-VERIFY ``` +只主动验证 rich menu(HTML + Markdown + 按钮 + 编辑 + logo fallback): + +```powershell +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" ` + .\cmd\bots\bedolagaformat\demo.py ` + --send-only ` + --rich-only ` + --send-chat-id 1780243200 ` + --marker BEDOLAGA-RICH-VERIFY +``` + `--base-url` 只接受 API server 根地址,不要追加 `/bot`。脚本不会打印 token,也不会 把 token 写入文件。 diff --git a/cmd/bots/bedolagaformat/demo.py b/cmd/bots/bedolagaformat/demo.py index a90746e5..d339a8db 100644 --- a/cmd/bots/bedolagaformat/demo.py +++ b/cmd/bots/bedolagaformat/demo.py @@ -21,8 +21,14 @@ from aiogram.client.default import DefaultBotProperties from aiogram.client.session.aiohttp import AiohttpSession from aiogram.client.telegram import TelegramAPIServer from aiogram.enums import ParseMode +from aiogram.exceptions import TelegramBadRequest from aiogram.filters import Command, CommandStart -from aiogram.types import Message +from aiogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + InputRichMessage, + Message, +) LOG = logging.getLogger("bedolagaformat") @@ -101,6 +107,16 @@ def parse_args() -> argparse.Namespace: help="Send the complete suite proactively before polling", ) parser.add_argument("--send-only", action="store_true") + parser.add_argument( + "--rich-menu", + action="store_true", + help="also send and edit Bedolaga-style rich HTML/Markdown menus", + ) + parser.add_argument( + "--rich-only", + action="store_true", + help="with --send-only, send only the rich menu suite", + ) parser.add_argument("--drop-pending", action="store_true") parser.add_argument("--polling-timeout", type=int, default=10) parser.add_argument("--marker", default=default_marker()) @@ -110,6 +126,8 @@ def parse_args() -> argparse.Namespace: parser.error("missing --token or TELESRV_BOT_TOKEN") if args.send_only and args.send_chat_id is None: parser.error("--send-only requires --send-chat-id") + if args.rich_only and (not args.send_only or args.send_chat_id is None): + parser.error("--rich-only requires --send-only and --send-chat-id") if not MARKER_RE.fullmatch(args.marker): parser.error("--marker must contain 1-64 ASCII letters, digits, or hyphens") if not 0 <= args.polling_timeout <= 50: @@ -151,6 +169,96 @@ async def send_format_suite(bot: Bot, chat_id: int, marker: str) -> list[int]: return message_ids +def rich_menu_html(marker: str, *, include_logo: bool) -> str: + """Build the rich HTML families used by Bedolaga's main menu.""" + logo = '' if include_logo else "" + return ( + f"{logo}

{marker} Admin

" + "
Subscription overview

" + "" + "" + "
StatusActive
Updated" + 'now' + "
" + "
Diagnostics" + "
rich menu online
" + "" + ) + + +def rich_menu_markdown(marker: str) -> str: + return ( + f"#### {marker} Markdown menu\n\n" + "**Subscription:** Active\n\n" + "> Rich Markdown transport is online.\n\n" + "`callback keyboard preserved`" + ) + + +def rich_menu_keyboard() -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Balance", callback_data="menu:balance"), + InlineKeyboardButton(text="Buy", callback_data="menu:buy"), + ], + [InlineKeyboardButton(text="Info", callback_data="menu:info")], + ] + ) + + +def is_rich_media_retry_error(exc: TelegramBadRequest) -> bool: + message = str(exc).lower() + return "webpage_" in message or "media_empty" in message or "media_invalid" in message + + +async def send_rich_suite(bot: Bot, chat_id: int, marker: str) -> list[int]: + """Exercise Bedolaga's send, no-logo retry, keyboard and rich edit path.""" + markup = rich_menu_keyboard() + try: + html_message = await bot.send_rich_message( + chat_id=chat_id, + rich_message=InputRichMessage( + html=rich_menu_html(marker, include_logo=True), + skip_entity_detection=True, + ), + reply_markup=markup, + ) + except TelegramBadRequest as exc: + if not is_rich_media_retry_error(exc): + raise + LOG.info("rich logo fetch rejected; retrying the menu without logo") + html_message = await bot.send_rich_message( + chat_id=chat_id, + rich_message=InputRichMessage( + html=rich_menu_html(marker, include_logo=False), + skip_entity_detection=True, + ), + reply_markup=markup, + ) + + markdown_message = await bot.send_rich_message( + chat_id=chat_id, + rich_message=InputRichMessage( + markdown=rich_menu_markdown(marker), + skip_entity_detection=True, + ), + reply_markup=markup, + ) + await bot.edit_message_text( + chat_id=chat_id, + message_id=html_message.message_id, + rich_message=InputRichMessage( + html=rich_menu_html(f"{marker} EDITED", include_logo=False), + skip_entity_detection=True, + ), + reply_markup=markup, + ) + ids = [html_message.message_id, markdown_message.message_id] + LOG.info("sent rich menu suite chat_id=%s message_ids=%s", chat_id, ids) + return ids + + def build_dispatcher(marker: str) -> Dispatcher: router = Router(name="telesrv-bedolaga-format") @@ -173,6 +281,16 @@ def build_dispatcher(marker: str) -> Dispatcher: ids, ) + @router.message(Command("richdemo")) + async def rich_demo(message: Message) -> None: + ids = await send_rich_suite(message.bot, message.chat.id, marker) + LOG.info( + "handled /richdemo chat_id=%s incoming_message_id=%s sent_message_ids=%s", + message.chat.id, + message.message_id, + ids, + ) + dispatcher = Dispatcher() dispatcher.include_router(router) return dispatcher @@ -190,13 +308,16 @@ async def run(args: argparse.Namespace) -> None: args.marker, ) if args.send_chat_id is not None: - await send_format_suite(bot, args.send_chat_id, args.marker) + if not args.rich_only: + await send_format_suite(bot, args.send_chat_id, args.marker) + if args.rich_menu or args.rich_only: + await send_rich_suite(bot, args.send_chat_id, args.marker) if args.send_only: return await bot.delete_webhook(drop_pending_updates=args.drop_pending) dispatcher = build_dispatcher(args.marker) - LOG.info("polling started; send /start or /formatdemo to @%s", me.username or me.id) + LOG.info("polling started; send /start, /formatdemo or /richdemo to @%s", me.username or me.id) await dispatcher.start_polling( bot, allowed_updates=["message"], diff --git a/cmd/bots/bedolagaformat/test_demo.py b/cmd/bots/bedolagaformat/test_demo.py index 26530af2..d4e005e2 100644 --- a/cmd/bots/bedolagaformat/test_demo.py +++ b/cmd/bots/bedolagaformat/test_demo.py @@ -5,6 +5,9 @@ import unittest from unittest.mock import AsyncMock from aiogram.enums import ParseMode +from aiogram.exceptions import TelegramBadRequest +from aiogram.methods import SendRichMessage +from aiogram.types import InputRichMessage MODULE_PATH = Path(__file__).with_name("demo.py") @@ -47,6 +50,45 @@ class BedolagaFormatDemoTest(unittest.IsolatedAsyncioTestCase): self.assertEqual(calls[1].kwargs["parse_mode"], ParseMode.MARKDOWN) self.assertEqual(calls[2].kwargs["parse_mode"], ParseMode.MARKDOWN_V2) + def test_rich_menu_covers_bedolaga_html_and_keyboard(self) -> None: + html = demo.rich_menu_html("BEDOLAGA123", include_logo=False) + self.assertIn("

BEDOLAGA123 Admin

", html) + self.assertIn("", html) + self.assertIn("", html) + self.assertIn("
", html) + markup = demo.rich_menu_keyboard() + self.assertEqual(markup.inline_keyboard[0][0].callback_data, "menu:balance") + self.assertEqual(markup.inline_keyboard[1][0].callback_data, "menu:info") + + async def test_rich_suite_retries_without_logo_and_edits(self) -> None: + bot = AsyncMock() + media_error = TelegramBadRequest( + method=SendRichMessage( + chat_id=1780243200, + rich_message=InputRichMessage(html="

fixture

"), + ), + message="WEBPAGE_MEDIA_EMPTY", + ) + bot.send_rich_message.side_effect = [ + media_error, + SentMessage(21), + SentMessage(22), + ] + bot.edit_message_text.return_value = SentMessage(21) + + ids = await demo.send_rich_suite(bot, 1780243200, "BEDOLAGA123") + + self.assertEqual(ids, [21, 22]) + sends = bot.send_rich_message.await_args_list + self.assertEqual(len(sends), 3) + self.assertIn(" 0 + return msg.Body != "" || (msg.RichMessage != nil && len(msg.RichMessage.BotAPIProjection) > 0) || len(apiMessageMedia(msg.Media, nil, nil)) > 0 } func apiUser(u domain.User) map[string]any { @@ -320,6 +320,12 @@ func apiMessage(msg domain.Message, users []domain.User, channelLists ...[]domai out["entities"] = entities } } + if msg.RichMessage != nil && len(msg.RichMessage.BotAPIProjection) > 0 { + var richMessage any + if json.Unmarshal(msg.RichMessage.BotAPIProjection, &richMessage) == nil && richMessage != nil { + out["rich_message"] = richMessage + } + } if msg.EditDate > 0 { out["edit_date"] = msg.EditDate } diff --git a/internal/botapi/rich_message.go b/internal/botapi/rich_message.go new file mode 100644 index 00000000..2fe9d309 --- /dev/null +++ b/internal/botapi/rich_message.go @@ -0,0 +1,99 @@ +package botapi + +import ( + "bytes" + "encoding/json" + "errors" + "strconv" + "strings" + + "telesrv/internal/domain" +) + +const maxBotAPIRichSourceBytes = 256 << 10 + +func richMessageInputFromAPI(raw string) (domain.BotAPIRichMessageInput, error) { + raw = strings.TrimSpace(raw) + if raw == "" || len(raw) > maxBotAPIRichSourceBytes { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + var fields map[string]json.RawMessage + if err := json.Unmarshal([]byte(raw), &fields); err != nil { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + var out domain.BotAPIRichMessageInput + sources := 0 + if value, ok := fields["html"]; ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) { + if err := json.Unmarshal(value, &out.HTML); err != nil || out.HTML == "" { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + sources++ + } + if value, ok := fields["markdown"]; ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) { + if err := json.Unmarshal(value, &out.Markdown); err != nil || out.Markdown == "" { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + sources++ + } + if value, ok := fields["blocks"]; ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) { + if len(bytes.TrimSpace(value)) == 0 { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + out.BlocksJSON = append([]byte(nil), value...) + sources++ + } + if sources != 1 { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + if len(out.BlocksJSON) != 0 { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_BLOCKS_UNSUPPORTED") + } + if value, ok := fields["media"]; ok && !bytes.Equal(bytes.TrimSpace(value), []byte("null")) && !bytes.Equal(bytes.TrimSpace(value), []byte("[]")) { + out.MediaJSON = append([]byte(nil), value...) + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_MEDIA_UNSUPPORTED") + } + if value, ok := fields["is_rtl"]; ok { + if err := json.Unmarshal(value, &out.RTL); err != nil { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + } + if value, ok := fields["skip_entity_detection"]; ok { + if err := json.Unmarshal(value, &out.SkipEntityDetection); err != nil { + return domain.BotAPIRichMessageInput{}, errors.New("RICH_MESSAGE_INVALID") + } + } + return out, nil +} + +func richReplyMessageID(values map[string]string) (int, error) { + legacy := apiInt(values["reply_to_message_id"], 0) + raw := strings.TrimSpace(values["reply_parameters"]) + if raw == "" { + if legacy < 0 { + return 0, errors.New("REPLY_MESSAGE_ID_INVALID") + } + return legacy, nil + } + if legacy != 0 { + return 0, errors.New("REPLY_PARAMETERS_INVALID") + } + var payload struct { + MessageID int `json:"message_id"` + } + if err := json.Unmarshal([]byte(raw), &payload); err != nil || payload.MessageID <= 0 { + return 0, errors.New("REPLY_PARAMETERS_INVALID") + } + return payload.MessageID, nil +} + +func apiInt64(raw string) (int64, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return 0, nil + } + value, err := strconv.ParseInt(raw, 10, 64) + if err != nil || value < 0 { + return 0, errors.New("VALUE_INVALID") + } + return value, nil +} diff --git a/internal/botapi/server.go b/internal/botapi/server.go index 5495724c..333fe288 100644 --- a/internal/botapi/server.go +++ b/internal/botapi/server.go @@ -43,9 +43,12 @@ type GatewayService interface { BotAPISelf(ctx context.Context, botID int64) (domain.User, error) BotAPIUpdates(ctx context.Context, botID int64, offset int64) ([]domain.UpdateEvent, error) BotAPISendMessage(ctx context.Context, botID, chatID int64, text string, entities []domain.MessageEntity, replyMarkup *domain.MessageReplyMarkup, disableWebPagePreview, silent bool, replyToMessageID int) (domain.Message, error) + BotAPISendRichMessage(ctx context.Context, botID, chatID int64, rich domain.BotAPIRichMessageInput, replyMarkup *domain.MessageReplyMarkup, silent, noForwards bool, replyToMessageID int, effectID int64) (domain.Message, error) BotAPISendMedia(ctx context.Context, botID, chatID int64, kind, locationKey, remoteURL, fileName, mimeType string, fileBytes []byte, caption string, entities []domain.MessageEntity, replyMarkup *domain.MessageReplyMarkup, silent bool, replyToMessageID int) (domain.Message, error) BotAPIEditMessageText(ctx context.Context, botID, chatID int64, messageID int, text string, entities []domain.MessageEntity, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup, disableWebPagePreview bool) (domain.Message, error) + BotAPIEditRichMessage(ctx context.Context, botID, chatID int64, messageID int, rich domain.BotAPIRichMessageInput, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup) (domain.Message, error) BotAPIEditInlineMessageText(ctx context.Context, botID int64, inlineMessageID domain.BotInlineMessageID, text string, entities []domain.MessageEntity, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup, disableWebPagePreview bool) (bool, error) + BotAPIEditInlineRichMessage(ctx context.Context, botID int64, inlineMessageID domain.BotInlineMessageID, rich domain.BotAPIRichMessageInput, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup) (bool, error) BotAPIDeleteMessage(ctx context.Context, botID, chatID int64, messageID int) (bool, error) BotAPIAnswerCallbackQuery(ctx context.Context, botID int64, callbackQueryID, text, url string, showAlert bool, cacheTime int) (bool, error) BotAPIGetFile(ctx context.Context, botID int64, locationKey string, offset int64, limit int) (domain.FileChunk, bool, error) @@ -204,6 +207,8 @@ func (h *handler) handle(w http.ResponseWriter, r *http.Request) { h.getUpdates(w, r, botID) case "sendmessage": h.sendMessage(w, r, botID) + case "sendrichmessage": + h.sendRichMessage(w, r, botID) case "sendphoto": h.sendMedia(w, r, botID, "photo") case "sendanimation": @@ -577,6 +582,71 @@ func (h *handler) sendMessage(w http.ResponseWriter, r *http.Request, botID int6 writeAPIOK(w, apiMessage(msg, users)) } +func (h *handler) sendRichMessage(w http.ResponseWriter, r *http.Request, botID int64) { + if h.gateway == nil { + writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") + return + } + values, err := requestValues(r) + if err != nil { + writeAPIError(w, http.StatusBadRequest, "BAD_REQUEST") + return + } + chatID, err := strconv.ParseInt(strings.TrimSpace(values["chat_id"]), 10, 64) + if err != nil || chatID == 0 { + writeAPIError(w, http.StatusBadRequest, "CHAT_ID_INVALID") + return + } + if strings.TrimSpace(values["business_connection_id"]) != "" { + writeAPIError(w, http.StatusBadRequest, "BUSINESS_CONNECTION_INVALID") + return + } + if apiInt(values["message_thread_id"], 0) != 0 || apiInt(values["direct_messages_topic_id"], 0) != 0 { + writeAPIError(w, http.StatusBadRequest, "MESSAGE_THREAD_INVALID") + return + } + if apiBool(values["allow_paid_broadcast"]) || strings.TrimSpace(values["suggested_post_parameters"]) != "" { + writeAPIError(w, http.StatusBadRequest, "RICH_MESSAGE_OPTION_UNSUPPORTED") + return + } + rich, err := richMessageInputFromAPI(values["rich_message"]) + if err != nil { + writeAPIError(w, http.StatusBadRequest, err.Error()) + return + } + var markup *domain.MessageReplyMarkup + if raw := strings.TrimSpace(values["reply_markup"]); raw != "" { + markup, err = inlineReplyMarkupFromAPI(json.RawMessage(raw)) + if err != nil { + writeAPIError(w, http.StatusBadRequest, err.Error()) + return + } + } + replyTo, err := richReplyMessageID(values) + if err != nil { + writeAPIError(w, http.StatusBadRequest, err.Error()) + return + } + effectID, err := apiInt64(values["message_effect_id"]) + if err != nil { + writeAPIError(w, http.StatusBadRequest, "EFFECT_ID_INVALID") + return + } + msg, err := h.gateway.BotAPISendRichMessage( + r.Context(), botID, chatID, rich, markup, + apiBool(values["disable_notification"]), apiBool(values["protect_content"]), replyTo, effectID, + ) + if err != nil { + writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) + return + } + users := []domain.User(nil) + if self, err := h.gateway.BotAPISelf(r.Context(), botID); err == nil && self.ID != 0 { + users = append(users, self) + } + writeAPIOK(w, apiMessage(msg, users)) +} + func (h *handler) sendMedia(w http.ResponseWriter, r *http.Request, botID int64, kind string) { if h.gateway == nil { writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") @@ -695,7 +765,26 @@ func (h *handler) editMessageText(w http.ResponseWriter, r *http.Request, botID writeAPIError(w, http.StatusBadRequest, "MESSAGE_IDENTIFIER_INVALID") return } - text, entities, err := botAPIFormattedTextRaw(values["text"], values["parse_mode"], values["entities"], domain.MaxMessageTextLength, true) + rawRich := strings.TrimSpace(values["rich_message"]) + _, textSpecified := values["text"] + if rawRich != "" && textSpecified { + writeAPIError(w, http.StatusBadRequest, "RICH_MESSAGE_INVALID") + return + } + var ( + text string + entities []domain.MessageEntity + rich domain.BotAPIRichMessageInput + ) + if rawRich != "" { + rich, err = richMessageInputFromAPI(rawRich) + } else { + if !textSpecified { + writeAPIError(w, http.StatusBadRequest, "MESSAGE_EMPTY") + return + } + text, entities, err = botAPIFormattedTextRaw(values["text"], values["parse_mode"], values["entities"], domain.MaxMessageTextLength, true) + } if err != nil { writeAPIError(w, http.StatusBadRequest, err.Error()) return @@ -715,7 +804,12 @@ func (h *handler) editMessageText(w http.ResponseWriter, r *http.Request, botID writeAPIError(w, http.StatusBadRequest, err.Error()) return } - ok, err := h.gateway.BotAPIEditInlineMessageText(r.Context(), botID, inlineID, text, entities, setReplyMarkup, markup, apiBool(values["disable_web_page_preview"])) + var ok bool + if rawRich != "" { + ok, err = h.gateway.BotAPIEditInlineRichMessage(r.Context(), botID, inlineID, rich, setReplyMarkup, markup) + } else { + ok, err = h.gateway.BotAPIEditInlineMessageText(r.Context(), botID, inlineID, text, entities, setReplyMarkup, markup, apiBool(values["disable_web_page_preview"])) + } if err != nil { writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) return @@ -723,7 +817,12 @@ func (h *handler) editMessageText(w http.ResponseWriter, r *http.Request, botID writeAPIOK(w, ok) return } - msg, err := h.gateway.BotAPIEditMessageText(r.Context(), botID, chatID, messageID, text, entities, setReplyMarkup, markup, apiBool(values["disable_web_page_preview"])) + var msg domain.Message + if rawRich != "" { + msg, err = h.gateway.BotAPIEditRichMessage(r.Context(), botID, chatID, messageID, rich, setReplyMarkup, markup) + } else { + msg, err = h.gateway.BotAPIEditMessageText(r.Context(), botID, chatID, messageID, text, entities, setReplyMarkup, markup, apiBool(values["disable_web_page_preview"])) + } if err != nil { writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) return @@ -1338,6 +1437,14 @@ func apiErrorDescription(err error) string { "RESULT_TYPE_INVALID", "MESSAGE_EMPTY", "MESSAGE_TOO_LONG", + "RICH_MESSAGE_INVALID", + "RICH_MESSAGE_TOO_LONG", + "RICH_MESSAGE_DATE_INVALID", + "RICH_MESSAGE_BLOCKS_UNSUPPORTED", + "RICH_MESSAGE_MEDIA_UNSUPPORTED", + "RICH_MESSAGE_OPTION_UNSUPPORTED", + "WEBPAGE_MEDIA_EMPTY", + "EFFECT_ID_INVALID", "BUTTON_INVALID", "BUTTON_DATA_INVALID", "BUTTON_URL_INVALID", diff --git a/internal/botapi/server_test.go b/internal/botapi/server_test.go index b6f33227..e6afa8de 100644 --- a/internal/botapi/server_test.go +++ b/internal/botapi/server_test.go @@ -600,6 +600,101 @@ func TestSendMessageParsesEntitiesMarkupAndCallsGateway(t *testing.T) { } } +func TestSendRichMessageAndEditPreserveInlineKeyboardAndProjection(t *testing.T) { + bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} + projection := json.RawMessage(`{"blocks":[{"type":"heading","size":4,"text":"Admin"}],"is_rtl":true}`) + markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{ + Type: domain.MarkupButtonCallback, Text: "Info", Data: []byte("menu:info"), + }}}} + message := domain.Message{ + ID: 21, OwnerUserID: 1001, + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 2001}, + From: domain.Peer{Type: domain.PeerTypeUser, ID: 1001}, + Date: 1700000021, Out: true, ReplyMarkup: markup, + RichMessage: &domain.MessageRichMessage{Rtl: true, Blocks: []byte{1}, BotAPIProjection: projection}, + } + gateway := &fakeBotAPIGateway{ + self: domain.User{ID: 1001, FirstName: "Bedolaga", Username: "bedolaga_bot", Bot: true}, + sendMessage: message, + editMessage: message, + } + h := (&handler{bots: bots, gateway: gateway}).routes() + + rec := performBotAPIRequest(t, h, bots.profile, "sendRichMessage", `{ + "chat_id":2001, + "rich_message":{"html":"

Admin

","is_rtl":true,"skip_entity_detection":true}, + "reply_markup":{"inline_keyboard":[[{"text":"Info","callback_data":"menu:info"}]]}, + "disable_notification":true, + "protect_content":true, + "reply_parameters":{"message_id":7} + }`) + if rec.Code != http.StatusOK { + t.Fatalf("sendRichMessage status=%d body=%s", rec.Code, rec.Body.String()) + } + if !gateway.sendRichCalled || gateway.sendChatID != 2001 || gateway.sendRichInput.HTML != "

Admin

" || + !gateway.sendRichInput.RTL || !gateway.sendRichInput.SkipEntityDetection || !gateway.sendSilent || gateway.sendReplyTo != 7 { + t.Fatalf("send rich call = %#v", gateway) + } + if gateway.sendRichMarkup == nil || len(gateway.sendRichMarkup.Inline) != 1 || + string(gateway.sendRichMarkup.Inline[0][0].Data) != "menu:info" { + t.Fatalf("send rich markup = %#v", gateway.sendRichMarkup) + } + assertBotAPIRichMenuResponse(t, rec.Body.Bytes(), 21) + + gateway.editMessage.RichMessage.BotAPIProjection = json.RawMessage(`{"blocks":[{"type":"paragraph","text":"Updated"}]}`) + rec = performBotAPIRequest(t, h, bots.profile, "editMessageText", `{ + "chat_id":2001, + "message_id":21, + "rich_message":{"markdown":"**Updated**","skip_entity_detection":true}, + "reply_markup":{"inline_keyboard":[[{"text":"Info","callback_data":"menu:info"}]]} + }`) + if rec.Code != http.StatusOK { + t.Fatalf("editMessageText rich status=%d body=%s", rec.Code, rec.Body.String()) + } + if !gateway.editRichCalled || gateway.editRichInput.Markdown != "**Updated**" || !gateway.editRichInput.SkipEntityDetection || !gateway.editSetMarkup { + t.Fatalf("edit rich call = %#v", gateway) + } + assertBotAPIRichMenuResponse(t, rec.Body.Bytes(), 21) +} + +func TestEditMessageTextRejectsTextAndRichMessageTogether(t *testing.T) { + bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} + h := (&handler{bots: bots, gateway: &fakeBotAPIGateway{}}).routes() + rec := performBotAPIRequest(t, h, bots.profile, "editMessageText", `{ + "chat_id":2001,"message_id":21,"text":"plain","rich_message":{"html":"

rich

"} + }`) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "RICH_MESSAGE_INVALID") { + t.Fatalf("edit text+rich status=%d body=%s", rec.Code, rec.Body.String()) + } +} + +func assertBotAPIRichMenuResponse(t *testing.T, raw []byte, messageID int) { + t.Helper() + var response struct { + OK bool `json:"ok"` + Result struct { + MessageID int `json:"message_id"` + RichMessage struct { + Blocks []struct { + Type string `json:"type"` + } `json:"blocks"` + } `json:"rich_message"` + ReplyMarkup struct { + InlineKeyboard [][]struct { + CallbackData string `json:"callback_data"` + } `json:"inline_keyboard"` + } `json:"reply_markup"` + } `json:"result"` + } + if err := json.Unmarshal(raw, &response); err != nil { + t.Fatalf("decode rich response: %v", err) + } + if !response.OK || response.Result.MessageID != messageID || len(response.Result.RichMessage.Blocks) != 1 || + len(response.Result.ReplyMarkup.InlineKeyboard) != 1 || response.Result.ReplyMarkup.InlineKeyboard[0][0].CallbackData != "menu:info" { + t.Fatalf("rich response = %s", raw) + } +} + func TestSendMessageParsesAndProjectsReplyKeyboard(t *testing.T) { bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} markup := &domain.MessageReplyMarkup{ @@ -1329,6 +1424,9 @@ type fakeBotAPIGateway struct { sendSilent bool sendReplyTo int sendMessage domain.Message + sendRichCalled bool + sendRichInput domain.BotAPIRichMessageInput + sendRichMarkup *domain.MessageReplyMarkup sendMediaCalled bool sendMediaKind string sendMediaChatID int64 @@ -1342,6 +1440,8 @@ type fakeBotAPIGateway struct { editEntities []domain.MessageEntity editSetMarkup bool editMessage domain.Message + editRichCalled bool + editRichInput domain.BotAPIRichMessageInput editInlineCalled bool editInlineID domain.BotInlineMessageID editInlineText string @@ -1448,6 +1548,17 @@ func (f *fakeBotAPIGateway) BotAPISendMessage(_ context.Context, botID, chatID i return f.sendMessage, nil } +func (f *fakeBotAPIGateway) BotAPISendRichMessage(_ context.Context, botID, chatID int64, rich domain.BotAPIRichMessageInput, replyMarkup *domain.MessageReplyMarkup, silent, noForwards bool, replyToMessageID int, effectID int64) (domain.Message, error) { + f.sendRichCalled = true + f.sendBotID = botID + f.sendChatID = chatID + f.sendRichInput = rich + f.sendRichMarkup = replyMarkup + f.sendSilent = silent + f.sendReplyTo = replyToMessageID + return f.sendMessage, nil +} + func (f *fakeBotAPIGateway) BotAPISendMedia(_ context.Context, botID, chatID int64, kind, locationKey, remoteURL, fileName, mimeType string, fileBytes []byte, caption string, entities []domain.MessageEntity, replyMarkup *domain.MessageReplyMarkup, silent bool, replyToMessageID int) (domain.Message, error) { f.sendMediaCalled = true f.sendMediaKind = kind @@ -1467,6 +1578,13 @@ func (f *fakeBotAPIGateway) BotAPIEditMessageText(_ context.Context, botID, chat return f.editMessage, nil } +func (f *fakeBotAPIGateway) BotAPIEditRichMessage(_ context.Context, botID, chatID int64, messageID int, rich domain.BotAPIRichMessageInput, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup) (domain.Message, error) { + f.editRichCalled = true + f.editRichInput = rich + f.editSetMarkup = setReplyMarkup + return f.editMessage, nil +} + func (f *fakeBotAPIGateway) BotAPIEditInlineMessageText(_ context.Context, _ int64, inlineMessageID domain.BotInlineMessageID, text string, entities []domain.MessageEntity, _ bool, _ *domain.MessageReplyMarkup, _ bool) (bool, error) { f.editInlineCalled, f.editInlineID = true, inlineMessageID f.editInlineText = text @@ -1474,6 +1592,12 @@ func (f *fakeBotAPIGateway) BotAPIEditInlineMessageText(_ context.Context, _ int return true, nil } +func (f *fakeBotAPIGateway) BotAPIEditInlineRichMessage(_ context.Context, _ int64, inlineMessageID domain.BotInlineMessageID, rich domain.BotAPIRichMessageInput, _ bool, _ *domain.MessageReplyMarkup) (bool, error) { + f.editInlineCalled, f.editInlineID = true, inlineMessageID + f.editRichInput = rich + return true, nil +} + func (f *fakeBotAPIGateway) BotAPIDeleteMessage(context.Context, int64, int64, int) (bool, error) { f.deleteCalled = true return true, nil diff --git a/internal/domain/botapi_rich_message.go b/internal/domain/botapi_rich_message.go new file mode 100644 index 00000000..514cb624 --- /dev/null +++ b/internal/domain/botapi_rich_message.go @@ -0,0 +1,29 @@ +package domain + +// BotAPIRichMessageInput is the protocol-neutral HTTP Bot API input passed to +// the RPC edge. Exactly one of HTML, Markdown, or BlocksJSON must be present. +// BlocksJSON and MediaJSON are retained in the DTO so unsupported Bot API 10.2 +// shapes are rejected explicitly at the conversion boundary instead of being +// flattened or silently dropped. +type BotAPIRichMessageInput struct { + HTML string + Markdown string + BlocksJSON []byte + MediaJSON []byte + RTL bool + SkipEntityDetection bool +} + +func (m BotAPIRichMessageInput) SourceCount() int { + n := 0 + if m.HTML != "" { + n++ + } + if m.Markdown != "" { + n++ + } + if len(m.BlocksJSON) != 0 { + n++ + } + return n +} diff --git a/internal/domain/channel.go b/internal/domain/channel.go index 5bc861da..027956a6 100644 --- a/internal/domain/channel.go +++ b/internal/domain/channel.go @@ -643,7 +643,7 @@ type ChannelMessage struct { Reactions *ChannelMessageReactions Action *ChannelMessageAction Media *MessageMedia - // RichMessage 是 Layer 227 富文本消息(richMessage)快照,可选;普通消息恒 nil。 + // RichMessage 是 Layer 228 富文本消息(richMessage)快照,可选;普通消息恒 nil。 RichMessage *MessageRichMessage // FromBoostsApplied 是发送时的 sender boost 数快照(message.from_boosts_applied)。 FromBoostsApplied int diff --git a/internal/domain/message.go b/internal/domain/message.go index 679bd7d3..c46fc9e0 100644 --- a/internal/domain/message.go +++ b/internal/domain/message.go @@ -154,7 +154,7 @@ type Message struct { // ReplyMarkup 是 bot 消息携带的 reply/inline keyboard 快照。仅 bot 出站消息可 // 非空;普通用户消息恒 nil(发送侧 is_bot 闸门)。双盒持同一快照(无 per-viewer 差异)。 ReplyMarkup *MessageReplyMarkup - // RichMessage 是 Layer 227 富文本消息(richMessage)快照,可选;普通消息恒 nil。 + // RichMessage 是 Layer 228 富文本消息(richMessage)快照,可选;普通消息恒 nil。 RichMessage *MessageRichMessage // Pinned 是 owner 视角的置顶标志(官方私聊多置顶语义:双方各自 // 的 box 行独立持有,非 pm_oneside 操作两侧同步翻转)。 @@ -165,15 +165,14 @@ type Message struct { SavedPeer Peer } -// MessageRichMessage 是 Layer 227 富文本消息(richMessage)的协议中立快照:一组 IV +// MessageRichMessage 是 Layer 228 富文本消息(richMessage)的协议中立快照:一组 IV // PageBlock(Blocks)+ 内嵌已解析的 Photos/Documents。 // // Blocks 存 gotd TL 序列化后的 []tg.PageBlockClass 不透明字节——PageBlock 体系庞大且 // input(inputRichMessage.blocks) 与 output(richMessage.blocks) 同构、原样透传,故不在 // domain 逐类型建模;rpc 层负责 tg.PageBlock 向量 ↔ bytes 的序列化(domain 不依赖 tg)。 // 与 message media 同理,Photos/Documents 存已解析快照(含 viewer 无关的 access_hash), -// 投影复用 tgPhoto/tgDocument。Phase 1 仅支持 inputRichMessage(blocks 形态),不解析 -// HTML/Markdown 变体。 +// 投影复用 tgPhoto/tgDocument。HTML/Markdown 输入也会在 RPC 边界归一为同一组 Blocks。 // // 已知局限:Blocks 是 gotd 线格式不透明字节,跨 gotd 版本(PageBlock 构造器变更)可能 // 失效——富文本消息为全新实验特性、无存量数据,Phase 1 接受该耦合。 @@ -183,6 +182,10 @@ type MessageRichMessage struct { Blocks []byte `json:"blocks,omitempty"` Photos []Photo `json:"photos,omitempty"` Documents []Document `json:"documents,omitempty"` + // BotAPIProjection 是由 RPC 边界从同一组已校验 PageBlock 派生出的 + // Bot API RichMessage JSON。它不是第二事实源:写入边界只允许从 Blocks + // 生成,HTTP Bot API 投影只读,避免 botapi 包反向依赖 tg 类型。 + BotAPIProjection []byte `json:"bot_api_projection,omitempty"` } // IsZero 表示无富文本载荷(落库时跳过空快照、投影时不下发 rich_message)。 @@ -289,7 +292,7 @@ type SendPrivateTextRequest struct { BusinessAutomationKind BusinessAutomationKind // ReplyMarkup 是 bot 出站消息的 reply/inline keyboard 快照;普通用户发送恒 nil。 ReplyMarkup *MessageReplyMarkup - // RichMessage 是 Layer 227 富文本消息(richMessage)快照,可选;普通消息恒 nil。 + // RichMessage 是 Layer 228 富文本消息(richMessage)快照,可选;普通消息恒 nil。 RichMessage *MessageRichMessage } diff --git a/internal/rpc/botapi_gateway.go b/internal/rpc/botapi_gateway.go index faf232d2..2df966ed 100644 --- a/internal/rpc/botapi_gateway.go +++ b/internal/rpc/botapi_gateway.go @@ -200,7 +200,7 @@ func (r *Router) BotAPISendMessage(ctx context.Context, botID, chatID int64, tex reply = &domain.MessageReply{Peer: peer, MessageID: replyToMessageID} } if peer.Type == domain.PeerTypeChannel { - return r.botAPISendChannelMessage(ctx, botID, peer.ID, text, entities, nil, replyMarkup, silent, reply) + return r.botAPISendChannelMessage(ctx, botID, peer.ID, text, entities, nil, nil, replyMarkup, silent, false, reply) } if r.deps.Messages == nil { return domain.Message{}, errors.New("BOT_INVALID") @@ -229,6 +229,66 @@ func (r *Router) BotAPISendMessage(ctx context.Context, botID, chatID int64, tex return res.SenderMessage, nil } +// BotAPISendRichMessage sends one durable rich message through the same +// private/channel state machines as messages.sendMessage. The HTTP input is +// parsed into canonical PageBlocks before any message row, pts or outbox entry +// is written. +func (r *Router) BotAPISendRichMessage(ctx context.Context, botID, chatID int64, input domain.BotAPIRichMessageInput, replyMarkup *domain.MessageReplyMarkup, silent, noForwards bool, replyToMessageID int, effectID int64) (domain.Message, error) { + if r == nil || botID == 0 { + return domain.Message{}, errors.New("BOT_INVALID") + } + peer, ok := botAPIPeerFromChatID(chatID) + if !ok { + return domain.Message{}, errors.New("CHAT_ID_INVALID") + } + if err := domain.ValidateReplyMarkup(replyMarkup); err != nil { + return domain.Message{}, replyMarkupErr(err) + } + if err := r.validateReplyMarkupForPeer(ctx, botID, peer, replyMarkup); err != nil { + return domain.Message{}, err + } + if effectID != 0 && (peer.Type != domain.PeerTypeUser || r.messageEffectInvalid(ctx, effectID)) { + return domain.Message{}, effectIDInvalidErr() + } + wire, err := tgInputRichMessageFromBotAPI(input) + if err != nil { + return domain.Message{}, err + } + richMessage, err := r.domainRichMessageFromInput(ctx, wire) + if err != nil { + return domain.Message{}, err + } + if richMessage.IsZero() { + return domain.Message{}, richMessageInvalidErr() + } + var reply *domain.MessageReply + if replyToMessageID > 0 { + reply = &domain.MessageReply{Peer: peer, MessageID: replyToMessageID} + } + if peer.Type == domain.PeerTypeChannel { + return r.botAPISendChannelMessage(ctx, botID, peer.ID, "", nil, nil, richMessage, replyMarkup, silent, noForwards, reply) + } + if r.deps.Messages == nil { + return domain.Message{}, errors.New("BOT_INVALID") + } + if r.deps.Users != nil && peer.ID != botID { + if _, found, err := r.deps.Users.ByID(ctx, botID, peer.ID); err != nil { + return domain.Message{}, err + } else if !found { + return domain.Message{}, errors.New("CHAT_ID_INVALID") + } + } + res, err := r.deps.Messages.SendPrivateText(ctx, botID, domain.SendPrivateTextRequest{ + SenderUserID: botID, RecipientUserID: peer.ID, RandomID: randomNonZeroInt64(), + RichMessage: richMessage, Silent: silent, NoForwards: noForwards, ReplyTo: reply, + Date: int(time.Now().Unix()), ReplyMarkup: replyMarkup, Effect: effectID, + }) + if err != nil { + return domain.Message{}, err + } + return res.SenderMessage, nil +} + // BotAPISendMedia sends a photo/document message through the same files service // and private/channel message state machines used by MTProto sendMedia. func (r *Router) BotAPISendMedia(ctx context.Context, botID, chatID int64, kind, locationKey, remoteURL, fileName, mimeType string, fileBytes []byte, caption string, entities []domain.MessageEntity, replyMarkup *domain.MessageReplyMarkup, silent bool, replyToMessageID int) (domain.Message, error) { @@ -257,7 +317,7 @@ func (r *Router) BotAPISendMedia(ctx context.Context, botID, chatID int64, kind, reply = &domain.MessageReply{Peer: peer, MessageID: replyToMessageID} } if peer.Type == domain.PeerTypeChannel { - return r.botAPISendChannelMessage(ctx, botID, peer.ID, caption, entities, media, replyMarkup, silent, reply) + return r.botAPISendChannelMessage(ctx, botID, peer.ID, caption, entities, media, nil, replyMarkup, silent, false, reply) } if r.deps.Messages == nil { return domain.Message{}, errors.New("BOT_INVALID") @@ -569,7 +629,7 @@ func botAPIPeerFromChatID(chatID int64) (domain.Peer, bool) { return domain.Peer{}, false } -func (r *Router) botAPISendChannelMessage(ctx context.Context, botID, channelID int64, text string, entities []domain.MessageEntity, media *domain.MessageMedia, replyMarkup *domain.MessageReplyMarkup, silent bool, reply *domain.MessageReply) (domain.Message, error) { +func (r *Router) botAPISendChannelMessage(ctx context.Context, botID, channelID int64, text string, entities []domain.MessageEntity, media *domain.MessageMedia, richMessage *domain.MessageRichMessage, replyMarkup *domain.MessageReplyMarkup, silent, noForwards bool, reply *domain.MessageReply) (domain.Message, error) { if r.deps.Channels == nil { return domain.Message{}, errors.New("CHAT_ID_INVALID") } @@ -581,10 +641,12 @@ func (r *Router) botAPISendChannelMessage(ctx context.Context, botID, channelID Message: text, Entities: append([]domain.MessageEntity(nil), entities...), Media: media, + RichMessage: richMessage, MentionUserIDs: mentionUserIDs, SkipRecipientLookup: true, PostAuthor: r.channelPostAuthorName(ctx, botID), Silent: silent, + NoForwards: noForwards, ReplyTo: reply, ReplyMarkup: replyMarkup, Date: int(time.Now().Unix()), @@ -781,6 +843,9 @@ func (r *Router) BotAPIEditMessageText(ctx context.Context, botID, chatID int64, EditDate: int(time.Now().Unix()), SetReplyMarkup: setReplyMarkup, ReplyMarkup: replyMarkup, + // An explicit plain-text edit replaces a previous rich payload. Keeping + // both would create a state that neither Bot API nor TDesktop permits. + SetRichMessage: true, }) if err != nil { return domain.Message{}, err @@ -792,6 +857,70 @@ func (r *Router) BotAPIEditMessageText(ctx context.Context, botID, chatID int64, return self.Message, nil } +// BotAPIEditRichMessage replaces message content with one rich payload while +// preserving the existing durable edit/pts/outbox semantics. +func (r *Router) BotAPIEditRichMessage(ctx context.Context, botID, chatID int64, messageID int, input domain.BotAPIRichMessageInput, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup) (domain.Message, error) { + if r == nil || botID == 0 { + return domain.Message{}, errors.New("BOT_INVALID") + } + peer, ok := botAPIPeerFromChatID(chatID) + if !ok { + return domain.Message{}, errors.New("CHAT_ID_INVALID") + } + if messageID <= 0 || messageID > domain.MaxMessageBoxID { + return domain.Message{}, errors.New("MESSAGE_ID_INVALID") + } + if err := domain.ValidateReplyMarkup(replyMarkup); err != nil { + return domain.Message{}, replyMarkupErr(err) + } + if err := r.validateReplyMarkupForPeer(ctx, botID, peer, replyMarkup); err != nil { + return domain.Message{}, err + } + wire, err := tgInputRichMessageFromBotAPI(input) + if err != nil { + return domain.Message{}, err + } + richMessage, err := r.domainRichMessageFromInput(ctx, wire) + if err != nil { + return domain.Message{}, err + } + if richMessage.IsZero() { + return domain.Message{}, richMessageInvalidErr() + } + if peer.Type == domain.PeerTypeChannel { + if r.deps.Channels == nil { + return domain.Message{}, errors.New("CHAT_ID_INVALID") + } + res, err := r.deps.Channels.EditMessage(ctx, botID, domain.EditChannelMessageRequest{ + UserID: botID, ChannelID: peer.ID, ID: messageID, Message: "", + SetReplyMarkup: setReplyMarkup, ReplyMarkup: replyMarkup, + SetRichMessage: true, RichMessage: richMessage, EditDate: int(time.Now().Unix()), + }) + if err != nil { + return domain.Message{}, channelEditErr(err) + } + r.enqueueChannelEditMessageFanout(ctx, botID, res) + return botAPIMessageFromChannel(botID, res.Message), nil + } + if r.deps.Messages == nil { + return domain.Message{}, errors.New("BOT_INVALID") + } + res, err := r.deps.Messages.EditMessage(ctx, botID, domain.EditMessageRequest{ + OwnerUserID: botID, Peer: peer, ID: messageID, Message: "", EditDate: int(time.Now().Unix()), + SetReplyMarkup: setReplyMarkup, ReplyMarkup: replyMarkup, + SetRichMessage: true, RichMessage: richMessage, + }) + if err != nil { + return domain.Message{}, err + } + r.enqueueBotAPIPrivateEditUpdatesAsync(ctx, res) + self := res.Self() + if self.Message.ID == 0 { + return domain.Message{}, errors.New("MESSAGE_ID_INVALID") + } + return self.Message, nil +} + func (r *Router) BotAPIEditInlineMessageText(ctx context.Context, botID int64, inlineMessageID domain.BotInlineMessageID, text string, entities []domain.MessageEntity, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup, disableWebPagePreview bool) (bool, error) { if r == nil || botID == 0 || !r.userIsBot(ctx, botID) { return false, errors.New("BOT_INVALID") @@ -823,6 +952,29 @@ func (r *Router) BotAPIEditInlineMessageText(ctx context.Context, botID int64, i return r.onMessagesEditInlineBotMessage(WithUserID(ctx, botID), req) } +func (r *Router) BotAPIEditInlineRichMessage(ctx context.Context, botID int64, inlineMessageID domain.BotInlineMessageID, input domain.BotAPIRichMessageInput, setReplyMarkup bool, replyMarkup *domain.MessageReplyMarkup) (bool, error) { + if r == nil || botID == 0 || !r.userIsBot(ctx, botID) { + return false, errors.New("BOT_INVALID") + } + if err := domain.ValidateReplyMarkup(replyMarkup); err != nil { + return false, replyMarkupErr(err) + } + wire, err := tgInputRichMessageFromBotAPI(input) + if err != nil { + return false, err + } + req := &tg.MessagesEditInlineBotMessageRequest{ID: tgInputBotInlineMessageID(inlineMessageID)} + req.SetRichMessage(wire) + if setReplyMarkup { + markup := tgReplyMarkup(replyMarkup) + if markup == nil { + markup = &tg.ReplyInlineMarkup{} + } + req.SetReplyMarkup(markup) + } + return r.onMessagesEditInlineBotMessage(WithUserID(ctx, botID), req) +} + // BotAPIDeleteMessage deletes a bot-owned private message with revoke=true so // the target user's MTProto clients observe the normal delete update. func (r *Router) BotAPIDeleteMessage(ctx context.Context, botID, chatID int64, messageID int) (bool, error) { diff --git a/internal/rpc/botapi_gateway_test.go b/internal/rpc/botapi_gateway_test.go index 902ec78b..1b6b8363 100644 --- a/internal/rpc/botapi_gateway_test.go +++ b/internal/rpc/botapi_gateway_test.go @@ -249,6 +249,112 @@ func TestBotAPISendMessageToSupergroupChatID(t *testing.T) { } } +func TestBotAPIRichMessagePrivateSendEditAndPlainReplacement(t *testing.T) { + fixture := newBotAPIReceiveFixture(t, false) + markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{ + Type: domain.MarkupButtonCallback, Text: "Info", Data: []byte("menu:info"), + }}}} + sent, err := fixture.router.BotAPISendRichMessage(fixture.ctx, fixture.bot.ID, fixture.owner.ID, domain.BotAPIRichMessageInput{ + HTML: `

Admin

Status: active

`, SkipEntityDetection: true, + }, markup, false, false, 0, 0) + if err != nil { + t.Fatalf("BotAPISendRichMessage: %v", err) + } + if sent.ID <= 0 || sent.Pts <= 0 || sent.Body != "" || sent.RichMessage == nil || len(sent.RichMessage.BotAPIProjection) == 0 || + sent.ReplyMarkup == nil || string(sent.ReplyMarkup.Inline[0][0].Data) != "menu:info" { + t.Fatalf("sent rich message = %+v", sent) + } + + botHistory := privateBotAPIHistory(t, fixture, fixture.bot.ID, fixture.owner.ID) + if botHistory.ID != sent.ID || botHistory.RichMessage == nil || botHistory.Body != "" { + t.Fatalf("bot rich history = %+v", botHistory) + } + ownerHistory := privateBotAPIHistory(t, fixture, fixture.owner.ID, fixture.bot.ID) + if ownerHistory.RichMessage == nil || len(ownerHistory.RichMessage.BotAPIProjection) == 0 || ownerHistory.ReplyMarkup == nil { + t.Fatalf("owner rich history = %+v", ownerHistory) + } + + edited, err := fixture.router.BotAPIEditRichMessage(fixture.ctx, fixture.bot.ID, fixture.owner.ID, sent.ID, domain.BotAPIRichMessageInput{ + Markdown: "## Updated\n\nSubscription: active", SkipEntityDetection: true, + }, true, markup) + if err != nil { + t.Fatalf("BotAPIEditRichMessage: %v", err) + } + if edited.RichMessage == nil || edited.Body != "" || edited.EditDate == 0 || edited.Pts <= sent.Pts || + !strings.Contains(string(edited.RichMessage.BotAPIProjection), "Updated") { + t.Fatalf("edited rich message = %+v projection=%s", edited, edited.RichMessage.BotAPIProjection) + } + ownerHistory = privateBotAPIHistory(t, fixture, fixture.owner.ID, fixture.bot.ID) + if ownerHistory.RichMessage == nil || !strings.Contains(string(ownerHistory.RichMessage.BotAPIProjection), "Updated") { + t.Fatalf("owner edited rich history = %+v", ownerHistory) + } + + plain, err := fixture.router.BotAPIEditMessageText(fixture.ctx, fixture.bot.ID, fixture.owner.ID, sent.ID, "Classic menu", nil, false, nil, false) + if err != nil { + t.Fatalf("BotAPIEditMessageText replacing rich: %v", err) + } + if plain.Body != "Classic menu" || plain.RichMessage != nil { + t.Fatalf("plain replacement = %+v", plain) + } + ownerHistory = privateBotAPIHistory(t, fixture, fixture.owner.ID, fixture.bot.ID) + if ownerHistory.Body != "Classic menu" || ownerHistory.RichMessage != nil { + t.Fatalf("owner plain replacement history = %+v", ownerHistory) + } +} + +func TestBotAPIRichMessageSupergroupSendAndEdit(t *testing.T) { + fixture := newBotAPIReceiveFixture(t, false) + chatID := -botAPIChannelChatIDBase - fixture.channel.ID + markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{ + Type: domain.MarkupButtonCallback, Text: "Status", Data: []byte("channel:status"), + }}}} + sent, err := fixture.router.BotAPISendRichMessage(fixture.ctx, fixture.bot.ID, chatID, domain.BotAPIRichMessageInput{ + HTML: `

Group menu

Status: active

`, SkipEntityDetection: true, + }, markup, false, false, 0, 0) + if err != nil { + t.Fatalf("BotAPISendRichMessage channel: %v", err) + } + if sent.Peer != (domain.Peer{Type: domain.PeerTypeChannel, ID: fixture.channel.ID}) || sent.ID <= 0 || sent.Pts <= 0 || + sent.RichMessage == nil || sent.ReplyMarkup == nil || string(sent.ReplyMarkup.Inline[0][0].Data) != "channel:status" { + t.Fatalf("sent channel rich message = %+v", sent) + } + history, err := fixture.channels.GetHistory(fixture.ctx, fixture.owner.ID, domain.ChannelHistoryFilter{ + ChannelID: fixture.channel.ID, Limit: 1, + }) + if err != nil || len(history.Messages) != 1 || history.Messages[0].RichMessage == nil || history.Messages[0].Body != "" { + t.Fatalf("channel rich history = %+v err=%v", history.Messages, err) + } + + edited, err := fixture.router.BotAPIEditRichMessage(fixture.ctx, fixture.bot.ID, chatID, sent.ID, domain.BotAPIRichMessageInput{ + Markdown: "## Updated group menu\n\nStatus: active", SkipEntityDetection: true, + }, true, markup) + if err != nil { + t.Fatalf("BotAPIEditRichMessage channel: %v", err) + } + if edited.RichMessage == nil || edited.Body != "" || edited.EditDate == 0 || edited.Pts <= sent.Pts || + !strings.Contains(string(edited.RichMessage.BotAPIProjection), "Updated group") { + t.Fatalf("edited channel rich message = %+v sent_pts=%d projection=%s", edited, sent.Pts, edited.RichMessage.BotAPIProjection) + } + history, err = fixture.channels.GetHistory(fixture.ctx, fixture.owner.ID, domain.ChannelHistoryFilter{ + ChannelID: fixture.channel.ID, Limit: 1, + }) + if err != nil || len(history.Messages) != 1 || history.Messages[0].RichMessage == nil || + !strings.Contains(string(history.Messages[0].RichMessage.BotAPIProjection), "Updated group") { + t.Fatalf("edited channel history = %+v err=%v", history.Messages, err) + } +} + +func privateBotAPIHistory(t *testing.T, fixture botAPIReceiveFixture, ownerID, peerID int64) domain.Message { + t.Helper() + history, err := fixture.messages.GetHistory(fixture.ctx, ownerID, domain.MessageFilter{ + HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerID}, Limit: 1, + }) + if err != nil || len(history.Messages) != 1 { + t.Fatalf("GetHistory owner=%d peer=%d len=%d err=%v", ownerID, peerID, len(history.Messages), err) + } + return history.Messages[0] +} + func TestBotAPISendMessageRejectsUnsupportedNegativeChatID(t *testing.T) { r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System) diff --git a/internal/rpc/botapi_rich_message.go b/internal/rpc/botapi_rich_message.go new file mode 100644 index 00000000..22d9bc3d --- /dev/null +++ b/internal/rpc/botapi_rich_message.go @@ -0,0 +1,305 @@ +package rpc + +import ( + "bytes" + "fmt" + "net/url" + "strconv" + "strings" + + richbuilder "github.com/iamxvbaba/td/telegram/message/rich" + "github.com/iamxvbaba/td/tg" + "golang.org/x/net/html" + + "telesrv/internal/domain" +) + +const ( + botAPIRichSentinelScheme = "telesrv-rich" + botAPIRichDateMaxUnix = int64(1<<31 - 1) +) + +type botAPIHTMLTableSpec struct { + bordered bool + striped bool + cells []botAPIHTMLTableCellSpec +} + +type botAPIHTMLTableCellSpec struct { + align string + valign string +} + +func tgInputRichMessageFromBotAPI(input domain.BotAPIRichMessageInput) (tg.InputRichMessageClass, error) { + if input.SourceCount() != 1 || len(input.BlocksJSON) != 0 { + return nil, richMessageInvalidErr() + } + if len(input.MediaJSON) != 0 { + return nil, richMessageMediaUnsupportedErr() + } + if input.HTML != "" { + return &tg.InputRichMessageHTML{ + Rtl: input.RTL, Noautolink: input.SkipEntityDetection, HTML: input.HTML, + }, nil + } + if input.Markdown != "" { + return &tg.InputRichMessageMarkdown{ + Rtl: input.RTL, Noautolink: input.SkipEntityDetection, Markdown: input.Markdown, + }, nil + } + return nil, richMessageInvalidErr() +} + +func parseBotAPIRichHTML(source string) ([]tg.PageBlockClass, error) { + doc, err := html.Parse(strings.NewReader(source)) + if err != nil { + return nil, richMessageInvalidErr() + } + tables := make([]botAPIHTMLTableSpec, 0) + var transform func(*html.Node) error + transform = func(node *html.Node) error { + if node.Type == html.ElementNode { + switch node.Data { + case "img", "video", "audio", "tg-map", "tg-collage", "tg-slideshow": + // The current local blob backend cannot materialize an arbitrary + // rich HTML media URL atomically. Fail explicitly so Bedolaga's + // documented one-shot no-logo retry is used instead of losing media. + return webpageMediaEmptyErr() + case "tg-time": + unixTime, err := strconv.ParseInt(htmlNodeAttr(node, "unix"), 10, 64) + if err != nil || unixTime <= 0 || unixTime > botAPIRichDateMaxUnix { + return richMessageDateInvalidErr() + } + format := htmlNodeAttr(node, "format") + if _, ok := botAPIRichDateFlags(format); !ok { + return richMessageDateInvalidErr() + } + node.Data = "a" + node.Attr = []html.Attribute{{Key: "href", Val: fmt.Sprintf("%s://time?unix=%d&format=%s", botAPIRichSentinelScheme, unixTime, url.QueryEscape(format))}} + case "footer": + node.Data = "p" + node.Attr = nil + anchor := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{{Key: "href", Val: botAPIRichSentinelScheme + "://footer"}}} + for child := node.FirstChild; child != nil; { + next := child.NextSibling + node.RemoveChild(child) + anchor.AppendChild(child) + child = next + } + node.AppendChild(anchor) + case "table": + tables = append(tables, botAPIHTMLTableSpecFromNode(node)) + } + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + if err := transform(child); err != nil { + return err + } + } + return nil + } + if err := transform(doc); err != nil { + return nil, err + } + var normalized bytes.Buffer + if err := html.Render(&normalized, doc); err != nil { + return nil, richMessageInvalidErr() + } + blocks, err := richbuilder.ParseHTML(strings.NewReader(normalized.String())) + if err != nil { + return nil, richMessageInvalidErr() + } + postProcessBotAPIRichHTML(blocks, tables) + return blocks, nil +} + +func parseBotAPIRichMarkdown(source string) ([]tg.PageBlockClass, error) { + blocks, err := richbuilder.ParseMarkdown(strings.NewReader(source)) + if err != nil { + return nil, richMessageInvalidErr() + } + return blocks, nil +} + +func botAPIHTMLTableSpecFromNode(table *html.Node) botAPIHTMLTableSpec { + spec := botAPIHTMLTableSpec{bordered: htmlNodeHasAttr(table, "bordered"), striped: htmlNodeHasAttr(table, "striped")} + var walk func(*html.Node) + walk = func(node *html.Node) { + for child := node.FirstChild; child != nil; child = child.NextSibling { + if child.Type == html.ElementNode && (child.Data == "td" || child.Data == "th") { + spec.cells = append(spec.cells, botAPIHTMLTableCellSpec{ + align: strings.ToLower(htmlNodeAttr(child, "align")), valign: strings.ToLower(htmlNodeAttr(child, "valign")), + }) + } + walk(child) + } + } + walk(table) + return spec +} + +func postProcessBotAPIRichHTML(blocks []tg.PageBlockClass, tables []botAPIHTMLTableSpec) { + tableIndex := 0 + var visit func([]tg.PageBlockClass) + visit = func(items []tg.PageBlockClass) { + for index, block := range items { + switch value := block.(type) { + case *tg.PageBlockParagraph: + if footer, ok := botAPIRichFooterText(value.Text); ok { + items[index] = &tg.PageBlockFooter{Text: postProcessBotAPIRichText(footer)} + } else { + value.Text = postProcessBotAPIRichText(value.Text) + } + case *tg.PageBlockHeading1: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockHeading2: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockHeading3: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockHeading4: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockHeading5: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockHeading6: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockFooter: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockPreformatted: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.PageBlockBlockquote: + value.Text = postProcessBotAPIRichText(value.Text) + value.Caption = postProcessBotAPIRichText(value.Caption) + case *tg.PageBlockBlockquoteBlocks: + value.Caption = postProcessBotAPIRichText(value.Caption) + visit(value.Blocks) + case *tg.PageBlockDetails: + value.Title = postProcessBotAPIRichText(value.Title) + visit(value.Blocks) + case *tg.PageBlockTable: + value.Title = postProcessBotAPIRichText(value.Title) + if tableIndex < len(tables) { + spec := tables[tableIndex] + tableIndex++ + value.Bordered, value.Striped = spec.bordered, spec.striped + cellIndex := 0 + for rowIndex := range value.Rows { + for columnIndex := range value.Rows[rowIndex].Cells { + cell := &value.Rows[rowIndex].Cells[columnIndex] + cell.Text = postProcessBotAPIRichText(cell.Text) + if cellIndex < len(spec.cells) { + cellSpec := spec.cells[cellIndex] + cell.AlignCenter = cellSpec.align == "center" + cell.AlignRight = cellSpec.align == "right" + cell.ValignMiddle = cellSpec.valign == "middle" + cell.ValignBottom = cellSpec.valign == "bottom" + } + cellIndex++ + } + } + } + } + } + } + visit(blocks) +} + +func postProcessBotAPIRichText(text tg.RichTextClass) tg.RichTextClass { + switch value := text.(type) { + case *tg.TextConcat: + for i := range value.Texts { + value.Texts[i] = postProcessBotAPIRichText(value.Texts[i]) + } + case *tg.TextBold: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextItalic: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextUnderline: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextStrike: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextFixed: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextSubscript: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextSuperscript: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextMarked: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextSpoiler: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextURL: + parsed, err := url.Parse(value.URL) + if err == nil && parsed.Scheme == botAPIRichSentinelScheme && parsed.Host == "time" { + unixTime, unixErr := strconv.ParseInt(parsed.Query().Get("unix"), 10, 32) + flags, ok := botAPIRichDateFlags(parsed.Query().Get("format")) + if unixErr == nil && ok { + return richbuilder.Date(postProcessBotAPIRichText(value.Text), int(unixTime), flags) + } + } + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextEmail: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextPhone: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextAnchor: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextMentionName: + value.Text = postProcessBotAPIRichText(value.Text) + case *tg.TextDate: + value.Text = postProcessBotAPIRichText(value.Text) + } + return text +} + +func botAPIRichFooterText(text tg.RichTextClass) (tg.RichTextClass, bool) { + link, ok := text.(*tg.TextURL) + if !ok || link.URL != botAPIRichSentinelScheme+"://footer" { + return nil, false + } + return link.Text, true +} + +func botAPIRichDateFlags(format string) (richbuilder.DateFlags, bool) { + if format == "r" || format == "R" { + return richbuilder.DateFlags{Relative: true}, true + } + var flags richbuilder.DateFlags + if format == "" { + return flags, false + } + for _, value := range format { + switch value { + case 't': + flags.ShortTime = true + case 'T': + flags.LongTime = true + case 'd': + flags.ShortDate = true + case 'D': + flags.LongDate = true + case 'w', 'W': + flags.DayOfWeek = true + default: + return richbuilder.DateFlags{}, false + } + } + return flags, true +} + +func htmlNodeAttr(node *html.Node, key string) string { + for _, attribute := range node.Attr { + if attribute.Key == key { + return attribute.Val + } + } + return "" +} + +func htmlNodeHasAttr(node *html.Node, key string) bool { + for _, attribute := range node.Attr { + if attribute.Key == key { + return true + } + } + return false +} diff --git a/internal/rpc/botapi_rich_projection.go b/internal/rpc/botapi_rich_projection.go new file mode 100644 index 00000000..6d9f8e95 --- /dev/null +++ b/internal/rpc/botapi_rich_projection.go @@ -0,0 +1,503 @@ +package rpc + +import ( + "encoding/json" + "errors" + "strconv" + "strings" + + "github.com/iamxvbaba/td/tg" +) + +func botAPIRichMessageProjection(blocks []tg.PageBlockClass, rtl bool) ([]byte, error) { + projected, err := botAPIRichBlocks(blocks) + if err != nil { + return nil, err + } + if len(projected) == 0 { + return nil, richMessageInvalidErr() + } + out := map[string]any{"blocks": projected} + if rtl { + out["is_rtl"] = true + } + return json.Marshal(out) +} + +func botAPIRichBlocks(blocks []tg.PageBlockClass) ([]any, error) { + out := make([]any, 0, len(blocks)) + for _, block := range blocks { + projected, err := botAPIRichBlock(block) + if err != nil { + return nil, err + } + if projected != nil { + out = append(out, projected) + } + } + return out, nil +} + +func botAPIRichBlock(block tg.PageBlockClass) (map[string]any, error) { + textBlock := func(kind string, text tg.RichTextClass) (map[string]any, error) { + value, err := botAPIRichText(text) + if err != nil { + return nil, err + } + return map[string]any{"type": kind, "text": value}, nil + } + heading := func(size int, text tg.RichTextClass) (map[string]any, error) { + value, err := botAPIRichText(text) + if err != nil { + return nil, err + } + return map[string]any{"type": "heading", "text": value, "size": size}, nil + } + switch value := block.(type) { + case *tg.PageBlockParagraph: + return textBlock("paragraph", value.Text) + case *tg.PageBlockTitle: + return heading(1, value.Text) + case *tg.PageBlockSubtitle: + return heading(2, value.Text) + case *tg.PageBlockHeader: + return heading(2, value.Text) + case *tg.PageBlockSubheader: + return heading(3, value.Text) + case *tg.PageBlockKicker: + return heading(6, value.Text) + case *tg.PageBlockHeading1: + return heading(1, value.Text) + case *tg.PageBlockHeading2: + return heading(2, value.Text) + case *tg.PageBlockHeading3: + return heading(3, value.Text) + case *tg.PageBlockHeading4: + return heading(4, value.Text) + case *tg.PageBlockHeading5: + return heading(5, value.Text) + case *tg.PageBlockHeading6: + return heading(6, value.Text) + case *tg.PageBlockPreformatted: + out, err := textBlock("pre", value.Text) + if err == nil && value.Language != "" { + out["language"] = value.Language + } + return out, err + case *tg.PageBlockFooter: + return textBlock("footer", value.Text) + case *tg.PageBlockDivider: + return map[string]any{"type": "divider"}, nil + case *tg.PageBlockMath: + return map[string]any{"type": "mathematical_expression", "expression": value.Source}, nil + case *tg.PageBlockAnchor: + return map[string]any{"type": "anchor", "name": value.Name}, nil + case *tg.PageBlockDetails: + blocks, err := botAPIRichBlocks(value.Blocks) + if err != nil { + return nil, err + } + summary, err := botAPIRichText(value.Title) + if err != nil { + return nil, err + } + out := map[string]any{"type": "details", "summary": summary, "blocks": blocks} + if value.Open { + out["is_open"] = true + } + return out, nil + case *tg.PageBlockBlockquote: + text, err := botAPIRichText(value.Text) + if err != nil { + return nil, err + } + out := map[string]any{"type": "blockquote", "blocks": []any{map[string]any{"type": "paragraph", "text": text}}} + if !botAPIRichTextEmpty(value.Caption) { + credit, err := botAPIRichText(value.Caption) + if err != nil { + return nil, err + } + out["credit"] = credit + } + return out, nil + case *tg.PageBlockBlockquoteBlocks: + blocks, err := botAPIRichBlocks(value.Blocks) + if err != nil { + return nil, err + } + out := map[string]any{"type": "blockquote", "blocks": blocks} + if !botAPIRichTextEmpty(value.Caption) { + credit, err := botAPIRichText(value.Caption) + if err != nil { + return nil, err + } + out["credit"] = credit + } + return out, nil + case *tg.PageBlockPullquote: + out, err := textBlock("pullquote", value.Text) + if err != nil { + return nil, err + } + if !botAPIRichTextEmpty(value.Caption) { + credit, err := botAPIRichText(value.Caption) + if err != nil { + return nil, err + } + out["credit"] = credit + } + return out, nil + case *tg.PageBlockList: + return botAPIUnorderedRichList(value) + case *tg.PageBlockOrderedList: + return botAPIOrderedRichList(value) + case *tg.PageBlockTable: + return botAPIRichTable(value) + case *tg.PageBlockCollage: + return botAPIRichBlockCollection("collage", value.Items, value.Caption) + case *tg.PageBlockSlideshow: + return botAPIRichBlockCollection("slideshow", value.Items, value.Caption) + case *tg.PageBlockCover: + return botAPIRichBlock(value.Cover) + case *tg.PageBlockThinking: + return textBlock("thinking", value.Text) + default: + return nil, errors.New("RICH_MESSAGE_PROJECTION_UNSUPPORTED") + } +} + +func botAPIUnorderedRichList(list *tg.PageBlockList) (map[string]any, error) { + items := make([]any, 0, len(list.Items)) + for _, raw := range list.Items { + item := map[string]any{"label": "•"} + switch value := raw.(type) { + case *tg.PageListItemText: + text, err := botAPIRichText(value.Text) + if err != nil { + return nil, err + } + item["blocks"] = []any{map[string]any{"type": "paragraph", "text": text}} + if value.Checkbox { + item["has_checkbox"] = true + if value.Checked { + item["is_checked"] = true + } + } + case *tg.PageListItemBlocks: + blocks, err := botAPIRichBlocks(value.Blocks) + if err != nil { + return nil, err + } + item["blocks"] = blocks + default: + return nil, errors.New("RICH_MESSAGE_PROJECTION_UNSUPPORTED") + } + items = append(items, item) + } + return map[string]any{"type": "list", "items": items}, nil +} + +func botAPIOrderedRichList(list *tg.PageBlockOrderedList) (map[string]any, error) { + items := make([]any, 0, len(list.Items)) + for index, raw := range list.Items { + item := map[string]any{"label": strconv.Itoa(index + 1)} + switch value := raw.(type) { + case *tg.PageListOrderedItemText: + text, err := botAPIRichText(value.Text) + if err != nil { + return nil, err + } + item["blocks"] = []any{map[string]any{"type": "paragraph", "text": text}} + botAPIFillOrderedListItem(item, value.Num, value.Value, value.Type, value.Checkbox, value.Checked) + case *tg.PageListOrderedItemBlocks: + blocks, err := botAPIRichBlocks(value.Blocks) + if err != nil { + return nil, err + } + item["blocks"] = blocks + botAPIFillOrderedListItem(item, value.Num, value.Value, value.Type, value.Checkbox, value.Checked) + default: + return nil, errors.New("RICH_MESSAGE_PROJECTION_UNSUPPORTED") + } + items = append(items, item) + } + return map[string]any{"type": "list", "items": items}, nil +} + +func botAPIFillOrderedListItem(item map[string]any, label string, value int, kind string, checkbox, checked bool) { + if label != "" { + item["label"] = label + } + if value != 0 { + item["value"] = value + } + if kind != "" { + item["type"] = kind + } + if checkbox { + item["has_checkbox"] = true + if checked { + item["is_checked"] = true + } + } +} + +func botAPIRichTable(table *tg.PageBlockTable) (map[string]any, error) { + rows := make([]any, 0, len(table.Rows)) + for _, row := range table.Rows { + cells := make([]any, 0, len(row.Cells)) + for _, cell := range row.Cells { + item := map[string]any{"align": "left", "valign": "top"} + if !botAPIRichTextEmpty(cell.Text) { + text, err := botAPIRichText(cell.Text) + if err != nil { + return nil, err + } + item["text"] = text + } + if cell.Header { + item["is_header"] = true + } + if cell.Colspan > 1 { + item["colspan"] = cell.Colspan + } + if cell.Rowspan > 1 { + item["rowspan"] = cell.Rowspan + } + if cell.AlignCenter { + item["align"] = "center" + } else if cell.AlignRight { + item["align"] = "right" + } + if cell.ValignMiddle { + item["valign"] = "middle" + } else if cell.ValignBottom { + item["valign"] = "bottom" + } + cells = append(cells, item) + } + rows = append(rows, cells) + } + out := map[string]any{"type": "table", "cells": rows} + if table.Bordered { + out["is_bordered"] = true + } + if table.Striped { + out["is_striped"] = true + } + if !botAPIRichTextEmpty(table.Title) { + caption, err := botAPIRichText(table.Title) + if err != nil { + return nil, err + } + out["caption"] = caption + } + return out, nil +} + +func botAPIRichBlockCollection(kind string, blocks []tg.PageBlockClass, caption tg.PageCaption) (map[string]any, error) { + items, err := botAPIRichBlocks(blocks) + if err != nil { + return nil, err + } + out := map[string]any{"type": kind, "blocks": items} + if !botAPIRichTextEmpty(caption.Text) || !botAPIRichTextEmpty(caption.Credit) { + projected := map[string]any{} + if !botAPIRichTextEmpty(caption.Text) { + projected["text"], err = botAPIRichText(caption.Text) + } + if err == nil && !botAPIRichTextEmpty(caption.Credit) { + projected["credit"], err = botAPIRichText(caption.Credit) + } + if err != nil { + return nil, err + } + out["caption"] = projected + } + return out, nil +} + +func botAPIRichText(text tg.RichTextClass) (any, error) { + wrapped := func(kind string, child tg.RichTextClass) (any, error) { + value, err := botAPIRichText(child) + if err != nil { + return nil, err + } + return map[string]any{"type": kind, "text": value}, nil + } + valued := func(kind, field, value string, child tg.RichTextClass) (any, error) { + out, err := wrapped(kind, child) + if err != nil { + return nil, err + } + out.(map[string]any)[field] = value + return out, nil + } + switch value := text.(type) { + case nil, *tg.TextEmpty: + return "", nil + case *tg.TextPlain: + return value.Text, nil + case *tg.TextConcat: + items := make([]any, 0, len(value.Texts)) + for _, child := range value.Texts { + item, err := botAPIRichText(child) + if err != nil { + return nil, err + } + items = append(items, item) + } + return items, nil + case *tg.TextBold: + return wrapped("bold", value.Text) + case *tg.TextItalic: + return wrapped("italic", value.Text) + case *tg.TextUnderline: + return wrapped("underline", value.Text) + case *tg.TextStrike: + return wrapped("strikethrough", value.Text) + case *tg.TextSpoiler: + return wrapped("spoiler", value.Text) + case *tg.TextFixed: + return wrapped("code", value.Text) + case *tg.TextSubscript: + return wrapped("subscript", value.Text) + case *tg.TextSuperscript: + return wrapped("superscript", value.Text) + case *tg.TextMarked: + return wrapped("marked", value.Text) + case *tg.TextDate: + out, err := wrapped("date_time", value.Text) + if err != nil { + return nil, err + } + item := out.(map[string]any) + item["unix_time"] = value.Date + item["date_time_format"] = botAPIRichDateFormat(value) + return item, nil + case *tg.TextCustomEmoji: + return map[string]any{"type": "custom_emoji", "custom_emoji_id": strconv.FormatInt(value.DocumentID, 10), "alternative_text": value.Alt}, nil + case *tg.TextMath: + return map[string]any{"type": "mathematical_expression", "expression": value.Source}, nil + case *tg.TextURL: + if strings.HasPrefix(value.URL, "#") { + return valued("anchor_link", "anchor_name", strings.TrimPrefix(value.URL, "#"), value.Text) + } + return valued("url", "url", value.URL, value.Text) + case *tg.TextEmail: + return valued("email_address", "email_address", value.Email, value.Text) + case *tg.TextPhone: + return valued("phone_number", "phone_number", value.Phone, value.Text) + case *tg.TextBankCard: + return valued("bank_card_number", "bank_card_number", botAPIRichPlainText(value.Text), value.Text) + case *tg.TextMention: + return valued("mention", "username", strings.TrimPrefix(botAPIRichPlainText(value.Text), "@"), value.Text) + case *tg.TextHashtag: + return valued("hashtag", "hashtag", strings.TrimPrefix(botAPIRichPlainText(value.Text), "#"), value.Text) + case *tg.TextCashtag: + return valued("cashtag", "cashtag", strings.TrimPrefix(botAPIRichPlainText(value.Text), "$"), value.Text) + case *tg.TextBotCommand: + return valued("bot_command", "bot_command", botAPIRichPlainText(value.Text), value.Text) + case *tg.TextAutoURL: + return valued("url", "url", botAPIRichPlainText(value.Text), value.Text) + case *tg.TextAutoEmail: + return valued("email_address", "email_address", botAPIRichPlainText(value.Text), value.Text) + case *tg.TextAutoPhone: + return valued("phone_number", "phone_number", botAPIRichPlainText(value.Text), value.Text) + case *tg.TextMentionName: + out, err := wrapped("text_mention", value.Text) + if err != nil { + return nil, err + } + out.(map[string]any)["user"] = map[string]any{"id": value.UserID, "is_bot": false, "first_name": "User " + strconv.FormatInt(value.UserID, 10)} + return out, nil + case *tg.TextAnchor: + anchor := map[string]any{"type": "anchor", "name": value.Name} + if botAPIRichTextEmpty(value.Text) { + return anchor, nil + } + inner, err := botAPIRichText(value.Text) + if err != nil { + return nil, err + } + return []any{anchor, inner}, nil + default: + return nil, errors.New("RICH_MESSAGE_PROJECTION_UNSUPPORTED") + } +} + +func botAPIRichDateFormat(date *tg.TextDate) string { + if date.Relative { + return "r" + } + var out strings.Builder + if date.ShortTime { + out.WriteByte('t') + } + if date.LongTime { + out.WriteByte('T') + } + if date.ShortDate { + out.WriteByte('d') + } + if date.LongDate { + out.WriteByte('D') + } + if date.DayOfWeek { + out.WriteByte('w') + } + return out.String() +} + +func botAPIRichTextEmpty(text tg.RichTextClass) bool { + return text == nil || botAPIRichPlainText(text) == "" +} + +func botAPIRichPlainText(text tg.RichTextClass) string { + var out strings.Builder + var walk func(tg.RichTextClass) + walk = func(value tg.RichTextClass) { + switch value := value.(type) { + case *tg.TextPlain: + out.WriteString(value.Text) + case *tg.TextConcat: + for _, child := range value.Texts { + walk(child) + } + case *tg.TextBold: + walk(value.Text) + case *tg.TextItalic: + walk(value.Text) + case *tg.TextUnderline: + walk(value.Text) + case *tg.TextStrike: + walk(value.Text) + case *tg.TextFixed: + walk(value.Text) + case *tg.TextSubscript: + walk(value.Text) + case *tg.TextSuperscript: + walk(value.Text) + case *tg.TextMarked: + walk(value.Text) + case *tg.TextSpoiler: + walk(value.Text) + case *tg.TextURL: + walk(value.Text) + case *tg.TextEmail: + walk(value.Text) + case *tg.TextPhone: + walk(value.Text) + case *tg.TextAnchor: + walk(value.Text) + case *tg.TextMentionName: + walk(value.Text) + case *tg.TextDate: + walk(value.Text) + case *tg.TextCustomEmoji: + out.WriteString(value.Alt) + } + } + walk(text) + return out.String() +} diff --git a/internal/rpc/convert_rich_message.go b/internal/rpc/convert_rich_message.go index 5397e13b..5bb39a82 100644 --- a/internal/rpc/convert_rich_message.go +++ b/internal/rpc/convert_rich_message.go @@ -10,10 +10,9 @@ import ( "telesrv/internal/domain" ) -// 本文件集中 Layer 227 富文本消息(richMessage)的 tg.* ↔ domain 转换。 -// Phase 1:仅支持 inputRichMessage(blocks 形态);HTML/Markdown 变体(需服务端解析为 -// PageBlock)尚未实现,直接拒绝。blocks 以 TL 向量序列化为不透明字节存 domain(详见 -// domain.MessageRichMessage)。 +// 本文件集中 Layer 228 富文本消息(richMessage)的 tg.* ↔ domain 转换。 +// inputRichMessage 的 blocks、HTML 与 Markdown 三种输入均在 RPC 边界归一为 PageBlock; +// blocks 以 TL 向量序列化为不透明字节存 domain(详见 domain.MessageRichMessage)。 // encodeRichBlocks 把 []tg.PageBlockClass 序列化为 TL 向量字节(含 vector 头)。 func encodeRichBlocks(blocks []tg.PageBlockClass) ([]byte, error) { @@ -127,22 +126,51 @@ func normalizeOrderedListForClients(list *tg.PageBlockOrderedList) { } // domainRichMessageFromInput 把入站 tg.InputRichMessageClass 解析为 domain 快照: -// 序列化 blocks + 按 id 解析内嵌 photos/documents(复用 sendMedia 同款媒体解析)。 -// 返回 nil 表示无富文本载荷。Phase 1 仅认 *tg.InputRichMessage。 +// HTML/Markdown 先在服务端解析为 PageBlock,再与 blocks 形态共用限额校验、 +// 序列化和 Bot API 输出投影;内嵌 photos/documents 复用 sendMedia 同款媒体解析。 +// 返回 nil 表示无富文本载荷。 func (r *Router) domainRichMessageFromInput(ctx context.Context, input tg.InputRichMessageClass) (*domain.MessageRichMessage, error) { if input == nil { return nil, nil } - in, ok := input.(*tg.InputRichMessage) - if !ok { - // Phase 1:HTML/Markdown 变体需服务端解析为 PageBlock,尚未支持。 - return nil, mediaInvalidErr() + var ( + in *tg.InputRichMessage + sourceParsed bool + ) + switch value := input.(type) { + case *tg.InputRichMessage: + in = value + case *tg.InputRichMessageHTML: + if value == nil || value.HTML == "" || len(value.Files) != 0 { + return nil, richMessageInvalidErr() + } + blocks, err := parseBotAPIRichHTML(value.HTML) + if err != nil { + return nil, err + } + in = &tg.InputRichMessage{Rtl: value.Rtl, Noautolink: value.Noautolink, Blocks: blocks} + sourceParsed = true + case *tg.InputRichMessageMarkdown: + if value == nil || value.Markdown == "" || len(value.Files) != 0 { + return nil, richMessageInvalidErr() + } + blocks, err := parseBotAPIRichMarkdown(value.Markdown) + if err != nil { + return nil, err + } + in = &tg.InputRichMessage{Rtl: value.Rtl, Noautolink: value.Noautolink, Blocks: blocks} + sourceParsed = true + default: + return nil, richMessageInvalidErr() } if len(in.Blocks) == 0 { if len(in.Photos) == 0 && len(in.Documents) == 0 { return nil, nil } - return nil, mediaInvalidErr() + return nil, richMessageInvalidErr() + } + if err := validateRichMessageBlocks(in.Blocks); err != nil { + return nil, err } if (len(in.Photos) > 0 || len(in.Documents) > 0) && r.deps.Files == nil { return nil, notImplementedErr() @@ -156,6 +184,13 @@ func (r *Router) domainRichMessageFromInput(ctx context.Context, input tg.InputR Rtl: in.Rtl, Blocks: blocks, } + projection, projectionErr := botAPIRichMessageProjection(in.Blocks, in.Rtl) + if projectionErr != nil && sourceParsed { + return nil, richMessageInvalidErr() + } + if projectionErr == nil { + rich.BotAPIProjection = projection + } for _, p := range in.Photos { id, ok := inputPhotoID(p) if !ok { diff --git a/internal/rpc/errors.go b/internal/rpc/errors.go index d429bf1f..9a969948 100644 --- a/internal/rpc/errors.go +++ b/internal/rpc/errors.go @@ -79,6 +79,18 @@ func addressInvalidErr() error { return tgerr.New(400, "ADDRESS_INVALID") } func mediaInvalidErr() error { return tgerr.New(400, "MEDIA_INVALID") } +func richMessageInvalidErr() error { return tgerr.New(400, "RICH_MESSAGE_INVALID") } + +func richMessageTooLongErr() error { return tgerr.New(400, "RICH_MESSAGE_TOO_LONG") } + +func richMessageDateInvalidErr() error { return tgerr.New(400, "RICH_MESSAGE_DATE_INVALID") } + +func richMessageMediaUnsupportedErr() error { + return tgerr.New(400, "RICH_MESSAGE_MEDIA_UNSUPPORTED") +} + +func webpageMediaEmptyErr() error { return tgerr.New(400, "WEBPAGE_MEDIA_EMPTY") } + func mediaTypeInvalidErr() error { return tgerr.New(400, "MEDIA_TYPE_INVALID") } func urlInvalidErr() error { return tgerr.New(400, "URL_INVALID") } diff --git a/internal/rpc/messages_bot_no_state.go b/internal/rpc/messages_bot_no_state.go index ea271fb8..2aea421e 100644 --- a/internal/rpc/messages_bot_no_state.go +++ b/internal/rpc/messages_bot_no_state.go @@ -114,7 +114,23 @@ func (r *Router) editPrivateInlineBotMessage(ctx context.Context, botID int64, t } message := target.Body entities := append([]domain.MessageEntity(nil), target.Entities...) - if rawMessage, ok := req.GetMessage(); ok { + richMessage := target.RichMessage + setRichMessage := false + rawRichMessage, hasRichMessage := req.GetRichMessage() + rawMessage, hasMessage := req.GetMessage() + if hasMessage && hasRichMessage { + return false, mediaInvalidErr() + } + if hasRichMessage { + richMessage, err = r.domainRichMessageFromInput(ctx, rawRichMessage) + if err != nil { + return false, err + } + if richMessage.IsZero() { + return false, richMessageInvalidErr() + } + message, entities, setRichMessage = "", nil, true + } else if hasMessage { if rawMessage == "" && newMedia == nil && target.Media.IsZero() { return false, messageEmptyErr() } @@ -127,6 +143,7 @@ func (r *Router) editPrivateInlineBotMessage(ctx context.Context, botID int64, t } message = rawMessage entities = domainMessageEntitiesForViewer(botID, rawEntities) + richMessage, setRichMessage = nil, true } else if req.ReplyMarkup == nil && newMedia == nil { return false, messageNotModifiedErr() } @@ -152,6 +169,8 @@ func (r *Router) editPrivateInlineBotMessage(ctx context.Context, botID int64, t EditDate: int(r.clock.Now().Unix()), SetReplyMarkup: setReplyMarkup, ReplyMarkup: replyMarkup, + SetRichMessage: setRichMessage, + RichMessage: richMessage, ViaBotEditBotID: botID, }) if err != nil { @@ -171,7 +190,23 @@ func (r *Router) editChannelInlineBotMessage(ctx context.Context, botID int64, t message := target.Body entities := append([]domain.MessageEntity(nil), target.Entities...) var mentionUserIDs []int64 - if rawMessage, ok := req.GetMessage(); ok { + richMessage := target.RichMessage + setRichMessage := false + rawRichMessage, hasRichMessage := req.GetRichMessage() + rawMessage, hasMessage := req.GetMessage() + if hasMessage && hasRichMessage { + return false, mediaInvalidErr() + } + if hasRichMessage { + richMessage, err = r.domainRichMessageFromInput(ctx, rawRichMessage) + if err != nil { + return false, err + } + if richMessage.IsZero() { + return false, richMessageInvalidErr() + } + message, entities, setRichMessage = "", nil, true + } else if hasMessage { if rawMessage == "" && newMedia == nil && target.Media.IsZero() { return false, messageEmptyErr() } @@ -184,6 +219,7 @@ func (r *Router) editChannelInlineBotMessage(ctx context.Context, botID int64, t } message = rawMessage entities = domainMessageEntitiesForViewer(botID, rawEntities) + richMessage, setRichMessage = nil, true var err error mentionUserIDs, err = r.mentionedUserIDsFromMessage(ctx, botID, message, rawEntities) if err != nil { @@ -221,6 +257,8 @@ func (r *Router) editChannelInlineBotMessage(ctx context.Context, botID int64, t EditDate: int(r.clock.Now().Unix()), SetReplyMarkup: setReplyMarkup, ReplyMarkup: replyMarkup, + SetRichMessage: setRichMessage, + RichMessage: richMessage, ViaBotEditBotID: botID, }) if err != nil { diff --git a/internal/rpc/messages_edit.go b/internal/rpc/messages_edit.go index 8c92807e..816daae4 100644 --- a/internal/rpc/messages_edit.go +++ b/internal/rpc/messages_edit.go @@ -41,6 +41,12 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit return nil, richErr } } + if hasMessage && hasRichMessage { + return nil, mediaInvalidErr() + } + // Explicit text and rich edits are replacement operations. A text edit must + // clear a previously stored rich payload; a rich edit already replaces it. + replaceRichMessage := hasRichMessage || hasMessage if hasMessage && richMessage == nil { // 编辑后的文本同样补服务端自动实体(url/@mention/#hashtag/bot command),与发送一致; // 覆盖频道/私聊编辑与各自的定时编辑分支(editScheduledMessage 仅由本处调用)。 @@ -61,7 +67,7 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit if media, ok := req.GetMedia(); ok && !editMessageMediaCanDegradeToText(media) { return nil, mediaInvalidErr() } - return r.editScheduledMessage(ctx, userID, peer, req.ID, message, hasMessage, entities, richMessage, hasRichMessage, scheduleDate) + return r.editScheduledMessage(ctx, userID, peer, req.ID, message, hasMessage, entities, richMessage, replaceRichMessage, scheduleDate) } if media, ok := req.GetMedia(); ok { // 关闭 poll 走 editMessage + InputMediaPoll(closed)(TDesktop "Stop poll" 路径)。 @@ -119,7 +125,7 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit Message: message, Entities: domainMessageEntitiesForViewer(userID, entities), MentionUserIDs: mentionUserIDs, - SetRichMessage: hasRichMessage, + SetRichMessage: replaceRichMessage, RichMessage: richMessage, EditDate: int(r.clock.Now().Unix()), }) @@ -154,7 +160,7 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit OriginSessionID: sessionID, SetReplyMarkup: setReplyMarkup, ReplyMarkup: replyMarkup, - SetRichMessage: hasRichMessage, + SetRichMessage: replaceRichMessage, RichMessage: richMessage, }) if err != nil { diff --git a/internal/rpc/messages_rich_message_rpc_test.go b/internal/rpc/messages_rich_message_rpc_test.go index e69adf12..f3b9dc5f 100644 --- a/internal/rpc/messages_rich_message_rpc_test.go +++ b/internal/rpc/messages_rich_message_rpc_test.go @@ -2,10 +2,13 @@ package rpc import ( "context" + "encoding/json" + "strings" "testing" "github.com/iamxvbaba/td/clock" "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tgerr" "go.uber.org/zap/zaptest" appchannels "telesrv/internal/app/channels" @@ -248,6 +251,108 @@ func TestRichMessageOrderedListNumsNormalized(t *testing.T) { assertOrderedListNums(t, "new input", got.Blocks, "1", "2") } +func TestBotAPIRichHTMLParsesBedolagaMenuStructures(t *testing.T) { + r := &Router{} + rich, err := r.domainRichMessageFromInput(context.Background(), &tg.InputRichMessageHTML{ + Rtl: true, + Noautolink: true, + HTML: `

Admin

+
Statusnow
+
More

healthy

+ `, + }) + if err != nil { + t.Fatalf("parse Bedolaga rich HTML: %v", err) + } + decoded, err := tgRichMessage(rich) + if err != nil { + t.Fatalf("decode rich HTML: %v", err) + } + if !decoded.Rtl { + t.Fatal("rich HTML lost is_rtl") + } + var heading, table, details, footer bool + for _, block := range decoded.Blocks { + switch value := block.(type) { + case *tg.PageBlockHeading4: + heading = true + case *tg.PageBlockTable: + table = true + if !value.Bordered || !value.Striped || len(value.Rows) != 1 || len(value.Rows[0].Cells) != 2 { + t.Fatalf("table shape = %+v", value) + } + cell := value.Rows[0].Cells[1] + if !cell.AlignRight || !cell.ValignBottom || !richTextContainsDate(cell.Text, 1700000000) { + t.Fatalf("table date/alignment = %+v", cell) + } + case *tg.PageBlockDetails: + details = value.Open && len(value.Blocks) != 0 + case *tg.PageBlockFooter: + footer = true + } + } + if !heading || !table || !details || !footer { + t.Fatalf("parsed blocks heading=%v table=%v details=%v footer=%v: %#v", heading, table, details, footer, decoded.Blocks) + } + var projected struct { + RTL bool `json:"is_rtl"` + Blocks []struct { + Type string `json:"type"` + } `json:"blocks"` + } + if err := json.Unmarshal(rich.BotAPIProjection, &projected); err != nil { + t.Fatalf("decode Bot API projection: %v", err) + } + if !projected.RTL || len(projected.Blocks) != len(decoded.Blocks) { + t.Fatalf("Bot API projection = %s", rich.BotAPIProjection) + } + + if _, err := r.domainRichMessageFromInput(context.Background(), &tg.InputRichMessageHTML{ + HTML: `

Admin

`, + }); err == nil || !tgerr.Is(err, "WEBPAGE_MEDIA_EMPTY") { + t.Fatalf("HTML media err = %v, want WEBPAGE_MEDIA_EMPTY for Bedolaga no-logo retry", err) + } +} + +func TestBotAPIRichMarkdownParsesAndProjects(t *testing.T) { + r := &Router{} + rich, err := r.domainRichMessageFromInput(context.Background(), &tg.InputRichMessageMarkdown{ + Markdown: "# Bedolaga\n\n**Subscription:** active", + }) + if err != nil { + t.Fatalf("parse rich Markdown: %v", err) + } + decoded, err := tgRichMessage(rich) + if err != nil { + t.Fatalf("decode rich Markdown: %v", err) + } + if len(decoded.Blocks) < 2 || len(rich.BotAPIProjection) == 0 || !strings.Contains(string(rich.BotAPIProjection), "Bedolaga") { + t.Fatalf("Markdown decoded=%#v projection=%s", decoded.Blocks, rich.BotAPIProjection) + } +} + +func richTextContainsDate(text tg.RichTextClass, want int) bool { + switch value := text.(type) { + case *tg.TextDate: + return value.Date == want + case *tg.TextConcat: + for _, child := range value.Texts { + if richTextContainsDate(child, want) { + return true + } + } + case *tg.TextBold: + return richTextContainsDate(value.Text, want) + case *tg.TextItalic: + return richTextContainsDate(value.Text, want) + case *tg.TextFixed: + return richTextContainsDate(value.Text, want) + case *tg.TextURL: + return richTextContainsDate(value.Text, want) + } + return false +} + func TestRichMessageRejectsResourcesWithoutBlocks(t *testing.T) { ctx := context.Background() r := &Router{} @@ -417,7 +522,7 @@ func TestSendMessageRichMessageTextBlocksRoundTrip(t *testing.T) { updates, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, - Message: "rich", + Message: "", RandomID: 7001, RichMessage: &tg.InputRichMessage{ Rtl: true, @@ -642,7 +747,7 @@ func TestGetRichMessageWrongPeerReturnsEmpty(t *testing.T) { updates, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, - Message: "rich", + Message: "", RandomID: 7002, RichMessage: &tg.InputRichMessage{Rtl: true, Blocks: richTextBlocks()}, }) @@ -681,7 +786,7 @@ func TestSendMessageRichMessageEmbeddedPhoto(t *testing.T) { updates, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, - Message: "rich+photo", + Message: "", RandomID: 7003, RichMessage: &tg.InputRichMessage{ Blocks: []tg.PageBlockClass{&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "see photo"}}}, diff --git a/internal/rpc/messages_send.go b/internal/rpc/messages_send.go index 5906cff8..be2e03cc 100644 --- a/internal/rpc/messages_send.go +++ b/internal/rpc/messages_send.go @@ -184,8 +184,8 @@ func (r *Router) onMessagesSendMessage(ctx context.Context, req *tg.MessagesSend return nil, sendErr } } - // rich_message(Layer 227 富文本):解析 blocks + 内嵌媒体快照;普通消息恒 nil。 - // Phase 1 仅认 inputRichMessage(blocks 形态),HTML/Markdown 变体返回错误。 + // rich_message(Layer 228 富文本):blocks、HTML、Markdown 均在边界归一为 + // PageBlock + 内嵌媒体快照;普通消息恒 nil。 var richMessage *domain.MessageRichMessage if req.RichMessage != nil { richMessage, err = r.domainRichMessageFromInput(ctx, req.RichMessage) @@ -194,6 +194,10 @@ func (r *Router) onMessagesSendMessage(ctx context.Context, req *tg.MessagesSend return nil, sendErr } } + if req.Message != "" && richMessage != nil { + sendErr = mediaInvalidErr() + return nil, sendErr + } if req.Message == "" && richMessage == nil { sendErr = messageEmptyErr() return nil, sendErr diff --git a/internal/rpc/rich_message_limits.go b/internal/rpc/rich_message_limits.go new file mode 100644 index 00000000..02201ac9 --- /dev/null +++ b/internal/rpc/rich_message_limits.go @@ -0,0 +1,215 @@ +package rpc + +import ( + "unicode/utf8" + + "github.com/iamxvbaba/td/tg" +) + +const ( + richMessageLengthLimit = 32768 + richMessageMaxBlocks = 500 + richMessageMaxDepth = 16 + richMessageMaxMedia = 50 + richMessageMaxTableCols = 20 +) + +type richMessageMetrics struct { + textLength int + blocks int + depth int + media int + tableCols int +} + +func validateRichMessageBlocks(blocks []tg.PageBlockClass) error { + metrics := richMessageMetrics{} + collectRichMessageBlockMetrics(blocks, 1, &metrics) + if metrics.textLength > richMessageLengthLimit || metrics.blocks > richMessageMaxBlocks || + metrics.depth > richMessageMaxDepth || metrics.media > richMessageMaxMedia || metrics.tableCols > richMessageMaxTableCols { + return richMessageTooLongErr() + } + if metrics.blocks == 0 || metrics.textLength == 0 && metrics.media == 0 { + return richMessageInvalidErr() + } + return nil +} + +func collectRichMessageBlockMetrics(blocks []tg.PageBlockClass, depth int, metrics *richMessageMetrics) { + if depth > metrics.depth { + metrics.depth = depth + } + for _, block := range blocks { + metrics.blocks++ + switch value := block.(type) { + case *tg.PageBlockTitle: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockSubtitle: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockHeader: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockSubheader: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockKicker: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockParagraph: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockPreformatted: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockFooter: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockHeading1: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockHeading2: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockHeading3: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockHeading4: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockHeading5: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockHeading6: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockMath: + metrics.textLength += utf16StringLength(value.Source) + case *tg.PageBlockThinking: + metrics.textLength += richTextUTF16Length(value.Text) + case *tg.PageBlockAuthorDate: + metrics.textLength += richTextUTF16Length(value.Author) + case *tg.PageBlockBlockquote: + metrics.textLength += richTextUTF16Length(value.Text) + richTextUTF16Length(value.Caption) + case *tg.PageBlockPullquote: + metrics.textLength += richTextUTF16Length(value.Text) + richTextUTF16Length(value.Caption) + case *tg.PageBlockBlockquoteBlocks: + metrics.textLength += richTextUTF16Length(value.Caption) + collectRichMessageBlockMetrics(value.Blocks, depth+1, metrics) + case *tg.PageBlockDetails: + metrics.textLength += richTextUTF16Length(value.Title) + collectRichMessageBlockMetrics(value.Blocks, depth+1, metrics) + case *tg.PageBlockList: + for _, item := range value.Items { + switch item := item.(type) { + case *tg.PageListItemText: + metrics.textLength += richTextUTF16Length(item.Text) + case *tg.PageListItemBlocks: + collectRichMessageBlockMetrics(item.Blocks, depth+1, metrics) + } + } + case *tg.PageBlockOrderedList: + for _, item := range value.Items { + switch item := item.(type) { + case *tg.PageListOrderedItemText: + metrics.textLength += richTextUTF16Length(item.Text) + case *tg.PageListOrderedItemBlocks: + collectRichMessageBlockMetrics(item.Blocks, depth+1, metrics) + } + } + case *tg.PageBlockTable: + metrics.textLength += richTextUTF16Length(value.Title) + for _, row := range value.Rows { + columns := 0 + for _, cell := range row.Cells { + metrics.textLength += richTextUTF16Length(cell.Text) + if cell.Colspan > 1 { + columns += cell.Colspan + } else { + columns++ + } + } + if columns > metrics.tableCols { + metrics.tableCols = columns + } + } + case *tg.PageBlockCollage: + metrics.textLength += richTextUTF16Length(value.Caption.Text) + richTextUTF16Length(value.Caption.Credit) + collectRichMessageBlockMetrics(value.Items, depth+1, metrics) + case *tg.PageBlockSlideshow: + metrics.textLength += richTextUTF16Length(value.Caption.Text) + richTextUTF16Length(value.Caption.Credit) + collectRichMessageBlockMetrics(value.Items, depth+1, metrics) + case *tg.PageBlockCover: + collectRichMessageBlockMetrics([]tg.PageBlockClass{value.Cover}, depth+1, metrics) + case *tg.PageBlockEmbedPost: + collectRichMessageBlockMetrics(value.Blocks, depth+1, metrics) + case *tg.PageBlockPhoto, *tg.PageBlockVideo, *tg.PageBlockAudio: + metrics.media++ + } + } +} + +func richTextUTF16Length(text tg.RichTextClass) int { + switch value := text.(type) { + case nil, *tg.TextEmpty: + return 0 + case *tg.TextPlain: + return utf16StringLength(value.Text) + case *tg.TextConcat: + total := 0 + for _, child := range value.Texts { + total += richTextUTF16Length(child) + } + return total + case *tg.TextBold: + return richTextUTF16Length(value.Text) + case *tg.TextItalic: + return richTextUTF16Length(value.Text) + case *tg.TextUnderline: + return richTextUTF16Length(value.Text) + case *tg.TextStrike: + return richTextUTF16Length(value.Text) + case *tg.TextFixed: + return richTextUTF16Length(value.Text) + case *tg.TextSubscript: + return richTextUTF16Length(value.Text) + case *tg.TextSuperscript: + return richTextUTF16Length(value.Text) + case *tg.TextMarked: + return richTextUTF16Length(value.Text) + case *tg.TextSpoiler: + return richTextUTF16Length(value.Text) + case *tg.TextURL: + return richTextUTF16Length(value.Text) + case *tg.TextMention: + return richTextUTF16Length(value.Text) + case *tg.TextHashtag: + return richTextUTF16Length(value.Text) + case *tg.TextBotCommand: + return richTextUTF16Length(value.Text) + case *tg.TextCashtag: + return richTextUTF16Length(value.Text) + case *tg.TextAutoURL: + return richTextUTF16Length(value.Text) + case *tg.TextAutoEmail: + return richTextUTF16Length(value.Text) + case *tg.TextAutoPhone: + return richTextUTF16Length(value.Text) + case *tg.TextBankCard: + return richTextUTF16Length(value.Text) + case *tg.TextEmail: + return richTextUTF16Length(value.Text) + case *tg.TextPhone: + return richTextUTF16Length(value.Text) + case *tg.TextAnchor: + return richTextUTF16Length(value.Text) + case *tg.TextMentionName: + return richTextUTF16Length(value.Text) + case *tg.TextDate: + return richTextUTF16Length(value.Text) + case *tg.TextCustomEmoji: + return utf16StringLength(value.Alt) + case *tg.TextMath: + return utf16StringLength(value.Source) + default: + return 0 + } +} + +func utf16StringLength(value string) int { + length := 0 + for _, r := range value { + length++ + if r > utf8.RuneSelf && r > 0xffff { + length++ + } + } + return length +} diff --git a/internal/store/memory/message_helpers.go b/internal/store/memory/message_helpers.go index f930af2e..ab5fd796 100644 --- a/internal/store/memory/message_helpers.go +++ b/internal/store/memory/message_helpers.go @@ -156,7 +156,7 @@ func cloneReplyMarkup(m *domain.MessageReplyMarkup) *domain.MessageReplyMarkup { return &clone } -// cloneRichMessage 深拷 Layer 227 富文本快照:复制不透明 blocks 字节与内嵌媒体切片, +// cloneRichMessage 深拷 Layer 228 富文本快照:复制不透明 blocks、Bot API 投影与内嵌媒体切片, // 避免发送方/接收方两行共享底层切片(与 postgres 每盒独立 decode 对齐)。 func cloneRichMessage(m *domain.MessageRichMessage) *domain.MessageRichMessage { if m == nil { @@ -166,6 +166,7 @@ func cloneRichMessage(m *domain.MessageRichMessage) *domain.MessageRichMessage { clone.Blocks = append([]byte(nil), m.Blocks...) clone.Photos = append([]domain.Photo(nil), m.Photos...) clone.Documents = append([]domain.Document(nil), m.Documents...) + clone.BotAPIProjection = append([]byte(nil), m.BotAPIProjection...) return &clone } From 30774f8c3910f08911ff703fec4a8b32f0e48fb4 Mon Sep 17 00:00:00 2001 From: A
Date: Tue, 21 Jul 2026 15:45:16 +0800 Subject: [PATCH 02/28] fix: sync StarGift private message references --- ..._star_gift_private_box_local_refs.down.sql | 4 + ...24_star_gift_private_box_local_refs.up.sql | 186 ++++++++++++++++++ internal/store/postgres/message_send.go | 46 ++++- ...star_gift_collectibles_integration_test.go | 4 + .../postgres/star_gift_craft_projection.go | 5 +- .../store/postgres/star_gift_entitlements.go | 5 + .../store/postgres/star_gift_lifecycle.go | 9 +- .../star_gift_lifecycle_integration_test.go | 74 +++++++ ...ft_lifecycle_migration_integration_test.go | 4 +- .../postgres/star_gift_lifecycle_test.go | 96 +++++++++ .../postgres/star_gift_private_projection.go | 151 ++++++++++++++ internal/store/postgres/star_gift_upgrade.go | 11 +- 12 files changed, 580 insertions(+), 15 deletions(-) create mode 100644 deploy/migrations/0124_star_gift_private_box_local_refs.down.sql create mode 100644 deploy/migrations/0124_star_gift_private_box_local_refs.up.sql create mode 100644 internal/store/postgres/star_gift_lifecycle_test.go create mode 100644 internal/store/postgres/star_gift_private_projection.go diff --git a/deploy/migrations/0124_star_gift_private_box_local_refs.down.sql b/deploy/migrations/0124_star_gift_private_box_local_refs.down.sql new file mode 100644 index 00000000..0029550e --- /dev/null +++ b/deploy/migrations/0124_star_gift_private_box_local_refs.down.sql @@ -0,0 +1,4 @@ +-- This migration emits durable per-user edit_message events. Reverting the +-- repaired ids or rewinding pts would reintroduce cross-account references and +-- create holes in updates.getDifference, so rollback intentionally preserves +-- both the corrected snapshots and their update facts. diff --git a/deploy/migrations/0124_star_gift_private_box_local_refs.up.sql b/deploy/migrations/0124_star_gift_private_box_local_refs.up.sql new file mode 100644 index 00000000..27e127a7 --- /dev/null +++ b/deploy/migrations/0124_star_gift_private_box_local_refs.up.sql @@ -0,0 +1,186 @@ +-- Private message box ids are account-local. Repair user-owned Star Gift +-- service actions that copied the owner's msg_id into both participants' +-- message boxes, and publish durable edit_message facts for already-visible +-- incorrect projections. + +CREATE TEMP TABLE star_gift_box_media_repairs ( + owner_user_id bigint NOT NULL, + box_id integer NOT NULL, + peer_type text NOT NULL, + peer_id bigint NOT NULL, + repaired_media jsonb NOT NULL, + PRIMARY KEY (owner_user_id, box_id) +) ON COMMIT DROP; + +-- An upgrade action points back to the original ordinary gift. user saved_id +-- is a management identity, not a conversation message link: only the current +-- gift owner's box may carry it. The other participant must omit the field. +INSERT INTO star_gift_box_media_repairs ( + owner_user_id, box_id, peer_type, peer_id, repaired_media +) +SELECT unique_box.owner_user_id, + unique_box.box_id, + unique_box.peer_type, + unique_box.peer_id, + CASE + WHEN unique_box.owner_user_id = gift.owner_peer_id THEN jsonb_set( + unique_box.media, + '{service_action,star_gift_unique,saved_id}', + to_jsonb(gift.msg_id::bigint), + true + ) + ELSE unique_box.media #- '{service_action,star_gift_unique,saved_id}' + END +FROM peer_star_gifts gift +JOIN message_boxes upgrade_owner + ON upgrade_owner.owner_user_id = gift.owner_peer_id + AND upgrade_owner.box_id = gift.upgrade_msg_id +JOIN message_boxes unique_box + ON unique_box.message_sender_id = upgrade_owner.message_sender_id + AND unique_box.private_message_id = upgrade_owner.private_message_id +WHERE gift.owner_peer_type = 'user' + AND gift.unique_gift_id IS NOT NULL + AND gift.msg_id > 0 + AND gift.upgrade_msg_id > 0 + AND NOT unique_box.deleted + AND unique_box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND unique_box.media #>> '{service_action,star_gift_unique,upgrade}' = 'true' + AND unique_box.media IS DISTINCT FROM CASE + WHEN unique_box.owner_user_id = gift.owner_peer_id THEN jsonb_set( + unique_box.media, + '{service_action,star_gift_unique,saved_id}', + to_jsonb(gift.msg_id::bigint), + true + ) + ELSE unique_box.media #- '{service_action,star_gift_unique,saved_id}' + END +ON CONFLICT (owner_user_id, box_id) DO UPDATE +SET repaired_media = EXCLUDED.repaired_media; + +-- For every other user-target unique action (transfer, resale, offer accept, +-- craft), the action message itself is the new user saved-gift identity. +-- saved_id is a channel-only field there and must be absent from every box. +INSERT INTO star_gift_box_media_repairs ( + owner_user_id, box_id, peer_type, peer_id, repaired_media +) +SELECT box.owner_user_id, + box.box_id, + box.peer_type, + box.peer_id, + box.media #- '{service_action,star_gift_unique,saved_id}' +FROM message_boxes box +WHERE NOT box.deleted + AND box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND box.media #>> '{service_action,star_gift_unique,peer,Type}' = 'user' + AND COALESCE((box.media #>> '{service_action,star_gift_unique,upgrade}')::boolean, false) = false + AND box.media #> '{service_action,star_gift_unique,saved_id}' IS NOT NULL +ON CONFLICT (owner_user_id, box_id) DO UPDATE +SET repaired_media = EXCLUDED.repaired_media; + +-- A separate prepaid-upgrade action points to the same ordinary gift. +-- Telegram defines gift_msg_id as receiver-only, so retain it only in the +-- owner's service-message box and remove it from the payer's outgoing copy. +INSERT INTO star_gift_box_media_repairs ( + owner_user_id, box_id, peer_type, peer_id, repaired_media +) +SELECT prepay_box.owner_user_id, + prepay_box.box_id, + prepay_box.peer_type, + prepay_box.peer_id, + CASE + WHEN prepay_box.owner_user_id = gift.owner_peer_id THEN jsonb_set( + prepay_box.media, + '{service_action,star_gift,gift_msg_id}', + to_jsonb(gift.msg_id::bigint), + true + ) + ELSE prepay_box.media #- '{service_action,star_gift,gift_msg_id}' + END +FROM peer_star_gifts gift +JOIN message_boxes prepay_owner + ON prepay_owner.owner_user_id = gift.owner_peer_id + AND prepay_owner.media #>> '{service_action,kind}' = 'star_gift' + AND prepay_owner.media #>> '{service_action,star_gift,prepaid_upgrade}' = 'true' + AND prepay_owner.media #>> '{service_action,star_gift,upgrade_separate}' = 'true' + AND (prepay_owner.media #>> '{service_action,star_gift,gift_msg_id}')::integer = gift.msg_id + AND (prepay_owner.media #>> '{service_action,star_gift,gift_id}')::bigint = gift.gift_id +JOIN message_boxes prepay_box + ON prepay_box.message_sender_id = prepay_owner.message_sender_id + AND prepay_box.private_message_id = prepay_owner.private_message_id +WHERE gift.owner_peer_type = 'user' + AND gift.msg_id > 0 + AND NOT prepay_box.deleted + AND prepay_box.media IS DISTINCT FROM CASE + WHEN prepay_box.owner_user_id = gift.owner_peer_id THEN jsonb_set( + prepay_box.media, + '{service_action,star_gift,gift_msg_id}', + to_jsonb(gift.msg_id::bigint), + true + ) + ELSE prepay_box.media #- '{service_action,star_gift,gift_msg_id}' + END +ON CONFLICT (owner_user_id, box_id) DO UPDATE +SET repaired_media = EXCLUDED.repaired_media; + +DO $$ +DECLARE + repair record; + next_pts integer; + event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer; +BEGIN + FOR repair IN + SELECT owner_user_id, box_id, peer_type, peer_id, repaired_media + FROM star_gift_box_media_repairs + ORDER BY owner_user_id, box_id + LOOP + INSERT INTO user_update_watermarks (user_id, contiguous_pts) + VALUES (repair.owner_user_id, 0) + ON CONFLICT (user_id) DO NOTHING; + + UPDATE user_update_watermarks + SET contiguous_pts = contiguous_pts + 1, + updated_at = now() + WHERE user_id = repair.owner_user_id + RETURNING contiguous_pts INTO next_pts; + + UPDATE message_boxes + SET media = repair.repaired_media, + pts = next_pts + WHERE owner_user_id = repair.owner_user_id + AND box_id = repair.box_id + AND NOT deleted; + + INSERT INTO user_update_events ( + user_id, pts, pts_count, date, event_type, + message_box_id, peer_type, peer_id + ) VALUES ( + repair.owner_user_id, next_pts, 1, event_date, 'edit_message', + repair.box_id, repair.peer_type, repair.peer_id + ); + + INSERT INTO dispatch_outbox ( + target_user_id, pts, event_type, + exclude_auth_key_id, exclude_session_id + ) VALUES (repair.owner_user_id, next_pts, 'edit_message', 0, 0); + END LOOP; +END +$$; + +-- private_messages is the logical shared envelope and cannot contain either +-- participant's local message id. User-visible history/difference always reads +-- the per-owner message_boxes snapshots repaired above. +UPDATE private_messages +SET media = media + #- '{service_action,star_gift,saved_id}' + #- '{service_action,star_gift,gift_msg_id}' + #- '{service_action,star_gift,upgrade_msg_id}' +WHERE media #>> '{service_action,kind}' = 'star_gift' + AND ( + media #> '{service_action,star_gift,peer_user_id}' IS NOT NULL + OR media #>> '{service_action,star_gift,to,Type}' = 'user' + ); + +UPDATE private_messages +SET media = media #- '{service_action,star_gift_unique,saved_id}' +WHERE media #>> '{service_action,kind}' = 'star_gift_unique' + AND media #>> '{service_action,star_gift_unique,peer,Type}' = 'user'; diff --git a/internal/store/postgres/message_send.go b/internal/store/postgres/message_send.go index 3ad81aa5..70d83abd 100644 --- a/internal/store/postgres/message_send.go +++ b/internal/store/postgres/message_send.go @@ -117,8 +117,19 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva } type privateSendTxHooks struct { - before func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) error - after func(context.Context, pgx.Tx, domain.SendPrivateTextResult) error + before func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) error + projectMedia func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) (privateSendMediaProjection, error) + after func(context.Context, pgx.Tx, domain.SendPrivateTextResult) error +} + +// privateSendMediaProjection separates the logical private-message payload +// from the two account-local message-box projections. Most messages use the +// same media for all three fields. Service actions that carry message ids must +// project those ids per account because box ids are not shared by both users. +type privateSendMediaProjection struct { + Shared *domain.MessageMedia + Sender *domain.MessageMedia + Recipient *domain.MessageMedia } func (s *MessageStore) sendPrivateTextWithHooks(ctx context.Context, req domain.SendPrivateTextRequest, hooks privateSendTxHooks) (res domain.SendPrivateTextResult, err error) { @@ -228,7 +239,22 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP return domain.SendPrivateTextResult{}, err } } - mediaJSON, err := encodeMessageMedia(req.Media) + media := privateSendMediaProjection{Shared: req.Media, Sender: req.Media, Recipient: req.Media} + if hooks.projectMedia != nil { + media, err = hooks.projectMedia(ctx, tx, &req) + if err != nil { + return domain.SendPrivateTextResult{}, err + } + } + sharedMediaJSON, err := encodeMessageMedia(media.Shared) + if err != nil { + return domain.SendPrivateTextResult{}, err + } + senderMediaJSON, err := encodeMessageMedia(media.Sender) + if err != nil { + return domain.SendPrivateTextResult{}, err + } + recipientMediaJSON, err := encodeMessageMedia(media.Recipient) if err != nil { return domain.SendPrivateTextResult{}, err } @@ -255,7 +281,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP TtlPeriod: int32(ttlPeriod), ExpiresAt: int32(expiresAt), EntitiesJson: entities, - MediaJson: mediaJSON, + MediaJson: sharedMediaJSON, ReplyMarkupJson: replyMarkupJSON, RichMessageJson: richMessageJSON, ViaBotID: req.ViaBotID, @@ -315,7 +341,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP ExpiresAt: int32(expiresAt), EntitiesJson: entities, Pts: int32(senderPts), - MediaJson: mediaJSON, + MediaJson: senderMediaJSON, ReplyMarkupJson: replyMarkupJSON, RichMessageJson: richMessageJSON, ViaBotID: req.ViaBotID, @@ -323,7 +349,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP Effect: req.Effect, // voice/round 在发送者自己的副本上也保持"未听",直到对端 // readMessageContents 触发 sender 侧清除;发给自己无人可听,恒已读。 - MediaUnread: req.Media.HasUnreadPayload() && !selfMessage, + MediaUnread: media.Sender.HasUnreadPayload() && !selfMessage, ReactionUnread: false, } applyCreateMessageBoxMetadata(&senderArg, senderMeta) @@ -334,7 +360,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP sender := messageFromBoxRow(senderRow) sender.RandomID = req.RandomID // 共享媒体索引(0118):发送者侧 box 按媒体类别建索引(peer=收件人)。 - if err := insertMessageBoxMediaIndexTx(ctx, tx, req.SenderUserID, req.RecipientUserID, int(senderBoxID), req.Date, req.Media, req.Entities); err != nil { + if err := insertMessageBoxMediaIndexTx(ctx, tx, req.SenderUserID, req.RecipientUserID, int(senderBoxID), req.Date, media.Sender, req.Entities); err != nil { return domain.SendPrivateTextResult{}, err } if err := qtx.UpsertOutboxDialog(ctx, sqlcgen.UpsertOutboxDialogParams{ @@ -388,13 +414,13 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP ExpiresAt: int32(expiresAt), EntitiesJson: entities, Pts: int32(recipientPts), - MediaJson: mediaJSON, + MediaJson: recipientMediaJSON, ReplyMarkupJson: replyMarkupJSON, RichMessageJson: richMessageJSON, ViaBotID: req.ViaBotID, GroupedID: req.GroupedID, Effect: req.Effect, - MediaUnread: req.Media.HasUnreadPayload(), + MediaUnread: media.Recipient.HasUnreadPayload(), ReactionUnread: false, } applyCreateMessageBoxMetadata(&recipientArg, recipientMeta) @@ -405,7 +431,7 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP recipient = messageFromBoxRow(recipientRow) recipient.RandomID = req.RandomID // 共享媒体索引(0118):收件人侧 box 按媒体类别建索引(peer=发送者)。 - if err := insertMessageBoxMediaIndexTx(ctx, tx, req.RecipientUserID, req.SenderUserID, int(recipientBoxID), req.Date, req.Media, req.Entities); err != nil { + if err := insertMessageBoxMediaIndexTx(ctx, tx, req.RecipientUserID, req.SenderUserID, int(recipientBoxID), req.Date, media.Recipient, req.Entities); err != nil { return domain.SendPrivateTextResult{}, err } if err := qtx.UpsertInboxDialog(ctx, sqlcgen.UpsertInboxDialogParams{ diff --git a/internal/store/postgres/star_gift_collectibles_integration_test.go b/internal/store/postgres/star_gift_collectibles_integration_test.go index 1a9d6288..a9ee78a3 100644 --- a/internal/store/postgres/star_gift_collectibles_integration_test.go +++ b/internal/store/postgres/star_gift_collectibles_integration_test.go @@ -119,6 +119,10 @@ func TestStarGiftCollectibleUpgradeAggregatePostgres(t *testing.T) { if uniqueAction.SavedID != int64(saved.MsgID) { t.Fatalf("unique action saved_id = %d, want stable source msg id %d", uniqueAction.SavedID, saved.MsgID) } + senderUniqueAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique + if senderUniqueAction == nil || senderUniqueAction.SavedID != 0 { + t.Fatalf("sender unique action leaked owner-only saved_id: %+v", senderUniqueAction) + } ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID) if ownerSourceEdit.Event.Pts <= ownerMessage.Pts || ownerSourceEdit.Message.Media == nil || ownerSourceEdit.Message.Media.ServiceAction == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil || diff --git a/internal/store/postgres/star_gift_craft_projection.go b/internal/store/postgres/star_gift_craft_projection.go index eed8a216..3ea1d1c9 100644 --- a/internal/store/postgres/star_gift_craft_projection.go +++ b/internal/store/postgres/star_gift_craft_projection.go @@ -149,7 +149,10 @@ WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, box.OwnerUserID, box.BoxI return nil, 0, fmt.Errorf("enqueue craft input edit: %w", err) } if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 { - privateMediaJSON = mediaJSON + privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media) + if err != nil { + return nil, 0, err + } } edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event}) } diff --git a/internal/store/postgres/star_gift_entitlements.go b/internal/store/postgres/star_gift_entitlements.go index b5f71d1d..dd261b1a 100644 --- a/internal/store/postgres/star_gift_entitlements.go +++ b/internal/store/postgres/star_gift_entitlements.go @@ -134,6 +134,11 @@ VALUES($1,$2,$3,$4,$5,$6,$7)`, req.PayerUserID, req.CommandKey, locked.ID, req.F locked.PrepaidUpgradeStars, locked.PrepaidUpgradeHash = req.ChargeStars, "" result.Saved, result.Balance = locked, balance return nil + }, projectMedia: func(ctx context.Context, tx pgx.Tx, messageReq *domain.SendPrivateTextRequest) (privateSendMediaProjection, error) { + if result.Saved.Owner.Type != domain.PeerTypeUser { + return privateSendMediaProjection{Shared: messageReq.Media, Sender: messageReq.Media, Recipient: messageReq.Media}, nil + } + return projectPrivateStarGiftSourceRef(ctx, tx, messageReq, result.Saved.Owner.ID, result.Saved.MsgID) }, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { if req.Owner.Type != domain.PeerTypeChannel { return nil diff --git a/internal/store/postgres/star_gift_lifecycle.go b/internal/store/postgres/star_gift_lifecycle.go index d54c4c4d..80b64069 100644 --- a/internal/store/postgres/star_gift_lifecycle.go +++ b/internal/store/postgres/star_gift_lifecycle.go @@ -1148,8 +1148,15 @@ func ensureNoStarGiftMarketConflict(ctx context.Context, tx pgx.Tx, uniqueID int } func transferUniqueAction(unique domain.UniqueStarGift, fromUserID int64, to domain.Peer, saved domain.SavedStarGift) *domain.MessageStarGiftUniqueAction { + savedID := saved.SavedID + if to.Type == domain.PeerTypeUser { + // For a user-owned transferred gift the action message itself becomes + // inputSavedStarGiftUser.msg_id. A channel saved_id belongs to a different + // identity namespace and must never leak into the recipient's user view. + savedID = 0 + } return &domain.MessageStarGiftUniqueAction{Gift: unique, FromUserID: fromUserID, Peer: to, - SavedID: saved.SavedID, Transferred: true, Saved: true, CanExportAt: saved.CanExportAt, + SavedID: savedID, Transferred: true, Saved: true, CanExportAt: saved.CanExportAt, TransferStars: saved.TransferStars, CanTransferAt: saved.CanTransferAt, CanResellAt: saved.CanResellAt, DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: saved.CanCraftAt} } diff --git a/internal/store/postgres/star_gift_lifecycle_integration_test.go b/internal/store/postgres/star_gift_lifecycle_integration_test.go index 8bff57f0..fdec70bd 100644 --- a/internal/store/postgres/star_gift_lifecycle_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_integration_test.go @@ -99,6 +99,39 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) { if err != nil || prepaid.Saved.PrepaidUpgradeStars != 100 || prepaid.Saved.PrepaidUpgradeHash != "" || prepaid.Balance.Balance != 9850 { t.Fatalf("prepay upgrade = %+v err %v", prepaid, err) } + prepaySenderAction := prepaid.Send.SenderMessage.Media.ServiceAction.StarGift + prepayOwnerAction := prepaid.Send.RecipientMessage.Media.ServiceAction.StarGift + if prepaySenderAction == nil || prepayOwnerAction == nil || + prepaySenderAction.GiftMsgID != 0 || + prepayOwnerAction.GiftMsgID != purchased.Send.RecipientMessage.ID { + t.Fatalf("prepay gift_msg_id is not owner-only: sender=%+v owner=%+v purchase=%+v", + prepaySenderAction, prepayOwnerAction, purchased.Send) + } + prepaySenderDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, buyer.ID, prepaid.Send.SenderMessage.Pts-1, 1) + if err != nil || len(prepaySenderDifference) != 1 || prepaySenderDifference[0].Message.Media == nil || + prepaySenderDifference[0].Message.Media.ServiceAction == nil || + prepaySenderDifference[0].Message.Media.ServiceAction.StarGift == nil || + prepaySenderDifference[0].Message.Media.ServiceAction.StarGift.GiftMsgID != 0 { + t.Fatalf("payer prepay difference leaked owner-only gift_msg_id: events=%+v err=%v", prepaySenderDifference, err) + } + prepayOwnerDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, owner.ID, prepaid.Send.RecipientMessage.Pts-1, 1) + if err != nil || len(prepayOwnerDifference) != 1 || prepayOwnerDifference[0].Message.Media == nil || + prepayOwnerDifference[0].Message.Media.ServiceAction == nil || + prepayOwnerDifference[0].Message.Media.ServiceAction.StarGift == nil || + prepayOwnerDifference[0].Message.Media.ServiceAction.StarGift.GiftMsgID != purchased.Send.RecipientMessage.ID { + t.Fatalf("owner prepay difference lost box-local gift_msg_id: events=%+v err=%v", prepayOwnerDifference, err) + } + var sharedPrepayMediaJSON string + if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p +JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id +WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, prepaid.Send.RecipientMessage.ID).Scan(&sharedPrepayMediaJSON); err != nil { + t.Fatalf("load shared prepay media: %v", err) + } + sharedPrepayMedia, err := decodeMessageMedia(sharedPrepayMediaJSON) + if err != nil || sharedPrepayMedia == nil || sharedPrepayMedia.ServiceAction == nil || + sharedPrepayMedia.ServiceAction.StarGift == nil || sharedPrepayMedia.ServiceAction.StarGift.GiftMsgID != 0 { + t.Fatalf("shared prepay media retained account-local gift_msg_id: media=%+v err=%v", sharedPrepayMedia, err) + } upgraded, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID}, RequirePrepaid: true, KeepOriginalDetails: true, CommandKey: "upgrade-" + suffix, Date: now + 2, @@ -111,14 +144,40 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) { t.Fatalf("issued lifecycle snapshot = saved %+v unique %+v", upgraded.Saved, upgraded.Unique) } upgradeAction := upgraded.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique + senderUpgradeAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID) if upgradeAction == nil || upgradeAction.SavedID != int64(purchased.Saved.MsgID) || + senderUpgradeAction == nil || senderUpgradeAction.SavedID != 0 || ownerSourceEdit.Message.Media == nil || ownerSourceEdit.Message.Media.ServiceAction == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != upgraded.Saved.UpgradeMsgID || ownerSourceEdit.Message.Media.ServiceAction.StarGift.CanUpgrade { t.Fatalf("upgrade message linkage = action %+v source edit %+v", upgradeAction, ownerSourceEdit) } + var sharedUpgradeSourceMediaJSON string + if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p +JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id +WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, purchased.Saved.MsgID).Scan(&sharedUpgradeSourceMediaJSON); err != nil { + t.Fatalf("load shared upgraded source media: %v", err) + } + sharedUpgradeSourceMedia, err := decodeMessageMedia(sharedUpgradeSourceMediaJSON) + if err != nil || sharedUpgradeSourceMedia == nil || sharedUpgradeSourceMedia.ServiceAction == nil || + sharedUpgradeSourceMedia.ServiceAction.StarGift == nil || + sharedUpgradeSourceMedia.ServiceAction.StarGift.UpgradeMsgID != 0 || + sharedUpgradeSourceMedia.ServiceAction.StarGift.GiftMsgID != 0 { + t.Fatalf("shared upgraded source media retained account-local message id: media=%+v err=%v", sharedUpgradeSourceMedia, err) + } + var sharedUpgradeMediaJSON string + if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p +JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id +WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, upgraded.Send.RecipientMessage.ID).Scan(&sharedUpgradeMediaJSON); err != nil { + t.Fatalf("load shared upgrade media: %v", err) + } + sharedUpgradeMedia, err := decodeMessageMedia(sharedUpgradeMediaJSON) + if err != nil || sharedUpgradeMedia == nil || sharedUpgradeMedia.ServiceAction == nil || + sharedUpgradeMedia.ServiceAction.StarGiftUnique == nil || sharedUpgradeMedia.ServiceAction.StarGiftUnique.SavedID != 0 { + t.Fatalf("shared upgrade media retained account-local saved_id: media=%+v err=%v", sharedUpgradeMedia, err) + } dropped, err := lifecycle.DropStarGiftOriginalDetails(ctx, domain.StarGiftDropOriginalDetailsRequest{ UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID}, ChargeStars: 25, FormID: 11003, CommandKey: "drop-" + suffix, Date: now + 3, @@ -359,6 +418,21 @@ WHERE target_user_id=$1 AND pts=$2 AND event_type='user_emoji_status'`, resaleBu burnedInputAction.Saved || burnedInputAction.CanCraftAt != 0 { t.Fatalf("burned input message projection = %+v", burnedInputAction) } + for _, edit := range []domain.EditedMessageForUser{craftedInputEdit, burnedInputEdit} { + var sharedCraftInputMediaJSON string + if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p +JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id +WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, edit.Message.ID).Scan(&sharedCraftInputMediaJSON); err != nil { + t.Fatalf("load shared craft input media for box %d: %v", edit.Message.ID, err) + } + sharedCraftInputMedia, err := decodeMessageMedia(sharedCraftInputMediaJSON) + if err != nil || sharedCraftInputMedia == nil || sharedCraftInputMedia.ServiceAction == nil || + sharedCraftInputMedia.ServiceAction.StarGiftUnique == nil || + sharedCraftInputMedia.ServiceAction.StarGiftUnique.SavedID != 0 { + t.Fatalf("shared craft input retained account-local saved_id for box %d: media=%+v err=%v", + edit.Message.ID, sharedCraftInputMedia, err) + } + } craftReq := domain.StarGiftCraftRequest{UserID: owner.ID, Refs: []domain.SavedStarGiftRef{ {Owner: ownerPeer, MsgID: transferred.Saved.MsgID}, diff --git a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go index 93f42293..8fe4e5c6 100644 --- a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go @@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) { if err != nil { t.Fatalf("migrate star gift lifecycle schema: %v", err) } - if status.Dirty || status.Empty || status.Version != 121 { - t.Fatalf("migration status = %+v, want clean version 121", status) + if status.Dirty || status.Empty || status.Version != 124 { + t.Fatalf("migration status = %+v, want clean version 124", status) } } diff --git a/internal/store/postgres/star_gift_lifecycle_test.go b/internal/store/postgres/star_gift_lifecycle_test.go new file mode 100644 index 00000000..04190537 --- /dev/null +++ b/internal/store/postgres/star_gift_lifecycle_test.go @@ -0,0 +1,96 @@ +package postgres + +import ( + "testing" + + "telesrv/internal/domain" +) + +func TestTransferUniqueActionSavedIDNamespace(t *testing.T) { + saved := domain.SavedStarGift{SavedID: 42} + unique := domain.UniqueStarGift{ID: 7} + user := domain.Peer{Type: domain.PeerTypeUser, ID: 100} + channel := domain.Peer{Type: domain.PeerTypeChannel, ID: 200} + + if action := transferUniqueAction(unique, 1, user, saved); action.SavedID != 0 { + t.Fatalf("user transfer action leaked channel saved_id: %+v", action) + } + if action := transferUniqueAction(unique, 1, channel, saved); action.SavedID != saved.SavedID { + t.Fatalf("channel transfer action lost channel saved_id: %+v", action) + } +} + +func TestEncodeSharedPrivateStarGiftMediaOmitsUserBoxLocalRefs(t *testing.T) { + ordinary := &domain.MessageMedia{ + Kind: domain.MessageMediaKindService, + ServiceAction: &domain.MessageServiceAction{ + Kind: domain.MessageServiceActionStarGift, + StarGift: &domain.MessageStarGiftAction{ + PeerUserID: 9, + SavedID: 10, GiftMsgID: 11, UpgradeMsgID: 12, + }, + }, + } + encoded, err := encodeSharedPrivateStarGiftMedia(ordinary) + if err != nil { + t.Fatalf("encode ordinary shared projection: %v", err) + } + sharedOrdinary, err := decodeMessageMedia(string(encoded)) + if err != nil { + t.Fatalf("decode ordinary shared projection: %v", err) + } + ordinaryAction := sharedOrdinary.ServiceAction.StarGift + if ordinaryAction.SavedID != 0 || ordinaryAction.GiftMsgID != 0 || ordinaryAction.UpgradeMsgID != 0 { + t.Fatalf("ordinary shared projection retained box-local refs: %+v", ordinaryAction) + } + if original := ordinary.ServiceAction.StarGift; original.SavedID != 10 || original.GiftMsgID != 11 || original.UpgradeMsgID != 12 { + t.Fatalf("ordinary source projection was mutated: %+v", original) + } + + unique := &domain.MessageMedia{ + Kind: domain.MessageMediaKindService, + ServiceAction: &domain.MessageServiceAction{ + Kind: domain.MessageServiceActionStarGiftUnique, + StarGiftUnique: &domain.MessageStarGiftUniqueAction{ + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 9}, SavedID: 13, + }, + }, + } + encoded, err = encodeSharedPrivateStarGiftMedia(unique) + if err != nil { + t.Fatalf("encode unique shared projection: %v", err) + } + sharedUnique, err := decodeMessageMedia(string(encoded)) + if err != nil { + t.Fatalf("decode unique shared projection: %v", err) + } + if action := sharedUnique.ServiceAction.StarGiftUnique; action.SavedID != 0 { + t.Fatalf("unique shared projection retained user saved_id: %+v", action) + } + if unique.ServiceAction.StarGiftUnique.SavedID != 13 { + t.Fatalf("unique source projection was mutated: %+v", unique.ServiceAction.StarGiftUnique) + } +} + +func TestEncodeSharedPrivateStarGiftMediaPreservesChannelSavedID(t *testing.T) { + media := &domain.MessageMedia{ + Kind: domain.MessageMediaKindService, + ServiceAction: &domain.MessageServiceAction{ + Kind: domain.MessageServiceActionStarGiftUnique, + StarGiftUnique: &domain.MessageStarGiftUniqueAction{ + Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 9}, SavedID: 14, + }, + }, + } + encoded, err := encodeSharedPrivateStarGiftMedia(media) + if err != nil { + t.Fatalf("encode channel shared projection: %v", err) + } + shared, err := decodeMessageMedia(string(encoded)) + if err != nil { + t.Fatalf("decode channel shared projection: %v", err) + } + if action := shared.ServiceAction.StarGiftUnique; action.SavedID != 14 { + t.Fatalf("channel shared projection lost saved_id: %+v", action) + } +} diff --git a/internal/store/postgres/star_gift_private_projection.go b/internal/store/postgres/star_gift_private_projection.go new file mode 100644 index 00000000..2fcfc665 --- /dev/null +++ b/internal/store/postgres/star_gift_private_projection.go @@ -0,0 +1,151 @@ +package postgres + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5" + + "telesrv/internal/domain" +) + +// projectPrivateStarGiftSourceRef exposes a user-owned gift's stable source +// message identity only in the gift owner's message-box projection. Telegram +// defines gift_msg_id as receiver-only, and TDesktop also treats user unique +// saved_id as an inputSavedStarGiftUser identity. A non-owner counterpart box +// id is therefore not a valid substitute: it could resolve to an unrelated +// gift owned by that viewer. The shared private_messages row omits the local +// reference for the same reason. +func projectPrivateStarGiftSourceRef( + _ context.Context, + _ pgx.Tx, + req *domain.SendPrivateTextRequest, + sourceOwnerUserID int64, + sourceOwnerBoxID int, +) (privateSendMediaProjection, error) { + if req == nil || req.Media == nil || sourceOwnerUserID <= 0 || sourceOwnerBoxID <= 0 || + (sourceOwnerUserID != req.SenderUserID && sourceOwnerUserID != req.RecipientUserID) { + return privateSendMediaProjection{}, fmt.Errorf("project private star gift source: invalid scope") + } + + shared, err := cloneMessageMedia(req.Media) + if err != nil { + return privateSendMediaProjection{}, err + } + sender, err := cloneMessageMedia(req.Media) + if err != nil { + return privateSendMediaProjection{}, err + } + recipient, err := cloneMessageMedia(req.Media) + if err != nil { + return privateSendMediaProjection{}, err + } + + switch { + case privateStarGiftAction(shared) != nil: + sharedAction := privateStarGiftAction(shared) + senderAction := privateStarGiftAction(sender) + recipientAction := privateStarGiftAction(recipient) + if sharedAction.GiftMsgID != sourceOwnerBoxID { + return privateSendMediaProjection{}, fmt.Errorf( + "project private star gift source: gift_msg_id %d does not match owner box %d", + sharedAction.GiftMsgID, sourceOwnerBoxID, + ) + } + sharedAction.GiftMsgID = 0 + senderAction.GiftMsgID = 0 + recipientAction.GiftMsgID = 0 + if req.SenderUserID == sourceOwnerUserID { + senderAction.GiftMsgID = sourceOwnerBoxID + } else { + recipientAction.GiftMsgID = sourceOwnerBoxID + } + case privateStarGiftUniqueAction(shared) != nil: + sharedAction := privateStarGiftUniqueAction(shared) + senderAction := privateStarGiftUniqueAction(sender) + recipientAction := privateStarGiftUniqueAction(recipient) + if sharedAction.Peer.Type != domain.PeerTypeUser || sharedAction.Peer.ID != sourceOwnerUserID || + sharedAction.SavedID != int64(sourceOwnerBoxID) { + return privateSendMediaProjection{}, fmt.Errorf( + "project private unique star gift source: saved_id %d does not match owner box %d", + sharedAction.SavedID, sourceOwnerBoxID, + ) + } + sharedAction.SavedID = 0 + senderAction.SavedID = 0 + recipientAction.SavedID = 0 + if req.SenderUserID == sourceOwnerUserID { + senderAction.SavedID = int64(sourceOwnerBoxID) + } else { + recipientAction.SavedID = int64(sourceOwnerBoxID) + } + default: + return privateSendMediaProjection{}, fmt.Errorf("project private star gift source: unsupported media") + } + + return privateSendMediaProjection{Shared: shared, Sender: sender, Recipient: recipient}, nil +} + +func cloneMessageMedia(media *domain.MessageMedia) (*domain.MessageMedia, error) { + encoded, err := encodeMessageMedia(media) + if err != nil { + return nil, fmt.Errorf("clone private message media: %w", err) + } + cloned, err := decodeMessageMedia(string(encoded)) + if err != nil { + return nil, fmt.Errorf("clone private message media: %w", err) + } + return cloned, nil +} + +// encodeSharedPrivateStarGiftMedia returns the logical private-message +// envelope for an already viewpoint-projected Star Gift service message. +// Conversation message ids belong to a single owner's message_boxes +// namespace, so the shared row must never retain them. saved_id is likewise +// box-local for user gifts, while channel saved ids remain globally meaningful +// inside the channel gift namespace. +func encodeSharedPrivateStarGiftMedia(media *domain.MessageMedia) ([]byte, error) { + shared, err := cloneMessageMedia(media) + if err != nil { + return nil, err + } + + switch { + case privateStarGiftAction(shared) != nil: + action := privateStarGiftAction(shared) + action.GiftMsgID = 0 + action.UpgradeMsgID = 0 + if action.PeerUserID > 0 || action.To.Type == domain.PeerTypeUser { + action.SavedID = 0 + } + case privateStarGiftUniqueAction(shared) != nil: + action := privateStarGiftUniqueAction(shared) + if action.Peer.Type == domain.PeerTypeUser { + action.SavedID = 0 + } + default: + return nil, fmt.Errorf("encode shared private star gift media: unsupported media") + } + + encoded, err := encodeMessageMedia(shared) + if err != nil { + return nil, fmt.Errorf("encode shared private star gift media: %w", err) + } + return encoded, nil +} + +func privateStarGiftAction(media *domain.MessageMedia) *domain.MessageStarGiftAction { + if media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil || + media.ServiceAction.Kind != domain.MessageServiceActionStarGift { + return nil + } + return media.ServiceAction.StarGift +} + +func privateStarGiftUniqueAction(media *domain.MessageMedia) *domain.MessageStarGiftUniqueAction { + if media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil || + media.ServiceAction.Kind != domain.MessageServiceActionStarGiftUnique { + return nil + } + return media.ServiceAction.StarGiftUnique +} diff --git a/internal/store/postgres/star_gift_upgrade.go b/internal/store/postgres/star_gift_upgrade.go index d7cfe383..56c89d93 100644 --- a/internal/store/postgres/star_gift_upgrade.go +++ b/internal/store/postgres/star_gift_upgrade.go @@ -230,6 +230,12 @@ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, req.UserID, commandKey, locked.ID, req.For } return nil }, + projectMedia: func(ctx context.Context, tx pgx.Tx, messageReq *domain.SendPrivateTextRequest) (privateSendMediaProjection, error) { + if result.Saved.Owner.Type != domain.PeerTypeUser { + return privateSendMediaProjection{Shared: messageReq.Media, Sender: messageReq.Media, Recipient: messageReq.Media}, nil + } + return projectPrivateStarGiftSourceRef(ctx, tx, messageReq, result.Saved.Owner.ID, result.Saved.MsgID) + }, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { ownerMessageID := sent.RecipientMessage.ID if saved.FromUserID == req.UserID { @@ -434,7 +440,10 @@ WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, box.OwnerUserID, box.BoxI return nil, fmt.Errorf("enqueue star gift source edit: %w", err) } if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 { - privateMediaJSON = mediaJSON + privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media) + if err != nil { + return nil, err + } } edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event}) } From ebead9e98ccd82301b3acfe1c9bc8e92f5f60404 Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 15:46:24 +0800 Subject: [PATCH 03/28] feat: sync Telegram Login OIDC provider --- .env.example | 18 + cmd/bots/bedolagaformat/README.md | 60 +- cmd/bots/bedolagaformat/demo.py | 123 +- cmd/bots/bedolagaformat/login_demo.py | 462 ++++++ cmd/bots/bedolagaformat/requirements.txt | 1 + cmd/bots/bedolagaformat/test_demo.py | 39 + cmd/bots/bedolagaformat/test_login_demo.py | 154 ++ cmd/telegramloginkeygen/main.go | 391 +++++ cmd/telegramloginkeygen/main_test.go | 109 ++ cmd/telesrv/main.go | 91 ++ .../0125_telegram_login_oidc.down.sql | 14 + .../0125_telegram_login_oidc.up.sql | 253 +++ go.mod | 10 + go.sum | 20 + internal/app/bots/botfather.go | 223 ++- internal/app/bots/botfather_login_test.go | 100 ++ internal/app/bots/service.go | 12 + internal/app/telegramlogin/crypto.go | 131 ++ internal/app/telegramlogin/jose.go | 406 +++++ .../app/telegramlogin/jose_es256k_disabled.go | 5 + .../jose_es256k_disabled_test.go | 17 + .../app/telegramlogin/jose_es256k_enabled.go | 5 + .../telegramlogin/jose_es256k_enabled_test.go | 57 + internal/app/telegramlogin/jose_test.go | 188 +++ internal/app/telegramlogin/keyfiles.go | 186 +++ internal/app/telegramlogin/keyfiles_test.go | 86 ++ internal/app/telegramlogin/native.go | 80 + internal/app/telegramlogin/service.go | 1374 +++++++++++++++++ internal/app/telegramlogin/service_test.go | 404 +++++ internal/app/telegramlogin/url.go | 141 ++ internal/app/telegramlogin/url_test.go | 115 ++ internal/botapi/inline.go | 65 +- internal/botapi/projection.go | 12 + internal/botapi/server_test.go | 13 + internal/config/config.go | 78 + internal/config/config_test.go | 81 +- internal/domain/message_markup.go | 50 +- internal/domain/message_markup_test.go | 4 + internal/domain/telegram_login.go | 506 ++++++ internal/domain/telegram_login_test.go | 112 ++ internal/rpc/account.go | 10 +- internal/rpc/botapi_gateway.go | 18 + internal/rpc/bots_inline.go | 3 + internal/rpc/convert_markup.go | 125 +- internal/rpc/convert_markup_test.go | 20 + internal/rpc/deps.go | 19 + internal/rpc/messages_bot_no_state.go | 13 + internal/rpc/messages_edit.go | 7 + internal/rpc/messages_register.go | 12 + internal/rpc/messages_webview.go | 9 + internal/rpc/telegram_login.go | 434 ++++++ internal/rpc/telegram_login_rpc_test.go | 380 +++++ internal/store/memory/bot.go | 3 + internal/store/memory/telegram_login.go | 721 +++++++++ internal/store/memory/telegram_login_test.go | 320 ++++ ...ft_lifecycle_migration_integration_test.go | 4 +- internal/store/postgres/telegram_login.go | 1106 +++++++++++++ .../telegram_login_integration_test.go | 434 ++++++ internal/store/telegram_login.go | 49 + internal/telegramloginhttp/handler.go | 748 +++++++++ internal/telegramloginhttp/handler_test.go | 675 ++++++++ internal/telegramloginhttp/sdk.go | 90 ++ internal/web/server.go | 15 + 63 files changed, 11374 insertions(+), 37 deletions(-) create mode 100644 cmd/bots/bedolagaformat/login_demo.py create mode 100644 cmd/bots/bedolagaformat/test_login_demo.py create mode 100644 cmd/telegramloginkeygen/main.go create mode 100644 cmd/telegramloginkeygen/main_test.go create mode 100644 deploy/migrations/0125_telegram_login_oidc.down.sql create mode 100644 deploy/migrations/0125_telegram_login_oidc.up.sql create mode 100644 internal/app/bots/botfather_login_test.go create mode 100644 internal/app/telegramlogin/crypto.go create mode 100644 internal/app/telegramlogin/jose.go create mode 100644 internal/app/telegramlogin/jose_es256k_disabled.go create mode 100644 internal/app/telegramlogin/jose_es256k_disabled_test.go create mode 100644 internal/app/telegramlogin/jose_es256k_enabled.go create mode 100644 internal/app/telegramlogin/jose_es256k_enabled_test.go create mode 100644 internal/app/telegramlogin/jose_test.go create mode 100644 internal/app/telegramlogin/keyfiles.go create mode 100644 internal/app/telegramlogin/keyfiles_test.go create mode 100644 internal/app/telegramlogin/native.go create mode 100644 internal/app/telegramlogin/service.go create mode 100644 internal/app/telegramlogin/service_test.go create mode 100644 internal/app/telegramlogin/url.go create mode 100644 internal/app/telegramlogin/url_test.go create mode 100644 internal/domain/telegram_login.go create mode 100644 internal/domain/telegram_login_test.go create mode 100644 internal/rpc/telegram_login.go create mode 100644 internal/rpc/telegram_login_rpc_test.go create mode 100644 internal/store/memory/telegram_login.go create mode 100644 internal/store/memory/telegram_login_test.go create mode 100644 internal/store/postgres/telegram_login.go create mode 100644 internal/store/postgres/telegram_login_integration_test.go create mode 100644 internal/store/telegram_login.go create mode 100644 internal/telegramloginhttp/handler.go create mode 100644 internal/telegramloginhttp/handler_test.go create mode 100644 internal/telegramloginhttp/sdk.go diff --git a/.env.example b/.env.example index 4d388357..c706c601 100644 --- a/.env.example +++ b/.env.example @@ -159,6 +159,24 @@ TELESRV_STICKER_SEED_DIR=data/sticker-seed # through nginx; public canonical URLs use TELESRV_PUBLIC_BASE_URL. TELESRV_PUBLIC_LINK_WEB_ADDR=127.0.0.1:2401 +# Self-hosted Telegram Login / OpenID Connect. The provider is mounted on the +# public-link listener above. Keep disabled until all three local key files +# have been generated with `go run ./cmd/telegramloginkeygen -mode init`. +TELESRV_TELEGRAM_LOGIN_ENABLE=false +TELESRV_TELEGRAM_LOGIN_ISSUER=https://telesrv.net +TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP=false +TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE=data/telegram-login/signing-keys.json +TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE=data/telegram-login/code-keys.json +TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE=data/telegram-login/client-secret-pepper +TELESRV_TELEGRAM_LOGIN_REQUEST_TTL=5m +TELESRV_TELEGRAM_LOGIN_CODE_TTL=2m +TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL=1h +# Trust only the loopback nginx hop in the documented single-host deployment. +TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS=127.0.0.1/32,::1/128 +TELESRV_TELEGRAM_LOGIN_RETENTION=168h +TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL=5m +TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH=500 + # AI compose for TDesktop/Android input box rewrite/polish. # The local provider is deterministic and does not call external services. TELESRV_AI_ENABLED=true diff --git a/cmd/bots/bedolagaformat/README.md b/cmd/bots/bedolagaformat/README.md index 155cff04..1eea2f53 100644 --- a/cmd/bots/bedolagaformat/README.md +++ b/cmd/bots/bedolagaformat/README.md @@ -1,4 +1,4 @@ -# Bedolaga formatted-text demo +# Bedolaga formatted-text + Telegram Login demo 这个 demo 复刻 Bedolaga 的 Bot 工厂关键配置: @@ -67,3 +67,61 @@ $env:TELESRV_BOT_API_SERVER = "http://127.0.0.1:8081" `--base-url` 只接受 API server 根地址,不要追加 `/bot`。脚本不会打印 token,也不会 把 token 写入文件。 + +## Telegram Login 全链路 + +同一个 demo 还提供 `/logindemo`,覆盖三个互相独立的公开契约: + +1. Bot API `login_url` 按钮 → TDesktop/Android 的 + `messages.requestUrlAuth/acceptUrlAuth` → legacy HMAC 回调; +2. telesrv 本地 `/telegram-login.js` → popup `postMessage` → JWKS 验签; +3. 服务端 Authorization Code + PKCE S256 → `/token` Basic Client Secret → + JWKS 验签和 `issuer/audience/nonce/subject` 复核。 + +先在 telesrv 的 @BotFather 中对目标 bot 运行 `/setlogin`。选择 bot 后逐条登记 demo +的精确 origin 和 callback(本机示例): + +```text +add origin http://127.0.0.1:3000 +add redirect http://127.0.0.1:3000/oauth/callback +enable +``` + +`/setlogin` 首次创建 client 时只展示一次 OIDC Client Secret;不要写进仓库。可用 +`/logininfo` 查看 Client ID 和登记结果,或用 `/resetloginsecret` 轮换 secret。 +loopback HTTP 仅应配合 telesrv 的显式开发开关使用;testserver/生产必须换成精确 +HTTPS origin。 + +把一次性 secret 和 Client ID 放入进程环境,再启动: + +```powershell +$env:TELESRV_BOT_LOGIN_DEMO = "1" +$env:TELESRV_BOT_LOGIN_ISSUER = "http://127.0.0.1:2401" +$env:TELESRV_BOT_LOGIN_CLIENT_ID = "" +$env:TELESRV_BOT_LOGIN_CLIENT_SECRET = "" +$env:TELESRV_BOT_LOGIN_PUBLIC_URL = "http://127.0.0.1:3000" +$env:TELESRV_BOT_LOGIN_LISTEN = "127.0.0.1:3000" + +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" ` + .\cmd\bots\bedolagaformat\demo.py --drop-pending --login-demo +``` + +向 bot 发送 `/logindemo`。第一颗按钮必须出现 Telegram 客户端原生授权确认框,批准 +后网页显示 `login_url HMAC verified`;第二颗按钮打开测试页,可分别运行 JS SDK popup +和 Authorization Code + PKCE。页面只展示验签后的 claims,不展示 access token 或 +Client Secret。省略 `TELESRV_BOT_LOGIN_CLIENT_SECRET` 时仍可验证 JS popup,但服务端 +code flow 会明确禁用。 + +demo 的 flow/state/nonce 只保存在单进程内存中,带 10 分钟过期和 256 条上限,专用于 +本地与 testserver 端到端验证,不是生产 relying-party 实现。官方 iOS/Android SDK +目前把 `https://oauth.telegram.org` 写死;验证自建 issuer 时需使用项目记录的最小 +base-URL patch 或等价测试构建,不能把官方生产 SDK 未修改的结果误判为自建服务结果。 + +测试命令: + +```powershell +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" ` + .\cmd\bots\bedolagaformat\test_demo.py -v +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" ` + .\cmd\bots\bedolagaformat\test_login_demo.py -v +``` diff --git a/cmd/bots/bedolagaformat/demo.py b/cmd/bots/bedolagaformat/demo.py index d339a8db..adc6b87e 100644 --- a/cmd/bots/bedolagaformat/demo.py +++ b/cmd/bots/bedolagaformat/demo.py @@ -27,15 +27,27 @@ from aiogram.types import ( InlineKeyboardButton, InlineKeyboardMarkup, InputRichMessage, + LoginUrl, Message, ) +from login_demo import ( + LoginDemoConfig, + LoginDemoServer, + normalize_web_base, + parse_listen, +) + LOG = logging.getLogger("bedolagaformat") MARKER_RE = re.compile(r"^[A-Za-z0-9-]{1,64}$") MARKDOWN_V2_RESERVED_RE = re.compile(r"([_\*\[\]\(\)~`>#+\-=|{}\.!\\])") +def env_flag(name: str) -> bool: + return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"} + + @dataclass(frozen=True) class FormatSample: name: str @@ -121,6 +133,34 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--polling-timeout", type=int, default=10) parser.add_argument("--marker", default=default_marker()) parser.add_argument("--log-level", default="INFO") + parser.add_argument( + "--login-demo", + action="store_true", + default=env_flag("TELESRV_BOT_LOGIN_DEMO"), + help="serve and send the Bedolaga Telegram Login/OIDC demo", + ) + parser.add_argument( + "--login-issuer", + default=os.getenv("TELESRV_BOT_LOGIN_ISSUER", "http://127.0.0.1:2401"), + ) + parser.add_argument( + "--login-client-id", + default=os.getenv("TELESRV_BOT_LOGIN_CLIENT_ID", ""), + ) + parser.add_argument( + "--login-client-secret", + default=os.getenv("TELESRV_BOT_LOGIN_CLIENT_SECRET", ""), + help="confidential OIDC secret; never printed (optional for JS-only demo)", + ) + parser.add_argument( + "--login-public-url", + default=os.getenv("TELESRV_BOT_LOGIN_PUBLIC_URL", "http://127.0.0.1:3000"), + help="registered origin where this demo is reachable", + ) + parser.add_argument( + "--login-listen", + default=os.getenv("TELESRV_BOT_LOGIN_LISTEN", "127.0.0.1:3000"), + ) args = parser.parse_args() if not args.token: parser.error("missing --token or TELESRV_BOT_TOKEN") @@ -132,6 +172,24 @@ def parse_args() -> argparse.Namespace: parser.error("--marker must contain 1-64 ASCII letters, digits, or hyphens") if not 0 <= args.polling_timeout <= 50: parser.error("--polling-timeout must be between 0 and 50") + args.login_config = None + if args.login_demo: + if not re.fullmatch(r"[0-9]{1,64}", args.login_client_id): + parser.error("--login-demo requires a numeric --login-client-id") + try: + issuer = normalize_web_base(args.login_issuer, name="login issuer") + public_url = normalize_web_base(args.login_public_url, name="login public URL") + listen_host, listen_port = parse_listen(args.login_listen) + except ValueError as exc: + parser.error(str(exc)) + args.login_config = LoginDemoConfig( + issuer=issuer, + client_id=args.login_client_id, + client_secret=args.login_client_secret, + public_url=public_url, + listen_host=listen_host, + listen_port=listen_port, + ) return args @@ -207,6 +265,39 @@ def rich_menu_keyboard() -> InlineKeyboardMarkup: ) +def login_demo_keyboard(config: LoginDemoConfig) -> InlineKeyboardMarkup: + """Exercise both Telegram's login_url button and a plain OIDC page URL.""" + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Log in with Telegram", + login_url=LoginUrl( + url=config.public_url + "/", + forward_text="Bedolaga Login", + request_write_access=True, + ), + ) + ], + [InlineKeyboardButton(text="Open OIDC test page", url=config.public_url + "/")], + ] + ) + + +async def send_login_demo(bot: Bot, chat_id: int, marker: str, config: LoginDemoConfig) -> int: + message = await bot.send_message( + chat_id=chat_id, + text=( + f"{marker} Telegram Login\n" + "The first button validates Bot API login_url; " + "the second page validates the local JS SDK and OIDC + PKCE." + ), + reply_markup=login_demo_keyboard(config), + ) + LOG.info("sent Telegram Login demo chat_id=%s message_id=%s", chat_id, message.message_id) + return message.message_id + + def is_rich_media_retry_error(exc: TelegramBadRequest) -> bool: message = str(exc).lower() return "webpage_" in message or "media_empty" in message or "media_invalid" in message @@ -259,7 +350,7 @@ async def send_rich_suite(bot: Bot, chat_id: int, marker: str) -> list[int]: return ids -def build_dispatcher(marker: str) -> Dispatcher: +def build_dispatcher(marker: str, login_config: LoginDemoConfig | None = None) -> Dispatcher: router = Router(name="telesrv-bedolaga-format") @router.message(CommandStart()) @@ -291,6 +382,22 @@ def build_dispatcher(marker: str) -> Dispatcher: ids, ) + @router.message(Command("logindemo")) + async def login_demo(message: Message) -> None: + if login_config is None: + await message.answer( + "Telegram Login demo is disabled. Start this program with " + "--login-demo." + ) + return + message_id = await send_login_demo(message.bot, message.chat.id, marker, login_config) + LOG.info( + "handled /logindemo chat_id=%s incoming_message_id=%s sent_message_id=%s", + message.chat.id, + message.message_id, + message_id, + ) + dispatcher = Dispatcher() dispatcher.include_router(router) return dispatcher @@ -298,7 +405,10 @@ def build_dispatcher(marker: str) -> Dispatcher: async def run(args: argparse.Namespace) -> None: bot = create_bot(args.token, args.base_url) + login_server = LoginDemoServer(args.login_config, args.token) if args.login_config else None try: + if login_server is not None: + await login_server.start() me = await bot.get_me() LOG.info( "authenticated bot_id=%s username=@%s bot_api=%s marker=%s", @@ -312,12 +422,17 @@ async def run(args: argparse.Namespace) -> None: await send_format_suite(bot, args.send_chat_id, args.marker) if args.rich_menu or args.rich_only: await send_rich_suite(bot, args.send_chat_id, args.marker) + if args.login_config is not None: + await send_login_demo(bot, args.send_chat_id, args.marker, args.login_config) if args.send_only: return await bot.delete_webhook(drop_pending_updates=args.drop_pending) - dispatcher = build_dispatcher(args.marker) - LOG.info("polling started; send /start, /formatdemo or /richdemo to @%s", me.username or me.id) + dispatcher = build_dispatcher(args.marker, args.login_config) + LOG.info( + "polling started; send /start, /formatdemo, /richdemo or /logindemo to @%s", + me.username or me.id, + ) await dispatcher.start_polling( bot, allowed_updates=["message"], @@ -325,6 +440,8 @@ async def run(args: argparse.Namespace) -> None: close_bot_session=False, ) finally: + if login_server is not None: + await login_server.close() await bot.session.close() diff --git a/cmd/bots/bedolagaformat/login_demo.py b/cmd/bots/bedolagaformat/login_demo.py new file mode 100644 index 00000000..c0cae522 --- /dev/null +++ b/cmd/bots/bedolagaformat/login_demo.py @@ -0,0 +1,462 @@ +"""Local Bedolaga Telegram Login/OIDC relying-party demo. + +This is deliberately a relying party, not a shortcut into telesrv internals. It +validates the three public contracts used by a Bedolaga-style bot: + +* Bot API ``login_url`` legacy HMAC callbacks; +* the self-hosted Telegram Login JavaScript SDK ``post_message`` response; and +* confidential authorization-code + PKCE followed by JWKS ID-token validation. + +The demo keeps its short-lived browser flows in memory. It is intended for +local/end-to-end verification only and must not be used as a production login +backend. +""" + +from __future__ import annotations + +import asyncio +import base64 +from dataclasses import dataclass +import hashlib +import hmac +import html +import json +import logging +import secrets +import time +from typing import Any +from urllib.parse import urlencode, urlsplit + +from aiohttp import BasicAuth, ClientSession, ClientTimeout, web +import jwt + + +LOG = logging.getLogger("bedolagaformat.login") +FLOW_TTL_SECONDS = 10 * 60 +MAX_PENDING_FLOWS = 256 +LEGACY_AUTH_MAX_AGE_SECONDS = 15 * 60 +OIDC_ALGORITHMS = ("RS256", "ES256", "EdDSA", "ES256K") + + +@dataclass(frozen=True) +class LoginDemoConfig: + issuer: str + client_id: str + client_secret: str + public_url: str + listen_host: str + listen_port: int + + @property + def redirect_uri(self) -> str: + return self.public_url + "/oauth/callback" + + @property + def origin(self) -> str: + parsed = urlsplit(self.public_url) + return f"{parsed.scheme}://{parsed.netloc}" + + @property + def code_flow_enabled(self) -> bool: + return bool(self.client_secret) + + +@dataclass(frozen=True) +class PendingFlow: + nonce: str + expires_at: float + code_verifier: str = "" + + +def _is_loopback(host: str | None) -> bool: + return host in {"127.0.0.1", "::1", "localhost"} + + +def normalize_web_base(value: str, *, name: str) -> str: + raw = value.strip().rstrip("/") + parsed = urlsplit(raw) + if ( + parsed.scheme not in {"http", "https"} + or not parsed.netloc + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or parsed.path not in {"", "/"} + ): + raise ValueError(f"{name} must be an absolute origin without path, query, or fragment") + if parsed.scheme != "https" and not _is_loopback(parsed.hostname): + raise ValueError(f"{name} must use HTTPS except on loopback") + return f"{parsed.scheme}://{parsed.netloc}" + + +def parse_listen(value: str) -> tuple[str, int]: + parsed = urlsplit("//" + value.strip()) + try: + port = parsed.port + except ValueError as exc: + raise ValueError("login demo listen port is invalid") from exc + if not parsed.hostname or port is None or not 1 <= port <= 65535: + raise ValueError("login demo listen address must be host:port") + return parsed.hostname, port + + +def base64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def generate_pkce() -> tuple[str, str]: + verifier = base64url(secrets.token_bytes(32)) + challenge = base64url(hashlib.sha256(verifier.encode("ascii")).digest()) + return verifier, challenge + + +def verify_legacy_login_query( + query: dict[str, str], bot_token: str, *, now: int | None = None +) -> dict[str, str]: + """Verify Telegram's legacy login_url data-check string. + + Only the documented signed identity fields participate. Query parameters + already present on the relying-party URL are intentionally excluded. + """ + + signed_names = ( + "auth_date", + "first_name", + "id", + "last_name", + "photo_url", + "username", + ) + supplied_hash = query.get("hash", "") + if len(supplied_hash) != 64: + raise ValueError("missing legacy login signature") + values = {name: query[name] for name in signed_names if name in query} + if not all(values.get(name) for name in ("auth_date", "first_name", "id")): + raise ValueError("incomplete legacy login payload") + try: + auth_date = int(values["auth_date"]) + user_id = int(values["id"]) + except ValueError as exc: + raise ValueError("invalid legacy login payload") from exc + current = int(time.time()) if now is None else now + if user_id <= 0 or auth_date > current + 30 or current - auth_date > LEGACY_AUTH_MAX_AGE_SECONDS: + raise ValueError("expired legacy login payload") + data_check = "\n".join(f"{name}={values[name]}" for name in sorted(values)) + secret_key = hashlib.sha256(bot_token.encode("utf-8")).digest() + actual = hmac.new(secret_key, data_check.encode("utf-8"), hashlib.sha256).hexdigest() + if not hmac.compare_digest(actual, supplied_hash.lower()): + raise ValueError("invalid legacy login signature") + return values + + +def _safe_claims(claims: dict[str, Any]) -> dict[str, Any]: + allowed = ( + "iss", + "aud", + "sub", + "iat", + "exp", + "nonce", + "id", + "name", + "given_name", + "family_name", + "preferred_username", + "picture", + "phone_number", + "phone_number_verified", + ) + return {key: claims[key] for key in allowed if key in claims} + + +class LoginDemoServer: + def __init__(self, config: LoginDemoConfig, bot_token: str) -> None: + self.config = config + self.bot_token = bot_token + self._flows: dict[str, PendingFlow] = {} + self._flow_lock = asyncio.Lock() + self._http: ClientSession | None = None + self._runner: web.AppRunner | None = None + + async def start(self) -> None: + timeout = ClientTimeout(total=10) + self._http = ClientSession(timeout=timeout) + app = web.Application(client_max_size=64 * 1024) + app.add_routes( + [ + web.get("/", self.root), + web.get("/login/code", self.start_code_flow), + web.get("/oauth/callback", self.code_callback), + web.post("/verify-popup", self.verify_popup), + web.get("/healthz", self.health), + ] + ) + self._runner = web.AppRunner(app, access_log=None) + await self._runner.setup() + site = web.TCPSite(self._runner, self.config.listen_host, self.config.listen_port) + await site.start() + LOG.info( + "Telegram Login demo listening at %s (issuer=%s client_id=%s)", + self.config.public_url, + self.config.issuer, + self.config.client_id, + ) + + async def close(self) -> None: + if self._runner is not None: + await self._runner.cleanup() + self._runner = None + if self._http is not None: + await self._http.close() + self._http = None + + async def _put_flow(self, flow: PendingFlow) -> str: + flow_id = secrets.token_urlsafe(24) + now = time.time() + async with self._flow_lock: + self._flows = { + key: value for key, value in self._flows.items() if value.expires_at > now + } + if len(self._flows) >= MAX_PENDING_FLOWS: + oldest = min(self._flows, key=lambda key: self._flows[key].expires_at) + del self._flows[oldest] + self._flows[flow_id] = flow + return flow_id + + async def _take_flow(self, flow_id: str, *, consume: bool) -> PendingFlow: + async with self._flow_lock: + flow = self._flows.get(flow_id) + if flow is None or flow.expires_at <= time.time(): + self._flows.pop(flow_id, None) + raise ValueError("login flow is invalid or expired") + if consume: + del self._flows[flow_id] + return flow + + @staticmethod + def _headers(response: web.StreamResponse) -> None: + response.headers["Cache-Control"] = "no-store" + response.headers["Pragma"] = "no-cache" + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["Referrer-Policy"] = "no-referrer" + + async def health(self, _: web.Request) -> web.Response: + response = web.json_response({"status": "ok"}) + self._headers(response) + return response + + async def root(self, request: web.Request) -> web.Response: + legacy_result = "" + if "hash" in request.query: + try: + signed_names = { + "auth_date", "first_name", "id", "last_name", "photo_url", "username", "hash" + } + if any(len(request.query.getall(key, [])) != 1 for key in signed_names if key in request.query): + raise ValueError("duplicate legacy login field") + query = {key: request.query[key] for key in request.query} + identity = verify_legacy_login_query(query, self.bot_token) + legacy_result = ( + "

login_url HMAC verified for user " + + html.escape(identity["id"]) + + ".

" + ) + except ValueError: + legacy_result = '

login_url HMAC verification failed.

' + + nonce = secrets.token_urlsafe(24) + flow_id = await self._put_flow(PendingFlow(nonce=nonce, expires_at=time.time() + FLOW_TTL_SECONDS)) + sdk_url = self.config.issuer + "/js/telegram-login.js" + csp_nonce = secrets.token_urlsafe(18) + code_link = '
Authorization Code + PKCE' + if not self.config.code_flow_enabled: + code_link = 'Code flow disabled: configure client secret.' + script_config = json.dumps( + {"clientID": self.config.client_id, "flowID": flow_id, "nonce": nonce}, + separators=(",", ":"), + ).replace("<", "\\u003c") + body = f""" +Bedolaga Telegram Login Demo + +

Bedolaga Telegram Login Demo

{legacy_result} +

This page verifies the self-hosted JavaScript SDK and the standard server-side OIDC flow.

+{code_link}
Ready.
+ +""" + response = web.Response(text=body, content_type="text/html") + self._headers(response) + response.headers["Content-Security-Policy"] = ( + "default-src 'none'; style-src 'unsafe-inline'; " + f"script-src '{csp_nonce}' {self.config.issuer}; connect-src 'self' {self.config.issuer}; " + "frame-ancestors 'none'; base-uri 'none'; form-action 'self'" + ) + # CSP nonce source expressions include the nonce- prefix. + response.headers["Content-Security-Policy"] = response.headers["Content-Security-Policy"].replace( + f"'{csp_nonce}'", f"'nonce-{csp_nonce}'" + ) + return response + + async def start_code_flow(self, _: web.Request) -> web.Response: + if not self.config.code_flow_enabled: + raise web.HTTPNotFound() + verifier, challenge = generate_pkce() + nonce = secrets.token_urlsafe(24) + state = await self._put_flow( + PendingFlow(nonce=nonce, code_verifier=verifier, expires_at=time.time() + FLOW_TTL_SECONDS) + ) + query = urlencode( + { + "client_id": self.config.client_id, + "redirect_uri": self.config.redirect_uri, + "response_type": "code", + "scope": "openid profile phone telegram:bot_access", + "state": state, + "nonce": nonce, + "code_challenge": challenge, + "code_challenge_method": "S256", + } + ) + response = web.HTTPFound(self.config.issuer + "/auth?" + query) + self._headers(response) + raise response + + async def code_callback(self, request: web.Request) -> web.Response: + state = request.query.get("state", "") + try: + flow = await self._take_flow(state, consume=True) + except ValueError: + return self._result_page("Authorization failed", {"error": "invalid_or_expired_state"}, ok=False) + if request.query.get("error"): + return self._result_page("Authorization declined", {"error": request.query["error"]}, ok=False) + code = request.query.get("code", "") + if not code or len(code) > 2048: + return self._result_page("Authorization failed", {"error": "missing_code"}, ok=False) + try: + token = await self._exchange_code(code, flow.code_verifier) + claims = await self.verify_id_token(token, flow.nonce) + except Exception as exc: # noqa: BLE001 - convert all protocol failures to a safe demo page + LOG.warning("OIDC code flow verification failed: %s", type(exc).__name__) + return self._result_page("Authorization failed", {"error": "token_verification_failed"}, ok=False) + return self._result_page("Authorization complete", _safe_claims(claims), ok=True) + + async def verify_popup(self, request: web.Request) -> web.Response: + origin = request.headers.get("Origin") + if origin and origin != self.config.origin: + return web.json_response({"error": "invalid_origin"}, status=403) + try: + payload = await request.json() + flow_id = str(payload.get("flow_id", "")) + id_token = str(payload.get("id_token", "")) + in_app = payload.get("in_app", False) + if ( + not flow_id + or len(flow_id) > 128 + or not id_token + or len(id_token) > 16384 + or not isinstance(in_app, bool) + ): + raise ValueError("invalid popup response") + flow = await self._take_flow(flow_id, consume=True) + # Telegram's official Mini App /inapp contract has exactly four + # request parameters and does not carry the JS API nonce. Popup + # flows still require it; Mini App tokens must omit it. + claims = await self.verify_id_token(id_token, "" if in_app else flow.nonce) + except Exception as exc: # noqa: BLE001 - safe public validation failure + LOG.info("OIDC popup verification rejected: %s", type(exc).__name__) + response = web.json_response({"error": "token_verification_failed"}, status=400) + self._headers(response) + return response + response = web.json_response({"claims": _safe_claims(claims)}) + self._headers(response) + return response + + async def _discovery(self) -> dict[str, Any]: + if self._http is None: + raise RuntimeError("login demo server is not started") + async with self._http.get(self.config.issuer + "/.well-known/openid-configuration") as response: + response.raise_for_status() + document = await response.json() + if document.get("issuer") != self.config.issuer: + raise ValueError("OIDC issuer mismatch") + issuer_origin = urlsplit(self.config.issuer).netloc + for name in ("token_endpoint", "jwks_uri"): + endpoint = urlsplit(str(document.get(name, ""))) + if endpoint.scheme != urlsplit(self.config.issuer).scheme or endpoint.netloc != issuer_origin: + raise ValueError(f"OIDC {name} must share the configured issuer origin") + return document + + async def _exchange_code(self, code: str, verifier: str) -> str: + if self._http is None: + raise RuntimeError("login demo server is not started") + discovery = await self._discovery() + form = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": self.config.redirect_uri, + "code_verifier": verifier, + } + async with self._http.post( + discovery["token_endpoint"], + data=form, + auth=BasicAuth(self.config.client_id, self.config.client_secret), + ) as response: + document = await response.json() + if response.status != 200: + raise ValueError("OIDC token endpoint rejected the grant") + token = document.get("id_token") + if not isinstance(token, str) or not token: + raise ValueError("OIDC token response omitted id_token") + return token + + async def verify_id_token(self, token: str, nonce: str) -> dict[str, Any]: + if self._http is None: + raise RuntimeError("login demo server is not started") + discovery = await self._discovery() + header = jwt.get_unverified_header(token) + kid, algorithm = header.get("kid"), header.get("alg") + if not isinstance(kid, str) or algorithm not in OIDC_ALGORITHMS: + raise ValueError("unsupported ID token header") + async with self._http.get(discovery["jwks_uri"]) as response: + response.raise_for_status() + document = await response.json() + raw_key = next((key for key in document.get("keys", []) if key.get("kid") == kid), None) + if raw_key is None: + raise ValueError("ID token signing key not found") + public_key = jwt.PyJWK.from_dict(raw_key, algorithm=algorithm).key + required_claims = ["iss", "aud", "sub", "iat", "exp"] + if nonce: + required_claims.append("nonce") + claims = jwt.decode( + token, + public_key, + algorithms=[algorithm], + audience=self.config.client_id, + issuer=self.config.issuer, + options={"require": required_claims}, + ) + if nonce and not hmac.compare_digest(str(claims.get("nonce", "")), nonce): + raise ValueError("ID token nonce mismatch") + if not nonce and claims.get("nonce") not in (None, ""): + raise ValueError("unexpected ID token nonce") + if str(claims.get("sub", "")) != str(claims.get("id", "")): + raise ValueError("ID token subject mismatch") + return claims + + def _result_page(self, title: str, payload: dict[str, Any], *, ok: bool) -> web.Response: + css_class = "ok" if ok else "error" + body = ( + '' + + html.escape(title) + + "

" + + html.escape(title) + + "

"
+            + html.escape(json.dumps(payload, indent=2, ensure_ascii=False))
+            + '

Run another flow

' + ) + response = web.Response(text=body, content_type="text/html") + self._headers(response) + response.headers["Content-Security-Policy"] = "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'" + return response diff --git a/cmd/bots/bedolagaformat/requirements.txt b/cmd/bots/bedolagaformat/requirements.txt index 1b94ec4e..658a9e23 100644 --- a/cmd/bots/bedolagaformat/requirements.txt +++ b/cmd/bots/bedolagaformat/requirements.txt @@ -1 +1,2 @@ aiogram==3.30.0 +PyJWT[crypto]==2.10.1 diff --git a/cmd/bots/bedolagaformat/test_demo.py b/cmd/bots/bedolagaformat/test_demo.py index d4e005e2..00cd7123 100644 --- a/cmd/bots/bedolagaformat/test_demo.py +++ b/cmd/bots/bedolagaformat/test_demo.py @@ -11,6 +11,7 @@ from aiogram.types import InputRichMessage MODULE_PATH = Path(__file__).with_name("demo.py") +sys.path.insert(0, str(MODULE_PATH.parent)) SPEC = importlib.util.spec_from_file_location("bedolagaformat_demo", MODULE_PATH) assert SPEC is not None and SPEC.loader is not None demo = importlib.util.module_from_spec(SPEC) @@ -89,6 +90,44 @@ class BedolagaFormatDemoTest(unittest.IsolatedAsyncioTestCase): self.assertEqual(edit.kwargs["message_id"], 21) self.assertIn("EDITED", edit.kwargs["rich_message"].html) + def test_login_demo_keyboard_has_login_url_and_plain_oidc_link(self) -> None: + config = demo.LoginDemoConfig( + issuer="https://oauth.example", + client_id="9001", + client_secret="secret", + public_url="https://rp.example", + listen_host="127.0.0.1", + listen_port=3000, + ) + markup = demo.login_demo_keyboard(config) + login = markup.inline_keyboard[0][0].login_url + self.assertIsNotNone(login) + self.assertEqual(login.url, "https://rp.example/") + self.assertTrue(login.request_write_access) + self.assertEqual(markup.inline_keyboard[1][0].url, "https://rp.example/") + + async def test_send_login_demo_preserves_default_html_and_keyboard(self) -> None: + config = demo.LoginDemoConfig( + issuer="https://oauth.example", + client_id="9001", + client_secret="secret", + public_url="https://rp.example", + listen_host="127.0.0.1", + listen_port=3000, + ) + bot = AsyncMock() + bot.send_message.return_value = SentMessage(31) + + message_id = await demo.send_login_demo(bot, 1780243200, "BEDOLAGA123", config) + + self.assertEqual(message_id, 31) + call = bot.send_message.await_args + self.assertNotIn("parse_mode", call.kwargs) + self.assertEqual( + call.kwargs["reply_markup"].inline_keyboard[0][0].login_url.url, + "https://rp.example/", + ) + if __name__ == "__main__": unittest.main() diff --git a/cmd/bots/bedolagaformat/test_login_demo.py b/cmd/bots/bedolagaformat/test_login_demo.py new file mode 100644 index 00000000..7ba9bb54 --- /dev/null +++ b/cmd/bots/bedolagaformat/test_login_demo.py @@ -0,0 +1,154 @@ +import asyncio +import hashlib +import hmac +from pathlib import Path +import sys +import time +import unittest + +from aiohttp import ClientSession, web +from aiohttp.test_utils import TestServer +from cryptography.hazmat.primitives.asymmetric import rsa +import jwt + + +sys.path.insert(0, str(Path(__file__).parent)) +import login_demo as demo # noqa: E402 + + +class LoginDemoHelpersTest(unittest.TestCase): + def test_legacy_login_hmac_and_freshness(self) -> None: + now = 1_800_000_000 + token = "9001:bot-secret" + values = { + "auth_date": str(now - 10), + "first_name": "Alice", + "id": "42", + "username": "alice", + } + data_check = "\n".join(f"{key}={values[key]}" for key in sorted(values)) + key = hashlib.sha256(token.encode()).digest() + values["hash"] = hmac.new(key, data_check.encode(), hashlib.sha256).hexdigest() + values["untrusted_existing_query"] = "not-signed" + + verified = demo.verify_legacy_login_query(values, token, now=now) + + self.assertEqual(verified["id"], "42") + self.assertNotIn("untrusted_existing_query", verified) + with self.assertRaisesRegex(ValueError, "signature"): + demo.verify_legacy_login_query({**values, "id": "43"}, token, now=now) + with self.assertRaisesRegex(ValueError, "expired"): + demo.verify_legacy_login_query(values, token, now=now + 3600) + + def test_web_origins_and_listen_are_strict(self) -> None: + self.assertEqual( + demo.normalize_web_base("https://rp.example/", name="RP"), + "https://rp.example", + ) + self.assertEqual( + demo.normalize_web_base("http://127.0.0.1:3000", name="RP"), + "http://127.0.0.1:3000", + ) + with self.assertRaises(ValueError): + demo.normalize_web_base("http://rp.example", name="RP") + with self.assertRaises(ValueError): + demo.normalize_web_base("https://rp.example/callback", name="RP") + self.assertEqual(demo.parse_listen("127.0.0.1:3000"), ("127.0.0.1", 3000)) + + +class LoginDemoTokenTest(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self) -> None: + self.private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + raw_jwk = jwt.algorithms.RSAAlgorithm.to_jwk(self.private_key.public_key(), as_dict=True) + raw_jwk.update({"kid": "demo-rs256", "alg": "RS256", "use": "sig"}) + self.jwk = raw_jwk + self.issuer = "" + + async def discovery(_: web.Request) -> web.Response: + return web.json_response( + { + "issuer": self.issuer, + "token_endpoint": self.issuer + "/token", + "jwks_uri": self.issuer + "/jwks", + } + ) + + async def jwks(_: web.Request) -> web.Response: + return web.json_response({"keys": [self.jwk]}) + + app = web.Application() + app.add_routes([web.get("/.well-known/openid-configuration", discovery), web.get("/jwks", jwks)]) + self.http_server = TestServer(app) + await self.http_server.start_server() + self.issuer = str(self.http_server.make_url("")).rstrip("/") + config = demo.LoginDemoConfig( + issuer=self.issuer, + client_id="9001", + client_secret="secret", + public_url="http://127.0.0.1:3000", + listen_host="127.0.0.1", + listen_port=3000, + ) + self.demo = demo.LoginDemoServer(config, "9001:bot-secret") + self.demo._http = ClientSession() + + async def asyncTearDown(self) -> None: + await self.demo._http.close() + await self.http_server.close() + + async def test_id_token_requires_signature_issuer_audience_nonce_and_subject(self) -> None: + now = int(time.time()) + claims = { + "iss": self.issuer, + "aud": "9001", + "sub": "42", + "id": 42, + "iat": now, + "exp": now + 300, + "nonce": "expected-nonce", + "name": "Alice", + } + token = jwt.encode( + claims, + self.private_key, + algorithm="RS256", + headers={"kid": "demo-rs256"}, + ) + + verified = await self.demo.verify_id_token(token, "expected-nonce") + + self.assertEqual(verified["sub"], "42") + with self.assertRaisesRegex(ValueError, "nonce"): + await self.demo.verify_id_token(token, "wrong-nonce") + with self.assertRaisesRegex(ValueError, "nonce"): + await self.demo.verify_id_token(token, "") + + in_app_claims = dict(claims) + in_app_claims.pop("nonce") + in_app_token = jwt.encode( + in_app_claims, + self.private_key, + algorithm="RS256", + headers={"kid": "demo-rs256"}, + ) + verified_in_app = await self.demo.verify_id_token(in_app_token, "") + self.assertEqual(verified_in_app["sub"], "42") + + async def test_pending_flow_is_one_time_and_expiring(self) -> None: + flow_id = await self.demo._put_flow( + demo.PendingFlow(nonce="n", expires_at=time.time() + 10) + ) + flow = await self.demo._take_flow(flow_id, consume=True) + self.assertEqual(flow.nonce, "n") + with self.assertRaises(ValueError): + await self.demo._take_flow(flow_id, consume=True) + + expired = await self.demo._put_flow( + demo.PendingFlow(nonce="old", expires_at=time.time() - 1) + ) + with self.assertRaises(ValueError): + await self.demo._take_flow(expired, consume=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/cmd/telegramloginkeygen/main.go b/cmd/telegramloginkeygen/main.go new file mode 100644 index 00000000..93745096 --- /dev/null +++ b/cmd/telegramloginkeygen/main.go @@ -0,0 +1,391 @@ +// Command telegramloginkeygen initializes and rotates telesrv Telegram Login +// key files without ever writing secret material to stdout. +package main + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + telegramlogin "telesrv/internal/app/telegramlogin" + "telesrv/internal/domain" +) + +const maxManifestBytes = 1 << 20 +const signingRetirementMargin = 10 * time.Minute + +type signingManifest struct { + Version int `json:"version"` + Keys []signingManifestKey `json:"keys"` +} + +type signingManifestKey struct { + Algorithm domain.TelegramLoginSigningAlgorithm `json:"algorithm"` + KeyID string `json:"kid"` + PrivateKeyFile string `json:"private_key_file"` + Active bool `json:"active"` + PublishUntil string `json:"publish_until,omitempty"` +} + +type codeManifest struct { + Version int `json:"version"` + Active string `json:"active"` + Keys map[string]string `json:"keys"` +} + +type options struct { + mode string + dir string + algorithm domain.TelegramLoginSigningAlgorithm + publishFor time.Duration + idTokenTTL time.Duration + now func() time.Time +} + +func main() { + mode := flag.String("mode", "init", "init, rotate-signing, or rotate-code") + dir := flag.String("dir", "data/telegram-login", "key directory") + algorithm := flag.String("algorithm", "RS256", "signing algorithm to rotate: RS256, ES256, or EdDSA") + publishFor := flag.Duration("publish-for", 2*time.Hour, "how long the retiring public key remains in JWKS") + idTokenTTL := flag.Duration("id-token-ttl", time.Hour, "configured TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL") + flag.Parse() + if flag.NArg() != 0 { + fatal(errors.New("positional arguments are not accepted")) + } + opts := options{ + mode: strings.ToLower(strings.TrimSpace(*mode)), dir: strings.TrimSpace(*dir), + algorithm: domain.TelegramLoginSigningAlgorithm(strings.ToUpper(strings.TrimSpace(*algorithm))), + publishFor: *publishFor, idTokenTTL: *idTokenTTL, now: time.Now, + } + if err := run(opts); err != nil { + fatal(err) + } + fmt.Printf("Telegram Login key operation %s completed in %s; restart all instances to load one consistent key ring.\n", opts.mode, opts.dir) +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, "telegramloginkeygen:", err) + os.Exit(1) +} + +func run(opts options) error { + if opts.now == nil { + opts.now = time.Now + } + if opts.dir == "" { + return errors.New("key directory is required") + } + absDir, err := filepath.Abs(opts.dir) + if err != nil { + return fmt.Errorf("resolve key directory: %w", err) + } + if err := os.MkdirAll(absDir, 0o700); err != nil { + return fmt.Errorf("create key directory: %w", err) + } + if err := os.Chmod(absDir, 0o700); err != nil { + return fmt.Errorf("restrict key directory: %w", err) + } + lockPath := filepath.Join(absDir, ".keygen.lock") + lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return fmt.Errorf("acquire key operation lock: %w", err) + } + _ = lock.Close() + defer func() { _ = os.Remove(lockPath) }() + + switch opts.mode { + case "init": + return initialize(absDir, opts.now().UTC()) + case "rotate-signing": + if opts.idTokenTTL < time.Minute || opts.idTokenTTL > 24*time.Hour { + return errors.New("id-token-ttl must match the configured 1m..24h ID-token TTL") + } + if opts.publishFor < opts.idTokenTTL+signingRetirementMargin || opts.publishFor > 90*24*time.Hour { + return fmt.Errorf("publish-for must be at least id-token-ttl plus %s and at most 2160h", signingRetirementMargin) + } + return rotateSigning(absDir, opts.algorithm, opts.publishFor, opts.now().UTC()) + case "rotate-code": + return rotateCode(absDir, opts.now().UTC()) + default: + return errors.New("mode must be init, rotate-signing, or rotate-code") + } +} + +func initialize(dir string, now time.Time) error { + for _, name := range []string{"signing-keys.json", "code-keys.json", "client-secret-pepper"} { + if _, err := os.Lstat(filepath.Join(dir, name)); err == nil { + return fmt.Errorf("refusing to overwrite existing %s", name) + } else if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("inspect %s: %w", name, err) + } + } + + manifest := signingManifest{Version: 1} + for _, algorithm := range []domain.TelegramLoginSigningAlgorithm{ + domain.TelegramLoginSigningRS256, + domain.TelegramLoginSigningES256, + domain.TelegramLoginSigningEdDSA, + } { + entry, err := generateSigningKey(dir, algorithm, now) + if err != nil { + return err + } + manifest.Keys = append(manifest.Keys, entry) + } + if err := writeSigningManifest(dir, manifest); err != nil { + return err + } + codeID, err := newKeyID("code", now) + if err != nil { + return err + } + codeKey, err := randomBytes(32) + if err != nil { + return err + } + if err := writeCodeManifest(dir, codeManifest{ + Version: 1, Active: codeID, + Keys: map[string]string{codeID: base64.RawURLEncoding.EncodeToString(codeKey)}, + }); err != nil { + return err + } + pepper, err := randomBytes(32) + if err != nil { + return err + } + if err := writeExclusive(filepath.Join(dir, "client-secret-pepper"), []byte(base64.RawURLEncoding.EncodeToString(pepper)+"\n")); err != nil { + return fmt.Errorf("write client-secret pepper: %w", err) + } + _, err = telegramlogin.LoadClientSecretPepper(filepath.Join(dir, "client-secret-pepper")) + return err +} + +func rotateSigning(dir string, algorithm domain.TelegramLoginSigningAlgorithm, publishFor time.Duration, now time.Time) error { + if algorithm != domain.TelegramLoginSigningRS256 && algorithm != domain.TelegramLoginSigningES256 && algorithm != domain.TelegramLoginSigningEdDSA { + return errors.New("default keygen supports RS256, ES256, and EdDSA; ES256K requires an explicit jwx_es256k build and external JWK lifecycle") + } + path := filepath.Join(dir, "signing-keys.json") + var manifest signingManifest + if err := readStrictJSON(path, &manifest); err != nil { + return fmt.Errorf("read signing manifest: %w", err) + } + if manifest.Version != 1 || len(manifest.Keys) == 0 || len(manifest.Keys) >= 32 { + return errors.New("signing manifest version or key count is invalid") + } + foundActive := false + kept := make([]signingManifestKey, 0, len(manifest.Keys)+1) + for _, key := range manifest.Keys { + if !key.Active && key.PublishUntil != "" { + until, err := time.Parse(time.RFC3339, key.PublishUntil) + if err != nil { + return fmt.Errorf("parse retiring key %s: %w", key.KeyID, err) + } + if !now.Before(until) { + continue + } + } + if key.Algorithm == algorithm && key.Active { + if foundActive { + return fmt.Errorf("multiple active %s keys", algorithm) + } + foundActive = true + key.Active = false + key.PublishUntil = now.Add(publishFor).UTC().Format(time.RFC3339) + } + kept = append(kept, key) + } + if !foundActive { + return fmt.Errorf("no active %s key to rotate", algorithm) + } + entry, err := generateSigningKey(dir, algorithm, now) + if err != nil { + return err + } + manifest.Keys = append(kept, entry) + return writeSigningManifest(dir, manifest) +} + +func rotateCode(dir string, now time.Time) error { + path := filepath.Join(dir, "code-keys.json") + var manifest codeManifest + if err := readStrictJSON(path, &manifest); err != nil { + return fmt.Errorf("read code-key manifest: %w", err) + } + if manifest.Version != 1 || manifest.Active == "" || len(manifest.Keys) == 0 || len(manifest.Keys) >= 16 { + return errors.New("code-key manifest is invalid or at its 16-key safety limit") + } + keyID, err := newKeyID("code", now) + if err != nil { + return err + } + key, err := randomBytes(32) + if err != nil { + return err + } + manifest.Active = keyID + manifest.Keys[keyID] = base64.RawURLEncoding.EncodeToString(key) + return writeCodeManifest(dir, manifest) +} + +func generateSigningKey(dir string, algorithm domain.TelegramLoginSigningAlgorithm, now time.Time) (signingManifestKey, error) { + keyID, err := newKeyID(strings.ToLower(string(algorithm)), now) + if err != nil { + return signingManifestKey{}, err + } + var privateKey any + switch algorithm { + case domain.TelegramLoginSigningRS256: + privateKey, err = rsa.GenerateKey(rand.Reader, 3072) + case domain.TelegramLoginSigningES256: + privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case domain.TelegramLoginSigningEdDSA: + _, privateKey, err = ed25519.GenerateKey(rand.Reader) + default: + return signingManifestKey{}, fmt.Errorf("unsupported keygen algorithm %s", algorithm) + } + if err != nil { + return signingManifestKey{}, fmt.Errorf("generate %s key: %w", algorithm, err) + } + der, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return signingManifestKey{}, fmt.Errorf("marshal %s key: %w", algorithm, err) + } + filename := "signing-" + keyID + ".pem" + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}) + if err := writeExclusive(filepath.Join(dir, filename), pemBytes); err != nil { + return signingManifestKey{}, fmt.Errorf("write %s key: %w", algorithm, err) + } + return signingManifestKey{ + Algorithm: algorithm, KeyID: keyID, PrivateKeyFile: filename, Active: true, + }, nil +} + +func newKeyID(prefix string, now time.Time) (string, error) { + raw, err := randomBytes(8) + if err != nil { + return "", err + } + return fmt.Sprintf("%s-%s-%s", prefix, now.UTC().Format("20060102T150405Z"), base64.RawURLEncoding.EncodeToString(raw)), nil +} + +func randomBytes(size int) ([]byte, error) { + raw := make([]byte, size) + if _, err := rand.Read(raw); err != nil { + return nil, fmt.Errorf("read cryptographic randomness: %w", err) + } + return raw, nil +} + +func writeSigningManifest(dir string, manifest signingManifest) error { + return writeValidatedManifest(filepath.Join(dir, "signing-keys.json"), manifest, func(path string) error { + _, err := telegramlogin.LoadSigningKeyRing(path, time.Now) + return err + }) +} + +func writeCodeManifest(dir string, manifest codeManifest) error { + return writeValidatedManifest(filepath.Join(dir, "code-keys.json"), manifest, func(path string) error { + _, err := telegramlogin.LoadCodeSealer(path) + return err + }) +} + +func writeValidatedManifest(path string, value any, validate func(string) error) error { + data, err := json.MarshalIndent(value, "", " ") + if err != nil { + return fmt.Errorf("marshal manifest: %w", err) + } + data = append(data, '\n') + temp, err := os.CreateTemp(filepath.Dir(path), ".telegram-login-manifest-*") + if err != nil { + return fmt.Errorf("create temporary manifest: %w", err) + } + tempPath := temp.Name() + defer func() { _ = os.Remove(tempPath) }() + if err := temp.Chmod(0o600); err != nil { + _ = temp.Close() + return err + } + if _, err := temp.Write(data); err != nil { + _ = temp.Close() + return err + } + if err := temp.Sync(); err != nil { + _ = temp.Close() + return err + } + if err := temp.Close(); err != nil { + return err + } + if err := validate(tempPath); err != nil { + return fmt.Errorf("validate generated manifest: %w", err) + } + if err := os.Rename(tempPath, path); err != nil { + return fmt.Errorf("atomically replace manifest: %w", err) + } + return nil +} + +func writeExclusive(path string, data []byte) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return err + } + ok := false + defer func() { + _ = file.Close() + if !ok { + _ = os.Remove(path) + } + }() + if _, err := file.Write(data); err != nil { + return err + } + if err := file.Sync(); err != nil { + return err + } + ok = true + return nil +} + +func readStrictJSON(path string, target any) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + info, err := file.Stat() + if err != nil { + return err + } + if !info.Mode().IsRegular() || info.Size() > maxManifestBytes { + return errors.New("manifest must be a bounded regular file") + } + decoder := json.NewDecoder(io.LimitReader(file, maxManifestBytes+1)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(target); err != nil { + return err + } + var extra any + if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) { + if err == nil { + return errors.New("manifest contains multiple JSON values") + } + return err + } + return nil +} diff --git a/cmd/telegramloginkeygen/main_test.go b/cmd/telegramloginkeygen/main_test.go new file mode 100644 index 00000000..3cf8dcc0 --- /dev/null +++ b/cmd/telegramloginkeygen/main_test.go @@ -0,0 +1,109 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + telegramlogin "telesrv/internal/app/telegramlogin" + "telesrv/internal/domain" +) + +func TestInitializeAndRotateKeyFiles(t *testing.T) { + dir := t.TempDir() + now := time.Date(2026, 7, 21, 1, 2, 3, 0, time.UTC) + if err := run(options{mode: "init", dir: dir, now: func() time.Time { return now }}); err != nil { + t.Fatal(err) + } + ring, err := telegramlogin.LoadSigningKeyRing(filepath.Join(dir, "signing-keys.json"), func() time.Time { return now }) + if err != nil { + t.Fatal(err) + } + if got := ring.SupportedAlgorithms(); len(got) != 3 || got[0] != "RS256" || got[1] != "ES256" || got[2] != "EdDSA" { + t.Fatalf("supported algorithms = %#v", got) + } + if _, err := telegramlogin.LoadCodeSealer(filepath.Join(dir, "code-keys.json")); err != nil { + t.Fatal(err) + } + if _, err := telegramlogin.LoadClientSecretPepper(filepath.Join(dir, "client-secret-pepper")); err != nil { + t.Fatal(err) + } + if err := run(options{mode: "init", dir: dir, now: func() time.Time { return now }}); err == nil { + t.Fatal("second initialization unexpectedly overwrote keys") + } + + later := now.Add(time.Minute) + if err := run(options{ + mode: "rotate-signing", dir: dir, algorithm: domain.TelegramLoginSigningRS256, + publishFor: 2 * time.Hour, idTokenTTL: time.Hour, now: func() time.Time { return later }, + }); err != nil { + t.Fatal(err) + } + var manifest signingManifest + readJSONForTest(t, filepath.Join(dir, "signing-keys.json"), &manifest) + active, retiring := 0, 0 + for _, key := range manifest.Keys { + if key.Algorithm != domain.TelegramLoginSigningRS256 { + continue + } + if key.Active { + active++ + } else if key.PublishUntil == later.Add(2*time.Hour).Format(time.RFC3339) { + retiring++ + } + } + if active != 1 || retiring != 1 { + t.Fatalf("RS256 active=%d retiring=%d manifest=%#v", active, retiring, manifest) + } + ring, err = telegramlogin.LoadSigningKeyRing(filepath.Join(dir, "signing-keys.json"), func() time.Time { return later }) + if err != nil { + t.Fatal(err) + } + jwks, _, err := ring.JWKS() + if err != nil { + t.Fatal(err) + } + var set struct { + Keys []json.RawMessage `json:"keys"` + } + if err := json.Unmarshal(jwks, &set); err != nil || len(set.Keys) != 4 { + t.Fatalf("JWKS key count=%d err=%v body=%s", len(set.Keys), err, jwks) + } + + var before codeManifest + readJSONForTest(t, filepath.Join(dir, "code-keys.json"), &before) + if err := run(options{mode: "rotate-code", dir: dir, now: func() time.Time { return later }}); err != nil { + t.Fatal(err) + } + var after codeManifest + readJSONForTest(t, filepath.Join(dir, "code-keys.json"), &after) + if after.Active == before.Active || len(after.Keys) != 2 { + t.Fatalf("code ring before=%#v after=%#v", before, after) + } + if _, err := telegramlogin.LoadCodeSealer(filepath.Join(dir, "code-keys.json")); err != nil { + t.Fatal(err) + } +} + +func TestRotateSigningRejectsTooShortRetirementWindow(t *testing.T) { + err := run(options{ + mode: "rotate-signing", dir: t.TempDir(), algorithm: domain.TelegramLoginSigningRS256, + publishFor: 69 * time.Minute, idTokenTTL: time.Hour, now: time.Now, + }) + if err == nil { + t.Fatal("short publish-for unexpectedly accepted") + } +} + +func readJSONForTest(t *testing.T, path string, target any) { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(data, target); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index 6df6a58a..b078525d 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -49,6 +49,7 @@ import ( "telesrv/internal/app/stargifts" "telesrv/internal/app/stars" storiesapp "telesrv/internal/app/stories" + telegramloginapp "telesrv/internal/app/telegramlogin" themesapp "telesrv/internal/app/themes" translationapp "telesrv/internal/app/translation" "telesrv/internal/app/updates" @@ -69,6 +70,7 @@ import ( "telesrv/internal/store/memory" "telesrv/internal/store/postgres" "telesrv/internal/store/redisstore" + "telesrv/internal/telegramloginhttp" "telesrv/internal/turnsrv" "telesrv/internal/web" ) @@ -346,12 +348,60 @@ func run(logger *zap.Logger) error { } defer pool.Close() + var telegramLoginService *telegramloginapp.Service + var telegramLoginIDTokens *telegramloginapp.IDTokenIssuer + var telegramLoginHTTPHandler http.Handler + if cfg.TelegramLoginEnabled { + codeSealer, err := telegramloginapp.LoadCodeSealer(cfg.TelegramLoginCodeKeysFile) + if err != nil { + return fmt.Errorf("load telegram login code keys: %w", err) + } + clientSecretPepper, err := telegramloginapp.LoadClientSecretPepper(cfg.TelegramLoginSecretPepperFile) + if err != nil { + return fmt.Errorf("load telegram login client-secret pepper: %w", err) + } + signingKeys, err := telegramloginapp.LoadSigningKeyRing(cfg.TelegramLoginSigningKeysFile, time.Now) + if err != nil { + return fmt.Errorf("load telegram login signing keys: %w", err) + } + telegramLoginService, err = telegramloginapp.NewService(postgres.NewTelegramLoginStore(pool), codeSealer, telegramloginapp.Config{ + Issuer: cfg.TelegramLoginIssuer, AppScheme: cfg.PublicAppScheme, + AllowLoopbackHTTP: cfg.TelegramLoginAllowLoopbackHTTP, + ClientSecretPepper: clientSecretPepper, + SupportedSigningAlgorithms: signingKeys.ActiveAlgorithms(), + RequestTTL: cfg.TelegramLoginRequestTTL, CodeTTL: cfg.TelegramLoginCodeTTL, + }) + if err != nil { + return fmt.Errorf("initialize telegram login service: %w", err) + } + telegramLoginIDTokens, err = telegramloginapp.NewIDTokenIssuer(signingKeys, telegramloginapp.IDTokenIssuerConfig{ + Issuer: cfg.TelegramLoginIssuer, TTL: cfg.TelegramLoginIDTokenTTL, + }) + if err != nil { + return fmt.Errorf("initialize telegram login ID-token issuer: %w", err) + } + } + rdb, err := redisstore.Open(ctx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB) if err != nil { return fmt.Errorf("connect redis: %w", err) } defer func() { _ = rdb.Close() }() logger.Info("持久化依赖就绪", zap.String("redis", cfg.RedisAddr)) + if cfg.TelegramLoginEnabled { + telegramLoginHTTPHandler, err = telegramloginhttp.NewHandler(telegramloginhttp.Config{ + Service: telegramLoginService, Tokens: telegramLoginIDTokens, + Limiter: redisstore.NewRateLimiter(rdb), AppName: cfg.PublicAppName, + Logger: logger.Named("telegram-login-http"), TrustedProxyCIDRs: cfg.TelegramLoginTrustedProxyCIDRs, + AllowLoopbackHTTP: cfg.TelegramLoginAllowLoopbackHTTP, + }) + if err != nil { + return fmt.Errorf("initialize telegram login HTTP provider: %w", err) + } + logger.Info("Telegram Login/OIDC provider enabled", + zap.String("issuer", telegramLoginIDTokens.Issuer()), + zap.Strings("signing_algorithms", telegramLoginIDTokens.SupportedAlgorithms())) + } authKeyStore := postgres.NewAuthKeyStore(pool) userStore := postgres.NewUserStore(pool) @@ -585,6 +635,7 @@ func run(logger *zap.Logger) error { botsapp.WithUserCache(userCache), botsapp.WithStickerSetCreator(filesService), botsapp.WithUserStickerSets(accountService), + botsapp.WithTelegramLogin(telegramLoginService), botsapp.WithPublicBaseURL(cfg.PublicBaseURL)) groupCallStore := postgres.NewGroupCallStore(pool) groupCallsService := groupcallsapp.NewService(groupCallStore, groupcallsapp.WithPublicBaseURL(cfg.PublicBaseURL)) @@ -800,6 +851,7 @@ func run(logger *zap.Logger) error { EphemeralPush: ephemeralStore, EphemeralReports: ephemeralReportStore, Users: usersService, + TelegramLogin: telegramLoginService, Updates: updatesService, BootstrapUpdates: bootstrapUpdateStore, BotAPIUpdates: botAPIUpdateStore, @@ -886,6 +938,9 @@ func run(logger *zap.Logger) error { go activeSessions.RunPendingSweeper(ctx, time.Minute) go router.RunPremiumSweeper(ctx, cfg.PremiumSweepInterval, cfg.PremiumSweepBatch) go router.RunAccountLifecycle(ctx, time.Minute, 500) + if telegramLoginService != nil { + go runTelegramLoginRetention(ctx, telegramLoginService, cfg.TelegramLoginRetention, cfg.TelegramLoginSweepInterval, cfg.TelegramLoginSweepBatch, logger.Named("telegram-login-retention")) + } go func() { interval := cfg.StarGiftSweepInterval if interval <= 0 { @@ -934,6 +989,7 @@ func run(logger *zap.Logger) error { Photos: filesService, UniqueGifts: giftsService, GiftWithdrawals: giftsService, + TelegramLogin: telegramLoginHTTPHandler, }, logger.Named("public-web")); err != nil { return fmt.Errorf("start public Web: %w", err) } @@ -984,3 +1040,38 @@ func run(logger *zap.Logger) error { // public listener so no seed/prewarm work can run after port 2398 is exposed. return srv.ListenAndServe(ctx, cfg.ListenAddr) } + +func runTelegramLoginRetention(ctx context.Context, service *telegramloginapp.Service, retention, interval time.Duration, batch int, logger *zap.Logger) { + run := func() { + var total int64 + // Bound one tick even when a deployment accumulated years of stale data; + // subsequent ticks continue without monopolizing the database pool. + for range 10 { + deleted, err := service.DeleteExpiredArtifacts(ctx, time.Now().UTC().Add(-retention), batch) + if err != nil { + if ctx.Err() == nil { + logger.Warn("telegram_login_retention_failed", zap.Error(err)) + } + return + } + total += deleted + if deleted < int64(batch) { + break + } + } + if total > 0 { + logger.Info("telegram_login_retention_completed", zap.Int64("deleted", total)) + } + } + run() + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + run() + } + } +} diff --git a/deploy/migrations/0125_telegram_login_oidc.down.sql b/deploy/migrations/0125_telegram_login_oidc.down.sql new file mode 100644 index 00000000..4e01d998 --- /dev/null +++ b/deploy/migrations/0125_telegram_login_oidc.down.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS public.telegram_login_codes; +DROP TABLE IF EXISTS public.web_authorizations; +DROP TABLE IF EXISTS public.telegram_login_requests; +DROP TABLE IF EXISTS public.bot_login_native_apps; +DROP TABLE IF EXISTS public.bot_login_allowed_urls; +DROP TABLE IF EXISTS public.bot_login_clients; +UPDATE public.bots +SET commands = COALESCE(( + SELECT jsonb_agg(command ORDER BY ordinal) + FROM jsonb_array_elements(commands) WITH ORDINALITY AS item(command, ordinal) + WHERE command->>'command' NOT IN ('setlogin','logininfo','resetloginsecret') + ), '[]'::jsonb), + updated_at = now() +WHERE bot_user_id = 93372553; diff --git a/deploy/migrations/0125_telegram_login_oidc.up.sql b/deploy/migrations/0125_telegram_login_oidc.up.sql new file mode 100644 index 00000000..6f389be2 --- /dev/null +++ b/deploy/migrations/0125_telegram_login_oidc.up.sql @@ -0,0 +1,253 @@ +-- Telegram Login / OIDC is one durable authorization aggregate shared by the +-- public HTTP provider and MTProto URL-auth RPCs. PostgreSQL is authoritative; +-- Redis/NOTIFY may wake waiters but may not own any transition below. +UPDATE public.bots +SET commands = commands || '[ + {"command":"setlogin","description":"configure Telegram Login"}, + {"command":"logininfo","description":"show Telegram Login configuration"}, + {"command":"resetloginsecret","description":"rotate an OIDC Client Secret"} +]'::jsonb, + updated_at = now() +WHERE bot_user_id = 93372553; + +CREATE TABLE public.bot_login_clients ( + bot_user_id bigint PRIMARY KEY REFERENCES public.bots(bot_user_id) ON DELETE CASCADE, + client_id text NOT NULL UNIQUE, + client_secret_hash bytea NOT NULL, + secret_version bigint DEFAULT 1 NOT NULL, + signing_algorithm text DEFAULT 'RS256'::text NOT NULL, + enabled boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT bot_login_clients_client_id_check + CHECK (client_id = bot_user_id::text AND length(client_id) BETWEEN 1 AND 64), + CONSTRAINT bot_login_clients_secret_hash_check CHECK (octet_length(client_secret_hash) = 32), + CONSTRAINT bot_login_clients_secret_version_check CHECK (secret_version > 0), + CONSTRAINT bot_login_clients_signing_algorithm_check + CHECK (signing_algorithm IN ('RS256','ES256','EdDSA','ES256K')) +); + +CREATE TABLE public.bot_login_allowed_urls ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + bot_user_id bigint NOT NULL REFERENCES public.bot_login_clients(bot_user_id) ON DELETE CASCADE, + kind text NOT NULL, + normalized_url text NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT bot_login_allowed_urls_kind_check CHECK (kind IN ('web_origin','redirect_uri')), + CONSTRAINT bot_login_allowed_urls_value_check CHECK (length(normalized_url) BETWEEN 1 AND 4096), + UNIQUE (bot_user_id, kind, normalized_url) +); + +CREATE INDEX bot_login_allowed_urls_bot_page_idx + ON public.bot_login_allowed_urls(bot_user_id, kind, id); + +CREATE TABLE public.bot_login_native_apps ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + bot_user_id bigint NOT NULL REFERENCES public.bot_login_clients(bot_user_id) ON DELETE CASCADE, + platform text NOT NULL, + application_id text NOT NULL, + verification_id text NOT NULL, + callback_uri text NOT NULL, + verified_display_name text NOT NULL, + enabled boolean DEFAULT true NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT bot_login_native_apps_platform_check CHECK (platform IN ('ios','android')), + CONSTRAINT bot_login_native_apps_app_id_check + CHECK (length(application_id) BETWEEN 3 AND 255 AND application_id ~ '^[A-Za-z0-9][A-Za-z0-9._-]*$'), + CONSTRAINT bot_login_native_apps_verification_check CHECK ( + (platform = 'ios' AND verification_id ~ '^[A-Z0-9]{10}$') + OR (platform = 'android' AND verification_id ~ '^[0-9A-F]{64}$') + ), + CONSTRAINT bot_login_native_apps_callback_check CHECK (length(callback_uri) BETWEEN 1 AND 4096), + CONSTRAINT bot_login_native_apps_name_check CHECK (length(btrim(verified_display_name)) BETWEEN 1 AND 128), + UNIQUE (bot_user_id, platform, application_id, verification_id), + UNIQUE (bot_user_id, callback_uri) +); + +CREATE INDEX bot_login_native_apps_bot_page_idx + ON public.bot_login_native_apps(bot_user_id, id); +CREATE INDEX bot_login_native_apps_callback_idx + ON public.bot_login_native_apps(bot_user_id, callback_uri) WHERE enabled; + +CREATE TABLE public.telegram_login_requests ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + request_token_hash bytea NOT NULL UNIQUE, + browser_token_hash bytea NOT NULL UNIQUE, + bot_user_id bigint NOT NULL REFERENCES public.bot_login_clients(bot_user_id) ON DELETE CASCADE, + client_id text NOT NULL, + signing_algorithm text NOT NULL, + source text NOT NULL, + response_type text NOT NULL, + redirect_uri text NOT NULL, + origin text DEFAULT ''::text NOT NULL, + domain text NOT NULL, + requested_scopes text[] NOT NULL, + oauth_state text DEFAULT ''::text NOT NULL, + nonce text DEFAULT ''::text NOT NULL, + code_challenge text NOT NULL, + code_challenge_method text NOT NULL, + browser text NOT NULL, + platform text NOT NULL, + ip text NOT NULL, + region text NOT NULL, + in_app_origin text DEFAULT ''::text NOT NULL, + is_app boolean DEFAULT false NOT NULL, + verified_app_name text DEFAULT ''::text NOT NULL, + match_codes text[] DEFAULT '{}'::text[] NOT NULL, + match_code text DEFAULT ''::text NOT NULL, + match_codes_first boolean DEFAULT false NOT NULL, + user_id_hint bigint DEFAULT 0 NOT NULL, + peer_type text DEFAULT ''::text NOT NULL, + peer_id bigint DEFAULT 0 NOT NULL, + message_id integer DEFAULT 0 NOT NULL, + button_id integer DEFAULT 0 NOT NULL, + status text DEFAULT 'pending'::text NOT NULL, + authorized_user_id bigint REFERENCES public.users(id), + profile_name text DEFAULT ''::text NOT NULL, + given_name text DEFAULT ''::text NOT NULL, + family_name text DEFAULT ''::text NOT NULL, + preferred_username text DEFAULT ''::text NOT NULL, + picture text DEFAULT ''::text NOT NULL, + phone_number text DEFAULT ''::text NOT NULL, + write_allowed boolean DEFAULT false NOT NULL, + phone_shared boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + expires_at timestamp with time zone NOT NULL, + approved_at timestamp with time zone, + declined_at timestamp with time zone, + CONSTRAINT telegram_login_requests_hashes_check + CHECK (octet_length(request_token_hash) = 32 AND octet_length(browser_token_hash) = 32), + CONSTRAINT telegram_login_requests_signing_algorithm_check + CHECK (signing_algorithm IN ('RS256','ES256','EdDSA','ES256K')), + CONSTRAINT telegram_login_requests_source_check + CHECK (source IN ('web','javascript','native','mini_app','message_button')), + CONSTRAINT telegram_login_requests_response_type_check CHECK (response_type IN ('code','post_message','legacy_url')), + CONSTRAINT telegram_login_requests_source_response_check CHECK ( + (source = 'web' AND response_type = 'code') + OR (source = 'javascript' AND response_type = 'post_message') + OR (source = 'native' AND response_type = 'code') + OR (source = 'mini_app' AND response_type = 'post_message') + OR (source = 'message_button' AND response_type = 'legacy_url') + ), + CONSTRAINT telegram_login_requests_url_check + CHECK (length(redirect_uri) BETWEEN 1 AND 4096 AND length(origin) <= 4096 + AND length(domain) BETWEEN 1 AND 255 AND length(in_app_origin) <= 4096), + CONSTRAINT telegram_login_requests_scope_check + CHECK (cardinality(requested_scopes) BETWEEN 1 AND 4 AND requested_scopes @> ARRAY['openid']::text[]), + CONSTRAINT telegram_login_requests_oauth_value_check + CHECK (length(oauth_state) <= 2048 AND length(nonce) <= 1024), + CONSTRAINT telegram_login_requests_pkce_check CHECK ( + (response_type = 'code' AND code_challenge_method = 'S256' AND length(code_challenge) BETWEEN 43 AND 128) + OR (response_type = 'post_message' AND ( + (code_challenge = '' AND code_challenge_method = '') + OR (code_challenge_method = 'S256' AND length(code_challenge) BETWEEN 43 AND 128))) + OR (response_type = 'legacy_url' AND code_challenge = '' AND code_challenge_method = '') + ), + CONSTRAINT telegram_login_requests_device_check + CHECK (length(browser) BETWEEN 1 AND 255 AND length(platform) BETWEEN 1 AND 255 AND length(ip) BETWEEN 1 AND 128 AND length(region) BETWEEN 1 AND 255), + CONSTRAINT telegram_login_requests_match_codes_check + CHECK (cardinality(match_codes) <= 8 + AND (cardinality(match_codes) = 0 OR (match_code <> '' AND match_code = ANY(match_codes))) + AND (NOT match_codes_first OR cardinality(match_codes) > 0)), + CONSTRAINT telegram_login_requests_context_check + CHECK (user_id_hint >= 0 AND peer_id >= 0 AND message_id >= 0 AND button_id >= 0), + CONSTRAINT telegram_login_requests_app_shape_check CHECK ( + (source = 'native' AND is_app AND verified_app_name <> '' AND origin = '') + OR (source <> 'native' AND NOT is_app AND verified_app_name = '' AND origin <> '') + ), + CONSTRAINT telegram_login_requests_in_app_shape_check CHECK ( + (source = 'mini_app' AND response_type = 'post_message' + AND in_app_origin <> '' AND origin = in_app_origin) + OR (source <> 'mini_app' AND in_app_origin = '') + ), + CONSTRAINT telegram_login_requests_consent_scope_check CHECK ( + (NOT write_allowed OR 'telegram:bot_access' = ANY(requested_scopes)) + AND (NOT phone_shared OR 'phone' = ANY(requested_scopes)) + AND ((phone_shared AND phone_number <> '') OR (NOT phone_shared AND phone_number = '')) + ), + CONSTRAINT telegram_login_requests_status_check CHECK (status IN ('pending','approved','declined','expired')), + CONSTRAINT telegram_login_requests_claims_check CHECK ( + length(profile_name) <= 255 AND length(given_name) <= 255 AND length(family_name) <= 255 + AND length(preferred_username) <= 64 AND length(picture) <= 4096 AND length(phone_number) <= 32 + ), + CONSTRAINT telegram_login_requests_time_check CHECK (expires_at > created_at), + CONSTRAINT telegram_login_requests_terminal_shape_check CHECK ( + (status = 'pending' AND authorized_user_id IS NULL AND profile_name = '' AND given_name = '' + AND family_name = '' AND preferred_username = '' AND picture = '' AND phone_number = '' + AND NOT write_allowed AND NOT phone_shared AND approved_at IS NULL AND declined_at IS NULL) + OR (status = 'approved' AND authorized_user_id IS NOT NULL AND approved_at IS NOT NULL AND declined_at IS NULL + AND (('profile' = ANY(requested_scopes) AND profile_name <> '' AND given_name <> '') + OR (NOT ('profile' = ANY(requested_scopes)) AND profile_name = '' AND given_name = '' + AND family_name = '' AND preferred_username = '' AND picture = ''))) + OR (status = 'declined' AND authorized_user_id IS NULL AND profile_name = '' AND given_name = '' + AND family_name = '' AND preferred_username = '' AND picture = '' AND phone_number = '' + AND NOT write_allowed AND NOT phone_shared AND approved_at IS NULL AND declined_at IS NOT NULL) + OR (status = 'expired' AND authorized_user_id IS NULL AND profile_name = '' AND given_name = '' + AND family_name = '' AND preferred_username = '' AND picture = '' AND phone_number = '' + AND NOT write_allowed AND NOT phone_shared AND approved_at IS NULL AND declined_at IS NULL) + ) +); + +CREATE INDEX telegram_login_requests_expiry_idx + ON public.telegram_login_requests(expires_at, id) WHERE status = 'pending'; +CREATE INDEX telegram_login_requests_user_active_idx + ON public.telegram_login_requests(authorized_user_id, approved_at DESC, id DESC) + WHERE status = 'approved'; + +CREATE TABLE public.web_authorizations ( + hash bigint PRIMARY KEY, + request_id bigint NOT NULL UNIQUE REFERENCES public.telegram_login_requests(id) ON DELETE CASCADE, + user_id bigint NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + bot_user_id bigint NOT NULL REFERENCES public.bots(bot_user_id) ON DELETE CASCADE, + domain text NOT NULL, + browser text NOT NULL, + platform text NOT NULL, + ip text NOT NULL, + region text NOT NULL, + granted_scopes text[] NOT NULL, + phone_shared boolean DEFAULT false NOT NULL, + bot_access_granted boolean DEFAULT false NOT NULL, + created_at timestamp with time zone NOT NULL, + last_active_at timestamp with time zone NOT NULL, + revoked_at timestamp with time zone, + CONSTRAINT web_authorizations_hash_check CHECK (hash <> 0), + CONSTRAINT web_authorizations_identity_check CHECK (user_id > 0 AND bot_user_id > 0), + CONSTRAINT web_authorizations_text_check + CHECK (length(domain) BETWEEN 1 AND 255 AND length(browser) BETWEEN 1 AND 255 + AND length(platform) BETWEEN 1 AND 255 AND length(ip) BETWEEN 1 AND 128 + AND length(region) BETWEEN 1 AND 255), + CONSTRAINT web_authorizations_scope_check + CHECK (cardinality(granted_scopes) BETWEEN 1 AND 4 AND granted_scopes @> ARRAY['openid']::text[] + AND (NOT phone_shared OR 'phone' = ANY(granted_scopes)) + AND (NOT bot_access_granted OR 'telegram:bot_access' = ANY(granted_scopes))), + CONSTRAINT web_authorizations_time_check + CHECK (last_active_at >= created_at AND (revoked_at IS NULL OR revoked_at >= created_at)) +); + +CREATE INDEX web_authorizations_user_active_page_idx + ON public.web_authorizations(user_id, last_active_at DESC, hash DESC) + WHERE revoked_at IS NULL; +CREATE INDEX web_authorizations_bot_active_idx + ON public.web_authorizations(bot_user_id, user_id, hash) + WHERE revoked_at IS NULL; + +CREATE TABLE public.telegram_login_codes ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + request_id bigint NOT NULL UNIQUE REFERENCES public.telegram_login_requests(id) ON DELETE CASCADE, + code_hash bytea NOT NULL UNIQUE, + sealed_code bytea NOT NULL, + seal_nonce bytea NOT NULL, + seal_key_id text NOT NULL, + issued_at timestamp with time zone NOT NULL, + expires_at timestamp with time zone NOT NULL, + consumed_at timestamp with time zone, + CONSTRAINT telegram_login_codes_hash_check CHECK (octet_length(code_hash) = 32), + CONSTRAINT telegram_login_codes_sealed_check + CHECK (octet_length(sealed_code) >= 32 AND octet_length(seal_nonce) >= 12 AND length(seal_key_id) BETWEEN 1 AND 128), + CONSTRAINT telegram_login_codes_time_check + CHECK (expires_at > issued_at AND (consumed_at IS NULL OR (consumed_at >= issued_at AND consumed_at < expires_at))) +); + +CREATE INDEX telegram_login_codes_expiry_idx + ON public.telegram_login_codes(expires_at, id) WHERE consumed_at IS NULL; diff --git a/go.mod b/go.mod index 8dae72dc..21f7e95f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module telesrv go 1.25.0 require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/fxamacker/cbor/v2 v2.8.0 github.com/go-faster/errors v0.7.1 github.com/golang-migrate/migrate/v4 v4.19.1 @@ -11,6 +12,7 @@ require ( github.com/iamxvbaba/td v1.1.3 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jackc/pgx/v5 v5.9.2 + github.com/lestrrat-go/jwx/v3 v3.1.1 github.com/pion/datachannel v1.6.2 github.com/pion/dtls/v3 v3.1.4 github.com/pion/ice/v4 v4.2.7 @@ -43,6 +45,7 @@ require ( github.com/go-faster/jx v1.2.0 // indirect github.com/go-faster/xor v1.0.0 // indirect github.com/go-faster/yaml v0.4.6 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gotd/log v0.1.0 // indirect github.com/gotd/neo v0.1.5 // indirect @@ -52,6 +55,12 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.19.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.2.1 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect @@ -64,6 +73,7 @@ require ( github.com/segmentio/asm v1.2.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/valyala/fastjson v1.6.10 // indirect github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yuin/goldmark v1.8.2 // indirect diff --git a/go.sum b/go.sum index eeed5c28..d3c16d1d 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -57,6 +59,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= @@ -98,6 +102,20 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc= +github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM= +github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw= +github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -175,6 +193,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/internal/app/bots/botfather.go b/internal/app/bots/botfather.go index d49bdc62..d37d492f 100644 --- a/internal/app/bots/botfather.go +++ b/internal/app/bots/botfather.go @@ -12,6 +12,7 @@ import ( "go.uber.org/zap" + telegramloginapp "telesrv/internal/app/telegramlogin" "telesrv/internal/branding" "telesrv/internal/domain" ) @@ -33,6 +34,9 @@ const ( botFatherCmdSetInlineFB = "setinlinefeedback" botFatherCmdSetJoinGroups = "setjoingroups" botFatherCmdSetPrivacy = "setprivacy" + botFatherCmdSetLogin = "setlogin" + botFatherCmdLoginInfo = "logininfo" + botFatherCmdResetLogin = "resetloginsecret" botFatherStepName = "name" botFatherStepUsername = "username" @@ -60,6 +64,9 @@ You can control me by sending these commands: /setinlinefeedback - change inline feedback settings /setjoingroups - toggle whether a bot can join groups /setprivacy - toggle a bot's group privacy mode +/setlogin - configure Telegram Login allowed URLs and signing +/logininfo - show a bot's Telegram Login configuration +/resetloginsecret - rotate a bot's OIDC Client Secret /cancel - cancel the current operation /help - show this message` @@ -175,6 +182,7 @@ var botFatherGlobalCommands = map[string]bool{ botFatherCmdSetName: true, botFatherCmdSetDescription: true, botFatherCmdSetAbout: true, botFatherCmdSetCommands: true, botFatherCmdSetInline: true, botFatherCmdSetInlineGeo: true, botFatherCmdSetInlineFB: true, botFatherCmdSetJoinGroups: true, botFatherCmdSetPrivacy: true, + botFatherCmdSetLogin: true, botFatherCmdLoginInfo: true, botFatherCmdResetLogin: true, } func (s *Service) handleBotFather(ctx context.Context, userID int64, text string) botReply { @@ -231,6 +239,9 @@ var pickerPrompts = map[string]string{ botFatherCmdSetInlineGeo: "Choose a bot to change inline location requests for. Send the bot's username:", botFatherCmdSetJoinGroups: "Choose a bot to configure group joining for. Send the bot's username:", botFatherCmdSetPrivacy: "Choose a bot to configure group privacy for. Send the bot's username:", + botFatherCmdSetLogin: "Choose a bot to configure Telegram Login for. Send the bot's username:", + botFatherCmdLoginInfo: "Choose a bot whose Telegram Login configuration you want to inspect:", + botFatherCmdResetLogin: "Choose a bot whose OIDC Client Secret you want to rotate:", } // startBotPicker 列出 owner 的 bot 并进入 choose step(所有需先选 bot 的命令共用)。 @@ -322,7 +333,8 @@ func (s *Service) handleBotFatherCommand(ctx context.Context, userID int64, cmd case botFatherCmdToken, botFatherCmdRevoke, botFatherCmdSetName, botFatherCmdSetDescription, botFatherCmdSetAbout, botFatherCmdSetCommands, botFatherCmdSetInline, botFatherCmdSetInlineGeo, - botFatherCmdSetJoinGroups, botFatherCmdSetPrivacy: + botFatherCmdSetJoinGroups, botFatherCmdSetPrivacy, + botFatherCmdSetLogin, botFatherCmdLoginInfo, botFatherCmdResetLogin: return s.startBotPicker(ctx, userID, cmd) case botFatherCmdSetInlineFB: _ = s.bots.DeleteBotChatState(ctx, domain.BotFatherUserID, userID) @@ -351,6 +363,8 @@ func valuePrompt(cmd, username string) string { return fmt.Sprintf("Send 'enable' to allow @%s to be added to groups, or 'disable' to prevent it.", username) case botFatherCmdSetPrivacy: return fmt.Sprintf("Send 'enable' to turn ON group privacy for @%s (it will only receive commands and replies), or 'disable' to let it receive all group messages.", username) + case botFatherCmdSetLogin: + return telegramLoginConfigurationPrompt(username) default: return "Send the new value, or /cancel." } @@ -445,6 +459,61 @@ func (s *Service) handleChooseBot(ctx context.Context, state domain.BotChatState } head := fmt.Sprintf("Token for @%s has been revoked. The old token will stop working immediately. New token:\n", chosen.Username) return tokenReply(head, token, "\n\nKeep your token secure and store it safely, it can be used by anyone to control your bot.") + case botFatherCmdLoginInfo: + defer s.clearState(ctx, state.UserID) + if s.telegramLogin == nil { + return botReply{Text: "Telegram Login is not enabled on this server."} + } + configuration, found, err := s.telegramLogin.ClientConfiguration(ctx, chosen.ID) + if err != nil { + s.log.Error("botfather: get telegram login configuration", zap.Int64("bot_user_id", chosen.ID), zap.Error(err)) + return internalReply() + } + if !found { + return botReply{Text: fmt.Sprintf("Telegram Login is not configured for @%s. Use /setlogin to create it.", chosen.Username)} + } + return botReply{Text: formatTelegramLoginConfiguration(chosen.Username, configuration)} + case botFatherCmdResetLogin: + defer s.clearState(ctx, state.UserID) + if s.telegramLogin == nil { + return botReply{Text: "Telegram Login is not enabled on this server."} + } + credentials, err := s.telegramLogin.RotateClientSecret(ctx, chosen.ID) + if errors.Is(err, domain.ErrTelegramLoginClientInvalid) { + return botReply{Text: fmt.Sprintf("Telegram Login is not configured for @%s. Use /setlogin first.", chosen.Username)} + } + if err != nil { + s.log.Error("botfather: rotate telegram login secret", zap.Int64("bot_user_id", chosen.ID), zap.Error(err)) + return internalReply() + } + head := fmt.Sprintf("The previous OIDC Client Secret for @%s is now invalid. Save this new secret; it will only be shown once:\n", chosen.Username) + return tokenReply(head, credentials.Secret, "\n\nClient ID: "+credentials.Client.ClientID) + case botFatherCmdSetLogin: + if s.telegramLogin == nil { + s.clearState(ctx, state.UserID) + return botReply{Text: "Telegram Login is not enabled on this server."} + } + credentials, created, err := s.telegramLogin.EnsureClient(ctx, chosen.ID) + if err != nil { + s.log.Error("botfather: ensure telegram login client", zap.Int64("bot_user_id", chosen.ID), zap.Error(err)) + return internalReply() + } + state.Step = botFatherStepValue + if state.Draft == nil { + state.Draft = map[string]string{} + } + state.Draft[botFatherDraftBotID] = strconv.FormatInt(chosen.ID, 10) + state.Draft[botFatherDraftBotUsername] = chosen.Username + if err := s.bots.UpsertBotChatState(ctx, state); err != nil { + s.log.Error("botfather: save telegram login state", zap.Int64("user_id", state.UserID), zap.Error(err)) + return internalReply() + } + prompt := telegramLoginConfigurationPrompt(chosen.Username) + if !created { + return botReply{Text: fmt.Sprintf("Telegram Login client %s is ready for @%s.\n\n%s", credentials.Client.ClientID, chosen.Username, prompt)} + } + head := fmt.Sprintf("Telegram Login is now enabled for @%s.\nClient ID: %s\nSave this Client Secret; it will only be shown once:\n", chosen.Username, credentials.Client.ClientID) + return tokenReply(head, credentials.Secret, "\n\n"+prompt) case botFatherCmdSetName, botFatherCmdSetDescription, botFatherCmdSetAbout, botFatherCmdSetCommands, botFatherCmdSetInline, botFatherCmdSetInlineGeo, botFatherCmdSetJoinGroups, botFatherCmdSetPrivacy: @@ -507,6 +576,8 @@ func (s *Service) handleSetValue(ctx context.Context, state domain.BotChatState, reply, err = s.applyToggle(ctx, botID, text, true) case botFatherCmdSetPrivacy: reply, err = s.applyToggle(ctx, botID, text, false) + case botFatherCmdSetLogin: + reply, err = s.applyTelegramLoginConfiguration(ctx, botID, username, text) default: s.clearState(ctx, state.UserID) return internalReply() @@ -583,6 +654,156 @@ func (s *Service) applySetInlineGeo(ctx context.Context, botID int64, text strin return botReply{Text: fmt.Sprintf("Success! Inline location requests are now %s.", state)}, nil } +func telegramLoginConfigurationPrompt(username string) string { + return fmt.Sprintf(`Send one configuration command for @%s: + +add origin https://example.com +add redirect https://example.com/auth/callback +add ios com.example.app ABCDE12345 exampleapp://tglogin Example iOS App +add android com.example.app AA:BB:...:FF exampleapp://telegram-login Example Android App +remove origin https://example.com +remove redirect https://example.com/auth/callback +remove app 12 +algorithm RS256|ES256|EdDSA|ES256K +enable +disable + +Origins authorize the JS SDK and legacy login_url buttons. Redirects are exact OIDC callbacks. Run /logininfo to inspect the result or /cancel to stop.`, username) +} + +func formatTelegramLoginConfiguration(username string, configuration telegramloginapp.ClientConfiguration) string { + status := "disabled" + if configuration.Client.Enabled { + status = "enabled" + } + var out strings.Builder + fmt.Fprintf(&out, "Telegram Login for @%s\nClient ID: %s\nStatus: %s\nSigning algorithm: %s\nSecret version: %d", + username, configuration.Client.ClientID, status, configuration.Client.SigningAlgorithm, configuration.Client.SecretVersion) + if len(configuration.AllowedURLs) == 0 { + out.WriteString("\nAllowed URLs: none") + } else { + out.WriteString("\nAllowed URLs:") + for _, allowed := range configuration.AllowedURLs { + fmt.Fprintf(&out, "\n- %s %s", allowed.Kind, allowed.NormalizedURL) + } + } + if len(configuration.NativeApps) > 0 { + out.WriteString("\nNative apps:") + for _, app := range configuration.NativeApps { + fmt.Fprintf(&out, "\n- #%d %s %s [%s] -> %s (%s)", app.ID, app.Platform, app.ApplicationID, app.VerificationID, app.CallbackURI, app.VerifiedDisplayName) + } + } + return out.String() +} + +func telegramLoginAllowedURLKind(raw string) (domain.TelegramLoginAllowedURLKind, bool) { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "origin": + return domain.TelegramLoginAllowedWebOrigin, true + case "redirect": + return domain.TelegramLoginAllowedRedirectURI, true + default: + return "", false + } +} + +func telegramLoginSigningAlgorithm(raw string) (domain.TelegramLoginSigningAlgorithm, bool) { + switch strings.ToUpper(strings.TrimSpace(raw)) { + case "RS256": + return domain.TelegramLoginSigningRS256, true + case "ES256": + return domain.TelegramLoginSigningES256, true + case "EDDSA": + return domain.TelegramLoginSigningEdDSA, true + case "ES256K": + return domain.TelegramLoginSigningES256K, true + default: + return "", false + } +} + +func (s *Service) applyTelegramLoginConfiguration(ctx context.Context, botID int64, username, text string) (botReply, error) { + if s.telegramLogin == nil { + return botReply{Text: "Telegram Login is not enabled on this server."}, domain.ErrTelegramLoginClientDisabled + } + fields := strings.Fields(strings.TrimSpace(text)) + if len(fields) == 1 { + switch strings.ToLower(fields[0]) { + case "enable": + if err := s.telegramLogin.SetClientEnabled(ctx, botID, true); err != nil { + return botReply{}, err + } + return botReply{Text: fmt.Sprintf("Telegram Login is enabled for @%s. Use /setlogin for another change or /logininfo to review it.", username)}, nil + case "disable": + if err := s.telegramLogin.SetClientEnabled(ctx, botID, false); err != nil { + return botReply{}, err + } + return botReply{Text: fmt.Sprintf("Telegram Login is disabled for @%s. Pending requests can no longer be approved or exchanged.", username)}, nil + } + } + if len(fields) == 2 && strings.EqualFold(fields[0], "algorithm") { + algorithm, ok := telegramLoginSigningAlgorithm(fields[1]) + if !ok { + return botReply{Text: "Unknown signing algorithm. Use RS256, ES256, EdDSA or ES256K, or /cancel."}, domain.ErrTelegramLoginClientInvalid + } + if _, err := s.telegramLogin.SetClientSigningAlgorithm(ctx, botID, algorithm); err != nil { + if errors.Is(err, domain.ErrTelegramLoginClientInvalid) { + return botReply{Text: fmt.Sprintf("%s is not available on this server because no active signing key is configured for it. Choose another algorithm or ask the operator to rotate the key ring.", algorithm)}, err + } + return botReply{}, err + } + return botReply{Text: fmt.Sprintf("Success! New ID tokens for @%s will use %s. EdDSA and ES256K accept only the openid scope.", username, algorithm)}, nil + } + if len(fields) == 3 && (strings.EqualFold(fields[0], "add") || strings.EqualFold(fields[0], "remove")) && + (strings.EqualFold(fields[1], "origin") || strings.EqualFold(fields[1], "redirect")) { + kind, ok := telegramLoginAllowedURLKind(fields[1]) + if !ok { + return botReply{Text: "URL kind must be origin or redirect. Try again or /cancel."}, domain.ErrTelegramLoginURLInvalid + } + if strings.EqualFold(fields[0], "add") { + allowed, err := s.telegramLogin.AddAllowedURL(ctx, botID, kind, fields[2]) + if err != nil { + return botReply{Text: "That URL is not allowed. Use an exact HTTPS URL without credentials, fragments or reserved OAuth query fields."}, err + } + return botReply{Text: fmt.Sprintf("Success! Added %s for @%s:\n%s", allowed.Kind, username, allowed.NormalizedURL)}, nil + } + deleted, err := s.telegramLogin.DeleteAllowedURL(ctx, botID, kind, fields[2]) + if err != nil { + return botReply{Text: "That URL is invalid. Try again or /cancel."}, err + } + if !deleted { + return botReply{Text: "That exact URL was not registered. Check /logininfo and try again."}, domain.ErrTelegramLoginURLInvalid + } + return botReply{Text: fmt.Sprintf("Success! Removed %s from @%s.", kind, username)}, nil + } + if len(fields) >= 6 && strings.EqualFold(fields[0], "add") && (strings.EqualFold(fields[1], "ios") || strings.EqualFold(fields[1], "android")) { + platform := domain.TelegramLoginNativeIOS + if strings.EqualFold(fields[1], "android") { + platform = domain.TelegramLoginNativeAndroid + } + app, err := s.telegramLogin.AddNativeApp(ctx, botID, platform, fields[2], fields[3], fields[4], strings.Join(fields[5:], " ")) + if err != nil { + return botReply{Text: "Invalid native app registration. iOS needs Bundle ID + 10-character Team ID; Android needs package name + SHA-256 signing fingerprint. Use an exact HTTPS callback or a custom scheme://host callback."}, err + } + return botReply{Text: fmt.Sprintf("Success! Registered native app #%d for @%s:\n%s %s -> %s", app.ID, username, app.Platform, app.ApplicationID, app.CallbackURI)}, nil + } + if len(fields) == 3 && strings.EqualFold(fields[0], "remove") && strings.EqualFold(fields[1], "app") { + appID, err := strconv.ParseInt(fields[2], 10, 64) + if err != nil || appID <= 0 { + return botReply{Text: "Native app ID must be the positive number shown by /logininfo."}, domain.ErrTelegramLoginClientInvalid + } + deleted, err := s.telegramLogin.DeleteNativeApp(ctx, botID, appID) + if err != nil { + return botReply{}, err + } + if !deleted { + return botReply{Text: "That native app was not registered for this bot. Check /logininfo."}, domain.ErrTelegramLoginClientInvalid + } + return botReply{Text: fmt.Sprintf("Success! Removed native app #%d from @%s.", appID, username)}, nil + } + return botReply{Text: telegramLoginConfigurationPrompt(username)}, domain.ErrTelegramLoginRequestInvalid +} + // applyToggle 解析 enable/disable 并设置 joingroups(join=true)或 privacy(join=false)。 func (s *Service) applyToggle(ctx context.Context, botID int64, text string, join bool) (botReply, error) { var on bool diff --git a/internal/app/bots/botfather_login_test.go b/internal/app/bots/botfather_login_test.go new file mode 100644 index 00000000..8a77f63c --- /dev/null +++ b/internal/app/bots/botfather_login_test.go @@ -0,0 +1,100 @@ +package bots + +import ( + "context" + "strconv" + "strings" + "testing" + "time" + + telegramloginapp "telesrv/internal/app/telegramlogin" + "telesrv/internal/store/memory" +) + +func newBotFatherTelegramLoginService(t *testing.T) *telegramloginapp.Service { + t.Helper() + sealKey := make([]byte, 32) + sealKey[0] = 1 + sealer, err := telegramloginapp.NewCodeSealer("test", map[string][]byte{"test": sealKey}) + if err != nil { + t.Fatal(err) + } + pepper := make([]byte, 32) + pepper[0] = 2 + service, err := telegramloginapp.NewService(memory.NewTelegramLoginStore(nil), sealer, telegramloginapp.Config{ + Issuer: "http://localhost:2404", AppScheme: "telesrv", AllowLoopbackHTTP: true, + ClientSecretPepper: pepper, Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() }, + }) + if err != nil { + t.Fatal(err) + } + return service +} + +func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) { + svc, users, _, messages := newTestService(t) + svc.telegramLogin = newBotFatherTelegramLoginService(t) + owner := newOwner(t, users, "+1090") + bot, _, err := svc.CreateBot(context.Background(), owner.ID, "Login Demo", "login_demo_bot") + if err != nil { + t.Fatal(err) + } + + if reply := sendToBotFather(t, svc, messages, owner, "/setlogin"); !strings.Contains(reply, "Choose a bot") { + t.Fatalf("/setlogin reply = %q", reply) + } + created := sendToBotFather(t, svc, messages, owner, "@login_demo_bot") + if !strings.Contains(created, "Client ID: "+strconv.FormatInt(bot.ID, 10)) || !strings.Contains(created, "only be shown once") { + t.Fatalf("create login reply = %q", created) + } + secretMarker := "only be shown once:\n" + secret := strings.SplitN(strings.SplitN(created, secretMarker, 2)[1], "\n", 2)[0] + if len(secret) < 32 { + t.Fatalf("client secret is unexpectedly short: %q", secret) + } + if reply := sendToBotFather(t, svc, messages, owner, "add origin http://localhost:3000"); !strings.Contains(reply, "Success!") { + t.Fatalf("add origin reply = %q", reply) + } + + sendToBotFather(t, svc, messages, owner, "/setlogin") + sendToBotFather(t, svc, messages, owner, "login_demo_bot") + if reply := sendToBotFather(t, svc, messages, owner, "add redirect http://localhost:3000/auth/callback"); !strings.Contains(reply, "Success!") { + t.Fatalf("add redirect reply = %q", reply) + } + + sendToBotFather(t, svc, messages, owner, "/setlogin") + sendToBotFather(t, svc, messages, owner, "login_demo_bot") + if reply := sendToBotFather(t, svc, messages, owner, "algorithm ES256"); !strings.Contains(reply, "ES256") { + t.Fatalf("algorithm reply = %q", reply) + } + + sendToBotFather(t, svc, messages, owner, "/setlogin") + sendToBotFather(t, svc, messages, owner, "login_demo_bot") + if reply := sendToBotFather(t, svc, messages, owner, "add ios dev.bedolaga.demo ABCDE12345 bedolaga://telegram-login Bedolaga iOS Demo"); !strings.Contains(reply, "Registered native app #") { + t.Fatalf("add iOS app reply = %q", reply) + } + + sendToBotFather(t, svc, messages, owner, "/setlogin") + sendToBotFather(t, svc, messages, owner, "login_demo_bot") + fingerprint := strings.Repeat("A", 64) + if reply := sendToBotFather(t, svc, messages, owner, "add android dev.bedolaga.demo "+fingerprint+" bedolaga://android-login Bedolaga Android Demo"); !strings.Contains(reply, "Registered native app #") { + t.Fatalf("add Android app reply = %q", reply) + } + + sendToBotFather(t, svc, messages, owner, "/logininfo") + info := sendToBotFather(t, svc, messages, owner, "login_demo_bot") + for _, want := range []string{"Signing algorithm: ES256", "web_origin http://localhost:3000", "redirect_uri http://localhost:3000/auth/callback", "dev.bedolaga.demo", "Bedolaga iOS Demo", "Bedolaga Android Demo"} { + if !strings.Contains(info, want) { + t.Fatalf("login info = %q, missing %q", info, want) + } + } + if strings.Contains(info, secret) { + t.Fatal("/logininfo leaked the one-time client secret") + } + + sendToBotFather(t, svc, messages, owner, "/resetloginsecret") + rotated := sendToBotFather(t, svc, messages, owner, "login_demo_bot") + if !strings.Contains(rotated, "previous OIDC Client Secret") || strings.Contains(rotated, secret) { + t.Fatalf("rotate reply = %q", rotated) + } +} diff --git a/internal/app/bots/service.go b/internal/app/bots/service.go index b715acfc..a610b8cd 100644 --- a/internal/app/bots/service.go +++ b/internal/app/bots/service.go @@ -16,6 +16,7 @@ import ( "go.uber.org/zap" + telegramloginapp "telesrv/internal/app/telegramlogin" "telesrv/internal/domain" "telesrv/internal/links" "telesrv/internal/store" @@ -82,6 +83,7 @@ type Service struct { stickers stickerSetCreator installer userStickerSetInstaller aiChat aiChatGenerator + telegramLogin *telegramloginapp.Service hooks RouterHooks textDrafts TextDraftPusher userCache store.UserCache @@ -175,6 +177,16 @@ func WithAIChatGenerator(g aiChatGenerator) Option { } } +// WithTelegramLogin injects the OIDC application service used by BotFather. +// BotFather never writes the login tables directly. +func WithTelegramLogin(login *telegramloginapp.Service) Option { + return func(s *Service) { + if login != nil { + s.telegramLogin = login + } + } +} + // WithAIChatStreamThrottle 调整 @ChatBot 流式草稿推送的最小时间间隔(测试用)。 func WithAIChatStreamThrottle(d time.Duration) Option { return func(s *Service) { diff --git a/internal/app/telegramlogin/crypto.go b/internal/app/telegramlogin/crypto.go new file mode 100644 index 00000000..44f8e650 --- /dev/null +++ b/internal/app/telegramlogin/crypto.go @@ -0,0 +1,131 @@ +package telegramlogin + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "errors" + "fmt" + + "telesrv/internal/domain" +) + +const opaqueTokenBytes = 32 + +func GenerateOpaqueToken() (string, error) { + raw := make([]byte, opaqueTokenBytes) + if _, err := rand.Read(raw); err != nil { + return "", fmt.Errorf("generate opaque token: %w", err) + } + return base64.RawURLEncoding.EncodeToString(raw), nil +} + +func HashOpaqueToken(token string) []byte { + sum := sha256.Sum256([]byte(token)) + return sum[:] +} + +func HashClientSecret(pepper []byte, secret string) ([]byte, error) { + if len(pepper) < 32 || secret == "" { + return nil, domain.ErrTelegramLoginSecretInvalid + } + mac := hmac.New(sha256.New, pepper) + _, _ = mac.Write([]byte(secret)) + return mac.Sum(nil), nil +} + +func VerifyClientSecret(pepper []byte, secret string, expected []byte) bool { + actual, err := HashClientSecret(pepper, secret) + if err != nil || len(expected) != sha256.Size { + return false + } + return subtle.ConstantTimeCompare(actual, expected) == 1 +} + +func PKCEChallenge(verifier string) (string, error) { + if len(verifier) < 43 || len(verifier) > 128 { + return "", domain.ErrTelegramLoginPKCEInvalid + } + for i := 0; i < len(verifier); i++ { + c := verifier[i] + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '~') { + return "", domain.ErrTelegramLoginPKCEInvalid + } + } + sum := sha256.Sum256([]byte(verifier)) + return base64.RawURLEncoding.EncodeToString(sum[:]), nil +} + +func ValidatePKCEChallenge(challenge, method string) error { + if method != "S256" || len(challenge) < 43 || len(challenge) > 128 { + return domain.ErrTelegramLoginPKCEInvalid + } + for i := 0; i < len(challenge); i++ { + c := challenge[i] + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_') { + return domain.ErrTelegramLoginPKCEInvalid + } + } + return nil +} + +type CodeSealer struct { + activeKeyID string + keys map[string]cipher.AEAD +} + +func NewCodeSealer(activeKeyID string, rawKeys map[string][]byte) (*CodeSealer, error) { + if activeKeyID == "" || len(rawKeys) == 0 { + return nil, errors.New("telegram login code seal key ring is empty") + } + keys := make(map[string]cipher.AEAD, len(rawKeys)) + for keyID, raw := range rawKeys { + if keyID == "" || len(raw) != 32 { + return nil, fmt.Errorf("invalid telegram login code seal key %q", keyID) + } + block, err := aes.NewCipher(raw) + if err != nil { + return nil, fmt.Errorf("create telegram login code seal key %q: %w", keyID, err) + } + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("create telegram login code sealer %q: %w", keyID, err) + } + keys[keyID] = aead + } + if _, ok := keys[activeKeyID]; !ok { + return nil, fmt.Errorf("active telegram login code seal key %q not found", activeKeyID) + } + return &CodeSealer{activeKeyID: activeKeyID, keys: keys}, nil +} + +func (s *CodeSealer) Seal(plaintext string, aad []byte) (sealed, nonce []byte, keyID string, err error) { + if s == nil || plaintext == "" { + return nil, nil, "", domain.ErrTelegramLoginCodeInvalid + } + aead := s.keys[s.activeKeyID] + nonce = make([]byte, aead.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return nil, nil, "", fmt.Errorf("generate telegram login code nonce: %w", err) + } + return aead.Seal(nil, nonce, []byte(plaintext), aad), nonce, s.activeKeyID, nil +} + +func (s *CodeSealer) Open(sealed, nonce []byte, keyID string, aad []byte) (string, error) { + if s == nil { + return "", domain.ErrTelegramLoginCodeInvalid + } + aead, ok := s.keys[keyID] + if !ok || len(nonce) != aead.NonceSize() { + return "", domain.ErrTelegramLoginCodeInvalid + } + plaintext, err := aead.Open(nil, nonce, sealed, aad) + if err != nil || len(plaintext) == 0 { + return "", domain.ErrTelegramLoginCodeInvalid + } + return string(plaintext), nil +} diff --git a/internal/app/telegramlogin/jose.go b/internal/app/telegramlogin/jose.go new file mode 100644 index 00000000..60986af6 --- /dev/null +++ b/internal/app/telegramlogin/jose.go @@ -0,0 +1,406 @@ +package telegramlogin + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" + + "telesrv/internal/domain" +) + +const defaultIDTokenTTL = time.Hour + +type SigningKeyMaterial struct { + Algorithm domain.TelegramLoginSigningAlgorithm + KeyID string + PrivateKey any + Active bool + PublishUntil time.Time +} + +type signingKey struct { + algorithm domain.TelegramLoginSigningAlgorithm + jwaAlgorithm jwa.SignatureAlgorithm + keyID string + private jwk.Key + public jwk.Key + active bool + publishUntil time.Time +} + +// SigningKeyRing owns no mutable crypto state. Rotation is performed by +// constructing a new ring containing the new active key and old public keys +// with a PublishUntil at least as long as the maximum ID-token lifetime. +type SigningKeyRing struct { + keys []signingKey + active map[domain.TelegramLoginSigningAlgorithm]signingKey + now func() time.Time +} + +func NewSigningKeyRing(materials []SigningKeyMaterial, now func() time.Time) (*SigningKeyRing, error) { + if len(materials) == 0 { + return nil, errors.New("telegram login signing key ring is empty") + } + if now == nil { + now = time.Now + } + ring := &SigningKeyRing{ + keys: make([]signingKey, 0, len(materials)), + active: make(map[domain.TelegramLoginSigningAlgorithm]signingKey), + now: now, + } + seenKeyIDs := make(map[string]struct{}, len(materials)) + for _, material := range materials { + key, err := importSigningKey(material) + if err != nil { + return nil, err + } + if _, duplicate := seenKeyIDs[key.keyID]; duplicate { + return nil, fmt.Errorf("duplicate telegram login signing kid %q", key.keyID) + } + seenKeyIDs[key.keyID] = struct{}{} + if key.active { + if _, duplicate := ring.active[key.algorithm]; duplicate { + return nil, fmt.Errorf("multiple active telegram login signing keys for %s", key.algorithm) + } + ring.active[key.algorithm] = key + } + ring.keys = append(ring.keys, key) + } + if len(ring.active) == 0 { + return nil, errors.New("telegram login signing key ring has no active key") + } + return ring, nil +} + +func importSigningKey(material SigningKeyMaterial) (signingKey, error) { + if !material.Algorithm.Valid() || material.PrivateKey == nil { + return signingKey{}, fmt.Errorf("invalid telegram login signing key material") + } + if material.Algorithm == domain.TelegramLoginSigningES256K && !telegramLoginES256KEnabled { + return signingKey{}, errors.New("telegram login ES256K requires a build with -tags jwx_es256k") + } + if err := validateRawSigningKey(material.Algorithm, material.PrivateKey); err != nil { + return signingKey{}, err + } + privateKey, err := jwk.Import(material.PrivateKey) + if err != nil { + return signingKey{}, fmt.Errorf("import telegram login %s private key: %w", material.Algorithm, err) + } + if err := privateKey.Validate(); err != nil { + return signingKey{}, fmt.Errorf("validate telegram login %s private JWK: %w", material.Algorithm, err) + } + publicKey, err := privateKey.PublicKey() + if err != nil { + return signingKey{}, fmt.Errorf("derive telegram login %s public JWK: %w", material.Algorithm, err) + } + thumbprint, err := publicKey.Thumbprint(crypto.SHA256) + if err != nil { + return signingKey{}, fmt.Errorf("thumbprint telegram login %s public JWK: %w", material.Algorithm, err) + } + keyID := strings.TrimSpace(material.KeyID) + if keyID == "" { + keyID = base64.RawURLEncoding.EncodeToString(thumbprint) + } + if len(keyID) > 128 || strings.IndexFunc(keyID, func(r rune) bool { return r <= 0x20 || r == 0x7f }) >= 0 { + return signingKey{}, fmt.Errorf("invalid telegram login signing kid") + } + jwaAlgorithm, err := telegramLoginJWA(material.Algorithm) + if err != nil { + return signingKey{}, err + } + for _, key := range []jwk.Key{privateKey, publicKey} { + if err := key.Set(jwk.KeyIDKey, keyID); err != nil { + return signingKey{}, fmt.Errorf("set telegram login signing kid: %w", err) + } + if err := key.Set(jwk.AlgorithmKey, jwaAlgorithm); err != nil { + return signingKey{}, fmt.Errorf("set telegram login signing algorithm: %w", err) + } + if err := key.Set(jwk.KeyUsageKey, "sig"); err != nil { + return signingKey{}, fmt.Errorf("set telegram login signing use: %w", err) + } + } + return signingKey{ + algorithm: material.Algorithm, jwaAlgorithm: jwaAlgorithm, keyID: keyID, + private: privateKey, public: publicKey, active: material.Active, + publishUntil: material.PublishUntil.UTC(), + }, nil +} + +func validateRawSigningKey(algorithm domain.TelegramLoginSigningAlgorithm, raw any) error { + switch algorithm { + case domain.TelegramLoginSigningRS256: + key, ok := rsaPrivateKey(raw) + if !ok || key.N == nil || key.N.BitLen() < 2048 || key.E < 3 { + return errors.New("telegram login RS256 requires an RSA private key of at least 2048 bits") + } + if err := key.Validate(); err != nil { + return fmt.Errorf("validate telegram login RSA private key: %w", err) + } + case domain.TelegramLoginSigningES256: + key, ok := ecdsaPrivateKey(raw) + if !ok || key.Curve != elliptic.P256() || key.D == nil || key.X == nil || key.Y == nil { + return errors.New("telegram login ES256 requires a P-256 ECDSA private key") + } + case domain.TelegramLoginSigningEdDSA: + key, ok := raw.(ed25519.PrivateKey) + if !ok || len(key) != ed25519.PrivateKeySize { + return errors.New("telegram login EdDSA requires an Ed25519 private key") + } + case domain.TelegramLoginSigningES256K: + key, ok := ecdsaPrivateKey(raw) + if !ok || key.Curve == nil || key.Curve.Params() == nil || + !strings.EqualFold(key.Curve.Params().Name, "secp256k1") || key.D == nil || key.X == nil || key.Y == nil { + return errors.New("telegram login ES256K requires a secp256k1 ECDSA private key") + } + default: + return domain.ErrTelegramLoginClientInvalid + } + return nil +} + +func rsaPrivateKey(raw any) (*rsa.PrivateKey, bool) { + switch key := raw.(type) { + case *rsa.PrivateKey: + return key, key != nil + case rsa.PrivateKey: + return &key, true + default: + return nil, false + } +} + +func ecdsaPrivateKey(raw any) (*ecdsa.PrivateKey, bool) { + switch key := raw.(type) { + case *ecdsa.PrivateKey: + return key, key != nil + case ecdsa.PrivateKey: + return &key, true + default: + return nil, false + } +} + +func telegramLoginJWA(algorithm domain.TelegramLoginSigningAlgorithm) (jwa.SignatureAlgorithm, error) { + switch algorithm { + case domain.TelegramLoginSigningRS256: + return jwa.RS256(), nil + case domain.TelegramLoginSigningES256: + return jwa.ES256(), nil + case domain.TelegramLoginSigningEdDSA: + return jwa.EdDSA(), nil + case domain.TelegramLoginSigningES256K: + if telegramLoginES256KEnabled { + return jwa.ES256K(), nil + } + return jwa.EmptySignatureAlgorithm(), errors.New("telegram login ES256K is disabled in this build") + default: + return jwa.EmptySignatureAlgorithm(), domain.ErrTelegramLoginClientInvalid + } +} + +func (r *SigningKeyRing) SupportedAlgorithms() []string { + if r == nil { + return nil + } + ordered := make([]string, 0, len(r.active)) + for _, algorithm := range []domain.TelegramLoginSigningAlgorithm{ + domain.TelegramLoginSigningRS256, + domain.TelegramLoginSigningES256, + domain.TelegramLoginSigningEdDSA, + domain.TelegramLoginSigningES256K, + } { + if _, ok := r.active[algorithm]; ok { + ordered = append(ordered, string(algorithm)) + } + } + return ordered +} + +// ActiveAlgorithms returns the algorithms that can sign new tokens on this +// instance. Callers use it to prevent durable client configuration from +// selecting an algorithm without an active private key. +func (r *SigningKeyRing) ActiveAlgorithms() []domain.TelegramLoginSigningAlgorithm { + if r == nil { + return nil + } + ordered := make([]domain.TelegramLoginSigningAlgorithm, 0, len(r.active)) + for _, algorithm := range []domain.TelegramLoginSigningAlgorithm{ + domain.TelegramLoginSigningRS256, + domain.TelegramLoginSigningES256, + domain.TelegramLoginSigningEdDSA, + domain.TelegramLoginSigningES256K, + } { + if _, ok := r.active[algorithm]; ok { + ordered = append(ordered, algorithm) + } + } + return ordered +} + +func (r *SigningKeyRing) JWKS() ([]byte, string, error) { + if r == nil { + return nil, "", errors.New("telegram login signing key ring is nil") + } + now := r.now().UTC() + set := jwk.NewSet() + for _, key := range r.keys { + if !key.active && (key.publishUntil.IsZero() || !now.Before(key.publishUntil)) { + continue + } + clone, err := key.public.Clone() + if err != nil { + return nil, "", fmt.Errorf("clone telegram login public JWK: %w", err) + } + if err := set.AddKey(clone); err != nil { + return nil, "", fmt.Errorf("add telegram login public JWK: %w", err) + } + } + body, err := json.Marshal(set) + if err != nil { + return nil, "", fmt.Errorf("marshal telegram login JWKS: %w", err) + } + sum := sha256.Sum256(body) + return body, `"` + base64.RawURLEncoding.EncodeToString(sum[:]) + `"`, nil +} + +func (r *SigningKeyRing) sign(algorithm domain.TelegramLoginSigningAlgorithm, token jwt.Token) (string, error) { + if r == nil || token == nil { + return "", errors.New("telegram login ID token signer is unavailable") + } + key, ok := r.active[algorithm] + if !ok { + return "", fmt.Errorf("no active telegram login signing key for %s", algorithm) + } + signed, err := jwt.Sign(token, jwt.WithKey(key.jwaAlgorithm, key.private)) + if err != nil { + return "", fmt.Errorf("sign telegram login ID token with %s: %w", algorithm, err) + } + return string(signed), nil +} + +type IDTokenIssuerConfig struct { + Issuer string + TTL time.Duration + Now func() time.Time +} + +type IDTokenIssuer struct { + issuer string + ttl time.Duration + now func() time.Time + keys *SigningKeyRing +} + +func (i *IDTokenIssuer) Issuer() string { + if i == nil { + return "" + } + return i.issuer +} + +func (i *IDTokenIssuer) TTL() time.Duration { + if i == nil { + return 0 + } + return i.ttl +} + +func (i *IDTokenIssuer) SupportedAlgorithms() []string { + if i == nil { + return nil + } + return i.keys.SupportedAlgorithms() +} + +func (i *IDTokenIssuer) JWKS() ([]byte, string, error) { + if i == nil { + return nil, "", errors.New("telegram login ID token issuer is nil") + } + return i.keys.JWKS() +} + +func NewIDTokenIssuer(keys *SigningKeyRing, cfg IDTokenIssuerConfig) (*IDTokenIssuer, error) { + if keys == nil { + return nil, errors.New("telegram login signing key ring is required") + } + issuer, err := NormalizeWebOrigin(cfg.Issuer, true) + if err != nil { + return nil, fmt.Errorf("telegram login ID token issuer: %w", err) + } + if cfg.TTL == 0 { + cfg.TTL = defaultIDTokenTTL + } + if cfg.TTL < time.Minute || cfg.TTL > 24*time.Hour { + return nil, errors.New("telegram login ID token TTL is outside the bounded range") + } + if cfg.Now == nil { + cfg.Now = time.Now + } + return &IDTokenIssuer{issuer: issuer, ttl: cfg.TTL, now: cfg.Now, keys: keys}, nil +} + +func (i *IDTokenIssuer) Issue(request domain.TelegramLoginRequest) (string, error) { + if i == nil || request.Status != domain.TelegramLoginRequestApproved || request.AuthorizedUserID <= 0 || + request.ClientID == "" || request.ApprovedAt.IsZero() { + return "", domain.ErrTelegramLoginRequestInvalid + } + if err := domain.ValidateTelegramLoginScopes(request.Scopes, request.SigningAlgorithm); err != nil { + return "", err + } + identity := domain.TelegramLoginIdentitySnapshot{ + UserID: request.AuthorizedUserID, Name: request.ProfileName, GivenName: request.GivenName, + FamilyName: request.FamilyName, PreferredUsername: request.PreferredUsername, + Picture: request.Picture, PhoneNumber: request.PhoneNumber, + } + identity, err := identity.Sanitized(request.Requests(domain.TelegramLoginScopeProfile), request.PhoneShared) + if err != nil { + return "", err + } + now := i.now().UTC() + builder := jwt.NewBuilder(). + Issuer(i.issuer). + Audience([]string{request.ClientID}). + Subject(fmt.Sprintf("%d", identity.UserID)). + IssuedAt(now). + Expiration(now.Add(i.ttl)) + if request.Nonce != "" { + builder.Claim("nonce", request.Nonce) + } + if request.Requests(domain.TelegramLoginScopeProfile) { + builder.Claim("id", identity.UserID). + Claim("name", identity.Name). + Claim("given_name", identity.GivenName) + if identity.FamilyName != "" { + builder.Claim("family_name", identity.FamilyName) + } + if identity.PreferredUsername != "" { + builder.Claim("preferred_username", identity.PreferredUsername) + } + if identity.Picture != "" { + builder.Claim("picture", identity.Picture) + } + } + if request.Requests(domain.TelegramLoginScopePhone) && request.PhoneShared { + builder.Claim("phone_number", identity.PhoneNumber). + Claim("phone_number_verified", true) + } + token, err := builder.Build() + if err != nil { + return "", fmt.Errorf("build telegram login ID token: %w", err) + } + return i.keys.sign(request.SigningAlgorithm, token) +} diff --git a/internal/app/telegramlogin/jose_es256k_disabled.go b/internal/app/telegramlogin/jose_es256k_disabled.go new file mode 100644 index 00000000..870c3753 --- /dev/null +++ b/internal/app/telegramlogin/jose_es256k_disabled.go @@ -0,0 +1,5 @@ +//go:build !jwx_es256k + +package telegramlogin + +const telegramLoginES256KEnabled = false diff --git a/internal/app/telegramlogin/jose_es256k_disabled_test.go b/internal/app/telegramlogin/jose_es256k_disabled_test.go new file mode 100644 index 00000000..a13925b4 --- /dev/null +++ b/internal/app/telegramlogin/jose_es256k_disabled_test.go @@ -0,0 +1,17 @@ +//go:build !jwx_es256k + +package telegramlogin + +import ( + "testing" + + "telesrv/internal/domain" +) + +func TestES256KFailsClosedWithoutBuildTag(t *testing.T) { + if _, err := NewSigningKeyRing([]SigningKeyMaterial{{ + Algorithm: domain.TelegramLoginSigningES256K, PrivateKey: struct{}{}, Active: true, + }}, nil); err == nil { + t.Fatal("ES256K configuration unexpectedly accepted without jwx_es256k") + } +} diff --git a/internal/app/telegramlogin/jose_es256k_enabled.go b/internal/app/telegramlogin/jose_es256k_enabled.go new file mode 100644 index 00000000..601999a3 --- /dev/null +++ b/internal/app/telegramlogin/jose_es256k_enabled.go @@ -0,0 +1,5 @@ +//go:build jwx_es256k + +package telegramlogin + +const telegramLoginES256KEnabled = true diff --git a/internal/app/telegramlogin/jose_es256k_enabled_test.go b/internal/app/telegramlogin/jose_es256k_enabled_test.go new file mode 100644 index 00000000..de31551b --- /dev/null +++ b/internal/app/telegramlogin/jose_es256k_enabled_test.go @@ -0,0 +1,57 @@ +//go:build jwx_es256k + +package telegramlogin + +import ( + "testing" + "time" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" + + "telesrv/internal/domain" +) + +func TestES256KIDTokenRoundTripWithBuildTag(t *testing.T) { + raw, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC) + ring, err := NewSigningKeyRing([]SigningKeyMaterial{{ + Algorithm: domain.TelegramLoginSigningES256K, + KeyID: "secp256k1-active", + PrivateKey: raw.ToECDSA(), + Active: true, + }}, func() time.Time { return now }) + if err != nil { + t.Fatal(err) + } + issuer, err := NewIDTokenIssuer(ring, IDTokenIssuerConfig{ + Issuer: "https://oauth.telesrv.test", Now: func() time.Time { return now }, + }) + if err != nil { + t.Fatal(err) + } + signed, err := issuer.Issue(domain.TelegramLoginRequest{ + ClientID: "9001", SigningAlgorithm: domain.TelegramLoginSigningES256K, + Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID}, + Status: domain.TelegramLoginRequestApproved, AuthorizedUserID: 42, + ApprovedAt: now.Add(-time.Minute), + }) + if err != nil { + t.Fatal(err) + } + body, _, err := ring.JWKS() + if err != nil { + t.Fatal(err) + } + set, err := jwk.Parse(body) + if err != nil { + t.Fatal(err) + } + if _, err := jwt.Parse([]byte(signed), jwt.WithKeySet(set), jwt.WithValidate(false)); err != nil { + t.Fatalf("verify ES256K token: %v", err) + } +} diff --git a/internal/app/telegramlogin/jose_test.go b/internal/app/telegramlogin/jose_test.go new file mode 100644 index 00000000..108cd20a --- /dev/null +++ b/internal/app/telegramlogin/jose_test.go @@ -0,0 +1,188 @@ +package telegramlogin + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "testing" + "time" + + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" + + "telesrv/internal/domain" +) + +func telegramLoginTestSigningKeys(t *testing.T, now *time.Time) *SigningKeyRing { + t.Helper() + oldRSA, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + activeRSA, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + es256, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + _, ed25519Key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + ring, err := NewSigningKeyRing([]SigningKeyMaterial{ + {Algorithm: domain.TelegramLoginSigningRS256, KeyID: "rsa-old", PrivateKey: oldRSA, PublishUntil: now.Add(2 * time.Hour)}, + {Algorithm: domain.TelegramLoginSigningRS256, KeyID: "rsa-active", PrivateKey: activeRSA, Active: true}, + {Algorithm: domain.TelegramLoginSigningES256, KeyID: "p256-active", PrivateKey: es256, Active: true}, + {Algorithm: domain.TelegramLoginSigningEdDSA, KeyID: "ed25519-active", PrivateKey: ed25519Key, Active: true}, + }, func() time.Time { return *now }) + if err != nil { + t.Fatal(err) + } + return ring +} + +func TestSigningKeyRingRotationAndAlgorithms(t *testing.T) { + now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC) + ring := telegramLoginTestSigningKeys(t, &now) + if got := ring.SupportedAlgorithms(); len(got) != 3 || got[0] != "RS256" || got[1] != "ES256" || got[2] != "EdDSA" { + t.Fatalf("SupportedAlgorithms = %#v", got) + } + body, etag, err := ring.JWKS() + if err != nil { + t.Fatal(err) + } + set, err := jwk.Parse(body) + if err != nil { + t.Fatalf("parse JWKS: %v", err) + } + if set.Len() != 4 || etag == "" { + t.Fatalf("JWKS len=%d etag=%q body=%s", set.Len(), etag, body) + } + var decoded map[string]any + if err := json.Unmarshal(body, &decoded); err != nil { + t.Fatal(err) + } + for i := 0; i < set.Len(); i++ { + key, _ := set.Key(i) + if key.Has("d") || key.Has("p") || key.Has("q") { + t.Fatalf("JWKS leaked private key material: %s", body) + } + } + now = now.Add(3 * time.Hour) + body, _, err = ring.JWKS() + if err != nil { + t.Fatal(err) + } + set, err = jwk.Parse(body) + if err != nil || set.Len() != 3 { + t.Fatalf("JWKS after retirement len=%d err=%v body=%s", set.Len(), err, body) + } +} + +func TestIDTokenIssuerScopeProjectionAndVerification(t *testing.T) { + now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC) + ring := telegramLoginTestSigningKeys(t, &now) + issuer, err := NewIDTokenIssuer(ring, IDTokenIssuerConfig{ + Issuer: "https://oauth.telesrv.test", Now: func() time.Time { return now }, + }) + if err != nil { + t.Fatal(err) + } + profileRequest := domain.TelegramLoginRequest{ + ClientID: "9001", SigningAlgorithm: domain.TelegramLoginSigningRS256, + Scopes: []domain.TelegramLoginScope{ + domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile, domain.TelegramLoginScopePhone, + }, + Nonce: "request-nonce", Status: domain.TelegramLoginRequestApproved, AuthorizedUserID: 42, + ProfileName: "Alice Example", GivenName: "Alice", FamilyName: "Example", + PreferredUsername: "alice", Picture: "https://oauth.telesrv.test/userpic/42", + PhoneNumber: "15551234567", PhoneShared: true, ApprovedAt: now.Add(-time.Minute), + } + signed, err := issuer.Issue(profileRequest) + if err != nil { + t.Fatal(err) + } + jwksBody, _, err := ring.JWKS() + if err != nil { + t.Fatal(err) + } + set, err := jwk.Parse(jwksBody) + if err != nil { + t.Fatal(err) + } + token, err := jwt.Parse([]byte(signed), jwt.WithKeySet(set), jwt.WithValidate(false)) + if err != nil { + t.Fatalf("verify signed token: %v", err) + } + issuerValue, _ := token.Issuer() + subject, _ := token.Subject() + audience, _ := token.Audience() + if issuerValue != "https://oauth.telesrv.test" || subject != "42" || len(audience) != 1 || audience[0] != "9001" { + t.Fatalf("standard claims iss=%q sub=%q aud=%#v", issuerValue, subject, audience) + } + var id float64 + var name, phone, nonce string + var verified bool + if err := token.Get("id", &id); err != nil || id != 42 { + t.Fatalf("id claim=%v err=%v", id, err) + } + if err := token.Get("name", &name); err != nil || name != "Alice Example" { + t.Fatalf("name claim=%q err=%v", name, err) + } + if err := token.Get("phone_number", &phone); err != nil || phone != "15551234567" { + t.Fatalf("phone claim=%q err=%v", phone, err) + } + if err := token.Get("phone_number_verified", &verified); err != nil || !verified { + t.Fatalf("phone verified=%v err=%v", verified, err) + } + if err := token.Get("nonce", &nonce); err != nil || nonce != "request-nonce" { + t.Fatalf("nonce=%q err=%v", nonce, err) + } + + openidOnly := profileRequest + openidOnly.SigningAlgorithm = domain.TelegramLoginSigningEdDSA + openidOnly.Scopes = []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID} + openidOnly.ProfileName = "" + openidOnly.GivenName = "" + openidOnly.FamilyName = "" + openidOnly.PreferredUsername = "" + openidOnly.Picture = "" + openidOnly.PhoneNumber = "" + openidOnly.PhoneShared = false + signed, err = issuer.Issue(openidOnly) + if err != nil { + t.Fatal(err) + } + token, err = jwt.Parse([]byte(signed), jwt.WithKeySet(set), jwt.WithValidate(false)) + if err != nil { + t.Fatalf("verify EdDSA token: %v", err) + } + if token.Has("id") || token.Has("name") || token.Has("phone_number") { + t.Fatalf("openid-only token leaked optional claims: %#v", token.Keys()) + } +} + +func TestSigningKeyRingRejectsWrongCurveAndDuplicateActiveKey(t *testing.T) { + p384, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + t.Fatal(err) + } + if _, err := NewSigningKeyRing([]SigningKeyMaterial{{ + Algorithm: domain.TelegramLoginSigningES256, PrivateKey: p384, Active: true, + }}, nil); err == nil { + t.Fatal("P-384 key unexpectedly accepted for ES256") + } + key1, _ := rsa.GenerateKey(rand.Reader, 2048) + key2, _ := rsa.GenerateKey(rand.Reader, 2048) + if _, err := NewSigningKeyRing([]SigningKeyMaterial{ + {Algorithm: domain.TelegramLoginSigningRS256, PrivateKey: key1, Active: true}, + {Algorithm: domain.TelegramLoginSigningRS256, PrivateKey: key2, Active: true}, + }, nil); err == nil { + t.Fatal("two active RS256 keys unexpectedly accepted") + } +} diff --git a/internal/app/telegramlogin/keyfiles.go b/internal/app/telegramlogin/keyfiles.go new file mode 100644 index 00000000..9d3023d5 --- /dev/null +++ b/internal/app/telegramlogin/keyfiles.go @@ -0,0 +1,186 @@ +package telegramlogin + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/lestrrat-go/jwx/v3/jwk" + + "telesrv/internal/domain" +) + +const ( + maxTelegramLoginManifestBytes = 1 << 20 + maxTelegramLoginKeyBytes = 256 << 10 +) + +type signingKeyManifest struct { + Version int `json:"version"` + Keys []signingKeyManifestEntry `json:"keys"` +} + +type signingKeyManifestEntry struct { + Algorithm domain.TelegramLoginSigningAlgorithm `json:"algorithm"` + KeyID string `json:"kid,omitempty"` + PrivateKeyFile string `json:"private_key_file"` + Active bool `json:"active"` + PublishUntil string `json:"publish_until,omitempty"` +} + +// LoadSigningKeyRing reads a versioned manifest and private PEM/JWK files. +// Relative key paths are resolved against the manifest directory. The caller +// should atomically replace files and rebuild/swap the ring when rotating. +func LoadSigningKeyRing(path string, now func() time.Time) (*SigningKeyRing, error) { + var manifest signingKeyManifest + if err := readStrictJSONFile(path, maxTelegramLoginManifestBytes, &manifest); err != nil { + return nil, fmt.Errorf("load telegram login signing manifest: %w", err) + } + if manifest.Version != 1 || len(manifest.Keys) == 0 || len(manifest.Keys) > 32 { + return nil, errors.New("telegram login signing manifest has invalid version or key count") + } + baseDir := filepath.Dir(path) + materials := make([]SigningKeyMaterial, 0, len(manifest.Keys)) + for index, entry := range manifest.Keys { + keyPath := strings.TrimSpace(entry.PrivateKeyFile) + if !entry.Algorithm.Valid() || keyPath == "" { + return nil, fmt.Errorf("telegram login signing manifest key %d is invalid", index) + } + if !filepath.IsAbs(keyPath) { + keyPath = filepath.Join(baseDir, keyPath) + } + data, err := readBoundedFile(keyPath, maxTelegramLoginKeyBytes) + if err != nil { + return nil, fmt.Errorf("read telegram login signing key %d: %w", index, err) + } + var parsed jwk.Key + if len(bytes.TrimSpace(data)) > 0 && bytes.TrimSpace(data)[0] == '{' { + parsed, err = jwk.ParseKey(data) + } else { + parsed, err = jwk.ParseKey(data, jwk.WithPEM(true)) + } + if err != nil { + return nil, fmt.Errorf("parse telegram login signing key %d: %w", index, err) + } + var raw any + if err := jwk.Export(parsed, &raw); err != nil { + return nil, fmt.Errorf("export telegram login signing key %d: %w", index, err) + } + var publishUntil time.Time + if entry.PublishUntil != "" { + publishUntil, err = time.Parse(time.RFC3339, entry.PublishUntil) + if err != nil { + return nil, fmt.Errorf("parse telegram login signing key %d publish_until: %w", index, err) + } + } + if entry.Active && !publishUntil.IsZero() { + return nil, fmt.Errorf("active telegram login signing key %d must not set publish_until", index) + } + if !entry.Active && publishUntil.IsZero() { + return nil, fmt.Errorf("retiring telegram login signing key %d requires publish_until", index) + } + materials = append(materials, SigningKeyMaterial{ + Algorithm: entry.Algorithm, KeyID: entry.KeyID, PrivateKey: raw, + Active: entry.Active, PublishUntil: publishUntil, + }) + } + return NewSigningKeyRing(materials, now) +} + +type codeKeyManifest struct { + Version int `json:"version"` + Active string `json:"active"` + Keys map[string]string `json:"keys"` +} + +func LoadCodeSealer(path string) (*CodeSealer, error) { + var manifest codeKeyManifest + if err := readStrictJSONFile(path, maxTelegramLoginManifestBytes, &manifest); err != nil { + return nil, fmt.Errorf("load telegram login code-key manifest: %w", err) + } + if manifest.Version != 1 || manifest.Active == "" || len(manifest.Keys) == 0 || len(manifest.Keys) > 16 { + return nil, errors.New("telegram login code-key manifest has invalid version or key count") + } + keys := make(map[string][]byte, len(manifest.Keys)) + for keyID, encoded := range manifest.Keys { + if strings.TrimSpace(keyID) == "" || keyID != strings.TrimSpace(keyID) || len(keyID) > 128 { + return nil, errors.New("telegram login code-key manifest has invalid key id") + } + raw, err := decodeBase64Key(encoded) + if err != nil || len(raw) != 32 { + return nil, fmt.Errorf("telegram login code-key %q must be 32 base64-encoded bytes", keyID) + } + keys[keyID] = raw + } + return NewCodeSealer(manifest.Active, keys) +} + +func LoadClientSecretPepper(path string) ([]byte, error) { + data, err := readBoundedFile(path, 4096) + if err != nil { + return nil, fmt.Errorf("read telegram login client-secret pepper: %w", err) + } + raw, err := decodeBase64Key(strings.TrimSpace(string(data))) + if err != nil || len(raw) != 32 { + return nil, errors.New("telegram login client-secret pepper must be 32 base64-encoded bytes") + } + return raw, nil +} + +func decodeBase64Key(value string) ([]byte, error) { + value = strings.TrimSpace(value) + for _, encoding := range []*base64.Encoding{ + base64.RawURLEncoding, base64.URLEncoding, base64.RawStdEncoding, base64.StdEncoding, + } { + if raw, err := encoding.DecodeString(value); err == nil { + return raw, nil + } + } + return nil, errors.New("invalid base64") +} + +func readStrictJSONFile(path string, maxBytes int64, target any) error { + data, err := readBoundedFile(path, maxBytes) + if err != nil { + return err + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(target); err != nil { + return err + } + var extra any + if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) { + if err == nil { + return errors.New("multiple JSON values") + } + return err + } + return nil +} + +func readBoundedFile(path string, maxBytes int64) ([]byte, error) { + if strings.TrimSpace(path) == "" || maxBytes <= 0 { + return nil, errors.New("invalid file path or size bound") + } + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + info, err := file.Stat() + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() || info.Size() > maxBytes { + return nil, errors.New("file is not regular or exceeds size bound") + } + return io.ReadAll(io.LimitReader(file, maxBytes+1)) +} diff --git a/internal/app/telegramlogin/keyfiles_test.go b/internal/app/telegramlogin/keyfiles_test.go new file mode 100644 index 00000000..8c785deb --- /dev/null +++ b/internal/app/telegramlogin/keyfiles_test.go @@ -0,0 +1,86 @@ +package telegramlogin + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "os" + "path/filepath" + "testing" + "time" +) + +func TestLoadSigningKeyRingAndSymmetricKeyFiles(t *testing.T) { + dir := t.TempDir() + rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaKey)}) + if err := os.WriteFile(filepath.Join(dir, "rsa.pem"), pemBytes, 0o600); err != nil { + t.Fatal(err) + } + manifest := map[string]any{ + "version": 1, + "keys": []map[string]any{{ + "algorithm": "RS256", "kid": "rsa-test", "private_key_file": "rsa.pem", "active": true, + }}, + } + manifestBytes, _ := json.Marshal(manifest) + manifestPath := filepath.Join(dir, "signing.json") + if err := os.WriteFile(manifestPath, manifestBytes, 0o600); err != nil { + t.Fatal(err) + } + ring, err := LoadSigningKeyRing(manifestPath, nil) + if err != nil { + t.Fatal(err) + } + if got := ring.SupportedAlgorithms(); len(got) != 1 || got[0] != "RS256" { + t.Fatalf("algorithms=%#v", got) + } + + codeKey := make([]byte, 32) + pepper := make([]byte, 32) + _, _ = rand.Read(codeKey) + _, _ = rand.Read(pepper) + codeManifest, _ := json.Marshal(map[string]any{ + "version": 1, "active": "2026-07", "keys": map[string]string{"2026-07": base64.RawURLEncoding.EncodeToString(codeKey)}, + }) + codePath := filepath.Join(dir, "code-keys.json") + pepperPath := filepath.Join(dir, "pepper") + if err := os.WriteFile(codePath, codeManifest, 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(pepperPath, []byte(base64.RawURLEncoding.EncodeToString(pepper)), 0o600); err != nil { + t.Fatal(err) + } + sealer, err := LoadCodeSealer(codePath) + if err != nil { + t.Fatal(err) + } + sealed, nonce, kid, err := sealer.Seal("code", []byte("aad")) + if err != nil { + t.Fatal(err) + } + if opened, err := sealer.Open(sealed, nonce, kid, []byte("aad")); err != nil || opened != "code" { + t.Fatalf("open=%q err=%v", opened, err) + } + loadedPepper, err := LoadClientSecretPepper(pepperPath) + if err != nil || string(loadedPepper) != string(pepper) { + t.Fatalf("pepper len=%d err=%v", len(loadedPepper), err) + } +} + +func TestLoadSigningKeyRingRejectsUnknownManifestFieldAndUnboundedRetiringKey(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "bad.json") + if err := os.WriteFile(path, []byte(`{"version":1,"keys":[],"unknown":true}`), 0o600); err != nil { + t.Fatal(err) + } + if _, err := LoadSigningKeyRing(path, func() time.Time { return time.Now() }); err == nil { + t.Fatal("unknown manifest field unexpectedly accepted") + } +} diff --git a/internal/app/telegramlogin/native.go b/internal/app/telegramlogin/native.go new file mode 100644 index 00000000..97d74282 --- /dev/null +++ b/internal/app/telegramlogin/native.go @@ -0,0 +1,80 @@ +package telegramlogin + +import ( + "net/url" + "regexp" + "strings" + "unicode" + + "telesrv/internal/domain" +) + +var nativeApplicationIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{2,254}$`) + +func normalizeNativeApplicationID(raw string) (string, error) { + raw = strings.TrimSpace(raw) + if !nativeApplicationIDPattern.MatchString(raw) || !strings.Contains(raw, ".") || strings.Contains(raw, "..") { + return "", domain.ErrTelegramLoginClientInvalid + } + return raw, nil +} + +func normalizeNativeVerificationID(platform domain.TelegramLoginNativePlatform, raw string) (string, error) { + raw = strings.ToUpper(strings.TrimSpace(raw)) + switch platform { + case domain.TelegramLoginNativeIOS: + if len(raw) != 10 || strings.IndexFunc(raw, func(r rune) bool { + return !((r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) + }) >= 0 { + return "", domain.ErrTelegramLoginClientInvalid + } + case domain.TelegramLoginNativeAndroid: + raw = strings.ReplaceAll(raw, ":", "") + if len(raw) != 64 || strings.IndexFunc(raw, func(r rune) bool { + return !((r >= 'A' && r <= 'F') || (r >= '0' && r <= '9')) + }) >= 0 { + return "", domain.ErrTelegramLoginClientInvalid + } + default: + return "", domain.ErrTelegramLoginClientInvalid + } + return raw, nil +} + +// NormalizeNativeCallbackURI accepts the exact HTTPS universal/app link or a +// non-web custom scheme registered for a native application. Query and +// fragment components are forbidden because OAuth response fields are +// appended by the provider and must not collide with application input. +func NormalizeNativeCallbackURI(raw string, allowLoopbackHTTP bool) (string, error) { + if raw == "" || len(raw) > maxTelegramLoginURLLength || raw != strings.TrimSpace(raw) || strings.IndexFunc(raw, unicode.IsControl) >= 0 { + return "", domain.ErrTelegramLoginURLInvalid + } + u, err := url.Parse(raw) + if err != nil || !u.IsAbs() || u.Opaque != "" || u.User != nil || u.Host == "" || u.RawQuery != "" || u.Fragment != "" || u.RawPath != "" { + return "", domain.ErrTelegramLoginURLInvalid + } + if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") { + normalized, _, err := NormalizeRedirectURI(raw, allowLoopbackHTTP) + return normalized, err + } + scheme := strings.ToLower(u.Scheme) + if !validAppScheme(scheme) || scheme == "tg" || scheme == "javascript" || scheme == "data" || scheme == "file" || u.Port() != "" { + return "", domain.ErrTelegramLoginURLInvalid + } + host := strings.ToLower(strings.TrimSuffix(u.Hostname(), ".")) + if host == "" || strings.IndexFunc(host, func(r rune) bool { + return !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '.') + }) >= 0 { + return "", domain.ErrTelegramLoginURLInvalid + } + u.Scheme, u.Host = scheme, host + return u.String(), nil +} + +func normalizeNativeDisplayName(raw string) (string, error) { + raw = strings.TrimSpace(raw) + if raw == "" || len(raw) > 128 || strings.IndexFunc(raw, unicode.IsControl) >= 0 { + return "", domain.ErrTelegramLoginClientInvalid + } + return raw, nil +} diff --git a/internal/app/telegramlogin/service.go b/internal/app/telegramlogin/service.go new file mode 100644 index 00000000..77c34f97 --- /dev/null +++ b/internal/app/telegramlogin/service.go @@ -0,0 +1,1374 @@ +package telegramlogin + +import ( + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "net/url" + "slices" + "sort" + "strconv" + "strings" + "time" + "unicode/utf8" + + "telesrv/internal/domain" + "telesrv/internal/store" +) + +const ( + defaultRequestTTL = 5 * time.Minute + defaultCodeTTL = 2 * time.Minute +) + +var telegramLoginMatchCodePool = []string{ + "🍏", "🍊", "🍋", "🍇", "🍉", "🍒", "🥝", "🥕", + "🚗", "🚲", "✈️", "🚀", "⛵", "🏠", "🏰", "⛺", + "⚽", "🏀", "🎾", "🎲", "🎸", "🎹", "📷", "💡", +} + +type Config struct { + Issuer string + AppScheme string + AllowLoopbackHTTP bool + ClientSecretPepper []byte + SupportedSigningAlgorithms []domain.TelegramLoginSigningAlgorithm + RequestTTL time.Duration + CodeTTL time.Duration + Now func() time.Time +} + +type Service struct { + store store.TelegramLoginStore + sealer *CodeSealer + issuer string + appScheme string + allowLoopbackHTTP bool + clientSecretPepper []byte + signingAlgorithms []domain.TelegramLoginSigningAlgorithm + signingAlgorithmSet map[domain.TelegramLoginSigningAlgorithm]struct{} + requestTTL time.Duration + codeTTL time.Duration + now func() time.Time +} + +func NewService(loginStore store.TelegramLoginStore, sealer *CodeSealer, cfg Config) (*Service, error) { + if loginStore == nil || sealer == nil || len(cfg.ClientSecretPepper) < 32 { + return nil, fmt.Errorf("telegram login dependencies are incomplete") + } + issuer, err := NormalizeWebOrigin(cfg.Issuer, cfg.AllowLoopbackHTTP) + if err != nil { + return nil, fmt.Errorf("telegram login issuer: %w", err) + } + if !validAppScheme(cfg.AppScheme) { + return nil, fmt.Errorf("telegram login app scheme is invalid") + } + if cfg.RequestTTL == 0 { + cfg.RequestTTL = defaultRequestTTL + } + if cfg.CodeTTL == 0 { + cfg.CodeTTL = defaultCodeTTL + } + if cfg.RequestTTL < time.Minute || cfg.RequestTTL > 15*time.Minute || cfg.CodeTTL < 30*time.Second || cfg.CodeTTL > 10*time.Minute { + return nil, fmt.Errorf("telegram login ttl is outside the bounded range") + } + if cfg.Now == nil { + cfg.Now = time.Now + } + var signingAlgorithmSet map[domain.TelegramLoginSigningAlgorithm]struct{} + if len(cfg.SupportedSigningAlgorithms) > 0 { + signingAlgorithmSet = make(map[domain.TelegramLoginSigningAlgorithm]struct{}, len(cfg.SupportedSigningAlgorithms)) + for _, algorithm := range cfg.SupportedSigningAlgorithms { + if !algorithm.Valid() { + return nil, fmt.Errorf("telegram login supported signing algorithm is invalid") + } + signingAlgorithmSet[algorithm] = struct{}{} + } + } + return &Service{ + store: loginStore, sealer: sealer, issuer: issuer, appScheme: strings.ToLower(cfg.AppScheme), + allowLoopbackHTTP: cfg.AllowLoopbackHTTP, + clientSecretPepper: append([]byte(nil), cfg.ClientSecretPepper...), + signingAlgorithms: append([]domain.TelegramLoginSigningAlgorithm(nil), cfg.SupportedSigningAlgorithms...), + signingAlgorithmSet: signingAlgorithmSet, + requestTTL: cfg.RequestTTL, codeTTL: cfg.CodeTTL, now: cfg.Now, + }, nil +} + +func (s *Service) signingAlgorithmSupported(algorithm domain.TelegramLoginSigningAlgorithm) bool { + if !algorithm.Valid() { + return false + } + if s.signingAlgorithmSet == nil { + return true + } + _, ok := s.signingAlgorithmSet[algorithm] + return ok +} + +func (s *Service) defaultSigningAlgorithm() (domain.TelegramLoginSigningAlgorithm, bool) { + if s.signingAlgorithmSupported(domain.TelegramLoginSigningRS256) { + return domain.TelegramLoginSigningRS256, true + } + if len(s.signingAlgorithms) > 0 { + return s.signingAlgorithms[0], true + } + return "", false +} + +func validAppScheme(value string) bool { + if value == "" || strings.ToLower(value) != value { + return false + } + for i, r := range value { + if (r >= 'a' && r <= 'z') || (i > 0 && r >= '0' && r <= '9') || (i > 0 && (r == '+' || r == '-' || r == '.')) { + continue + } + return false + } + return true +} + +type ClientCredentials struct { + Client domain.TelegramLoginClient + Secret string +} + +type ClientConfiguration struct { + Client domain.TelegramLoginClient + AllowedURLs []domain.TelegramLoginAllowedURL + NativeApps []domain.TelegramLoginNativeApp +} + +// EnsureClient creates the bot's OIDC client once. Concurrent BotFather +// sessions converge on the durable winner; only the creator receives the +// one-time secret. +func (s *Service) EnsureClient(ctx context.Context, botUserID int64) (ClientCredentials, bool, error) { + if client, found, err := s.store.GetTelegramLoginClientByBot(ctx, botUserID); err != nil { + return ClientCredentials{}, false, err + } else if found { + return ClientCredentials{Client: client}, false, nil + } + algorithm, ok := s.defaultSigningAlgorithm() + if !ok { + return ClientCredentials{}, false, domain.ErrTelegramLoginClientInvalid + } + created, err := s.CreateClient(ctx, botUserID, algorithm) + if err == nil { + return created, true, nil + } + if !errors.Is(err, domain.ErrTelegramLoginRequestConflict) { + return ClientCredentials{}, false, err + } + client, found, readErr := s.store.GetTelegramLoginClientByBot(ctx, botUserID) + if readErr != nil { + return ClientCredentials{}, false, readErr + } + if !found { + return ClientCredentials{}, false, err + } + return ClientCredentials{Client: client}, false, nil +} + +func (s *Service) ClientConfiguration(ctx context.Context, botUserID int64) (ClientConfiguration, bool, error) { + client, found, err := s.store.GetTelegramLoginClientByBot(ctx, botUserID) + if err != nil || !found { + return ClientConfiguration{}, found, err + } + allowed, err := s.store.ListTelegramLoginAllowedURLs(ctx, botUserID) + if err != nil { + return ClientConfiguration{}, false, err + } + apps, err := s.store.ListTelegramLoginNativeApps(ctx, botUserID) + if err != nil { + return ClientConfiguration{}, false, err + } + return ClientConfiguration{Client: client, AllowedURLs: allowed, NativeApps: apps}, true, nil +} + +func (s *Service) CreateClient(ctx context.Context, botUserID int64, algorithm domain.TelegramLoginSigningAlgorithm) (ClientCredentials, error) { + if botUserID <= 0 || !s.signingAlgorithmSupported(algorithm) { + return ClientCredentials{}, domain.ErrTelegramLoginClientInvalid + } + now := s.now().UTC() + return s.createClientWithSecret(ctx, domain.TelegramLoginClient{ + BotUserID: botUserID, ClientID: strconv.FormatInt(botUserID, 10), + SecretVersion: 1, SigningAlgorithm: algorithm, Enabled: true, + CreatedAt: now, UpdatedAt: now, + }) +} + +func (s *Service) RotateClientSecret(ctx context.Context, botUserID int64) (ClientCredentials, error) { + client, found, err := s.store.GetTelegramLoginClientByBot(ctx, botUserID) + if err != nil { + return ClientCredentials{}, err + } + if !found { + return ClientCredentials{}, domain.ErrTelegramLoginClientInvalid + } + secret, err := GenerateOpaqueToken() + if err != nil { + return ClientCredentials{}, err + } + secretHash, err := HashClientSecret(s.clientSecretPepper, secret) + if err != nil { + return ClientCredentials{}, err + } + client, err = s.store.RotateTelegramLoginClientSecret(ctx, botUserID, client.SecretVersion, secretHash, s.now().UTC()) + if err != nil { + return ClientCredentials{}, err + } + return ClientCredentials{Client: client, Secret: secret}, nil +} + +func (s *Service) createClientWithSecret(ctx context.Context, client domain.TelegramLoginClient) (ClientCredentials, error) { + secret, err := GenerateOpaqueToken() + if err != nil { + return ClientCredentials{}, err + } + client.SecretHash, err = HashClientSecret(s.clientSecretPepper, secret) + if err != nil { + return ClientCredentials{}, err + } + client, err = s.store.CreateTelegramLoginClient(ctx, client) + if err != nil { + return ClientCredentials{}, err + } + return ClientCredentials{Client: client, Secret: secret}, nil +} + +func (s *Service) AddAllowedURL(ctx context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, raw string) (domain.TelegramLoginAllowedURL, error) { + var normalized string + var err error + switch kind { + case domain.TelegramLoginAllowedWebOrigin: + normalized, err = NormalizeWebOrigin(raw, s.allowLoopbackHTTP) + case domain.TelegramLoginAllowedRedirectURI: + normalized, _, err = NormalizeRedirectURI(raw, s.allowLoopbackHTTP) + default: + err = domain.ErrTelegramLoginURLInvalid + } + if err != nil { + return domain.TelegramLoginAllowedURL{}, err + } + return s.store.AddTelegramLoginAllowedURL(ctx, domain.TelegramLoginAllowedURL{ + BotUserID: botUserID, Kind: kind, NormalizedURL: normalized, CreatedAt: s.now().UTC(), + }) +} + +func (s *Service) DeleteAllowedURL(ctx context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, raw string) (bool, error) { + var normalized string + var err error + switch kind { + case domain.TelegramLoginAllowedWebOrigin: + normalized, err = NormalizeWebOrigin(raw, s.allowLoopbackHTTP) + case domain.TelegramLoginAllowedRedirectURI: + normalized, _, err = NormalizeRedirectURI(raw, s.allowLoopbackHTTP) + default: + err = domain.ErrTelegramLoginURLInvalid + } + if err != nil { + return false, err + } + return s.store.DeleteTelegramLoginAllowedURL(ctx, botUserID, kind, normalized) +} + +func (s *Service) SetClientEnabled(ctx context.Context, botUserID int64, enabled bool) error { + if enabled { + client, found, err := s.store.GetTelegramLoginClientByBot(ctx, botUserID) + if err != nil { + return err + } + if !found || !s.signingAlgorithmSupported(client.SigningAlgorithm) { + return domain.ErrTelegramLoginClientInvalid + } + } + return s.store.SetTelegramLoginClientEnabled(ctx, botUserID, enabled, s.now().UTC()) +} + +func (s *Service) SetClientSigningAlgorithm(ctx context.Context, botUserID int64, algorithm domain.TelegramLoginSigningAlgorithm) (domain.TelegramLoginClient, error) { + if botUserID <= 0 || !s.signingAlgorithmSupported(algorithm) { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + return s.store.SetTelegramLoginClientSigningAlgorithm(ctx, botUserID, algorithm, s.now().UTC()) +} + +func (s *Service) AddNativeApp(ctx context.Context, botUserID int64, platform domain.TelegramLoginNativePlatform, applicationID, verificationID, callbackURI, displayName string) (domain.TelegramLoginNativeApp, error) { + if botUserID <= 0 || !platform.Valid() { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginClientInvalid + } + applicationID, err := normalizeNativeApplicationID(applicationID) + if err != nil { + return domain.TelegramLoginNativeApp{}, err + } + verificationID, err = normalizeNativeVerificationID(platform, verificationID) + if err != nil { + return domain.TelegramLoginNativeApp{}, err + } + callbackURI, err = NormalizeNativeCallbackURI(callbackURI, s.allowLoopbackHTTP) + if err != nil { + return domain.TelegramLoginNativeApp{}, err + } + displayName, err = normalizeNativeDisplayName(displayName) + if err != nil { + return domain.TelegramLoginNativeApp{}, err + } + now := s.now().UTC() + return s.store.UpsertTelegramLoginNativeApp(ctx, domain.TelegramLoginNativeApp{ + BotUserID: botUserID, Platform: platform, ApplicationID: applicationID, + VerificationID: verificationID, CallbackURI: callbackURI, VerifiedDisplayName: displayName, + Enabled: true, CreatedAt: now, UpdatedAt: now, + }) +} + +func (s *Service) DeleteNativeApp(ctx context.Context, botUserID, appID int64) (bool, error) { + if botUserID <= 0 || appID <= 0 { + return false, domain.ErrTelegramLoginClientInvalid + } + return s.store.DeleteTelegramLoginNativeApp(ctx, botUserID, appID) +} + +func (s *Service) matchNativeApp(ctx context.Context, botUserID int64, platform domain.TelegramLoginNativePlatform, rawCallbackURI string) (domain.TelegramLoginNativeApp, string, bool, error) { + callbackURI, err := NormalizeNativeCallbackURI(rawCallbackURI, s.allowLoopbackHTTP) + if err != nil { + return domain.TelegramLoginNativeApp{}, "", false, nil + } + apps, err := s.store.ListTelegramLoginNativeApps(ctx, botUserID) + if err != nil { + return domain.TelegramLoginNativeApp{}, "", false, err + } + for _, app := range apps { + if app.Enabled && app.CallbackURI == callbackURI && (!platform.Valid() || app.Platform == platform) { + return app, callbackURI, true, nil + } + } + return domain.TelegramLoginNativeApp{}, callbackURI, false, nil +} + +// ValidateMessageButton verifies the linked-domain invariant for a legacy +// Bot API login_url button. Bot buttons are origin-bound (not exact callback +// URI-bound): the final signed user fields are appended to the original URL. +func (s *Service) ValidateMessageButton(ctx context.Context, botUserID int64, rawURL string) (normalizedURL, domainName string, err error) { + client, found, err := s.store.GetTelegramLoginClientByBot(ctx, botUserID) + if err != nil { + return "", "", err + } + if !found || !client.Enabled { + return "", "", domain.ErrTelegramLoginClientDisabled + } + normalizedURL, domainName, err = NormalizeRedirectURI(rawURL, s.allowLoopbackHTTP) + if err != nil { + return "", "", err + } + u, err := url.Parse(normalizedURL) + if err != nil { + return "", "", domain.ErrTelegramLoginURLInvalid + } + origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowLoopbackHTTP) + if err != nil { + return "", "", err + } + allowed, err := s.store.IsTelegramLoginURLAllowed(ctx, botUserID, domain.TelegramLoginAllowedWebOrigin, origin) + if err != nil { + return "", "", err + } + if !allowed { + return "", "", domain.ErrTelegramLoginOriginNotAllowed + } + return normalizedURL, domainName, nil +} + +// AuthorizeMessageButton implements the legacy Seamless Login button path. +// It creates a short-lived internal request and immediately performs the same +// transactional approval used by OIDC, so web_authorizations and optional bot +// write access cannot diverge. The returned URL uses Telegram's documented +// legacy HMAC format and is independent from authorization-code/PKCE tokens. +func (s *Service) AuthorizeMessageButton(ctx context.Context, params domain.TelegramLoginMessageButtonAuthorization) (domain.TelegramLoginMessageButtonResult, error) { + if params.UserID <= 0 || params.BotUserID <= 0 || params.Identity.UserID != params.UserID || + params.MessageID <= 0 || params.ButtonID < 0 || params.Peer.ID <= 0 || + (params.Peer.Type != domain.PeerTypeUser && params.Peer.Type != domain.PeerTypeChannel) || + params.WriteAllowed && !params.RequestWriteAccess { + return domain.TelegramLoginMessageButtonResult{}, domain.ErrTelegramLoginRequestInvalid + } + tokenBotID, _, ok := domain.ParseBotToken(params.BotToken) + if !ok || tokenBotID != params.BotUserID { + return domain.TelegramLoginMessageButtonResult{}, domain.ErrTelegramLoginSecretInvalid + } + normalizedURL, domainName, err := s.ValidateMessageButton(ctx, params.BotUserID, params.URL) + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + client, found, err := s.store.GetTelegramLoginClientByBot(ctx, params.BotUserID) + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + if !found || !client.Enabled { + return domain.TelegramLoginMessageButtonResult{}, domain.ErrTelegramLoginClientDisabled + } + u, err := url.Parse(normalizedURL) + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, domain.ErrTelegramLoginURLInvalid + } + origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowLoopbackHTTP) + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + requestToken, err := GenerateOpaqueToken() + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + browserToken, err := GenerateOpaqueToken() + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + scopes := []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile} + if params.RequestWriteAccess { + scopes = append(scopes, domain.TelegramLoginScopeBotAccess) + } + now := s.now().UTC() + request, err := s.store.CreateTelegramLoginRequest(ctx, domain.TelegramLoginRequest{ + RequestTokenHash: HashOpaqueToken(requestToken), BrowserTokenHash: HashOpaqueToken(browserToken), + BotUserID: params.BotUserID, ClientID: client.ClientID, SigningAlgorithm: client.SigningAlgorithm, + Source: domain.TelegramLoginRequestMessageButton, ResponseType: "legacy_url", + RedirectURI: normalizedURL, Origin: origin, Domain: domainName, Scopes: scopes, + Browser: boundedValue(params.Browser, "Telegram", 255), Platform: boundedValue(params.Platform, "Telegram Client", 255), + IP: boundedValue(params.IP, "Unknown IP", 128), Region: boundedValue(params.Region, "Unknown region", 255), + UserIDHint: params.UserID, PeerType: params.Peer.Type, PeerID: params.Peer.ID, + MessageID: params.MessageID, ButtonID: params.ButtonID, + Status: domain.TelegramLoginRequestPending, CreatedAt: now, ExpiresAt: now.Add(s.requestTTL), + }) + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + hash, err := randomWebAuthorizationHash() + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + approved, webAuthorization, err := s.store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{ + RequestID: request.ID, Identity: params.Identity, WriteAllowed: params.WriteAllowed, ApprovedAt: now, + }, hash) + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + acceptedURL, err := appendLegacyTelegramLoginResult(normalizedURL, approved, params.BotToken) + if err != nil { + return domain.TelegramLoginMessageButtonResult{}, err + } + return domain.TelegramLoginMessageButtonResult{URL: acceptedURL, Request: approved, WebAuthorization: webAuthorization}, nil +} + +func appendLegacyTelegramLoginResult(rawURL string, request domain.TelegramLoginRequest, botToken string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil || !u.IsAbs() || request.Status != domain.TelegramLoginRequestApproved || request.AuthorizedUserID <= 0 || request.ApprovedAt.IsZero() { + return "", domain.ErrTelegramLoginRequestInvalid + } + values := map[string]string{ + "auth_date": strconv.FormatInt(request.ApprovedAt.Unix(), 10), + "first_name": request.GivenName, + "id": strconv.FormatInt(request.AuthorizedUserID, 10), + } + if request.FamilyName != "" { + values["last_name"] = request.FamilyName + } + if request.PreferredUsername != "" { + values["username"] = request.PreferredUsername + } + if request.Picture != "" { + values["photo_url"] = request.Picture + } + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + check := make([]string, 0, len(keys)) + for _, key := range keys { + check = append(check, key+"="+values[key]) + } + secret := sha256.Sum256([]byte(botToken)) + mac := hmac.New(sha256.New, secret[:]) + _, _ = mac.Write([]byte(strings.Join(check, "\n"))) + query := u.Query() + for key, value := range values { + query.Set(key, value) + } + query.Set("hash", hex.EncodeToString(mac.Sum(nil))) + u.RawQuery = query.Encode() + return u.String(), nil +} + +type CreateAuthorizationParams struct { + ClientID string + RedirectURI string + ResponseType string + Scope string + State string + Nonce string + CodeChallenge string + CodeChallengeMethod string + Origin string + InAppOrigin string + Source domain.TelegramLoginRequestSource + Browser string + Platform string + IP string + Region string + UserIDHint int64 + NativePlatform domain.TelegramLoginNativePlatform + IncludeMatchCodes bool + MatchCodesFirst bool +} + +type CreatedAuthorization struct { + Request domain.TelegramLoginRequest + RequestToken string + BrowserToken string + DeepLink string +} + +func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthorizationParams) (CreatedAuthorization, error) { + client, found, err := s.store.GetTelegramLoginClient(ctx, params.ClientID) + if err != nil { + return CreatedAuthorization{}, err + } + if !found || !client.Enabled { + return CreatedAuthorization{}, domain.ErrTelegramLoginClientDisabled + } + if !s.signingAlgorithmSupported(client.SigningAlgorithm) { + return CreatedAuthorization{}, domain.ErrTelegramLoginClientDisabled + } + if params.ResponseType != "code" && params.ResponseType != "post_message" { + return CreatedAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + source := params.Source + if source == "" { + source = domain.TelegramLoginRequestWeb + } + var allowed, isApp bool + var nativeApp domain.TelegramLoginNativeApp + redirectURI, domainName, redirectErr := NormalizeRedirectURI(params.RedirectURI, s.allowLoopbackHTTP) + if params.ResponseType == "code" && redirectErr == nil && !params.NativePlatform.Valid() { + allowed, err = s.store.IsTelegramLoginURLAllowed(ctx, client.BotUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI) + if err != nil { + return CreatedAuthorization{}, err + } + } + if params.ResponseType == "code" && (!allowed || params.NativePlatform.Valid()) { + nativeApp, redirectURI, isApp, err = s.matchNativeApp(ctx, client.BotUserID, params.NativePlatform, params.RedirectURI) + if err != nil { + return CreatedAuthorization{}, err + } + if isApp { + source, domainName = domain.TelegramLoginRequestNative, nativeApp.ApplicationID + } + } + if params.ResponseType == "code" && !allowed && !isApp { + if redirectErr != nil { + return CreatedAuthorization{}, redirectErr + } + return CreatedAuthorization{}, domain.ErrTelegramLoginRedirectNotAllowed + } + if params.ResponseType == "post_message" && redirectErr != nil { + return CreatedAuthorization{}, redirectErr + } + if params.NativePlatform.Valid() && !isApp || source == domain.TelegramLoginRequestNative && !isApp { + return CreatedAuthorization{}, domain.ErrTelegramLoginRedirectNotAllowed + } + scopeValue := params.Scope + if isApp && !slices.Contains(strings.Fields(scopeValue), string(domain.TelegramLoginScopeOpenID)) { + scopeValue = string(domain.TelegramLoginScopeOpenID) + " " + scopeValue + } + scopes, err := ParseScopes(scopeValue, client.SigningAlgorithm) + if err != nil { + return CreatedAuthorization{}, err + } + if params.ResponseType == "code" || params.CodeChallenge != "" || params.CodeChallengeMethod != "" { + if err := ValidatePKCEChallenge(params.CodeChallenge, params.CodeChallengeMethod); err != nil { + return CreatedAuthorization{}, err + } + } + if len(params.State) > 2048 || len(params.Nonce) > 1024 || params.UserIDHint < 0 { + return CreatedAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + origin := "" + if !isApp { + origin = params.Origin + if origin == "" { + u, _ := url.Parse(redirectURI) + origin = u.Scheme + "://" + u.Host + } + origin, err = NormalizeWebOrigin(origin, s.allowLoopbackHTTP) + if err != nil { + return CreatedAuthorization{}, err + } + } + if params.ResponseType == "post_message" { + redirectURL, _ := url.Parse(redirectURI) + redirectOrigin, redirectOriginErr := NormalizeWebOrigin(redirectURL.Scheme+"://"+redirectURL.Host, s.allowLoopbackHTTP) + if redirectOriginErr != nil || redirectOrigin != origin { + return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + allowed, err = s.store.IsTelegramLoginURLAllowed(ctx, client.BotUserID, domain.TelegramLoginAllowedWebOrigin, origin) + if err != nil { + return CreatedAuthorization{}, err + } + if !allowed { + return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + } + inAppOrigin := "" + if params.InAppOrigin != "" { + if isApp { + return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + inAppOrigin, err = NormalizeWebOrigin(params.InAppOrigin, s.allowLoopbackHTTP) + if err != nil { + return CreatedAuthorization{}, err + } + allowed, err = s.store.IsTelegramLoginURLAllowed(ctx, client.BotUserID, domain.TelegramLoginAllowedWebOrigin, inAppOrigin) + if err != nil { + return CreatedAuthorization{}, err + } + if !allowed { + return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + } + requestToken, err := GenerateOpaqueToken() + if err != nil { + return CreatedAuthorization{}, err + } + browserToken, err := GenerateOpaqueToken() + if err != nil { + return CreatedAuthorization{}, err + } + matchCodes, matchCode, err := generateMatchCodes(params.IncludeMatchCodes) + if err != nil { + return CreatedAuthorization{}, err + } + now := s.now().UTC() + request := domain.TelegramLoginRequest{ + RequestTokenHash: HashOpaqueToken(requestToken), BrowserTokenHash: HashOpaqueToken(browserToken), + BotUserID: client.BotUserID, ClientID: client.ClientID, SigningAlgorithm: client.SigningAlgorithm, + Source: source, ResponseType: params.ResponseType, RedirectURI: redirectURI, Origin: origin, Domain: domainName, + Scopes: scopes, State: params.State, Nonce: params.Nonce, + CodeChallenge: params.CodeChallenge, CodeChallengeMethod: params.CodeChallengeMethod, + Browser: boundedValue(params.Browser, "Unknown browser", 255), Platform: boundedValue(params.Platform, "Unknown platform", 255), + IP: boundedValue(params.IP, "Unknown IP", 128), Region: boundedValue(params.Region, "Unknown region", 255), + InAppOrigin: inAppOrigin, IsApp: isApp, VerifiedAppName: nativeApp.VerifiedDisplayName, + MatchCodes: matchCodes, MatchCode: matchCode, MatchCodesFirst: params.MatchCodesFirst && len(matchCodes) > 0, + UserIDHint: params.UserIDHint, Status: domain.TelegramLoginRequestPending, + CreatedAt: now, ExpiresAt: now.Add(s.requestTTL), + } + request, err = s.store.CreateTelegramLoginRequest(ctx, request) + if err != nil { + return CreatedAuthorization{}, err + } + deepLink := s.appScheme + "://oauth?token=" + url.QueryEscape(requestToken) + return CreatedAuthorization{Request: request, RequestToken: requestToken, BrowserToken: browserToken, DeepLink: deepLink}, nil +} + +type AuthorizationErrorTarget struct { + ResponseType string + RedirectURI string + Origin string +} + +// ResolveAuthorizationErrorTarget validates an OAuth error destination +// independently of the invalid parameter that caused the request to fail. It +// prevents redirect and postMessage error handling from becoming an open +// redirect/origin oracle. +func (s *Service) ResolveAuthorizationErrorTarget(ctx context.Context, clientID, responseType, rawRedirectURI, rawOrigin string) (AuthorizationErrorTarget, bool, error) { + client, found, err := s.store.GetTelegramLoginClient(ctx, clientID) + if err != nil || !found || !client.Enabled { + return AuthorizationErrorTarget{}, false, err + } + switch responseType { + case "code": + redirectURI, safe, err := s.safeCodeRedirect(ctx, client.BotUserID, rawRedirectURI) + return AuthorizationErrorTarget{ResponseType: responseType, RedirectURI: redirectURI}, safe, err + case "post_message": + redirectURI, _, err := NormalizeRedirectURI(rawRedirectURI, s.allowLoopbackHTTP) + if err != nil { + return AuthorizationErrorTarget{}, false, nil + } + origin := rawOrigin + if origin == "" { + redirect, _ := url.Parse(redirectURI) + origin = redirect.Scheme + "://" + redirect.Host + } + origin, err = NormalizeWebOrigin(origin, s.allowLoopbackHTTP) + if err != nil { + return AuthorizationErrorTarget{}, false, nil + } + redirect, _ := url.Parse(redirectURI) + redirectOrigin, err := NormalizeWebOrigin(redirect.Scheme+"://"+redirect.Host, s.allowLoopbackHTTP) + if err != nil || redirectOrigin != origin { + return AuthorizationErrorTarget{}, false, nil + } + allowed, err := s.store.IsTelegramLoginURLAllowed(ctx, client.BotUserID, domain.TelegramLoginAllowedWebOrigin, origin) + if err != nil || !allowed { + return AuthorizationErrorTarget{}, false, err + } + return AuthorizationErrorTarget{ResponseType: responseType, RedirectURI: redirectURI, Origin: origin}, true, nil + default: + // An unsupported response_type may still report the error through a + // pre-registered redirect URI, per OAuth 2.0. + redirectURI, safe, err := s.safeCodeRedirect(ctx, client.BotUserID, rawRedirectURI) + return AuthorizationErrorTarget{ResponseType: "code", RedirectURI: redirectURI}, safe, err + } +} + +func (s *Service) safeCodeRedirect(ctx context.Context, botUserID int64, raw string) (string, bool, error) { + if redirectURI, _, err := NormalizeRedirectURI(raw, s.allowLoopbackHTTP); err == nil { + allowed, err := s.store.IsTelegramLoginURLAllowed(ctx, botUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI) + if err != nil || allowed { + return redirectURI, allowed, err + } + } + _, redirectURI, allowed, err := s.matchNativeApp(ctx, botUserID, "", raw) + return redirectURI, allowed, err +} + +func ParseScopes(raw string, algorithm domain.TelegramLoginSigningAlgorithm) ([]domain.TelegramLoginScope, error) { + fields := strings.Fields(raw) + if len(fields) == 0 || len(fields) > 4 { + return nil, domain.ErrTelegramLoginScopeInvalid + } + set := make(map[domain.TelegramLoginScope]struct{}, len(fields)) + for _, field := range fields { + if field == "write" { + field = string(domain.TelegramLoginScopeBotAccess) + } + scope := domain.TelegramLoginScope(field) + if !scope.Valid() { + return nil, domain.ErrTelegramLoginScopeInvalid + } + if _, duplicate := set[scope]; duplicate { + return nil, domain.ErrTelegramLoginScopeInvalid + } + set[scope] = struct{}{} + } + ordered := make([]domain.TelegramLoginScope, 0, len(set)) + for _, scope := range []domain.TelegramLoginScope{ + domain.TelegramLoginScopeOpenID, + domain.TelegramLoginScopeProfile, + domain.TelegramLoginScopePhone, + domain.TelegramLoginScopeBotAccess, + } { + if _, ok := set[scope]; ok { + ordered = append(ordered, scope) + } + } + if err := domain.ValidateTelegramLoginScopes(ordered, algorithm); err != nil { + return nil, err + } + return ordered, nil +} + +func boundedValue(value, fallback string, max int) string { + value = strings.TrimSpace(strings.ToValidUTF8(value, "�")) + if value == "" { + value = fallback + } + for len(value) > max { + _, size := utf8.DecodeLastRuneInString(value) + value = value[:len(value)-size] + } + return value +} + +func generateMatchCodes(enabled bool) ([]string, string, error) { + if !enabled { + return []string{}, "", nil + } + pool := append([]string(nil), telegramLoginMatchCodePool...) + for i := len(pool) - 1; i > 0; i-- { + n, err := cryptoRandInt(i + 1) + if err != nil { + return nil, "", err + } + pool[i], pool[n] = pool[n], pool[i] + } + codes := append([]string(nil), pool[:5]...) + selected, err := cryptoRandInt(len(codes)) + if err != nil { + return nil, "", err + } + return codes, codes[selected], nil +} + +func cryptoRandInt(max int) (int, error) { + if max <= 0 { + return 0, fmt.Errorf("invalid crypto random bound") + } + var raw [8]byte + limit := ^uint64(0) - (^uint64(0) % uint64(max)) + for { + if _, err := rand.Read(raw[:]); err != nil { + return 0, fmt.Errorf("crypto random integer: %w", err) + } + value := binary.LittleEndian.Uint64(raw[:]) + if value < limit { + return int(value % uint64(max)), nil + } + } +} + +func (s *Service) RequestByDeepLink(ctx context.Context, rawURL string) (domain.TelegramLoginRequest, error) { + token, err := s.deepLinkToken(rawURL) + if err != nil { + return domain.TelegramLoginRequest{}, err + } + request, found, err := s.store.GetTelegramLoginRequestByTokenHash(ctx, HashOpaqueToken(token)) + if err != nil { + return domain.TelegramLoginRequest{}, err + } + if !found { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestInvalid + } + if !s.now().Before(request.ExpiresAt) { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestExpired + } + return request, nil +} + +// RequestByDeepLinkForOrigin resolves an OAuth deep link and proves the +// immutable Mini App origin that was bound when /inapp created the request. +// A copied deep link cannot be approved from another origin, and a normal web +// request can never be mutated into a Mini App request by a later MTProto call. +func (s *Service) RequestByDeepLinkForOrigin(ctx context.Context, rawURL, rawOrigin string) (domain.TelegramLoginRequest, error) { + request, err := s.RequestByDeepLink(ctx, rawURL) + if err != nil { + return domain.TelegramLoginRequest{}, err + } + if request.Source != domain.TelegramLoginRequestMiniApp { + if rawOrigin != "" || request.InAppOrigin != "" { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginOriginNotAllowed + } + return request, nil + } + if rawOrigin == "" || request.InAppOrigin == "" { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginOriginNotAllowed + } + origin, err := NormalizeWebOrigin(rawOrigin, s.allowLoopbackHTTP) + if err != nil { + return domain.TelegramLoginRequest{}, err + } + allowed, err := s.store.IsTelegramLoginURLAllowed(ctx, request.BotUserID, domain.TelegramLoginAllowedWebOrigin, origin) + if err != nil { + return domain.TelegramLoginRequest{}, err + } + if !allowed { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginOriginNotAllowed + } + if request.InAppOrigin != origin { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginOriginNotAllowed + } + return request, nil +} + +func (s *Service) deepLinkToken(rawURL string) (string, error) { + if rawURL == "" || len(rawURL) > maxTelegramLoginURLLength || rawURL != strings.TrimSpace(rawURL) { + return "", domain.ErrTelegramLoginURLInvalid + } + u, err := url.Parse(rawURL) + if err != nil || u.Fragment != "" || u.User != nil { + return "", domain.ErrTelegramLoginURLInvalid + } + query, err := url.ParseQuery(u.RawQuery) + if err != nil { + return "", domain.ErrTelegramLoginURLInvalid + } + customOrCanonicalScheme := strings.EqualFold(u.Scheme, s.appScheme) || strings.EqualFold(u.Scheme, "tg") + var token string + switch { + case customOrCanonicalScheme && strings.EqualFold(u.Host, "oauth") && u.Path == "": + token, _ = singleQueryValue(query, "token") + case customOrCanonicalScheme && strings.EqualFold(u.Host, "resolve") && u.Path == "": + domainValue, domainOK := singleQueryValue(query, "domain") + startApp, startAppOK := singleQueryValue(query, "startapp") + if domainOK && startAppOK && strings.EqualFold(domainValue, "oauth") { + token = startApp + } + case strings.EqualFold(u.Scheme, "https") && (strings.EqualFold(u.Hostname(), "t.me") || strings.EqualFold(u.Hostname(), "telegram.me")) && strings.Trim(u.Path, "/") == "oauth": + token, _ = singleQueryValue(query, "startapp") + default: + return "", domain.ErrTelegramLoginURLInvalid + } + if len(token) < 16 || len(token) > 1024 || strings.IndexFunc(token, func(r rune) bool { return r <= 0x20 || r == 0x7f }) >= 0 { + return "", domain.ErrTelegramLoginURLInvalid + } + return token, nil +} + +func singleQueryValue(query url.Values, name string) (string, bool) { + values, ok := query[name] + if !ok || len(values) != 1 { + return "", false + } + return values[0], true +} + +func (s *Service) CheckMatchCode(ctx context.Context, deepLink, selected string) (bool, error) { + request, err := s.RequestByDeepLink(ctx, deepLink) + if err != nil { + return false, err + } + if !request.MatchCodesFirst || len(request.MatchCodes) == 0 || !slices.Contains(request.MatchCodes, selected) { + return false, domain.ErrTelegramLoginMatchCodeInvalid + } + if subtle.ConstantTimeCompare([]byte(selected), []byte(request.MatchCode)) != 1 { + return false, domain.ErrTelegramLoginMatchCodeInvalid + } + return true, nil +} + +func (s *Service) Approve(ctx context.Context, deepLink string, identity domain.TelegramLoginIdentitySnapshot, writeAllowed, phoneShared bool, matchCode string) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) { + request, err := s.RequestByDeepLink(ctx, deepLink) + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, err + } + hash, err := randomWebAuthorizationHash() + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, err + } + return s.store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{ + RequestID: request.ID, Identity: identity, WriteAllowed: writeAllowed, + PhoneShared: phoneShared, MatchCode: matchCode, ApprovedAt: s.now().UTC(), + }, hash) +} + +func randomWebAuthorizationHash() (int64, error) { + var raw [8]byte + if _, err := rand.Read(raw[:]); err != nil { + return 0, fmt.Errorf("generate web authorization hash: %w", err) + } + value := int64(binary.LittleEndian.Uint64(raw[:]) & uint64(^uint64(0)>>1)) + if value == 0 { + value = 1 + } + return value, nil +} + +func (s *Service) Decline(ctx context.Context, deepLink string, userID int64) (domain.TelegramLoginRequest, error) { + request, err := s.RequestByDeepLink(ctx, deepLink) + if err != nil { + return domain.TelegramLoginRequest{}, err + } + return s.store.DeclineTelegramLoginRequest(ctx, request.ID, userID, s.now().UTC()) +} + +type FinalizedAuthorization struct { + Request domain.TelegramLoginRequest + Code string + RedirectURL string +} + +func (s *Service) FinalizeByBrowserToken(ctx context.Context, browserToken string) (FinalizedAuthorization, error) { + request, found, err := s.store.GetTelegramLoginRequestByBrowserTokenHash(ctx, HashOpaqueToken(browserToken)) + if err != nil { + return FinalizedAuthorization{}, err + } + if !found || request.Status != domain.TelegramLoginRequestApproved || request.ResponseType != "code" { + return FinalizedAuthorization{}, domain.ErrTelegramLoginRequestConflict + } + return s.finalizeAuthorizationRequest(ctx, request) +} + +// FinalizeRedirectByDeepLink is used by native SDK cross-app flows: there is +// no browser poller, so messages.acceptUrlAuth must return the exact registered +// callback URI with a one-time authorization code. +func (s *Service) FinalizeRedirectByDeepLink(ctx context.Context, deepLink string) (string, error) { + token, err := s.deepLinkToken(deepLink) + if err != nil { + return "", err + } + request, found, err := s.store.GetTelegramLoginRequestByTokenHash(ctx, HashOpaqueToken(token)) + if err != nil { + return "", err + } + if !found || request.Status != domain.TelegramLoginRequestApproved || request.ResponseType != "code" || request.Source != domain.TelegramLoginRequestNative || !request.IsApp { + return "", domain.ErrTelegramLoginRequestConflict + } + finalized, err := s.finalizeAuthorizationRequest(ctx, request) + return finalized.RedirectURL, err +} + +// FinalizeInAppRedirectByDeepLink returns the result_url consumed by the +// official JavaScript SDK after the Telegram client emits +// oauth_result_confirmed. The URL contains only a short-lived one-time token; +// the ID token is signed and returned by /inapp after the webview proves the +// immutable requesting origin again. +func (s *Service) FinalizeInAppRedirectByDeepLink(ctx context.Context, deepLink string) (string, error) { + token, err := s.deepLinkToken(deepLink) + if err != nil { + return "", err + } + request, found, err := s.store.GetTelegramLoginRequestByTokenHash(ctx, HashOpaqueToken(token)) + if err != nil { + return "", err + } + if !found || request.Status != domain.TelegramLoginRequestApproved || + request.Source != domain.TelegramLoginRequestMiniApp || request.ResponseType != "post_message" || request.InAppOrigin == "" { + return "", domain.ErrTelegramLoginRequestConflict + } + directToken, err := s.finalizeInAppToken(ctx, request) + if err != nil { + return "", err + } + return s.issuer + "/inapp?token=" + url.QueryEscape(directToken), nil +} + +func (s *Service) finalizeInAppToken(ctx context.Context, request domain.TelegramLoginRequest) (string, error) { + if existing, found, err := s.store.GetTelegramLoginAuthorizationCodeByRequest(ctx, request.ID); err != nil { + return "", err + } else if found { + if !existing.ConsumedAt.IsZero() { + return "", domain.ErrTelegramLoginCodeConsumed + } + if !s.now().Before(existing.ExpiresAt) { + return "", domain.ErrTelegramLoginCodeInvalid + } + existing, err = s.store.PutTelegramLoginAuthorizationCode(ctx, existing) + if err != nil { + return "", err + } + return s.openCode(request, existing) + } + token, err := GenerateOpaqueToken() + if err != nil { + return "", err + } + sealed, nonce, keyID, err := s.sealer.Seal(token, codeAAD(request)) + if err != nil { + return "", err + } + now := s.now().UTC() + stored, err := s.store.PutTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginAuthorizationCode{ + RequestID: request.ID, CodeHash: HashOpaqueToken(token), SealedCode: sealed, + SealNonce: nonce, SealKeyID: keyID, IssuedAt: now, ExpiresAt: now.Add(s.codeTTL), + }) + if err != nil { + return "", err + } + return s.openCode(request, stored) +} + +// ExchangeInAppTokenAndIssue performs the second official /inapp step. The +// direct token is one-time across all instances and remains bound to the +// original registered webview origin and active Web authorization. +func (s *Service) ExchangeInAppTokenAndIssue(ctx context.Context, token, rawOrigin string, issuer *IDTokenIssuer) (IssuedAuthorization, error) { + if issuer == nil || len(token) < 16 || len(token) > 1024 || strings.IndexFunc(token, func(r rune) bool { return r <= 0x20 || r == 0x7f }) >= 0 { + return IssuedAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + origin, err := NormalizeWebOrigin(rawOrigin, s.allowLoopbackHTTP) + if err != nil { + return IssuedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + hash := HashOpaqueToken(token) + stored, found, err := s.store.GetTelegramLoginAuthorizationCodeByHash(ctx, hash) + if err != nil { + return IssuedAuthorization{}, err + } + if !found { + return IssuedAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + request, found, err := s.store.GetTelegramLoginRequest(ctx, stored.RequestID) + if err != nil { + return IssuedAuthorization{}, err + } + if !found || request.Source != domain.TelegramLoginRequestMiniApp || request.ResponseType != "post_message" || request.InAppOrigin != origin { + return IssuedAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + opened, err := s.openCode(request, stored) + if err != nil || subtle.ConstantTimeCompare([]byte(opened), []byte(token)) != 1 { + return IssuedAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + idToken, err := issuer.Issue(request) + if err != nil { + return IssuedAuthorization{}, err + } + _, consumed, web, err := s.store.ConsumeTelegramLoginDirectToken(ctx, hash, origin, s.now().UTC()) + if err != nil { + return IssuedAuthorization{}, err + } + return IssuedAuthorization{ + ExchangedAuthorization: ExchangedAuthorization{Request: consumed, WebAuthorization: web}, + IDToken: idToken, + }, nil +} + +func (s *Service) finalizeAuthorizationRequest(ctx context.Context, request domain.TelegramLoginRequest) (FinalizedAuthorization, error) { + if existing, found, err := s.store.GetTelegramLoginAuthorizationCodeByRequest(ctx, request.ID); err != nil { + return FinalizedAuthorization{}, err + } else if found { + if !existing.ConsumedAt.IsZero() { + return FinalizedAuthorization{}, domain.ErrTelegramLoginCodeConsumed + } + if !s.now().Before(existing.ExpiresAt) { + return FinalizedAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + existing, err = s.store.PutTelegramLoginAuthorizationCode(ctx, existing) + if err != nil { + return FinalizedAuthorization{}, err + } + code, err := s.openCode(request, existing) + if err != nil { + return FinalizedAuthorization{}, err + } + redirectURL, err := AppendAuthorizationResult(request.RedirectURI, code, request.State) + return FinalizedAuthorization{Request: request, Code: code, RedirectURL: redirectURL}, err + } + code, err := GenerateOpaqueToken() + if err != nil { + return FinalizedAuthorization{}, err + } + sealed, nonce, keyID, err := s.sealer.Seal(code, codeAAD(request)) + if err != nil { + return FinalizedAuthorization{}, err + } + now := s.now().UTC() + stored, err := s.store.PutTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginAuthorizationCode{ + RequestID: request.ID, CodeHash: HashOpaqueToken(code), SealedCode: sealed, + SealNonce: nonce, SealKeyID: keyID, IssuedAt: now, ExpiresAt: now.Add(s.codeTTL), + }) + if err != nil { + return FinalizedAuthorization{}, err + } + code, err = s.openCode(request, stored) + if err != nil { + return FinalizedAuthorization{}, err + } + redirectURL, err := AppendAuthorizationResult(request.RedirectURI, code, request.State) + return FinalizedAuthorization{Request: request, Code: code, RedirectURL: redirectURL}, err +} + +type FinalizedDirectAuthorization struct { + Request domain.TelegramLoginRequest + IDToken string +} + +// FinalizeDirectByBrowserToken implements the JS SDK post_message response. +// The signed token is sealed in the durable artifact table so a lost HTTP +// response can retry and receive byte-identical output. Code consumption +// rejects post_message requests, so the artifact cannot be exchanged as an +// authorization code. +func (s *Service) FinalizeDirectByBrowserToken(ctx context.Context, browserToken string, issuer *IDTokenIssuer) (FinalizedDirectAuthorization, error) { + if issuer == nil { + return FinalizedDirectAuthorization{}, fmt.Errorf("telegram login ID token issuer is required") + } + request, err := s.RequestByBrowserToken(ctx, browserToken) + if err != nil { + return FinalizedDirectAuthorization{}, err + } + if request.Status != domain.TelegramLoginRequestApproved || request.ResponseType != "post_message" { + return FinalizedDirectAuthorization{}, domain.ErrTelegramLoginRequestConflict + } + if existing, found, err := s.store.GetTelegramLoginAuthorizationCodeByRequest(ctx, request.ID); err != nil { + return FinalizedDirectAuthorization{}, err + } else if found { + if !s.now().Before(existing.ExpiresAt) { + return FinalizedDirectAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + existing, err = s.store.PutTelegramLoginAuthorizationCode(ctx, existing) + if err != nil { + return FinalizedDirectAuthorization{}, err + } + idToken, err := s.openCode(request, existing) + return FinalizedDirectAuthorization{Request: request, IDToken: idToken}, err + } + idToken, err := issuer.Issue(request) + if err != nil { + return FinalizedDirectAuthorization{}, err + } + sealed, nonce, keyID, err := s.sealer.Seal(idToken, codeAAD(request)) + if err != nil { + return FinalizedDirectAuthorization{}, err + } + now := s.now().UTC() + stored, err := s.store.PutTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginAuthorizationCode{ + RequestID: request.ID, CodeHash: HashOpaqueToken(idToken), SealedCode: sealed, + SealNonce: nonce, SealKeyID: keyID, IssuedAt: now, ExpiresAt: now.Add(s.codeTTL), + }) + if err != nil { + return FinalizedDirectAuthorization{}, err + } + idToken, err = s.openCode(request, stored) + return FinalizedDirectAuthorization{Request: request, IDToken: idToken}, err +} + +func codeAAD(request domain.TelegramLoginRequest) []byte { + return []byte(fmt.Sprintf("telesrv-telegram-login-code\x00%d\x00%s\x00%s", request.ID, request.ClientID, request.RedirectURI)) +} + +func (s *Service) openCode(request domain.TelegramLoginRequest, stored domain.TelegramLoginAuthorizationCode) (string, error) { + code, err := s.sealer.Open(stored.SealedCode, stored.SealNonce, stored.SealKeyID, codeAAD(request)) + if err != nil || subtle.ConstantTimeCompare(HashOpaqueToken(code), stored.CodeHash) != 1 { + return "", domain.ErrTelegramLoginCodeInvalid + } + return code, nil +} + +type ExchangeAuthorizationCodeParams struct { + Code string + ClientID string + ClientSecret string + RedirectURI string + CodeVerifier string + PublicNativeClient bool +} + +type ExchangedAuthorization struct { + Request domain.TelegramLoginRequest + WebAuthorization domain.TelegramLoginWebAuthorization +} + +func (s *Service) ExchangeAuthorizationCode(ctx context.Context, params ExchangeAuthorizationCodeParams) (ExchangedAuthorization, error) { + exchanged, _, err := s.exchangeAuthorizationCode(ctx, params, nil) + return exchanged, err +} + +type IssuedAuthorization struct { + ExchangedAuthorization + IDToken string +} + +// ExchangeAuthorizationCodeAndIssue signs the immutable approval snapshot +// before the one-time code is consumed, but does not expose the signed token +// unless the locked consume succeeds. This keeps a transient signing/key error +// retryable while preserving exactly-once code exchange under concurrency. +func (s *Service) ExchangeAuthorizationCodeAndIssue(ctx context.Context, params ExchangeAuthorizationCodeParams, issuer *IDTokenIssuer) (IssuedAuthorization, error) { + if issuer == nil { + return IssuedAuthorization{}, fmt.Errorf("telegram login ID token issuer is required") + } + exchanged, idToken, err := s.exchangeAuthorizationCode(ctx, params, issuer.Issue) + if err != nil { + return IssuedAuthorization{}, err + } + return IssuedAuthorization{ExchangedAuthorization: exchanged, IDToken: idToken}, nil +} + +func (s *Service) exchangeAuthorizationCode(ctx context.Context, params ExchangeAuthorizationCodeParams, issue func(domain.TelegramLoginRequest) (string, error)) (ExchangedAuthorization, string, error) { + client, found, err := s.store.GetTelegramLoginClient(ctx, params.ClientID) + if err != nil { + return ExchangedAuthorization{}, "", err + } + if !found || !client.Enabled { + return ExchangedAuthorization{}, "", domain.ErrTelegramLoginSecretInvalid + } + challenge, err := PKCEChallenge(params.CodeVerifier) + if err != nil { + return ExchangedAuthorization{}, "", err + } + codeHash := HashOpaqueToken(params.Code) + stored, found, err := s.store.GetTelegramLoginAuthorizationCodeByHash(ctx, codeHash) + if err != nil { + return ExchangedAuthorization{}, "", err + } + if !found { + return ExchangedAuthorization{}, "", domain.ErrTelegramLoginCodeInvalid + } + // The request is recovered by the durable consume operation. Opening the + // sealed code first prevents storage corruption/key loss from consuming it. + requestByBrowser, found, err := s.store.GetTelegramLoginRequest(ctx, stored.RequestID) + if err != nil || !found { + if err != nil { + return ExchangedAuthorization{}, "", err + } + return ExchangedAuthorization{}, "", domain.ErrTelegramLoginCodeInvalid + } + var redirectURI string + if requestByBrowser.Source == domain.TelegramLoginRequestNative { + var allowed bool + _, redirectURI, allowed, err = s.matchNativeApp(ctx, client.BotUserID, "", params.RedirectURI) + if err != nil { + return ExchangedAuthorization{}, "", err + } + if !allowed { + return ExchangedAuthorization{}, "", domain.ErrTelegramLoginCodeInvalid + } + } else { + redirectURI, _, err = NormalizeRedirectURI(params.RedirectURI, s.allowLoopbackHTTP) + if err != nil { + return ExchangedAuthorization{}, "", err + } + } + if params.PublicNativeClient { + if requestByBrowser.Source != domain.TelegramLoginRequestNative || params.ClientSecret != "" { + return ExchangedAuthorization{}, "", domain.ErrTelegramLoginSecretInvalid + } + } else if !VerifyClientSecret(s.clientSecretPepper, params.ClientSecret, client.SecretHash) { + return ExchangedAuthorization{}, "", domain.ErrTelegramLoginSecretInvalid + } + opened, err := s.openCode(requestByBrowser, stored) + if err != nil || subtle.ConstantTimeCompare([]byte(opened), []byte(params.Code)) != 1 { + return ExchangedAuthorization{}, "", domain.ErrTelegramLoginCodeInvalid + } + var idToken string + if issue != nil { + idToken, err = issue(requestByBrowser) + if err != nil { + return ExchangedAuthorization{}, "", err + } + } + _, consumedRequest, web, err := s.store.ConsumeTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginCodeExchange{ + CodeHash: codeHash, ClientID: params.ClientID, ClientSecretVersion: client.SecretVersion, + RedirectURI: redirectURI, CodeChallenge: challenge, Now: s.now().UTC(), + }) + if err != nil { + return ExchangedAuthorization{}, "", err + } + return ExchangedAuthorization{Request: consumedRequest, WebAuthorization: web}, idToken, nil +} + +func (s *Service) RequestByBrowserToken(ctx context.Context, browserToken string) (domain.TelegramLoginRequest, error) { + if len(browserToken) < 16 || len(browserToken) > 1024 || strings.IndexFunc(browserToken, func(r rune) bool { return r <= 0x20 || r == 0x7f }) >= 0 { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestInvalid + } + request, found, err := s.store.GetTelegramLoginRequestByBrowserTokenHash(ctx, HashOpaqueToken(browserToken)) + if err != nil { + return domain.TelegramLoginRequest{}, err + } + if !found { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestInvalid + } + if request.Status == domain.TelegramLoginRequestPending && !s.now().Before(request.ExpiresAt) { + request.Status = domain.TelegramLoginRequestExpired + } + return request, nil +} + +func (s *Service) ListWebAuthorizations(ctx context.Context, userID int64) ([]domain.TelegramLoginWebAuthorization, error) { + return s.store.ListTelegramLoginWebAuthorizations(ctx, userID) +} + +func (s *Service) RevokeWebAuthorization(ctx context.Context, userID, hash int64) error { + revoked, err := s.store.RevokeTelegramLoginWebAuthorization(ctx, userID, hash, s.now().UTC()) + if err != nil { + return err + } + if !revoked { + return domain.ErrTelegramLoginWebAuthHashInvalid + } + return nil +} + +func (s *Service) RevokeAllWebAuthorizations(ctx context.Context, userID int64) (int64, error) { + return s.store.RevokeAllTelegramLoginWebAuthorizations(ctx, userID, s.now().UTC()) +} + +// DeleteExpiredArtifacts removes only terminal login artifacts older than the +// configured retention boundary. Active web authorizations remain durable and +// keep their immutable approved request/claim snapshot reachable. +func (s *Service) DeleteExpiredArtifacts(ctx context.Context, before time.Time, limit int) (int64, error) { + if before.IsZero() || limit <= 0 || limit > 1000 { + return 0, domain.ErrTelegramLoginRequestInvalid + } + return s.store.DeleteExpiredTelegramLoginArtifacts(ctx, before.UTC(), limit) +} diff --git a/internal/app/telegramlogin/service_test.go b/internal/app/telegramlogin/service_test.go new file mode 100644 index 00000000..d0d02216 --- /dev/null +++ b/internal/app/telegramlogin/service_test.go @@ -0,0 +1,404 @@ +package telegramlogin + +import ( + "context" + "errors" + "net/url" + "sync" + "sync/atomic" + "testing" + "time" + + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +func TestServiceClientCreationAndSecretRotationAreSingleWinner(t *testing.T) { + ctx := context.Background() + now := time.Unix(1_780_000_000, 0).UTC() + service, loginStore := newTelegramLoginTestService(t, &now) + + const contenders = 24 + start := make(chan struct{}) + var wg sync.WaitGroup + var created atomic.Int32 + var conflicts atomic.Int32 + for i := 0; i < contenders; i++ { + wg.Add(1) + go func() { + defer wg.Done() + <-start + _, err := service.CreateClient(ctx, 9010, domain.TelegramLoginSigningRS256) + switch { + case err == nil: + created.Add(1) + case errors.Is(err, domain.ErrTelegramLoginRequestConflict): + conflicts.Add(1) + default: + t.Errorf("CreateClient: %v", err) + } + }() + } + close(start) + wg.Wait() + if created.Load() != 1 || conflicts.Load() != contenders-1 { + t.Fatalf("create winners=%d conflicts=%d", created.Load(), conflicts.Load()) + } + + client, found, err := loginStore.GetTelegramLoginClientByBot(ctx, 9010) + if err != nil || !found { + t.Fatalf("GetTelegramLoginClientByBot: found=%v err=%v", found, err) + } + start = make(chan struct{}) + created.Store(0) + conflicts.Store(0) + for i := 0; i < contenders; i++ { + wg.Add(1) + go func(seed byte) { + defer wg.Done() + <-start + hash := make([]byte, 32) + hash[0] = seed + _, err := loginStore.RotateTelegramLoginClientSecret(ctx, client.BotUserID, client.SecretVersion, hash, now.Add(time.Second)) + switch { + case err == nil: + created.Add(1) + case errors.Is(err, domain.ErrTelegramLoginRequestConflict): + conflicts.Add(1) + default: + t.Errorf("RotateTelegramLoginClientSecret: %v", err) + } + }(byte(i + 1)) + } + close(start) + wg.Wait() + if created.Load() != 1 || conflicts.Load() != contenders-1 { + t.Fatalf("rotate winners=%d conflicts=%d", created.Load(), conflicts.Load()) + } +} + +func newTelegramLoginTestService(t *testing.T, now *time.Time) (*Service, *memory.TelegramLoginStore) { + return newTelegramLoginTestServiceWithAlgorithms(t, now, nil) +} + +func newTelegramLoginTestServiceWithAlgorithms(t *testing.T, now *time.Time, algorithms []domain.TelegramLoginSigningAlgorithm) (*Service, *memory.TelegramLoginStore) { + t.Helper() + key := make([]byte, 32) + key[0] = 7 + sealer, err := NewCodeSealer("test", map[string][]byte{"test": key}) + if err != nil { + t.Fatal(err) + } + loginStore := memory.NewTelegramLoginStore(nil) + pepper := make([]byte, 32) + pepper[0] = 9 + service, err := NewService(loginStore, sealer, Config{ + Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv", + AllowLoopbackHTTP: true, ClientSecretPepper: pepper, + SupportedSigningAlgorithms: algorithms, + Now: func() time.Time { return *now }, + }) + if err != nil { + t.Fatal(err) + } + return service, loginStore +} + +func TestServiceAcceptsOfficialClientCanonicalOAuthDeepLinks(t *testing.T) { + ctx := context.Background() + now := time.Unix(1_780_000_000, 0).UTC() + service, _ := newTelegramLoginTestService(t, &now) + credentials, err := service.CreateClient(ctx, 9030, domain.TelegramLoginSigningRS256) + if err != nil { + t.Fatal(err) + } + const redirectURI = "https://rp.example/callback" + if _, err := service.AddAllowedURL(ctx, 9030, domain.TelegramLoginAllowedRedirectURI, redirectURI); err != nil { + t.Fatal(err) + } + challenge, err := PKCEChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") + if err != nil { + t.Fatal(err) + } + created, err := service.CreateAuthorization(ctx, CreateAuthorizationParams{ + ClientID: credentials.Client.ClientID, RedirectURI: redirectURI, ResponseType: "code", + Scope: "openid", CodeChallenge: challenge, CodeChallengeMethod: "S256", + }) + if err != nil { + t.Fatal(err) + } + parsed, err := url.Parse(created.DeepLink) + if err != nil { + t.Fatal(err) + } + token := parsed.Query().Get("token") + valid := []string{ + created.DeepLink, + "tg://oauth?token=" + url.QueryEscape(token), + "tg://resolve?domain=oauth&startapp=" + url.QueryEscape(token), + "https://t.me/oauth?startapp=" + url.QueryEscape(token), + } + for _, deepLink := range valid { + request, err := service.RequestByDeepLink(ctx, deepLink) + if err != nil || request.ID != created.Request.ID { + t.Fatalf("RequestByDeepLink(%q) request=%#v err=%v", deepLink, request, err) + } + } + invalid := []string{ + "telegram://oauth?token=" + url.QueryEscape(token), + "tg://oauth/path?token=" + url.QueryEscape(token), + "tg://oauth?token=" + url.QueryEscape(token) + "&token=other", + "tg://resolve?domain=oauth&domain=other&startapp=" + url.QueryEscape(token), + "tg://resolve?domain=oauth&startapp=" + url.QueryEscape(token) + "&startapp=other", + "tg://oauth?token=" + url.QueryEscape(token) + "#fragment", + } + for _, deepLink := range invalid { + if _, err := service.RequestByDeepLink(ctx, deepLink); !errors.Is(err, domain.ErrTelegramLoginURLInvalid) { + t.Fatalf("RequestByDeepLink(%q) error=%v, want URL invalid", deepLink, err) + } + } +} + +func TestServiceRejectsSigningAlgorithmsWithoutActiveKeys(t *testing.T) { + ctx := context.Background() + now := time.Unix(1_780_000_000, 0).UTC() + service, loginStore := newTelegramLoginTestServiceWithAlgorithms(t, &now, []domain.TelegramLoginSigningAlgorithm{ + domain.TelegramLoginSigningES256, + }) + if _, err := service.CreateClient(ctx, 9020, domain.TelegramLoginSigningRS256); !errors.Is(err, domain.ErrTelegramLoginClientInvalid) { + t.Fatalf("CreateClient unsupported algorithm error=%v", err) + } + credentials, created, err := service.EnsureClient(ctx, 9020) + if err != nil || !created || credentials.Client.SigningAlgorithm != domain.TelegramLoginSigningES256 { + t.Fatalf("EnsureClient credentials=%#v created=%v err=%v", credentials, created, err) + } + if _, err := service.SetClientSigningAlgorithm(ctx, 9020, domain.TelegramLoginSigningEdDSA); !errors.Is(err, domain.ErrTelegramLoginClientInvalid) { + t.Fatalf("SetClientSigningAlgorithm unsupported error=%v", err) + } + + // Simulate configuration drift from a previous deployment. Authorization + // must fail before a request is persisted instead of failing after consent. + if _, err := loginStore.SetTelegramLoginClientSigningAlgorithm(ctx, 9020, domain.TelegramLoginSigningRS256, now.Add(time.Second)); err != nil { + t.Fatal(err) + } + if err := service.SetClientEnabled(ctx, 9020, false); err != nil { + t.Fatal(err) + } + if err := service.SetClientEnabled(ctx, 9020, true); !errors.Is(err, domain.ErrTelegramLoginClientInvalid) { + t.Fatalf("SetClientEnabled unavailable algorithm error=%v", err) + } + if _, err := service.AddAllowedURL(ctx, 9020, domain.TelegramLoginAllowedWebOrigin, "https://rp.example"); err != nil { + t.Fatal(err) + } + if _, err := service.CreateAuthorization(ctx, CreateAuthorizationParams{ + ClientID: credentials.Client.ClientID, RedirectURI: "https://rp.example/", ResponseType: "post_message", + Scope: "openid profile", + }); !errors.Is(err, domain.ErrTelegramLoginClientDisabled) { + t.Fatalf("CreateAuthorization unavailable algorithm error=%v", err) + } +} + +func TestServiceAuthorizationCodeFlowAndRevocation(t *testing.T) { + ctx := context.Background() + now := time.Unix(1_780_000_000, 0).UTC() + service, _ := newTelegramLoginTestService(t, &now) + credentials, err := service.CreateClient(ctx, 9001, domain.TelegramLoginSigningRS256) + if err != nil { + t.Fatalf("CreateClient: %v", err) + } + const redirectURI = "https://rp.example/callback" + if _, err := service.AddAllowedURL(ctx, 9001, domain.TelegramLoginAllowedRedirectURI, redirectURI); err != nil { + t.Fatalf("AddAllowedURL redirect: %v", err) + } + if _, err := service.AddAllowedURL(ctx, 9001, domain.TelegramLoginAllowedWebOrigin, "https://rp.example"); err != nil { + t.Fatalf("AddAllowedURL origin: %v", err) + } + const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + challenge, _ := PKCEChallenge(verifier) + created, err := service.CreateAuthorization(ctx, CreateAuthorizationParams{ + ClientID: credentials.Client.ClientID, RedirectURI: redirectURI, + ResponseType: "code", Scope: "openid profile phone telegram:bot_access", + State: "opaque-state", Nonce: "nonce", CodeChallenge: challenge, CodeChallengeMethod: "S256", + Browser: "Firefox", Platform: "Windows", IP: "192.0.2.10", Region: "Test Region", + IncludeMatchCodes: true, MatchCodesFirst: true, + }) + if err != nil { + t.Fatalf("CreateAuthorization: %v", err) + } + if created.DeepLink == "" || created.Request.ID == 0 || len(created.Request.MatchCodes) != 5 { + t.Fatalf("created authorization = %#v", created) + } + if _, err := service.CheckMatchCode(ctx, created.DeepLink, created.Request.MatchCodes[0]); err == nil && created.Request.MatchCodes[0] != created.Request.MatchCode { + t.Fatal("wrong match code unexpectedly accepted") + } + if ok, err := service.CheckMatchCode(ctx, created.DeepLink, created.Request.MatchCode); err != nil || !ok { + t.Fatalf("CheckMatchCode correct = %v,%v", ok, err) + } + now = now.Add(time.Second) + identity := domain.TelegramLoginIdentitySnapshot{ + UserID: 42, Name: "Alice Example", GivenName: "Alice", FamilyName: "Example", + PreferredUsername: "alice", Picture: "https://oauth.telesrv.test/userpic/42", + } + approved, web, err := service.Approve(ctx, created.DeepLink, identity, true, false, created.Request.MatchCode) + if err != nil { + t.Fatalf("Approve: %v", err) + } + if approved.Status != domain.TelegramLoginRequestApproved || web.PhoneShared || !web.BotAccessGranted { + t.Fatalf("approved=%#v web=%#v", approved, web) + } + if approved.ProfileName != "Alice Example" || approved.PhoneNumber != "" { + t.Fatalf("identity snapshot = %#v", approved) + } + now = now.Add(time.Second) + finalized, err := service.FinalizeByBrowserToken(ctx, created.BrowserToken) + if err != nil { + t.Fatalf("FinalizeByBrowserToken: %v", err) + } + redirect, err := url.Parse(finalized.RedirectURL) + if err != nil || redirect.Query().Get("code") != finalized.Code || redirect.Query().Get("state") != "opaque-state" { + t.Fatalf("final redirect = %q,%v", finalized.RedirectURL, err) + } + if _, err := service.ExchangeAuthorizationCode(ctx, ExchangeAuthorizationCodeParams{ + Code: finalized.Code, ClientID: credentials.Client.ClientID, ClientSecret: credentials.Secret, + RedirectURI: redirectURI, CodeVerifier: verifier + "x", + }); !errors.Is(err, domain.ErrTelegramLoginCodeInvalid) { + t.Fatalf("exchange wrong verifier error = %v, want code invalid", err) + } + now = now.Add(time.Second) + exchanged, err := service.ExchangeAuthorizationCode(ctx, ExchangeAuthorizationCodeParams{ + Code: finalized.Code, ClientID: credentials.Client.ClientID, ClientSecret: credentials.Secret, + RedirectURI: redirectURI, CodeVerifier: verifier, + }) + if err != nil { + t.Fatalf("ExchangeAuthorizationCode: %v", err) + } + if exchanged.Request.AuthorizedUserID != 42 || exchanged.WebAuthorization.Hash != web.Hash { + t.Fatalf("exchanged = %#v", exchanged) + } + if _, err := service.ExchangeAuthorizationCode(ctx, ExchangeAuthorizationCodeParams{ + Code: finalized.Code, ClientID: credentials.Client.ClientID, ClientSecret: credentials.Secret, + RedirectURI: redirectURI, CodeVerifier: verifier, + }); !errors.Is(err, domain.ErrTelegramLoginCodeConsumed) { + t.Fatalf("replay exchange error = %v, want consumed", err) + } + if err := service.RevokeWebAuthorization(ctx, 42, web.Hash); err != nil { + t.Fatalf("RevokeWebAuthorization: %v", err) + } + if list, err := service.ListWebAuthorizations(ctx, 42); err != nil || len(list) != 0 { + t.Fatalf("ListWebAuthorizations after revoke = %#v,%v", list, err) + } + if err := service.RevokeWebAuthorization(ctx, 42, web.Hash); !errors.Is(err, domain.ErrTelegramLoginWebAuthHashInvalid) { + t.Fatalf("second revoke error = %v, want hash invalid", err) + } +} + +func TestFinalizationRetryRechecksLiveAuthorization(t *testing.T) { + ctx := context.Background() + now := time.Unix(1_780_000_000, 0).UTC() + service, _ := newTelegramLoginTestService(t, &now) + credentials, err := service.CreateClient(ctx, 9010, domain.TelegramLoginSigningRS256) + if err != nil { + t.Fatal(err) + } + const redirectURI = "https://retry.example/callback" + const origin = "https://retry.example" + if _, err := service.AddAllowedURL(ctx, 9010, domain.TelegramLoginAllowedRedirectURI, redirectURI); err != nil { + t.Fatal(err) + } + if _, err := service.AddAllowedURL(ctx, 9010, domain.TelegramLoginAllowedWebOrigin, origin); err != nil { + t.Fatal(err) + } + challenge, err := PKCEChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") + if err != nil { + t.Fatal(err) + } + codeRequest, err := service.CreateAuthorization(ctx, CreateAuthorizationParams{ + ClientID: credentials.Client.ClientID, RedirectURI: redirectURI, ResponseType: "code", + Scope: "openid", CodeChallenge: challenge, CodeChallengeMethod: "S256", + }) + if err != nil { + t.Fatal(err) + } + _, codeWeb, err := service.Approve(ctx, codeRequest.DeepLink, domain.TelegramLoginIdentitySnapshot{UserID: 51}, false, false, "") + if err != nil { + t.Fatal(err) + } + if _, err := service.FinalizeByBrowserToken(ctx, codeRequest.BrowserToken); err != nil { + t.Fatal(err) + } + if err := service.RevokeWebAuthorization(ctx, 51, codeWeb.Hash); err != nil { + t.Fatal(err) + } + if _, err := service.FinalizeByBrowserToken(ctx, codeRequest.BrowserToken); !errors.Is(err, domain.ErrTelegramLoginRequestConflict) { + t.Fatalf("authorization-code retry after revoke error = %v, want conflict", err) + } + + miniRequest, err := service.CreateAuthorization(ctx, CreateAuthorizationParams{ + ClientID: credentials.Client.ClientID, RedirectURI: origin + "/", ResponseType: "post_message", Scope: "openid", + Origin: origin, InAppOrigin: origin, Source: domain.TelegramLoginRequestMiniApp, + }) + if err != nil { + t.Fatal(err) + } + _, miniWeb, err := service.Approve(ctx, miniRequest.DeepLink, domain.TelegramLoginIdentitySnapshot{UserID: 52}, false, false, "") + if err != nil { + t.Fatal(err) + } + if _, err := service.FinalizeInAppRedirectByDeepLink(ctx, miniRequest.DeepLink); err != nil { + t.Fatal(err) + } + if err := service.RevokeWebAuthorization(ctx, 52, miniWeb.Hash); err != nil { + t.Fatal(err) + } + if _, err := service.FinalizeInAppRedirectByDeepLink(ctx, miniRequest.DeepLink); !errors.Is(err, domain.ErrTelegramLoginRequestConflict) { + t.Fatalf("Mini App token retry after revoke error = %v, want conflict", err) + } +} + +func TestServiceSecretRotationClosesExchangeTOCTOU(t *testing.T) { + ctx := context.Background() + now := time.Unix(1_780_000_000, 0).UTC() + service, _ := newTelegramLoginTestService(t, &now) + oldCredentials, err := service.CreateClient(ctx, 9002, domain.TelegramLoginSigningRS256) + if err != nil { + t.Fatal(err) + } + const redirectURI = "https://rotate.example/callback" + if _, err := service.AddAllowedURL(ctx, 9002, domain.TelegramLoginAllowedRedirectURI, redirectURI); err != nil { + t.Fatal(err) + } + const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + challenge, _ := PKCEChallenge(verifier) + created, err := service.CreateAuthorization(ctx, CreateAuthorizationParams{ + ClientID: oldCredentials.Client.ClientID, RedirectURI: redirectURI, ResponseType: "code", + Scope: "openid", CodeChallenge: challenge, CodeChallengeMethod: "S256", + }) + if err != nil { + t.Fatal(err) + } + now = now.Add(time.Second) + if _, _, err := service.Approve(ctx, created.DeepLink, domain.TelegramLoginIdentitySnapshot{UserID: 43}, false, false, ""); err != nil { + t.Fatal(err) + } + now = now.Add(time.Second) + finalized, err := service.FinalizeByBrowserToken(ctx, created.BrowserToken) + if err != nil { + t.Fatal(err) + } + newCredentials, err := service.RotateClientSecret(ctx, 9002) + if err != nil { + t.Fatal(err) + } + if _, err := service.ExchangeAuthorizationCode(ctx, ExchangeAuthorizationCodeParams{ + Code: finalized.Code, ClientID: oldCredentials.Client.ClientID, ClientSecret: oldCredentials.Secret, + RedirectURI: redirectURI, CodeVerifier: verifier, + }); !errors.Is(err, domain.ErrTelegramLoginSecretInvalid) { + t.Fatalf("old secret exchange error = %v", err) + } + if _, err := service.ExchangeAuthorizationCode(ctx, ExchangeAuthorizationCodeParams{ + Code: finalized.Code, ClientID: newCredentials.Client.ClientID, ClientSecret: newCredentials.Secret, + RedirectURI: redirectURI, CodeVerifier: verifier, + }); err != nil { + t.Fatalf("new secret exchange: %v", err) + } +} diff --git a/internal/app/telegramlogin/url.go b/internal/app/telegramlogin/url.go new file mode 100644 index 00000000..dfc9a21a --- /dev/null +++ b/internal/app/telegramlogin/url.go @@ -0,0 +1,141 @@ +package telegramlogin + +import ( + "net" + "net/url" + "strconv" + "strings" + "unicode" + + "golang.org/x/net/idna" + + "telesrv/internal/domain" +) + +const maxTelegramLoginURLLength = 4096 + +func NormalizeRedirectURI(raw string, allowLoopbackHTTP bool) (normalized, domainName string, err error) { + u, err := parseWebURL(raw, allowLoopbackHTTP) + if err != nil { + return "", "", err + } + if u.Fragment != "" { + return "", "", domain.ErrTelegramLoginURLInvalid + } + query := u.Query() + for _, reserved := range []string{"code", "state", "error", "error_description"} { + if _, exists := query[reserved]; exists { + return "", "", domain.ErrTelegramLoginURLInvalid + } + } + if u.Path == "" { + u.Path = "/" + } + return u.String(), u.Hostname(), nil +} + +func NormalizeWebOrigin(raw string, allowLoopbackHTTP bool) (string, error) { + u, err := parseWebURL(raw, allowLoopbackHTTP) + if err != nil { + return "", err + } + if (u.Path != "" && u.Path != "/") || u.RawQuery != "" || u.Fragment != "" || u.RawPath != "" { + return "", domain.ErrTelegramLoginURLInvalid + } + u.Path = "" + return u.String(), nil +} + +func parseWebURL(raw string, allowLoopbackHTTP bool) (*url.URL, error) { + if raw == "" || len(raw) > maxTelegramLoginURLLength || raw != strings.TrimSpace(raw) || strings.IndexFunc(raw, unicode.IsControl) >= 0 { + return nil, domain.ErrTelegramLoginURLInvalid + } + u, err := url.Parse(raw) + if err != nil || !u.IsAbs() || u.Opaque != "" || u.User != nil || u.Host == "" { + return nil, domain.ErrTelegramLoginURLInvalid + } + u.Scheme = strings.ToLower(u.Scheme) + host := strings.TrimSuffix(strings.ToLower(u.Hostname()), ".") + if host == "" { + return nil, domain.ErrTelegramLoginURLInvalid + } + if ip := net.ParseIP(host); ip == nil { + host, err = idna.Lookup.ToASCII(host) + if err != nil || host == "" { + return nil, domain.ErrTelegramLoginURLInvalid + } + } + port := u.Port() + if port != "" { + n, err := strconv.Atoi(port) + if err != nil || n < 1 || n > 65535 { + return nil, domain.ErrTelegramLoginURLInvalid + } + } + switch u.Scheme { + case "https": + if port == "443" { + port = "" + } + case "http": + if !allowLoopbackHTTP || !isLoopbackHost(host) { + return nil, domain.ErrTelegramLoginURLInvalid + } + if port == "80" { + port = "" + } + default: + return nil, domain.ErrTelegramLoginURLInvalid + } + if port == "" { + if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") { + u.Host = "[" + host + "]" + } else { + u.Host = host + } + } else { + u.Host = net.JoinHostPort(host, port) + } + return u, nil +} + +func isLoopbackHost(host string) bool { + if host == "localhost" { + return true + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + +func AppendAuthorizationResult(redirectURI, code, state string) (string, error) { + u, err := url.Parse(redirectURI) + if err != nil || !u.IsAbs() || code == "" { + return "", domain.ErrTelegramLoginURLInvalid + } + q := u.Query() + q.Set("code", code) + if state != "" { + q.Set("state", state) + } + u.RawQuery = q.Encode() + return u.String(), nil +} + +func AppendAuthorizationError(redirectURI, errorCode, state string) (string, error) { + switch errorCode { + case "access_denied", "temporarily_unavailable", "server_error", "invalid_request", "invalid_scope", "unsupported_response_type": + default: + return "", domain.ErrTelegramLoginRequestInvalid + } + u, err := url.Parse(redirectURI) + if err != nil || !u.IsAbs() { + return "", domain.ErrTelegramLoginURLInvalid + } + q := u.Query() + q.Set("error", errorCode) + if state != "" { + q.Set("state", state) + } + u.RawQuery = q.Encode() + return u.String(), nil +} diff --git a/internal/app/telegramlogin/url_test.go b/internal/app/telegramlogin/url_test.go new file mode 100644 index 00000000..17dc4549 --- /dev/null +++ b/internal/app/telegramlogin/url_test.go @@ -0,0 +1,115 @@ +package telegramlogin + +import ( + "errors" + "net/url" + "testing" + + "telesrv/internal/domain" +) + +func TestNormalizeRedirectURIIsExactAndRejectsOpenRedirectShapes(t *testing.T) { + tests := []struct { + name string + raw string + allowHTTP bool + want string + valid bool + }{ + {name: "https canonical", raw: "https://EXAMPLE.com:443/callback?tenant=one", want: "https://example.com/callback?tenant=one", valid: true}, + {name: "idna", raw: "https://例子.测试/callback", want: "https://xn--fsqu00a.xn--0zwm56d/callback", valid: true}, + {name: "loopback dev", raw: "http://127.0.0.1:8080/callback", allowHTTP: true, want: "http://127.0.0.1:8080/callback", valid: true}, + {name: "http production", raw: "http://example.com/callback"}, + {name: "userinfo", raw: "https://user@example.com/callback"}, + {name: "fragment", raw: "https://example.com/callback#token"}, + {name: "reserved code", raw: "https://example.com/callback?code=attacker"}, + {name: "leading whitespace", raw: " https://example.com/callback"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := NormalizeRedirectURI(test.raw, test.allowHTTP) + if test.valid { + if err != nil || got != test.want { + t.Fatalf("NormalizeRedirectURI() = %q,%v, want %q,nil", got, err, test.want) + } + } else if !errors.Is(err, domain.ErrTelegramLoginURLInvalid) { + t.Fatalf("NormalizeRedirectURI() error = %v, want URL invalid", err) + } + }) + } +} + +func TestAppendAuthorizationErrorPreservesState(t *testing.T) { + got, err := AppendAuthorizationError("https://example.com/callback?tenant=one", "access_denied", "opaque") + if err != nil { + t.Fatal(err) + } + u, _ := url.Parse(got) + if u.Query().Get("tenant") != "one" || u.Query().Get("error") != "access_denied" || u.Query().Get("state") != "opaque" { + t.Fatalf("error redirect = %q", got) + } + if _, err := AppendAuthorizationError("https://example.com/callback", "invalid_client", ""); err == nil { + t.Fatal("unsafe authorization error unexpectedly accepted") + } +} + +func TestNormalizeWebOriginRejectsPathAndQuery(t *testing.T) { + if got, err := NormalizeWebOrigin("https://Example.com/", false); err != nil || got != "https://example.com" { + t.Fatalf("NormalizeWebOrigin = %q,%v", got, err) + } + for _, raw := range []string{"https://example.com/path", "https://example.com/?x=1", "https://example.com/#x"} { + if _, err := NormalizeWebOrigin(raw, false); !errors.Is(err, domain.ErrTelegramLoginURLInvalid) { + t.Fatalf("NormalizeWebOrigin(%q) error = %v, want URL invalid", raw, err) + } + } +} + +func TestNormalizeLoopbackIPv6PreservesURLBrackets(t *testing.T) { + origin, err := NormalizeWebOrigin("http://[0:0:0:0:0:0:0:1]:80/", true) + if err != nil { + t.Fatal(err) + } + if origin != "http://[0:0:0:0:0:0:0:1]" { + t.Fatalf("origin=%q", origin) + } + redirect, domainName, err := NormalizeRedirectURI("http://[::1]/callback", true) + if err != nil { + t.Fatal(err) + } + if redirect != "http://[::1]/callback" || domainName != "::1" { + t.Fatalf("redirect=%q domain=%q", redirect, domainName) + } +} + +func TestPKCERFC7636Vector(t *testing.T) { + const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + const want = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + got, err := PKCEChallenge(verifier) + if err != nil || got != want { + t.Fatalf("PKCEChallenge = %q,%v, want %q,nil", got, err, want) + } +} + +func TestCodeSealerUsesAADAndRetiringKeys(t *testing.T) { + oldKey := make([]byte, 32) + newKey := make([]byte, 32) + oldKey[0], newKey[0] = 1, 2 + old, err := NewCodeSealer("old", map[string][]byte{"old": oldKey}) + if err != nil { + t.Fatal(err) + } + sealed, nonce, keyID, err := old.Seal("authorization-code", []byte("request-1")) + if err != nil { + t.Fatal(err) + } + rotated, err := NewCodeSealer("new", map[string][]byte{"old": oldKey, "new": newKey}) + if err != nil { + t.Fatal(err) + } + if got, err := rotated.Open(sealed, nonce, keyID, []byte("request-1")); err != nil || got != "authorization-code" { + t.Fatalf("Open after rotation = %q,%v", got, err) + } + if _, err := rotated.Open(sealed, nonce, keyID, []byte("request-2")); !errors.Is(err, domain.ErrTelegramLoginCodeInvalid) { + t.Fatalf("Open with wrong AAD error = %v", err) + } +} diff --git a/internal/botapi/inline.go b/internal/botapi/inline.go index 74d03bf5..56c98351 100644 --- a/internal/botapi/inline.go +++ b/internal/botapi/inline.go @@ -347,6 +347,9 @@ func markupButtonFromAPI(button apiInlineKeyboardButton) (domain.MarkupButton, e if button.CopyTextSet { constructors++ } + if button.LoginURLSet { + constructors++ + } if constructors != 1 { return domain.MarkupButton{}, errors.New("BUTTON_INVALID") } @@ -357,6 +360,13 @@ func markupButtonFromAPI(button apiInlineKeyboardButton) (domain.MarkupButton, e if button.URLSet { return domain.MarkupButton{Type: domain.MarkupButtonURL, Text: button.Text, URL: button.URL, Style: style, IconCustomEmojiID: icon}, nil } + if button.LoginURLSet { + return domain.MarkupButton{ + Type: domain.MarkupButtonLoginURL, Text: button.Text, URL: button.LoginURL, + ForwardText: button.LoginForwardText, LoginBotUsername: button.LoginBotUsername, + RequestWriteAccess: button.LoginRequestWriteAccess, Style: style, IconCustomEmojiID: icon, + }, nil + } if button.CallbackDataSet { if button.CallbackData == "" || len([]byte(button.CallbackData)) > domain.MaxCallbackDataLen { return domain.MarkupButton{}, errors.New("BUTTON_DATA_INVALID") @@ -689,23 +699,28 @@ type apiForceReply struct { } type apiInlineKeyboardButton struct { - Text string - URL string - URLSet bool - CallbackData string - CallbackDataSet bool - Style string - IconCustomEmojiID string - IconCustomEmojiIDSet bool - Unsupported bool - WebAppURL string - WebAppSet bool - SwitchInlineQuery string - SwitchInlineSet bool - SwitchInlineSamePeer bool - SwitchInlinePeerTypes []string - CopyText string - CopyTextSet bool + Text string + URL string + URLSet bool + CallbackData string + CallbackDataSet bool + Style string + IconCustomEmojiID string + IconCustomEmojiIDSet bool + Unsupported bool + WebAppURL string + WebAppSet bool + SwitchInlineQuery string + SwitchInlineSet bool + SwitchInlineSamePeer bool + SwitchInlinePeerTypes []string + CopyText string + CopyTextSet bool + LoginURL string + LoginForwardText string + LoginBotUsername string + LoginRequestWriteAccess bool + LoginURLSet bool } func (b *apiInlineKeyboardButton) UnmarshalJSON(data []byte) error { @@ -739,6 +754,20 @@ func (b *apiInlineKeyboardButton) UnmarshalJSON(data []byte) error { } b.WebAppURL = app.URL } + if raw, ok := fields["login_url"]; ok { + b.LoginURLSet = true + var login struct { + URL string `json:"url"` + ForwardText string `json:"forward_text"` + BotUsername string `json:"bot_username"` + RequestWriteAccess bool `json:"request_write_access"` + } + if json.Unmarshal(raw, &login) != nil { + return errors.New("invalid login url") + } + b.LoginURL, b.LoginForwardText = login.URL, login.ForwardText + b.LoginBotUsername, b.LoginRequestWriteAccess = login.BotUsername, login.RequestWriteAccess + } switchActions := 0 if raw, ok := fields["switch_inline_query"]; ok { switchActions++ @@ -807,7 +836,7 @@ func (b *apiInlineKeyboardButton) UnmarshalJSON(data []byte) error { } for key := range fields { switch key { - case "text", "url", "callback_data", "web_app", "switch_inline_query", "switch_inline_query_current_chat", "switch_inline_query_chosen_chat", "copy_text", "style", "icon_custom_emoji_id": + case "text", "url", "callback_data", "web_app", "login_url", "switch_inline_query", "switch_inline_query_current_chat", "switch_inline_query_chosen_chat", "copy_text", "style", "icon_custom_emoji_id": default: b.Unsupported = true } diff --git a/internal/botapi/projection.go b/internal/botapi/projection.go index 8048fb97..6c4c10ca 100644 --- a/internal/botapi/projection.go +++ b/internal/botapi/projection.go @@ -509,6 +509,18 @@ func apiReplyMarkup(markup *domain.MessageReplyMarkup) map[string]any { switch button.Type { case domain.MarkupButtonURL: item["url"] = button.URL + case domain.MarkupButtonLoginURL: + login := map[string]any{"url": button.URL} + if button.ForwardText != "" { + login["forward_text"] = button.ForwardText + } + if button.LoginBotUsername != "" { + login["bot_username"] = button.LoginBotUsername + } + if button.RequestWriteAccess { + login["request_write_access"] = true + } + item["login_url"] = login case domain.MarkupButtonCallback: item["callback_data"] = string(button.Data) case domain.MarkupButtonWebView: diff --git a/internal/botapi/server_test.go b/internal/botapi/server_test.go index e6afa8de..1bbc014a 100644 --- a/internal/botapi/server_test.go +++ b/internal/botapi/server_test.go @@ -886,6 +886,19 @@ func TestReplyMarkupFromAPIReplyKeyboardVariants(t *testing.T) { if err != nil || webApp == nil || webApp.Inline[0][0].Type != domain.MarkupButtonWebView { t.Fatalf("web_app inline button = %#v err=%v", webApp, err) } + login, err := replyMarkupFromAPI(json.RawMessage(`{"inline_keyboard":[[{"text":"Log in","login_url":{"url":"https://example.com/login","forward_text":"Open","bot_username":"auth_bot","request_write_access":true}}]]}`)) + if err != nil || login == nil { + t.Fatalf("login_url inline button = %#v err=%v", login, err) + } + loginButton := login.Inline[0][0] + if loginButton.Type != domain.MarkupButtonLoginURL || loginButton.URL != "https://example.com/login" || loginButton.ForwardText != "Open" || + loginButton.LoginBotUsername != "auth_bot" || !loginButton.RequestWriteAccess { + t.Fatalf("login_url button = %#v", loginButton) + } + projectedLogin := apiReplyMarkup(login)["inline_keyboard"].([][]map[string]any)[0][0]["login_url"].(map[string]any) + if projectedLogin["url"] != "https://example.com/login" || projectedLogin["bot_username"] != "auth_bot" || projectedLogin["request_write_access"] != true { + t.Fatalf("projected login_url = %#v", projectedLogin) + } } func TestReplyMarkupFromAPIPreservesSemanticButtonStyles(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index ed9aacfc..3c08d7dd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,8 @@ package config import ( "bufio" "fmt" + "net" + "net/netip" "net/url" "os" "strconv" @@ -88,6 +90,22 @@ type Config struct { // PublicLinkWebAddr 是公开链接落地页监听地址;为空关闭。 // 生产应只监听 loopback,并由 nginx 将 /、/addstickers/、/addemoji/ 与 /addlist/ 反代到该地址。 PublicLinkWebAddr string + // TelegramLoginEnabled mounts the self-hosted Telegram Login/OIDC provider + // on PublicLinkWebAddr. Secrets are file-backed so they are not exposed in + // process listings or accidentally copied into tracked .env templates. + TelegramLoginEnabled bool + TelegramLoginIssuer string + TelegramLoginAllowLoopbackHTTP bool + TelegramLoginSigningKeysFile string + TelegramLoginCodeKeysFile string + TelegramLoginSecretPepperFile string + TelegramLoginRequestTTL time.Duration + TelegramLoginCodeTTL time.Duration + TelegramLoginIDTokenTTL time.Duration + TelegramLoginTrustedProxyCIDRs []string + TelegramLoginRetention time.Duration + TelegramLoginSweepInterval time.Duration + TelegramLoginSweepBatch int // Admin UI 独立进程配置项保留在统一配置中,cmd/telesrv-admin 也按同名 env 读取。 AdminUIAddr string AdminUIPassword string @@ -471,6 +489,19 @@ func Load() (Config, error) { PublicWebBaseURL: publicWebBaseURL, PublicAppName: publicAppName, PublicLinkWebAddr: envAllowEmptyOr("TELESRV_PUBLIC_LINK_WEB_ADDR", ""), + TelegramLoginEnabled: envBoolOr("TELESRV_TELEGRAM_LOGIN_ENABLE", false), + TelegramLoginIssuer: strings.TrimSuffix(envOr("TELESRV_TELEGRAM_LOGIN_ISSUER", publicBaseURL), "/"), + TelegramLoginAllowLoopbackHTTP: envBoolOr("TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP", false), + TelegramLoginSigningKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "data/telegram-login/signing-keys.json"), + TelegramLoginCodeKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "data/telegram-login/code-keys.json"), + TelegramLoginSecretPepperFile: envOr("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "data/telegram-login/client-secret-pepper"), + TelegramLoginRequestTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_REQUEST_TTL", 5*time.Minute), + TelegramLoginCodeTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_CODE_TTL", 2*time.Minute), + TelegramLoginIDTokenTTL: envDurationOr("TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL", time.Hour), + TelegramLoginTrustedProxyCIDRs: envListOr("TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS", nil), + TelegramLoginRetention: envDurationOr("TELESRV_TELEGRAM_LOGIN_RETENTION", 7*24*time.Hour), + TelegramLoginSweepInterval: envDurationOr("TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL", 5*time.Minute), + TelegramLoginSweepBatch: envIntOr("TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH", 500), AdminUIAddr: envOr("TELESRV_ADMIN_UI_ADDR", "127.0.0.1:2600"), AdminUIPassword: envOr("TELESRV_ADMIN_UI_PASSWORD", ""), AdminUIToken: envOr("TELESRV_ADMIN_UI_TOKEN", ""), @@ -629,9 +660,56 @@ func Load() (Config, error) { if err := validateStarGiftConfig(cfg); err != nil { return Config{}, err } + if err := validateTelegramLoginConfig(cfg); err != nil { + return Config{}, err + } return cfg, nil } +func validateTelegramLoginConfig(cfg Config) error { + if !cfg.TelegramLoginEnabled { + return nil + } + if strings.TrimSpace(cfg.PublicLinkWebAddr) == "" { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ENABLE requires TELESRV_PUBLIC_LINK_WEB_ADDR") + } + issuer, err := url.Parse(strings.TrimSpace(cfg.TelegramLoginIssuer)) + if err != nil || issuer.User != nil || issuer.Host == "" || issuer.RawQuery != "" || issuer.Fragment != "" || + (issuer.Path != "" && issuer.Path != "/") { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER must be an absolute origin URL") + } + switch issuer.Scheme { + case "https": + case "http": + host := issuer.Hostname() + ip := net.ParseIP(host) + if !cfg.TelegramLoginAllowLoopbackHTTP || (host != "localhost" && (ip == nil || !ip.IsLoopback())) { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER http is allowed only for explicit loopback development") + } + default: + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER must use https") + } + if strings.TrimSpace(cfg.TelegramLoginSigningKeysFile) == "" || strings.TrimSpace(cfg.TelegramLoginCodeKeysFile) == "" || strings.TrimSpace(cfg.TelegramLoginSecretPepperFile) == "" { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_* key and pepper files are required") + } + if cfg.TelegramLoginRequestTTL < time.Minute || cfg.TelegramLoginRequestTTL > 15*time.Minute || + cfg.TelegramLoginCodeTTL < 30*time.Second || cfg.TelegramLoginCodeTTL > 10*time.Minute || + cfg.TelegramLoginIDTokenTTL < time.Minute || cfg.TelegramLoginIDTokenTTL > 24*time.Hour { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN TTL values are outside their bounded ranges") + } + if cfg.TelegramLoginRetention < time.Hour || cfg.TelegramLoginRetention > 90*24*time.Hour || + cfg.TelegramLoginSweepInterval < 10*time.Second || cfg.TelegramLoginSweepInterval > time.Hour || + cfg.TelegramLoginSweepBatch <= 0 || cfg.TelegramLoginSweepBatch > 1000 { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN retention must be 1h..90d, sweep interval 10s..1h, and sweep batch 1..1000") + } + for _, raw := range cfg.TelegramLoginTrustedProxyCIDRs { + if _, err := netip.ParsePrefix(strings.TrimSpace(raw)); err != nil { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS contains invalid CIDR %q: %w", raw, err) + } + } + return nil +} + func validateStarGiftConfig(cfg Config) error { if cfg.StarGiftSweepInterval <= 0 || cfg.StarGiftSweepBatch <= 0 || cfg.StarGiftSweepBatch > 10000 { return fmt.Errorf("TELESRV_STARGIFT_SWEEP_INTERVAL must be positive and TELESRV_STARGIFT_SWEEP_BATCH must be 1..10000") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ebc5cf2f..b8e4c0f2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -35,13 +35,13 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) { func TestLoadUsesExplicitAdvertiseIP(t *testing.T) { disableDefaultConfigFile(t) - t.Setenv("TELESRV_ADVERTISE_IP", "192.0.2.10") + t.Setenv("TELESRV_ADVERTISE_IP", "10.172.61.102") cfg, err := Load() if err != nil { t.Fatalf("Load: %v", err) } - if cfg.AdvertiseIP != "192.0.2.10" { + if cfg.AdvertiseIP != "10.172.61.102" { t.Fatalf("AdvertiseIP = %q, want explicit env", cfg.AdvertiseIP) } } @@ -431,6 +431,83 @@ func TestLoadNormalizesLocalPublicBaseURL(t *testing.T) { } } +func TestLoadTelegramLoginConfig(t *testing.T) { + disableDefaultConfigFile(t) + t.Setenv("TELESRV_PUBLIC_LINK_WEB_ADDR", "127.0.0.1:2401") + t.Setenv("TELESRV_TELEGRAM_LOGIN_ENABLE", "true") + t.Setenv("TELESRV_TELEGRAM_LOGIN_ISSUER", "http://127.0.0.1:2401/") + t.Setenv("TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP", "true") + t.Setenv("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "secrets/signing.json") + t.Setenv("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "secrets/codes.json") + t.Setenv("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "secrets/pepper") + t.Setenv("TELESRV_TELEGRAM_LOGIN_REQUEST_TTL", "7m") + t.Setenv("TELESRV_TELEGRAM_LOGIN_CODE_TTL", "90s") + t.Setenv("TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL", "45m") + t.Setenv("TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS", "127.0.0.0/8,10.0.0.0/8") + t.Setenv("TELESRV_TELEGRAM_LOGIN_RETENTION", "48h") + t.Setenv("TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL", "30s") + t.Setenv("TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH", "73") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if !cfg.TelegramLoginEnabled || cfg.TelegramLoginIssuer != "http://127.0.0.1:2401" || !cfg.TelegramLoginAllowLoopbackHTTP { + t.Fatalf("telegram login endpoint config = enabled:%v issuer:%q loopback:%v", cfg.TelegramLoginEnabled, cfg.TelegramLoginIssuer, cfg.TelegramLoginAllowLoopbackHTTP) + } + if cfg.TelegramLoginSigningKeysFile != "secrets/signing.json" || cfg.TelegramLoginCodeKeysFile != "secrets/codes.json" || cfg.TelegramLoginSecretPepperFile != "secrets/pepper" { + t.Fatalf("telegram login secret files = %q / %q / %q", cfg.TelegramLoginSigningKeysFile, cfg.TelegramLoginCodeKeysFile, cfg.TelegramLoginSecretPepperFile) + } + if cfg.TelegramLoginRequestTTL != 7*time.Minute || cfg.TelegramLoginCodeTTL != 90*time.Second || cfg.TelegramLoginIDTokenTTL != 45*time.Minute || + cfg.TelegramLoginRetention != 48*time.Hour || cfg.TelegramLoginSweepInterval != 30*time.Second || cfg.TelegramLoginSweepBatch != 73 { + t.Fatalf("telegram login durations/batch = %v / %v / %v / %v / %v / %d", cfg.TelegramLoginRequestTTL, cfg.TelegramLoginCodeTTL, + cfg.TelegramLoginIDTokenTTL, cfg.TelegramLoginRetention, cfg.TelegramLoginSweepInterval, cfg.TelegramLoginSweepBatch) + } + if len(cfg.TelegramLoginTrustedProxyCIDRs) != 2 || cfg.TelegramLoginTrustedProxyCIDRs[1] != "10.0.0.0/8" { + t.Fatalf("trusted proxy CIDRs = %#v", cfg.TelegramLoginTrustedProxyCIDRs) + } +} + +func TestValidateTelegramLoginConfigRejectsUnsafeOrUnboundedSettings(t *testing.T) { + valid := Config{ + TelegramLoginEnabled: true, PublicLinkWebAddr: "127.0.0.1:2401", TelegramLoginIssuer: "https://login.example.test", + TelegramLoginSigningKeysFile: "signing.json", TelegramLoginCodeKeysFile: "codes.json", TelegramLoginSecretPepperFile: "pepper", + TelegramLoginRequestTTL: 5 * time.Minute, TelegramLoginCodeTTL: 2 * time.Minute, TelegramLoginIDTokenTTL: time.Hour, + TelegramLoginRetention: 7 * 24 * time.Hour, TelegramLoginSweepInterval: 5 * time.Minute, TelegramLoginSweepBatch: 500, + } + if err := validateTelegramLoginConfig(valid); err != nil { + t.Fatalf("valid config: %v", err) + } + tests := []struct { + name string + mutate func(*Config) + }{ + {name: "missing listener", mutate: func(c *Config) { c.PublicLinkWebAddr = "" }}, + {name: "issuer path", mutate: func(c *Config) { c.TelegramLoginIssuer = "https://login.example.test/oauth" }}, + {name: "public http", mutate: func(c *Config) { + c.TelegramLoginIssuer = "http://login.example.test" + c.TelegramLoginAllowLoopbackHTTP = true + }}, + {name: "loopback http disabled", mutate: func(c *Config) { c.TelegramLoginIssuer = "http://127.0.0.1:2401" }}, + {name: "missing key file", mutate: func(c *Config) { c.TelegramLoginSigningKeysFile = "" }}, + {name: "request ttl too long", mutate: func(c *Config) { c.TelegramLoginRequestTTL = 16 * time.Minute }}, + {name: "code ttl too short", mutate: func(c *Config) { c.TelegramLoginCodeTTL = 29 * time.Second }}, + {name: "id token ttl too long", mutate: func(c *Config) { c.TelegramLoginIDTokenTTL = 25 * time.Hour }}, + {name: "retention too short", mutate: func(c *Config) { c.TelegramLoginRetention = 59 * time.Minute }}, + {name: "sweep unbounded", mutate: func(c *Config) { c.TelegramLoginSweepBatch = 1001 }}, + {name: "invalid proxy CIDR", mutate: func(c *Config) { c.TelegramLoginTrustedProxyCIDRs = []string{"10.0.0.0/33"} }}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := valid + tc.mutate(&cfg) + if err := validateTelegramLoginConfig(cfg); err == nil { + t.Fatal("unsafe Telegram Login config was accepted") + } + }) + } +} + func TestLoadRejectsInvalidPublicBaseURL(t *testing.T) { disableDefaultConfigFile(t) t.Setenv("TELESRV_PUBLIC_BASE_URL", "https://links.example.test/root?tenant=one") diff --git a/internal/domain/message_markup.go b/internal/domain/message_markup.go index 35c992d9..9196d574 100644 --- a/internal/domain/message_markup.go +++ b/internal/domain/message_markup.go @@ -2,6 +2,7 @@ package domain import ( "errors" + "net" "net/url" "strings" "unicode/utf8" @@ -51,7 +52,12 @@ const ( // MarkupButtonCallback 是 keyboardButtonCallback(点击触发 getBotCallbackAnswer)。 MarkupButtonCallback MarkupButtonType = "callback" // MarkupButtonURL 是 keyboardButtonUrl(点击打开链接)。 - MarkupButtonURL MarkupButtonType = "url" + MarkupButtonURL MarkupButtonType = "url" + // MarkupButtonLoginURL is Bot API login_url / inputKeyboardButtonUrlAuth. + // The target bot is resolved and the linked origin is verified before the + // message is persisted; ButtonID is the stable flattened keyboard index + // returned to clients as keyboardButtonUrlAuth.button_id. + MarkupButtonLoginURL MarkupButtonType = "login_url" MarkupButtonRequestPhone MarkupButtonType = "request_phone" MarkupButtonRequestLocation MarkupButtonType = "request_location" MarkupButtonRequestPoll MarkupButtonType = "request_poll" @@ -122,6 +128,13 @@ type MarkupButton struct { Data []byte `json:"data,omitempty"` // URL 仅 url 使用。 URL string `json:"url,omitempty"` + // Login URL-only fields. LoginBotUserID=0 means the sending bot until the + // RPC/Bot API edge resolves it. LoginBotUsername is input-only and must be + // cleared before persistence. + ForwardText string `json:"forward_text,omitempty"` + LoginBotUserID int64 `json:"login_bot_user_id,omitempty"` + LoginBotUsername string `json:"login_bot_username,omitempty"` + RequestWriteAccess bool `json:"request_write_access,omitempty"` // RequiresPassword 仅 callback 使用(keyboardButtonCallback.requires_password, // 2FA SRP 校验 P3 stub)。 RequiresPassword bool `json:"requires_password,omitempty"` @@ -343,6 +356,14 @@ func validateMarkupButton(b MarkupButton, replyKeyboard bool) error { if err := validateButtonURL(b.URL); err != nil { return err } + case MarkupButtonLoginURL: + if err := validateLoginButtonURL(b.URL); err != nil { + return err + } + if b.ButtonID < 0 || b.LoginBotUserID < 0 || utf8.RuneCountInString(b.ForwardText) > MaxReplyKeyboardButtonTextLen || + utf8.RuneCountInString(b.LoginBotUsername) > 64 { + return ErrButtonInvalid + } case MarkupButtonWebView: if err := validateButtonURL(b.URL); err != nil { return err @@ -374,6 +395,33 @@ func validateButtonURL(raw string) error { return nil } +// validateLoginButtonURL performs only the protocol-shape validation shared by +// Bot API and MTProto input buttons. The Telegram Login service remains the +// authority for the deployment policy: it rejects loopback HTTP unless the +// explicit development switch is enabled and the exact origin is registered. +func validateLoginButtonURL(raw string) error { + raw = strings.TrimSpace(raw) + if raw == "" || len(raw) > MaxBotMenuButtonURLLen { + return ErrButtonURLInvalid + } + u, err := url.Parse(raw) + if err != nil || u.Host == "" || u.User != nil { + return ErrButtonURLInvalid + } + if u.Scheme == "https" { + return nil + } + if u.Scheme != "http" { + return ErrButtonURLInvalid + } + host := strings.ToLower(u.Hostname()) + ip := net.ParseIP(host) + if host != "localhost" && (ip == nil || !ip.IsLoopback()) { + return ErrButtonURLInvalid + } + return nil +} + // BotCallbackAnswer 是 bot 对一次 callback query 的应答(setBotCallbackAnswer → // 解挂等待中的 getBotCallbackAnswer)。 type BotCallbackAnswer struct { diff --git a/internal/domain/message_markup_test.go b/internal/domain/message_markup_test.go index 38444c4d..8ff348ed 100644 --- a/internal/domain/message_markup_test.go +++ b/internal/domain/message_markup_test.go @@ -26,6 +26,10 @@ func TestValidateReplyMarkup(t *testing.T) { {"url http bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonURL, Text: "go", URL: "http://example.com"}}}}, ErrButtonURLInvalid}, {"url javascript bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonURL, Text: "go", URL: "javascript:alert(1)"}}}}, ErrButtonURLInvalid}, {"url empty bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonURL, Text: "go", URL: ""}}}}, ErrButtonURLInvalid}, + {"login url loopback http ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://127.0.0.1:8080/login"}}}}, nil}, + {"login url localhost http ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://localhost:8080/login"}}}}, nil}, + {"login url public http bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://example.com/login"}}}}, ErrButtonURLInvalid}, + {"login url credentials bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "https://user@example.com/login"}}}}, ErrButtonURLInvalid}, {"unknown type bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: "rainbow", Text: "x"}}}}, ErrButtonTypeInvalid}, {"reply keyboard ok", &MessageReplyMarkup{Type: MessageReplyMarkupKeyboard, Keyboard: [][]MarkupButton{{{Type: MarkupButtonText, Text: "Help"}}}, Resize: true, Persistent: true, Placeholder: "Choose"}, nil}, {"reply keyboard semantic style ok", &MessageReplyMarkup{Type: MessageReplyMarkupKeyboard, Keyboard: [][]MarkupButton{{{Type: MarkupButtonText, Text: "Delete", Style: MarkupButtonStyleDanger, IconCustomEmojiID: 123}}}}, nil}, diff --git a/internal/domain/telegram_login.go b/internal/domain/telegram_login.go new file mode 100644 index 00000000..bddab136 --- /dev/null +++ b/internal/domain/telegram_login.go @@ -0,0 +1,506 @@ +package domain + +import ( + "errors" + "slices" + "strings" + "time" + "unicode/utf8" +) + +var ( + ErrTelegramLoginClientInvalid = errors.New("telegram login client invalid") + ErrTelegramLoginClientDisabled = errors.New("telegram login client disabled") + ErrTelegramLoginURLInvalid = errors.New("telegram login url invalid") + ErrTelegramLoginRequestInvalid = errors.New("telegram login request invalid") + ErrTelegramLoginRequestExpired = errors.New("telegram login request expired") + ErrTelegramLoginRequestConflict = errors.New("telegram login request conflict") + ErrTelegramLoginMatchCodeInvalid = errors.New("telegram login match code invalid") + ErrTelegramLoginScopeInvalid = errors.New("telegram login scope invalid") + ErrTelegramLoginCodeInvalid = errors.New("telegram login code invalid") + ErrTelegramLoginCodeConsumed = errors.New("telegram login code consumed") + ErrTelegramLoginWebAuthHashInvalid = errors.New("telegram login web authorization hash invalid") + ErrTelegramLoginRedirectNotAllowed = errors.New("telegram login redirect not allowed") + ErrTelegramLoginOriginNotAllowed = errors.New("telegram login origin not allowed") + ErrTelegramLoginSecretInvalid = errors.New("telegram login client secret invalid") + ErrTelegramLoginPKCEInvalid = errors.New("telegram login pkce invalid") + ErrTelegramLoginAuthorizationsTooMany = errors.New("telegram login authorizations too many") +) + +const MaxTelegramLoginWebAuthorizations = 1000 + +type TelegramLoginSigningAlgorithm string + +const ( + TelegramLoginSigningRS256 TelegramLoginSigningAlgorithm = "RS256" + TelegramLoginSigningES256 TelegramLoginSigningAlgorithm = "ES256" + TelegramLoginSigningEdDSA TelegramLoginSigningAlgorithm = "EdDSA" + TelegramLoginSigningES256K TelegramLoginSigningAlgorithm = "ES256K" +) + +func (a TelegramLoginSigningAlgorithm) Valid() bool { + switch a { + case TelegramLoginSigningRS256, TelegramLoginSigningES256, TelegramLoginSigningEdDSA, TelegramLoginSigningES256K: + return true + default: + return false + } +} + +type TelegramLoginScope string + +const ( + TelegramLoginScopeOpenID TelegramLoginScope = "openid" + TelegramLoginScopeProfile TelegramLoginScope = "profile" + TelegramLoginScopePhone TelegramLoginScope = "phone" + TelegramLoginScopeBotAccess TelegramLoginScope = "telegram:bot_access" +) + +func (s TelegramLoginScope) Valid() bool { + switch s { + case TelegramLoginScopeOpenID, TelegramLoginScopeProfile, TelegramLoginScopePhone, TelegramLoginScopeBotAccess: + return true + default: + return false + } +} + +type TelegramLoginClient struct { + BotUserID int64 + ClientID string + SecretHash []byte + SecretVersion int64 + SigningAlgorithm TelegramLoginSigningAlgorithm + Enabled bool + CreatedAt time.Time + UpdatedAt time.Time +} + +func (c TelegramLoginClient) Clone() TelegramLoginClient { + out := c + out.SecretHash = append([]byte(nil), c.SecretHash...) + return out +} + +func (c TelegramLoginClient) Validate() error { + if c.BotUserID <= 0 || c.ClientID == "" || len(c.SecretHash) != 32 || c.SecretVersion <= 0 || !c.SigningAlgorithm.Valid() { + return ErrTelegramLoginClientInvalid + } + return nil +} + +type TelegramLoginAllowedURLKind string + +const ( + TelegramLoginAllowedWebOrigin TelegramLoginAllowedURLKind = "web_origin" + TelegramLoginAllowedRedirectURI TelegramLoginAllowedURLKind = "redirect_uri" +) + +type TelegramLoginAllowedURL struct { + ID int64 + BotUserID int64 + Kind TelegramLoginAllowedURLKind + NormalizedURL string + CreatedAt time.Time +} + +type TelegramLoginNativePlatform string + +const ( + TelegramLoginNativeIOS TelegramLoginNativePlatform = "ios" + TelegramLoginNativeAndroid TelegramLoginNativePlatform = "android" +) + +type TelegramLoginNativeApp struct { + ID int64 + BotUserID int64 + Platform TelegramLoginNativePlatform + ApplicationID string + // VerificationID is the 10-character Apple Team ID on iOS and the + // normalized 64-hex SHA-256 signing-certificate fingerprint on Android. + VerificationID string + CallbackURI string + VerifiedDisplayName string + Enabled bool + CreatedAt time.Time + UpdatedAt time.Time +} + +const MaxTelegramLoginNativeApps = 20 + +func (p TelegramLoginNativePlatform) Valid() bool { + return p == TelegramLoginNativeIOS || p == TelegramLoginNativeAndroid +} + +func (a TelegramLoginNativeApp) Validate() error { + if a.BotUserID <= 0 || !a.Platform.Valid() || a.ApplicationID == "" || len(a.ApplicationID) > 255 || + a.VerificationID == "" || a.CallbackURI == "" || len(a.CallbackURI) > 4096 || + a.VerifiedDisplayName == "" || len(a.VerifiedDisplayName) > 128 || + a.CreatedAt.IsZero() || a.UpdatedAt.IsZero() { + return ErrTelegramLoginClientInvalid + } + return nil +} + +type TelegramLoginRequestSource string + +const ( + TelegramLoginRequestWeb TelegramLoginRequestSource = "web" + TelegramLoginRequestJavaScript TelegramLoginRequestSource = "javascript" + TelegramLoginRequestNative TelegramLoginRequestSource = "native" + TelegramLoginRequestMiniApp TelegramLoginRequestSource = "mini_app" + TelegramLoginRequestMessageButton TelegramLoginRequestSource = "message_button" +) + +type TelegramLoginRequestState string + +const ( + TelegramLoginRequestPending TelegramLoginRequestState = "pending" + TelegramLoginRequestApproved TelegramLoginRequestState = "approved" + TelegramLoginRequestDeclined TelegramLoginRequestState = "declined" + TelegramLoginRequestExpired TelegramLoginRequestState = "expired" +) + +func (s TelegramLoginRequestState) Terminal() bool { + return s == TelegramLoginRequestApproved || s == TelegramLoginRequestDeclined || s == TelegramLoginRequestExpired +} + +func CanTransitionTelegramLoginRequest(from, to TelegramLoginRequestState) bool { + if from != TelegramLoginRequestPending { + return false + } + return to == TelegramLoginRequestApproved || to == TelegramLoginRequestDeclined || to == TelegramLoginRequestExpired +} + +type TelegramLoginRequest struct { + ID int64 + RequestTokenHash []byte + BrowserTokenHash []byte + BotUserID int64 + ClientID string + SigningAlgorithm TelegramLoginSigningAlgorithm + Source TelegramLoginRequestSource + ResponseType string + RedirectURI string + Origin string + Domain string + Scopes []TelegramLoginScope + State string + Nonce string + CodeChallenge string + CodeChallengeMethod string + Browser string + Platform string + IP string + Region string + InAppOrigin string + IsApp bool + VerifiedAppName string + MatchCodes []string + MatchCode string + MatchCodesFirst bool + UserIDHint int64 + PeerType PeerType + PeerID int64 + MessageID int + ButtonID int + Status TelegramLoginRequestState + AuthorizedUserID int64 + ProfileName string + GivenName string + FamilyName string + PreferredUsername string + Picture string + PhoneNumber string + WriteAllowed bool + PhoneShared bool + CreatedAt time.Time + ExpiresAt time.Time + ApprovedAt time.Time + DeclinedAt time.Time +} + +func (r TelegramLoginRequest) Clone() TelegramLoginRequest { + out := r + out.RequestTokenHash = append([]byte(nil), r.RequestTokenHash...) + out.BrowserTokenHash = append([]byte(nil), r.BrowserTokenHash...) + out.Scopes = append([]TelegramLoginScope(nil), r.Scopes...) + out.MatchCodes = append([]string(nil), r.MatchCodes...) + return out +} + +func (r TelegramLoginRequest) Requests(scope TelegramLoginScope) bool { + return slices.Contains(r.Scopes, scope) +} + +func (r TelegramLoginRequest) Validate() error { + if len(r.RequestTokenHash) != 32 || len(r.BrowserTokenHash) != 32 || r.BotUserID <= 0 || r.ClientID == "" || r.ClientID != strings.TrimSpace(r.ClientID) || r.RedirectURI == "" || r.Domain == "" { + return ErrTelegramLoginRequestInvalid + } + if !r.SigningAlgorithm.Valid() || !r.Source.Valid() || (r.ResponseType != "code" && r.ResponseType != "post_message" && r.ResponseType != "legacy_url") || + r.Status != TelegramLoginRequestPending || r.CreatedAt.IsZero() || !r.ExpiresAt.After(r.CreatedAt) { + return ErrTelegramLoginRequestInvalid + } + switch r.Source { + case TelegramLoginRequestWeb: + if r.ResponseType != "code" { + return ErrTelegramLoginRequestInvalid + } + case TelegramLoginRequestJavaScript: + if r.ResponseType != "post_message" { + return ErrTelegramLoginRequestInvalid + } + case TelegramLoginRequestNative: + if r.ResponseType != "code" || !r.IsApp || r.VerifiedAppName == "" || r.Origin != "" { + return ErrTelegramLoginRequestInvalid + } + case TelegramLoginRequestMiniApp: + if r.ResponseType != "post_message" { + return ErrTelegramLoginRequestInvalid + } + case TelegramLoginRequestMessageButton: + if r.ResponseType != "legacy_url" { + return ErrTelegramLoginRequestInvalid + } + default: + return ErrTelegramLoginRequestInvalid + } + if r.Source != TelegramLoginRequestNative && (r.IsApp || r.VerifiedAppName != "" || r.Origin == "") { + return ErrTelegramLoginRequestInvalid + } + if r.AuthorizedUserID != 0 || r.ProfileName != "" || r.GivenName != "" || r.FamilyName != "" || + r.PreferredUsername != "" || r.Picture != "" || r.PhoneNumber != "" || r.WriteAllowed || r.PhoneShared || + !r.ApprovedAt.IsZero() || !r.DeclinedAt.IsZero() { + return ErrTelegramLoginRequestInvalid + } + if len(r.RedirectURI) > 4096 || len(r.Origin) > 4096 || len(r.Domain) > 255 || len(r.InAppOrigin) > 4096 || + len(r.State) > 2048 || len(r.Nonce) > 1024 || len(r.Browser) == 0 || len(r.Browser) > 255 || + len(r.Platform) == 0 || len(r.Platform) > 255 || len(r.IP) == 0 || len(r.IP) > 128 || + len(r.Region) == 0 || len(r.Region) > 255 || len(r.VerifiedAppName) > 128 || r.UserIDHint < 0 || + r.PeerID < 0 || r.MessageID < 0 || r.ButtonID < 0 || len(r.MatchCodes) > 8 { + return ErrTelegramLoginRequestInvalid + } + if r.ResponseType == "legacy_url" { + if r.Source != TelegramLoginRequestMessageButton || r.PeerID <= 0 || r.MessageID <= 0 || + (r.PeerType != PeerTypeUser && r.PeerType != PeerTypeChannel) || r.CodeChallenge != "" || r.CodeChallengeMethod != "" || + len(r.MatchCodes) != 0 || r.MatchCode != "" || r.MatchCodesFirst { + return ErrTelegramLoginRequestInvalid + } + if !slices.Contains(r.Scopes, TelegramLoginScopeOpenID) || !slices.Contains(r.Scopes, TelegramLoginScopeProfile) { + return ErrTelegramLoginScopeInvalid + } + seen := make(map[TelegramLoginScope]struct{}, len(r.Scopes)) + for _, scope := range r.Scopes { + if !scope.Valid() || scope == TelegramLoginScopePhone { + return ErrTelegramLoginScopeInvalid + } + if _, duplicate := seen[scope]; duplicate { + return ErrTelegramLoginScopeInvalid + } + seen[scope] = struct{}{} + } + } else if r.ResponseType == "code" { + if err := ValidateTelegramLoginScopes(r.Scopes, r.SigningAlgorithm); err != nil { + return err + } + if r.CodeChallengeMethod != "S256" || r.CodeChallenge == "" { + return ErrTelegramLoginPKCEInvalid + } + } else { + if err := ValidateTelegramLoginScopes(r.Scopes, r.SigningAlgorithm); err != nil { + return err + } + // Telegram's official JavaScript SDK returns an ID token directly and + // therefore sends no authorization-code PKCE parameters. Accept a PKCE + // pair for generic callers, but never a partial pair. + if r.CodeChallenge == "" && r.CodeChallengeMethod == "" { + // Official post_message/Mini App shape. + } else if r.CodeChallengeMethod != "S256" || r.CodeChallenge == "" { + return ErrTelegramLoginPKCEInvalid + } + } + if r.Source == TelegramLoginRequestMiniApp { + if r.ResponseType != "post_message" || r.InAppOrigin == "" || r.Origin != r.InAppOrigin { + return ErrTelegramLoginRequestInvalid + } + } else if r.InAppOrigin != "" { + return ErrTelegramLoginRequestInvalid + } + if r.MatchCodesFirst && len(r.MatchCodes) == 0 { + return ErrTelegramLoginRequestInvalid + } + if len(r.MatchCodes) > 0 && (r.MatchCode == "" || !slices.Contains(r.MatchCodes, r.MatchCode)) { + return ErrTelegramLoginRequestInvalid + } + return nil +} + +// TelegramLoginMessageButtonAuthorization is the domain-only input for the +// legacy login_url consent path. BotToken is used transiently to produce the +// official HMAC response and is never persisted in the login aggregate. +type TelegramLoginMessageButtonAuthorization struct { + UserID int64 + BotUserID int64 + BotToken string + URL string + RequestWriteAccess bool + WriteAllowed bool + Peer Peer + MessageID int + ButtonID int + Browser string + Platform string + IP string + Region string + Identity TelegramLoginIdentitySnapshot +} + +type TelegramLoginMessageButtonResult struct { + URL string + Request TelegramLoginRequest + WebAuthorization TelegramLoginWebAuthorization +} + +func (s TelegramLoginRequestSource) Valid() bool { + switch s { + case TelegramLoginRequestWeb, TelegramLoginRequestJavaScript, TelegramLoginRequestNative, + TelegramLoginRequestMiniApp, TelegramLoginRequestMessageButton: + return true + default: + return false + } +} + +// TelegramLoginIdentitySnapshot is the immutable identity presented on the +// approval screen and later signed into the ID token. It is written together +// with the pending->approved transition so a profile/phone mutation between +// approval and code exchange cannot change what the relying party receives. +type TelegramLoginIdentitySnapshot struct { + UserID int64 + Name string + GivenName string + FamilyName string + PreferredUsername string + Picture string + PhoneNumber string +} + +func (s TelegramLoginIdentitySnapshot) Sanitized(includeProfile, includePhone bool) (TelegramLoginIdentitySnapshot, error) { + if s.UserID <= 0 { + return TelegramLoginIdentitySnapshot{}, ErrTelegramLoginRequestInvalid + } + out := TelegramLoginIdentitySnapshot{UserID: s.UserID} + if includeProfile { + out.Name = strings.TrimSpace(s.Name) + out.GivenName = strings.TrimSpace(s.GivenName) + out.FamilyName = strings.TrimSpace(s.FamilyName) + out.PreferredUsername = strings.TrimSpace(s.PreferredUsername) + out.Picture = strings.TrimSpace(s.Picture) + if out.Name == "" || out.GivenName == "" { + return TelegramLoginIdentitySnapshot{}, ErrTelegramLoginRequestInvalid + } + } + if includePhone { + out.PhoneNumber = NormalizePhone(s.PhoneNumber) + if !ValidPhone(out.PhoneNumber) { + return TelegramLoginIdentitySnapshot{}, ErrPhoneNumberInvalid + } + } + if !boundedUTF8(out.Name, 255) || !boundedUTF8(out.GivenName, 255) || !boundedUTF8(out.FamilyName, 255) || + !boundedUTF8(out.PreferredUsername, 64) || !boundedUTF8(out.Picture, 4096) || len(out.PhoneNumber) > 32 { + return TelegramLoginIdentitySnapshot{}, ErrTelegramLoginRequestInvalid + } + return out, nil +} + +func boundedUTF8(value string, maxBytes int) bool { + return utf8.ValidString(value) && len(value) <= maxBytes +} + +func ValidateTelegramLoginScopes(scopes []TelegramLoginScope, alg TelegramLoginSigningAlgorithm) error { + if !alg.Valid() || len(scopes) == 0 || !slices.Contains(scopes, TelegramLoginScopeOpenID) { + return ErrTelegramLoginScopeInvalid + } + seen := make(map[TelegramLoginScope]struct{}, len(scopes)) + for _, scope := range scopes { + if !scope.Valid() { + return ErrTelegramLoginScopeInvalid + } + if _, duplicate := seen[scope]; duplicate { + return ErrTelegramLoginScopeInvalid + } + seen[scope] = struct{}{} + } + if alg == TelegramLoginSigningEdDSA || alg == TelegramLoginSigningES256K { + if len(scopes) != 1 || scopes[0] != TelegramLoginScopeOpenID { + return ErrTelegramLoginScopeInvalid + } + } + return nil +} + +type TelegramLoginApproval struct { + RequestID int64 + Identity TelegramLoginIdentitySnapshot + WriteAllowed bool + PhoneShared bool + MatchCode string + ApprovedAt time.Time +} + +type TelegramLoginAuthorizationCode struct { + ID int64 + RequestID int64 + CodeHash []byte + SealedCode []byte + SealNonce []byte + SealKeyID string + IssuedAt time.Time + ExpiresAt time.Time + ConsumedAt time.Time +} + +// TelegramLoginCodeExchange carries the values already normalized/hashed by +// the application service. The durable store compares them again while the +// code/request/client rows are locked, closing redirect, PKCE and secret- +// rotation TOCTOU gaps between HTTP validation and one-time consumption. +type TelegramLoginCodeExchange struct { + CodeHash []byte + ClientID string + ClientSecretVersion int64 + RedirectURI string + CodeChallenge string + Now time.Time +} + +func (c TelegramLoginAuthorizationCode) Clone() TelegramLoginAuthorizationCode { + out := c + out.CodeHash = append([]byte(nil), c.CodeHash...) + out.SealedCode = append([]byte(nil), c.SealedCode...) + out.SealNonce = append([]byte(nil), c.SealNonce...) + return out +} + +type TelegramLoginWebAuthorization struct { + Hash int64 + RequestID int64 + UserID int64 + BotUserID int64 + Domain string + Browser string + Platform string + IP string + Region string + Scopes []TelegramLoginScope + PhoneShared bool + BotAccessGranted bool + CreatedAt time.Time + LastActiveAt time.Time + RevokedAt time.Time +} + +func (a TelegramLoginWebAuthorization) Clone() TelegramLoginWebAuthorization { + out := a + out.Scopes = append([]TelegramLoginScope(nil), a.Scopes...) + return out +} diff --git a/internal/domain/telegram_login_test.go b/internal/domain/telegram_login_test.go new file mode 100644 index 00000000..792a8dfc --- /dev/null +++ b/internal/domain/telegram_login_test.go @@ -0,0 +1,112 @@ +package domain + +import ( + "strings" + "testing" + "time" +) + +func TestValidateTelegramLoginScopes(t *testing.T) { + tests := []struct { + name string + scopes []TelegramLoginScope + alg TelegramLoginSigningAlgorithm + valid bool + }{ + {name: "rs profile phone", scopes: []TelegramLoginScope{TelegramLoginScopeOpenID, TelegramLoginScopeProfile, TelegramLoginScopePhone}, alg: TelegramLoginSigningRS256, valid: true}, + {name: "missing openid", scopes: []TelegramLoginScope{TelegramLoginScopeProfile}, alg: TelegramLoginSigningRS256}, + {name: "duplicate", scopes: []TelegramLoginScope{TelegramLoginScopeOpenID, TelegramLoginScopeOpenID}, alg: TelegramLoginSigningRS256}, + {name: "unknown", scopes: []TelegramLoginScope{TelegramLoginScopeOpenID, "admin"}, alg: TelegramLoginSigningRS256}, + {name: "eddsa openid", scopes: []TelegramLoginScope{TelegramLoginScopeOpenID}, alg: TelegramLoginSigningEdDSA, valid: true}, + {name: "eddsa profile forbidden", scopes: []TelegramLoginScope{TelegramLoginScopeOpenID, TelegramLoginScopeProfile}, alg: TelegramLoginSigningEdDSA}, + {name: "es256k phone forbidden", scopes: []TelegramLoginScope{TelegramLoginScopeOpenID, TelegramLoginScopePhone}, alg: TelegramLoginSigningES256K}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := ValidateTelegramLoginScopes(test.scopes, test.alg) + if (err == nil) != test.valid { + t.Fatalf("ValidateTelegramLoginScopes() error = %v, valid = %v", err, test.valid) + } + }) + } +} + +func TestTelegramLoginRequestTransitions(t *testing.T) { + for _, terminal := range []TelegramLoginRequestState{ + TelegramLoginRequestApproved, + TelegramLoginRequestDeclined, + TelegramLoginRequestExpired, + } { + if !CanTransitionTelegramLoginRequest(TelegramLoginRequestPending, terminal) { + t.Fatalf("pending -> %s must be valid", terminal) + } + if CanTransitionTelegramLoginRequest(terminal, TelegramLoginRequestPending) { + t.Fatalf("%s -> pending must be forbidden", terminal) + } + } + if CanTransitionTelegramLoginRequest(TelegramLoginRequestApproved, TelegramLoginRequestDeclined) { + t.Fatal("approved -> declined must be forbidden") + } +} + +func TestTelegramLoginRequestSourceShapeMatrix(t *testing.T) { + now := time.Unix(1_780_000_000, 0).UTC() + base := TelegramLoginRequest{ + RequestTokenHash: make([]byte, 32), BrowserTokenHash: make([]byte, 32), + BotUserID: 9001, ClientID: "9001", SigningAlgorithm: TelegramLoginSigningRS256, + Source: TelegramLoginRequestWeb, ResponseType: "code", RedirectURI: "https://rp.example/callback", + Origin: "https://rp.example", Domain: "rp.example", Scopes: []TelegramLoginScope{TelegramLoginScopeOpenID}, + CodeChallenge: strings.Repeat("A", 43), CodeChallengeMethod: "S256", + Browser: "Firefox", Platform: "Windows", IP: "192.0.2.1", Region: "Test", + Status: TelegramLoginRequestPending, CreatedAt: now, ExpiresAt: now.Add(5 * time.Minute), + } + if err := base.Validate(); err != nil { + t.Fatalf("valid web request: %v", err) + } + invalid := []struct { + name string + mutate func(*TelegramLoginRequest) + }{ + {name: "web post message", mutate: func(r *TelegramLoginRequest) { + r.ResponseType = "post_message" + r.CodeChallenge = "" + r.CodeChallengeMethod = "" + }}, + {name: "javascript code", mutate: func(r *TelegramLoginRequest) { r.Source = TelegramLoginRequestJavaScript }}, + {name: "message button code", mutate: func(r *TelegramLoginRequest) { r.Source = TelegramLoginRequestMessageButton }}, + {name: "web app flag", mutate: func(r *TelegramLoginRequest) { r.IsApp = true; r.VerifiedAppName = "Forged" }}, + {name: "web missing origin", mutate: func(r *TelegramLoginRequest) { r.Origin = "" }}, + } + for _, tc := range invalid { + t.Run(tc.name, func(t *testing.T) { + request := base.Clone() + tc.mutate(&request) + if err := request.Validate(); err == nil { + t.Fatal("forbidden source shape was accepted") + } + }) + } + + native := base.Clone() + native.Source, native.Origin, native.Domain = TelegramLoginRequestNative, "", "dev.bedolaga.demo" + native.IsApp, native.VerifiedAppName = true, "Bedolaga" + if err := native.Validate(); err != nil { + t.Fatalf("valid native request: %v", err) + } + native.IsApp = false + if err := native.Validate(); err == nil { + t.Fatal("native request without verified app state was accepted") + } + + mini := base.Clone() + mini.Source, mini.ResponseType = TelegramLoginRequestMiniApp, "post_message" + mini.CodeChallenge, mini.CodeChallengeMethod = "", "" + mini.RedirectURI, mini.InAppOrigin = "https://rp.example/", mini.Origin + if err := mini.Validate(); err != nil { + t.Fatalf("valid Mini App request: %v", err) + } + mini.InAppOrigin = "https://other.example" + if err := mini.Validate(); err == nil { + t.Fatal("Mini App origin mismatch was accepted") + } +} diff --git a/internal/rpc/account.go b/internal/rpc/account.go index 9f729fa9..b48cf1fb 100644 --- a/internal/rpc/account.go +++ b/internal/rpc/account.go @@ -380,14 +380,10 @@ func (r *Router) registerAccount(d *tlprofile.Dispatcher) { ID) }) registerRPC[*tg.AccountGetWebAuthorizationsRequest](d, tlprofile.SemanticMethodAccountGetWebAuthorizations, func(ctx context.Context, layerRequest *tg.AccountGetWebAuthorizationsRequest) (any, error) { - return tdesktop.WebAuthorizations(), nil + return r.onAccountGetWebAuthorizations(ctx) }) registerRPC[*tg.AccountResetWebAuthorizationRequest](d, tlprofile.SemanticMethodAccountResetWebAuthorization, func(ctx context.Context, layerRequest *tg.AccountResetWebAuthorizationRequest) (any, error) { - hash := layerRequest. - Hash - _ = hash - - return true, nil + return r.onAccountResetWebAuthorization(ctx, layerRequest.Hash) }) registerRPC[*tg.AccountResetWebAuthorizationsRequest](d, tlprofile.SemanticMethodAccountResetWebAuthorizations, func(ctx context.Context, layerRequest *tg.AccountResetWebAuthorizationsRequest) ( @@ -395,7 +391,7 @@ func (r *Router) registerAccount(d *tlprofile.Dispatcher) { // (无内置浏览器例外、不强制外部浏览器)。Android 启动时会拉取,缺它会反复 500 // NOT_IMPLEMENTED。空结构 Hash=0,客户端按默认(内置浏览器、无例外)渲染。 any, error) { - return true, nil + return r.onAccountResetWebAuthorizations(ctx) }) registerRPC[*tg.AccountGetWebBrowserSettingsRequest](d, tlprofile.SemanticMethodAccountGetWebBrowserSettings, func(ctx context.Context, layerRequest *tg.AccountGetWebBrowserSettingsRequest) (any, error) { hash := layerRequest. diff --git a/internal/rpc/botapi_gateway.go b/internal/rpc/botapi_gateway.go index 2df966ed..c6a02a56 100644 --- a/internal/rpc/botapi_gateway.go +++ b/internal/rpc/botapi_gateway.go @@ -834,6 +834,14 @@ func (r *Router) BotAPIEditMessageText(ctx context.Context, botID, chatID int64, return domain.Message{}, errors.New("MESSAGE_TOO_LONG") } peer := domain.Peer{Type: domain.PeerTypeUser, ID: chatID} + if setReplyMarkup { + if err := domain.ValidateReplyMarkup(replyMarkup); err != nil { + return domain.Message{}, replyMarkupErr(err) + } + if err := r.validateReplyMarkupForPeer(ctx, botID, peer, replyMarkup); err != nil { + return domain.Message{}, err + } + } res, err := r.deps.Messages.EditMessage(ctx, botID, domain.EditMessageRequest{ OwnerUserID: botID, Peer: peer, @@ -934,6 +942,11 @@ func (r *Router) BotAPIEditInlineMessageText(ctx context.Context, botID int64, i if err := domain.ValidateReplyMarkup(replyMarkup); err != nil { return false, replyMarkupErr(err) } + if setReplyMarkup { + if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil { + return false, replyMarkupErr(err) + } + } req := &tg.MessagesEditInlineBotMessageRequest{ ID: tgInputBotInlineMessageID(inlineMessageID), NoWebpage: disableWebPagePreview, @@ -959,6 +972,11 @@ func (r *Router) BotAPIEditInlineRichMessage(ctx context.Context, botID int64, i if err := domain.ValidateReplyMarkup(replyMarkup); err != nil { return false, replyMarkupErr(err) } + if setReplyMarkup { + if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil { + return false, replyMarkupErr(err) + } + } wire, err := tgInputRichMessageFromBotAPI(input) if err != nil { return false, err diff --git a/internal/rpc/bots_inline.go b/internal/rpc/bots_inline.go index e96bbca6..30c4520d 100644 --- a/internal/rpc/bots_inline.go +++ b/internal/rpc/bots_inline.go @@ -300,6 +300,9 @@ func (r *Router) domainInlineResultsFromTG(ctx context.Context, botID int64, req if err != nil { return domain.BotInlineResults{}, err } + if err := r.prepareTelegramLoginMarkup(ctx, botID, item.ReplyMarkup); err != nil { + return domain.BotInlineResults{}, replyMarkupErr(err) + } if _, ok := seen[item.ID]; ok { return domain.BotInlineResults{}, resultIDDuplicateErr() } diff --git a/internal/rpc/convert_markup.go b/internal/rpc/convert_markup.go index ef248d2e..9005e162 100644 --- a/internal/rpc/convert_markup.go +++ b/internal/rpc/convert_markup.go @@ -3,6 +3,7 @@ package rpc import ( "context" "errors" + "strings" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tgerr" @@ -14,6 +15,9 @@ import ( // a chat input field and are not supported in broadcast channels. Inline keyboards remain // valid in both megagroups and broadcasts. func (r *Router) validateReplyMarkupForPeer(ctx context.Context, userID int64, peer domain.Peer, markup *domain.MessageReplyMarkup) error { + if err := r.prepareTelegramLoginMarkup(ctx, userID, markup); err != nil { + return replyMarkupErr(err) + } if markup == nil || !markup.IsReplyKeyboardFamily() || peer.Type != domain.PeerTypeChannel { return nil } @@ -30,6 +34,92 @@ func (r *Router) validateReplyMarkupForPeer(ctx context.Context, userID int64, p return nil } +// prepareTelegramLoginMarkup resolves every login_url target and validates its +// linked web origin before persistence. It mutates only the freshly parsed +// request DTO and assigns a deterministic flattened button id, which is later +// re-read by messages.requestUrlAuth. +func (r *Router) prepareTelegramLoginMarkup(ctx context.Context, senderBotID int64, markup *domain.MessageReplyMarkup) error { + if markup == nil || markup.Kind() != domain.MessageReplyMarkupInline { + return nil + } + hasLoginButton := false + for rowIndex := range markup.Inline { + for buttonIndex := range markup.Inline[rowIndex] { + if markup.Inline[rowIndex][buttonIndex].Type == domain.MarkupButtonLoginURL { + hasLoginButton = true + break + } + } + if hasLoginButton { + break + } + } + if !hasLoginButton { + return nil + } + if r == nil || r.deps.TelegramLogin == nil || r.deps.Users == nil || senderBotID <= 0 { + return domain.ErrButtonTypeInvalid + } + sender, found, err := r.deps.Users.ByID(ctx, senderBotID, senderBotID) + if err != nil { + return err + } + if !found || !sender.Bot || sender.Deleted { + return domain.ErrButtonTypeInvalid + } + flatID := 0 + for rowIndex := range markup.Inline { + for buttonIndex := range markup.Inline[rowIndex] { + button := &markup.Inline[rowIndex][buttonIndex] + if button.Type != domain.MarkupButtonLoginURL { + flatID++ + continue + } + botID := button.LoginBotUserID + if button.LoginBotUsername != "" { + resolver, ok := r.deps.Users.(UserIdentityService) + if !ok { + return domain.ErrButtonInvalid + } + bot, found, err := resolver.ResolveUsername(ctx, senderBotID, strings.TrimPrefix(button.LoginBotUsername, "@")) + if err != nil { + return err + } + if !found || !bot.Bot || bot.Deleted { + return domain.ErrButtonInvalid + } + botID = bot.ID + } + if botID == 0 { + botID = senderBotID + } + bot, found, err := r.deps.Users.ByID(ctx, senderBotID, botID) + if err != nil { + return err + } + if !found || !bot.Bot || bot.Deleted { + return domain.ErrButtonInvalid + } + normalized, _, err := r.deps.TelegramLogin.ValidateMessageButton(ctx, botID, button.URL) + if err != nil { + if errors.Is(err, domain.ErrTelegramLoginURLInvalid) || errors.Is(err, domain.ErrTelegramLoginOriginNotAllowed) { + return domain.ErrButtonURLInvalid + } + if errors.Is(err, domain.ErrTelegramLoginClientDisabled) { + return domain.ErrButtonInvalid + } + return err + } + button.URL = normalized + button.LoginBotUserID = botID + button.LoginBotUsername = "" + button.ButtonID = flatID + flatID++ + } + } + return domain.ValidateReplyMarkup(markup) +} + // P3 reply_markup 错误码(对齐官方)。 func buttonDataInvalidErr() error { return tgerr.New(400, "BUTTON_DATA_INVALID") } func buttonInvalidErr() error { return tgerr.New(400, "BUTTON_INVALID") } @@ -175,21 +265,23 @@ func domainReplyKeyboardButton(button tg.KeyboardButtonClass) (domain.MarkupButt func domainInlineMarkup(inline *tg.ReplyInlineMarkup) (*domain.MessageReplyMarkup, error) { out := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: make([][]domain.MarkupButton, 0, len(inline.Rows))} + buttonID := 0 for _, row := range inline.Rows { domainRow := make([]domain.MarkupButton, 0, len(row.Buttons)) for _, btn := range row.Buttons { - db, err := domainMarkupButton(btn) + db, err := domainMarkupButton(btn, buttonID) if err != nil { return nil, err } domainRow = append(domainRow, db) + buttonID++ } out.Inline = append(out.Inline, domainRow) } return out, nil } -func domainMarkupButton(btn tg.KeyboardButtonClass) (domain.MarkupButton, error) { +func domainMarkupButton(btn tg.KeyboardButtonClass, buttonID int) (domain.MarkupButton, error) { style, icon, err := domainMarkupButtonStyle(btn) if err != nil { return domain.MarkupButton{}, err @@ -209,6 +301,26 @@ func domainMarkupButton(btn tg.KeyboardButtonClass) (domain.MarkupButton, error) Type: domain.MarkupButtonURL, Text: b.Text, URL: b.URL, Style: style, IconCustomEmojiID: icon, }, nil + case *tg.InputKeyboardButtonURLAuth: + botUserID := int64(0) + switch bot := b.Bot.(type) { + case nil, *tg.InputUserEmpty, *tg.InputUserSelf: + case *tg.InputUser: + botUserID = bot.UserID + default: + return domain.MarkupButton{}, domain.ErrButtonInvalid + } + return domain.MarkupButton{ + Type: domain.MarkupButtonLoginURL, Text: b.Text, URL: b.URL, + ForwardText: b.FwdText, ButtonID: buttonID, LoginBotUserID: botUserID, + RequestWriteAccess: b.RequestWriteAccess, Style: style, IconCustomEmojiID: icon, + }, nil + case *tg.KeyboardButtonURLAuth: + return domain.MarkupButton{ + Type: domain.MarkupButtonLoginURL, Text: b.Text, URL: b.URL, + ForwardText: b.FwdText, ButtonID: b.ButtonID, + Style: style, IconCustomEmojiID: icon, + }, nil case *tg.KeyboardButtonWebView: return domain.MarkupButton{Type: domain.MarkupButtonWebView, Text: b.Text, URL: b.URL, Style: style, IconCustomEmojiID: icon}, nil case *tg.KeyboardButtonSwitchInline: @@ -322,6 +434,15 @@ func tgMarkupButton(btn domain.MarkupButton) tg.KeyboardButtonClass { out.SetStyle(style) } return out + case domain.MarkupButtonLoginURL: + out := &tg.KeyboardButtonURLAuth{Text: btn.Text, URL: btn.URL, ButtonID: btn.ButtonID} + if btn.ForwardText != "" { + out.SetFwdText(btn.ForwardText) + } + if style, ok := tgMarkupButtonStyle(btn); ok { + out.SetStyle(style) + } + return out case domain.MarkupButtonWebView: out := &tg.KeyboardButtonWebView{Text: btn.Text, URL: btn.URL} if style, ok := tgMarkupButtonStyle(btn); ok { diff --git a/internal/rpc/convert_markup_test.go b/internal/rpc/convert_markup_test.go index a5336e79..834ca2df 100644 --- a/internal/rpc/convert_markup_test.go +++ b/internal/rpc/convert_markup_test.go @@ -71,6 +71,26 @@ func TestInlineButtonStyleTLDomainRoundTrip(t *testing.T) { } } +func TestLoginURLButtonTLDomainProjection(t *testing.T) { + button := &tg.InputKeyboardButtonURLAuth{ + Text: "Log in", URL: "https://example.com/login", Bot: &tg.InputUser{UserID: 9001, AccessHash: 77}, + } + button.SetRequestWriteAccess(true) + button.SetFwdText("Open login") + markup, err := domainReplyMarkupForSender(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{button}}}}, true) + if err != nil { + t.Fatal(err) + } + got := markup.Inline[0][0] + if got.Type != domain.MarkupButtonLoginURL || got.LoginBotUserID != 9001 || !got.RequestWriteAccess || got.ForwardText != "Open login" || got.ButtonID != 0 { + t.Fatalf("domain login_url = %#v", got) + } + wire, ok := tgReplyMarkup(markup).(*tg.ReplyInlineMarkup).Rows[0].Buttons[0].(*tg.KeyboardButtonURLAuth) + if !ok || wire.Text != "Log in" || wire.URL != "https://example.com/login" || wire.ButtonID != 0 || wire.FwdText != "Open login" { + t.Fatalf("wire login_url = %#v", wire) + } +} + func TestReplyKeyboardHideAndForceReplyTLDomainRoundTrip(t *testing.T) { hide, err := domainOutgoingReplyMarkupForSender(&tg.ReplyKeyboardHide{Selective: true}, true) if err != nil { diff --git a/internal/rpc/deps.go b/internal/rpc/deps.go index 2341b2bb..f4eab885 100644 --- a/internal/rpc/deps.go +++ b/internal/rpc/deps.go @@ -241,6 +241,24 @@ type UsersService interface { ByIDs(ctx context.Context, currentUserID int64, userIDs []int64) ([]domain.User, error) } +// TelegramLoginService is the domain-only boundary shared by the MTProto RPC +// edge and the public OIDC provider. PostgreSQL remains authoritative for all +// consent transitions; the RPC layer only projects domain state to TL. +type TelegramLoginService interface { + ValidateMessageButton(ctx context.Context, botUserID int64, rawURL string) (normalizedURL, domainName string, err error) + AuthorizeMessageButton(ctx context.Context, params domain.TelegramLoginMessageButtonAuthorization) (domain.TelegramLoginMessageButtonResult, error) + RequestByDeepLink(ctx context.Context, deepLink string) (domain.TelegramLoginRequest, error) + RequestByDeepLinkForOrigin(ctx context.Context, deepLink, inAppOrigin string) (domain.TelegramLoginRequest, error) + CheckMatchCode(ctx context.Context, deepLink, selected string) (bool, error) + Approve(ctx context.Context, deepLink string, identity domain.TelegramLoginIdentitySnapshot, writeAllowed, phoneShared bool, matchCode string) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) + FinalizeRedirectByDeepLink(ctx context.Context, deepLink string) (string, error) + FinalizeInAppRedirectByDeepLink(ctx context.Context, deepLink string) (string, error) + Decline(ctx context.Context, deepLink string, userID int64) (domain.TelegramLoginRequest, error) + ListWebAuthorizations(ctx context.Context, userID int64) ([]domain.TelegramLoginWebAuthorization, error) + RevokeWebAuthorization(ctx context.Context, userID, hash int64) error + RevokeAllWebAuthorizations(ctx context.Context, userID int64) (int64, error) +} + // BatchViewerUsersResolver 是 UsersService 的可选能力:跨多个 viewer 一次性投影同一组 user // (fan-out 模板化,把 per-recipient 的 ByIDs(=ForViewer) 折叠成 O(owner) 查询)。结果按 viewer // 与 ByIDs(viewer, ids) 字节等价(personal photo overlay 除外,见 users.ByIDsForViewers)。 @@ -873,6 +891,7 @@ type Deps struct { EphemeralPush store.EphemeralPushBroker EphemeralReports store.EphemeralReportStore Users UsersService + TelegramLogin TelegramLoginService Updates UpdatesService BootstrapUpdates store.BootstrapUpdateJobStore BotAPIUpdates store.BotAPIUpdateStore diff --git a/internal/rpc/messages_bot_no_state.go b/internal/rpc/messages_bot_no_state.go index 2aea421e..8cedef6e 100644 --- a/internal/rpc/messages_bot_no_state.go +++ b/internal/rpc/messages_bot_no_state.go @@ -35,6 +35,9 @@ func (r *Router) onMessagesSavePreparedInlineMessage(ctx context.Context, req *t if err != nil { return nil, err } + if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil { + return nil, replyMarkupErr(err) + } peerTypes, err := preparedInlinePeerTypesFromTG(req.PeerTypes) if err != nil { return nil, err @@ -159,6 +162,11 @@ func (r *Router) editPrivateInlineBotMessage(ctx context.Context, botID int64, t setReplyMarkup = true } } + if setReplyMarkup { + if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil { + return false, replyMarkupErr(err) + } + } _, err = r.deps.Messages.EditMessage(ctx, target.OwnerUserID, domain.EditMessageRequest{ OwnerUserID: target.OwnerUserID, Peer: target.Peer, @@ -246,6 +254,11 @@ func (r *Router) editChannelInlineBotMessage(ctx context.Context, botID int64, t setReplyMarkup = true } } + if setReplyMarkup { + if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil { + return false, replyMarkupErr(err) + } + } res, err := r.deps.Channels.EditInlineBotMessage(ctx, botID, domain.EditChannelMessageRequest{ UserID: target.SenderUserID, ChannelID: target.ChannelID, diff --git a/internal/rpc/messages_edit.go b/internal/rpc/messages_edit.go index 816daae4..2e09ca6a 100644 --- a/internal/rpc/messages_edit.go +++ b/internal/rpc/messages_edit.go @@ -110,6 +110,11 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit setReplyMarkup = true } } + if setReplyMarkup { + if err := r.validateReplyMarkupForPeer(ctx, userID, peer, replyMarkup); err != nil { + return nil, err + } + } if peer.Type == domain.PeerTypeChannel { if r.deps.Channels == nil { return nil, peerIDInvalidErr() @@ -125,6 +130,8 @@ func (r *Router) onMessagesEditMessage(ctx context.Context, req *tg.MessagesEdit Message: message, Entities: domainMessageEntitiesForViewer(userID, entities), MentionUserIDs: mentionUserIDs, + SetReplyMarkup: setReplyMarkup, + ReplyMarkup: replyMarkup, SetRichMessage: replaceRichMessage, RichMessage: richMessage, EditDate: int(r.clock.Now().Unix()), diff --git a/internal/rpc/messages_register.go b/internal/rpc/messages_register.go index 515d19fc..809f1c5a 100644 --- a/internal/rpc/messages_register.go +++ b/internal/rpc/messages_register.go @@ -11,6 +11,18 @@ import ( // registerMessages 注册 messages.* RPC handler。 func (r *Router) registerMessages(d *tlprofile.Dispatcher) { + registerRPC[*tg.MessagesRequestURLAuthRequest](d, tlprofile.SemanticMethodMessagesRequestURLAuth, func(ctx context.Context, req *tg.MessagesRequestURLAuthRequest) (any, error) { + return r.onMessagesRequestURLAuth(ctx, req) + }) + registerRPC[*tg.MessagesAcceptURLAuthRequest](d, tlprofile.SemanticMethodMessagesAcceptURLAuth, func(ctx context.Context, req *tg.MessagesAcceptURLAuthRequest) (any, error) { + return r.onMessagesAcceptURLAuth(ctx, req) + }) + registerRPC[*tg.MessagesDeclineURLAuthRequest](d, tlprofile.SemanticMethodMessagesDeclineURLAuth, func(ctx context.Context, req *tg.MessagesDeclineURLAuthRequest) (any, error) { + return r.onMessagesDeclineURLAuth(ctx, req.URL) + }) + registerRPC[*tg.MessagesCheckURLAuthMatchCodeRequest](d, tlprofile.SemanticMethodMessagesCheckURLAuthMatchCode, func(ctx context.Context, req *tg.MessagesCheckURLAuthMatchCodeRequest) (any, error) { + return r.onMessagesCheckURLAuthMatchCode(ctx, req.URL, req.MatchCode) + }) registerRPC[*tg.MessagesReceivedMessagesRequest](d, tlprofile.SemanticMethodMessagesReceivedMessages, func(ctx context.Context, layerRequest *tg.MessagesReceivedMessagesRequest) (any, error) { return r.onMessagesReceivedMessages(ctx, layerRequest. MaxID) diff --git a/internal/rpc/messages_webview.go b/internal/rpc/messages_webview.go index 74c224c6..44e1b468 100644 --- a/internal/rpc/messages_webview.go +++ b/internal/rpc/messages_webview.go @@ -314,6 +314,9 @@ func (r *Router) onMessagesSendWebViewResultMessage(ctx context.Context, req *tg if err != nil { return nil, err } + if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil { + return nil, replyMarkupErr(err) + } if err := r.sendWebViewDomainResultMessage(ctx, botID, req.BotQueryID, result); err != nil { return nil, err } @@ -336,6 +339,9 @@ func (r *Router) AnswerWebAppQueryFromBotAPI(ctx context.Context, botID int64, b } else if !found { return "", userBotRequiredErr() } + if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil { + return "", replyMarkupErr(err) + } if err := r.sendWebViewDomainResultMessage(ctx, botID, botQueryID, result); err != nil { return "", err } @@ -362,6 +368,9 @@ func (r *Router) SavePreparedInlineMessageFromBotAPI(ctx context.Context, botID, } else if !found { return "", 0, userIDInvalidErr() } + if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil { + return "", 0, replyMarkupErr(err) + } id, expireDate := r.inlines.savePreparedInlineContext(ctx, r.clock.Now(), botID, userID, result, peerTypes) return id, expireDate, nil } diff --git a/internal/rpc/telegram_login.go b/internal/rpc/telegram_login.go new file mode 100644 index 00000000..352a5b9b --- /dev/null +++ b/internal/rpc/telegram_login.go @@ -0,0 +1,434 @@ +package rpc + +import ( + "context" + "errors" + "math" + "net/url" + "strconv" + "strings" + "time" + + "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tgerr" + + "telesrv/internal/domain" +) + +func telegramLoginOAuthInvalidErr() error { return tgerr.New(500, "OAUTH_REQUEST_INVALID") } +func telegramLoginURLExpiredErr() error { return tgerr.New(400, "URL_EXPIRED") } +func telegramLoginURLInvalidErr() error { return tgerr.New(400, "URL_INVALID") } +func telegramLoginHashInvalidErr() error { return tgerr.New(400, "HASH_INVALID") } + +func telegramLoginRPCError(err error) error { + switch { + case errors.Is(err, domain.ErrTelegramLoginRequestExpired): + return telegramLoginURLExpiredErr() + case errors.Is(err, domain.ErrTelegramLoginURLInvalid): + return telegramLoginURLInvalidErr() + case errors.Is(err, domain.ErrTelegramLoginMatchCodeInvalid), + errors.Is(err, domain.ErrTelegramLoginRequestInvalid), + errors.Is(err, domain.ErrTelegramLoginRequestConflict), + errors.Is(err, domain.ErrTelegramLoginClientDisabled), + errors.Is(err, domain.ErrTelegramLoginOriginNotAllowed), + errors.Is(err, domain.ErrTelegramLoginRedirectNotAllowed), + errors.Is(err, domain.ErrTelegramLoginScopeInvalid), + errors.Is(err, domain.ErrTelegramLoginAuthorizationsTooMany): + return telegramLoginOAuthInvalidErr() + default: + return internalErr() + } +} + +func (r *Router) requireTelegramLoginUser(ctx context.Context) (int64, error) { + userID, _, err := r.currentUserID(ctx) + if err != nil || userID <= 0 || r.deps.TelegramLogin == nil || r.deps.Users == nil { + return 0, internalErr() + } + self, err := r.deps.Users.Self(ctx, userID) + if err != nil || self.Bot || self.Deleted { + return 0, telegramLoginOAuthInvalidErr() + } + return userID, nil +} + +func (r *Router) telegramLoginRequestResult(ctx context.Context, viewerUserID int64, request domain.TelegramLoginRequest, deepLink string) (tg.URLAuthResultClass, error) { + switch request.Status { + case domain.TelegramLoginRequestApproved: + if request.AuthorizedUserID != viewerUserID { + return nil, telegramLoginOAuthInvalidErr() + } + return r.telegramLoginAcceptedResult(ctx, request, deepLink) + case domain.TelegramLoginRequestPending: + // Continue below. + case domain.TelegramLoginRequestDeclined, domain.TelegramLoginRequestExpired: + return nil, telegramLoginURLExpiredErr() + default: + return nil, telegramLoginOAuthInvalidErr() + } + bot, found, err := r.deps.Users.ByID(ctx, viewerUserID, request.BotUserID) + if err != nil { + return nil, internalErr() + } + if !found || !bot.Bot || bot.Deleted { + return nil, telegramLoginOAuthInvalidErr() + } + botTL := r.withBotProfileFlags(ctx, r.tgUser(bot)) + out := &tg.URLAuthResultRequest{ + RequestWriteAccess: request.Requests(domain.TelegramLoginScopeBotAccess), + RequestPhoneNumber: request.Requests(domain.TelegramLoginScopePhone), + MatchCodesFirst: request.MatchCodesFirst, + IsApp: request.IsApp, + Bot: botTL, + Domain: request.Domain, + } + // OAuth requests carry the complete device tuple. Keep the four fields on + // their shared flag together so old exact-layer codecs never see a partial + // conditional shape. + if request.Browser != "" && request.Platform != "" && request.IP != "" && request.Region != "" { + out.SetBrowser(request.Browser) + out.SetPlatform(request.Platform) + out.SetIP(request.IP) + out.SetRegion(request.Region) + } + if len(request.MatchCodes) > 0 { + out.SetMatchCodes(append([]string(nil), request.MatchCodes...)) + } + if request.UserIDHint > 0 { + out.SetUserIDHint(request.UserIDHint) + } + if request.IsApp && request.VerifiedAppName != "" { + out.SetVerifiedAppName(request.VerifiedAppName) + } + return out, nil +} + +func (r *Router) telegramLoginAcceptedResult(ctx context.Context, request domain.TelegramLoginRequest, deepLink string) (tg.URLAuthResultClass, error) { + accepted := &tg.URLAuthResultAccepted{} + switch { + case request.Source == domain.TelegramLoginRequestNative && request.IsApp: + redirectURL, err := r.deps.TelegramLogin.FinalizeRedirectByDeepLink(ctx, deepLink) + if err != nil { + return nil, telegramLoginRPCError(err) + } + accepted.SetURL(redirectURL) + case request.Source == domain.TelegramLoginRequestMiniApp: + resultURL, err := r.deps.TelegramLogin.FinalizeInAppRedirectByDeepLink(ctx, deepLink) + if err != nil { + return nil, telegramLoginRPCError(err) + } + accepted.SetURL(resultURL) + } + return accepted, nil +} + +func (r *Router) onMessagesRequestURLAuth(ctx context.Context, req *tg.MessagesRequestURLAuthRequest) (tg.URLAuthResultClass, error) { + userID, err := r.requireTelegramLoginUser(ctx) + if err != nil { + return nil, err + } + _, hasPeer := req.GetPeer() + urlValue, hasURL := req.GetURL() + _, hasOrigin := req.GetInAppOrigin() + if hasPeer == hasURL || (!hasPeer && strings.TrimSpace(urlValue) == "") || hasOrigin && !hasURL { + return nil, telegramLoginOAuthInvalidErr() + } + if hasPeer { + button, peer, err := r.telegramLoginButtonFromMessage(ctx, userID, req.Peer, req.MsgID, req.ButtonID) + if err != nil { + return nil, err + } + u, err := url.Parse(button.URL) + if err != nil || u.Hostname() == "" { + return nil, telegramLoginURLInvalidErr() + } + request := domain.TelegramLoginRequest{ + BotUserID: button.LoginBotUserID, Source: domain.TelegramLoginRequestMessageButton, + ResponseType: "legacy_url", RedirectURI: button.URL, Domain: u.Hostname(), + Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile}, + PeerType: peer.Type, PeerID: peer.ID, MessageID: req.MsgID, ButtonID: req.ButtonID, + Status: domain.TelegramLoginRequestPending, + } + if button.RequestWriteAccess { + request.Scopes = append(request.Scopes, domain.TelegramLoginScopeBotAccess) + } + return r.telegramLoginRequestResult(ctx, userID, request, "") + } + if hasOrigin && req.InAppOrigin == "" { + return nil, telegramLoginURLInvalidErr() + } + request, err := r.deps.TelegramLogin.RequestByDeepLinkForOrigin(ctx, urlValue, req.InAppOrigin) + if err != nil { + return nil, telegramLoginRPCError(err) + } + return r.telegramLoginRequestResult(ctx, userID, request, urlValue) +} + +func (r *Router) onMessagesAcceptURLAuth(ctx context.Context, req *tg.MessagesAcceptURLAuthRequest) (tg.URLAuthResultClass, error) { + userID, err := r.requireTelegramLoginUser(ctx) + if err != nil { + return nil, err + } + _, hasPeer := req.GetPeer() + deepLink, hasURL := req.GetURL() + matchCode, hasMatchCode := req.GetMatchCode() + if hasPeer == hasURL || (!hasPeer && strings.TrimSpace(deepLink) == "") || (hasMatchCode && matchCode == "") { + return nil, telegramLoginOAuthInvalidErr() + } + if hasPeer { + if hasMatchCode || req.SharePhoneNumber { + return nil, telegramLoginOAuthInvalidErr() + } + button, peer, err := r.telegramLoginButtonFromMessage(ctx, userID, req.Peer, req.MsgID, req.ButtonID) + if err != nil { + return nil, err + } + if r.deps.Bots == nil { + return nil, internalErr() + } + profile, found, err := r.deps.Bots.BotInfo(ctx, button.LoginBotUserID) + if err != nil { + return nil, internalErr() + } + if !found || profile.TokenSecret == "" { + return nil, telegramLoginOAuthInvalidErr() + } + self, err := r.deps.Users.Self(ctx, userID) + if err != nil { + return nil, internalErr() + } + identity := r.telegramLoginIdentity(self) + result, err := r.deps.TelegramLogin.AuthorizeMessageButton(ctx, domain.TelegramLoginMessageButtonAuthorization{ + UserID: userID, BotUserID: button.LoginBotUserID, + BotToken: domain.FormatBotToken(button.LoginBotUserID, profile.TokenSecret), URL: button.URL, + RequestWriteAccess: button.RequestWriteAccess, WriteAllowed: req.WriteAllowed, + Peer: peer, MessageID: req.MsgID, ButtonID: req.ButtonID, + Browser: "Telegram", Platform: "Telegram Client", IP: "Unknown IP", Region: "Unknown region", + Identity: identity, + }) + if err != nil { + return nil, telegramLoginRPCError(err) + } + accepted := &tg.URLAuthResultAccepted{} + accepted.SetURL(result.URL) + return accepted, nil + } + request, err := r.deps.TelegramLogin.RequestByDeepLink(ctx, deepLink) + if err != nil { + return nil, telegramLoginRPCError(err) + } + if request.Status == domain.TelegramLoginRequestApproved { + if request.AuthorizedUserID == userID { + return r.telegramLoginAcceptedResult(ctx, request, deepLink) + } + return nil, telegramLoginOAuthInvalidErr() + } + if request.Status != domain.TelegramLoginRequestPending { + return nil, telegramLoginURLExpiredErr() + } + self, err := r.deps.Users.Self(ctx, userID) + if err != nil { + return nil, internalErr() + } + identity := r.telegramLoginIdentity(self) + approved, _, err := r.deps.TelegramLogin.Approve(ctx, deepLink, identity, req.WriteAllowed, req.SharePhoneNumber, matchCode) + if err != nil { + return nil, telegramLoginRPCError(err) + } + if approved.AuthorizedUserID != userID { + return nil, telegramLoginOAuthInvalidErr() + } + return r.telegramLoginAcceptedResult(ctx, approved, deepLink) +} + +func (r *Router) telegramLoginIdentity(self domain.User) domain.TelegramLoginIdentitySnapshot { + identity := domain.TelegramLoginIdentitySnapshot{ + UserID: self.ID, Name: strings.TrimSpace(strings.TrimSpace(self.FirstName) + " " + strings.TrimSpace(self.LastName)), + GivenName: self.FirstName, FamilyName: self.LastName, + PreferredUsername: self.Username, PhoneNumber: self.Phone, + } + if strings.TrimSpace(r.cfg.PublicBaseURL) != "" && self.Username != "" && self.PhotoID > 0 { + identity.Picture = strings.TrimSuffix(r.cfg.PublicBaseURL, "/") + "/_public/avatar/" + url.PathEscape(self.Username) + "/" + strconv.FormatInt(self.PhotoID, 10) + } + return identity +} + +func (r *Router) telegramLoginButtonFromMessage(ctx context.Context, userID int64, inputPeer tg.InputPeerClass, messageID, buttonID int) (domain.MarkupButton, domain.Peer, error) { + if messageID <= 0 || messageID > domain.MaxMessageBoxID || buttonID < 0 { + return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() + } + peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inputPeer) + if err != nil { + return domain.MarkupButton{}, domain.Peer{}, err + } + var markup *domain.MessageReplyMarkup + switch peer.Type { + case domain.PeerTypeUser: + message, found, err := r.lookupOwnerMessage(ctx, userID, messageID) + if err != nil { + return domain.MarkupButton{}, domain.Peer{}, internalErr() + } + if !found || message.Peer != peer { + return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() + } + markup = message.ReplyMarkup + case domain.PeerTypeChannel: + if r.deps.Channels == nil { + return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() + } + history, err := r.deps.Channels.GetMessages(ctx, userID, peer.ID, []int{messageID}) + if err != nil || len(history.Messages) != 1 || history.Messages[0].ID != messageID { + return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() + } + markup = history.Messages[0].ReplyMarkup + default: + return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() + } + if markup == nil || markup.Kind() != domain.MessageReplyMarkupInline { + return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() + } + for _, row := range markup.Inline { + for _, button := range row { + if button.Type == domain.MarkupButtonLoginURL && button.ButtonID == buttonID && button.LoginBotUserID > 0 { + return button, peer, nil + } + } + } + return domain.MarkupButton{}, domain.Peer{}, telegramLoginOAuthInvalidErr() +} + +func (r *Router) onMessagesDeclineURLAuth(ctx context.Context, deepLink string) (bool, error) { + userID, err := r.requireTelegramLoginUser(ctx) + if err != nil { + return false, err + } + if strings.TrimSpace(deepLink) == "" { + return false, telegramLoginURLInvalidErr() + } + request, err := r.deps.TelegramLogin.RequestByDeepLink(ctx, deepLink) + if err != nil { + return false, telegramLoginRPCError(err) + } + if request.Status == domain.TelegramLoginRequestDeclined { + return true, nil + } + if request.Status != domain.TelegramLoginRequestPending { + return false, telegramLoginOAuthInvalidErr() + } + if _, err := r.deps.TelegramLogin.Decline(ctx, deepLink, userID); err != nil { + return false, telegramLoginRPCError(err) + } + return true, nil +} + +func (r *Router) onMessagesCheckURLAuthMatchCode(ctx context.Context, deepLink, matchCode string) (bool, error) { + if _, err := r.requireTelegramLoginUser(ctx); err != nil { + return false, err + } + if strings.TrimSpace(deepLink) == "" || matchCode == "" { + return false, telegramLoginURLInvalidErr() + } + ok, err := r.deps.TelegramLogin.CheckMatchCode(ctx, deepLink, matchCode) + if err != nil { + return false, telegramLoginRPCError(err) + } + return ok, nil +} + +func (r *Router) onAccountGetWebAuthorizations(ctx context.Context) (*tg.AccountWebAuthorizations, error) { + if r.deps.TelegramLogin == nil { + if _, _, err := r.currentUserID(ctx); err != nil { + return nil, internalErr() + } + return &tg.AccountWebAuthorizations{Authorizations: []tg.WebAuthorization{}, Users: []tg.UserClass{}}, nil + } + userID, err := r.requireTelegramLoginUser(ctx) + if err != nil { + return nil, err + } + authorizations, err := r.deps.TelegramLogin.ListWebAuthorizations(ctx, userID) + if err != nil { + return nil, internalErr() + } + result := &tg.AccountWebAuthorizations{ + Authorizations: make([]tg.WebAuthorization, 0, len(authorizations)), + Users: []tg.UserClass{}, + } + botIDs := make([]int64, 0, len(authorizations)) + seenBots := make(map[int64]struct{}, len(authorizations)) + for _, authorization := range authorizations { + result.Authorizations = append(result.Authorizations, tg.WebAuthorization{ + Hash: authorization.Hash, BotID: authorization.BotUserID, Domain: authorization.Domain, + Browser: authorization.Browser, Platform: authorization.Platform, + DateCreated: telegramLoginUnixInt(authorization.CreatedAt), DateActive: telegramLoginUnixInt(authorization.LastActiveAt), + IP: authorization.IP, Region: authorization.Region, + }) + if _, duplicate := seenBots[authorization.BotUserID]; !duplicate { + seenBots[authorization.BotUserID] = struct{}{} + botIDs = append(botIDs, authorization.BotUserID) + } + } + if len(botIDs) > 0 { + bots, err := r.deps.Users.ByIDs(ctx, userID, botIDs) + if err != nil { + return nil, internalErr() + } + for _, bot := range bots { + if bot.Bot && !bot.Deleted { + result.Users = append(result.Users, r.withBotProfileFlags(ctx, r.tgUser(bot))) + } + } + } + return result, nil +} + +func (r *Router) onAccountResetWebAuthorization(ctx context.Context, hash int64) (bool, error) { + if r.deps.TelegramLogin == nil { + if _, _, err := r.currentUserID(ctx); err != nil { + return false, internalErr() + } + return true, nil + } + userID, err := r.requireTelegramLoginUser(ctx) + if err != nil { + return false, err + } + if hash == 0 { + return false, telegramLoginHashInvalidErr() + } + if err := r.deps.TelegramLogin.RevokeWebAuthorization(ctx, userID, hash); err != nil { + if errors.Is(err, domain.ErrTelegramLoginWebAuthHashInvalid) { + return false, telegramLoginHashInvalidErr() + } + return false, internalErr() + } + return true, nil +} + +func (r *Router) onAccountResetWebAuthorizations(ctx context.Context) (bool, error) { + if r.deps.TelegramLogin == nil { + if _, _, err := r.currentUserID(ctx); err != nil { + return false, internalErr() + } + return true, nil + } + userID, err := r.requireTelegramLoginUser(ctx) + if err != nil { + return false, err + } + if _, err := r.deps.TelegramLogin.RevokeAllWebAuthorizations(ctx, userID); err != nil { + return false, internalErr() + } + return true, nil +} + +func telegramLoginUnixInt(value time.Time) int { + unix := value.Unix() + if unix < 0 { + return 0 + } + if unix > math.MaxInt32 { + return math.MaxInt32 + } + return int(unix) +} diff --git a/internal/rpc/telegram_login_rpc_test.go b/internal/rpc/telegram_login_rpc_test.go new file mode 100644 index 00000000..6b22ce70 --- /dev/null +++ b/internal/rpc/telegram_login_rpc_test.go @@ -0,0 +1,380 @@ +package rpc + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "net/url" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/iamxvbaba/td/clock" + "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tlprofile" + "go.uber.org/zap/zaptest" + + telegramloginapp "telesrv/internal/app/telegramlogin" + appusers "telesrv/internal/app/users" + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +type telegramLoginBotPermissionAdapter struct{ bots BotsService } + +func (a telegramLoginBotPermissionAdapter) AllowBotSendMessage(ctx context.Context, botUserID, userID int64, fromRequest bool) (bool, error) { + return a.bots.AllowSendMessage(ctx, userID, botUserID, fromRequest) +} + +type telegramLoginRPCFixture struct { + ctx context.Context + service *telegramloginapp.Service + router *Router + user domain.User + intruder domain.User + bot domain.User + client telegramloginapp.ClientCredentials + redirect string +} + +func newTelegramLoginRPCFixture(t *testing.T) *telegramLoginRPCFixture { + t.Helper() + ctx := context.Background() + users := memory.NewUserStore() + user, err := users.Create(ctx, domain.User{Phone: "+15551001", FirstName: "Alice", LastName: "Example", Username: "alice", AccessHash: 11}) + if err != nil { + t.Fatal(err) + } + intruder, err := users.Create(ctx, domain.User{Phone: "+15551002", FirstName: "Mallory", Username: "mallory", AccessHash: 13}) + if err != nil { + t.Fatal(err) + } + bot, err := users.Create(ctx, domain.User{FirstName: "Login Bot", Username: "login_rpc_bot", AccessHash: 12, Bot: true, BotInfoVersion: 1}) + if err != nil { + t.Fatal(err) + } + sealKey := make([]byte, 32) + sealKey[0] = 3 + sealer, err := telegramloginapp.NewCodeSealer("test", map[string][]byte{"test": sealKey}) + if err != nil { + t.Fatal(err) + } + pepper := make([]byte, 32) + pepper[0] = 4 + service, err := telegramloginapp.NewService(memory.NewTelegramLoginStore(nil), sealer, telegramloginapp.Config{ + Issuer: "https://oauth.test", AppScheme: "telesrv", ClientSecretPepper: pepper, + Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() }, + }) + if err != nil { + t.Fatal(err) + } + client, err := service.CreateClient(ctx, bot.ID, domain.TelegramLoginSigningRS256) + if err != nil { + t.Fatal(err) + } + redirect := "https://rp.test/callback" + if _, err := service.AddAllowedURL(ctx, bot.ID, domain.TelegramLoginAllowedRedirectURI, redirect); err != nil { + t.Fatal(err) + } + if _, err := service.AddAllowedURL(ctx, bot.ID, domain.TelegramLoginAllowedWebOrigin, "https://rp.test"); err != nil { + t.Fatal(err) + } + router := New(Config{}, Deps{Users: appusers.NewService(users), TelegramLogin: service}, zaptest.NewLogger(t), clock.System) + return &telegramLoginRPCFixture{ctx: WithUserID(ctx, user.ID), service: service, router: router, user: user, intruder: intruder, bot: bot, client: client, redirect: redirect} +} + +func (f *telegramLoginRPCFixture) authorization(t *testing.T, match bool) telegramloginapp.CreatedAuthorization { + t.Helper() + challenge, err := telegramloginapp.PKCEChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") + if err != nil { + t.Fatal(err) + } + created, err := f.service.CreateAuthorization(f.ctx, telegramloginapp.CreateAuthorizationParams{ + ClientID: f.client.Client.ClientID, RedirectURI: f.redirect, ResponseType: "code", + Scope: "openid profile telegram:bot_access", CodeChallenge: challenge, CodeChallengeMethod: "S256", + IncludeMatchCodes: match, MatchCodesFirst: match, + }) + if err != nil { + t.Fatal(err) + } + return created +} + +func TestTelegramLoginRPCsAcrossExactLayerProfiles(t *testing.T) { + for profile := tlprofile.Profile225; profile <= tlprofile.Profile228; profile++ { + t.Run(fmt.Sprintf("layer_%d", profile), func(t *testing.T) { + f := newTelegramLoginRPCFixture(t) + + approve := f.authorization(t, true) + // TDesktop normalizes the configured telesrv:// launcher to the + // official internal tg://oauth form before invoking MTProto. + canonicalURL := strings.Replace(approve.DeepLink, "telesrv://", "tg://", 1) + request := &tg.MessagesRequestURLAuthRequest{} + request.SetURL(canonicalURL) + result, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, request) + if method != "messages.requestUrlAuth" { + t.Fatalf("method = %q", method) + } + prompt, ok := dispatchCanonicalValue(result).(*tg.URLAuthResultRequest) + if !ok || prompt.Bot.GetID() != f.bot.ID || !prompt.RequestWriteAccess || len(prompt.MatchCodes) != 5 { + t.Fatalf("request result = %#v", dispatchCanonicalValue(result)) + } + + checked, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.MessagesCheckURLAuthMatchCodeRequest{ + URL: canonicalURL, MatchCode: approve.Request.MatchCode, + }) + if method != "messages.checkUrlAuthMatchCode" || dispatchCanonicalValue(checked) != true { + t.Fatalf("check result = %#v method=%q", dispatchCanonicalValue(checked), method) + } + accept := &tg.MessagesAcceptURLAuthRequest{WriteAllowed: true} + accept.SetURL(canonicalURL) + accept.SetMatchCode(approve.Request.MatchCode) + accepted, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, accept) + if method != "messages.acceptUrlAuth" { + t.Fatalf("accept method = %q", method) + } + if _, ok := dispatchCanonicalValue(accepted).(*tg.URLAuthResultAccepted); !ok { + t.Fatalf("accept result = %#v", dispatchCanonicalValue(accepted)) + } + + decline := f.authorization(t, false) + declined, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.MessagesDeclineURLAuthRequest{URL: decline.DeepLink}) + if method != "messages.declineUrlAuth" || dispatchCanonicalValue(declined) != true { + t.Fatalf("decline result = %#v method=%q", dispatchCanonicalValue(declined), method) + } + + listed, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.AccountGetWebAuthorizationsRequest{}) + web, ok := dispatchCanonicalValue(listed).(*tg.AccountWebAuthorizations) + if method != "account.getWebAuthorizations" || !ok || len(web.Authorizations) != 1 || web.Authorizations[0].BotID != f.bot.ID { + t.Fatalf("getWebAuthorizations = %#v method=%q", dispatchCanonicalValue(listed), method) + } + reset, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.AccountResetWebAuthorizationRequest{Hash: web.Authorizations[0].Hash}) + if method != "account.resetWebAuthorization" || dispatchCanonicalValue(reset) != true { + t.Fatalf("resetWebAuthorization = %#v method=%q", dispatchCanonicalValue(reset), method) + } + + const nativeCallback = "bedolaga://telegram-login" + if _, err := f.service.AddNativeApp(f.ctx, f.bot.ID, domain.TelegramLoginNativeAndroid, + "dev.bedolaga.demo", strings.Repeat("A", 64), nativeCallback, "Bedolaga Android Demo"); err != nil { + t.Fatal(err) + } + challenge, err := telegramloginapp.PKCEChallenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk") + if err != nil { + t.Fatal(err) + } + native, err := f.service.CreateAuthorization(f.ctx, telegramloginapp.CreateAuthorizationParams{ + ClientID: f.client.Client.ClientID, RedirectURI: nativeCallback, ResponseType: "code", + Scope: "profile", CodeChallenge: challenge, CodeChallengeMethod: "S256", + NativePlatform: domain.TelegramLoginNativeAndroid, IncludeMatchCodes: true, MatchCodesFirst: true, + }) + if err != nil { + t.Fatal(err) + } + nativeRequest := &tg.MessagesRequestURLAuthRequest{} + nativeRequest.SetURL(native.DeepLink) + nativeResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, nativeRequest) + nativePrompt, ok := dispatchCanonicalValue(nativeResult).(*tg.URLAuthResultRequest) + if !ok || !nativePrompt.IsApp || nativePrompt.VerifiedAppName != "Bedolaga Android Demo" || len(nativePrompt.MatchCodes) != 5 { + t.Fatalf("native request result = %#v", dispatchCanonicalValue(nativeResult)) + } + nativeAccept := &tg.MessagesAcceptURLAuthRequest{} + nativeAccept.SetURL(native.DeepLink) + nativeAccept.SetMatchCode(native.Request.MatchCode) + nativeAcceptedResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, nativeAccept) + nativeAccepted, ok := dispatchCanonicalValue(nativeAcceptedResult).(*tg.URLAuthResultAccepted) + if !ok || !strings.HasPrefix(nativeAccepted.URL, nativeCallback+"?code=") { + t.Fatalf("native accepted result = %#v", dispatchCanonicalValue(nativeAcceptedResult)) + } + nativeRetryResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, nativeRequest) + nativeRetry, ok := dispatchCanonicalValue(nativeRetryResult).(*tg.URLAuthResultAccepted) + if !ok || nativeRetry.URL != nativeAccepted.URL { + t.Fatalf("native retry result = %#v, want URL %q", dispatchCanonicalValue(nativeRetryResult), nativeAccepted.URL) + } + + const miniAppOrigin = "https://rp.test" + miniApp, err := f.service.CreateAuthorization(f.ctx, telegramloginapp.CreateAuthorizationParams{ + ClientID: f.client.Client.ClientID, RedirectURI: miniAppOrigin + "/", ResponseType: "post_message", + Scope: "openid profile", Origin: miniAppOrigin, InAppOrigin: miniAppOrigin, + Source: domain.TelegramLoginRequestMiniApp, IncludeMatchCodes: true, MatchCodesFirst: true, + }) + if err != nil { + t.Fatal(err) + } + miniAppRequest := &tg.MessagesRequestURLAuthRequest{} + miniAppRequest.SetURL(miniApp.DeepLink) + miniAppRequest.SetInAppOrigin(miniAppOrigin) + miniAppResult, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, miniAppRequest) + miniAppPrompt, ok := dispatchCanonicalValue(miniAppResult).(*tg.URLAuthResultRequest) + if method != "messages.requestUrlAuth" || !ok || len(miniAppPrompt.MatchCodes) != 5 { + t.Fatalf("mini-app request result = %#v method=%q", dispatchCanonicalValue(miniAppResult), method) + } + miniAppAccept := &tg.MessagesAcceptURLAuthRequest{} + miniAppAccept.SetURL(miniApp.DeepLink) + miniAppAccept.SetMatchCode(miniApp.Request.MatchCode) + miniAppAcceptedResult, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, miniAppAccept) + miniAppAccepted, ok := dispatchCanonicalValue(miniAppAcceptedResult).(*tg.URLAuthResultAccepted) + if method != "messages.acceptUrlAuth" || !ok || !strings.HasPrefix(miniAppAccepted.URL, "https://oauth.test/inapp?token=") { + t.Fatalf("mini-app accepted result = %#v method=%q", dispatchCanonicalValue(miniAppAcceptedResult), method) + } + miniAppRetryResult, _ := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, miniAppRequest) + miniAppRetry, ok := dispatchCanonicalValue(miniAppRetryResult).(*tg.URLAuthResultAccepted) + if !ok || miniAppRetry.URL != miniAppAccepted.URL { + t.Fatalf("mini-app retry result = %#v, want URL %q", dispatchCanonicalValue(miniAppRetryResult), miniAppAccepted.URL) + } + resetAll, method := dispatchExactLayerRPCTest(t, f.router, f.ctx, profile, &tg.AccountResetWebAuthorizationsRequest{}) + if method != "account.resetWebAuthorizations" || dispatchCanonicalValue(resetAll) != true { + t.Fatalf("resetWebAuthorizations = %#v method=%q", dispatchCanonicalValue(resetAll), method) + } + }) + } +} + +func TestTelegramLoginApprovedDeepLinkRejectsAnotherUser(t *testing.T) { + f := newTelegramLoginRPCFixture(t) + created := f.authorization(t, false) + accept := &tg.MessagesAcceptURLAuthRequest{} + accept.SetURL(created.DeepLink) + if _, err := f.router.onMessagesAcceptURLAuth(f.ctx, accept); err != nil { + t.Fatal(err) + } + request := &tg.MessagesRequestURLAuthRequest{} + request.SetURL(created.DeepLink) + if _, err := f.router.onMessagesRequestURLAuth(WithUserID(context.Background(), f.intruder.ID), request); err == nil { + t.Fatal("another user observed an approved deep link as accepted") + } +} + +func TestTelegramLoginMessageButtonRereadSignsAndGrantsWriteAccess(t *testing.T) { + f := newBotAPIReceiveFixture(t, false) + sealKey := make([]byte, 32) + sealKey[0] = 7 + sealer, err := telegramloginapp.NewCodeSealer("test", map[string][]byte{"test": sealKey}) + if err != nil { + t.Fatal(err) + } + pepper := make([]byte, 32) + pepper[0] = 8 + loginStore := memory.NewTelegramLoginStore(telegramLoginBotPermissionAdapter{bots: f.router.deps.Bots}) + login, err := telegramloginapp.NewService(loginStore, sealer, telegramloginapp.Config{ + Issuer: "https://oauth.test", AppScheme: "telesrv", ClientSecretPepper: pepper, + Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() }, + }) + if err != nil { + t.Fatal(err) + } + if _, err := login.CreateClient(f.ctx, f.bot.ID, domain.TelegramLoginSigningRS256); err != nil { + t.Fatal(err) + } + if _, err := login.AddAllowedURL(f.ctx, f.bot.ID, domain.TelegramLoginAllowedWebOrigin, "https://rp.test"); err != nil { + t.Fatal(err) + } + f.router.deps.TelegramLogin = login + + markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{ + Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "https://rp.test/login?next=%2Fhome", RequestWriteAccess: true, + }}}} + if _, err := f.router.BotAPISendMessage(f.ctx, f.bot.ID, f.owner.ID, "Authorize", nil, markup, false, false, 0); err != nil { + t.Fatalf("BotAPISendMessage: %v", err) + } + history, err := f.messages.GetHistory(f.ctx, f.owner.ID, domain.MessageFilter{ + HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: f.bot.ID}, Limit: 10, + }) + if err != nil || len(history.Messages) == 0 { + t.Fatalf("GetHistory: messages=%d err=%v", len(history.Messages), err) + } + message := history.Messages[0] + if message.ReplyMarkup == nil || message.ReplyMarkup.Inline[0][0].LoginBotUserID != f.bot.ID { + t.Fatalf("persisted login button = %#v", message.ReplyMarkup) + } + + peer := &tg.InputPeerUser{UserID: f.bot.ID, AccessHash: f.bot.AccessHash} + request := &tg.MessagesRequestURLAuthRequest{} + request.SetPeer(peer) + request.SetMsgID(message.ID) + request.SetButtonID(0) + requested, err := f.router.onMessagesRequestURLAuth(WithUserID(f.ctx, f.owner.ID), request) + if err != nil { + t.Fatalf("requestUrlAuth: %v", err) + } + prompt, ok := requested.(*tg.URLAuthResultRequest) + if !ok || !prompt.RequestWriteAccess || prompt.Domain != "rp.test" { + t.Fatalf("requestUrlAuth result = %#v", requested) + } + accept := &tg.MessagesAcceptURLAuthRequest{} + accept.SetWriteAllowed(true) + accept.SetPeer(peer) + accept.SetMsgID(message.ID) + accept.SetButtonID(0) + accepted, err := f.router.onMessagesAcceptURLAuth(WithUserID(f.ctx, f.owner.ID), accept) + if err != nil { + t.Fatalf("acceptUrlAuth: %v", err) + } + final, ok := accepted.(*tg.URLAuthResultAccepted) + if !ok || final.URL == "" { + t.Fatalf("acceptUrlAuth result = %#v", accepted) + } + verifyLegacyTelegramLoginURL(t, final.URL, domain.FormatBotToken(f.bot.ID, "secret"), f.owner.ID) + if allowed, err := f.router.deps.Bots.CanSendMessage(f.ctx, f.owner.ID, f.bot.ID); err != nil || !allowed { + t.Fatalf("bot write permission = %v,%v", allowed, err) + } + web, err := login.ListWebAuthorizations(f.ctx, f.owner.ID) + if err != nil || len(web) != 1 || !web[0].BotAccessGranted || web[0].Domain != "rp.test" { + t.Fatalf("web authorizations = %#v err=%v", web, err) + } + + // The server must re-read durable message state. A forged button id never + // falls back to URL data supplied by the client. + forged := &tg.MessagesAcceptURLAuthRequest{} + forged.SetPeer(peer) + forged.SetMsgID(message.ID) + forged.SetButtonID(99) + if _, err := f.router.onMessagesAcceptURLAuth(WithUserID(f.ctx, f.owner.ID), forged); err == nil { + t.Fatal("forged button id was accepted") + } +} + +func TestTelegramLoginMarkupRequiresBotSender(t *testing.T) { + f := newTelegramLoginRPCFixture(t) + markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{ + Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "https://rp.test/login", + }}}} + if err := f.router.prepareTelegramLoginMarkup(WithUserID(f.ctx, f.user.ID), f.user.ID, markup); !errors.Is(err, domain.ErrButtonTypeInvalid) { + t.Fatalf("ordinary user login_url error = %v, want ErrButtonTypeInvalid", err) + } +} + +func verifyLegacyTelegramLoginURL(t *testing.T, raw, botToken string, wantUserID int64) { + t.Helper() + u, err := url.Parse(raw) + if err != nil { + t.Fatal(err) + } + query := u.Query() + provided := query.Get("hash") + query.Del("hash") + if query.Get("id") != strconv.FormatInt(wantUserID, 10) || query.Get("auth_date") == "" || query.Get("next") != "/home" { + t.Fatalf("legacy login query = %#v", query) + } + keys := make([]string, 0, len(query)) + for key := range query { + if key != "next" { // Existing application query fields are not signed. + keys = append(keys, key) + } + } + sort.Strings(keys) + lines := make([]string, 0, len(keys)) + for _, key := range keys { + lines = append(lines, key+"="+query.Get(key)) + } + secret := sha256.Sum256([]byte(botToken)) + mac := hmac.New(sha256.New, secret[:]) + _, _ = mac.Write([]byte(strings.Join(lines, "\n"))) + if !hmac.Equal([]byte(strings.ToLower(provided)), []byte(hex.EncodeToString(mac.Sum(nil)))) { + t.Fatalf("legacy login hash = %q, want %s", provided, hex.EncodeToString(mac.Sum(nil))) + } +} diff --git a/internal/store/memory/bot.go b/internal/store/memory/bot.go index c2eac3b8..63f457d1 100644 --- a/internal/store/memory/bot.go +++ b/internal/store/memory/bot.go @@ -75,6 +75,9 @@ func botFatherSeedProfile() domain.BotProfile { {Command: "mybots", Description: "list your bots"}, {Command: "token", Description: "show a bot's token"}, {Command: "revoke", Description: "revoke a bot's token"}, + {Command: "setlogin", Description: "configure Telegram Login"}, + {Command: "logininfo", Description: "show Telegram Login configuration"}, + {Command: "resetloginsecret", Description: "rotate an OIDC Client Secret"}, {Command: "cancel", Description: "cancel the current operation"}, {Command: "help", Description: "show help"}, }, diff --git a/internal/store/memory/telegram_login.go b/internal/store/memory/telegram_login.go new file mode 100644 index 00000000..3cbc2d8b --- /dev/null +++ b/internal/store/memory/telegram_login.go @@ -0,0 +1,721 @@ +package memory + +import ( + "context" + "sort" + "strconv" + "sync" + "time" + + "telesrv/internal/domain" +) + +type telegramLoginBotPermissionWriter interface { + AllowBotSendMessage(ctx context.Context, botUserID, userID int64, fromRequest bool) (bool, error) +} + +// TelegramLoginStore is the deterministic in-memory implementation used by +// application and RPC tests. A single mutex makes the same aggregate changes +// atomic; production uses PostgreSQL row locks and one transaction. +type TelegramLoginStore struct { + mu sync.RWMutex + + permissions telegramLoginBotPermissionWriter + nextURLID int64 + nextAppID int64 + nextRequestID int64 + nextCodeID int64 + + clientsByID map[string]domain.TelegramLoginClient + clientByBot map[int64]string + allowedURLs map[string]domain.TelegramLoginAllowedURL + nativeApps map[int64]domain.TelegramLoginNativeApp + requests map[int64]domain.TelegramLoginRequest + requestToken map[string]int64 + browserToken map[string]int64 + codes map[int64]domain.TelegramLoginAuthorizationCode + codeByHash map[string]int64 + codeByRequest map[int64]int64 + webAuths map[int64]domain.TelegramLoginWebAuthorization +} + +func NewTelegramLoginStore(permissions telegramLoginBotPermissionWriter) *TelegramLoginStore { + return &TelegramLoginStore{ + permissions: permissions, + clientsByID: make(map[string]domain.TelegramLoginClient), + clientByBot: make(map[int64]string), + allowedURLs: make(map[string]domain.TelegramLoginAllowedURL), + nativeApps: make(map[int64]domain.TelegramLoginNativeApp), + requests: make(map[int64]domain.TelegramLoginRequest), + requestToken: make(map[string]int64), + browserToken: make(map[string]int64), + codes: make(map[int64]domain.TelegramLoginAuthorizationCode), + codeByHash: make(map[string]int64), + codeByRequest: make(map[int64]int64), + webAuths: make(map[int64]domain.TelegramLoginWebAuthorization), + } +} + +func (s *TelegramLoginStore) CreateTelegramLoginClient(_ context.Context, client domain.TelegramLoginClient) (domain.TelegramLoginClient, error) { + if err := client.Validate(); err != nil { + return domain.TelegramLoginClient{}, err + } + s.mu.Lock() + defer s.mu.Unlock() + if _, exists := s.clientByBot[client.BotUserID]; exists { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginRequestConflict + } + if _, exists := s.clientsByID[client.ClientID]; exists { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginRequestConflict + } + s.clientsByID[client.ClientID] = client.Clone() + s.clientByBot[client.BotUserID] = client.ClientID + return client.Clone(), nil +} + +func (s *TelegramLoginStore) UpsertTelegramLoginClient(_ context.Context, client domain.TelegramLoginClient) (domain.TelegramLoginClient, error) { + if err := client.Validate(); err != nil { + return domain.TelegramLoginClient{}, err + } + s.mu.Lock() + defer s.mu.Unlock() + if existingID, exists := s.clientByBot[client.BotUserID]; exists && existingID != client.ClientID { + delete(s.clientsByID, existingID) + } + if existing, exists := s.clientsByID[client.ClientID]; exists && existing.BotUserID != client.BotUserID { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + s.clientsByID[client.ClientID] = client.Clone() + s.clientByBot[client.BotUserID] = client.ClientID + return client.Clone(), nil +} + +func (s *TelegramLoginStore) GetTelegramLoginClient(_ context.Context, clientID string) (domain.TelegramLoginClient, bool, error) { + s.mu.RLock() + client, ok := s.clientsByID[clientID] + s.mu.RUnlock() + return client.Clone(), ok, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginClientByBot(_ context.Context, botUserID int64) (domain.TelegramLoginClient, bool, error) { + s.mu.RLock() + clientID, ok := s.clientByBot[botUserID] + client := s.clientsByID[clientID] + s.mu.RUnlock() + return client.Clone(), ok, nil +} + +func (s *TelegramLoginStore) RotateTelegramLoginClientSecret(_ context.Context, botUserID, expectedVersion int64, secretHash []byte, now time.Time) (domain.TelegramLoginClient, error) { + if botUserID <= 0 || expectedVersion <= 0 || len(secretHash) != 32 { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + clientID, ok := s.clientByBot[botUserID] + if !ok { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + client := s.clientsByID[clientID] + if client.SecretVersion != expectedVersion { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginRequestConflict + } + client.SecretVersion++ + client.SecretHash = append([]byte(nil), secretHash...) + client.UpdatedAt = now + s.clientsByID[clientID] = client + return client.Clone(), nil +} + +func (s *TelegramLoginStore) SetTelegramLoginClientSigningAlgorithm(_ context.Context, botUserID int64, algorithm domain.TelegramLoginSigningAlgorithm, now time.Time) (domain.TelegramLoginClient, error) { + if botUserID <= 0 || !algorithm.Valid() { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + clientID, ok := s.clientByBot[botUserID] + if !ok { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + client := s.clientsByID[clientID] + client.SigningAlgorithm = algorithm + client.UpdatedAt = now + s.clientsByID[clientID] = client + return client.Clone(), nil +} + +func (s *TelegramLoginStore) SetTelegramLoginClientEnabled(_ context.Context, botUserID int64, enabled bool, now time.Time) error { + s.mu.Lock() + defer s.mu.Unlock() + clientID, ok := s.clientByBot[botUserID] + if !ok { + return domain.ErrTelegramLoginClientInvalid + } + client := s.clientsByID[clientID] + client.Enabled = enabled + client.UpdatedAt = now + s.clientsByID[clientID] = client + return nil +} + +func telegramLoginAllowedURLKey(botUserID int64, kind domain.TelegramLoginAllowedURLKind, value string) string { + return strconv.FormatInt(botUserID, 10) + "\x00" + string(kind) + "\x00" + value +} + +func (s *TelegramLoginStore) AddTelegramLoginAllowedURL(_ context.Context, allowed domain.TelegramLoginAllowedURL) (domain.TelegramLoginAllowedURL, error) { + if allowed.BotUserID <= 0 || allowed.NormalizedURL == "" || (allowed.Kind != domain.TelegramLoginAllowedWebOrigin && allowed.Kind != domain.TelegramLoginAllowedRedirectURI) { + return domain.TelegramLoginAllowedURL{}, domain.ErrTelegramLoginURLInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.clientByBot[allowed.BotUserID]; !ok { + return domain.TelegramLoginAllowedURL{}, domain.ErrTelegramLoginClientInvalid + } + key := telegramLoginAllowedURLKey(allowed.BotUserID, allowed.Kind, allowed.NormalizedURL) + if existing, ok := s.allowedURLs[key]; ok { + return existing, nil + } + s.nextURLID++ + allowed.ID = s.nextURLID + s.allowedURLs[key] = allowed + return allowed, nil +} + +func (s *TelegramLoginStore) DeleteTelegramLoginAllowedURL(_ context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, normalizedURL string) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + key := telegramLoginAllowedURLKey(botUserID, kind, normalizedURL) + if _, ok := s.allowedURLs[key]; !ok { + return false, nil + } + delete(s.allowedURLs, key) + return true, nil +} + +func (s *TelegramLoginStore) ListTelegramLoginAllowedURLs(_ context.Context, botUserID int64) ([]domain.TelegramLoginAllowedURL, error) { + s.mu.RLock() + out := make([]domain.TelegramLoginAllowedURL, 0) + for _, allowed := range s.allowedURLs { + if allowed.BotUserID == botUserID { + out = append(out, allowed) + } + } + s.mu.RUnlock() + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + return out, nil +} + +func (s *TelegramLoginStore) IsTelegramLoginURLAllowed(_ context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, normalizedURL string) (bool, error) { + s.mu.RLock() + _, ok := s.allowedURLs[telegramLoginAllowedURLKey(botUserID, kind, normalizedURL)] + s.mu.RUnlock() + return ok, nil +} + +func (s *TelegramLoginStore) UpsertTelegramLoginNativeApp(_ context.Context, app domain.TelegramLoginNativeApp) (domain.TelegramLoginNativeApp, error) { + if err := app.Validate(); err != nil { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginClientInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.clientByBot[app.BotUserID]; !ok { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginClientInvalid + } + if app.ID == 0 { + for id, existing := range s.nativeApps { + if existing.BotUserID == app.BotUserID && existing.Platform == app.Platform && existing.ApplicationID == app.ApplicationID && existing.VerificationID == app.VerificationID { + app.ID, app.CreatedAt = id, existing.CreatedAt + s.nativeApps[id] = app + return app, nil + } + if existing.BotUserID == app.BotUserID && existing.CallbackURI == app.CallbackURI { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginRequestConflict + } + } + count := 0 + for _, existing := range s.nativeApps { + if existing.BotUserID == app.BotUserID { + count++ + } + } + if count >= domain.MaxTelegramLoginNativeApps { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginRequestInvalid + } + s.nextAppID++ + app.ID = s.nextAppID + } else if existing, ok := s.nativeApps[app.ID]; ok && existing.BotUserID != app.BotUserID { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginClientInvalid + } + s.nativeApps[app.ID] = app + return app, nil +} + +func (s *TelegramLoginStore) DeleteTelegramLoginNativeApp(_ context.Context, botUserID, appID int64) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + app, ok := s.nativeApps[appID] + if !ok || app.BotUserID != botUserID { + return false, nil + } + delete(s.nativeApps, appID) + return true, nil +} + +func (s *TelegramLoginStore) ListTelegramLoginNativeApps(_ context.Context, botUserID int64) ([]domain.TelegramLoginNativeApp, error) { + s.mu.RLock() + out := make([]domain.TelegramLoginNativeApp, 0) + for _, app := range s.nativeApps { + if app.BotUserID == botUserID { + out = append(out, app) + } + } + s.mu.RUnlock() + sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) + if len(out) > domain.MaxTelegramLoginNativeApps { + out = out[:domain.MaxTelegramLoginNativeApps] + } + return out, nil +} + +func (s *TelegramLoginStore) CreateTelegramLoginRequest(_ context.Context, request domain.TelegramLoginRequest) (domain.TelegramLoginRequest, error) { + if err := request.Validate(); err != nil { + return domain.TelegramLoginRequest{}, err + } + s.mu.Lock() + defer s.mu.Unlock() + client, ok := s.clientsByID[request.ClientID] + if !ok || client.BotUserID != request.BotUserID || !client.Enabled || client.SigningAlgorithm != request.SigningAlgorithm { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginClientDisabled + } + if _, exists := s.requestToken[string(request.RequestTokenHash)]; exists { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestConflict + } + if _, exists := s.browserToken[string(request.BrowserTokenHash)]; exists { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestConflict + } + s.nextRequestID++ + request.ID = s.nextRequestID + s.requests[request.ID] = request.Clone() + s.requestToken[string(request.RequestTokenHash)] = request.ID + s.browserToken[string(request.BrowserTokenHash)] = request.ID + return request.Clone(), nil +} + +func (s *TelegramLoginStore) GetTelegramLoginRequest(_ context.Context, requestID int64) (domain.TelegramLoginRequest, bool, error) { + s.mu.RLock() + request, ok := s.requests[requestID] + s.mu.RUnlock() + return request.Clone(), ok, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginRequestByTokenHash(_ context.Context, tokenHash []byte) (domain.TelegramLoginRequest, bool, error) { + s.mu.RLock() + id, ok := s.requestToken[string(tokenHash)] + request := s.requests[id] + s.mu.RUnlock() + return request.Clone(), ok, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginRequestByBrowserTokenHash(_ context.Context, tokenHash []byte) (domain.TelegramLoginRequest, bool, error) { + s.mu.RLock() + id, ok := s.browserToken[string(tokenHash)] + request := s.requests[id] + s.mu.RUnlock() + return request.Clone(), ok, nil +} + +func grantedTelegramLoginScopes(request domain.TelegramLoginRequest, approval domain.TelegramLoginApproval) ([]domain.TelegramLoginScope, error) { + if approval.WriteAllowed && !request.Requests(domain.TelegramLoginScopeBotAccess) { + return nil, domain.ErrTelegramLoginScopeInvalid + } + if approval.PhoneShared && !request.Requests(domain.TelegramLoginScopePhone) { + return nil, domain.ErrTelegramLoginScopeInvalid + } + out := make([]domain.TelegramLoginScope, 0, len(request.Scopes)) + for _, scope := range request.Scopes { + if scope == domain.TelegramLoginScopePhone && !approval.PhoneShared { + continue + } + if scope == domain.TelegramLoginScopeBotAccess && !approval.WriteAllowed { + continue + } + out = append(out, scope) + } + return out, nil +} + +func (s *TelegramLoginStore) ApproveTelegramLoginRequest(ctx context.Context, approval domain.TelegramLoginApproval, webAuthorizationHash int64) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) { + if approval.RequestID <= 0 || approval.Identity.UserID <= 0 || webAuthorizationHash == 0 || approval.ApprovedAt.IsZero() { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + request, ok := s.requests[approval.RequestID] + if !ok { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + if request.Status != domain.TelegramLoginRequestPending { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestConflict + } + if !approval.ApprovedAt.Before(request.ExpiresAt) { + request.Status = domain.TelegramLoginRequestExpired + s.requests[request.ID] = request + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestExpired + } + client, clientExists := s.clientsByID[request.ClientID] + if !clientExists || !client.Enabled || client.BotUserID != request.BotUserID || client.SigningAlgorithm != request.SigningAlgorithm { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginClientDisabled + } + if request.ResponseType == "code" { + _, webAllowed := s.allowedURLs[telegramLoginAllowedURLKey(request.BotUserID, domain.TelegramLoginAllowedRedirectURI, request.RedirectURI)] + if !webAllowed && !(request.Source == domain.TelegramLoginRequestNative && s.nativeCallbackAllowedLocked(request.BotUserID, request.RedirectURI)) { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRedirectNotAllowed + } + } else if request.ResponseType == "post_message" || request.ResponseType == "legacy_url" { + if _, ok := s.allowedURLs[telegramLoginAllowedURLKey(request.BotUserID, domain.TelegramLoginAllowedWebOrigin, request.Origin)]; !ok { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + } else { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + if request.InAppOrigin != "" { + if _, ok := s.allowedURLs[telegramLoginAllowedURLKey(request.BotUserID, domain.TelegramLoginAllowedWebOrigin, request.InAppOrigin)]; !ok { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + } + if len(request.MatchCodes) > 0 && approval.MatchCode != request.MatchCode { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginMatchCodeInvalid + } + scopes, err := grantedTelegramLoginScopes(request, approval) + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, err + } + if _, exists := s.webAuths[webAuthorizationHash]; exists { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestConflict + } + identity, err := approval.Identity.Sanitized(request.Requests(domain.TelegramLoginScopeProfile), approval.PhoneShared) + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, err + } + activeAuthorizations := 0 + for _, authorization := range s.webAuths { + if authorization.UserID == identity.UserID && authorization.RevokedAt.IsZero() { + activeAuthorizations++ + } + } + if activeAuthorizations >= domain.MaxTelegramLoginWebAuthorizations { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginAuthorizationsTooMany + } + if approval.WriteAllowed && s.permissions != nil { + if _, err := s.permissions.AllowBotSendMessage(ctx, request.BotUserID, identity.UserID, true); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, err + } + } + request.Status = domain.TelegramLoginRequestApproved + request.AuthorizedUserID = identity.UserID + request.ProfileName = identity.Name + request.GivenName = identity.GivenName + request.FamilyName = identity.FamilyName + request.PreferredUsername = identity.PreferredUsername + request.Picture = identity.Picture + request.PhoneNumber = identity.PhoneNumber + request.WriteAllowed = approval.WriteAllowed + request.PhoneShared = approval.PhoneShared + request.ApprovedAt = approval.ApprovedAt + s.requests[request.ID] = request.Clone() + web := domain.TelegramLoginWebAuthorization{ + Hash: webAuthorizationHash, + RequestID: request.ID, + UserID: identity.UserID, + BotUserID: request.BotUserID, + Domain: request.Domain, + Browser: request.Browser, + Platform: request.Platform, + IP: request.IP, + Region: request.Region, + Scopes: scopes, + PhoneShared: approval.PhoneShared, + BotAccessGranted: approval.WriteAllowed, + CreatedAt: approval.ApprovedAt, + LastActiveAt: approval.ApprovedAt, + } + s.webAuths[web.Hash] = web.Clone() + return request.Clone(), web.Clone(), nil +} + +func (s *TelegramLoginStore) DeclineTelegramLoginRequest(_ context.Context, requestID, userID int64, now time.Time) (domain.TelegramLoginRequest, error) { + if requestID <= 0 || userID <= 0 || now.IsZero() { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + request, ok := s.requests[requestID] + if !ok { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestInvalid + } + if request.Status != domain.TelegramLoginRequestPending { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestConflict + } + if !now.Before(request.ExpiresAt) { + request.Status = domain.TelegramLoginRequestExpired + s.requests[request.ID] = request + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestExpired + } + request.Status = domain.TelegramLoginRequestDeclined + request.DeclinedAt = now + s.requests[request.ID] = request.Clone() + return request.Clone(), nil +} + +func (s *TelegramLoginStore) PutTelegramLoginAuthorizationCode(_ context.Context, code domain.TelegramLoginAuthorizationCode) (domain.TelegramLoginAuthorizationCode, error) { + if code.RequestID <= 0 || len(code.CodeHash) != 32 || len(code.SealedCode) < 32 || len(code.SealNonce) < 12 || code.SealKeyID == "" || code.IssuedAt.IsZero() || !code.ExpiresAt.After(code.IssuedAt) { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginCodeInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + request, ok := s.requests[code.RequestID] + if !ok || request.Status != domain.TelegramLoginRequestApproved { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestConflict + } + client, clientExists := s.clientsByID[request.ClientID] + if !clientExists || !client.Enabled || client.BotUserID != request.BotUserID || client.SigningAlgorithm != request.SigningAlgorithm { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginClientDisabled + } + switch request.ResponseType { + case "code": + _, allowed := s.allowedURLs[telegramLoginAllowedURLKey(request.BotUserID, domain.TelegramLoginAllowedRedirectURI, request.RedirectURI)] + if !allowed && !(request.Source == domain.TelegramLoginRequestNative && s.nativeCallbackAllowedLocked(request.BotUserID, request.RedirectURI)) { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRedirectNotAllowed + } + case "post_message": + if _, allowed := s.allowedURLs[telegramLoginAllowedURLKey(request.BotUserID, domain.TelegramLoginAllowedWebOrigin, request.Origin)]; !allowed { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginOriginNotAllowed + } + default: + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestConflict + } + web, active := s.webAuthByRequestLocked(request.ID) + if !active || !web.RevokedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestConflict + } + if id, exists := s.codeByRequest[code.RequestID]; exists { + return s.codes[id].Clone(), nil + } + if _, exists := s.codeByHash[string(code.CodeHash)]; exists { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestConflict + } + s.nextCodeID++ + code.ID = s.nextCodeID + s.codes[code.ID] = code.Clone() + s.codeByHash[string(code.CodeHash)] = code.ID + s.codeByRequest[code.RequestID] = code.ID + return code.Clone(), nil +} + +func (s *TelegramLoginStore) GetTelegramLoginAuthorizationCodeByRequest(_ context.Context, requestID int64) (domain.TelegramLoginAuthorizationCode, bool, error) { + s.mu.RLock() + id, ok := s.codeByRequest[requestID] + code := s.codes[id] + s.mu.RUnlock() + return code.Clone(), ok, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginAuthorizationCodeByHash(_ context.Context, codeHash []byte) (domain.TelegramLoginAuthorizationCode, bool, error) { + s.mu.RLock() + id, ok := s.codeByHash[string(codeHash)] + code := s.codes[id] + s.mu.RUnlock() + return code.Clone(), ok, nil +} + +func (s *TelegramLoginStore) ConsumeTelegramLoginAuthorizationCode(_ context.Context, exchange domain.TelegramLoginCodeExchange) (domain.TelegramLoginAuthorizationCode, domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) { + if len(exchange.CodeHash) != 32 || exchange.ClientID == "" || exchange.ClientSecretVersion <= 0 || exchange.RedirectURI == "" || exchange.CodeChallenge == "" || exchange.Now.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + id, ok := s.codeByHash[string(exchange.CodeHash)] + if !ok { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + code := s.codes[id] + if !code.ConsumedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeConsumed + } + if !exchange.Now.Before(code.ExpiresAt) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + request := s.requests[code.RequestID] + client, clientExists := s.clientsByID[exchange.ClientID] + if !clientExists || !client.Enabled || client.SecretVersion != exchange.ClientSecretVersion || request.ResponseType != "code" || request.ClientID != exchange.ClientID || request.RedirectURI != exchange.RedirectURI || request.CodeChallenge != exchange.CodeChallenge { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + _, webAllowed := s.allowedURLs[telegramLoginAllowedURLKey(request.BotUserID, domain.TelegramLoginAllowedRedirectURI, request.RedirectURI)] + if !webAllowed && !(request.Source == domain.TelegramLoginRequestNative && s.nativeCallbackAllowedLocked(request.BotUserID, request.RedirectURI)) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + web, exists := s.webAuthByRequestLocked(code.RequestID) + if request.Status != domain.TelegramLoginRequestApproved || !exists || !web.RevokedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + code.ConsumedAt = exchange.Now + web.LastActiveAt = exchange.Now + s.codes[id] = code.Clone() + s.webAuths[web.Hash] = web.Clone() + return code.Clone(), request.Clone(), web.Clone(), nil +} + +func (s *TelegramLoginStore) ConsumeTelegramLoginDirectToken(_ context.Context, tokenHash []byte, origin string, now time.Time) (domain.TelegramLoginAuthorizationCode, domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) { + if len(tokenHash) != 32 || origin == "" || now.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + id, ok := s.codeByHash[string(tokenHash)] + if !ok { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + code := s.codes[id] + if !code.ConsumedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeConsumed + } + if !now.Before(code.ExpiresAt) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + request := s.requests[code.RequestID] + client, clientExists := s.clientsByID[request.ClientID] + if !clientExists || !client.Enabled || client.BotUserID != request.BotUserID || + request.Status != domain.TelegramLoginRequestApproved || request.Source != domain.TelegramLoginRequestMiniApp || + request.ResponseType != "post_message" || request.Origin != origin || request.InAppOrigin != origin { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + if _, allowed := s.allowedURLs[telegramLoginAllowedURLKey(request.BotUserID, domain.TelegramLoginAllowedWebOrigin, origin)]; !allowed { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + web, exists := s.webAuthByRequestLocked(code.RequestID) + if !exists || !web.RevokedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + code.ConsumedAt = now + web.LastActiveAt = now + s.codes[id] = code.Clone() + s.webAuths[web.Hash] = web.Clone() + return code.Clone(), request.Clone(), web.Clone(), nil +} + +func (s *TelegramLoginStore) webAuthByRequestLocked(requestID int64) (domain.TelegramLoginWebAuthorization, bool) { + for _, web := range s.webAuths { + if web.RequestID == requestID { + return web, true + } + } + return domain.TelegramLoginWebAuthorization{}, false +} + +func (s *TelegramLoginStore) nativeCallbackAllowedLocked(botUserID int64, callbackURI string) bool { + for _, app := range s.nativeApps { + if app.BotUserID == botUserID && app.Enabled && app.CallbackURI == callbackURI { + return true + } + } + return false +} + +func (s *TelegramLoginStore) ListTelegramLoginWebAuthorizations(_ context.Context, userID int64) ([]domain.TelegramLoginWebAuthorization, error) { + s.mu.RLock() + out := make([]domain.TelegramLoginWebAuthorization, 0, min(len(s.webAuths), domain.MaxTelegramLoginWebAuthorizations)) + for _, web := range s.webAuths { + if web.UserID == userID && web.RevokedAt.IsZero() { + out = append(out, web.Clone()) + } + } + s.mu.RUnlock() + sort.Slice(out, func(i, j int) bool { + if out[i].LastActiveAt.Equal(out[j].LastActiveAt) { + return out[i].Hash > out[j].Hash + } + return out[i].LastActiveAt.After(out[j].LastActiveAt) + }) + if len(out) > domain.MaxTelegramLoginWebAuthorizations { + out = out[:domain.MaxTelegramLoginWebAuthorizations] + } + return out, nil +} + +func (s *TelegramLoginStore) RevokeTelegramLoginWebAuthorization(_ context.Context, userID, hash int64, now time.Time) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + web, ok := s.webAuths[hash] + if !ok || web.UserID != userID || !web.RevokedAt.IsZero() { + return false, nil + } + web.RevokedAt = now + s.webAuths[hash] = web + return true, nil +} + +func (s *TelegramLoginStore) RevokeAllTelegramLoginWebAuthorizations(_ context.Context, userID int64, now time.Time) (int64, error) { + s.mu.Lock() + defer s.mu.Unlock() + var count int64 + for hash, web := range s.webAuths { + if web.UserID == userID && web.RevokedAt.IsZero() { + web.RevokedAt = now + s.webAuths[hash] = web + count++ + } + } + return count, nil +} + +func (s *TelegramLoginStore) DeleteExpiredTelegramLoginArtifacts(_ context.Context, before time.Time, limit int) (int64, error) { + if limit <= 0 || limit > 1000 { + return 0, domain.ErrTelegramLoginRequestInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + var deleted int64 + for id, code := range s.codes { + if deleted >= int64(limit) { + break + } + if code.ExpiresAt.Before(before) || (!code.ConsumedAt.IsZero() && code.ConsumedAt.Before(before)) { + delete(s.codes, id) + delete(s.codeByHash, string(code.CodeHash)) + delete(s.codeByRequest, code.RequestID) + deleted++ + } + } + for id, request := range s.requests { + if deleted >= int64(limit) { + break + } + deleteRequest := (request.Status == domain.TelegramLoginRequestPending || request.Status == domain.TelegramLoginRequestDeclined || request.Status == domain.TelegramLoginRequestExpired) && request.ExpiresAt.Before(before) + var revokedWebHash int64 + if request.Status == domain.TelegramLoginRequestApproved && !request.ApprovedAt.IsZero() && request.ApprovedAt.Before(before) { + // Approved requests remain the immutable claim snapshot behind an active + // web authorization. They may only be collected after the grant itself + // was revoked and every exchange code has left the retention window. + for hash, web := range s.webAuths { + if web.RequestID == id && !web.RevokedAt.IsZero() && web.RevokedAt.Before(before) { + deleteRequest = true + revokedWebHash = hash + break + } + } + if _, hasCode := s.codeByRequest[id]; hasCode { + deleteRequest = false + } + } + if !deleteRequest { + continue + } + delete(s.requests, id) + delete(s.requestToken, string(request.RequestTokenHash)) + delete(s.browserToken, string(request.BrowserTokenHash)) + if revokedWebHash != 0 { + delete(s.webAuths, revokedWebHash) + } + deleted++ + } + return deleted, nil +} diff --git a/internal/store/memory/telegram_login_test.go b/internal/store/memory/telegram_login_test.go new file mode 100644 index 00000000..0e02d27d --- /dev/null +++ b/internal/store/memory/telegram_login_test.go @@ -0,0 +1,320 @@ +package memory + +import ( + "context" + "crypto/sha256" + "errors" + "sync" + "testing" + "time" + + "telesrv/internal/domain" +) + +type telegramLoginPermissionRecorder struct { + mu sync.Mutex + grants map[[2]int64]int +} + +func (r *telegramLoginPermissionRecorder) AllowBotSendMessage(_ context.Context, botUserID, userID int64, _ bool) (bool, error) { + r.mu.Lock() + defer r.mu.Unlock() + if r.grants == nil { + r.grants = make(map[[2]int64]int) + } + key := [2]int64{botUserID, userID} + created := r.grants[key] == 0 + r.grants[key]++ + return created, nil +} + +func telegramLoginTestHash(value string) []byte { + sum := sha256.Sum256([]byte(value)) + return sum[:] +} + +func seedTelegramLoginRequest(t *testing.T, s *TelegramLoginStore, now time.Time) domain.TelegramLoginRequest { + t.Helper() + ctx := context.Background() + client := domain.TelegramLoginClient{ + BotUserID: 9001, + ClientID: "9001", + SecretHash: telegramLoginTestHash("client-secret"), + SecretVersion: 1, + SigningAlgorithm: domain.TelegramLoginSigningRS256, + Enabled: true, + CreatedAt: now, + UpdatedAt: now, + } + if _, err := s.UpsertTelegramLoginClient(ctx, client); err != nil { + t.Fatalf("UpsertTelegramLoginClient: %v", err) + } + if _, err := s.AddTelegramLoginAllowedURL(ctx, domain.TelegramLoginAllowedURL{ + BotUserID: client.BotUserID, Kind: domain.TelegramLoginAllowedRedirectURI, + NormalizedURL: "https://rp.example/callback", CreatedAt: now, + }); err != nil { + t.Fatalf("AddTelegramLoginAllowedURL: %v", err) + } + request := domain.TelegramLoginRequest{ + RequestTokenHash: telegramLoginTestHash("request-token"), + BrowserTokenHash: telegramLoginTestHash("browser-token"), + BotUserID: client.BotUserID, + ClientID: client.ClientID, + SigningAlgorithm: client.SigningAlgorithm, + Source: domain.TelegramLoginRequestWeb, + ResponseType: "code", + RedirectURI: "https://rp.example/callback", + Origin: "https://rp.example", + Domain: "rp.example", + Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile, domain.TelegramLoginScopePhone, domain.TelegramLoginScopeBotAccess}, + State: "state", + Nonce: "nonce", + CodeChallenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + CodeChallengeMethod: "S256", + Browser: "Firefox", + Platform: "Windows", + IP: "192.0.2.10", + Region: "Test Region", + MatchCodes: []string{"🟢", "🔵", "🟠"}, + MatchCode: "🔵", + MatchCodesFirst: true, + Status: domain.TelegramLoginRequestPending, + CreatedAt: now, + ExpiresAt: now.Add(5 * time.Minute), + } + created, err := s.CreateTelegramLoginRequest(ctx, request) + if err != nil { + t.Fatalf("CreateTelegramLoginRequest: %v", err) + } + return created +} + +func approveTelegramLoginRequest(t *testing.T, s *TelegramLoginStore, request domain.TelegramLoginRequest, now time.Time) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization) { + t.Helper() + approved, web, err := s.ApproveTelegramLoginRequest(context.Background(), domain.TelegramLoginApproval{ + RequestID: request.ID, + Identity: domain.TelegramLoginIdentitySnapshot{ + UserID: 42, Name: "Alice Example", GivenName: "Alice", FamilyName: "Example", + PreferredUsername: "alice", Picture: "https://oauth.example/userpic/42", + }, + WriteAllowed: true, + PhoneShared: false, + MatchCode: request.MatchCode, + ApprovedAt: now, + }, 7000+request.ID) + if err != nil { + t.Fatalf("ApproveTelegramLoginRequest: %v", err) + } + return approved, web +} + +func TestTelegramLoginApproveIsAtomicAndShrinksConsent(t *testing.T) { + now := time.Unix(1_780_000_000, 0) + permissions := &telegramLoginPermissionRecorder{} + s := NewTelegramLoginStore(permissions) + request := seedTelegramLoginRequest(t, s, now) + approved, web := approveTelegramLoginRequest(t, s, request, now.Add(time.Second)) + if approved.Status != domain.TelegramLoginRequestApproved || approved.AuthorizedUserID != 42 { + t.Fatalf("approved request = %#v", approved) + } + if web.PhoneShared || web.BotAccessGranted != true { + t.Fatalf("web consent = %#v", web) + } + if len(web.Scopes) != 3 || web.Scopes[0] != domain.TelegramLoginScopeOpenID || web.Scopes[1] != domain.TelegramLoginScopeProfile || web.Scopes[2] != domain.TelegramLoginScopeBotAccess { + t.Fatalf("granted scopes = %#v", web.Scopes) + } + permissions.mu.Lock() + grants := permissions.grants[[2]int64{9001, 42}] + permissions.mu.Unlock() + if grants != 1 { + t.Fatalf("bot permission grants = %d, want 1", grants) + } +} + +func TestTelegramLoginAcceptDeclineRaceHasOneTerminalState(t *testing.T) { + now := time.Unix(1_780_000_000, 0) + s := NewTelegramLoginStore(nil) + request := seedTelegramLoginRequest(t, s, now) + start := make(chan struct{}) + errs := make(chan error, 2) + go func() { + <-start + _, _, err := s.ApproveTelegramLoginRequest(context.Background(), domain.TelegramLoginApproval{ + RequestID: request.ID, + Identity: domain.TelegramLoginIdentitySnapshot{UserID: 42, Name: "Alice", GivenName: "Alice"}, + MatchCode: request.MatchCode, ApprovedAt: now.Add(time.Second), + }, 7001) + errs <- err + }() + go func() { + <-start + _, err := s.DeclineTelegramLoginRequest(context.Background(), request.ID, 42, now.Add(time.Second)) + errs <- err + }() + close(start) + var success, conflict int + for range 2 { + err := <-errs + switch { + case err == nil: + success++ + case errors.Is(err, domain.ErrTelegramLoginRequestConflict): + conflict++ + default: + t.Fatalf("unexpected race error: %v", err) + } + } + if success != 1 || conflict != 1 { + t.Fatalf("success=%d conflict=%d, want 1/1", success, conflict) + } +} + +func TestTelegramLoginAuthorizationCodeSingleConsumeAndRevocation(t *testing.T) { + now := time.Unix(1_780_000_000, 0) + s := NewTelegramLoginStore(nil) + request := seedTelegramLoginRequest(t, s, now) + approveTelegramLoginRequest(t, s, request, now.Add(time.Second)) + code := domain.TelegramLoginAuthorizationCode{ + RequestID: request.ID, + CodeHash: telegramLoginTestHash("authorization-code"), + SealedCode: append(make([]byte, 32), 1), + SealNonce: make([]byte, 12), + SealKeyID: "test-key", + IssuedAt: now.Add(2 * time.Second), + ExpiresAt: now.Add(time.Minute), + } + if _, err := s.PutTelegramLoginAuthorizationCode(context.Background(), code); err != nil { + t.Fatalf("PutTelegramLoginAuthorizationCode: %v", err) + } + + start := make(chan struct{}) + errs := make(chan error, 8) + for range 8 { + go func() { + <-start + _, _, _, err := s.ConsumeTelegramLoginAuthorizationCode(context.Background(), domain.TelegramLoginCodeExchange{ + CodeHash: code.CodeHash, ClientID: request.ClientID, ClientSecretVersion: 1, + RedirectURI: request.RedirectURI, CodeChallenge: request.CodeChallenge, Now: now.Add(3 * time.Second), + }) + errs <- err + }() + } + close(start) + var success, consumed int + for range 8 { + err := <-errs + switch { + case err == nil: + success++ + case errors.Is(err, domain.ErrTelegramLoginCodeConsumed): + consumed++ + default: + t.Fatalf("unexpected consume error: %v", err) + } + } + if success != 1 || consumed != 7 { + t.Fatalf("success=%d consumed=%d, want 1/7", success, consumed) + } + + request2 := request.Clone() + request2.ID = 0 + request2.RequestTokenHash = telegramLoginTestHash("request-token-2") + request2.BrowserTokenHash = telegramLoginTestHash("browser-token-2") + request2, err := s.CreateTelegramLoginRequest(context.Background(), request2) + if err != nil { + t.Fatalf("Create second request: %v", err) + } + _, web2 := approveTelegramLoginRequest(t, s, request2, now.Add(4*time.Second)) + code2 := code.Clone() + code2.ID = 0 + code2.RequestID = request2.ID + code2.CodeHash = telegramLoginTestHash("authorization-code-2") + if _, err := s.PutTelegramLoginAuthorizationCode(context.Background(), code2); err != nil { + t.Fatalf("Put second code: %v", err) + } + if revoked, err := s.RevokeTelegramLoginWebAuthorization(context.Background(), web2.UserID, web2.Hash, now.Add(5*time.Second)); err != nil || !revoked { + t.Fatalf("RevokeTelegramLoginWebAuthorization = %v,%v", revoked, err) + } + if _, _, _, err := s.ConsumeTelegramLoginAuthorizationCode(context.Background(), domain.TelegramLoginCodeExchange{ + CodeHash: code2.CodeHash, ClientID: request2.ClientID, ClientSecretVersion: 1, + RedirectURI: request2.RedirectURI, CodeChallenge: request2.CodeChallenge, Now: now.Add(6 * time.Second), + }); !errors.Is(err, domain.ErrTelegramLoginCodeInvalid) { + t.Fatalf("consume after revoke error = %v, want code invalid", err) + } +} + +func TestTelegramLoginRetentionPreservesActiveAndReferencedApprovals(t *testing.T) { + ctx := context.Background() + now := time.Unix(1_780_000_000, 0) + before := now.Add(24 * time.Hour) + s := NewTelegramLoginStore(nil) + + active := seedTelegramLoginRequest(t, s, now) + _, activeWeb := approveTelegramLoginRequest(t, s, active, now.Add(time.Second)) + + revoked := active.Clone() + revoked.ID = 0 + revoked.RequestTokenHash = telegramLoginTestHash("retention-revoked-request") + revoked.BrowserTokenHash = telegramLoginTestHash("retention-revoked-browser") + revoked.Status = domain.TelegramLoginRequestPending + revoked.AuthorizedUserID = 0 + revoked.ProfileName, revoked.GivenName, revoked.FamilyName = "", "", "" + revoked.PreferredUsername, revoked.Picture, revoked.PhoneNumber = "", "", "" + revoked.WriteAllowed, revoked.PhoneShared = false, false + revoked.ApprovedAt = time.Time{} + revoked, err := s.CreateTelegramLoginRequest(ctx, revoked) + if err != nil { + t.Fatalf("create revoked request: %v", err) + } + _, revokedWeb := approveTelegramLoginRequest(t, s, revoked, now.Add(2*time.Second)) + if ok, err := s.RevokeTelegramLoginWebAuthorization(ctx, revokedWeb.UserID, revokedWeb.Hash, now.Add(3*time.Second)); err != nil || !ok { + t.Fatalf("revoke old authorization = %v,%v", ok, err) + } + + referenced := revoked.Clone() + referenced.ID = 0 + referenced.RequestTokenHash = telegramLoginTestHash("retention-referenced-request") + referenced.BrowserTokenHash = telegramLoginTestHash("retention-referenced-browser") + referenced.Status = domain.TelegramLoginRequestPending + referenced.AuthorizedUserID = 0 + referenced.ProfileName, referenced.GivenName, referenced.FamilyName = "", "", "" + referenced.PreferredUsername, referenced.Picture, referenced.PhoneNumber = "", "", "" + referenced.WriteAllowed, referenced.PhoneShared = false, false + referenced.ApprovedAt = time.Time{} + referenced, err = s.CreateTelegramLoginRequest(ctx, referenced) + if err != nil { + t.Fatalf("create referenced request: %v", err) + } + _, referencedWeb := approveTelegramLoginRequest(t, s, referenced, now.Add(4*time.Second)) + if _, err := s.PutTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginAuthorizationCode{ + RequestID: referenced.ID, CodeHash: telegramLoginTestHash("retention-live-code"), + SealedCode: append(make([]byte, 32), 1), SealNonce: make([]byte, 12), SealKeyID: "test-key", + IssuedAt: before.Add(time.Hour), ExpiresAt: before.Add(2 * time.Hour), + }); err != nil { + t.Fatalf("put retained code: %v", err) + } + if ok, err := s.RevokeTelegramLoginWebAuthorization(ctx, referencedWeb.UserID, referencedWeb.Hash, now.Add(5*time.Second)); err != nil || !ok { + t.Fatalf("revoke referenced authorization = %v,%v", ok, err) + } + + deleted, err := s.DeleteExpiredTelegramLoginArtifacts(ctx, before, 100) + if err != nil { + t.Fatalf("delete expired artifacts: %v", err) + } + if deleted != 1 { + t.Fatalf("deleted = %d, want revoked request only", deleted) + } + if _, found, _ := s.GetTelegramLoginRequest(ctx, active.ID); !found { + t.Fatal("active authorization request was deleted") + } + if _, found, _ := s.GetTelegramLoginRequest(ctx, referenced.ID); !found { + t.Fatal("request with retained code was deleted") + } + if _, found, _ := s.GetTelegramLoginRequest(ctx, revoked.ID); found { + t.Fatal("old revoked authorization request was retained") + } + listed, err := s.ListTelegramLoginWebAuthorizations(ctx, activeWeb.UserID) + if err != nil || len(listed) != 1 || listed[0].Hash != activeWeb.Hash { + t.Fatalf("active authorizations after retention = %#v, %v", listed, err) + } +} diff --git a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go index 8fe4e5c6..a66c4fb5 100644 --- a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go @@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) { if err != nil { t.Fatalf("migrate star gift lifecycle schema: %v", err) } - if status.Dirty || status.Empty || status.Version != 124 { - t.Fatalf("migration status = %+v, want clean version 124", status) + if status.Dirty || status.Empty || status.Version != 125 { + t.Fatalf("migration status = %+v, want clean version 125", status) } } diff --git a/internal/store/postgres/telegram_login.go b/internal/store/postgres/telegram_login.go new file mode 100644 index 00000000..58f4fd8a --- /dev/null +++ b/internal/store/postgres/telegram_login.go @@ -0,0 +1,1106 @@ +package postgres + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + + "telesrv/internal/domain" + "telesrv/internal/store/postgres/sqlcgen" +) + +type TelegramLoginStore struct { + db sqlcgen.DBTX +} + +func NewTelegramLoginStore(db sqlcgen.DBTX) *TelegramLoginStore { + return &TelegramLoginStore{db: db} +} + +type telegramLoginRowScanner interface { + Scan(dest ...any) error +} + +const telegramLoginClientColumns = `bot_user_id, client_id, client_secret_hash, secret_version, signing_algorithm, enabled, created_at, updated_at` + +func scanTelegramLoginClient(row telegramLoginRowScanner) (domain.TelegramLoginClient, error) { + var client domain.TelegramLoginClient + var algorithm string + if err := row.Scan(&client.BotUserID, &client.ClientID, &client.SecretHash, &client.SecretVersion, &algorithm, &client.Enabled, &client.CreatedAt, &client.UpdatedAt); err != nil { + return domain.TelegramLoginClient{}, err + } + client.SigningAlgorithm = domain.TelegramLoginSigningAlgorithm(algorithm) + return client, nil +} + +func (s *TelegramLoginStore) CreateTelegramLoginClient(ctx context.Context, client domain.TelegramLoginClient) (domain.TelegramLoginClient, error) { + if err := client.Validate(); err != nil { + return domain.TelegramLoginClient{}, err + } + createdAt := client.CreatedAt + if createdAt.IsZero() { + createdAt = time.Now().UTC() + } + updatedAt := client.UpdatedAt + if updatedAt.IsZero() { + updatedAt = createdAt + } + client, err := scanTelegramLoginClient(s.db.QueryRow(ctx, ` +INSERT INTO bot_login_clients ( + bot_user_id, client_id, client_secret_hash, secret_version, signing_algorithm, + enabled, created_at, updated_at +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) +RETURNING `+telegramLoginClientColumns, + client.BotUserID, client.ClientID, client.SecretHash, client.SecretVersion, + string(client.SigningAlgorithm), client.Enabled, createdAt, updatedAt)) + if err != nil { + return domain.TelegramLoginClient{}, mapTelegramLoginWriteError("create telegram login client", err) + } + return client, nil +} + +func (s *TelegramLoginStore) UpsertTelegramLoginClient(ctx context.Context, client domain.TelegramLoginClient) (domain.TelegramLoginClient, error) { + if err := client.Validate(); err != nil { + return domain.TelegramLoginClient{}, err + } + createdAt := client.CreatedAt + if createdAt.IsZero() { + createdAt = time.Now().UTC() + } + updatedAt := client.UpdatedAt + if updatedAt.IsZero() { + updatedAt = createdAt + } + row := s.db.QueryRow(ctx, ` +INSERT INTO bot_login_clients ( + bot_user_id, client_id, client_secret_hash, secret_version, signing_algorithm, + enabled, created_at, updated_at +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) +ON CONFLICT (bot_user_id) DO UPDATE SET + client_id = EXCLUDED.client_id, + client_secret_hash = EXCLUDED.client_secret_hash, + secret_version = EXCLUDED.secret_version, + signing_algorithm = EXCLUDED.signing_algorithm, + enabled = EXCLUDED.enabled, + updated_at = EXCLUDED.updated_at +RETURNING `+telegramLoginClientColumns, + client.BotUserID, client.ClientID, client.SecretHash, client.SecretVersion, + string(client.SigningAlgorithm), client.Enabled, createdAt, updatedAt) + out, err := scanTelegramLoginClient(row) + if err != nil { + return domain.TelegramLoginClient{}, mapTelegramLoginWriteError("upsert telegram login client", err) + } + return out, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginClient(ctx context.Context, clientID string) (domain.TelegramLoginClient, bool, error) { + client, err := scanTelegramLoginClient(s.db.QueryRow(ctx, `SELECT `+telegramLoginClientColumns+` FROM bot_login_clients WHERE client_id = $1`, clientID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginClient{}, false, nil + } + if err != nil { + return domain.TelegramLoginClient{}, false, fmt.Errorf("get telegram login client: %w", err) + } + return client, true, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginClientByBot(ctx context.Context, botUserID int64) (domain.TelegramLoginClient, bool, error) { + client, err := scanTelegramLoginClient(s.db.QueryRow(ctx, `SELECT `+telegramLoginClientColumns+` FROM bot_login_clients WHERE bot_user_id = $1`, botUserID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginClient{}, false, nil + } + if err != nil { + return domain.TelegramLoginClient{}, false, fmt.Errorf("get telegram login client by bot: %w", err) + } + return client, true, nil +} + +func (s *TelegramLoginStore) RotateTelegramLoginClientSecret(ctx context.Context, botUserID, expectedVersion int64, secretHash []byte, now time.Time) (domain.TelegramLoginClient, error) { + if botUserID <= 0 || expectedVersion <= 0 || len(secretHash) != 32 { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + client, err := scanTelegramLoginClient(s.db.QueryRow(ctx, ` +UPDATE bot_login_clients +SET client_secret_hash = $3, secret_version = secret_version + 1, updated_at = $4 +WHERE bot_user_id = $1 AND secret_version = $2 +RETURNING `+telegramLoginClientColumns, botUserID, expectedVersion, secretHash, now)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginRequestConflict + } + if err != nil { + return domain.TelegramLoginClient{}, fmt.Errorf("rotate telegram login client secret: %w", err) + } + return client, nil +} + +func (s *TelegramLoginStore) SetTelegramLoginClientSigningAlgorithm(ctx context.Context, botUserID int64, algorithm domain.TelegramLoginSigningAlgorithm, now time.Time) (domain.TelegramLoginClient, error) { + if botUserID <= 0 || !algorithm.Valid() { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + client, err := scanTelegramLoginClient(s.db.QueryRow(ctx, ` +UPDATE bot_login_clients SET signing_algorithm = $2, updated_at = $3 +WHERE bot_user_id = $1 +RETURNING `+telegramLoginClientColumns, botUserID, string(algorithm), now)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginClient{}, domain.ErrTelegramLoginClientInvalid + } + if err != nil { + return domain.TelegramLoginClient{}, fmt.Errorf("set telegram login client signing algorithm: %w", err) + } + return client, nil +} + +func (s *TelegramLoginStore) SetTelegramLoginClientEnabled(ctx context.Context, botUserID int64, enabled bool, now time.Time) error { + tag, err := s.db.Exec(ctx, `UPDATE bot_login_clients SET enabled = $2, updated_at = $3 WHERE bot_user_id = $1`, botUserID, enabled, now) + if err != nil { + return fmt.Errorf("set telegram login client enabled: %w", err) + } + if tag.RowsAffected() == 0 { + return domain.ErrTelegramLoginClientInvalid + } + return nil +} + +func scanTelegramLoginAllowedURL(row telegramLoginRowScanner) (domain.TelegramLoginAllowedURL, error) { + var allowed domain.TelegramLoginAllowedURL + var kind string + if err := row.Scan(&allowed.ID, &allowed.BotUserID, &kind, &allowed.NormalizedURL, &allowed.CreatedAt); err != nil { + return domain.TelegramLoginAllowedURL{}, err + } + allowed.Kind = domain.TelegramLoginAllowedURLKind(kind) + return allowed, nil +} + +func (s *TelegramLoginStore) AddTelegramLoginAllowedURL(ctx context.Context, allowed domain.TelegramLoginAllowedURL) (domain.TelegramLoginAllowedURL, error) { + if allowed.BotUserID <= 0 || allowed.NormalizedURL == "" || (allowed.Kind != domain.TelegramLoginAllowedWebOrigin && allowed.Kind != domain.TelegramLoginAllowedRedirectURI) { + return domain.TelegramLoginAllowedURL{}, domain.ErrTelegramLoginURLInvalid + } + createdAt := allowed.CreatedAt + if createdAt.IsZero() { + createdAt = time.Now().UTC() + } + row := s.db.QueryRow(ctx, ` +INSERT INTO bot_login_allowed_urls (bot_user_id, kind, normalized_url, created_at) +VALUES ($1,$2,$3,$4) +ON CONFLICT (bot_user_id, kind, normalized_url) DO UPDATE +SET normalized_url = EXCLUDED.normalized_url +RETURNING id, bot_user_id, kind, normalized_url, created_at`, + allowed.BotUserID, string(allowed.Kind), allowed.NormalizedURL, createdAt) + out, err := scanTelegramLoginAllowedURL(row) + if err != nil { + return domain.TelegramLoginAllowedURL{}, mapTelegramLoginWriteError("add telegram login allowed url", err) + } + return out, nil +} + +func (s *TelegramLoginStore) DeleteTelegramLoginAllowedURL(ctx context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, normalizedURL string) (bool, error) { + beginner, ok := s.db.(txBeginner) + if !ok { + return false, fmt.Errorf("delete telegram login allowed url: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return false, fmt.Errorf("delete telegram login allowed url: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + // Approval and code consumption lock this same client row before their + // final allow-list recheck. Configuration removal therefore serializes + // with those transitions across every server instance. + var exists bool + if err := tx.QueryRow(ctx, `SELECT true FROM bot_login_clients WHERE bot_user_id = $1 FOR UPDATE`, botUserID).Scan(&exists); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return false, domain.ErrTelegramLoginClientInvalid + } + return false, fmt.Errorf("delete telegram login allowed url: lock client: %w", err) + } + tag, err := tx.Exec(ctx, `DELETE FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = $2 AND normalized_url = $3`, botUserID, string(kind), normalizedURL) + if err != nil { + return false, fmt.Errorf("delete telegram login allowed url: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return false, fmt.Errorf("delete telegram login allowed url: commit: %w", err) + } + return tag.RowsAffected() > 0, nil +} + +func (s *TelegramLoginStore) ListTelegramLoginAllowedURLs(ctx context.Context, botUserID int64) ([]domain.TelegramLoginAllowedURL, error) { + rows, err := s.db.Query(ctx, `SELECT id, bot_user_id, kind, normalized_url, created_at FROM bot_login_allowed_urls WHERE bot_user_id = $1 ORDER BY kind, id`, botUserID) + if err != nil { + return nil, fmt.Errorf("list telegram login allowed urls: %w", err) + } + defer rows.Close() + out := make([]domain.TelegramLoginAllowedURL, 0) + for rows.Next() { + allowed, err := scanTelegramLoginAllowedURL(rows) + if err != nil { + return nil, fmt.Errorf("scan telegram login allowed url: %w", err) + } + out = append(out, allowed) + } + return out, rows.Err() +} + +func (s *TelegramLoginStore) IsTelegramLoginURLAllowed(ctx context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, normalizedURL string) (bool, error) { + var allowed bool + err := s.db.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = $2 AND normalized_url = $3)`, botUserID, string(kind), normalizedURL).Scan(&allowed) + if err != nil { + return false, fmt.Errorf("check telegram login allowed url: %w", err) + } + return allowed, nil +} + +func scanTelegramLoginNativeApp(row telegramLoginRowScanner) (domain.TelegramLoginNativeApp, error) { + var app domain.TelegramLoginNativeApp + var platform string + if err := row.Scan(&app.ID, &app.BotUserID, &platform, &app.ApplicationID, &app.VerificationID, &app.CallbackURI, &app.VerifiedDisplayName, &app.Enabled, &app.CreatedAt, &app.UpdatedAt); err != nil { + return domain.TelegramLoginNativeApp{}, err + } + app.Platform = domain.TelegramLoginNativePlatform(platform) + return app, nil +} + +const telegramLoginNativeAppColumns = `id, bot_user_id, platform, application_id, verification_id, callback_uri, verified_display_name, enabled, created_at, updated_at` + +func (s *TelegramLoginStore) UpsertTelegramLoginNativeApp(ctx context.Context, app domain.TelegramLoginNativeApp) (domain.TelegramLoginNativeApp, error) { + if err := app.Validate(); err != nil { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginClientInvalid + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.TelegramLoginNativeApp{}, fmt.Errorf("upsert telegram login native app: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.TelegramLoginNativeApp{}, fmt.Errorf("upsert telegram login native app: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + var exists bool + if err := tx.QueryRow(ctx, `SELECT true FROM bot_login_clients WHERE bot_user_id = $1 FOR UPDATE`, app.BotUserID).Scan(&exists); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginClientInvalid + } + return domain.TelegramLoginNativeApp{}, fmt.Errorf("upsert telegram login native app: lock client: %w", err) + } + if app.ID == 0 { + var current int + if err := tx.QueryRow(ctx, `SELECT count(*) FROM bot_login_native_apps WHERE bot_user_id = $1`, app.BotUserID).Scan(¤t); err != nil { + return domain.TelegramLoginNativeApp{}, fmt.Errorf("upsert telegram login native app: capacity: %w", err) + } + if current >= domain.MaxTelegramLoginNativeApps { + // A duplicate configuration remains an idempotent update at capacity. + var duplicate bool + if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bot_login_native_apps WHERE bot_user_id=$1 AND platform=$2 AND application_id=$3 AND verification_id=$4)`, app.BotUserID, string(app.Platform), app.ApplicationID, app.VerificationID).Scan(&duplicate); err != nil { + return domain.TelegramLoginNativeApp{}, fmt.Errorf("upsert telegram login native app: duplicate check: %w", err) + } + if !duplicate { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginRequestInvalid + } + } + } + var row telegramLoginRowScanner + if app.ID == 0 { + row = tx.QueryRow(ctx, ` +INSERT INTO bot_login_native_apps ( + bot_user_id, platform, application_id, verification_id, callback_uri, + verified_display_name, enabled, created_at, updated_at +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) +ON CONFLICT (bot_user_id, platform, application_id, verification_id) DO UPDATE SET + callback_uri = EXCLUDED.callback_uri, + verified_display_name = EXCLUDED.verified_display_name, + enabled = EXCLUDED.enabled, + updated_at = EXCLUDED.updated_at +RETURNING `+telegramLoginNativeAppColumns, + app.BotUserID, string(app.Platform), app.ApplicationID, app.VerificationID, + app.CallbackURI, app.VerifiedDisplayName, app.Enabled, app.CreatedAt, app.UpdatedAt) + } else { + row = tx.QueryRow(ctx, ` +UPDATE bot_login_native_apps SET + platform = $3, application_id = $4, verification_id = $5, + callback_uri = $6, verified_display_name = $7, enabled = $8, updated_at = $9 +WHERE id = $1 AND bot_user_id = $2 +RETURNING `+telegramLoginNativeAppColumns, + app.ID, app.BotUserID, string(app.Platform), app.ApplicationID, app.VerificationID, + app.CallbackURI, app.VerifiedDisplayName, app.Enabled, app.UpdatedAt) + } + out, err := scanTelegramLoginNativeApp(row) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginNativeApp{}, domain.ErrTelegramLoginClientInvalid + } + if err != nil { + return domain.TelegramLoginNativeApp{}, mapTelegramLoginWriteError("upsert telegram login native app", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginNativeApp{}, fmt.Errorf("upsert telegram login native app: commit: %w", err) + } + return out, nil +} + +func (s *TelegramLoginStore) DeleteTelegramLoginNativeApp(ctx context.Context, botUserID, appID int64) (bool, error) { + beginner, ok := s.db.(txBeginner) + if !ok { + return false, fmt.Errorf("delete telegram login native app: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return false, fmt.Errorf("delete telegram login native app: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + var exists bool + if err := tx.QueryRow(ctx, `SELECT true FROM bot_login_clients WHERE bot_user_id = $1 FOR UPDATE`, botUserID).Scan(&exists); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return false, domain.ErrTelegramLoginClientInvalid + } + return false, fmt.Errorf("delete telegram login native app: lock client: %w", err) + } + tag, err := tx.Exec(ctx, `DELETE FROM bot_login_native_apps WHERE id = $1 AND bot_user_id = $2`, appID, botUserID) + if err != nil { + return false, fmt.Errorf("delete telegram login native app: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return false, fmt.Errorf("delete telegram login native app: commit: %w", err) + } + return tag.RowsAffected() > 0, nil +} + +func (s *TelegramLoginStore) ListTelegramLoginNativeApps(ctx context.Context, botUserID int64) ([]domain.TelegramLoginNativeApp, error) { + rows, err := s.db.Query(ctx, `SELECT `+telegramLoginNativeAppColumns+` FROM bot_login_native_apps WHERE bot_user_id = $1 ORDER BY id LIMIT $2`, botUserID, domain.MaxTelegramLoginNativeApps) + if err != nil { + return nil, fmt.Errorf("list telegram login native apps: %w", err) + } + defer rows.Close() + out := make([]domain.TelegramLoginNativeApp, 0) + for rows.Next() { + app, err := scanTelegramLoginNativeApp(rows) + if err != nil { + return nil, fmt.Errorf("scan telegram login native app: %w", err) + } + out = append(out, app) + } + return out, rows.Err() +} + +const telegramLoginRequestColumns = ` +id, request_token_hash, browser_token_hash, bot_user_id, client_id, +signing_algorithm, source, response_type, redirect_uri, origin, domain, +requested_scopes, oauth_state, nonce, code_challenge, code_challenge_method, +browser, platform, ip, region, in_app_origin, is_app, verified_app_name, +match_codes, match_code, match_codes_first, user_id_hint, +peer_type, peer_id, message_id, button_id, +status, authorized_user_id, profile_name, given_name, family_name, +preferred_username, picture, phone_number, write_allowed, phone_shared, +created_at, expires_at, approved_at, declined_at` + +func telegramLoginScopeStrings(scopes []domain.TelegramLoginScope) []string { + out := make([]string, len(scopes)) + for i, scope := range scopes { + out[i] = string(scope) + } + return out +} + +func telegramLoginScopes(values []string) []domain.TelegramLoginScope { + out := make([]domain.TelegramLoginScope, len(values)) + for i, value := range values { + out[i] = domain.TelegramLoginScope(value) + } + return out +} + +func scanTelegramLoginRequest(row telegramLoginRowScanner) (domain.TelegramLoginRequest, error) { + var request domain.TelegramLoginRequest + var algorithm, source, status, peerType string + var scopes []string + var authorizedUserID sql.NullInt64 + var approvedAt, declinedAt sql.NullTime + var messageID, buttonID int32 + if err := row.Scan( + &request.ID, &request.RequestTokenHash, &request.BrowserTokenHash, &request.BotUserID, &request.ClientID, + &algorithm, &source, &request.ResponseType, &request.RedirectURI, &request.Origin, &request.Domain, + &scopes, &request.State, &request.Nonce, &request.CodeChallenge, &request.CodeChallengeMethod, + &request.Browser, &request.Platform, &request.IP, &request.Region, &request.InAppOrigin, &request.IsApp, &request.VerifiedAppName, + &request.MatchCodes, &request.MatchCode, &request.MatchCodesFirst, &request.UserIDHint, + &peerType, &request.PeerID, &messageID, &buttonID, + &status, &authorizedUserID, &request.ProfileName, &request.GivenName, &request.FamilyName, + &request.PreferredUsername, &request.Picture, &request.PhoneNumber, &request.WriteAllowed, &request.PhoneShared, + &request.CreatedAt, &request.ExpiresAt, &approvedAt, &declinedAt, + ); err != nil { + return domain.TelegramLoginRequest{}, err + } + request.SigningAlgorithm = domain.TelegramLoginSigningAlgorithm(algorithm) + request.Source = domain.TelegramLoginRequestSource(source) + request.Scopes = telegramLoginScopes(scopes) + request.PeerType = domain.PeerType(peerType) + request.MessageID = int(messageID) + request.ButtonID = int(buttonID) + request.Status = domain.TelegramLoginRequestState(status) + if authorizedUserID.Valid { + request.AuthorizedUserID = authorizedUserID.Int64 + } + if approvedAt.Valid { + request.ApprovedAt = approvedAt.Time + } + if declinedAt.Valid { + request.DeclinedAt = declinedAt.Time + } + return request, nil +} + +func (s *TelegramLoginStore) CreateTelegramLoginRequest(ctx context.Context, request domain.TelegramLoginRequest) (domain.TelegramLoginRequest, error) { + if err := request.Validate(); err != nil { + return domain.TelegramLoginRequest{}, err + } + row := s.db.QueryRow(ctx, ` +INSERT INTO telegram_login_requests ( + request_token_hash, browser_token_hash, bot_user_id, client_id, + signing_algorithm, source, response_type, redirect_uri, origin, domain, + requested_scopes, oauth_state, nonce, code_challenge, code_challenge_method, + browser, platform, ip, region, in_app_origin, is_app, verified_app_name, + match_codes, match_code, match_codes_first, user_id_hint, + peer_type, peer_id, message_id, button_id, status, created_at, expires_at +) +SELECT + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20, + $21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33 +FROM bot_login_clients c +WHERE c.bot_user_id = $3 AND c.client_id = $4 AND c.enabled + AND c.signing_algorithm = $5 +RETURNING `+telegramLoginRequestColumns, + request.RequestTokenHash, request.BrowserTokenHash, request.BotUserID, request.ClientID, + string(request.SigningAlgorithm), string(request.Source), request.ResponseType, request.RedirectURI, request.Origin, request.Domain, + telegramLoginScopeStrings(request.Scopes), request.State, request.Nonce, request.CodeChallenge, request.CodeChallengeMethod, + request.Browser, request.Platform, request.IP, request.Region, request.InAppOrigin, request.IsApp, request.VerifiedAppName, + request.MatchCodes, request.MatchCode, request.MatchCodesFirst, request.UserIDHint, + string(request.PeerType), request.PeerID, request.MessageID, request.ButtonID, string(request.Status), request.CreatedAt, request.ExpiresAt) + out, err := scanTelegramLoginRequest(row) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginClientDisabled + } + if err != nil { + return domain.TelegramLoginRequest{}, mapTelegramLoginWriteError("create telegram login request", err) + } + return out, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginRequest(ctx context.Context, requestID int64) (domain.TelegramLoginRequest, bool, error) { + request, err := scanTelegramLoginRequest(s.db.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE id = $1`, requestID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginRequest{}, false, nil + } + if err != nil { + return domain.TelegramLoginRequest{}, false, fmt.Errorf("get telegram login request: %w", err) + } + return request, true, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginRequestByTokenHash(ctx context.Context, tokenHash []byte) (domain.TelegramLoginRequest, bool, error) { + request, err := scanTelegramLoginRequest(s.db.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE request_token_hash = $1`, tokenHash)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginRequest{}, false, nil + } + if err != nil { + return domain.TelegramLoginRequest{}, false, fmt.Errorf("get telegram login request by token: %w", err) + } + return request, true, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginRequestByBrowserTokenHash(ctx context.Context, tokenHash []byte) (domain.TelegramLoginRequest, bool, error) { + request, err := scanTelegramLoginRequest(s.db.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE browser_token_hash = $1`, tokenHash)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginRequest{}, false, nil + } + if err != nil { + return domain.TelegramLoginRequest{}, false, fmt.Errorf("get telegram login request by browser token: %w", err) + } + return request, true, nil +} + +func (s *TelegramLoginStore) ApproveTelegramLoginRequest(ctx context.Context, approval domain.TelegramLoginApproval, webAuthorizationHash int64) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) { + if approval.RequestID <= 0 || approval.Identity.UserID <= 0 || webAuthorizationHash == 0 || approval.ApprovedAt.IsZero() { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + request, err := scanTelegramLoginRequest(tx.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE id = $1 FOR UPDATE`, approval.RequestID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: lock: %w", err) + } + if request.Status != domain.TelegramLoginRequestPending { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestConflict + } + if !approval.ApprovedAt.Before(request.ExpiresAt) { + if _, err := tx.Exec(ctx, `UPDATE telegram_login_requests SET status = 'expired' WHERE id = $1 AND status = 'pending'`, request.ID); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: mark expired: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: commit expiry: %w", err) + } + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestExpired + } + var clientEnabled, redirectAllowed, originAllowed bool + if err := tx.QueryRow(ctx, `SELECT enabled FROM bot_login_clients WHERE bot_user_id = $1 AND client_id = $2 AND signing_algorithm = $3 FOR UPDATE`, request.BotUserID, request.ClientID, string(request.SigningAlgorithm)).Scan(&clientEnabled); err != nil || !clientEnabled { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: client: %w", err) + } + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginClientDisabled + } + redirectAllowed = request.ResponseType != "code" + if request.ResponseType == "code" { + if err := tx.QueryRow(ctx, `SELECT + EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = 'redirect_uri' AND normalized_url = $2) + OR ($3 = 'native' AND EXISTS(SELECT 1 FROM bot_login_native_apps WHERE bot_user_id = $1 AND callback_uri = $2 AND enabled))`, request.BotUserID, request.RedirectURI, string(request.Source)).Scan(&redirectAllowed); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: redirect: %w", err) + } + if !redirectAllowed { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRedirectNotAllowed + } + } else if request.ResponseType == "post_message" || request.ResponseType == "legacy_url" { + if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = 'web_origin' AND normalized_url = $2)`, request.BotUserID, request.Origin).Scan(&originAllowed); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: post-message origin: %w", err) + } + if !originAllowed { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + } else { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginRequestInvalid + } + originAllowed = request.InAppOrigin == "" + if !originAllowed { + if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = 'web_origin' AND normalized_url = $2)`, request.BotUserID, request.InAppOrigin).Scan(&originAllowed); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: origin: %w", err) + } + } + if !originAllowed { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed + } + if len(request.MatchCodes) > 0 && approval.MatchCode != request.MatchCode { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginMatchCodeInvalid + } + scopes, err := grantedTelegramLoginScopesPG(request, approval) + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, err + } + identity, err := approval.Identity.Sanitized(request.Requests(domain.TelegramLoginScopeProfile), approval.PhoneShared) + if err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, err + } + // Serialize the per-user capacity check across requests and server + // instances. This prevents unbounded account.getWebAuthorizations payloads. + const telegramLoginAuthorizationLockNamespace int64 = 0x544c000000000000 + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1)`, identity.UserID^telegramLoginAuthorizationLockNamespace); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: authorization capacity lock: %w", err) + } + var activeAuthorizations int + if err := tx.QueryRow(ctx, `SELECT count(*) FROM web_authorizations WHERE user_id = $1 AND revoked_at IS NULL`, identity.UserID).Scan(&activeAuthorizations); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: authorization capacity: %w", err) + } + if activeAuthorizations >= domain.MaxTelegramLoginWebAuthorizations { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginAuthorizationsTooMany + } + if approval.WriteAllowed { + if _, err := tx.Exec(ctx, ` +INSERT INTO bot_user_permissions (bot_user_id, user_id, from_request) +VALUES ($1,$2,true) +ON CONFLICT (bot_user_id, user_id) DO UPDATE SET + from_request = true, updated_at = now()`, request.BotUserID, identity.UserID); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: grant bot access: %w", err) + } + } + if _, err := tx.Exec(ctx, ` +UPDATE telegram_login_requests SET + status = 'approved', authorized_user_id = $2, write_allowed = $3, + phone_shared = $4, approved_at = $5, profile_name = $6, given_name = $7, + family_name = $8, preferred_username = $9, picture = $10, phone_number = $11 +WHERE id = $1`, request.ID, identity.UserID, approval.WriteAllowed, approval.PhoneShared, approval.ApprovedAt, + identity.Name, identity.GivenName, identity.FamilyName, identity.PreferredUsername, identity.Picture, identity.PhoneNumber); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: update request: %w", err) + } + web := domain.TelegramLoginWebAuthorization{ + Hash: webAuthorizationHash, RequestID: request.ID, UserID: identity.UserID, BotUserID: request.BotUserID, + Domain: request.Domain, Browser: request.Browser, Platform: request.Platform, IP: request.IP, Region: request.Region, + Scopes: scopes, PhoneShared: approval.PhoneShared, BotAccessGranted: approval.WriteAllowed, + CreatedAt: approval.ApprovedAt, LastActiveAt: approval.ApprovedAt, + } + if _, err := tx.Exec(ctx, ` +INSERT INTO web_authorizations ( + hash, request_id, user_id, bot_user_id, domain, browser, platform, ip, region, + granted_scopes, phone_shared, bot_access_granted, created_at, last_active_at +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`, + web.Hash, web.RequestID, web.UserID, web.BotUserID, web.Domain, web.Browser, web.Platform, web.IP, web.Region, + telegramLoginScopeStrings(web.Scopes), web.PhoneShared, web.BotAccessGranted, web.CreatedAt, web.LastActiveAt); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, mapTelegramLoginWriteError("approve telegram login request: insert web authorization", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("approve telegram login request: commit: %w", err) + } + request.Status = domain.TelegramLoginRequestApproved + request.AuthorizedUserID = identity.UserID + request.ProfileName = identity.Name + request.GivenName = identity.GivenName + request.FamilyName = identity.FamilyName + request.PreferredUsername = identity.PreferredUsername + request.Picture = identity.Picture + request.PhoneNumber = identity.PhoneNumber + request.WriteAllowed = approval.WriteAllowed + request.PhoneShared = approval.PhoneShared + request.ApprovedAt = approval.ApprovedAt + return request, web, nil +} + +func grantedTelegramLoginScopesPG(request domain.TelegramLoginRequest, approval domain.TelegramLoginApproval) ([]domain.TelegramLoginScope, error) { + if approval.WriteAllowed && !request.Requests(domain.TelegramLoginScopeBotAccess) { + return nil, domain.ErrTelegramLoginScopeInvalid + } + if approval.PhoneShared && !request.Requests(domain.TelegramLoginScopePhone) { + return nil, domain.ErrTelegramLoginScopeInvalid + } + out := make([]domain.TelegramLoginScope, 0, len(request.Scopes)) + for _, scope := range request.Scopes { + if scope == domain.TelegramLoginScopePhone && !approval.PhoneShared { + continue + } + if scope == domain.TelegramLoginScopeBotAccess && !approval.WriteAllowed { + continue + } + out = append(out, scope) + } + return out, nil +} + +func (s *TelegramLoginStore) DeclineTelegramLoginRequest(ctx context.Context, requestID, userID int64, now time.Time) (domain.TelegramLoginRequest, error) { + if requestID <= 0 || userID <= 0 || now.IsZero() { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestInvalid + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.TelegramLoginRequest{}, fmt.Errorf("decline telegram login request: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.TelegramLoginRequest{}, fmt.Errorf("decline telegram login request: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + request, err := scanTelegramLoginRequest(tx.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE id = $1 FOR UPDATE`, requestID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestInvalid + } + if err != nil { + return domain.TelegramLoginRequest{}, fmt.Errorf("decline telegram login request: lock: %w", err) + } + if request.Status != domain.TelegramLoginRequestPending { + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestConflict + } + if !now.Before(request.ExpiresAt) { + if _, err := tx.Exec(ctx, `UPDATE telegram_login_requests SET status = 'expired' WHERE id = $1`, request.ID); err != nil { + return domain.TelegramLoginRequest{}, fmt.Errorf("decline telegram login request: mark expired: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginRequest{}, fmt.Errorf("decline telegram login request: commit expiry: %w", err) + } + return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginRequestExpired + } + if _, err := tx.Exec(ctx, `UPDATE telegram_login_requests SET status = 'declined', declined_at = $2 WHERE id = $1`, request.ID, now); err != nil { + return domain.TelegramLoginRequest{}, fmt.Errorf("decline telegram login request: update: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginRequest{}, fmt.Errorf("decline telegram login request: commit: %w", err) + } + request.Status = domain.TelegramLoginRequestDeclined + request.DeclinedAt = now + return request, nil +} + +const telegramLoginCodeColumns = `id, request_id, code_hash, sealed_code, seal_nonce, seal_key_id, issued_at, expires_at, consumed_at` + +func scanTelegramLoginAuthorizationCode(row telegramLoginRowScanner) (domain.TelegramLoginAuthorizationCode, error) { + var code domain.TelegramLoginAuthorizationCode + var consumedAt sql.NullTime + if err := row.Scan(&code.ID, &code.RequestID, &code.CodeHash, &code.SealedCode, &code.SealNonce, &code.SealKeyID, &code.IssuedAt, &code.ExpiresAt, &consumedAt); err != nil { + return domain.TelegramLoginAuthorizationCode{}, err + } + if consumedAt.Valid { + code.ConsumedAt = consumedAt.Time + } + return code, nil +} + +func (s *TelegramLoginStore) PutTelegramLoginAuthorizationCode(ctx context.Context, code domain.TelegramLoginAuthorizationCode) (domain.TelegramLoginAuthorizationCode, error) { + if code.RequestID <= 0 || len(code.CodeHash) != 32 || len(code.SealedCode) < 32 || len(code.SealNonce) < 12 || code.SealKeyID == "" || code.IssuedAt.IsZero() || !code.ExpiresAt.After(code.IssuedAt) { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginCodeInvalid + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + request, err := scanTelegramLoginRequest(tx.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE id = $1 FOR UPDATE`, code.RequestID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestInvalid + } else if err != nil { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: lock request: %w", err) + } + if request.Status != domain.TelegramLoginRequestApproved { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestConflict + } + var clientEnabled bool + if err := tx.QueryRow(ctx, `SELECT enabled FROM bot_login_clients WHERE bot_user_id = $1 AND client_id = $2 AND signing_algorithm = $3 FOR UPDATE`, + request.BotUserID, request.ClientID, string(request.SigningAlgorithm)).Scan(&clientEnabled); err != nil || !clientEnabled { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: client: %w", err) + } + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginClientDisabled + } + switch request.ResponseType { + case "code": + var allowed bool + if err := tx.QueryRow(ctx, `SELECT + EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = 'redirect_uri' AND normalized_url = $2) + OR ($3 = 'native' AND EXISTS(SELECT 1 FROM bot_login_native_apps WHERE bot_user_id = $1 AND callback_uri = $2 AND enabled))`, + request.BotUserID, request.RedirectURI, string(request.Source)).Scan(&allowed); err != nil { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: redirect: %w", err) + } + if !allowed { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRedirectNotAllowed + } + case "post_message": + var allowed bool + if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = 'web_origin' AND normalized_url = $2)`, + request.BotUserID, request.Origin).Scan(&allowed); err != nil { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: origin: %w", err) + } + if !allowed { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginOriginNotAllowed + } + default: + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestConflict + } + web, err := scanTelegramLoginWebAuthorization(tx.QueryRow(ctx, `SELECT `+telegramLoginWebAuthorizationColumns+` FROM web_authorizations WHERE request_id = $1 FOR UPDATE`, code.RequestID)) + if errors.Is(err, pgx.ErrNoRows) || !web.RevokedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.ErrTelegramLoginRequestConflict + } + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: web authorization: %w", err) + } + existing, err := scanTelegramLoginAuthorizationCode(tx.QueryRow(ctx, `SELECT `+telegramLoginCodeColumns+` FROM telegram_login_codes WHERE request_id = $1`, code.RequestID)) + if err == nil { + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: commit existing: %w", err) + } + return existing, nil + } + if !errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: read existing: %w", err) + } + created, err := scanTelegramLoginAuthorizationCode(tx.QueryRow(ctx, ` +INSERT INTO telegram_login_codes ( + request_id, code_hash, sealed_code, seal_nonce, seal_key_id, issued_at, expires_at +) VALUES ($1,$2,$3,$4,$5,$6,$7) +RETURNING `+telegramLoginCodeColumns, + code.RequestID, code.CodeHash, code.SealedCode, code.SealNonce, code.SealKeyID, code.IssuedAt, code.ExpiresAt)) + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, mapTelegramLoginWriteError("put telegram login code", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginAuthorizationCode{}, fmt.Errorf("put telegram login code: commit: %w", err) + } + return created, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginAuthorizationCodeByRequest(ctx context.Context, requestID int64) (domain.TelegramLoginAuthorizationCode, bool, error) { + code, err := scanTelegramLoginAuthorizationCode(s.db.QueryRow(ctx, `SELECT `+telegramLoginCodeColumns+` FROM telegram_login_codes WHERE request_id = $1`, requestID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, false, nil + } + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, false, fmt.Errorf("get telegram login code by request: %w", err) + } + return code, true, nil +} + +func (s *TelegramLoginStore) GetTelegramLoginAuthorizationCodeByHash(ctx context.Context, codeHash []byte) (domain.TelegramLoginAuthorizationCode, bool, error) { + code, err := scanTelegramLoginAuthorizationCode(s.db.QueryRow(ctx, `SELECT `+telegramLoginCodeColumns+` FROM telegram_login_codes WHERE code_hash = $1`, codeHash)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, false, nil + } + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, false, fmt.Errorf("get telegram login code by hash: %w", err) + } + return code, true, nil +} + +const telegramLoginWebAuthorizationColumns = `hash, request_id, user_id, bot_user_id, domain, browser, platform, ip, region, granted_scopes, phone_shared, bot_access_granted, created_at, last_active_at, revoked_at` + +func scanTelegramLoginWebAuthorization(row telegramLoginRowScanner) (domain.TelegramLoginWebAuthorization, error) { + var web domain.TelegramLoginWebAuthorization + var scopes []string + var revokedAt sql.NullTime + if err := row.Scan(&web.Hash, &web.RequestID, &web.UserID, &web.BotUserID, &web.Domain, &web.Browser, &web.Platform, &web.IP, &web.Region, &scopes, &web.PhoneShared, &web.BotAccessGranted, &web.CreatedAt, &web.LastActiveAt, &revokedAt); err != nil { + return domain.TelegramLoginWebAuthorization{}, err + } + web.Scopes = telegramLoginScopes(scopes) + if revokedAt.Valid { + web.RevokedAt = revokedAt.Time + } + return web, nil +} + +func (s *TelegramLoginStore) ConsumeTelegramLoginAuthorizationCode(ctx context.Context, exchange domain.TelegramLoginCodeExchange) (domain.TelegramLoginAuthorizationCode, domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) { + if len(exchange.CodeHash) != 32 || exchange.ClientID == "" || exchange.ClientSecretVersion <= 0 || exchange.RedirectURI == "" || exchange.CodeChallenge == "" || exchange.Now.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + code, err := scanTelegramLoginAuthorizationCode(tx.QueryRow(ctx, `SELECT `+telegramLoginCodeColumns+` FROM telegram_login_codes WHERE code_hash = $1 FOR UPDATE`, exchange.CodeHash)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: lock code: %w", err) + } + if !code.ConsumedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeConsumed + } + if !exchange.Now.Before(code.ExpiresAt) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + request, err := scanTelegramLoginRequest(tx.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE id = $1 FOR UPDATE`, code.RequestID)) + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: lock request: %w", err) + } + if request.Status != domain.TelegramLoginRequestApproved || request.ResponseType != "code" || request.ClientID != exchange.ClientID || request.RedirectURI != exchange.RedirectURI || request.CodeChallenge != exchange.CodeChallenge { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + var clientEnabled, redirectAllowed bool + var secretVersion int64 + if err := tx.QueryRow(ctx, `SELECT enabled, secret_version FROM bot_login_clients WHERE client_id = $1 FOR UPDATE`, exchange.ClientID).Scan(&clientEnabled, &secretVersion); err != nil || !clientEnabled || secretVersion != exchange.ClientSecretVersion { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: client: %w", err) + } + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + if err := tx.QueryRow(ctx, `SELECT + EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = 'redirect_uri' AND normalized_url = $2) + OR ($3 = 'native' AND EXISTS(SELECT 1 FROM bot_login_native_apps WHERE bot_user_id = $1 AND callback_uri = $2 AND enabled))`, request.BotUserID, exchange.RedirectURI, string(request.Source)).Scan(&redirectAllowed); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: redirect: %w", err) + } + if !redirectAllowed { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + web, err := scanTelegramLoginWebAuthorization(tx.QueryRow(ctx, `SELECT `+telegramLoginWebAuthorizationColumns+` FROM web_authorizations WHERE request_id = $1 FOR UPDATE`, request.ID)) + if err != nil || !web.RevokedAt.IsZero() { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: web authorization: %w", err) + } + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + if _, err := tx.Exec(ctx, `UPDATE telegram_login_codes SET consumed_at = $2 WHERE id = $1`, code.ID, exchange.Now); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: mark consumed: %w", err) + } + if _, err := tx.Exec(ctx, `UPDATE web_authorizations SET last_active_at = $2 WHERE hash = $1`, web.Hash, exchange.Now); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: touch web authorization: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login code: commit: %w", err) + } + code.ConsumedAt = exchange.Now + web.LastActiveAt = exchange.Now + return code, request, web, nil +} + +func (s *TelegramLoginStore) ConsumeTelegramLoginDirectToken(ctx context.Context, tokenHash []byte, origin string, now time.Time) (domain.TelegramLoginAuthorizationCode, domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) { + if len(tokenHash) != 32 || origin == "" || now.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + code, err := scanTelegramLoginAuthorizationCode(tx.QueryRow(ctx, `SELECT `+telegramLoginCodeColumns+` FROM telegram_login_codes WHERE code_hash = $1 FOR UPDATE`, tokenHash)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: lock token: %w", err) + } + if !code.ConsumedAt.IsZero() { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeConsumed + } + if !now.Before(code.ExpiresAt) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + request, err := scanTelegramLoginRequest(tx.QueryRow(ctx, `SELECT `+telegramLoginRequestColumns+` FROM telegram_login_requests WHERE id = $1 FOR UPDATE`, code.RequestID)) + if err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: lock request: %w", err) + } + if request.Status != domain.TelegramLoginRequestApproved || request.Source != domain.TelegramLoginRequestMiniApp || + request.ResponseType != "post_message" || request.Origin != origin || request.InAppOrigin != origin { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + var clientEnabled, originAllowed bool + if err := tx.QueryRow(ctx, `SELECT enabled FROM bot_login_clients WHERE bot_user_id = $1 AND client_id = $2 FOR UPDATE`, request.BotUserID, request.ClientID).Scan(&clientEnabled); err != nil || !clientEnabled { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: client: %w", err) + } + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bot_login_allowed_urls WHERE bot_user_id = $1 AND kind = 'web_origin' AND normalized_url = $2)`, request.BotUserID, origin).Scan(&originAllowed); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: origin: %w", err) + } + if !originAllowed { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + web, err := scanTelegramLoginWebAuthorization(tx.QueryRow(ctx, `SELECT `+telegramLoginWebAuthorizationColumns+` FROM web_authorizations WHERE request_id = $1 FOR UPDATE`, request.ID)) + if err != nil || !web.RevokedAt.IsZero() { + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: web authorization: %w", err) + } + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, domain.ErrTelegramLoginCodeInvalid + } + if _, err := tx.Exec(ctx, `UPDATE telegram_login_codes SET consumed_at = $2 WHERE id = $1`, code.ID, now); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: mark consumed: %w", err) + } + if _, err := tx.Exec(ctx, `UPDATE web_authorizations SET last_active_at = $2 WHERE hash = $1`, web.Hash, now); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: touch web authorization: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.TelegramLoginAuthorizationCode{}, domain.TelegramLoginRequest{}, domain.TelegramLoginWebAuthorization{}, fmt.Errorf("consume telegram login direct token: commit: %w", err) + } + code.ConsumedAt = now + web.LastActiveAt = now + return code, request, web, nil +} + +func (s *TelegramLoginStore) ListTelegramLoginWebAuthorizations(ctx context.Context, userID int64) ([]domain.TelegramLoginWebAuthorization, error) { + rows, err := s.db.Query(ctx, `SELECT `+telegramLoginWebAuthorizationColumns+` FROM web_authorizations WHERE user_id = $1 AND revoked_at IS NULL ORDER BY last_active_at DESC, hash DESC LIMIT $2`, userID, domain.MaxTelegramLoginWebAuthorizations) + if err != nil { + return nil, fmt.Errorf("list telegram login web authorizations: %w", err) + } + defer rows.Close() + out := make([]domain.TelegramLoginWebAuthorization, 0) + for rows.Next() { + web, err := scanTelegramLoginWebAuthorization(rows) + if err != nil { + return nil, fmt.Errorf("scan telegram login web authorization: %w", err) + } + out = append(out, web) + } + return out, rows.Err() +} + +func (s *TelegramLoginStore) RevokeTelegramLoginWebAuthorization(ctx context.Context, userID, hash int64, now time.Time) (bool, error) { + tag, err := s.db.Exec(ctx, `UPDATE web_authorizations SET revoked_at = $3 WHERE user_id = $1 AND hash = $2 AND revoked_at IS NULL`, userID, hash, now) + if err != nil { + return false, fmt.Errorf("revoke telegram login web authorization: %w", err) + } + return tag.RowsAffected() > 0, nil +} + +func (s *TelegramLoginStore) RevokeAllTelegramLoginWebAuthorizations(ctx context.Context, userID int64, now time.Time) (int64, error) { + tag, err := s.db.Exec(ctx, `UPDATE web_authorizations SET revoked_at = $2 WHERE user_id = $1 AND revoked_at IS NULL`, userID, now) + if err != nil { + return 0, fmt.Errorf("revoke all telegram login web authorizations: %w", err) + } + return tag.RowsAffected(), nil +} + +func (s *TelegramLoginStore) DeleteExpiredTelegramLoginArtifacts(ctx context.Context, before time.Time, limit int) (int64, error) { + if limit <= 0 || limit > 1000 { + return 0, domain.ErrTelegramLoginRequestInvalid + } + beginner, ok := s.db.(txBeginner) + if !ok { + return 0, fmt.Errorf("delete expired telegram login artifacts: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return 0, fmt.Errorf("delete expired telegram login artifacts: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + var deletedCodes int64 + if err := tx.QueryRow(ctx, ` +WITH doomed AS ( + SELECT id FROM telegram_login_codes + WHERE expires_at < $1 OR (consumed_at IS NOT NULL AND consumed_at < $1) + ORDER BY expires_at, id + LIMIT $2 + FOR UPDATE SKIP LOCKED +), deleted AS ( + DELETE FROM telegram_login_codes c USING doomed d WHERE c.id = d.id RETURNING c.id +) +SELECT count(*) FROM deleted`, before, limit).Scan(&deletedCodes); err != nil { + return 0, fmt.Errorf("delete expired telegram login codes: %w", err) + } + remaining := int64(limit) - deletedCodes + var deletedRequests int64 + if remaining > 0 { + if err := tx.QueryRow(ctx, ` +WITH doomed AS ( + SELECT r.id FROM telegram_login_requests r + WHERE (r.status IN ('pending','declined','expired') AND r.expires_at < $1) + OR (r.status = 'approved' AND r.approved_at < $1 + AND EXISTS ( + SELECT 1 FROM web_authorizations w + WHERE w.request_id = r.id AND w.revoked_at < $1 + ) + AND NOT EXISTS ( + SELECT 1 FROM telegram_login_codes c WHERE c.request_id = r.id + )) + ORDER BY COALESCE(r.approved_at, r.expires_at), r.id + LIMIT $2 + FOR UPDATE OF r SKIP LOCKED +), deleted AS ( + DELETE FROM telegram_login_requests r USING doomed d WHERE r.id = d.id RETURNING r.id +) +SELECT count(*) FROM deleted`, before, remaining).Scan(&deletedRequests); err != nil { + return 0, fmt.Errorf("delete expired telegram login requests: %w", err) + } + } + if err := tx.Commit(ctx); err != nil { + return 0, fmt.Errorf("delete expired telegram login artifacts: commit: %w", err) + } + return deletedCodes + deletedRequests, nil +} + +func mapTelegramLoginWriteError(op string, err error) error { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + switch pgErr.Code { + case pgerrcode.UniqueViolation: + return fmt.Errorf("%s: %w", op, domain.ErrTelegramLoginRequestConflict) + case pgerrcode.ForeignKeyViolation, pgerrcode.CheckViolation: + return fmt.Errorf("%s: %w", op, domain.ErrTelegramLoginRequestInvalid) + } + } + return fmt.Errorf("%s: %w", op, err) +} diff --git a/internal/store/postgres/telegram_login_integration_test.go b/internal/store/postgres/telegram_login_integration_test.go new file mode 100644 index 00000000..8a15b51b --- /dev/null +++ b/internal/store/postgres/telegram_login_integration_test.go @@ -0,0 +1,434 @@ +package postgres + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "strings" + "sync" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "telesrv/internal/domain" +) + +func telegramLoginPGHash(value string) []byte { + sum := sha256.Sum256([]byte(value)) + return sum[:] +} + +func TestTelegramLoginStorePostgresAtomicStateAndCodeConsumption(t *testing.T) { + pool := testPool(t) + ctx := context.Background() + now := time.Now().UTC().Truncate(time.Microsecond) + suffix := now.UnixNano() % 1_000_000_000 + + users := NewUserStore(pool) + bots := NewBotStore(pool) + owner, err := users.Create(ctx, domain.User{ + AccessHash: suffix + 101, + Phone: fmt.Sprintf("1777%09d", suffix), + FirstName: "OIDC Owner", + }) + if err != nil { + t.Fatalf("create oidc owner: %v", err) + } + bot, _, err := bots.CreateBotAccount(ctx, domain.User{ + AccessHash: suffix + 102, + FirstName: "OIDC Test Bot", + Username: fmt.Sprintf("oidc_%09d_bot", suffix), + }, domain.BotProfile{OwnerUserID: owner.ID, TokenSecret: "bot-secret"}) + if err != nil { + t.Fatalf("create oidc bot: %v", err) + } + t.Cleanup(func() { + _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id IN ($1,$2)", owner.ID, bot.ID) + }) + + store := NewTelegramLoginStore(pool) + client, err := store.UpsertTelegramLoginClient(ctx, domain.TelegramLoginClient{ + BotUserID: bot.ID, + ClientID: fmt.Sprintf("%d", bot.ID), + SecretHash: telegramLoginPGHash("client-secret"), + SecretVersion: 1, + SigningAlgorithm: domain.TelegramLoginSigningRS256, + Enabled: true, + CreatedAt: now, + UpdatedAt: now, + }) + if err != nil { + t.Fatalf("upsert oidc client: %v", err) + } + redirectURI := fmt.Sprintf("https://rp-%d.example/callback", suffix) + origin := fmt.Sprintf("https://rp-%d.example", suffix) + if _, err := store.AddTelegramLoginAllowedURL(ctx, domain.TelegramLoginAllowedURL{ + BotUserID: client.BotUserID, Kind: domain.TelegramLoginAllowedRedirectURI, + NormalizedURL: redirectURI, CreatedAt: now, + }); err != nil { + t.Fatalf("add redirect: %v", err) + } + if _, err := store.AddTelegramLoginAllowedURL(ctx, domain.TelegramLoginAllowedURL{ + BotUserID: client.BotUserID, Kind: domain.TelegramLoginAllowedWebOrigin, + NormalizedURL: origin, CreatedAt: now, + }); err != nil { + t.Fatalf("add web origin: %v", err) + } + + newRequest := func(label string) domain.TelegramLoginRequest { + request, err := store.CreateTelegramLoginRequest(ctx, domain.TelegramLoginRequest{ + RequestTokenHash: telegramLoginPGHash("request-" + label), + BrowserTokenHash: telegramLoginPGHash("browser-" + label), + BotUserID: bot.ID, + ClientID: client.ClientID, + SigningAlgorithm: client.SigningAlgorithm, + Source: domain.TelegramLoginRequestWeb, + ResponseType: "code", + RedirectURI: redirectURI, + Origin: origin, + Domain: fmt.Sprintf("rp-%d.example", suffix), + Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile, domain.TelegramLoginScopeBotAccess}, + State: "state", + Nonce: "nonce", + CodeChallenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + CodeChallengeMethod: "S256", + Browser: "Firefox", + Platform: "Windows", + IP: "192.0.2.10", + Region: "Test Region", + MatchCodes: []string{"🟢", "🔵", "🟠"}, + MatchCode: "🔵", + MatchCodesFirst: true, + Status: domain.TelegramLoginRequestPending, + CreatedAt: now, + ExpiresAt: now.Add(5 * time.Minute), + }) + if err != nil { + t.Fatalf("create request %s: %v", label, err) + } + return request + } + + request := newRequest(fmt.Sprintf("race-%d", suffix)) + start := make(chan struct{}) + errs := make(chan error, 2) + go func() { + <-start + _, _, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{ + RequestID: request.ID, + Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: owner.FirstName, GivenName: owner.FirstName}, + WriteAllowed: true, + MatchCode: request.MatchCode, ApprovedAt: now.Add(time.Second), + }, suffix+10_000) + errs <- err + }() + go func() { + <-start + _, err := store.DeclineTelegramLoginRequest(ctx, request.ID, owner.ID, now.Add(time.Second)) + errs <- err + }() + close(start) + var success, conflict int + for range 2 { + err := <-errs + switch { + case err == nil: + success++ + case errors.Is(err, domain.ErrTelegramLoginRequestConflict): + conflict++ + default: + t.Fatalf("accept/decline race error: %v", err) + } + } + if success != 1 || conflict != 1 { + t.Fatalf("accept/decline success=%d conflict=%d, want 1/1", success, conflict) + } + + codeRequest := newRequest(fmt.Sprintf("code-%d", suffix)) + _, web, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{ + RequestID: codeRequest.ID, + Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: owner.FirstName, GivenName: owner.FirstName}, + WriteAllowed: true, + MatchCode: codeRequest.MatchCode, ApprovedAt: now.Add(2 * time.Second), + }, suffix+20_000) + if err != nil { + t.Fatalf("approve code request: %v", err) + } + canSend, err := bots.CanBotSendMessage(ctx, bot.ID, owner.ID) + if err != nil || !canSend { + t.Fatalf("bot access after atomic approval = %v,%v", canSend, err) + } + code := domain.TelegramLoginAuthorizationCode{ + RequestID: codeRequest.ID, + CodeHash: telegramLoginPGHash(fmt.Sprintf("code-%d", suffix)), + SealedCode: append(make([]byte, 32), 1), + SealNonce: make([]byte, 12), + SealKeyID: "integration-key", + IssuedAt: now.Add(3 * time.Second), + ExpiresAt: now.Add(time.Minute), + } + if _, err := store.PutTelegramLoginAuthorizationCode(ctx, code); err != nil { + t.Fatalf("put code: %v", err) + } + exchange := domain.TelegramLoginCodeExchange{ + CodeHash: code.CodeHash, ClientID: client.ClientID, ClientSecretVersion: client.SecretVersion, + RedirectURI: codeRequest.RedirectURI, CodeChallenge: codeRequest.CodeChallenge, Now: now.Add(4 * time.Second), + } + start = make(chan struct{}) + errs = make(chan error, 8) + var wg sync.WaitGroup + for range 8 { + wg.Add(1) + go func() { + defer wg.Done() + <-start + _, _, _, err := store.ConsumeTelegramLoginAuthorizationCode(ctx, exchange) + errs <- err + }() + } + close(start) + wg.Wait() + close(errs) + success, conflict = 0, 0 + for err := range errs { + switch { + case err == nil: + success++ + case errors.Is(err, domain.ErrTelegramLoginCodeConsumed): + conflict++ + default: + t.Fatalf("code consume race error: %v", err) + } + } + if success != 1 || conflict != 7 { + t.Fatalf("code consume success=%d consumed=%d, want 1/7", success, conflict) + } + + miniRequest, err := store.CreateTelegramLoginRequest(ctx, domain.TelegramLoginRequest{ + RequestTokenHash: telegramLoginPGHash(fmt.Sprintf("mini-request-%d", suffix)), + BrowserTokenHash: telegramLoginPGHash(fmt.Sprintf("mini-browser-%d", suffix)), + BotUserID: bot.ID, ClientID: client.ClientID, SigningAlgorithm: client.SigningAlgorithm, + Source: domain.TelegramLoginRequestMiniApp, ResponseType: "post_message", + RedirectURI: origin + "/", Origin: origin, InAppOrigin: origin, Domain: fmt.Sprintf("rp-%d.example", suffix), + Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile}, + Browser: "Telegram Mini App", Platform: "Telegram Mini App", IP: "192.0.2.11", Region: "Test Region", + MatchCodes: []string{"🟢", "🔵", "🟠"}, MatchCode: "🔵", MatchCodesFirst: true, + Status: domain.TelegramLoginRequestPending, CreatedAt: now, ExpiresAt: now.Add(5 * time.Minute), + }) + if err != nil { + t.Fatalf("create mini-app request: %v", err) + } + if _, _, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{ + RequestID: miniRequest.ID, + Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: owner.FirstName, GivenName: owner.FirstName}, + MatchCode: miniRequest.MatchCode, ApprovedAt: now.Add(5 * time.Second), + }, suffix+25_000); err != nil { + t.Fatalf("approve mini-app request: %v", err) + } + directToken := domain.TelegramLoginAuthorizationCode{ + RequestID: miniRequest.ID, CodeHash: telegramLoginPGHash(fmt.Sprintf("mini-token-%d", suffix)), + SealedCode: append(make([]byte, 32), 1), SealNonce: make([]byte, 12), SealKeyID: "integration-key", + IssuedAt: now.Add(6 * time.Second), ExpiresAt: now.Add(time.Minute), + } + if _, err := store.PutTelegramLoginAuthorizationCode(ctx, directToken); err != nil { + t.Fatalf("put mini-app token: %v", err) + } + start = make(chan struct{}) + errs = make(chan error, 8) + for range 8 { + wg.Add(1) + go func() { + defer wg.Done() + <-start + _, _, _, err := store.ConsumeTelegramLoginDirectToken(ctx, directToken.CodeHash, origin, now.Add(7*time.Second)) + errs <- err + }() + } + close(start) + wg.Wait() + close(errs) + success, conflict = 0, 0 + for err := range errs { + switch { + case err == nil: + success++ + case errors.Is(err, domain.ErrTelegramLoginCodeConsumed): + conflict++ + default: + t.Fatalf("mini-app token consume race error: %v", err) + } + } + if success != 1 || conflict != 7 { + t.Fatalf("mini-app token consume success=%d consumed=%d, want 1/7", success, conflict) + } + + if revoked, err := store.RevokeTelegramLoginWebAuthorization(ctx, owner.ID, web.Hash, now.Add(5*time.Second)); err != nil || !revoked { + t.Fatalf("revoke web authorization = %v,%v", revoked, err) + } + if listed, err := store.ListTelegramLoginWebAuthorizations(ctx, owner.ID); err != nil { + t.Fatalf("list web authorizations: %v", err) + } else { + for _, got := range listed { + if got.Hash == web.Hash { + t.Fatalf("revoked web authorization still listed: %#v", got) + } + } + } + assertTelegramLoginConfigDeleteTakesClientLock(t, pool, client.BotUserID, func() (bool, error) { + return store.DeleteTelegramLoginAllowedURL(ctx, client.BotUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI) + }) +} + +func TestTelegramLoginStorePostgresNativeCallbackAndRetention(t *testing.T) { + pool := testPool(t) + ctx := context.Background() + now := time.Now().UTC().Truncate(time.Microsecond) + suffix := now.UnixNano() % 1_000_000_000 + + users := NewUserStore(pool) + bots := NewBotStore(pool) + owner, err := users.Create(ctx, domain.User{ + AccessHash: suffix + 301, Phone: fmt.Sprintf("1666%09d", suffix), FirstName: "Native Owner", + }) + if err != nil { + t.Fatal(err) + } + bot, _, err := bots.CreateBotAccount(ctx, domain.User{ + AccessHash: suffix + 302, FirstName: "Native Login Bot", Username: fmt.Sprintf("native_%09d_bot", suffix), + }, domain.BotProfile{OwnerUserID: owner.ID, TokenSecret: "native-bot-secret"}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id IN ($1,$2)", owner.ID, bot.ID) }) + + store := NewTelegramLoginStore(pool) + client, err := store.CreateTelegramLoginClient(ctx, domain.TelegramLoginClient{ + BotUserID: bot.ID, ClientID: fmt.Sprintf("%d", bot.ID), SecretHash: telegramLoginPGHash("native-secret"), + SecretVersion: 1, SigningAlgorithm: domain.TelegramLoginSigningRS256, Enabled: true, + CreatedAt: now, UpdatedAt: now, + }) + if err != nil { + t.Fatal(err) + } + const callbackURI = "bedolaga://telegram-login" + nativeApp, err := store.UpsertTelegramLoginNativeApp(ctx, domain.TelegramLoginNativeApp{ + BotUserID: bot.ID, Platform: domain.TelegramLoginNativeAndroid, ApplicationID: "dev.bedolaga.demo", + VerificationID: strings.Repeat("A", 64), CallbackURI: callbackURI, VerifiedDisplayName: "Bedolaga Demo", + Enabled: true, CreatedAt: now, UpdatedAt: now, + }) + if err != nil { + t.Fatal(err) + } + + createRequest := func(label string) domain.TelegramLoginRequest { + t.Helper() + request, err := store.CreateTelegramLoginRequest(ctx, domain.TelegramLoginRequest{ + RequestTokenHash: telegramLoginPGHash("native-request-" + label), BrowserTokenHash: telegramLoginPGHash("native-browser-" + label), + BotUserID: bot.ID, ClientID: client.ClientID, SigningAlgorithm: client.SigningAlgorithm, + Source: domain.TelegramLoginRequestNative, ResponseType: "code", RedirectURI: callbackURI, + Domain: "dev.bedolaga.demo", Scopes: []domain.TelegramLoginScope{domain.TelegramLoginScopeOpenID, domain.TelegramLoginScopeProfile}, + CodeChallenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", CodeChallengeMethod: "S256", + Browser: "TelegramLogin/Android", Platform: "Android", IP: "192.0.2.20", Region: "Test Region", + IsApp: true, VerifiedAppName: "Bedolaga Demo", MatchCodes: []string{}, Status: domain.TelegramLoginRequestPending, + CreatedAt: now, ExpiresAt: now.Add(5 * time.Minute), + }) + if err != nil { + t.Fatalf("create native request: %v", err) + } + return request + } + approve := func(request domain.TelegramLoginRequest, hash int64) domain.TelegramLoginWebAuthorization { + t.Helper() + _, web, err := store.ApproveTelegramLoginRequest(ctx, domain.TelegramLoginApproval{ + RequestID: request.ID, Identity: domain.TelegramLoginIdentitySnapshot{UserID: owner.ID, Name: "Native Owner", GivenName: "Native"}, + ApprovedAt: now.Add(time.Second), + }, hash) + if err != nil { + t.Fatalf("approve native request: %v", err) + } + return web + } + + revokedRequest := createRequest(fmt.Sprintf("revoked-%d", suffix)) + revokedWeb := approve(revokedRequest, suffix+30_000) + code := domain.TelegramLoginAuthorizationCode{ + RequestID: revokedRequest.ID, CodeHash: telegramLoginPGHash(fmt.Sprintf("native-code-%d", suffix)), + SealedCode: append(make([]byte, 32), 1), SealNonce: make([]byte, 12), SealKeyID: "integration-key", + IssuedAt: now.Add(2 * time.Second), ExpiresAt: now.Add(time.Minute), + } + if _, err := store.PutTelegramLoginAuthorizationCode(ctx, code); err != nil { + t.Fatal(err) + } + if _, _, _, err := store.ConsumeTelegramLoginAuthorizationCode(ctx, domain.TelegramLoginCodeExchange{ + CodeHash: code.CodeHash, ClientID: client.ClientID, ClientSecretVersion: client.SecretVersion, + RedirectURI: callbackURI, CodeChallenge: revokedRequest.CodeChallenge, Now: now.Add(3 * time.Second), + }); err != nil { + t.Fatalf("consume native code: %v", err) + } + if ok, err := store.RevokeTelegramLoginWebAuthorization(ctx, owner.ID, revokedWeb.Hash, now.Add(4*time.Second)); err != nil || !ok { + t.Fatalf("revoke native authorization = %v,%v", ok, err) + } + + activeRequest := createRequest(fmt.Sprintf("active-%d", suffix)) + activeWeb := approve(activeRequest, suffix+40_000) + deleted, err := store.DeleteExpiredTelegramLoginArtifacts(ctx, now.Add(2*time.Hour), 100) + if err != nil { + t.Fatal(err) + } + if deleted < 2 { + t.Fatalf("retention deleted=%d, want at least code and revoked request", deleted) + } + if _, found, _ := store.GetTelegramLoginRequest(ctx, revokedRequest.ID); found { + t.Fatal("revoked native request survived retention") + } + if _, found, _ := store.GetTelegramLoginRequest(ctx, activeRequest.ID); !found { + t.Fatal("active native request was deleted") + } + listed, err := store.ListTelegramLoginWebAuthorizations(ctx, owner.ID) + if err != nil || len(listed) != 1 || listed[0].Hash != activeWeb.Hash { + t.Fatalf("active authorization list=%#v err=%v", listed, err) + } + assertTelegramLoginConfigDeleteTakesClientLock(t, pool, client.BotUserID, func() (bool, error) { + return store.DeleteTelegramLoginNativeApp(ctx, client.BotUserID, nativeApp.ID) + }) +} + +func assertTelegramLoginConfigDeleteTakesClientLock(t *testing.T, pool *pgxpool.Pool, botUserID int64, remove func() (bool, error)) { + t.Helper() + ctx := context.Background() + tx, err := pool.Begin(ctx) + if err != nil { + t.Fatal(err) + } + defer func() { _ = tx.Rollback(ctx) }() + var lockedID int64 + if err := tx.QueryRow(ctx, `SELECT bot_user_id FROM bot_login_clients WHERE bot_user_id = $1 FOR UPDATE`, botUserID).Scan(&lockedID); err != nil { + t.Fatal(err) + } + result := make(chan error, 1) + go func() { + deleted, err := remove() + if err == nil && !deleted { + err = errors.New("configuration row was not deleted") + } + result <- err + }() + select { + case err := <-result: + t.Fatalf("configuration delete bypassed client serialization lock: %v", err) + case <-time.After(150 * time.Millisecond): + } + if err := tx.Commit(ctx); err != nil { + t.Fatal(err) + } + select { + case err := <-result: + if err != nil { + t.Fatal(err) + } + case <-time.After(5 * time.Second): + t.Fatal("configuration delete remained blocked after client lock committed") + } +} diff --git a/internal/store/telegram_login.go b/internal/store/telegram_login.go new file mode 100644 index 00000000..7eb8fa48 --- /dev/null +++ b/internal/store/telegram_login.go @@ -0,0 +1,49 @@ +package store + +import ( + "context" + "time" + + "telesrv/internal/domain" +) + +// TelegramLoginStore is the single durable boundary shared by the HTTP OIDC +// adapter, MTProto URL-authorization RPCs and account Web-authorization RPCs. +// Implementations must use compare-and-set transitions and must not treat an +// in-memory cache as the source of truth. +type TelegramLoginStore interface { + CreateTelegramLoginClient(ctx context.Context, client domain.TelegramLoginClient) (domain.TelegramLoginClient, error) + UpsertTelegramLoginClient(ctx context.Context, client domain.TelegramLoginClient) (domain.TelegramLoginClient, error) + GetTelegramLoginClient(ctx context.Context, clientID string) (domain.TelegramLoginClient, bool, error) + GetTelegramLoginClientByBot(ctx context.Context, botUserID int64) (domain.TelegramLoginClient, bool, error) + RotateTelegramLoginClientSecret(ctx context.Context, botUserID, expectedVersion int64, secretHash []byte, now time.Time) (domain.TelegramLoginClient, error) + SetTelegramLoginClientSigningAlgorithm(ctx context.Context, botUserID int64, algorithm domain.TelegramLoginSigningAlgorithm, now time.Time) (domain.TelegramLoginClient, error) + SetTelegramLoginClientEnabled(ctx context.Context, botUserID int64, enabled bool, now time.Time) error + + AddTelegramLoginAllowedURL(ctx context.Context, allowed domain.TelegramLoginAllowedURL) (domain.TelegramLoginAllowedURL, error) + DeleteTelegramLoginAllowedURL(ctx context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, normalizedURL string) (bool, error) + ListTelegramLoginAllowedURLs(ctx context.Context, botUserID int64) ([]domain.TelegramLoginAllowedURL, error) + IsTelegramLoginURLAllowed(ctx context.Context, botUserID int64, kind domain.TelegramLoginAllowedURLKind, normalizedURL string) (bool, error) + + UpsertTelegramLoginNativeApp(ctx context.Context, app domain.TelegramLoginNativeApp) (domain.TelegramLoginNativeApp, error) + DeleteTelegramLoginNativeApp(ctx context.Context, botUserID, appID int64) (bool, error) + ListTelegramLoginNativeApps(ctx context.Context, botUserID int64) ([]domain.TelegramLoginNativeApp, error) + + CreateTelegramLoginRequest(ctx context.Context, request domain.TelegramLoginRequest) (domain.TelegramLoginRequest, error) + GetTelegramLoginRequest(ctx context.Context, requestID int64) (domain.TelegramLoginRequest, bool, error) + GetTelegramLoginRequestByTokenHash(ctx context.Context, tokenHash []byte) (domain.TelegramLoginRequest, bool, error) + GetTelegramLoginRequestByBrowserTokenHash(ctx context.Context, tokenHash []byte) (domain.TelegramLoginRequest, bool, error) + ApproveTelegramLoginRequest(ctx context.Context, approval domain.TelegramLoginApproval, webAuthorizationHash int64) (domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) + DeclineTelegramLoginRequest(ctx context.Context, requestID, userID int64, now time.Time) (domain.TelegramLoginRequest, error) + + PutTelegramLoginAuthorizationCode(ctx context.Context, code domain.TelegramLoginAuthorizationCode) (domain.TelegramLoginAuthorizationCode, error) + GetTelegramLoginAuthorizationCodeByRequest(ctx context.Context, requestID int64) (domain.TelegramLoginAuthorizationCode, bool, error) + GetTelegramLoginAuthorizationCodeByHash(ctx context.Context, codeHash []byte) (domain.TelegramLoginAuthorizationCode, bool, error) + ConsumeTelegramLoginAuthorizationCode(ctx context.Context, exchange domain.TelegramLoginCodeExchange) (domain.TelegramLoginAuthorizationCode, domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) + ConsumeTelegramLoginDirectToken(ctx context.Context, tokenHash []byte, origin string, now time.Time) (domain.TelegramLoginAuthorizationCode, domain.TelegramLoginRequest, domain.TelegramLoginWebAuthorization, error) + + ListTelegramLoginWebAuthorizations(ctx context.Context, userID int64) ([]domain.TelegramLoginWebAuthorization, error) + RevokeTelegramLoginWebAuthorization(ctx context.Context, userID, hash int64, now time.Time) (bool, error) + RevokeAllTelegramLoginWebAuthorizations(ctx context.Context, userID int64, now time.Time) (int64, error) + DeleteExpiredTelegramLoginArtifacts(ctx context.Context, before time.Time, limit int) (int64, error) +} diff --git a/internal/telegramloginhttp/handler.go b/internal/telegramloginhttp/handler.go new file mode 100644 index 00000000..9130a5e8 --- /dev/null +++ b/internal/telegramloginhttp/handler.go @@ -0,0 +1,748 @@ +// Package telegramloginhttp is the public HTTP adapter for Telegram Login and +// OpenID Connect. It contains protocol parsing/rendering only; durable state +// and authorization transitions remain in app/telegramlogin and its store. +package telegramloginhttp + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "html/template" + "io" + "mime" + "net" + "net/http" + "net/netip" + "net/url" + "strings" + "time" + "unicode/utf8" + + "go.uber.org/zap" + + loginapp "telesrv/internal/app/telegramlogin" + "telesrv/internal/domain" +) + +const ( + maxAuthorizationQueryBytes = 16 << 10 + maxTokenFormBytes = 16 << 10 + maxStatusFormBytes = 4 << 10 +) + +type Config struct { + Service *loginapp.Service + Tokens *loginapp.IDTokenIssuer + Limiter RateLimiter + AppName string + Logger *zap.Logger + TrustedProxyCIDRs []string + AllowLoopbackHTTP bool +} + +type Handler struct { + service *loginapp.Service + tokens *loginapp.IDTokenIssuer + appName string + logger *zap.Logger + limiter RateLimiter + trustedProxies []netip.Prefix + allowLoopbackHTTP bool + mux *http.ServeMux +} + +type RateLimiter interface { + Allow(ctx context.Context, key string, limit int, window time.Duration) (allowed bool, retryAfterSeconds int, err error) +} + +func NewHandler(cfg Config) (*Handler, error) { + if cfg.Service == nil || cfg.Tokens == nil || cfg.Tokens.Issuer() == "" { + return nil, errors.New("telegram login HTTP dependencies are incomplete") + } + if strings.TrimSpace(cfg.AppName) == "" { + cfg.AppName = "Telesrv" + } + if cfg.Logger == nil { + cfg.Logger = zap.NewNop() + } + trustedProxies := make([]netip.Prefix, 0, len(cfg.TrustedProxyCIDRs)) + for _, raw := range cfg.TrustedProxyCIDRs { + prefix, err := netip.ParsePrefix(strings.TrimSpace(raw)) + if err != nil { + return nil, fmt.Errorf("telegram login trusted proxy CIDR %q: %w", raw, err) + } + trustedProxies = append(trustedProxies, prefix.Masked()) + } + h := &Handler{service: cfg.Service, tokens: cfg.Tokens, appName: strings.TrimSpace(cfg.AppName), logger: cfg.Logger, limiter: cfg.Limiter, trustedProxies: trustedProxies, allowLoopbackHTTP: cfg.AllowLoopbackHTTP} + mux := http.NewServeMux() + mux.HandleFunc("GET /.well-known/openid-configuration", h.discovery) + mux.HandleFunc("GET /.well-known/jwks.json", h.jwks) + mux.HandleFunc("GET /auth", h.authorize) + mux.HandleFunc("GET /crossapp", h.crossApp) + mux.HandleFunc("GET /inapp", h.inApp) + mux.HandleFunc("POST /auth/status", h.authorizationStatus) + mux.HandleFunc("POST /token", h.token) + mux.HandleFunc("GET /telegram-login.js", h.loginJavaScript) + mux.HandleFunc("GET /js/telegram-login.js", h.loginJavaScript) + h.mux = mux + return h, nil +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("Cross-Origin-Opener-Policy", "same-origin-allow-popups") + h.mux.ServeHTTP(w, r) +} + +type discoveryDocument struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + JWKSURI string `json:"jwks_uri"` + ScopesSupported []string `json:"scopes_supported"` + ResponseTypesSupported []string `json:"response_types_supported"` + ResponseModesSupported []string `json:"response_modes_supported"` + GrantTypesSupported []string `json:"grant_types_supported"` + SubjectTypesSupported []string `json:"subject_types_supported"` + IDTokenSigningAlgorithms []string `json:"id_token_signing_alg_values_supported"` + TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"` + ClaimsSupported []string `json:"claims_supported"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` +} + +func (h *Handler) discovery(w http.ResponseWriter, _ *http.Request) { + issuer := h.tokens.Issuer() + writeJSON(w, http.StatusOK, discoveryDocument{ + Issuer: issuer, AuthorizationEndpoint: issuer + "/auth", TokenEndpoint: issuer + "/token", + JWKSURI: issuer + "/.well-known/jwks.json", + ScopesSupported: []string{"openid", "profile", "phone", "telegram:bot_access"}, + ResponseTypesSupported: []string{"code"}, ResponseModesSupported: []string{"query"}, + GrantTypesSupported: []string{"authorization_code"}, SubjectTypesSupported: []string{"public"}, + IDTokenSigningAlgorithms: h.tokens.SupportedAlgorithms(), + TokenEndpointAuthMethods: []string{"client_secret_basic", "client_secret_post", "none"}, + ClaimsSupported: []string{ + "iss", "aud", "sub", "iat", "exp", "nonce", "id", "name", "given_name", + "family_name", "preferred_username", "picture", "phone_number", "phone_number_verified", + }, + CodeChallengeMethodsSupported: []string{"S256"}, + }) +} + +func (h *Handler) jwks(w http.ResponseWriter, r *http.Request) { + body, etag, err := h.tokens.JWKS() + if err != nil { + h.logger.Error("telegram_login_jwks_failed", zap.Error(err)) + writeOAuthError(w, http.StatusInternalServerError, "server_error", "key service unavailable") + return + } + w.Header().Set("Cache-Control", "public, max-age=300, must-revalidate") + w.Header().Set("ETag", etag) + if r.Header.Get("If-None-Match") == etag { + w.WriteHeader(http.StatusNotModified) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(body) +} + +type authorizationPageData struct { + AppName string + DeepLink template.URL + MatchCode string + BrowserToken string + ExpiresAt string + CSPNonce string + ResponseType string + TargetOrigin string +} + +var authorizationPage = template.Must(template.New("telegram-login").Parse(` + +Log in with {{.AppName}} +

Log in with {{.AppName}}

Open the {{.AppName}} app and approve this request. Keep this page open.

Open {{.AppName}}{{if .MatchCode}}

When prompted, select this emoji in {{.AppName}}:

{{end}}

Waiting for approval…

This request expires at {{.ExpiresAt}}.

+`)) + +func (h *Handler) authorize(w http.ResponseWriter, r *http.Request) { + clientIP := h.requestIP(r) + if !h.allow(w, r, "authorize", clientIP, 30, time.Minute) { + return + } + if len(r.URL.RawQuery) > maxAuthorizationQueryBytes { + h.authorizationRequestError(w) + return + } + query := r.URL.Query() + nativePlatform, nativeMarkerOK := nativeSDKPlatform(query) + if !nativeMarkerOK { + h.authorizationRequestError(w) + return + } + values := make(map[string]string, 9) + for _, key := range []string{"client_id", "redirect_uri", "response_type", "scope", "state", "nonce", "code_challenge", "code_challenge_method", "origin"} { + value, ok := singleValue(query, key) + if !ok { + h.authorizationRequestError(w) + return + } + values[key] = value + } + platformLabel := "Web" + if nativePlatform == domain.TelegramLoginNativeIOS { + platformLabel = "iOS" + } else if nativePlatform == domain.TelegramLoginNativeAndroid { + platformLabel = "Android" + } + source := domain.TelegramLoginRequestWeb + if values["response_type"] == "post_message" { + source = domain.TelegramLoginRequestJavaScript + } + created, err := h.service.CreateAuthorization(r.Context(), loginapp.CreateAuthorizationParams{ + ClientID: values["client_id"], RedirectURI: values["redirect_uri"], ResponseType: values["response_type"], + Scope: values["scope"], State: values["state"], Nonce: values["nonce"], + CodeChallenge: values["code_challenge"], CodeChallengeMethod: values["code_challenge_method"], + Origin: values["origin"], + Source: source, NativePlatform: nativePlatform, + Browser: boundedHeader(r.UserAgent(), "Unknown browser", 255), + Platform: platformLabel, IP: clientIP, Region: "Unknown region", IncludeMatchCodes: true, MatchCodesFirst: true, + }) + if err != nil { + h.logger.Info("telegram_login_authorize_rejected", zap.String("error", errorClass(err))) + h.authorizationError(w, r, values, err) + return + } + cspNonce, err := loginapp.GenerateOpaqueToken() + if err != nil { + h.logger.Error("telegram_login_csp_nonce_failed", zap.Error(err)) + h.authorizationRequestError(w) + return + } + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-"+cspNonce+"'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + if err := authorizationPage.Execute(w, authorizationPageData{ + AppName: h.appName, DeepLink: template.URL(created.DeepLink), MatchCode: created.Request.MatchCode, BrowserToken: created.BrowserToken, + ExpiresAt: created.Request.ExpiresAt.UTC().Format(time.RFC3339), CSPNonce: cspNonce, + ResponseType: created.Request.ResponseType, TargetOrigin: created.Request.Origin, + }); err != nil { + h.logger.Warn("telegram_login_authorize_render_failed", zap.Error(err)) + } +} + +func (h *Handler) crossApp(w http.ResponseWriter, r *http.Request) { + clientIP := h.requestIP(r) + if !h.allow(w, r, "crossapp", clientIP, 30, time.Minute) { + return + } + if len(r.URL.RawQuery) > maxAuthorizationQueryBytes { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid native login request") + return + } + query := r.URL.Query() + platform, ok := nativeSDKPlatform(query) + if !ok || !platform.Valid() { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "exactly one native SDK marker is required") + return + } + values := make(map[string]string, 8) + for _, key := range []string{"client_id", "redirect_uri", "response_type", "scope", "state", "nonce", "code_challenge", "code_challenge_method"} { + value, unique := singleValue(query, key) + if !unique { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "duplicate parameter") + return + } + values[key] = value + } + platformLabel := "iOS" + if platform == domain.TelegramLoginNativeAndroid { + platformLabel = "Android" + } + created, err := h.service.CreateAuthorization(r.Context(), loginapp.CreateAuthorizationParams{ + ClientID: values["client_id"], RedirectURI: values["redirect_uri"], ResponseType: values["response_type"], + Scope: values["scope"], State: values["state"], Nonce: values["nonce"], + CodeChallenge: values["code_challenge"], CodeChallengeMethod: values["code_challenge_method"], + Source: domain.TelegramLoginRequestNative, NativePlatform: platform, + Browser: boundedHeader(r.UserAgent(), "Native SDK", 255), Platform: platformLabel, + IP: clientIP, Region: "Unknown region", IncludeMatchCodes: true, MatchCodesFirst: true, + }) + if err != nil { + h.logger.Info("telegram_login_crossapp_rejected", zap.String("error", errorClass(err))) + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "native login request is not registered") + return + } + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + writeJSON(w, http.StatusOK, map[string]string{"url": created.DeepLink}) +} + +func (h *Handler) inApp(w http.ResponseWriter, r *http.Request) { + clientIP := h.requestIP(r) + if !h.allow(w, r, "inapp", clientIP, 60, time.Minute) { + return + } + if len(r.URL.RawQuery) > maxAuthorizationQueryBytes { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid in-app login request") + return + } + query := r.URL.Query() + if rawCode, exists := query["code"]; exists { + if len(query) != 1 || len(rawCode) != 1 || rawCode[0] == "" { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid in-app token") + return + } + issued, err := h.service.ExchangeInAppTokenAndIssue(r.Context(), rawCode[0], r.Header.Get("Origin"), h.tokens) + if err != nil { + h.logger.Info("telegram_login_inapp_exchange_rejected", zap.String("error", errorClass(err))) + writeOAuthError(w, http.StatusBadRequest, "invalid_grant", "in-app token is invalid or expired") + return + } + setInAppCORS(w, issued.Request.InAppOrigin) + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + writeJSON(w, http.StatusOK, map[string]string{"result": issued.IDToken}) + return + } + if len(query) != 4 { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid in-app login request") + return + } + values := make(map[string]string, 4) + for _, key := range []string{"client_id", "scope", "origin", "response_type"} { + value, unique := singleValue(query, key) + if !unique || value == "" { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid in-app login request") + return + } + values[key] = value + } + if values["response_type"] != "id_token" { + writeOAuthError(w, http.StatusBadRequest, "unsupported_response_type", "only id_token is supported") + return + } + origin, err := loginapp.NormalizeWebOrigin(values["origin"], h.allowLoopbackHTTP) + if err != nil || r.Header.Get("Origin") != origin { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "in-app origin is invalid") + return + } + setInAppCORS(w, origin) + created, err := h.service.CreateAuthorization(r.Context(), loginapp.CreateAuthorizationParams{ + ClientID: values["client_id"], RedirectURI: origin + "/", ResponseType: "post_message", + Scope: values["scope"], Origin: origin, InAppOrigin: origin, + Source: domain.TelegramLoginRequestMiniApp, + Browser: boundedHeader(r.UserAgent(), "Telegram Mini App", 255), Platform: "Telegram Mini App", + IP: clientIP, Region: "Unknown region", IncludeMatchCodes: true, MatchCodesFirst: true, + }) + if err != nil { + h.logger.Info("telegram_login_inapp_rejected", zap.String("error", errorClass(err))) + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "in-app login request is not registered") + return + } + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + writeJSON(w, http.StatusOK, map[string]string{"url": created.DeepLink}) +} + +func setInAppCORS(w http.ResponseWriter, origin string) { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Vary", "Origin") +} + +func nativeSDKPlatform(values url.Values) (domain.TelegramLoginNativePlatform, bool) { + ios, iosUnique := singleValue(values, "ios_sdk") + android, androidUnique := singleValue(values, "android_sdk") + if !iosUnique || !androidUnique || (ios != "" && ios != "1") || (android != "" && android != "1") || (ios != "" && android != "") { + return "", false + } + if ios == "1" { + return domain.TelegramLoginNativeIOS, true + } + if android == "1" { + return domain.TelegramLoginNativeAndroid, true + } + return "", true +} + +var authorizationErrorPage = template.Must(template.New("telegram-login-error").Parse(`Telegram Login

Login could not be started.

`)) + +func (h *Handler) authorizationError(w http.ResponseWriter, r *http.Request, values map[string]string, cause error) { + target, safe, err := h.service.ResolveAuthorizationErrorTarget(r.Context(), values["client_id"], values["response_type"], values["redirect_uri"], values["origin"]) + if err != nil || !safe { + if err != nil { + h.logger.Error("telegram_login_authorize_error_target_failed", zap.String("error", errorClass(err))) + } + h.authorizationRequestError(w) + return + } + code := "invalid_request" + switch { + case errors.Is(cause, domain.ErrTelegramLoginScopeInvalid): + code = "invalid_scope" + case values["response_type"] != "code" && values["response_type"] != "post_message": + code = "unsupported_response_type" + default: + // Known validation failures use invalid_request. Unknown failures are + // reported as server_error without exposing their details. + if strings.HasPrefix(errorClass(cause), "internal_") { + code = "server_error" + } + } + state := values["state"] + if len(state) > 2048 { + state = "" + } + if target.ResponseType == "code" { + redirectURL, err := loginapp.AppendAuthorizationError(target.RedirectURI, code, state) + if err != nil { + h.authorizationRequestError(w) + return + } + w.Header().Set("Cache-Control", "no-store") + http.Redirect(w, r, redirectURL, http.StatusFound) + return + } + nonce, err := loginapp.GenerateOpaqueToken() + if err != nil { + h.authorizationRequestError(w) + return + } + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'nonce-"+nonce+"'; frame-ancestors 'none'; base-uri 'none'") + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + _ = authorizationErrorPage.Execute(w, struct { + Nonce string + Error string + Origin string + }{Nonce: nonce, Error: code, Origin: target.Origin}) +} + +func (h *Handler) authorizationRequestError(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + http.Error(w, "Invalid Telegram Login request.", http.StatusBadRequest) +} + +func (h *Handler) authorizationStatus(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + if !h.allow(w, r, "status", h.requestIP(r), 300, time.Minute) { + return + } + form, ok := parseBoundedForm(w, r, maxStatusFormBytes) + if !ok { + return + } + browserToken, unique := singleValue(form, "browser_token") + if !unique || browserToken == "" { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid browser request") + return + } + request, err := h.service.RequestByBrowserToken(r.Context(), browserToken) + if err != nil { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid browser request") + return + } + switch request.Status { + case domain.TelegramLoginRequestPending: + writeJSON(w, http.StatusOK, map[string]string{"status": "pending"}) + case domain.TelegramLoginRequestApproved: + if request.ResponseType == "post_message" { + finalized, err := h.service.FinalizeDirectByBrowserToken(r.Context(), browserToken, h.tokens) + if err != nil { + h.logger.Error("telegram_login_direct_finalize_failed", zap.Int64("request_id", request.ID), zap.Error(err)) + writeOAuthError(w, http.StatusInternalServerError, "server_error", "authorization could not be finalized") + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "approved", "id_token": finalized.IDToken}) + return + } + finalized, err := h.service.FinalizeByBrowserToken(r.Context(), browserToken) + if err != nil { + h.logger.Error("telegram_login_finalize_failed", zap.Int64("request_id", request.ID), zap.Error(err)) + writeOAuthError(w, http.StatusInternalServerError, "server_error", "authorization could not be finalized") + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "approved", "redirect_url": finalized.RedirectURL}) + case domain.TelegramLoginRequestDeclined, domain.TelegramLoginRequestExpired: + errorCode := "access_denied" + status := "declined" + if request.Status == domain.TelegramLoginRequestExpired { + errorCode, status = "temporarily_unavailable", "expired" + } + if request.ResponseType == "post_message" { + writeJSON(w, http.StatusOK, map[string]string{"status": status, "error": errorCode}) + return + } + redirectURL, err := loginapp.AppendAuthorizationError(request.RedirectURI, errorCode, request.State) + if err != nil { + writeOAuthError(w, http.StatusInternalServerError, "server_error", "authorization could not be finalized") + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": status, "redirect_url": redirectURL}) + default: + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid browser request") + } +} + +func (h *Handler) token(w http.ResponseWriter, r *http.Request) { + if !h.allow(w, r, "token-ip", h.requestIP(r), 60, time.Minute) { + return + } + form, ok := parseBoundedForm(w, r, maxTokenFormBytes) + if !ok { + return + } + authorizationHeaders := r.Header.Values("Authorization") + var clientID, clientSecret string + var publicNativeClient bool + switch len(authorizationHeaders) { + case 0: + var unique bool + clientID, unique = requiredSingleValue(form, "client_id", 64) + if !unique { + h.invalidClient(w) + return + } + clientSecret, unique = optionalSingleValue(form, "client_secret") + if !unique || len(clientSecret) > 1024 { + h.invalidClient(w) + return + } + publicNativeClient = clientSecret == "" + case 1: + var ok bool + clientID, clientSecret, ok = r.BasicAuth() + if !ok || clientID == "" || clientSecret == "" || len(clientID) > 64 || len(clientSecret) > 1024 { + h.invalidClient(w) + return + } + if _, supplied := form["client_secret"]; supplied { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "multiple client authentication methods are not allowed") + return + } + default: + h.invalidClient(w) + return + } + if !h.allow(w, r, "token-client", clientID, 60, time.Minute) { + return + } + formClientID, unique := optionalSingleValue(form, "client_id") + if !unique || (formClientID != "" && formClientID != clientID) { + h.invalidClient(w) + return + } + grantType, okGrant := requiredSingleValue(form, "grant_type", 64) + if !okGrant { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "grant_type is required") + return + } + if grantType != "authorization_code" { + writeOAuthError(w, http.StatusBadRequest, "unsupported_grant_type", "only authorization_code is supported") + return + } + code, okCode := requiredSingleValue(form, "code", 1024) + redirectURI, okRedirect := requiredSingleValue(form, "redirect_uri", 4096) + codeVerifier, okVerifier := requiredSingleValue(form, "code_verifier", 128) + if !okCode || !okRedirect || !okVerifier { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "code, redirect_uri and code_verifier are required") + return + } + // Generate every response artifact before consuming the one-time code. A + // transient entropy failure must leave the grant retryable. + accessToken, err := loginapp.GenerateOpaqueToken() + if err != nil { + h.logger.Error("telegram_login_access_token_failed", zap.Error(err)) + writeOAuthError(w, http.StatusInternalServerError, "server_error", "token service unavailable") + return + } + issued, err := h.service.ExchangeAuthorizationCodeAndIssue(r.Context(), loginapp.ExchangeAuthorizationCodeParams{ + Code: code, ClientID: clientID, ClientSecret: clientSecret, RedirectURI: redirectURI, CodeVerifier: codeVerifier, + PublicNativeClient: publicNativeClient, + }, h.tokens) + if err != nil { + switch { + case errors.Is(err, domain.ErrTelegramLoginSecretInvalid), errors.Is(err, domain.ErrTelegramLoginClientDisabled): + h.invalidClient(w) + case errors.Is(err, domain.ErrTelegramLoginCodeInvalid), errors.Is(err, domain.ErrTelegramLoginCodeConsumed), + errors.Is(err, domain.ErrTelegramLoginPKCEInvalid), errors.Is(err, domain.ErrTelegramLoginURLInvalid): + writeOAuthError(w, http.StatusBadRequest, "invalid_grant", "authorization code is invalid or expired") + default: + h.logger.Error("telegram_login_token_exchange_failed", zap.String("error", errorClass(err))) + writeOAuthError(w, http.StatusInternalServerError, "server_error", "token service unavailable") + } + return + } + _ = issued.WebAuthorization // durable authorization is intentionally not encoded into the opaque access token. + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + writeJSON(w, http.StatusOK, map[string]any{ + "access_token": accessToken, "token_type": "Bearer", "expires_in": int64(h.tokens.TTL().Seconds()), + "id_token": issued.IDToken, + }) +} + +func (h *Handler) invalidClient(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Basic realm="telegram-login-token", charset="UTF-8"`) + writeOAuthError(w, http.StatusUnauthorized, "invalid_client", "client authentication failed") +} + +func parseBoundedForm(w http.ResponseWriter, r *http.Request, maxBytes int64) (url.Values, bool) { + mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil || mediaType != "application/x-www-form-urlencoded" { + writeOAuthError(w, http.StatusUnsupportedMediaType, "invalid_request", "form content type is required") + return nil, false + } + r.Body = http.MaxBytesReader(w, r.Body, maxBytes) + body, err := io.ReadAll(r.Body) + if err != nil { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "request body is invalid") + return nil, false + } + form, err := url.ParseQuery(string(body)) + if err != nil { + writeOAuthError(w, http.StatusBadRequest, "invalid_request", "request body is invalid") + return nil, false + } + return form, true +} + +func singleValue(values url.Values, key string) (string, bool) { + items, exists := values[key] + if !exists { + return "", true + } + if len(items) != 1 { + return "", false + } + return items[0], true +} + +func optionalSingleValue(values url.Values, key string) (string, bool) { + value, ok := singleValue(values, key) + return value, ok +} + +func requiredSingleValue(values url.Values, key string, max int) (string, bool) { + value, ok := singleValue(values, key) + return value, ok && value != "" && len(value) <= max +} + +func boundedHeader(value, fallback string, max int) string { + value = strings.TrimSpace(strings.ToValidUTF8(value, "�")) + if value == "" { + value = fallback + } + for len(value) > max { + _, size := utf8.DecodeLastRuneInString(value) + value = value[:len(value)-size] + } + return value +} + +func (h *Handler) requestIP(r *http.Request) string { + remote, ok := parseRequestIP(r.RemoteAddr) + if !ok { + return "Unknown IP" + } + if !prefixContains(h.trustedProxies, remote) { + return remote.String() + } + forwarded := strings.Split(r.Header.Get("X-Forwarded-For"), ",") + for i := len(forwarded) - 1; i >= 0; i-- { + candidate, ok := parseRequestIP(strings.TrimSpace(forwarded[i])) + if !ok { + continue + } + remote = candidate + if !prefixContains(h.trustedProxies, candidate) { + return candidate.String() + } + } + if candidate, ok := parseRequestIP(strings.TrimSpace(r.Header.Get("X-Real-IP"))); ok { + return candidate.String() + } + return remote.String() +} + +func parseRequestIP(raw string) (netip.Addr, bool) { + if addrPort, err := netip.ParseAddrPort(raw); err == nil { + return addrPort.Addr().Unmap(), true + } + if host, _, err := net.SplitHostPort(raw); err == nil { + raw = host + } + addr, err := netip.ParseAddr(strings.Trim(raw, "[]")) + return addr.Unmap(), err == nil +} + +func prefixContains(prefixes []netip.Prefix, addr netip.Addr) bool { + for _, prefix := range prefixes { + if prefix.Contains(addr) { + return true + } + } + return false +} + +func (h *Handler) allow(w http.ResponseWriter, r *http.Request, bucket, subject string, limit int, window time.Duration) bool { + if h.limiter == nil { + return true + } + sum := sha256.Sum256([]byte(subject)) + key := "telegram-login-http:" + bucket + ":" + base64.RawURLEncoding.EncodeToString(sum[:]) + allowed, retryAfter, err := h.limiter.Allow(r.Context(), key, limit, window) + if err != nil { + h.logger.Error("telegram_login_rate_limit_failed", zap.String("bucket", bucket), zap.Error(err)) + writeOAuthError(w, http.StatusServiceUnavailable, "temporarily_unavailable", "login service temporarily unavailable") + return false + } + if allowed { + return true + } + if retryAfter <= 0 { + retryAfter = 1 + } + w.Header().Set("Retry-After", fmt.Sprintf("%d", retryAfter)) + writeOAuthError(w, http.StatusTooManyRequests, "temporarily_unavailable", "too many login requests") + return false +} + +func writeJSON(w http.ResponseWriter, status int, value any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(value) +} + +func writeOAuthError(w http.ResponseWriter, status int, code, description string) { + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + writeJSON(w, status, map[string]string{"error": code, "error_description": description}) +} + +func errorClass(err error) string { + for _, candidate := range []struct { + target error + name string + }{ + {domain.ErrTelegramLoginClientInvalid, "client_invalid"}, + {domain.ErrTelegramLoginClientDisabled, "client_disabled"}, + {domain.ErrTelegramLoginRedirectNotAllowed, "redirect_not_allowed"}, + {domain.ErrTelegramLoginOriginNotAllowed, "origin_not_allowed"}, + {domain.ErrTelegramLoginScopeInvalid, "scope_invalid"}, + {domain.ErrTelegramLoginPKCEInvalid, "pkce_invalid"}, + {domain.ErrTelegramLoginRequestInvalid, "request_invalid"}, + } { + if errors.Is(err, candidate.target) { + return candidate.name + } + } + return fmt.Sprintf("internal_%T", err) +} diff --git a/internal/telegramloginhttp/handler_test.go b/internal/telegramloginhttp/handler_test.go new file mode 100644 index 00000000..c3dd05bd --- /dev/null +++ b/internal/telegramloginhttp/handler_test.go @@ -0,0 +1,675 @@ +package telegramloginhttp + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "encoding/json" + "html" + "io" + "net/http" + "net/http/httptest" + "net/netip" + "net/url" + "regexp" + "strings" + "sync" + "testing" + "time" + "unicode/utf8" + + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jwt" + + loginapp "telesrv/internal/app/telegramlogin" + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +func TestRequestIPTrustsForwardingHeadersOnlyFromConfiguredProxies(t *testing.T) { + h := &Handler{trustedProxies: []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32"), netip.MustParsePrefix("10.0.0.0/8")}} + req := httptest.NewRequest(http.MethodGet, "https://oauth.test/auth", nil) + req.RemoteAddr = "127.0.0.1:44321" + req.Header.Set("X-Forwarded-For", "198.51.100.7, 10.0.0.4") + if got := h.requestIP(req); got != "198.51.100.7" { + t.Fatalf("trusted proxy client IP = %q", got) + } + req.RemoteAddr = "203.0.113.9:44321" + req.Header.Set("X-Forwarded-For", "198.51.100.8") + if got := h.requestIP(req); got != "203.0.113.9" { + t.Fatalf("untrusted spoofed client IP = %q", got) + } +} + +func TestBoundedHeaderPreservesValidUTF8AtByteLimit(t *testing.T) { + got := boundedHeader(strings.Repeat("界", 100)+string([]byte{0xff}), "fallback", 255) + if !utf8.ValidString(got) || len(got) > 255 || got == "" { + t.Fatalf("bounded header len=%d valid=%v value=%q", len(got), utf8.ValidString(got), got) + } +} + +type telegramLoginHTTPFixture struct { + handler *Handler + service *loginapp.Service + credentials loginapp.ClientCredentials + redirectURI string + verifier string + challenge string + now *time.Time +} + +type telegramLoginHTTPDenyLimiter struct{} + +func (telegramLoginHTTPDenyLimiter) Allow(context.Context, string, int, time.Duration) (bool, int, error) { + return false, 17, nil +} + +func newTelegramLoginHTTPFixture(t *testing.T) telegramLoginHTTPFixture { + t.Helper() + now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC) + sealKey := make([]byte, 32) + sealKey[0] = 1 + sealer, err := loginapp.NewCodeSealer("test", map[string][]byte{"test": sealKey}) + if err != nil { + t.Fatal(err) + } + pepper := make([]byte, 32) + pepper[0] = 2 + service, err := loginapp.NewService(memory.NewTelegramLoginStore(nil), sealer, loginapp.Config{ + Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv", ClientSecretPepper: pepper, + Now: func() time.Time { return now }, + }) + if err != nil { + t.Fatal(err) + } + credentials, err := service.CreateClient(context.Background(), 9001, domain.TelegramLoginSigningRS256) + if err != nil { + t.Fatal(err) + } + const redirectURI = "https://rp.example/callback" + if _, err := service.AddAllowedURL(context.Background(), 9001, domain.TelegramLoginAllowedRedirectURI, redirectURI); err != nil { + t.Fatal(err) + } + if _, err := service.AddAllowedURL(context.Background(), 9001, domain.TelegramLoginAllowedWebOrigin, "https://rp.example"); err != nil { + t.Fatal(err) + } + signingKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + ring, err := loginapp.NewSigningKeyRing([]loginapp.SigningKeyMaterial{{ + Algorithm: domain.TelegramLoginSigningRS256, KeyID: "rsa-test", PrivateKey: signingKey, Active: true, + }}, func() time.Time { return now }) + if err != nil { + t.Fatal(err) + } + tokens, err := loginapp.NewIDTokenIssuer(ring, loginapp.IDTokenIssuerConfig{ + Issuer: "https://oauth.telesrv.test", Now: func() time.Time { return now }, + }) + if err != nil { + t.Fatal(err) + } + handler, err := NewHandler(Config{Service: service, Tokens: tokens, AppName: "Telesrv", AllowLoopbackHTTP: true}) + if err != nil { + t.Fatal(err) + } + const verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + challenge, err := loginapp.PKCEChallenge(verifier) + if err != nil { + t.Fatal(err) + } + return telegramLoginHTTPFixture{ + handler: handler, service: service, credentials: credentials, redirectURI: redirectURI, + verifier: verifier, challenge: challenge, now: &now, + } +} + +func (f telegramLoginHTTPFixture) authorize(t *testing.T) (browserToken, deepLink string) { + t.Helper() + query := url.Values{ + "client_id": {f.credentials.Client.ClientID}, "redirect_uri": {f.redirectURI}, + "response_type": {"code"}, "scope": {"openid profile phone telegram:bot_access"}, + "state": {"state-value"}, "nonce": {"nonce-value"}, "code_challenge": {f.challenge}, + "code_challenge_method": {"S256"}, + } + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/auth?"+query.Encode(), nil) + request.RemoteAddr = "192.0.2.10:4242" + f.handler.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("authorize status=%d body=%s", recorder.Code, recorder.Body.String()) + } + body := recorder.Body.String() + tokenMatch := regexp.MustCompile(`const token=("[^"]+")`).FindStringSubmatch(body) + if len(tokenMatch) != 2 || json.Unmarshal([]byte(tokenMatch[1]), &browserToken) != nil { + t.Fatalf("browser token not found in page: %s", body) + } + deepLinkMatch := regexp.MustCompile(`href="([^"]+)"`).FindStringSubmatch(body) + if len(deepLinkMatch) != 2 { + t.Fatalf("deep link not found in page: %s", body) + } + deepLink = html.UnescapeString(deepLinkMatch[1]) + pending, err := f.service.RequestByDeepLink(context.Background(), deepLink) + if err != nil { + t.Fatalf("resolve authorization page deep link: %v", err) + } + if pending.MatchCode == "" || !strings.Contains(body, `id="match-code"`) || !strings.Contains(body, pending.MatchCode) { + t.Fatalf("matching emoji missing from authorization page: match=%q body=%s", pending.MatchCode, body) + } + if !strings.Contains(body, "poll();") { + t.Fatalf("authorization status polling is not started: %s", body) + } + return browserToken, deepLink +} + +func TestAuthorizationErrorsUseOnlyPreRegisteredTargets(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + base := url.Values{ + "client_id": {f.credentials.Client.ClientID}, "redirect_uri": {f.redirectURI}, + "response_type": {"code"}, "scope": {"openid unsupported"}, "state": {"safe-state"}, + "code_challenge": {f.challenge}, "code_challenge_method": {"S256"}, + } + recorder := httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/auth?"+base.Encode(), nil)) + if recorder.Code != http.StatusFound { + t.Fatalf("valid redirect error status=%d body=%s", recorder.Code, recorder.Body.String()) + } + location, err := url.Parse(recorder.Header().Get("Location")) + if err != nil || location.Scheme+"://"+location.Host+location.Path != f.redirectURI || location.Query().Get("error") != "invalid_scope" || location.Query().Get("state") != "safe-state" { + t.Fatalf("error redirect=%q err=%v", recorder.Header().Get("Location"), err) + } + + forged := cloneURLValues(base) + forged.Set("redirect_uri", "https://attacker.example/callback") + recorder = httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/auth?"+forged.Encode(), nil)) + if recorder.Code != http.StatusBadRequest || recorder.Header().Get("Location") != "" { + t.Fatalf("forged redirect status=%d location=%q", recorder.Code, recorder.Header().Get("Location")) + } + + post := cloneURLValues(base) + post.Set("redirect_uri", "https://rp.example/") + post.Set("response_type", "post_message") + post.Set("origin", "https://rp.example") + recorder = httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/auth?"+post.Encode(), nil)) + if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `postMessage`) || !strings.Contains(recorder.Body.String(), `https://rp.example`) { + t.Fatalf("post_message error status=%d body=%s", recorder.Code, recorder.Body.String()) + } + post.Set("redirect_uri", "https://attacker.example/") + recorder = httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/auth?"+post.Encode(), nil)) + if recorder.Code != http.StatusBadRequest { + t.Fatalf("cross-origin post_message status=%d body=%s", recorder.Code, recorder.Body.String()) + } +} + +func cloneURLValues(in url.Values) url.Values { + out := make(url.Values, len(in)) + for key, values := range in { + out[key] = append([]string(nil), values...) + } + return out +} + +func (f telegramLoginHTTPFixture) approveAndFinalize(t *testing.T) (code string) { + t.Helper() + browserToken, deepLink := f.authorize(t) + *f.now = f.now.Add(time.Second) + pending, err := f.service.RequestByDeepLink(context.Background(), deepLink) + if err != nil { + t.Fatalf("RequestByDeepLink(%q): %v", deepLink, err) + } + _, _, err = f.service.Approve(context.Background(), deepLink, domain.TelegramLoginIdentitySnapshot{ + UserID: 42, Name: "Alice Example", GivenName: "Alice", FamilyName: "Example", + PreferredUsername: "alice", Picture: "https://oauth.telesrv.test/userpic/42", PhoneNumber: "+1 555 123 4567", + }, true, true, pending.MatchCode) + if err != nil { + t.Fatal(err) + } + form := url.Values{"browser_token": {browserToken}} + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/auth/status", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + f.handler.ServeHTTP(recorder, req) + if recorder.Code != http.StatusOK { + t.Fatalf("status status=%d body=%s", recorder.Code, recorder.Body.String()) + } + var response struct { + Status string `json:"status"` + RedirectURL string `json:"redirect_url"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil || response.Status != "approved" { + t.Fatalf("status response=%+v err=%v body=%s", response, err, recorder.Body.String()) + } + redirect, err := url.Parse(response.RedirectURL) + if err != nil || redirect.Query().Get("state") != "state-value" { + t.Fatalf("redirect=%q err=%v", response.RedirectURL, err) + } + return redirect.Query().Get("code") +} + +func TestDiscoveryAndJWKS(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + for _, path := range []string{"/.well-known/openid-configuration", "/.well-known/jwks.json"} { + recorder := httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, path, nil)) + if recorder.Code != http.StatusOK || recorder.Header().Get("X-Content-Type-Options") != "nosniff" { + t.Fatalf("GET %s status=%d headers=%v body=%s", path, recorder.Code, recorder.Header(), recorder.Body.String()) + } + } +} + +func TestAuthorizationCodeHTTPFlowAndReplay(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + code := f.approveAndFinalize(t) + form := url.Values{ + "grant_type": {"authorization_code"}, "code": {code}, "redirect_uri": {f.redirectURI}, + "client_id": {f.credentials.Client.ClientID}, "code_verifier": {f.verifier}, + } + exchange := func() *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(f.credentials.Client.ClientID, f.credentials.Secret) + f.handler.ServeHTTP(recorder, req) + return recorder + } + recorder := exchange() + if recorder.Code != http.StatusOK || recorder.Header().Get("Cache-Control") != "no-store" { + t.Fatalf("token status=%d headers=%v body=%s", recorder.Code, recorder.Header(), recorder.Body.String()) + } + var response struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil || response.AccessToken == "" || response.IDToken == "" || response.TokenType != "Bearer" || response.ExpiresIn != 3600 { + t.Fatalf("token response=%+v err=%v body=%s", response, err, recorder.Body.String()) + } + jwksRecorder := httptest.NewRecorder() + f.handler.ServeHTTP(jwksRecorder, httptest.NewRequest(http.MethodGet, "/.well-known/jwks.json", nil)) + set, err := jwk.Parse(jwksRecorder.Body.Bytes()) + if err != nil { + t.Fatal(err) + } + token, err := jwt.Parse([]byte(response.IDToken), jwt.WithKeySet(set), jwt.WithValidate(false)) + if err != nil || !token.Has("phone_number") || !token.Has("preferred_username") { + t.Fatalf("verified ID token=%v err=%v", token, err) + } + replay := exchange() + if replay.Code != http.StatusBadRequest || !strings.Contains(replay.Body.String(), "invalid_grant") { + t.Fatalf("replay status=%d body=%s", replay.Code, replay.Body.String()) + } +} + +func TestNativeSDKCrossAppAndPublicPKCEExchange(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + const callbackURI = "bedolaga://telegram-login" + if _, err := f.service.AddNativeApp(context.Background(), 9001, domain.TelegramLoginNativeIOS, + "dev.bedolaga.demo", "ABCDE12345", callbackURI, "Bedolaga Demo"); err != nil { + t.Fatalf("AddNativeApp: %v", err) + } + query := url.Values{ + "client_id": {f.credentials.Client.ClientID}, "redirect_uri": {callbackURI}, + "response_type": {"code"}, "scope": {"profile"}, "ios_sdk": {"1"}, + "code_challenge": {f.challenge}, "code_challenge_method": {"S256"}, + } + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/crossapp?"+query.Encode(), nil) + request.Header.Set("User-Agent", "TelegramLogin/iOS") + f.handler.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK || recorder.Header().Get("Cache-Control") != "no-store" { + t.Fatalf("crossapp status=%d headers=%v body=%s", recorder.Code, recorder.Header(), recorder.Body.String()) + } + var crossApp struct { + URL string `json:"url"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &crossApp); err != nil || crossApp.URL == "" { + t.Fatalf("crossapp response=%+v err=%v", crossApp, err) + } + pending, err := f.service.RequestByDeepLink(context.Background(), crossApp.URL) + if err != nil { + t.Fatal(err) + } + if pending.Source != domain.TelegramLoginRequestNative || !pending.IsApp || pending.VerifiedAppName != "Bedolaga Demo" || + len(pending.Scopes) != 2 || pending.Scopes[0] != domain.TelegramLoginScopeOpenID { + t.Fatalf("native request=%#v", pending) + } + *f.now = f.now.Add(time.Second) + if _, _, err := f.service.Approve(context.Background(), crossApp.URL, domain.TelegramLoginIdentitySnapshot{ + UserID: 42, Name: "Alice", GivenName: "Alice", + }, false, false, pending.MatchCode); err != nil { + t.Fatal(err) + } + redirectURL, err := f.service.FinalizeRedirectByDeepLink(context.Background(), crossApp.URL) + if err != nil { + t.Fatal(err) + } + callback, err := url.Parse(redirectURL) + if err != nil || callback.Scheme != "bedolaga" || callback.Host != "telegram-login" || callback.Query().Get("code") == "" { + t.Fatalf("native callback=%q err=%v", redirectURL, err) + } + form := url.Values{ + "grant_type": {"authorization_code"}, "client_id": {f.credentials.Client.ClientID}, + "code": {callback.Query().Get("code")}, "redirect_uri": {callbackURI}, "code_verifier": {f.verifier}, + } + tokenRecorder := httptest.NewRecorder() + tokenRequest := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode())) + tokenRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") + f.handler.ServeHTTP(tokenRecorder, tokenRequest) + if tokenRecorder.Code != http.StatusOK || !strings.Contains(tokenRecorder.Body.String(), `"id_token"`) { + t.Fatalf("native token status=%d body=%s", tokenRecorder.Code, tokenRecorder.Body.String()) + } + + query.Set("android_sdk", "1") + recorder = httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/crossapp?"+query.Encode(), nil)) + if recorder.Code != http.StatusBadRequest { + t.Fatalf("dual SDK marker status=%d body=%s", recorder.Code, recorder.Body.String()) + } +} + +func TestPublicTokenAuthenticationCannotExchangeWebCode(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + code := f.approveAndFinalize(t) + form := url.Values{ + "grant_type": {"authorization_code"}, "client_id": {f.credentials.Client.ClientID}, + "code": {code}, "redirect_uri": {f.redirectURI}, "code_verifier": {f.verifier}, + } + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode())) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + f.handler.ServeHTTP(recorder, request) + if recorder.Code != http.StatusUnauthorized || !strings.Contains(recorder.Body.String(), "invalid_client") { + t.Fatalf("public web exchange status=%d body=%s", recorder.Code, recorder.Body.String()) + } + + // Authentication failure happens before the one-time consume, so the same + // code remains usable by the confidential web client. + recorder = httptest.NewRecorder() + request = httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode())) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request.SetBasicAuth(f.credentials.Client.ClientID, f.credentials.Secret) + f.handler.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("authenticated retry status=%d body=%s", recorder.Code, recorder.Body.String()) + } +} + +func TestConcurrentTokenExchangeHasOneSuccess(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + code := f.approveAndFinalize(t) + form := url.Values{ + "grant_type": {"authorization_code"}, "code": {code}, "redirect_uri": {f.redirectURI}, + "client_id": {f.credentials.Client.ClientID}, "code_verifier": {f.verifier}, + }.Encode() + const workers = 8 + statuses := make(chan int, workers) + var wg sync.WaitGroup + for range workers { + wg.Add(1) + go func() { + defer wg.Done() + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(f.credentials.Client.ClientID, f.credentials.Secret) + f.handler.ServeHTTP(recorder, req) + statuses <- recorder.Code + }() + } + wg.Wait() + close(statuses) + success := 0 + for status := range statuses { + if status == http.StatusOK { + success++ + } else if status != http.StatusBadRequest { + t.Fatalf("unexpected concurrent exchange status %d", status) + } + } + if success != 1 { + t.Fatalf("successful exchanges=%d, want 1", success) + } +} + +func TestTokenEndpointSupportsBodySecretAndRejectsMixedAuthentication(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + code := f.approveAndFinalize(t) + form := url.Values{ + "grant_type": {"authorization_code"}, "client_id": {f.credentials.Client.ClientID}, + "client_secret": {f.credentials.Secret}, "code": {code}, "redirect_uri": {f.redirectURI}, + "code_verifier": {f.verifier}, + } + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + f.handler.ServeHTTP(recorder, req) + if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"id_token"`) { + t.Fatalf("client_secret_post status=%d body=%s", recorder.Code, recorder.Body.String()) + } + + secondCode := f.approveAndFinalize(t) + form.Set("code", secondCode) + recorder = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(f.credentials.Client.ClientID, f.credentials.Secret) + f.handler.ServeHTTP(recorder, req) + if recorder.Code != http.StatusBadRequest || !strings.Contains(recorder.Body.String(), "invalid_request") { + t.Fatalf("mixed authentication status=%d body=%s", recorder.Code, recorder.Body.String()) + } + + recorder = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(`{}`)) + req.Header.Set("Content-Type", "application/json") + f.handler.ServeHTTP(recorder, req) + _, _ = io.Copy(io.Discard, recorder.Result().Body) + if recorder.Code != http.StatusUnsupportedMediaType { + t.Fatalf("JSON token content status=%d body=%s", recorder.Code, recorder.Body.String()) + } +} + +func TestHTTPRateLimitFailsBeforeAuthorizationCreation(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + f.handler.limiter = telegramLoginHTTPDenyLimiter{} + recorder := httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/auth", nil)) + if recorder.Code != http.StatusTooManyRequests || recorder.Header().Get("Retry-After") != "17" { + t.Fatalf("rate limit status=%d headers=%v body=%s", recorder.Code, recorder.Header(), recorder.Body.String()) + } +} + +func TestJavaScriptPostMessageFlowReturnsStableDirectIDToken(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + query := url.Values{ + "client_id": {f.credentials.Client.ClientID}, "redirect_uri": {"https://rp.example/"}, + "response_type": {"post_message"}, "scope": {"openid profile phone"}, + "nonce": {"js-nonce"}, + } + recorder := httptest.NewRecorder() + f.handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/auth?"+query.Encode(), nil)) + if recorder.Code != http.StatusOK { + t.Fatalf("JS authorize status=%d body=%s", recorder.Code, recorder.Body.String()) + } + body := recorder.Body.String() + tokenMatch := regexp.MustCompile(`const token=("[^"]+")`).FindStringSubmatch(body) + deepLinkMatch := regexp.MustCompile(`href="([^"]+)"`).FindStringSubmatch(body) + if len(tokenMatch) != 2 || len(deepLinkMatch) != 2 { + t.Fatalf("JS authorize artifacts missing: %s", body) + } + var browserToken string + if err := json.Unmarshal([]byte(tokenMatch[1]), &browserToken); err != nil { + t.Fatal(err) + } + deepLink := html.UnescapeString(deepLinkMatch[1]) + pending, err := f.service.RequestByDeepLink(context.Background(), deepLink) + if err != nil { + t.Fatal(err) + } + if pending.Source != domain.TelegramLoginRequestJavaScript || pending.Origin != "https://rp.example" || pending.CodeChallenge != "" { + t.Fatalf("official JavaScript request = %#v", pending) + } + *f.now = f.now.Add(time.Second) + if _, _, err := f.service.Approve(context.Background(), deepLink, domain.TelegramLoginIdentitySnapshot{ + UserID: 42, Name: "Alice Example", GivenName: "Alice", PhoneNumber: "15551234567", + }, false, true, pending.MatchCode); err != nil { + t.Fatal(err) + } + poll := func() map[string]string { + t.Helper() + statusRecorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/auth/status", strings.NewReader(url.Values{"browser_token": {browserToken}}.Encode())) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + f.handler.ServeHTTP(statusRecorder, request) + if statusRecorder.Code != http.StatusOK { + t.Fatalf("JS status=%d body=%s", statusRecorder.Code, statusRecorder.Body.String()) + } + var result map[string]string + if err := json.Unmarshal(statusRecorder.Body.Bytes(), &result); err != nil { + t.Fatal(err) + } + return result + } + first, second := poll(), poll() + if first["status"] != "approved" || first["id_token"] == "" || second["id_token"] != first["id_token"] { + t.Fatalf("direct token first=%v second=%v", first, second) + } + web, err := f.service.ListWebAuthorizations(context.Background(), 42) + if err != nil || len(web) != 1 { + t.Fatalf("direct web authorization=%#v err=%v", web, err) + } + if err := f.service.RevokeWebAuthorization(context.Background(), 42, web[0].Hash); err != nil { + t.Fatal(err) + } + revokedRecorder := httptest.NewRecorder() + revokedRequest := httptest.NewRequest(http.MethodPost, "/auth/status", strings.NewReader(url.Values{"browser_token": {browserToken}}.Encode())) + revokedRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") + f.handler.ServeHTTP(revokedRecorder, revokedRequest) + if revokedRecorder.Code != http.StatusInternalServerError || !strings.Contains(revokedRecorder.Body.String(), "server_error") { + t.Fatalf("revoked direct delivery status=%d body=%s", revokedRecorder.Code, revokedRecorder.Body.String()) + } + + exchangeForm := url.Values{ + "grant_type": {"authorization_code"}, "code": {first["id_token"]}, "redirect_uri": {"https://rp.example/"}, + "client_id": {f.credentials.Client.ClientID}, "code_verifier": {f.verifier}, + } + exchangeRecorder := httptest.NewRecorder() + exchangeRequest := httptest.NewRequest(http.MethodPost, "/token", strings.NewReader(exchangeForm.Encode())) + exchangeRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded") + exchangeRequest.SetBasicAuth(f.credentials.Client.ClientID, f.credentials.Secret) + f.handler.ServeHTTP(exchangeRecorder, exchangeRequest) + if exchangeRecorder.Code != http.StatusBadRequest || !strings.Contains(exchangeRecorder.Body.String(), "invalid_grant") { + t.Fatalf("direct token exchange status=%d body=%s", exchangeRecorder.Code, exchangeRecorder.Body.String()) + } +} + +func TestMiniAppOfficialInAppFlowIsOriginBoundAndOneTime(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + const origin = "https://rp.example" + query := url.Values{ + "client_id": {f.credentials.Client.ClientID}, "scope": {"openid profile phone telegram:bot_access"}, + "origin": {origin}, "response_type": {"id_token"}, + } + create := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/inapp?"+query.Encode(), nil) + request.Header.Set("Origin", origin) + f.handler.ServeHTTP(create, request) + if create.Code != http.StatusOK || create.Header().Get("Access-Control-Allow-Origin") != origin || create.Header().Get("Vary") != "Origin" { + t.Fatalf("in-app create status=%d headers=%v body=%s", create.Code, create.Header(), create.Body.String()) + } + var created struct { + URL string `json:"url"` + } + if err := json.Unmarshal(create.Body.Bytes(), &created); err != nil || created.URL == "" { + t.Fatalf("in-app create response=%+v err=%v", created, err) + } + pending, err := f.service.RequestByDeepLinkForOrigin(context.Background(), created.URL, origin) + if err != nil { + t.Fatal(err) + } + if pending.Source != domain.TelegramLoginRequestMiniApp || pending.InAppOrigin != origin || pending.ResponseType != "post_message" { + t.Fatalf("in-app request=%#v", pending) + } + *f.now = f.now.Add(time.Second) + if _, _, err := f.service.Approve(context.Background(), created.URL, domain.TelegramLoginIdentitySnapshot{ + UserID: 42, Name: "Alice Example", GivenName: "Alice", PhoneNumber: "15551234567", + }, true, true, pending.MatchCode); err != nil { + t.Fatal(err) + } + resultURL, err := f.service.FinalizeInAppRedirectByDeepLink(context.Background(), created.URL) + if err != nil { + t.Fatal(err) + } + parsed, err := url.Parse(resultURL) + if err != nil || parsed.Scheme+"://"+parsed.Host != "https://oauth.telesrv.test" || parsed.Path != "/inapp" || parsed.Query().Get("token") == "" { + t.Fatalf("in-app result URL=%q err=%v", resultURL, err) + } + token := parsed.Query().Get("token") + + wrongOrigin := httptest.NewRecorder() + wrongRequest := httptest.NewRequest(http.MethodGet, "/inapp?code="+url.QueryEscape(token), nil) + wrongRequest.Header.Set("Origin", "https://attacker.example") + f.handler.ServeHTTP(wrongOrigin, wrongRequest) + if wrongOrigin.Code != http.StatusBadRequest { + t.Fatalf("wrong-origin exchange status=%d body=%s", wrongOrigin.Code, wrongOrigin.Body.String()) + } + + const workers = 8 + statuses := make(chan int, workers) + results := make(chan string, workers) + var wg sync.WaitGroup + for range workers { + wg.Add(1) + go func() { + defer wg.Done() + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/inapp?code="+url.QueryEscape(token), nil) + req.Header.Set("Origin", origin) + f.handler.ServeHTTP(recorder, req) + statuses <- recorder.Code + results <- recorder.Body.String() + }() + } + wg.Wait() + close(statuses) + close(results) + successes := 0 + responses := make([]string, 0, workers) + for status := range statuses { + if status == http.StatusOK { + successes++ + } else if status != http.StatusBadRequest { + t.Fatalf("unexpected in-app exchange status=%d", status) + } + } + for response := range results { + responses = append(responses, response) + } + if successes != 1 { + t.Fatalf("in-app exchange successes=%d, want 1; responses=%v", successes, responses) + } +} + +func TestTelegramLoginJavaScriptIsCacheableAndConditional(t *testing.T) { + f := newTelegramLoginHTTPFixture(t) + first := httptest.NewRecorder() + f.handler.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "/js/telegram-login.js", nil)) + if first.Code != http.StatusOK || first.Header().Get("ETag") == "" || + !strings.Contains(first.Body.String(), "Telegram.Login") || + !strings.Contains(first.Body.String(), "auth_result") || + !strings.Contains(first.Body.String(), "oauth_supported") || + !strings.Contains(first.Body.String(), "/inapp?") || + !strings.Contains(first.Body.String(), "data-client-id") { + t.Fatalf("SDK status=%d headers=%v body=%s", first.Code, first.Header(), first.Body.String()) + } + second := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/js/telegram-login.js", nil) + request.Header.Set("If-None-Match", first.Header().Get("ETag")) + f.handler.ServeHTTP(second, request) + if second.Code != http.StatusNotModified { + t.Fatalf("conditional SDK status=%d", second.Code) + } +} diff --git a/internal/telegramloginhttp/sdk.go b/internal/telegramloginhttp/sdk.go new file mode 100644 index 00000000..cadd2c00 --- /dev/null +++ b/internal/telegramloginhttp/sdk.go @@ -0,0 +1,90 @@ +package telegramloginhttp + +import ( + "crypto/sha256" + "encoding/base64" + "net/http" +) + +// telegramLoginJavaScript intentionally follows the public Telegram Login +// SDK contract (programmatic API, data-* auto-init, popup auth_result events, +// and the Mini App oauth_* bridge). It derives the provider from its own +// script origin so the same file works on a self-hosted issuer. +const telegramLoginJavaScript = `(function(global){ +'use strict'; +var current=document.currentScript; +if(!current){throw new Error('Telegram Login SDK must be loaded by a script element');} +var provider=new URL(current.src,document.baseURI).origin; +var saved=null,active=null,inApp=false,inAppPending=false; +function callback(cb,value){if(typeof cb==='function'){try{cb(value);}catch(error){setTimeout(function(){throw error;},0);}}} +function decode(token){try{var part=token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/');while(part.length%4){part+='=';}return JSON.parse(decodeURIComponent(Array.from(atob(part),function(c){return '%'+c.charCodeAt(0).toString(16).padStart(2,'0');}).join('')));}catch(_){return null;}} +function build(data){if(data&&data.error){return {error:String(data.error)};}var token=data&&data.result;if(typeof token!=='string'||!token){return {error:'missing id_token'};}var user=decode(token);return user?{id_token:token,user:user}:{error:'malformed id_token'};} +function normalize(options){ + if(!options||!/^[0-9]{1,64}$/.test(String(options.client_id||''))){throw new Error('Telegram.Login client_id is required');} + var scopes=['openid'],input=options.scope; + if(input===undefined||input===null||input===''){scopes.push('profile');input=options.request_access||[];} + if(typeof input==='string'){input=input.trim()?input.trim().split(/\s+/):[];} + if(!Array.isArray(input)){throw new Error('Telegram.Login scope must be an array or string');} + var allowed={profile:'profile',phone:'phone',write:'telegram:bot_access','telegram:bot_access':'telegram:bot_access'}; + input.forEach(function(value){var mapped=allowed[value];if(!mapped){throw new Error('Telegram.Login scope is invalid');}if(scopes.indexOf(mapped)<0){scopes.push(mapped);}}); + return {client_id:String(options.client_id),scope:scopes.join(' '),nonce:String(options.nonce||'').slice(0,1024),lang:String(options.lang||'').slice(0,16)}; +} +function randomURL(bytes){var data=new Uint8Array(bytes);crypto.getRandomValues(data);var raw='';data.forEach(function(value){raw+=String.fromCharCode(value);});return btoa(raw).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');} +function finish(flow,result){if(active!==flow){return;}active=null;if(flow.timer){clearInterval(flow.timer);}if(flow.listener){global.removeEventListener('message',flow.listener);}callback(flow.callback,result);} +function sendEvent(type,data){if(global.TelegramWebviewProxy&&typeof global.TelegramWebviewProxy.postEvent==='function'){global.TelegramWebviewProxy.postEvent(type,JSON.stringify(data||{}));}} +async function receiveEvent(type,data){ + if(type==='oauth_supported'){inApp=true;return;} + if(type==='oauth_result_failed'){if(active){finish(active,{error:'access_denied'});}return;} + if(type!=='oauth_result_confirmed'||!active||!data||!data.result_url){return;} + try{var resultURL=new URL(data.result_url);if(resultURL.origin!==provider||resultURL.pathname!=='/inapp'){throw new Error('invalid in-app result URL');}var token=resultURL.searchParams.get('token');if(!token){throw new Error('missing in-app token');} + var response=await fetch(provider+'/inapp?code='+encodeURIComponent(token),{credentials:'omit',cache:'no-store'}),result=await response.json(); + if(!response.ok){throw new Error(result.error||'in-app exchange failed');}finish(active,build(result)); + }catch(error){finish(active,{error:error.message||'in_app_failed'});} +} +function begin(options,cb,isNormalized){ + var normalized;try{normalized=isNormalized?options:normalize(options);}catch(error){callback(cb,{error:error.message});return null;} + if(active){callback(cb,{error:'login_in_progress'});return null;} + var flow={popup:null,callback:cb,listener:null,timer:null};active=flow; + if(inApp){ + if(inAppPending){finish(flow,{error:'login_in_progress'});return null;}inAppPending=true; + var inAppParams=new URLSearchParams({scope:normalized.scope,origin:global.location.origin,client_id:normalized.client_id,response_type:'id_token'}); + fetch(provider+'/inapp?'+inAppParams.toString(),{credentials:'omit',cache:'no-store'}).then(function(response){return response.json().then(function(body){if(!response.ok){throw new Error(body.error||'in-app request failed');}return body;});}).then(function(body){if(!body.url){throw new Error('missing OAuth deep link');}sendEvent('oauth_request',{url:body.url});}).catch(function(error){finish(flow,{error:error.message||'in_app_failed'});}).finally(function(){setTimeout(function(){inAppPending=false;},600);}); + return null; + } + var popup=global.open('about:blank','telegram-login-'+randomURL(8),'popup,width=550,height=650,resizable=yes,scrollbars=yes'); + if(!popup){finish(flow,{error:'popup_blocked'});return null;}flow.popup=popup; + flow.listener=function(event){if(event.origin!==provider||event.source!==popup){return;}var data=event.data;try{if(typeof data==='string'){data=JSON.parse(data);}}catch(_){return;}if(!data||data.event!=='auth_result'){return;}finish(flow,build(data));}; + global.addEventListener('message',flow.listener);flow.timer=setInterval(function(){if(popup.closed){finish(flow,{error:'popup_closed'});}},500); + try{var params=new URLSearchParams({client_id:normalized.client_id,redirect_uri:global.location.origin+global.location.pathname,response_type:'post_message',scope:normalized.scope});if(normalized.nonce){params.set('nonce',normalized.nonce);}if(normalized.lang){params.set('lang',normalized.lang);}popup.location.replace(provider+'/auth?'+params.toString());} + catch(error){try{popup.close();}catch(_){}finish(flow,{error:error.message||'login_failed'});} + return popup; +} +var api={ + init:function(options,cb){saved={options:normalize(options),callback:cb};return api;}, + open:function(cb){if(!saved){callback(cb,{error:'not_initialized'});return null;}return begin(saved.options,cb||saved.callback,true);}, + auth:function(options,cb){return begin(options,cb,false);}, + close:function(){if(active&&active.popup){try{active.popup.close();}catch(_){}}if(active){finish(active,{error:'popup_closed'});}} +}; +global.Telegram=global.Telegram||{};global.Telegram.Login=api; +global.Telegram.WebView=global.Telegram.WebView||{};global.Telegram.WebView.receiveEvent=receiveEvent; +global.Telegram.TelegramGameProxy=global.Telegram.TelegramGameProxy||{};global.Telegram.TelegramGameProxy.receiveEvent=receiveEvent; +if(global.TelegramWebviewProxy){sendEvent('oauth_request',{});} +document.addEventListener('click',function(event){var node=event.target;while(node&&node!==document){if(node.classList&&node.classList.contains('tg-auth-button')){api.open();return;}node=node.parentNode;}}); +function resolveCallback(source){var match=/^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\(\s*data\s*\)\s*;?$/.exec(source||'');if(!match){return null;}return function(data){var target=global,parts=match[1].split('.');for(var i=0;i Date: Tue, 21 Jul 2026 15:46:52 +0800 Subject: [PATCH 04/28] fix: sync Telegram Login mobile popup delivery --- internal/telegramloginhttp/handler.go | 35 +++++++++++++++- internal/telegramloginhttp/handler_test.go | 46 ++++++++++++++++++++-- internal/telegramloginhttp/sdk.go | 9 +++-- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/internal/telegramloginhttp/handler.go b/internal/telegramloginhttp/handler.go index 9130a5e8..b977b76d 100644 --- a/internal/telegramloginhttp/handler.go +++ b/internal/telegramloginhttp/handler.go @@ -166,9 +166,16 @@ var authorizationPage = template.Must(template.New("telegram-login").Parse(`Log in with {{.AppName}}

Log in with {{.AppName}}

Open the {{.AppName}} app and approve this request. Keep this page open.

Open {{.AppName}}{{if .MatchCode}}

When prompted, select this emoji in {{.AppName}}:

{{end}}

Waiting for approval…

This request expires at {{.ExpiresAt}}.

-`)) +`)) func (h *Handler) authorize(w http.ResponseWriter, r *http.Request) { + // Authorization popups must retain their cross-origin opener long enough to + // hand the registered RP a short-lived browser token. The default + // same-origin-allow-popups policy isolates a document that was itself opened + // cross-origin, so it breaks the exact-origin postMessage flow before the + // external Telegram app is launched. Other provider endpoints keep the + // stricter default policy. + w.Header().Set("Cross-Origin-Opener-Policy", "unsafe-none") clientIP := h.requestIP(r) if !h.allow(w, r, "authorize", clientIP, 30, time.Minute) { return @@ -449,6 +456,9 @@ func (h *Handler) authorizationStatus(w http.ResponseWriter, r *http.Request) { writeOAuthError(w, http.StatusBadRequest, "invalid_request", "invalid browser request") return } + if !h.authorizeStatusOrigin(w, r, request) { + return + } switch request.Status { case domain.TelegramLoginRequestPending: writeJSON(w, http.StatusOK, map[string]string{"status": "pending"}) @@ -491,6 +501,29 @@ func (h *Handler) authorizationStatus(w http.ResponseWriter, r *http.Request) { } } +// authorizeStatusOrigin keeps the original same-origin popup poll working and +// permits the registered relying-party origin to take over polling when a +// mobile browser drops window.opener during an external app round-trip. The +// short-lived browser token remains a bearer credential, but browser-readable +// responses are exposed only to the exact origin persisted on the request. +func (h *Handler) authorizeStatusOrigin(w http.ResponseWriter, r *http.Request, request domain.TelegramLoginRequest) bool { + origin := strings.TrimSpace(r.Header.Get("Origin")) + if origin == "" { + return true + } + issuerOrigin, err := loginapp.NormalizeWebOrigin(h.tokens.Issuer(), h.allowLoopbackHTTP) + if err == nil && origin == issuerOrigin { + return true + } + if request.ResponseType == "post_message" && origin == request.Origin { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Add("Vary", "Origin") + return true + } + writeOAuthError(w, http.StatusForbidden, "access_denied", "browser origin is not authorized") + return false +} + func (h *Handler) token(w http.ResponseWriter, r *http.Request) { if !h.allow(w, r, "token-ip", h.requestIP(r), 60, time.Minute) { return diff --git a/internal/telegramloginhttp/handler_test.go b/internal/telegramloginhttp/handler_test.go index c3dd05bd..2563de45 100644 --- a/internal/telegramloginhttp/handler_test.go +++ b/internal/telegramloginhttp/handler_test.go @@ -494,16 +494,51 @@ func TestJavaScriptPostMessageFlowReturnsStableDirectIDToken(t *testing.T) { if recorder.Code != http.StatusOK { t.Fatalf("JS authorize status=%d body=%s", recorder.Code, recorder.Body.String()) } + if got := recorder.Header().Get("Cross-Origin-Opener-Policy"); got != "unsafe-none" { + t.Fatalf("JS authorize COOP=%q, want unsafe-none for cross-origin opener handoff", got) + } body := recorder.Body.String() tokenMatch := regexp.MustCompile(`const token=("[^"]+")`).FindStringSubmatch(body) deepLinkMatch := regexp.MustCompile(`href="([^"]+)"`).FindStringSubmatch(body) if len(tokenMatch) != 2 || len(deepLinkMatch) != 2 { t.Fatalf("JS authorize artifacts missing: %s", body) } + if !strings.Contains(body, "auth_pending") || !strings.Contains(body, "browser_token") { + t.Fatalf("JS authorize page does not hand parent polling off before the app round-trip: %s", body) + } var browserToken string if err := json.Unmarshal([]byte(tokenMatch[1]), &browserToken); err != nil { t.Fatal(err) } + requestStatus := func(origin string) *httptest.ResponseRecorder { + t.Helper() + statusRecorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodPost, "/auth/status", strings.NewReader(url.Values{"browser_token": {browserToken}}.Encode())) + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if origin != "" { + request.Header.Set("Origin", origin) + } + f.handler.ServeHTTP(statusRecorder, request) + return statusRecorder + } + attackerStatus := requestStatus("https://attacker.example") + if attackerStatus.Code != http.StatusForbidden || attackerStatus.Header().Get("Access-Control-Allow-Origin") != "" { + t.Fatalf("attacker parent poll status=%d headers=%v body=%s", attackerStatus.Code, attackerStatus.Header(), attackerStatus.Body.String()) + } + for _, origin := range []string{"https://rp.example", "https://oauth.telesrv.test"} { + pendingStatus := requestStatus(origin) + if pendingStatus.Code != http.StatusOK || !strings.Contains(pendingStatus.Body.String(), `"status":"pending"`) { + t.Fatalf("pending parent poll origin=%q status=%d body=%s", origin, pendingStatus.Code, pendingStatus.Body.String()) + } + allowedOrigin := pendingStatus.Header().Get("Access-Control-Allow-Origin") + if origin == "https://rp.example" { + if allowedOrigin != origin || !strings.Contains(pendingStatus.Header().Get("Vary"), "Origin") { + t.Fatalf("RP parent poll origin=%q headers=%v", origin, pendingStatus.Header()) + } + } else if allowedOrigin != "" { + t.Fatalf("same-origin popup unexpectedly received CORS header: %v", pendingStatus.Header()) + } + } deepLink := html.UnescapeString(deepLinkMatch[1]) pending, err := f.service.RequestByDeepLink(context.Background(), deepLink) if err != nil { @@ -520,13 +555,13 @@ func TestJavaScriptPostMessageFlowReturnsStableDirectIDToken(t *testing.T) { } poll := func() map[string]string { t.Helper() - statusRecorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodPost, "/auth/status", strings.NewReader(url.Values{"browser_token": {browserToken}}.Encode())) - request.Header.Set("Content-Type", "application/x-www-form-urlencoded") - f.handler.ServeHTTP(statusRecorder, request) + statusRecorder := requestStatus("https://rp.example") if statusRecorder.Code != http.StatusOK { t.Fatalf("JS status=%d body=%s", statusRecorder.Code, statusRecorder.Body.String()) } + if statusRecorder.Header().Get("Access-Control-Allow-Origin") != "https://rp.example" { + t.Fatalf("JS status CORS headers=%v", statusRecorder.Header()) + } var result map[string]string if err := json.Unmarshal(statusRecorder.Body.Bytes(), &result); err != nil { t.Fatal(err) @@ -660,6 +695,9 @@ func TestTelegramLoginJavaScriptIsCacheableAndConditional(t *testing.T) { if first.Code != http.StatusOK || first.Header().Get("ETag") == "" || !strings.Contains(first.Body.String(), "Telegram.Login") || !strings.Contains(first.Body.String(), "auth_result") || + !strings.Contains(first.Body.String(), "auth_pending") || + !strings.Contains(first.Body.String(), "pollFromParent") || + !strings.Contains(first.Body.String(), "/auth/status") || !strings.Contains(first.Body.String(), "oauth_supported") || !strings.Contains(first.Body.String(), "/inapp?") || !strings.Contains(first.Body.String(), "data-client-id") { diff --git a/internal/telegramloginhttp/sdk.go b/internal/telegramloginhttp/sdk.go index cadd2c00..d95e18ec 100644 --- a/internal/telegramloginhttp/sdk.go +++ b/internal/telegramloginhttp/sdk.go @@ -30,7 +30,8 @@ function normalize(options){ return {client_id:String(options.client_id),scope:scopes.join(' '),nonce:String(options.nonce||'').slice(0,1024),lang:String(options.lang||'').slice(0,16)}; } function randomURL(bytes){var data=new Uint8Array(bytes);crypto.getRandomValues(data);var raw='';data.forEach(function(value){raw+=String.fromCharCode(value);});return btoa(raw).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');} -function finish(flow,result){if(active!==flow){return;}active=null;if(flow.timer){clearInterval(flow.timer);}if(flow.listener){global.removeEventListener('message',flow.listener);}callback(flow.callback,result);} +function finish(flow,result){if(active!==flow){return;}active=null;if(flow.timer){clearInterval(flow.timer);}if(flow.pollTimer){clearTimeout(flow.pollTimer);}if(flow.listener){global.removeEventListener('message',flow.listener);}callback(flow.callback,result);} +async function pollFromParent(flow,token){if(active!==flow){return;}try{var body=new URLSearchParams({browser_token:token});var response=await fetch(provider+'/auth/status',{method:'POST',headers:{'content-type':'application/x-www-form-urlencoded'},body:body,credentials:'omit',cache:'no-store'});var data=await response.json();if(!response.ok){throw new Error(data.error||'request_failed');}if(data.status==='pending'){flow.pollTimer=setTimeout(function(){pollFromParent(flow,token);},1000);return;}finish(flow,data.id_token?build({result:data.id_token}):{error:data.error||data.status});}catch(error){finish(flow,{error:error.message||'login_failed'});}} function sendEvent(type,data){if(global.TelegramWebviewProxy&&typeof global.TelegramWebviewProxy.postEvent==='function'){global.TelegramWebviewProxy.postEvent(type,JSON.stringify(data||{}));}} async function receiveEvent(type,data){ if(type==='oauth_supported'){inApp=true;return;} @@ -44,7 +45,7 @@ async function receiveEvent(type,data){ function begin(options,cb,isNormalized){ var normalized;try{normalized=isNormalized?options:normalize(options);}catch(error){callback(cb,{error:error.message});return null;} if(active){callback(cb,{error:'login_in_progress'});return null;} - var flow={popup:null,callback:cb,listener:null,timer:null};active=flow; + var flow={popup:null,callback:cb,listener:null,timer:null,pollTimer:null,browserToken:''};active=flow; if(inApp){ if(inAppPending){finish(flow,{error:'login_in_progress'});return null;}inAppPending=true; var inAppParams=new URLSearchParams({scope:normalized.scope,origin:global.location.origin,client_id:normalized.client_id,response_type:'id_token'}); @@ -53,8 +54,8 @@ function begin(options,cb,isNormalized){ } var popup=global.open('about:blank','telegram-login-'+randomURL(8),'popup,width=550,height=650,resizable=yes,scrollbars=yes'); if(!popup){finish(flow,{error:'popup_blocked'});return null;}flow.popup=popup; - flow.listener=function(event){if(event.origin!==provider||event.source!==popup){return;}var data=event.data;try{if(typeof data==='string'){data=JSON.parse(data);}}catch(_){return;}if(!data||data.event!=='auth_result'){return;}finish(flow,build(data));}; - global.addEventListener('message',flow.listener);flow.timer=setInterval(function(){if(popup.closed){finish(flow,{error:'popup_closed'});}},500); + flow.listener=function(event){if(event.origin!==provider||event.source!==popup){return;}var data=event.data;try{if(typeof data==='string'){data=JSON.parse(data);}}catch(_){return;}if(!data){return;}if(data.event==='auth_pending'){var token=String(data.browser_token||'');if(!/^[A-Za-z0-9_-]{43}$/.test(token)){finish(flow,{error:'invalid_browser_token'});return;}if(flow.browserToken&&flow.browserToken!==token){finish(flow,{error:'invalid_browser_token'});return;}if(!flow.browserToken){flow.browserToken=token;pollFromParent(flow,token);}return;}if(data.event!=='auth_result'){return;}finish(flow,build(data));}; + global.addEventListener('message',flow.listener);flow.timer=setInterval(function(){if(popup.closed&&!flow.browserToken){finish(flow,{error:'popup_closed'});}},500); try{var params=new URLSearchParams({client_id:normalized.client_id,redirect_uri:global.location.origin+global.location.pathname,response_type:'post_message',scope:normalized.scope});if(normalized.nonce){params.set('nonce',normalized.nonce);}if(normalized.lang){params.set('lang',normalized.lang);}popup.location.replace(provider+'/auth?'+params.toString());} catch(error){try{popup.close();}catch(_){}finish(flow,{error:error.message||'login_failed'});} return popup; From bfdfe825f63eebb7678cb35c54d979c7401def13 Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 15:47:16 +0800 Subject: [PATCH 05/28] docs: sync Telegram Login demo validation notes --- cmd/bots/bedolagaformat/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/bots/bedolagaformat/README.md b/cmd/bots/bedolagaformat/README.md index 1eea2f53..5287ee5c 100644 --- a/cmd/bots/bedolagaformat/README.md +++ b/cmd/bots/bedolagaformat/README.md @@ -112,6 +112,11 @@ $env:TELESRV_BOT_LOGIN_LISTEN = "127.0.0.1:3000" Client Secret。省略 `TELESRV_BOT_LOGIN_CLIENT_SECRET` 时仍可验证 JS popup,但服务端 code flow 会明确禁用。 +移动 Chrome 会在跳入 Telegram/DrKLO 前让 popup 把短期 browser token 交给原 RP +标签;父标签随后以精确登记 origin 轮询 `/auth/status`。因此外部 app round-trip +关闭 popup 或丢失 `window.opener` 时,原标签仍能完成 JWKS 验签。不要给 +`/auth/status` 配 wildcard CORS,也不要在 RP 中记录 browser token 或 ID token。 + demo 的 flow/state/nonce 只保存在单进程内存中,带 10 分钟过期和 256 条上限,专用于 本地与 testserver 端到端验证,不是生产 relying-party 实现。官方 iOS/Android SDK 目前把 `https://oauth.telegram.org` 写死;验证自建 issuer 时需使用项目记录的最小 From d69a34a4a8bef60a0b054d7acc45631d2483a2f7 Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 15:47:55 +0800 Subject: [PATCH 06/28] fix: sync StarGift lifecycle hardening --- ..._gift_user_refs_and_profile_state.down.sql | 6 + ...ar_gift_user_refs_and_profile_state.up.sql | 127 +++++ ..._validate_star_gift_profile_state.down.sql | 1 + ...27_validate_star_gift_profile_state.up.sql | 5 + .../0128_star_gift_craft_readiness.down.sql | 38 ++ .../0128_star_gift_craft_readiness.up.sql | 498 ++++++++++++++++++ ...29_star_gift_craft_output_receipt.down.sql | 4 + ...0129_star_gift_craft_output_receipt.up.sql | 65 +++ internal/app/help/service.go | 4 +- internal/app/help/service_premium_test.go | 1 + internal/domain/star_gift.go | 3 + internal/rpc/convert_messages.go | 5 +- internal/rpc/payments_star_gift_lifecycle.go | 4 +- internal/rpc/payments_star_gifts.go | 21 + internal/rpc/payments_star_gifts_rpc_test.go | 165 +++++- internal/store/memory/star_gift.go | 18 + .../memory/star_gift_profile_order_test.go | 18 + internal/store/postgres/star_gift.go | 54 +- .../store/postgres/star_gift_collectibles.go | 8 +- ...star_gift_collectibles_integration_test.go | 19 +- .../store/postgres/star_gift_craft_auction.go | 184 +++++-- .../postgres/star_gift_integration_test.go | 20 + .../store/postgres/star_gift_lifecycle.go | 73 ++- .../star_gift_lifecycle_integration_test.go | 301 ++++++++++- ...ft_lifecycle_migration_integration_test.go | 4 +- .../star_gift_lifecycle_projection.go | 196 +++++++ .../postgres/star_gift_lifecycle_test.go | 25 +- .../postgres/star_gift_private_projection.go | 28 +- internal/store/postgres/star_gift_upgrade.go | 61 ++- .../postgres/star_gift_user_message_ref.go | 59 +++ 30 files changed, 1883 insertions(+), 132 deletions(-) create mode 100644 deploy/migrations/0126_star_gift_user_refs_and_profile_state.down.sql create mode 100644 deploy/migrations/0126_star_gift_user_refs_and_profile_state.up.sql create mode 100644 deploy/migrations/0127_validate_star_gift_profile_state.down.sql create mode 100644 deploy/migrations/0127_validate_star_gift_profile_state.up.sql create mode 100644 deploy/migrations/0128_star_gift_craft_readiness.down.sql create mode 100644 deploy/migrations/0128_star_gift_craft_readiness.up.sql create mode 100644 deploy/migrations/0129_star_gift_craft_output_receipt.down.sql create mode 100644 deploy/migrations/0129_star_gift_craft_output_receipt.up.sql create mode 100644 internal/store/postgres/star_gift_lifecycle_projection.go create mode 100644 internal/store/postgres/star_gift_user_message_ref.go diff --git a/deploy/migrations/0126_star_gift_user_refs_and_profile_state.down.sql b/deploy/migrations/0126_star_gift_user_refs_and_profile_state.down.sql new file mode 100644 index 00000000..0db8ee07 --- /dev/null +++ b/deploy/migrations/0126_star_gift_user_refs_and_profile_state.down.sql @@ -0,0 +1,6 @@ +-- Durable edit events and repaired user gift projections are intentionally not +-- rewound. Dropping the new lookup/index constraints is sufficient rollback. +ALTER TABLE peer_star_gifts + DROP CONSTRAINT IF EXISTS peer_star_gifts_hidden_unpinned_check; + +DROP TABLE IF EXISTS star_gift_user_message_refs; diff --git a/deploy/migrations/0126_star_gift_user_refs_and_profile_state.up.sql b/deploy/migrations/0126_star_gift_user_refs_and_profile_state.up.sql new file mode 100644 index 00000000..393ece1c --- /dev/null +++ b/deploy/migrations/0126_star_gift_user_refs_and_profile_state.up.sql @@ -0,0 +1,127 @@ +-- Official clients may continue lifecycle actions from a freshly emitted +-- messageActionStarGiftUnique. Keep those user-local message ids as explicit +-- durable references to the same saved gift aggregate. +CREATE TABLE star_gift_user_message_refs ( + owner_user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, + msg_id integer NOT NULL, + saved_gift_id bigint NOT NULL REFERENCES peer_star_gifts(id) ON DELETE CASCADE, + created_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (owner_user_id, msg_id), + CONSTRAINT star_gift_user_message_refs_msg_check CHECK (owner_user_id > 0 AND msg_id > 0) +); + +CREATE INDEX star_gift_user_message_refs_saved_idx + ON star_gift_user_message_refs(saved_gift_id, owner_user_id, msg_id); + +INSERT INTO star_gift_user_message_refs(owner_user_id,msg_id,saved_gift_id) +SELECT box.owner_user_id, box.box_id, gift.id +FROM message_boxes box +JOIN unique_star_gifts unique_gift + ON (box.media #>> '{service_action,star_gift_unique,gift,ID}') ~ '^[0-9]+$' + AND unique_gift.id = (box.media #>> '{service_action,star_gift_unique,gift,ID}')::bigint +JOIN peer_star_gifts gift + ON gift.id = unique_gift.source_saved_gift_id + AND gift.owner_peer_type = 'user' + AND gift.owner_peer_id = box.owner_user_id +WHERE NOT box.deleted + AND box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND box.box_id <> gift.msg_id; + +-- Hidden gifts cannot remain pinned. Compacting the whole owner vector here +-- also repairs historical gaps before the invariant is constrained. +ALTER TABLE peer_star_gifts + ADD CONSTRAINT peer_star_gifts_hidden_unpinned_check + CHECK (pinned_order<=6 AND (NOT unsaved OR pinned_order=0)) NOT VALID; + +CREATE TEMP TABLE star_gift_pin_repairs ON COMMIT DROP AS +SELECT id,new_order +FROM ( + SELECT id, + row_number() OVER (PARTITION BY owner_peer_type,owner_peer_id + ORDER BY pinned_order,id)::integer AS new_order + FROM peer_star_gifts + WHERE lifecycle_status='active' AND NOT unsaved AND pinned_order>0 +) ranked +WHERE new_order<=6; + +UPDATE peer_star_gifts SET pinned_order=0 WHERE pinned_order<>0; + +UPDATE peer_star_gifts gift +SET pinned_order=repair.new_order +FROM star_gift_pin_repairs repair +WHERE gift.id=repair.id; + +-- peer and saved_id share one TL flag and are channel-only. Earlier user gift +-- projections set peer=user (and sometimes a user box id in saved_id), which +-- made TDesktop select zero/stale ids instead of the emitted service message. +CREATE TEMP TABLE star_gift_user_unique_media_repairs ( + owner_user_id bigint NOT NULL, + box_id integer NOT NULL, + peer_type text NOT NULL, + peer_id bigint NOT NULL, + repaired_media jsonb NOT NULL, + PRIMARY KEY(owner_user_id,box_id) +) ON COMMIT DROP; + +INSERT INTO star_gift_user_unique_media_repairs(owner_user_id,box_id,peer_type,peer_id,repaired_media) +SELECT box.owner_user_id, + box.box_id, + box.peer_type, + box.peer_id, + jsonb_set( + box.media #- '{service_action,star_gift_unique,saved_id}', + '{service_action,star_gift_unique,peer}', + '{"ID":0,"Type":""}'::jsonb, + true + ) +FROM message_boxes box +WHERE NOT box.deleted + AND box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND box.media #>> '{service_action,star_gift_unique,peer,Type}' = 'user'; + +DO $$ +DECLARE + repair record; + next_pts integer; + event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer; +BEGIN + FOR repair IN + SELECT owner_user_id,box_id,peer_type,peer_id,repaired_media + FROM star_gift_user_unique_media_repairs + ORDER BY owner_user_id,box_id + LOOP + INSERT INTO user_update_watermarks(user_id,contiguous_pts) + VALUES(repair.owner_user_id,0) + ON CONFLICT(user_id) DO NOTHING; + + UPDATE user_update_watermarks + SET contiguous_pts=contiguous_pts+1,updated_at=now() + WHERE user_id=repair.owner_user_id + RETURNING contiguous_pts INTO next_pts; + + UPDATE message_boxes + SET media=repair.repaired_media,pts=next_pts + WHERE owner_user_id=repair.owner_user_id AND box_id=repair.box_id AND NOT deleted; + + INSERT INTO user_update_events( + user_id,pts,pts_count,date,event_type,message_box_id,peer_type,peer_id + ) VALUES( + repair.owner_user_id,next_pts,1,event_date,'edit_message',repair.box_id,repair.peer_type,repair.peer_id + ); + + INSERT INTO dispatch_outbox( + target_user_id,pts,event_type,exclude_auth_key_id,exclude_session_id + ) VALUES(repair.owner_user_id,next_pts,'edit_message',0,0); + END LOOP; +END +$$; + +UPDATE private_messages +SET media=jsonb_set( + media #- '{service_action,star_gift_unique,saved_id}', + '{service_action,star_gift_unique,peer}', + '{"ID":0,"Type":""}'::jsonb, + true +) +WHERE media #>> '{service_action,kind}' = 'star_gift_unique' + AND media #>> '{service_action,star_gift_unique,peer,Type}' = 'user'; diff --git a/deploy/migrations/0127_validate_star_gift_profile_state.down.sql b/deploy/migrations/0127_validate_star_gift_profile_state.down.sql new file mode 100644 index 00000000..6c702f48 --- /dev/null +++ b/deploy/migrations/0127_validate_star_gift_profile_state.down.sql @@ -0,0 +1 @@ +-- Validation changes no data and the constraint belongs to migration 0126. diff --git a/deploy/migrations/0127_validate_star_gift_profile_state.up.sql b/deploy/migrations/0127_validate_star_gift_profile_state.up.sql new file mode 100644 index 00000000..dfa9ef6d --- /dev/null +++ b/deploy/migrations/0127_validate_star_gift_profile_state.up.sql @@ -0,0 +1,5 @@ +-- 0126 repaired historical rows and installed the constraint as NOT VALID so +-- it could coexist with the deferrable unique-gift owner trigger in one +-- migration transaction. Validate after that transaction has committed. +ALTER TABLE peer_star_gifts + VALIDATE CONSTRAINT peer_star_gifts_hidden_unpinned_check; diff --git a/deploy/migrations/0128_star_gift_craft_readiness.down.sql b/deploy/migrations/0128_star_gift_craft_readiness.down.sql new file mode 100644 index 00000000..f8007feb --- /dev/null +++ b/deploy/migrations/0128_star_gift_craft_readiness.down.sql @@ -0,0 +1,38 @@ +-- Aggregate/message repairs and emitted edit events are authoritative business +-- history and are intentionally not reversed. Restore only the pre-0128 +-- deferred owner guard shape. +CREATE OR REPLACE FUNCTION public.telesrv_check_unique_star_gift_owner() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE + unique_id bigint; + gift_owner_type text; + gift_owner_id bigint; + gift_owner_address text; + gift_burned boolean; + saved_status text; + saved_owner_type text; + saved_owner_id bigint; +BEGIN + IF TG_TABLE_NAME = 'unique_star_gifts' THEN + unique_id := COALESCE(NEW.id, OLD.id); + ELSE + unique_id := COALESCE(NEW.unique_gift_id, OLD.unique_gift_id); + END IF; + IF unique_id IS NULL THEN RETURN NULL; END IF; + SELECT owner_peer_type, owner_peer_id, owner_address, burned + INTO gift_owner_type, gift_owner_id, gift_owner_address, gift_burned + FROM public.unique_star_gifts WHERE id=unique_id; + IF NOT FOUND THEN RETURN NULL; END IF; + SELECT lifecycle_status, owner_peer_type, owner_peer_id + INTO saved_status, saved_owner_type, saved_owner_id + FROM public.peer_star_gifts WHERE unique_gift_id=unique_id; + IF NOT FOUND THEN RAISE EXCEPTION 'unique star gift missing saved aggregate'; END IF; + IF gift_burned THEN + IF saved_status <> 'burned' THEN RAISE EXCEPTION 'burned unique star gift has live saved aggregate'; END IF; + ELSIF gift_owner_address <> '' THEN + IF saved_status <> 'exported' THEN RAISE EXCEPTION 'exported unique star gift has non-exported saved aggregate'; END IF; + ELSIF saved_status <> 'active' OR gift_owner_type IS DISTINCT FROM saved_owner_type OR gift_owner_id IS DISTINCT FROM saved_owner_id THEN + RAISE EXCEPTION 'unique star gift owner mismatch'; + END IF; + RETURN NULL; +END; +$$; diff --git a/deploy/migrations/0128_star_gift_craft_readiness.up.sql b/deploy/migrations/0128_star_gift_craft_readiness.up.sql new file mode 100644 index 00000000..19af1d49 --- /dev/null +++ b/deploy/migrations/0128_star_gift_craft_readiness.up.sql @@ -0,0 +1,498 @@ +-- Official Android clients use a positive can_craft_at both as the Craft +-- capability marker and as the readiness boundary. Earlier zero-delay +-- upgrades persisted 0 while retaining a positive craft chance, so TDesktop +-- could Craft the gift but Android hid the entry entirely. +-- +-- Block concurrent lifecycle/message writers while aggregate facts, message +-- snapshots and durable edit edges are repaired in this migration transaction. +LOCK TABLE public.peer_star_gifts, public.unique_star_gifts, + public.message_boxes, public.private_messages IN SHARE ROW EXCLUSIVE MODE; + +DO $$ +BEGIN + -- Craft capability remains an intrinsic collectible fact while ownership + -- moves between users and channels. Terminal/external states cannot Craft. + UPDATE public.unique_star_gifts unique_gift + SET craft_chance_permille = 0, + updated_at = now() + FROM public.peer_star_gifts saved_gift + WHERE saved_gift.unique_gift_id = unique_gift.id + AND unique_gift.craft_chance_permille > 0 + AND ( + saved_gift.lifecycle_status <> 'active' + OR unique_gift.owner_address <> '' + OR unique_gift.burned + OR unique_gift.crafted + ); + + UPDATE public.peer_star_gifts saved_gift + SET can_craft_at = 0 + FROM public.unique_star_gifts unique_gift + WHERE unique_gift.id = saved_gift.unique_gift_id + AND unique_gift.craft_chance_permille = 0 + AND saved_gift.can_craft_at <> 0; + + IF EXISTS ( + SELECT 1 + FROM public.unique_star_gifts unique_gift + JOIN public.peer_star_gifts saved_gift + ON saved_gift.unique_gift_id = unique_gift.id + WHERE unique_gift.craft_chance_permille > 0 + AND ( + saved_gift.owner_peer_type NOT IN ('user', 'channel') + OR saved_gift.lifecycle_status <> 'active' + OR unique_gift.owner_address <> '' + OR unique_gift.burned + OR unique_gift.crafted + OR NOT EXISTS ( + SELECT 1 + FROM public.star_gift_collectible_models model + WHERE model.collectible_revision_id = unique_gift.collectible_revision_id + AND model.crafted + ) + ) + ) THEN + RAISE EXCEPTION 'positive star gift craft chance has no valid owned aggregate'; + END IF; + + -- created_at is the stable persisted proxy for the original upgrade + -- transaction date on legacy rows. New writes use the exact request date. + UPDATE public.peer_star_gifts saved_gift + SET can_craft_at = GREATEST( + 1, + LEAST(2147483647, FLOOR(EXTRACT(EPOCH FROM unique_gift.created_at))::bigint)::integer + ) + FROM public.unique_star_gifts unique_gift + WHERE unique_gift.id = saved_gift.unique_gift_id + AND unique_gift.craft_chance_permille > 0 + AND saved_gift.can_craft_at = 0; + + IF EXISTS ( + SELECT 1 + FROM public.peer_star_gifts saved_gift + JOIN public.unique_star_gifts unique_gift + ON unique_gift.id = saved_gift.unique_gift_id + WHERE (unique_gift.craft_chance_permille > 0) + IS DISTINCT FROM (saved_gift.can_craft_at > 0) + ) THEN + RAISE EXCEPTION 'star gift craft chance/readiness repair did not converge'; + END IF; +END +$$; + +CREATE TEMP TABLE star_gift_craft_message_repairs ( + owner_user_id bigint NOT NULL, + box_id integer NOT NULL, + unique_gift_id bigint NOT NULL, + desired_craft_chance integer NOT NULL, + desired_can_craft_at integer NOT NULL, + PRIMARY KEY (owner_user_id, box_id) +) ON COMMIT DROP; + +-- Adding capability is owner-scoped: repair only the current owner's +-- authoritative unique action (upgrade_msg_id) and the other visible box of +-- that same logical private message. Never add Craft back to an old owner's +-- historical transfer/resale action. +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM public.peer_star_gifts saved_gift + JOIN public.unique_star_gifts unique_gift + ON unique_gift.id = saved_gift.unique_gift_id + WHERE saved_gift.owner_peer_type = 'user' + AND saved_gift.lifecycle_status = 'active' + AND saved_gift.can_craft_at > 0 + AND NOT EXISTS ( + SELECT 1 + FROM public.message_boxes owner_box + WHERE owner_box.owner_user_id = saved_gift.owner_peer_id + AND owner_box.box_id = saved_gift.upgrade_msg_id + AND NOT owner_box.deleted + AND owner_box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND owner_box.media #>> '{service_action,star_gift_unique,gift,ID}' = unique_gift.id::text + ) + ) THEN + RAISE EXCEPTION 'craftable star gift is missing its current owner action'; + END IF; + + IF EXISTS ( + SELECT 1 + FROM public.message_boxes box + WHERE box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND box.media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL + AND ( + jsonb_typeof(box.media #> '{service_action,star_gift_unique,can_craft_at}') <> 'number' + OR COALESCE(box.media #>> '{service_action,star_gift_unique,can_craft_at}', '') !~ '^[0-9]+$' + OR (box.media #>> '{service_action,star_gift_unique,can_craft_at}')::numeric > 2147483647 + ) + ) THEN + RAISE EXCEPTION 'star gift message has malformed can_craft_at'; + END IF; + + IF EXISTS ( + SELECT 1 + FROM public.message_boxes box + WHERE box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND box.media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL + AND ( + jsonb_typeof(box.media #> '{service_action,star_gift_unique,gift,CraftChancePermille}') <> 'number' + OR COALESCE(box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}', '') !~ '^[0-9]+$' + OR (box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}')::numeric > 1000 + ) + ) THEN + RAISE EXCEPTION 'star gift message has malformed craft chance'; + END IF; +END +$$; + +INSERT INTO star_gift_craft_message_repairs( + owner_user_id, box_id, unique_gift_id, + desired_craft_chance, desired_can_craft_at +) +SELECT visible_box.owner_user_id, + visible_box.box_id, + unique_gift.id, + unique_gift.craft_chance_permille, + saved_gift.can_craft_at +FROM public.peer_star_gifts saved_gift +JOIN public.unique_star_gifts unique_gift + ON unique_gift.id = saved_gift.unique_gift_id +JOIN public.message_boxes owner_box + ON owner_box.owner_user_id = saved_gift.owner_peer_id + AND owner_box.box_id = saved_gift.upgrade_msg_id + AND NOT owner_box.deleted + AND owner_box.media #>> '{service_action,star_gift_unique,gift,ID}' = unique_gift.id::text +JOIN public.message_boxes visible_box + ON visible_box.message_sender_id = owner_box.message_sender_id + AND visible_box.private_message_id = owner_box.private_message_id + AND NOT visible_box.deleted +WHERE saved_gift.owner_peer_type = 'user' + AND saved_gift.lifecycle_status = 'active' + AND saved_gift.can_craft_at > 0 + AND ( + COALESCE(NULLIF(visible_box.media #>> '{service_action,star_gift_unique,can_craft_at}', '')::integer, 0) + IS DISTINCT FROM saved_gift.can_craft_at + OR COALESCE(NULLIF(visible_box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}', '')::integer, 0) + IS DISTINCT FROM unique_gift.craft_chance_permille + ); + +-- Only the current owner's authoritative logical message may expose Craft. +-- Terminal gifts, channel-owned gifts (until channel Craft is implemented), +-- and old-owner historical actions must have both wire markers removed. +INSERT INTO star_gift_craft_message_repairs( + owner_user_id, box_id, unique_gift_id, + desired_craft_chance, desired_can_craft_at +) +SELECT box.owner_user_id, box.box_id, unique_gift.id, 0, 0 +FROM public.message_boxes box +JOIN public.unique_star_gifts unique_gift + ON (box.media #>> '{service_action,star_gift_unique,gift,ID}') ~ '^[0-9]+$' + AND unique_gift.id = (box.media #>> '{service_action,star_gift_unique,gift,ID}')::bigint +JOIN public.peer_star_gifts saved_gift + ON saved_gift.unique_gift_id = unique_gift.id +WHERE NOT box.deleted + AND box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND ( + box.media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL + OR box.media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL + ) + AND NOT EXISTS ( + SELECT 1 + FROM public.message_boxes authority + WHERE saved_gift.owner_peer_type = 'user' + AND saved_gift.lifecycle_status = 'active' + AND saved_gift.can_craft_at > 0 + AND unique_gift.craft_chance_permille > 0 + AND authority.owner_user_id = saved_gift.owner_peer_id + AND authority.box_id = saved_gift.upgrade_msg_id + AND NOT authority.deleted + AND authority.media #>> '{service_action,kind}' = 'star_gift_unique' + AND authority.media #>> '{service_action,star_gift_unique,gift,ID}' = unique_gift.id::text + AND authority.message_sender_id = box.message_sender_id + AND authority.private_message_id = box.private_message_id + ) +ON CONFLICT (owner_user_id, box_id) DO UPDATE +SET unique_gift_id = EXCLUDED.unique_gift_id, + desired_craft_chance = 0, + desired_can_craft_at = 0; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM star_gift_craft_message_repairs target + JOIN public.message_boxes box + ON box.owner_user_id = target.owner_user_id + AND box.box_id = target.box_id + WHERE box.deleted + OR box.media #>> '{service_action,kind}' <> 'star_gift_unique' + OR box.media #>> '{service_action,star_gift_unique,gift,ID}' <> target.unique_gift_id::text + ) THEN + RAISE EXCEPTION 'craft readiness repair target is not the expected unique gift action'; + END IF; + + IF EXISTS ( + SELECT 1 + FROM star_gift_craft_message_repairs target + JOIN public.message_boxes box + ON box.owner_user_id = target.owner_user_id + AND box.box_id = target.box_id + WHERE NOT EXISTS ( + SELECT 1 + FROM public.private_messages private_message + WHERE private_message.sender_user_id = box.message_sender_id + AND private_message.id = box.private_message_id + AND private_message.media #>> '{service_action,kind}' = 'star_gift_unique' + AND private_message.media #>> '{service_action,star_gift_unique,gift,ID}' = target.unique_gift_id::text + ) + ) THEN + RAISE EXCEPTION 'craft readiness repair target has no matching private message'; + END IF; + + IF EXISTS ( + SELECT 1 + FROM star_gift_craft_message_repairs target + JOIN public.message_boxes box + ON box.owner_user_id = target.owner_user_id + AND box.box_id = target.box_id + GROUP BY box.message_sender_id, box.private_message_id + HAVING COUNT(DISTINCT ( + target.unique_gift_id, + target.desired_craft_chance, + target.desired_can_craft_at + )) <> 1 + ) THEN + RAISE EXCEPTION 'craft readiness repair has conflicting logical message targets'; + END IF; +END +$$; + +DO $$ +DECLARE + repair record; + next_pts integer; + event_date integer := LEAST(2147483647, EXTRACT(EPOCH FROM clock_timestamp())::bigint)::integer; + repaired_media jsonb; + repaired_private_media jsonb; + affected_rows bigint; +BEGIN + FOR repair IN + SELECT target.owner_user_id, + target.box_id, + target.unique_gift_id, + target.desired_craft_chance, + target.desired_can_craft_at, + box.peer_type, + box.peer_id, + box.message_sender_id, + box.private_message_id, + box.media + FROM star_gift_craft_message_repairs target + JOIN public.message_boxes box + ON box.owner_user_id = target.owner_user_id + AND box.box_id = target.box_id + AND NOT box.deleted + ORDER BY target.owner_user_id, target.box_id + FOR UPDATE OF box + LOOP + IF repair.desired_craft_chance > 0 THEN + repaired_media := jsonb_set( + jsonb_set( + repair.media, + '{service_action,star_gift_unique,gift,CraftChancePermille}', + to_jsonb(repair.desired_craft_chance), + true + ), + '{service_action,star_gift_unique,can_craft_at}', + to_jsonb(repair.desired_can_craft_at), + true + ); + ELSE + repaired_media := repair.media + #- '{service_action,star_gift_unique,can_craft_at}' + #- '{service_action,star_gift_unique,gift,CraftChancePermille}'; + END IF; + + IF repaired_media #>> '{service_action,kind}' <> 'star_gift_unique' + OR repaired_media #>> '{service_action,star_gift_unique,gift,ID}' <> repair.unique_gift_id::text + OR ( + repair.desired_craft_chance > 0 + AND ( + repaired_media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}' + IS DISTINCT FROM repair.desired_craft_chance::text + OR repaired_media #>> '{service_action,star_gift_unique,can_craft_at}' + IS DISTINCT FROM repair.desired_can_craft_at::text + ) + ) + OR ( + repair.desired_craft_chance = 0 + AND ( + repaired_media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL + OR repaired_media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL + ) + ) THEN + RAISE EXCEPTION 'craft readiness repair cannot project message box for user %, box %', + repair.owner_user_id, repair.box_id; + END IF; + + INSERT INTO public.user_update_watermarks (user_id, contiguous_pts) + VALUES (repair.owner_user_id, 0) + ON CONFLICT (user_id) DO NOTHING; + + UPDATE public.user_update_watermarks + SET contiguous_pts = contiguous_pts + 1, + updated_at = now() + WHERE user_id = repair.owner_user_id + RETURNING contiguous_pts INTO next_pts; + + UPDATE public.message_boxes + SET media = repaired_media, + pts = next_pts + WHERE owner_user_id = repair.owner_user_id + AND box_id = repair.box_id + AND NOT deleted; + GET DIAGNOSTICS affected_rows = ROW_COUNT; + IF affected_rows <> 1 THEN + RAISE EXCEPTION 'craft readiness repair lost user %, box %', repair.owner_user_id, repair.box_id; + END IF; + + SELECT media + INTO repaired_private_media + FROM public.private_messages + WHERE sender_user_id = repair.message_sender_id + AND id = repair.private_message_id + FOR UPDATE; + IF NOT FOUND THEN + RAISE EXCEPTION 'craft readiness repair missing private message for user %, box %', + repair.owner_user_id, repair.box_id; + END IF; + + IF repaired_private_media #>> '{service_action,kind}' <> 'star_gift_unique' + OR repaired_private_media #>> '{service_action,star_gift_unique,gift,ID}' <> repair.unique_gift_id::text THEN + RAISE EXCEPTION 'craft readiness repair found mismatched private message for user %, box %', + repair.owner_user_id, repair.box_id; + END IF; + + IF repair.desired_craft_chance > 0 THEN + repaired_private_media := jsonb_set( + jsonb_set( + repaired_private_media, + '{service_action,star_gift_unique,gift,CraftChancePermille}', + to_jsonb(repair.desired_craft_chance), + true + ), + '{service_action,star_gift_unique,can_craft_at}', + to_jsonb(repair.desired_can_craft_at), + true + ); + IF repaired_private_media #>> '{service_action,star_gift_unique,can_craft_at}' + IS DISTINCT FROM repair.desired_can_craft_at::text + OR repaired_private_media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}' + IS DISTINCT FROM repair.desired_craft_chance::text THEN + RAISE EXCEPTION 'craft readiness repair cannot project private message for user %, box %', + repair.owner_user_id, repair.box_id; + END IF; + ELSE + repaired_private_media := repaired_private_media + #- '{service_action,star_gift_unique,can_craft_at}' + #- '{service_action,star_gift_unique,gift,CraftChancePermille}'; + IF repaired_private_media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL + OR repaired_private_media #> '{service_action,star_gift_unique,gift,CraftChancePermille}' IS NOT NULL THEN + RAISE EXCEPTION 'craft readiness repair cannot clear private message for user %, box %', + repair.owner_user_id, repair.box_id; + END IF; + END IF; + + UPDATE public.private_messages + SET media = repaired_private_media + WHERE sender_user_id = repair.message_sender_id + AND id = repair.private_message_id; + GET DIAGNOSTICS affected_rows = ROW_COUNT; + IF affected_rows <> 1 THEN + RAISE EXCEPTION 'craft readiness repair lost private message for user %, box %', + repair.owner_user_id, repair.box_id; + END IF; + + INSERT INTO public.user_update_events ( + user_id, pts, pts_count, date, event_type, + message_box_id, peer_type, peer_id + ) VALUES ( + repair.owner_user_id, next_pts, 1, event_date, 'edit_message', + repair.box_id, repair.peer_type, repair.peer_id + ); + + INSERT INTO public.dispatch_outbox ( + target_user_id, pts, event_type, + exclude_auth_key_id, exclude_session_id + ) VALUES ( + repair.owner_user_id, next_pts, 'edit_message', 0, 0 + ); + END LOOP; +END +$$; + +-- Extend the existing deferred unique/saved aggregate guard. Upgrade, Craft +-- and export update the two tables in separate statements, so commit-time +-- validation observes the final atomic state without a read fallback. +CREATE OR REPLACE FUNCTION public.telesrv_check_unique_star_gift_owner() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE + unique_id bigint; + gift_owner_type text; + gift_owner_id bigint; + gift_owner_address text; + gift_burned boolean; + gift_crafted boolean; + gift_craft_chance integer; + gift_revision_id bigint; + saved_status text; + saved_owner_type text; + saved_owner_id bigint; + saved_can_craft_at integer; +BEGIN + IF TG_TABLE_NAME = 'unique_star_gifts' THEN + unique_id := COALESCE(NEW.id, OLD.id); + ELSE + unique_id := COALESCE(NEW.unique_gift_id, OLD.unique_gift_id); + END IF; + IF unique_id IS NULL THEN RETURN NULL; END IF; + SELECT owner_peer_type, owner_peer_id, owner_address, burned, crafted, + craft_chance_permille, collectible_revision_id + INTO gift_owner_type, gift_owner_id, gift_owner_address, gift_burned, gift_crafted, + gift_craft_chance, gift_revision_id + FROM public.unique_star_gifts WHERE id=unique_id; + IF NOT FOUND THEN RETURN NULL; END IF; + SELECT lifecycle_status, owner_peer_type, owner_peer_id, can_craft_at + INTO saved_status, saved_owner_type, saved_owner_id, saved_can_craft_at + FROM public.peer_star_gifts WHERE unique_gift_id=unique_id; + IF NOT FOUND THEN RAISE EXCEPTION 'unique star gift missing saved aggregate'; END IF; + IF gift_burned THEN + IF saved_status <> 'burned' THEN RAISE EXCEPTION 'burned unique star gift has live saved aggregate'; END IF; + ELSIF gift_owner_address <> '' THEN + IF saved_status <> 'exported' THEN RAISE EXCEPTION 'exported unique star gift has non-exported saved aggregate'; END IF; + ELSIF saved_status <> 'active' OR gift_owner_type IS DISTINCT FROM saved_owner_type OR gift_owner_id IS DISTINCT FROM saved_owner_id THEN + RAISE EXCEPTION 'unique star gift owner mismatch'; + END IF; + IF gift_craft_chance > 0 THEN + IF saved_can_craft_at <= 0 + OR saved_status <> 'active' + OR saved_owner_type NOT IN ('user', 'channel') + OR gift_owner_address <> '' + OR gift_burned + OR gift_crafted THEN + RAISE EXCEPTION 'unique star gift craft capability has invalid aggregate state'; + END IF; + IF NOT EXISTS ( + SELECT 1 + FROM public.star_gift_collectible_models model + WHERE model.collectible_revision_id = gift_revision_id + AND model.crafted + ) THEN + RAISE EXCEPTION 'unique star gift craft chance has no crafted model'; + END IF; + ELSIF saved_can_craft_at <> 0 THEN + RAISE EXCEPTION 'unique star gift readiness exists without craft chance'; + END IF; + RETURN NULL; +END; +$$; diff --git a/deploy/migrations/0129_star_gift_craft_output_receipt.down.sql b/deploy/migrations/0129_star_gift_craft_output_receipt.down.sql new file mode 100644 index 00000000..62e0f108 --- /dev/null +++ b/deploy/migrations/0129_star_gift_craft_output_receipt.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE public.star_gift_craft_commands + DROP CONSTRAINT IF EXISTS star_gift_craft_output_receipt_check, + DROP COLUMN IF EXISTS output_fingerprint, + DROP COLUMN IF EXISTS output_media; diff --git a/deploy/migrations/0129_star_gift_craft_output_receipt.up.sql b/deploy/migrations/0129_star_gift_craft_output_receipt.up.sql new file mode 100644 index 00000000..5fd8c420 --- /dev/null +++ b/deploy/migrations/0129_star_gift_craft_output_receipt.up.sql @@ -0,0 +1,65 @@ +-- A successful Craft outcome and its self-service message are separated by a +-- process boundary. Freeze the exact output intent in the outcome receipt so +-- retries never rebuild a different message from mutable gift/profile state. +ALTER TABLE public.star_gift_craft_commands + ADD COLUMN output_media jsonb, + ADD COLUMN output_fingerprint bytea; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM public.star_gift_craft_commands command + WHERE command.success + AND 1 <> ( + SELECT COUNT(*) + FROM public.private_messages message + WHERE message.sender_user_id = command.user_id + AND message.recipient_user_id = command.user_id + AND message.sender_snapshot #>> '{message,Media,service_action,kind}' = 'star_gift_unique' + AND message.sender_snapshot #>> '{message,Media,service_action,star_gift_unique,gift,ID}' = command.result_unique_gift_id::text + AND COALESCE((message.sender_snapshot #>> '{message,Media,service_action,star_gift_unique,craft}')::boolean, false) + AND octet_length(message.request_fingerprint) = 32 + ) + ) THEN + RAISE EXCEPTION 'successful craft command is missing its exact immutable output receipt'; + END IF; +END +$$; + +WITH outputs AS ( + SELECT command.user_id, + command.command_key, + message.sender_snapshot #> '{message,Media}' AS media, + message.request_fingerprint + FROM public.star_gift_craft_commands command + JOIN public.private_messages message + ON message.sender_user_id = command.user_id + AND message.recipient_user_id = command.user_id + AND message.sender_snapshot #>> '{message,Media,service_action,kind}' = 'star_gift_unique' + AND message.sender_snapshot #>> '{message,Media,service_action,star_gift_unique,gift,ID}' = command.result_unique_gift_id::text + AND COALESCE((message.sender_snapshot #>> '{message,Media,service_action,star_gift_unique,craft}')::boolean, false) + WHERE command.success +) +UPDATE public.star_gift_craft_commands command +SET output_media = output.media, + output_fingerprint = output.request_fingerprint +FROM outputs output +WHERE command.user_id = output.user_id + AND command.command_key = output.command_key; + +ALTER TABLE public.star_gift_craft_commands + ADD CONSTRAINT star_gift_craft_output_receipt_check CHECK ( + (success + AND result_unique_gift_id IS NOT NULL + AND output_media IS NOT NULL + AND output_media #>> '{service_action,kind}' = 'star_gift_unique' + AND COALESCE((output_media #>> '{service_action,star_gift_unique,craft}')::boolean, false) + AND output_media #>> '{service_action,star_gift_unique,gift,ID}' = result_unique_gift_id::text + AND octet_length(output_fingerprint) = 32) + OR + (NOT success + AND result_unique_gift_id IS NULL + AND output_media IS NULL + AND output_fingerprint IS NULL) + ); diff --git a/internal/app/help/service.go b/internal/app/help/service.go index 09b2c3e2..b2d64492 100644 --- a/internal/app/help/service.go +++ b/internal/app/help/service.go @@ -55,9 +55,9 @@ const tdesktopClient = "tdesktop" // // WebK directly calls Array.some on fragment_prefixes while rendering user profiles, // so this compatibility key must always remain an array, even when it is empty. -const tdesktopDefaultAppConfigBase = `{"chat_read_mark_expire_period":604800,"chat_read_mark_size_threshold":50,"pm_read_date_expire_period":604800,"quote_length_max":1024,"telegram_antispam_group_size_min":200,"telegram_antispam_user_id":"5434988373","fragment_prefixes":["888"],"forum_upgrade_participants_min":2,"reactions_default":{"_":"reactionEmoji","emoticon":"👍"},"reactions_uniq_max":11,"reactions_user_max_default":1,"reactions_user_max_premium":3,"reactions_in_chat_max":3,"boosts_channel_level_max":100,"rich_message_posting":"enabled","upload_markup_video":true,"emojies_send_dice":["🎲","🎯","🏀","⚽","⚽️","🎳","🎰"],"premium_purchase_blocked":false,"stars_purchase_blocked":false,"stargifts_blocked":false,"stories_stealth_future_period":1500,"stories_stealth_past_period":300,"stories_stealth_cooldown_period":10800,"quick_replies_limit":100,"quick_reply_messages_limit":20,"business_chat_links_limit":100,"dialog_filters_enabled":true,"chatlist_update_period":3600,"chatlist_invites_limit_default":3,"chatlist_invites_limit_premium":20,"chatlists_joined_limit_default":2,"chatlists_joined_limit_premium":20,"about_length_limit_default":70,"about_length_limit_premium":140,"caption_length_limit_default":1024,"caption_length_limit_premium":4096,"channels_limit_default":500,"channels_limit_premium":1000,"channels_public_limit_default":10,"channels_public_limit_premium":20,"dialog_filters_limit_default":10,"dialog_filters_limit_premium":20,"dialog_filters_chats_limit_default":100,"dialog_filters_chats_limit_premium":200,"dialogs_pinned_limit_default":5,"dialogs_pinned_limit_premium":10,"dialogs_folder_pinned_limit_default":100,"dialogs_folder_pinned_limit_premium":200,"saved_dialogs_pinned_limit_default":5,"saved_dialogs_pinned_limit_premium":100,"saved_gifs_limit_default":200,"saved_gifs_limit_premium":400,"stickers_faved_limit_default":5,"stickers_faved_limit_premium":10,"recommended_channels_limit_default":10,"recommended_channels_limit_premium":100,"aicompose_tone_examples_num":3,"aicompose_tone_title_length_max":12,"aicompose_tone_prompt_length_max":1024,"aicompose_tone_saved_limit_default":5,"aicompose_tone_saved_limit_premium":20,"upload_max_fileparts_default":4000,"upload_max_fileparts_premium":8000` +const tdesktopDefaultAppConfigBase = `{"chat_read_mark_expire_period":604800,"chat_read_mark_size_threshold":50,"pm_read_date_expire_period":604800,"quote_length_max":1024,"telegram_antispam_group_size_min":200,"telegram_antispam_user_id":"5434988373","fragment_prefixes":["888"],"forum_upgrade_participants_min":2,"reactions_default":{"_":"reactionEmoji","emoticon":"👍"},"reactions_uniq_max":11,"reactions_user_max_default":1,"reactions_user_max_premium":3,"reactions_in_chat_max":3,"boosts_channel_level_max":100,"rich_message_posting":"enabled","upload_markup_video":true,"emojies_send_dice":["🎲","🎯","🏀","⚽","⚽️","🎳","🎰"],"premium_purchase_blocked":false,"stars_purchase_blocked":false,"stargifts_blocked":false,"stargifts_pinned_to_top_limit":6,"stories_stealth_future_period":1500,"stories_stealth_past_period":300,"stories_stealth_cooldown_period":10800,"quick_replies_limit":100,"quick_reply_messages_limit":20,"business_chat_links_limit":100,"dialog_filters_enabled":true,"chatlist_update_period":3600,"chatlist_invites_limit_default":3,"chatlist_invites_limit_premium":20,"chatlists_joined_limit_default":2,"chatlists_joined_limit_premium":20,"about_length_limit_default":70,"about_length_limit_premium":140,"caption_length_limit_default":1024,"caption_length_limit_premium":4096,"channels_limit_default":500,"channels_limit_premium":1000,"channels_public_limit_default":10,"channels_public_limit_premium":20,"dialog_filters_limit_default":10,"dialog_filters_limit_premium":20,"dialog_filters_chats_limit_default":100,"dialog_filters_chats_limit_premium":200,"dialogs_pinned_limit_default":5,"dialogs_pinned_limit_premium":10,"dialogs_folder_pinned_limit_default":100,"dialogs_folder_pinned_limit_premium":200,"saved_dialogs_pinned_limit_default":5,"saved_dialogs_pinned_limit_premium":100,"saved_gifs_limit_default":200,"saved_gifs_limit_premium":400,"stickers_faved_limit_default":5,"stickers_faved_limit_premium":10,"recommended_channels_limit_default":10,"recommended_channels_limit_premium":100,"aicompose_tone_examples_num":3,"aicompose_tone_title_length_max":12,"aicompose_tone_prompt_length_max":1024,"aicompose_tone_saved_limit_default":5,"aicompose_tone_saved_limit_premium":20,"upload_max_fileparts_default":4000,"upload_max_fileparts_premium":8000` -const defaultAppConfigHash = 23 // 默认 app config 内容变更时必须递增,否则缓存端只会收到 notModified。 +const defaultAppConfigHash = 24 // 默认 app config 内容变更时必须递增,否则缓存端只会收到 notModified。 // Service 提供客户端启动配置与国家区号目录。 // diff --git a/internal/app/help/service_premium_test.go b/internal/app/help/service_premium_test.go index fc19fa37..3f82287a 100644 --- a/internal/app/help/service_premium_test.go +++ b/internal/app/help/service_premium_test.go @@ -48,6 +48,7 @@ func TestAppConfigPremiumKeys(t *testing.T) { "reactions_user_max_default": 1, "reactions_user_max_premium": 3, "boosts_channel_level_max": 100, + "stargifts_pinned_to_top_limit": 6, "about_length_limit_default": 70, "about_length_limit_premium": 140, "dialogs_pinned_limit_default": 5, diff --git a/internal/domain/star_gift.go b/internal/domain/star_gift.go index 21ea447b..5a8a54f1 100644 --- a/internal/domain/star_gift.go +++ b/internal/domain/star_gift.go @@ -879,6 +879,9 @@ const ( MaxStarGiftCollectionTitleRunes = 12 MaxStarGiftCollectionsPerPeer = 100 MaxStarGiftCollectionItems = 1000 + // MaxPinnedStarGifts matches stargifts_pinned_to_top_limit advertised to + // official clients. Pin requests are complete replacement vectors. + MaxPinnedStarGifts = 6 ) // Star gift 哨兵错误(rpc 层 errors.Is 映射为 tgerr)。 diff --git a/internal/rpc/convert_messages.go b/internal/rpc/convert_messages.go index 8715cb1b..96f88f54 100644 --- a/internal/rpc/convert_messages.go +++ b/internal/rpc/convert_messages.go @@ -269,7 +269,10 @@ func tgMessageActionStarGiftUnique(action *domain.MessageStarGiftUniqueAction) t if action.DropOriginalDetailsStars > 0 { out.SetDropOriginalDetailsStars(action.DropOriginalDetailsStars) } - if action.CanCraftAt > 0 { + // Channel Craft is not executable yet. Gate on the authoritative gift owner + // as a final wire boundary so historical JSON/admin-log actions or a future + // constructor cannot accidentally expose Android's Craft entry marker. + if action.Gift.Owner.Type == domain.PeerTypeUser && action.CanCraftAt > 0 { out.SetCanCraftAt(action.CanCraftAt) } if action.FromUserID != 0 { diff --git a/internal/rpc/payments_star_gift_lifecycle.go b/internal/rpc/payments_star_gift_lifecycle.go index 904eae74..aee0df20 100644 --- a/internal/rpc/payments_star_gift_lifecycle.go +++ b/internal/rpc/payments_star_gift_lifecycle.go @@ -1007,9 +1007,11 @@ func starGiftLifecycleErr(err error) error { return tgerr.New(400, "STARGIFT_OWNER_INVALID") case errors.Is(err, domain.ErrStarGiftWithdrawalUnavailable): return tgerr.New(400, "STARGIFT_WITHDRAWAL_UNAVAILABLE") + case errors.Is(err, domain.ErrStarGiftCraftUnavailable): + return tgerr.New(400, "STARGIFT_CRAFT_UNAVAILABLE") case errors.Is(err, domain.ErrStarGiftNotFound), errors.Is(err, domain.ErrStarGiftResaleUnavailable), errors.Is(err, domain.ErrStarGiftTransferUnavailable), errors.Is(err, domain.ErrStarGiftOfferInvalid), - errors.Is(err, domain.ErrStarGiftCraftUnavailable), errors.Is(err, domain.ErrStarGiftAuctionUnavailable), + errors.Is(err, domain.ErrStarGiftAuctionUnavailable), errors.Is(err, domain.ErrStarGiftUnavailable), errors.Is(err, domain.ErrStarGiftInvalid), errors.Is(err, domain.ErrStarGiftCollectibleUnavailable): return starGiftInvalidErr() diff --git a/internal/rpc/payments_star_gifts.go b/internal/rpc/payments_star_gifts.go index d74864cf..ee63ace1 100644 --- a/internal/rpc/payments_star_gifts.go +++ b/internal/rpc/payments_star_gifts.go @@ -1071,6 +1071,27 @@ func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.Sta item.SetPrepaidUpgradeHash(g.PrepaidUpgradeHash) } } + if g.CanExportAt > 0 { + item.SetCanExportAt(g.CanExportAt) + } + if g.TransferStars > 0 { + item.SetTransferStars(g.TransferStars) + } + if g.CanTransferAt > 0 { + item.SetCanTransferAt(g.CanTransferAt) + } + if g.CanResellAt > 0 { + item.SetCanResellAt(g.CanResellAt) + } + if g.DropOriginalDetailsStars > 0 { + item.SetDropOriginalDetailsStars(g.DropOriginalDetailsStars) + } + // Channel Craft execution is not implemented yet. Android uses this field + // as the entry/capability marker, so only advertise the currently + // executable user-owned path while retaining the durable DB entitlement. + if g.Owner.Type == domain.PeerTypeUser && g.CanCraftAt > 0 { + item.SetCanCraftAt(g.CanCraftAt) + } if g.PinnedOrder > 0 { item.PinnedToTop = true } diff --git a/internal/rpc/payments_star_gifts_rpc_test.go b/internal/rpc/payments_star_gifts_rpc_test.go index 7d7da827..8e615e9e 100644 --- a/internal/rpc/payments_star_gifts_rpc_test.go +++ b/internal/rpc/payments_star_gifts_rpc_test.go @@ -118,7 +118,7 @@ func (s *craftStarGiftRPCService) GetSaved(_ context.Context, ref domain.SavedSt } continue } - if saved.MsgID == ref.MsgID { + if saved.MsgID == ref.MsgID || saved.UpgradeMsgID == ref.MsgID { return saved, true, nil } } @@ -181,11 +181,12 @@ func TestCraftStarGiftAcceptsOfficialSlugAndCanonicalizesAliases(t *testing.T) { t.Fatalf("duplicate aliases err=%v craft calls=%d", err, service.craftCall) } - _, err = r.onPaymentsCraftStarGift(ctx, &tg.PaymentsCraftStarGiftRequest{Stargift: []tg.InputSavedStarGiftClass{ + updates, err = r.onPaymentsCraftStarGift(ctx, &tg.PaymentsCraftStarGiftRequest{Stargift: []tg.InputSavedStarGiftClass{ &tg.InputSavedStarGiftUser{MsgID: 116}, }}) - if !tgerr.Is(err, "STARGIFT_INVALID") || service.craftCall != 0 { - t.Fatalf("upgrade message id accepted as gift identity: err=%v craft calls=%d", err, service.craftCall) + if err != nil || updates == nil || service.craftCall != 1 || service.craftReq.CommandKey != "rpc:50" || + len(service.craftReq.Refs) != 1 || service.craftReq.Refs[0].MsgID != 116 { + t.Fatalf("upgrade message alias craft: updates=%T req=%+v err=%v calls=%d", updates, service.craftReq, err, service.craftCall) } } @@ -314,6 +315,162 @@ func TestSavedStarGiftProjectionCombinesHistoricalCatalogWithCurrentCollectibleA } } +func TestSavedStarGiftProjectionPreservesCollectibleLifecycle(t *testing.T) { + const ( + giftID = int64(8001) + revision = int64(9001) + readyAt = 1_780_000_123 + exportAt = 1_780_000_200 + transferAt = 1_780_000_300 + resellAt = 1_780_000_400 + ) + unique := domain.UniqueStarGift{ID: 9901, GiftID: giftID, Title: "Craftable", Slug: "craftable-1", Num: 1, + Owner: domain.Peer{Type: domain.PeerTypeUser, ID: 7102}, CraftChancePermille: 250} + saved := domain.SavedStarGift{ + Owner: domain.Peer{Type: domain.PeerTypeUser, ID: 7102}, GiftID: giftID, RevisionID: revision, + MsgID: 44, Date: 100, UniqueGiftID: unique.ID, Unique: &unique, + CanExportAt: exportAt, TransferStars: 25, CanTransferAt: transferAt, CanResellAt: resellAt, + DropOriginalDetailsStars: 30, CanCraftAt: readyAt, + } + projected := tgSavedStarGifts([]domain.SavedStarGift{saved}, nil, nil) + if len(projected) != 1 { + t.Fatalf("saved lifecycle projection count = %d", len(projected)) + } + assertLifecycle := func(t *testing.T, item tg.SavedStarGift) { + t.Helper() + if value, ok := item.GetCanExportAt(); !ok || value != exportAt { + t.Fatalf("can_export_at = %d set=%v", value, ok) + } + if value, ok := item.GetTransferStars(); !ok || value != 25 { + t.Fatalf("transfer_stars = %d set=%v", value, ok) + } + if value, ok := item.GetCanTransferAt(); !ok || value != transferAt { + t.Fatalf("can_transfer_at = %d set=%v", value, ok) + } + if value, ok := item.GetCanResellAt(); !ok || value != resellAt { + t.Fatalf("can_resell_at = %d set=%v", value, ok) + } + if value, ok := item.GetDropOriginalDetailsStars(); !ok || value != 30 { + t.Fatalf("drop_original_details_stars = %d set=%v", value, ok) + } + if value, ok := item.GetCanCraftAt(); !ok || value != readyAt { + t.Fatalf("can_craft_at = %d set=%v", value, ok) + } + } + assertLifecycle(t, projected[0]) + zero := tgSavedStarGifts([]domain.SavedStarGift{{ + Owner: domain.Peer{Type: domain.PeerTypeUser, ID: 7102}, GiftID: giftID, RevisionID: revision, + MsgID: 45, Date: 101, UniqueGiftID: unique.ID, Unique: &unique, + }}, nil, nil)[0] + if _, ok := zero.GetCanExportAt(); ok { + t.Fatal("zero can_export_at must be absent") + } + if _, ok := zero.GetTransferStars(); ok { + t.Fatal("zero transfer_stars must be absent") + } + if _, ok := zero.GetCanTransferAt(); ok { + t.Fatal("zero can_transfer_at must be absent") + } + if _, ok := zero.GetCanResellAt(); ok { + t.Fatal("zero can_resell_at must be absent") + } + if _, ok := zero.GetDropOriginalDetailsStars(); ok { + t.Fatal("zero drop_original_details_stars must be absent") + } + if _, ok := zero.GetCanCraftAt(); ok { + t.Fatal("zero can_craft_at must be absent") + } + channelSaved := saved + channelSaved.Owner = domain.Peer{Type: domain.PeerTypeChannel, ID: 8102} + channelSaved.MsgID = 0 + channelSaved.SavedID = 51 + channelProjected := tgSavedStarGifts([]domain.SavedStarGift{channelSaved}, nil, nil)[0] + if _, ok := channelProjected.GetCanCraftAt(); ok { + t.Fatal("channel can_craft_at must be absent until channel Craft is executable") + } + if value, ok := channelProjected.GetSavedID(); !ok || value != channelSaved.SavedID { + t.Fatalf("channel saved_id = %d set=%v", value, ok) + } + for _, profile := range []tlprofile.Profile{tlprofile.Profile225, tlprofile.Profile226, tlprofile.Profile227, tlprofile.Profile228} { + wire := &tg.PaymentsSavedStarGifts{Count: 1, Gifts: projected, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}} + encoded := &bin.Buffer{} + if err := tlprofile.EncodeObject(profile, wire, encoded); err != nil { + t.Fatalf("encode Layer %d saved lifecycle: %v", profile, err) + } + decodedObject, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: encoded.Buf}, tlprofile.Limits{}) + if err != nil { + t.Fatalf("decode Layer %d saved lifecycle: %v", profile, err) + } + decoded, ok := decodedObject.(*tg.PaymentsSavedStarGifts) + if !ok || len(decoded.Gifts) != 1 { + t.Fatalf("decode Layer %d saved lifecycle type = %T", profile, decodedObject) + } + assertLifecycle(t, decoded.Gifts[0]) + + channelWire := &tg.PaymentsSavedStarGifts{Count: 1, Gifts: []tg.SavedStarGift{channelProjected}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}} + channelEncoded := &bin.Buffer{} + if err := tlprofile.EncodeObject(profile, channelWire, channelEncoded); err != nil { + t.Fatalf("encode Layer %d channel saved lifecycle: %v", profile, err) + } + channelDecodedObject, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: channelEncoded.Buf}, tlprofile.Limits{}) + if err != nil { + t.Fatalf("decode Layer %d channel saved lifecycle: %v", profile, err) + } + channelDecoded, ok := channelDecodedObject.(*tg.PaymentsSavedStarGifts) + if !ok || len(channelDecoded.Gifts) != 1 { + t.Fatalf("decode Layer %d channel saved lifecycle type = %T", profile, channelDecodedObject) + } + if _, ok := channelDecoded.Gifts[0].GetCanCraftAt(); ok { + t.Fatalf("Layer %d channel saved gift exposed can_craft_at", profile) + } + } +} + +func TestChannelUniqueActionSuppressesCraftReadinessAcrossProfiles(t *testing.T) { + const readyAt = 1_780_000_123 + unique := domain.UniqueStarGift{ + ID: 9902, GiftID: 8002, Title: "Channel Craftable", Slug: "channel-craftable-1", Num: 1, + Owner: domain.Peer{Type: domain.PeerTypeChannel, ID: 8102}, CraftChancePermille: 250, + } + action := tgMessageActionStarGiftUnique(&domain.MessageStarGiftUniqueAction{ + Gift: unique, Peer: unique.Owner, SavedID: 52, Saved: true, CanCraftAt: readyAt, + }).(*tg.MessageActionStarGiftUnique) + if _, ok := action.GetCanCraftAt(); ok { + t.Fatal("channel unique action must not expose can_craft_at") + } + projectedGift, ok := action.Gift.(*tg.StarGiftUnique) + if !ok || projectedGift.CraftChancePermille != unique.CraftChancePermille { + t.Fatalf("channel unique gift lost intrinsic Craft chance: %#v", action.Gift) + } + for _, profile := range []tlprofile.Profile{tlprofile.Profile225, tlprofile.Profile226, tlprofile.Profile227, tlprofile.Profile228} { + wire := &bin.Buffer{} + if err := tlprofile.EncodeObject(profile, action, wire); err != nil { + t.Fatalf("encode Layer %d channel unique action: %v", profile, err) + } + decodedObject, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: wire.Buf}, tlprofile.Limits{}) + if err != nil { + t.Fatalf("decode Layer %d channel unique action: %v", profile, err) + } + decoded, ok := decodedObject.(*tg.MessageActionStarGiftUnique) + if !ok { + t.Fatalf("decode Layer %d channel unique action type = %T", profile, decodedObject) + } + if _, ok := decoded.GetCanCraftAt(); ok { + t.Fatalf("Layer %d channel unique action exposed can_craft_at", profile) + } + gift, ok := decoded.Gift.(*tg.StarGiftUnique) + if !ok || gift.CraftChancePermille != unique.CraftChancePermille { + t.Fatalf("Layer %d channel unique gift = %#v", profile, decoded.Gift) + } + } +} + +func TestStarGiftLifecycleCraftUnavailableError(t *testing.T) { + if err := starGiftLifecycleErr(domain.ErrStarGiftCraftUnavailable); !tgerr.Is(err, "STARGIFT_CRAFT_UNAVAILABLE") { + t.Fatalf("craft unavailable mapping = %v", err) + } +} + func TestMessageStarGiftProjectionSeparatesPaidPriceFromPrepaidAmount(t *testing.T) { ordinary, ok := tgMessageActionStarGift(&domain.MessageStarGiftAction{ GiftID: 8001, Stars: 50, ConvertStars: 25, CanUpgrade: true, UpgradePriceStars: 75, diff --git a/internal/store/memory/star_gift.go b/internal/store/memory/star_gift.go index 2a6733f5..63369feb 100644 --- a/internal/store/memory/star_gift.go +++ b/internal/store/memory/star_gift.go @@ -530,6 +530,15 @@ func (s *StarGiftStore) SetUnsaved(_ context.Context, ref domain.SavedStarGiftRe for i := range s.gifts { if s.savedStarGiftMatchesRef(s.gifts[i], ref) && s.gifts[i].LifecycleStatus.Live() { s.gifts[i].Unsaved = unsaved + if unsaved && s.gifts[i].PinnedOrder > 0 { + removedOrder := s.gifts[i].PinnedOrder + s.gifts[i].PinnedOrder = 0 + for j := range s.gifts { + if s.gifts[j].Owner == ref.Owner && s.gifts[j].PinnedOrder > removedOrder { + s.gifts[j].PinnedOrder-- + } + } + } return true, nil } } @@ -701,10 +710,16 @@ func (s *StarGiftStore) ReorderCollections(_ context.Context, owner domain.Peer, func (s *StarGiftStore) SetPinned(_ context.Context, owner domain.Peer, savedGiftIDs []int64) error { s.mu.Lock() defer s.mu.Unlock() + if len(savedGiftIDs) > domain.MaxPinnedStarGifts { + return domain.ErrStarGiftCollectibleInvalid + } ids, err := s.validCollectionGiftIDsLocked(owner, savedGiftIDs) if err != nil { return err } + if len(ids) != len(savedGiftIDs) { + return domain.ErrStarGiftCollectibleInvalid + } order := make(map[int64]int, len(ids)) for i, id := range ids { order[id] = i + 1 @@ -712,6 +727,9 @@ func (s *StarGiftStore) SetPinned(_ context.Context, owner domain.Peer, savedGif for i := range s.gifts { if s.gifts[i].Owner == owner { s.gifts[i].PinnedOrder = order[s.gifts[i].ID] + if s.gifts[i].PinnedOrder > 0 { + s.gifts[i].Unsaved = false + } } } return nil diff --git a/internal/store/memory/star_gift_profile_order_test.go b/internal/store/memory/star_gift_profile_order_test.go index e7e008e0..0fb02bee 100644 --- a/internal/store/memory/star_gift_profile_order_test.go +++ b/internal/store/memory/star_gift_profile_order_test.go @@ -47,6 +47,24 @@ func TestStarGiftProfilePinOrderAndPagination(t *testing.T) { if !slices.Equal(got, want) { t.Fatalf("paged order = %v, want %v", got, want) } + if ok, err := store.SetUnsaved(ctx, domain.SavedStarGiftRef{Owner: owner, MsgID: 100}, true); err != nil || !ok { + t.Fatalf("hide pinned gift = %v err %v", ok, err) + } + hidden, found, err := store.GetByRef(ctx, domain.SavedStarGiftRef{Owner: owner, MsgID: 100}) + if err != nil || !found || !hidden.Unsaved || hidden.PinnedOrder != 0 { + t.Fatalf("hidden pinned gift = %+v found %v err %v", hidden, found, err) + } + remaining, found, err := store.GetByRef(ctx, domain.SavedStarGiftRef{Owner: owner, MsgID: 102}) + if err != nil || !found || remaining.PinnedOrder != 1 { + t.Fatalf("remaining pin = %+v found %v err %v", remaining, found, err) + } + if err := store.SetPinned(ctx, owner, []int64{ids[0], ids[2]}); err != nil { + t.Fatalf("repin hidden gift: %v", err) + } + repinned, found, err := store.GetByRef(ctx, domain.SavedStarGiftRef{Owner: owner, MsgID: 100}) + if err != nil || !found || repinned.Unsaved || repinned.PinnedOrder != 1 { + t.Fatalf("repinned gift = %+v found %v err %v", repinned, found, err) + } if err := store.SetPinned(ctx, owner, nil); err != nil { t.Fatalf("clear pinned: %v", err) diff --git a/internal/store/postgres/star_gift.go b/internal/store/postgres/star_gift.go index 7f83fdb3..c488f6cd 100644 --- a/internal/store/postgres/star_gift.go +++ b/internal/store/postgres/star_gift.go @@ -649,11 +649,16 @@ LEFT JOIN unique_star_gifts u ON u.id=p.unique_gift_id WHERE p.owner_peer_type=$1 AND p.owner_peer_id=$2 AND p.lifecycle_status='active' AND (p.saved_id::bigint=ANY($3::bigint[]) OR u.slug=ANY($4::text[]))` if owner.Type == domain.PeerTypeUser { - query = `SELECT p.msg_id::bigint, COALESCE(u.slug, ''), p.id + query = `SELECT ref.msg_id, COALESCE(u.slug, ''), p.id FROM peer_star_gifts p LEFT JOIN unique_star_gifts u ON u.id=p.unique_gift_id +CROSS JOIN LATERAL ( + SELECT p.msg_id::bigint AS msg_id + UNION ALL + SELECT r.msg_id::bigint FROM star_gift_user_message_refs r WHERE r.saved_gift_id=p.id +) ref WHERE p.owner_peer_type=$1 AND p.owner_peer_id=$2 AND p.lifecycle_status='active' - AND (p.msg_id::bigint=ANY($3::bigint[]) + AND (ref.msg_id=ANY($3::bigint[]) OR u.slug=ANY($4::text[]))` } rows, err := s.db.Query(ctx, query, string(owner.Type), owner.ID, values, slugs) @@ -744,15 +749,42 @@ func (s *StarGiftStore) SetUnsaved(ctx context.Context, ref domain.SavedStarGift if !ref.Valid() { return false, domain.ErrStarGiftNotFound } - where, args := savedStarGiftRefWhere(ref) - args = append(args, unsaved) - tag, err := s.db.Exec(ctx, ` -UPDATE peer_star_gifts SET unsaved = $4 -WHERE `+where+` AND lifecycle_status='active'`, args...) + changed := false + err := withTx(ctx, s.db, "set star gift unsaved", func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(ref.Owner)); err != nil { + return err + } + where, args := savedStarGiftRefWhere(ref) + var savedID int64 + var pinnedOrder int + err := tx.QueryRow(ctx, `SELECT id,pinned_order FROM peer_star_gifts WHERE `+where+` AND lifecycle_status='active' FOR UPDATE`, args...).Scan(&savedID, &pinnedOrder) + if errors.Is(err, pgx.ErrNoRows) { + return nil + } + if err != nil { + return err + } + if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET unsaved=$2,pinned_order=CASE WHEN $2 THEN 0 ELSE pinned_order END WHERE id=$1`, savedID, unsaved); err != nil { + return err + } + if unsaved && pinnedOrder > 0 { + // The positive-order unique index is immediate. Move the bounded + // vector one vacant slot at a time so no transient duplicate order + // can be observed by PostgreSQL. + for order := pinnedOrder + 1; order <= domain.MaxPinnedStarGifts; order++ { + if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET pinned_order=$4 +WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND pinned_order=$3`, string(ref.Owner.Type), ref.Owner.ID, order, order-1); err != nil { + return err + } + } + } + changed = true + return nil + }) if err != nil { return false, fmt.Errorf("set star gift unsaved: %w", err) } - return tag.RowsAffected() > 0, nil + return changed, nil } func (s *StarGiftStore) MarkConverted(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) { @@ -836,7 +868,11 @@ func savedStarGiftRefWhere(ref domain.SavedStarGiftRef) (string, []any) { return "owner_peer_type = $1 AND owner_peer_id = $2 AND saved_id = $3", args default: args = append(args, ref.MsgID) - return "owner_peer_type = $1 AND owner_peer_id = $2 AND msg_id = $3", args + return `owner_peer_type = $1 AND owner_peer_id = $2 AND ( +msg_id = $3 OR EXISTS ( + SELECT 1 FROM star_gift_user_message_refs r + WHERE r.saved_gift_id = id AND r.owner_user_id = $2 AND r.msg_id = $3 +))`, args } } diff --git a/internal/store/postgres/star_gift_collectibles.go b/internal/store/postgres/star_gift_collectibles.go index 7bad16aa..d942b863 100644 --- a/internal/store/postgres/star_gift_collectibles.go +++ b/internal/store/postgres/star_gift_collectibles.go @@ -690,6 +690,9 @@ func (s *StarGiftStore) ReorderCollections(ctx context.Context, owner domain.Pee } func (s *StarGiftStore) SetPinned(ctx context.Context, owner domain.Peer, savedGiftIDs []int64) error { + if len(savedGiftIDs) > domain.MaxPinnedStarGifts { + return domain.ErrStarGiftCollectibleInvalid + } 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 @@ -698,11 +701,14 @@ func (s *StarGiftStore) SetPinned(ctx context.Context, owner domain.Peer, savedG if err != nil { return err } + if len(ids) != len(savedGiftIDs) { + return domain.ErrStarGiftCollectibleInvalid + } 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 { + if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET pinned_order=$2,unsaved=false WHERE id=$1`, id, order+1); err != nil { return err } } diff --git a/internal/store/postgres/star_gift_collectibles_integration_test.go b/internal/store/postgres/star_gift_collectibles_integration_test.go index a9ee78a3..b73f8106 100644 --- a/internal/store/postgres/star_gift_collectibles_integration_test.go +++ b/internal/store/postgres/star_gift_collectibles_integration_test.go @@ -116,13 +116,28 @@ func TestStarGiftCollectibleUpgradeAggregatePostgres(t *testing.T) { t.Fatalf("owner upgrade service message = %+v", ownerMessage) } uniqueAction := ownerMessage.Media.ServiceAction.StarGiftUnique - if uniqueAction.SavedID != int64(saved.MsgID) { - t.Fatalf("unique action saved_id = %d, want stable source msg id %d", uniqueAction.SavedID, saved.MsgID) + if uniqueAction.SavedID != 0 || uniqueAction.Peer.Type != "" || uniqueAction.Peer.ID != 0 { + t.Fatalf("user unique action leaked channel peer/saved_id: %+v", uniqueAction) } senderUniqueAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique if senderUniqueAction == nil || senderUniqueAction.SavedID != 0 { t.Fatalf("sender unique action leaked owner-only saved_id: %+v", senderUniqueAction) } + if byOutput, found, err := gifts.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: ownerMessage.ID}); err != nil || !found || byOutput.ID != savedID { + t.Fatalf("owner upgrade output ref = %+v found %v err %v", byOutput, found, err) + } + var ownerAliasCount, senderAliasCount int + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND msg_id=$2 AND saved_gift_id=$3`, owner.ID, ownerMessage.ID, savedID).Scan(&ownerAliasCount); err != nil { + t.Fatalf("load owner upgrade output alias: %v", err) + } + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND msg_id=$2`, sender.ID, ownerMessage.ID).Scan(&senderAliasCount); err != nil { + t.Fatalf("load sender upgrade output alias: %v", err) + } + if ownerAliasCount != 1 || senderAliasCount != 0 { + t.Fatalf("upgrade output aliases owner=%d sender=%d, want owner-only", ownerAliasCount, senderAliasCount) + } ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID) if ownerSourceEdit.Event.Pts <= ownerMessage.Pts || ownerSourceEdit.Message.Media == nil || ownerSourceEdit.Message.Media.ServiceAction == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil || diff --git a/internal/store/postgres/star_gift_craft_auction.go b/internal/store/postgres/star_gift_craft_auction.go index 229ba392..e55e9779 100644 --- a/internal/store/postgres/star_gift_craft_auction.go +++ b/internal/store/postgres/star_gift_craft_auction.go @@ -11,6 +11,7 @@ import ( "github.com/jackc/pgx/v5" "telesrv/internal/domain" + "telesrv/internal/store" ) const ( @@ -18,6 +19,37 @@ const ( maxStarGiftAuctionAcquired = 1000 ) +// starGiftCraftOutputIntent is the immutable message intent committed with a +// successful craft outcome. The aggregate may subsequently be hidden, listed +// or otherwise edited; an exact retry must still use the first intent and its +// fingerprint instead of rebuilding a different message from mutable state. +type starGiftCraftOutputIntent struct { + Media *domain.MessageMedia + Fingerprint []byte + Date int + SavedGiftID int64 +} + +func starGiftCraftOutputMedia(userID int64, gift domain.UniqueStarGift, saved domain.SavedStarGift) *domain.MessageMedia { + return &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{ + Kind: domain.MessageServiceActionStarGiftUnique, StarGiftUnique: &domain.MessageStarGiftUniqueAction{ + Gift: gift, FromUserID: userID, Saved: !saved.Unsaved, Craft: true, + CanExportAt: saved.CanExportAt, TransferStars: saved.TransferStars, CanTransferAt: saved.CanTransferAt, + CanResellAt: saved.CanResellAt, DropOriginalDetailsStars: saved.DropOriginalDetailsStars, + CanCraftAt: saved.CanCraftAt, + }, + }} +} + +func starGiftCraftOutputRequest(req domain.StarGiftCraftRequest, intent starGiftCraftOutputIntent) domain.SendPrivateTextRequest { + return domain.SendPrivateTextRequest{ + SenderUserID: req.UserID, RecipientUserID: req.UserID, + RandomID: lifecycleCommandRandomID("craft", req.UserID, req.CommandKey), Date: intent.Date, + OriginAuthKeyID: req.OriginAuthKeyID, OriginSessionID: req.OriginSessionID, OriginUserID: req.UserID, + IdempotencyFingerprint: append([]byte(nil), intent.Fingerprint...), Media: intent.Media, + } +} + func defaultStarGiftCraftDraw(upper int) (int, error) { if upper <= 0 { return 0, domain.ErrStarGiftCraftUnavailable @@ -158,7 +190,8 @@ func (s *StarGiftLifecycleStore) ListCraftStarGifts(ctx context.Context, userID, } args := []any{userID, giftID} where := `p.owner_peer_type='user' AND p.owner_peer_id=$1 AND p.gift_id=$2 - AND p.lifecycle_status='active' AND p.unique_gift_id IS NOT NULL AND p.can_craft_at<=EXTRACT(EPOCH FROM now())::integer + AND p.lifecycle_status='active' AND p.unique_gift_id IS NOT NULL + AND p.can_craft_at>0 AND p.can_craft_at<=EXTRACT(EPOCH FROM now())::integer AND NOT u.burned AND u.owner_address='' AND u.craft_chance_permille>0 AND EXISTS (SELECT 1 FROM star_gift_collectible_models m WHERE m.collectible_revision_id=u.collectible_revision_id AND m.crafted)` @@ -234,11 +267,11 @@ func (s *StarGiftLifecycleStore) CraftStarGift(ctx context.Context, req domain.S // A committed failed craft has already moved every input out of the active // lifecycle. Consult the immutable receipt before active-gift resolution so // an exact transport retry can still replay the same terminal result. - if replay, found, err := s.loadCraftReplay(ctx, req); err != nil || found { + if replay, output, found, err := s.loadCraftReplay(ctx, req); err != nil || found { if err != nil || !replay.Success { return replay, err } - return s.deliverCraftSuccess(ctx, req, replay) + return s.deliverCraftSuccess(ctx, req, replay, output) } savedIDs, err := NewStarGiftStore(s.db).ResolveSavedIDs(ctx, owner, req.Refs) if err != nil { @@ -248,7 +281,7 @@ func (s *StarGiftLifecycleStore) CraftStarGift(ctx context.Context, req domain.S return domain.StarGiftCraftResult{}, domain.ErrStarGiftCraftUnavailable } var result domain.StarGiftCraftResult - var resultUniqueID int64 + var output starGiftCraftOutputIntent err = withTx(ctx, s.db, "craft star gift", func(tx pgx.Tx) error { lockedRows, err := tx.Query(ctx, `SELECT id FROM peer_star_gifts WHERE id=ANY($1::bigint[]) ORDER BY id FOR UPDATE`, sortedUniqueInt64(savedIDs)) if err != nil { @@ -269,7 +302,7 @@ func (s *StarGiftLifecycleStore) CraftStarGift(ctx context.Context, req domain.S chance := 0 for i := range req.Refs { saved, err := lockSavedStarGiftByID(ctx, tx, savedIDs[i]) - if err != nil || !saved.LifecycleStatus.Live() || saved.UniqueGiftID == 0 || saved.CanCraftAt > req.Date { + if err != nil || !saved.LifecycleStatus.Live() || saved.UniqueGiftID == 0 || saved.CanCraftAt <= 0 || saved.CanCraftAt > req.Date { return domain.ErrStarGiftCraftUnavailable } unique, found, err := NewStarGiftStore(tx).UniqueByID(ctx, saved.UniqueGiftID) @@ -374,111 +407,164 @@ WHERE id=ANY($1::bigint[])`, savedIDs[burnFrom:]); err != nil { } result.SourceEdits = sourceEdits var resultID any + var outputMediaJSON any + var outputFingerprint any if result.Success { resultID = firstUniqueID - resultUniqueID = firstUniqueID + gift, found, err := NewStarGiftStore(tx).UniqueByID(ctx, firstUniqueID) + if err != nil || !found { + if err != nil { + return err + } + return domain.ErrStarGiftCraftUnavailable + } + saved, found, err := savedStarGiftByID(ctx, tx, firstSavedID) + if err != nil || !found || saved.Owner != owner || saved.UniqueGiftID != gift.ID || + !saved.LifecycleStatus.Live() || gift.Owner != owner || gift.Burned || !gift.Crafted { + if err != nil { + return err + } + return domain.ErrStarGiftCraftUnavailable + } + media := starGiftCraftOutputMedia(req.UserID, gift, saved) + intent := starGiftCraftOutputIntent{Media: media, Date: req.Date, SavedGiftID: saved.ID} + fingerprint, err := store.PrivateSendFingerprint(starGiftCraftOutputRequest(req, intent)) + if err != nil { + return fmt.Errorf("fingerprint crafted gift output: %w", err) + } + mediaJSON, err := encodeMessageMedia(media) + if err != nil { + return fmt.Errorf("encode crafted gift output: %w", err) + } + intent.Fingerprint = fingerprint + output = intent + outputMediaJSON = mediaJSON + outputFingerprint = fingerprint + giftCopy := gift + result.Gift = &giftCopy } _, err = tx.Exec(ctx, `INSERT INTO star_gift_craft_commands(user_id,command_key,input_unique_gift_ids,gift_id, -success,result_unique_gift_id,chance_permille,created_at,source_edit_pts) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)`, req.UserID, - strings.TrimSpace(req.CommandKey), uniqueIDs, giftID, result.Success, resultID, chance, req.Date, sourceEditPTS) +success,result_unique_gift_id,chance_permille,created_at,source_edit_pts,output_media,output_fingerprint) +VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, req.UserID, strings.TrimSpace(req.CommandKey), uniqueIDs, + giftID, result.Success, resultID, chance, req.Date, sourceEditPTS, outputMediaJSON, outputFingerprint) return err }) if err != nil { if isUniqueViolation(err) { - if replay, found, replayErr := s.loadCraftReplay(ctx, req); replayErr != nil || found { + if replay, replayOutput, found, replayErr := s.loadCraftReplay(ctx, req); replayErr != nil || found { + if replayErr == nil && found && replay.Success { + return s.deliverCraftSuccess(ctx, req, replay, replayOutput) + } return replay, replayErr } } return domain.StarGiftCraftResult{}, err } if result.Success { - gift, found, err := NewStarGiftStore(s.db).UniqueByID(ctx, resultUniqueID) - if err != nil || !found { - return domain.StarGiftCraftResult{}, domain.ErrStarGiftCraftUnavailable - } - result.Gift = &gift - } - if result.Success { - return s.deliverCraftSuccess(ctx, req, result) + return s.deliverCraftSuccess(ctx, req, result, output) } return result, nil } -func (s *StarGiftLifecycleStore) deliverCraftSuccess(ctx context.Context, req domain.StarGiftCraftRequest, result domain.StarGiftCraftResult) (domain.StarGiftCraftResult, error) { - if result.Gift == nil || s.messages == nil { +func (s *StarGiftLifecycleStore) deliverCraftSuccess(ctx context.Context, req domain.StarGiftCraftRequest, result domain.StarGiftCraftResult, output starGiftCraftOutputIntent) (domain.StarGiftCraftResult, error) { + if result.Gift == nil || s.messages == nil || output.Media == nil || output.Date <= 0 || output.SavedGiftID <= 0 || + store.ValidateSendFingerprint(output.Fingerprint, "crafted gift output") != nil { return domain.StarGiftCraftResult{}, domain.ErrStarGiftCraftUnavailable } - saved, found, err := savedStarGiftByUniqueID(ctx, s.db, result.Gift.ID) - if err != nil || !found || saved.Owner != (domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}) { - return domain.StarGiftCraftResult{}, domain.ErrStarGiftCraftUnavailable - } - sent, err := s.messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{SenderUserID: req.UserID, - RecipientUserID: req.UserID, RandomID: lifecycleCommandRandomID("craft", req.UserID, req.CommandKey), Date: req.Date, - OriginAuthKeyID: req.OriginAuthKeyID, OriginSessionID: req.OriginSessionID, OriginUserID: req.UserID, - Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{ - Kind: domain.MessageServiceActionStarGiftUnique, StarGiftUnique: &domain.MessageStarGiftUniqueAction{ - Gift: *result.Gift, FromUserID: req.UserID, Peer: saved.Owner, Saved: !saved.Unsaved, Craft: true, - CanExportAt: saved.CanExportAt, TransferStars: saved.TransferStars, CanTransferAt: saved.CanTransferAt, - CanResellAt: saved.CanResellAt, DropOriginalDetailsStars: saved.DropOriginalDetailsStars, - CanCraftAt: saved.CanCraftAt}}}}) + messageReq := starGiftCraftOutputRequest(req, output) + hooks := privateSendTxHooks{after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { + return registerUserStarGiftMessageRef(ctx, tx, req.UserID, sent.SenderMessage.ID, output.SavedGiftID, result.Gift.ID) + }} + sent, err := s.messages.sendPrivateTextWithHooks(ctx, messageReq, hooks) if err != nil { return domain.StarGiftCraftResult{}, err } + registered, err := userStarGiftMessageRefMatches(ctx, s.db, req.UserID, sent.SenderMessage.ID, output.SavedGiftID) + if err != nil || !registered { + if err != nil { + return domain.StarGiftCraftResult{}, err + } + return domain.StarGiftCraftResult{}, domain.ErrStarGiftCraftUnavailable + } result.Send = sent result.Duplicate = result.Duplicate || sent.Duplicate return result, nil } -func (s *StarGiftLifecycleStore) loadCraftReplay(ctx context.Context, req domain.StarGiftCraftRequest) (domain.StarGiftCraftResult, bool, error) { +func (s *StarGiftLifecycleStore) loadCraftReplay(ctx context.Context, req domain.StarGiftCraftRequest) (domain.StarGiftCraftResult, starGiftCraftOutputIntent, bool, error) { var success bool var resultID *int64 var chance int var inputUniqueIDs []int64 var sourceEditPTS []int32 - err := s.db.QueryRow(ctx, `SELECT input_unique_gift_ids,success,result_unique_gift_id,chance_permille,source_edit_pts + var createdAt int + var outputMediaJSON, outputFingerprint []byte + err := s.db.QueryRow(ctx, `SELECT input_unique_gift_ids,success,result_unique_gift_id,chance_permille,source_edit_pts, +created_at,output_media,output_fingerprint FROM star_gift_craft_commands WHERE user_id=$1 AND command_key=$2`, - req.UserID, strings.TrimSpace(req.CommandKey)).Scan(&inputUniqueIDs, &success, &resultID, &chance, &sourceEditPTS) + req.UserID, strings.TrimSpace(req.CommandKey)).Scan(&inputUniqueIDs, &success, &resultID, &chance, &sourceEditPTS, + &createdAt, &outputMediaJSON, &outputFingerprint) if errors.Is(err, pgx.ErrNoRows) { - return domain.StarGiftCraftResult{}, false, nil + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, nil } if err != nil { - return domain.StarGiftCraftResult{}, false, err + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, err } if len(req.Refs) != len(inputUniqueIDs) || len(req.Refs) != len(sourceEditPTS) { - return domain.StarGiftCraftResult{}, false, domain.ErrStarGiftCraftUnavailable + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable } savedIDs := make([]int64, 0, len(inputUniqueIDs)) owner := domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID} for i, uniqueID := range inputUniqueIDs { saved, found, err := savedStarGiftByUniqueID(ctx, s.db, uniqueID) if err != nil || !found || saved.Owner != owner || saved.UniqueGiftID != uniqueID { - return domain.StarGiftCraftResult{}, false, domain.ErrStarGiftCraftUnavailable + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable } ref := req.Refs[i] - if ref.Owner != owner || ref.Slug == "" && ref.MsgID != saved.MsgID { - return domain.StarGiftCraftResult{}, false, domain.ErrStarGiftCraftUnavailable + if ref.Owner != owner { + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable } if ref.Slug != "" { unique, found, err := NewStarGiftStore(s.db).UniqueByID(ctx, uniqueID) if err != nil || !found || !strings.EqualFold(ref.Slug, unique.Slug) { - return domain.StarGiftCraftResult{}, false, domain.ErrStarGiftCraftUnavailable + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable + } + } else if ref.MsgID != saved.MsgID { + matches, err := userStarGiftMessageRefMatches(ctx, s.db, req.UserID, ref.MsgID, saved.ID) + if err != nil { + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, err + } + if !matches { + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable } } savedIDs = append(savedIDs, saved.ID) } sourceEdits, err := s.loadCraftInputMessageReplays(ctx, req, savedIDs, sourceEditPTS) if err != nil { - return domain.StarGiftCraftResult{}, false, err + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, err } result := domain.StarGiftCraftResult{Success: success, Chance: chance, SourceEdits: sourceEdits, Duplicate: true} - if resultID != nil { - gift, found, err := NewStarGiftStore(s.db).UniqueByID(ctx, *resultID) - if err != nil || !found { - return domain.StarGiftCraftResult{}, false, domain.ErrStarGiftCraftUnavailable + if !success { + if resultID != nil || len(outputMediaJSON) != 0 || len(outputFingerprint) != 0 { + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable } - result.Gift = &gift + return result, starGiftCraftOutputIntent{}, true, nil } - return result, true, nil + if resultID == nil || createdAt <= 0 || store.ValidateSendFingerprint(outputFingerprint, "crafted gift replay") != nil { + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable + } + media, err := decodeMessageMedia(string(outputMediaJSON)) + if err != nil || media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil || + media.ServiceAction.Kind != domain.MessageServiceActionStarGiftUnique || media.ServiceAction.StarGiftUnique == nil || + !media.ServiceAction.StarGiftUnique.Craft || media.ServiceAction.StarGiftUnique.Gift.ID != *resultID { + return domain.StarGiftCraftResult{}, starGiftCraftOutputIntent{}, false, domain.ErrStarGiftCraftUnavailable + } + gift := media.ServiceAction.StarGiftUnique.Gift + result.Gift = &gift + output := starGiftCraftOutputIntent{Media: media, Fingerprint: append([]byte(nil), outputFingerprint...), + Date: createdAt, SavedGiftID: savedIDs[0]} + return result, output, true, nil } func chooseCraftedModel(ctx context.Context, tx pgx.Tx, revisionID int64) (int64, error) { diff --git a/internal/store/postgres/star_gift_integration_test.go b/internal/store/postgres/star_gift_integration_test.go index 17ba3ce1..bcbdb65e 100644 --- a/internal/store/postgres/star_gift_integration_test.go +++ b/internal/store/postgres/star_gift_integration_test.go @@ -142,6 +142,26 @@ func TestStarGiftStorePostgres(t *testing.T) { if !slices.Equal(gotMsgIDs, wantMsgIDs) { t.Fatalf("pinned paged msg ids = %v, want %v", gotMsgIDs, wantMsgIDs) } + // Hiding a pinned gift atomically unpins it and compacts the remaining + // vector. Pinning it again makes it visible in the same owner transaction. + if ok, err := st.SetUnsaved(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 100}, true); err != nil || !ok { + t.Fatalf("hide pinned gift = %v err %v", ok, err) + } + hiddenPinned, found, err := st.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 100}) + if err != nil || !found || !hiddenPinned.Unsaved || hiddenPinned.PinnedOrder != 0 { + t.Fatalf("hidden pinned gift = %+v found %v err %v", hiddenPinned, found, err) + } + remainingPinned, found, err := st.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 102}) + if err != nil || !found || remainingPinned.PinnedOrder != 1 { + t.Fatalf("remaining pin after compaction = %+v found %v err %v", remainingPinned, found, err) + } + if err := st.SetPinned(ctx, ownerPeer, []int64{savedIDs[0], savedIDs[2]}); err != nil { + t.Fatalf("repin hidden gift: %v", err) + } + repinned, found, err := st.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 100}) + if err != nil || !found || repinned.Unsaved || repinned.PinnedOrder != 1 { + t.Fatalf("repinned hidden gift = %+v found %v err %v", repinned, found, err) + } if err := st.SetPinned(ctx, ownerPeer, nil); err != nil { t.Fatalf("clear pinned profile order: %v", err) } diff --git a/internal/store/postgres/star_gift_lifecycle.go b/internal/store/postgres/star_gift_lifecycle.go index 80b64069..d1d9aca7 100644 --- a/internal/store/postgres/star_gift_lifecycle.go +++ b/internal/store/postgres/star_gift_lifecycle.go @@ -389,6 +389,7 @@ func (s *StarGiftLifecycleStore) TransferStarGift(ctx context.Context, req domai }}, } var result domain.StarGiftTransferResult + var sourceSaved domain.SavedStarGift hooks := privateSendTxHooks{ before: func(ctx context.Context, tx pgx.Tx, send *domain.SendPrivateTextRequest) error { saved, unique, err := lockTransferableStarGift(ctx, tx, req.ActorUserID, req.Ref, req.Date) @@ -414,6 +415,7 @@ func (s *StarGiftLifecycleStore) TransferStarGift(ctx context.Context, req domai if err := removeSavedGiftFromCollections(ctx, tx, saved.Owner, saved.ID); err != nil { return err } + sourceSaved = saved unique.Owner = req.To saved.Owner = req.To result.Saved, result.Unique, result.Balance = saved, unique, balance @@ -433,6 +435,9 @@ func (s *StarGiftLifecycleStore) TransferStarGift(ctx context.Context, req domai can_transfer_at=0 WHERE id=$1`, result.Saved.ID, req.To.ID, req.ActorUserID, msgID, req.Date); err != nil { return err } + if err := registerUserStarGiftMessageRef(ctx, tx, req.To.ID, msgID, result.Saved.ID, result.Unique.ID); err != nil { + return err + } if _, err := tx.Exec(ctx, `INSERT INTO star_gift_transfer_commands(actor_user_id,command_key,unique_gift_id, from_peer_type,from_peer_id,to_peer_type,to_peer_id,charge_stars,balance_after,created_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, req.ActorUserID, strings.TrimSpace(req.CommandKey), result.Unique.ID, @@ -441,6 +446,10 @@ func (s *StarGiftLifecycleStore) TransferStarGift(ctx context.Context, req domai } result.Saved.MsgID, result.Saved.SavedID, result.Saved.UpgradeMsgID, result.Saved.Date = msgID, 0, msgID, req.Date result.Saved.FromUserID = req.ActorUserID + if sourceSaved.Owner.Type == domain.PeerTypeUser { + _, err := s.retireUserStarGiftMessagesTx(ctx, tx, sourceSaved, result.Unique, req.Date) + return err + } return nil }, } @@ -502,6 +511,7 @@ func (s *StarGiftLifecycleStore) PurchaseResaleStarGift(ctx context.Context, req } var result domain.StarGiftTransferResult var commissionAmount int64 + var sourceSaved domain.SavedStarGift hooks := privateSendTxHooks{ before: func(ctx context.Context, tx pgx.Tx, send *domain.SendPrivateTextRequest) error { var listingCurrency, sellerType string @@ -557,12 +567,15 @@ func (s *StarGiftLifecycleStore) PurchaseResaleStarGift(ctx context.Context, req gift.ResellAmount = nil gift.LastSaleDate = req.Date gift.LastSaleAmount = &domain.StarGiftAmount{Currency: req.Amount.Currency, Amount: req.Amount.Amount} + sourceSaved = saved saved.Owner = req.To if req.To.Type == domain.PeerTypeChannel { saved.MsgID, saved.SavedID = 0, saved.ID } result.Saved, result.Unique, result.Balance = saved, gift, balance send.Media.ServiceAction.StarGiftUnique = transferUniqueAction(gift, messageSenderID, req.To, saved) + resaleAmount := req.Amount + send.Media.ServiceAction.StarGiftUnique.ResaleAmount = &resaleAmount return nil }, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { @@ -578,6 +591,11 @@ func (s *StarGiftLifecycleStore) PurchaseResaleStarGift(ctx context.Context, req WHERE id=$1`, result.Saved.ID, string(req.To.Type), req.To.ID, messageSenderID, msgID, savedID, req.Date); err != nil { return err } + if req.To.Type == domain.PeerTypeUser { + if err := registerUserStarGiftMessageRef(ctx, tx, req.To.ID, msgID, result.Saved.ID, result.Unique.ID); err != nil { + return err + } + } if _, err := tx.Exec(ctx, `INSERT INTO star_gift_sales(unique_gift_id,seller_peer_type,seller_peer_id, buyer_peer_type,buyer_peer_id,currency,amount,commission_amount,sold_at,command_key) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, result.Unique.ID, string(seller.Type), seller.ID, @@ -601,6 +619,11 @@ func (s *StarGiftLifecycleStore) PurchaseResaleStarGift(ctx context.Context, req } result.Saved.MsgID, result.Saved.SavedID, result.Saved.UpgradeMsgID, result.Saved.Date = msgID, savedID, msgID, req.Date result.Saved.FromUserID = messageSenderID + if sourceSaved.Owner.Type == domain.PeerTypeUser { + if _, err := s.retireUserStarGiftMessagesTx(ctx, tx, sourceSaved, result.Unique, req.Date); err != nil { + return err + } + } return updateStarGiftResaleProjection(ctx, tx, result.Unique.GiftID) }, } @@ -803,7 +826,7 @@ func (s *StarGiftLifecycleStore) ResolveStarGiftOffer(ctx context.Context, req d offer.Gift = gift actionKind := domain.MessageServiceActionStarGiftUnique action := &domain.MessageServiceAction{Kind: actionKind, StarGiftUnique: &domain.MessageStarGiftUniqueAction{ - Gift: gift, FromUserID: req.OwnerUserID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: offer.BuyerUserID}, + Gift: gift, FromUserID: req.OwnerUserID, Transferred: true, FromOffer: true, Saved: true, }} if req.Decline { @@ -816,6 +839,7 @@ func (s *StarGiftLifecycleStore) ResolveStarGiftOffer(ctx context.Context, req d Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: action}} var result domain.StarGiftOfferResult var commissionAmount int64 + var sourceSaved domain.SavedStarGift hooks := privateSendTxHooks{ before: func(ctx context.Context, tx pgx.Tx, send *domain.SendPrivateTextRequest) error { locked, err := scanStarGiftOffer(tx.QueryRow(ctx, `SELECT id,buyer_user_id,owner_peer_type,owner_peer_id,unique_gift_id, @@ -874,10 +898,15 @@ func (s *StarGiftLifecycleStore) ResolveStarGiftOffer(ctx context.Context, req d current.LastSaleDate = req.Date current.LastSaleAmount = &locked.Price locked.Status, locked.ResolvedAt, locked.Gift = "accepted", req.Date, current + sourceSaved = saved + saved.Owner = domain.Peer{Type: domain.PeerTypeUser, ID: locked.BuyerUserID} result.Offer = locked result.Unique = current result.Saved = saved - send.Media.ServiceAction.StarGiftUnique.Gift = current + send.Media.ServiceAction.StarGiftUnique = transferUniqueAction(current, req.OwnerUserID, saved.Owner, saved) + send.Media.ServiceAction.StarGiftUnique.FromOffer = true + resaleAmount := locked.Price + send.Media.ServiceAction.StarGiftUnique.ResaleAmount = &resaleAmount return nil }, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { @@ -890,6 +919,10 @@ func (s *StarGiftLifecycleStore) ResolveStarGiftOffer(ctx context.Context, req d WHERE id=$1`, result.Saved.ID, result.Offer.BuyerUserID, req.OwnerUserID, msgID, req.Date); err != nil { return err } + if err := registerUserStarGiftMessageRef(ctx, tx, result.Offer.BuyerUserID, msgID, + result.Saved.ID, result.Unique.ID); err != nil { + return err + } result.Saved.Owner = domain.Peer{Type: domain.PeerTypeUser, ID: result.Offer.BuyerUserID} result.Saved.FromUserID, result.Saved.MsgID, result.Saved.SavedID, result.Saved.UpgradeMsgID, result.Saved.Date = req.OwnerUserID, msgID, 0, msgID, req.Date if _, err := tx.Exec(ctx, `INSERT INTO star_gift_sales(unique_gift_id,seller_peer_type,seller_peer_id, @@ -899,6 +932,9 @@ func (s *StarGiftLifecycleStore) ResolveStarGiftOffer(ctx context.Context, req d req.Date, fmt.Sprintf("offer:%d", result.Offer.ID)); err != nil { return err } + if _, err := s.retireUserStarGiftMessagesTx(ctx, tx, sourceSaved, result.Unique, req.Date); err != nil { + return err + } return updateStarGiftResaleProjection(ctx, tx, result.Offer.Gift.GiftID) }, } @@ -1077,6 +1113,7 @@ func (s *StarGiftLifecycleStore) transferStarGiftWithoutPrivateMessage(ctx conte if err := removeSavedGiftFromCollections(ctx, tx, saved.Owner, saved.ID); err != nil { return err } + sourceSaved := saved if _, err := tx.Exec(ctx, `UPDATE unique_star_gifts SET owner_peer_type='channel',owner_peer_id=$2,updated_at=now() WHERE id=$1`, unique.ID, req.To.ID); err != nil { return err } @@ -1100,6 +1137,11 @@ func (s *StarGiftLifecycleStore) transferStarGiftWithoutPrivateMessage(ctx conte return err } result.Saved, result.Unique, result.Balance = saved, unique, balance + if sourceSaved.Owner.Type == domain.PeerTypeUser { + if _, err := s.retireUserStarGiftMessagesTx(ctx, tx, sourceSaved, unique, req.Date); err != nil { + return err + } + } return nil }) return result, err @@ -1148,17 +1190,23 @@ func ensureNoStarGiftMarketConflict(ctx context.Context, tx pgx.Tx, uniqueID int } func transferUniqueAction(unique domain.UniqueStarGift, fromUserID int64, to domain.Peer, saved domain.SavedStarGift) *domain.MessageStarGiftUniqueAction { + peer := to savedID := saved.SavedID + canCraftAt := saved.CanCraftAt if to.Type == domain.PeerTypeUser { - // For a user-owned transferred gift the action message itself becomes - // inputSavedStarGiftUser.msg_id. A channel saved_id belongs to a different - // identity namespace and must never leak into the recipient's user view. + // peer and saved_id are a shared channel-only TL flag. A user-owned + // transferred gift is managed by this action message's id. + peer = domain.Peer{} savedID = 0 + } else { + // Preserve the durable entitlement for a future transfer back to a user, + // but keep channel Craft hidden until its write/update path exists. + canCraftAt = 0 } - return &domain.MessageStarGiftUniqueAction{Gift: unique, FromUserID: fromUserID, Peer: to, + return &domain.MessageStarGiftUniqueAction{Gift: unique, FromUserID: fromUserID, Peer: peer, SavedID: savedID, Transferred: true, Saved: true, CanExportAt: saved.CanExportAt, TransferStars: saved.TransferStars, CanTransferAt: saved.CanTransferAt, CanResellAt: saved.CanResellAt, - DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: saved.CanCraftAt} + DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: canCraftAt} } func (s *StarGiftLifecycleStore) debitLifecycleAmount(ctx context.Context, tx pgx.Tx, userID int64, amount domain.StarGiftAmount, @@ -1468,15 +1516,22 @@ WHERE provider_request_id=$1 FOR UPDATE`, providerRequestID).Scan(&uniqueID, &ow requestHash := sha256.Sum256([]byte(providerRequestID)) giftAddress := fmt.Sprintf("telesrv-gift:%s:%x", unique.Slug, requestHash[:8]) if _, err := tx.Exec(ctx, `UPDATE unique_star_gifts SET owner_peer_type=NULL,owner_peer_id=NULL, -owner_address=$2,gift_address=$3,updated_at=now() WHERE id=$1`, uniqueID, ownerAddress, giftAddress); err != nil { +owner_address=$2,gift_address=$3,craft_chance_permille=0,updated_at=now() WHERE id=$1`, uniqueID, ownerAddress, giftAddress); err != nil { return err } - if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET lifecycle_status='exported',unsaved=true,pinned_order=0 WHERE id=$1`, saved.ID); err != nil { + if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET lifecycle_status='exported',unsaved=true,pinned_order=0,can_craft_at=0 WHERE id=$1`, saved.ID); err != nil { return err } if _, err := tx.Exec(ctx, `UPDATE star_gift_withdrawal_requests SET status='completed',completed_at=$2 WHERE provider_request_id=$1`, providerRequestID, date); err != nil { return err } + unique.Owner = domain.Peer{} + unique.OwnerAddress = ownerAddress + unique.GiftAddress = giftAddress + unique.CraftChancePermille = 0 + if _, err := s.retireUserStarGiftMessagesTx(ctx, tx, saved, unique, date); err != nil { + return err + } return updateStarGiftResaleProjection(ctx, tx, unique.GiftID) }) if err != nil { diff --git a/internal/store/postgres/star_gift_lifecycle_integration_test.go b/internal/store/postgres/star_gift_lifecycle_integration_test.go index fdec70bd..2102abc6 100644 --- a/internal/store/postgres/star_gift_lifecycle_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_integration_test.go @@ -140,14 +140,61 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, prepaid.Send.RecipientMessa t.Fatalf("upgrade prepaid gift: %v", err) } if upgraded.Saved.TransferStars != 25 || upgraded.Saved.DropOriginalDetailsStars != 25 || + upgraded.Saved.CanCraftAt != now+2 || upgraded.Unique.CraftChancePermille != 500 || !upgraded.Unique.KeepOriginalDetails { t.Fatalf("issued lifecycle snapshot = saved %+v unique %+v", upgraded.Saved, upgraded.Unique) } + readinessTx, err := pool.Begin(ctx) + if err != nil { + t.Fatalf("begin craft readiness guard probe: %v", err) + } + if _, err = readinessTx.Exec(ctx, `UPDATE peer_star_gifts SET can_craft_at=0 WHERE id=$1`, upgraded.Saved.ID); err != nil { + _ = readinessTx.Rollback(ctx) + t.Fatalf("stage mismatched craft readiness: %v", err) + } + if _, err = readinessTx.Exec(ctx, `SET CONSTRAINTS ALL IMMEDIATE`); err == nil { + _ = readinessTx.Rollback(ctx) + t.Fatal("deferred guard accepted positive craft chance with zero readiness") + } + _ = readinessTx.Rollback(ctx) + chanceTx, err := pool.Begin(ctx) + if err != nil { + t.Fatalf("begin craft chance guard probe: %v", err) + } + if _, err = chanceTx.Exec(ctx, `UPDATE unique_star_gifts SET craft_chance_permille=0 WHERE id=$1`, upgraded.Unique.ID); err != nil { + _ = chanceTx.Rollback(ctx) + t.Fatalf("stage mismatched craft chance: %v", err) + } + if _, err = chanceTx.Exec(ctx, `SET CONSTRAINTS ALL IMMEDIATE`); err == nil { + _ = chanceTx.Rollback(ctx) + t.Fatal("deferred guard accepted positive readiness with zero craft chance") + } + _ = chanceTx.Rollback(ctx) + terminalTx, err := pool.Begin(ctx) + if err != nil { + t.Fatalf("begin atomic craft terminal guard probe: %v", err) + } + if _, err = terminalTx.Exec(ctx, `UPDATE unique_star_gifts SET craft_chance_permille=0 WHERE id=$1`, upgraded.Unique.ID); err != nil { + _ = terminalTx.Rollback(ctx) + t.Fatalf("stage terminal craft chance: %v", err) + } + if _, err = terminalTx.Exec(ctx, `UPDATE peer_star_gifts SET can_craft_at=0 WHERE id=$1`, upgraded.Saved.ID); err != nil { + _ = terminalTx.Rollback(ctx) + t.Fatalf("stage terminal craft readiness: %v", err) + } + if _, err = terminalTx.Exec(ctx, `SET CONSTRAINTS ALL IMMEDIATE`); err != nil { + _ = terminalTx.Rollback(ctx) + t.Fatalf("deferred guard rejected atomic craft terminal state: %v", err) + } + if err = terminalTx.Rollback(ctx); err != nil { + t.Fatalf("rollback atomic craft terminal guard probe: %v", err) + } upgradeAction := upgraded.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique senderUpgradeAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID) - if upgradeAction == nil || upgradeAction.SavedID != int64(purchased.Saved.MsgID) || - senderUpgradeAction == nil || senderUpgradeAction.SavedID != 0 || + if upgradeAction == nil || upgradeAction.SavedID != 0 || upgradeAction.Peer.Type != "" || upgradeAction.Peer.ID != 0 || + upgradeAction.CanCraftAt != now+2 || senderUpgradeAction == nil || senderUpgradeAction.SavedID != 0 || + senderUpgradeAction.CanCraftAt != now+2 || ownerSourceEdit.Message.Media == nil || ownerSourceEdit.Message.Media.ServiceAction == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != upgraded.Saved.UpgradeMsgID || @@ -307,6 +354,37 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, upgraded.Send.RecipientMess if err != nil || transferred.Unique.Owner != ownerPeer || transferred.Saved.TransferStars != 25 || transferred.Balance.Balance != 9975 { t.Fatalf("paid transfer = %+v err %v", transferred, err) } + var retiredSourceMediaJSON string + var retiredSourcePTS int + if err := pool.QueryRow(ctx, `SELECT media::text,pts FROM message_boxes +WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, resaleBuyer.ID, resold.Saved.MsgID). + Scan(&retiredSourceMediaJSON, &retiredSourcePTS); err != nil { + t.Fatalf("load retired transfer source projection: %v", err) + } + retiredSourceMedia, err := decodeMessageMedia(retiredSourceMediaJSON) + if err != nil || retiredSourceMedia == nil || retiredSourceMedia.ServiceAction == nil || + retiredSourceMedia.ServiceAction.StarGiftUnique == nil { + t.Fatalf("decode retired transfer source projection: media=%+v err=%v", retiredSourceMedia, err) + } + retiredSourceAction := retiredSourceMedia.ServiceAction.StarGiftUnique + if retiredSourceAction.Gift.Owner != ownerPeer || retiredSourceAction.Gift.CraftChancePermille != 0 || + !retiredSourceAction.Transferred || retiredSourceAction.Saved || retiredSourceAction.CanCraftAt != 0 || + retiredSourceAction.CanExportAt != 0 || retiredSourceAction.TransferStars != 0 || + retiredSourceAction.CanTransferAt != 0 || retiredSourceAction.CanResellAt != 0 || + retiredSourceAction.DropOriginalDetailsStars != 0 || retiredSourceAction.ResaleAmount != nil { + t.Fatalf("retired transfer source remained actionable: %+v", retiredSourceAction) + } + var retiredEventCount, retiredOutboxCount int + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM user_update_events +WHERE user_id=$1 AND pts=$2 AND event_type='edit_message' AND message_box_id=$3`, resaleBuyer.ID, retiredSourcePTS, resold.Saved.MsgID). + Scan(&retiredEventCount); err != nil || retiredEventCount != 1 { + t.Fatalf("retired transfer source event count=%d err=%v", retiredEventCount, err) + } + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM dispatch_outbox +WHERE target_user_id=$1 AND pts=$2 AND event_type='edit_message'`, resaleBuyer.ID, retiredSourcePTS). + Scan(&retiredOutboxCount); err != nil || retiredOutboxCount != 1 { + t.Fatalf("retired transfer source outbox count=%d err=%v", retiredOutboxCount, err) + } clearedUser, found, err := users.ByID(ctx, resaleBuyer.ID) if err != nil || !found || !clearedUser.EmojiStatus().Empty() { t.Fatalf("transferred collectible status was not cleared: user=%+v found=%v err=%v", clearedUser, found, err) @@ -381,13 +459,13 @@ WHERE target_user_id=$1 AND pts=$2 AND event_type='user_emoji_status'`, resaleBu if _, err := gifts.ResolveSavedIDs(ctx, ownerPeer, []domain.SavedStarGiftRef{ {Owner: ownerPeer, MsgID: secondUpgrade.Saved.UpgradeMsgID}, {Owner: ownerPeer, Slug: secondUpgrade.Unique.Slug}, - }); !errors.Is(err, domain.ErrStarGiftNotFound) { - t.Fatalf("upgrade message id lookup err = %v, want ErrStarGiftNotFound", err) + }); !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) { + t.Fatalf("duplicate upgrade output/slug identities err = %v", err) } if saved, found, err := gifts.GetByRef(ctx, domain.SavedStarGiftRef{ Owner: ownerPeer, MsgID: secondUpgrade.Saved.UpgradeMsgID, - }); err != nil || found { - t.Fatalf("upgrade message id resolved a gift: saved=%+v found=%v err=%v", saved, found, err) + }); err != nil || !found || saved.ID != secondUpgrade.Saved.ID { + t.Fatalf("upgrade output message id failed to resolve: saved=%+v found=%v err=%v", saved, found, err) } if _, err := gifts.ResolveSavedIDs(ctx, ownerPeer, []domain.SavedStarGiftRef{ {Owner: ownerPeer, MsgID: secondUpgrade.Saved.MsgID}, @@ -406,6 +484,15 @@ WHERE target_user_id=$1 AND pts=$2 AND event_type='user_emoji_status'`, resaleBu if err != nil || !crafted.Success || crafted.Chance != 1000 || crafted.Gift == nil || !crafted.Gift.Crafted || crafted.Send.RecipientMessage.ID <= 0 { t.Fatalf("craft result = %+v err %v", crafted, err) } + craftOutputAction := crafted.Send.SenderMessage.Media.ServiceAction.StarGiftUnique + if craftOutputAction == nil || craftOutputAction.Peer.Type != "" || craftOutputAction.Peer.ID != 0 || craftOutputAction.SavedID != 0 || !craftOutputAction.Craft { + t.Fatalf("craft output action leaked channel identity: %+v", craftOutputAction) + } + if byOutput, found, err := gifts.GetByRef(ctx, domain.SavedStarGiftRef{ + Owner: ownerPeer, MsgID: crafted.Send.SenderMessage.ID, + }); err != nil || !found || byOutput.ID != transferred.Saved.ID || byOutput.UniqueGiftID != crafted.Gift.ID { + t.Fatalf("craft output message ref = %+v found %v err %v", byOutput, found, err) + } craftedInputEdit := craftedSourceEditForUserAndGift(crafted, owner.ID, transferred.Unique.ID) craftedInputAction := starGiftUniqueActionFromEdit(craftedInputEdit) burnedInputEdit := craftedSourceEditForUserAndGift(crafted, owner.ID, secondUpgrade.Unique.ID) @@ -446,6 +533,40 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, edit.Message.ID).Scan(&shar craftedSourceEditForUserAndGift(craftedReplay, owner.ID, secondUpgrade.Unique.ID).Event.Pts != burnedInputEdit.Event.Pts { t.Fatalf("craft success replay = %+v err %v", craftedReplay, err) } + craftOutputRef := domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: crafted.Send.SenderMessage.ID} + if changed, err := gifts.SetUnsaved(ctx, craftOutputRef, true); err != nil || !changed { + t.Fatalf("hide crafted output before replay: changed=%v err=%v", changed, err) + } + hiddenReplay, err := lifecycle.CraftStarGift(ctx, craftReq) + if err != nil || !hiddenReplay.Duplicate || hiddenReplay.Send.SenderMessage.ID != crafted.Send.SenderMessage.ID { + t.Fatalf("craft replay after hide = %+v err %v", hiddenReplay, err) + } + if changed, err := gifts.SetUnsaved(ctx, craftOutputRef, false); err != nil || !changed { + t.Fatalf("restore crafted output before listing replay: changed=%v err=%v", changed, err) + } + listedCraftOutput, err := lifecycle.SetStarGiftListing(ctx, domain.StarGiftListingRequest{ActorUserID: owner.ID, + Ref: craftOutputRef, Amount: &domain.StarGiftAmount{Currency: domain.StarGiftCurrencyStars, Amount: 125}, Date: now + 148, + }) + if err != nil || listedCraftOutput.ResellAmount == nil || listedCraftOutput.ResellAmount.Amount != 125 { + t.Fatalf("list crafted output before replay = %+v err %v", listedCraftOutput, err) + } + listedReplay, err := lifecycle.CraftStarGift(ctx, craftReq) + if err != nil || !listedReplay.Duplicate || listedReplay.Gift == nil || listedReplay.Gift.ResellAmount != nil || + listedReplay.Send.SenderMessage.ID != crafted.Send.SenderMessage.ID { + t.Fatalf("craft replay after listing did not use frozen output = %+v err %v", listedReplay, err) + } + if _, err := lifecycle.SetStarGiftListing(ctx, domain.StarGiftListingRequest{ActorUserID: owner.ID, + Ref: craftOutputRef, Date: now + 149, + }); err != nil { + t.Fatalf("remove crafted output listing: %v", err) + } + var outputReceiptMedia string + var outputReceiptFingerprint []byte + if err := pool.QueryRow(ctx, `SELECT output_media::text,output_fingerprint FROM star_gift_craft_commands +WHERE user_id=$1 AND command_key=$2`, owner.ID, craftReq.CommandKey).Scan(&outputReceiptMedia, &outputReceiptFingerprint); err != nil || + outputReceiptMedia == "" || len(outputReceiptFingerprint) != 32 { + t.Fatalf("craft immutable output receipt: media=%q fingerprint=%d err=%v", outputReceiptMedia, len(outputReceiptFingerprint), err) + } var craftListings, resaleAvailability int if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_listings WHERE unique_gift_id=ANY($1::bigint[])`, []int64{transferred.Unique.ID, secondUpgrade.Unique.ID}).Scan(&craftListings); err != nil || craftListings != 0 { @@ -487,7 +608,7 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, edit.Message.ID).Scan(&shar WithStarGiftMarketPolicy(domain.StarGiftMarketPolicy{StarsProceedsPermille: 900, TONProceedsPermille: 900}), WithStarGiftCraftDraw(func(upper int) (int, error) { return upper - 1, nil })) failureReq := domain.StarGiftCraftRequest{UserID: owner.ID, - Refs: []domain.SavedStarGiftRef{{Owner: ownerPeer, MsgID: thirdUpgrade.Saved.MsgID}}, + Refs: []domain.SavedStarGiftRef{{Owner: ownerPeer, MsgID: thirdUpgrade.Saved.UpgradeMsgID}}, CommandKey: "craft-fail-" + suffix, Date: now + 150, } failedCraft, err := failingLifecycle.CraftStarGift(ctx, failureReq) @@ -526,6 +647,11 @@ can_resell_at,drop_original_details_stars,can_craft_at FROM peer_star_gifts WHER craftedSourceEditForUserAndGift(failedReplay, owner.ID, thirdUpgrade.Unique.ID).Event.Pts != failedInputEdit.Event.Pts { t.Fatalf("craft failure replay = %+v err %v", failedReplay, err) } + wrongAliasReplay := failureReq + wrongAliasReplay.Refs = []domain.SavedStarGiftRef{{Owner: ownerPeer, MsgID: secondUpgrade.Saved.UpgradeMsgID}} + if _, err := failingLifecycle.CraftStarGift(ctx, wrongAliasReplay); !errors.Is(err, domain.ErrStarGiftCraftUnavailable) { + t.Fatalf("craft replay accepted another aggregate alias: %v", err) + } invalidRetry := failureReq invalidRetry.CommandKey = "craft-fail-new-command-" + suffix if _, err := failingLifecycle.CraftStarGift(ctx, invalidRetry); !errors.Is(err, domain.ErrStarGiftCraftUnavailable) { @@ -629,10 +755,14 @@ func TestStarGiftChannelLifecycleAtomicPostgres(t *testing.T) { } if _, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 5, SlugPrefix: "channel-life-" + suffix, - Models: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleModel, Name: "Channel Model", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestDocumentPtr(baseDocumentID+1, "channel-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "channel-model"), Animation: collectibleTestAnimationPtr("channel-model.tgs")}}, + Models: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleModel, Name: "Channel Model", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+1, "channel-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "channel-model"), Animation: collectibleTestAnimationPtr("channel-model.tgs")}, + {Kind: domain.StarGiftCollectibleModel, Name: "Channel Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true, + Document: collectibleTestDocumentPtr(baseDocumentID+2, "channel-crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "channel-crafted"), Animation: collectibleTestAnimationPtr("channel-crafted.tgs")}, + }, Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Channel Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "channel-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "channel-pattern"), Animation: collectibleTestAnimationPtr("channel-pattern.tgs")}}, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "channel-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "channel-pattern"), Animation: collectibleTestAnimationPtr("channel-pattern.tgs")}}, Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Channel Backdrop", BackdropID: 88, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}}, @@ -761,7 +891,9 @@ WHERE channel_id=$1 AND message::text LIKE '%prepaid_upgrade%'`, created.Channel } action := upgraded.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique if action == nil || action.FromUserID != domain.OfficialSystemUserID || action.Peer != channelPeer || - action.SavedID != prepaidPurchase.Saved.SavedID || !action.Upgrade || !action.PrepaidUpgrade || action.TransferStars != 25 { + action.SavedID != prepaidPurchase.Saved.SavedID || !action.Upgrade || !action.PrepaidUpgrade || action.TransferStars != 25 || + action.CanCraftAt != 0 || action.Gift.CraftChancePermille != 500 || + upgraded.Saved.CanCraftAt != now+5 || upgraded.Unique.CraftChancePermille != 500 { t.Fatalf("channel upgrade service action = %+v", action) } var ptsAfterUpgrade int @@ -805,7 +937,9 @@ WHERE channel_id=$1 AND message::text LIKE '%star_gift_unique%'`, created.Channe } resold, err := lifecycle.PurchaseResaleStarGift(ctx, resaleReq) if err != nil || resold.Unique.Owner != targetChannelPeer || resold.Saved.Owner != targetChannelPeer || - resold.Saved.SavedID != upgraded.Saved.ID || resold.Balance.Balance != 999000 { + resold.Saved.SavedID != upgraded.Saved.ID || resold.Balance.Balance != 999000 || + resold.Saved.CanCraftAt != upgraded.Saved.CanCraftAt || + resold.Unique.CraftChancePermille != upgraded.Unique.CraftChancePermille { t.Fatalf("channel-to-channel local TON resale = %+v err %v", resold, err) } var channelTON, channelTONTxns, targetResaleLogs, commission int64 @@ -837,6 +971,36 @@ WHERE channel_id=$1 AND message::text LIKE '%star_gift_unique%'`, created.Channe if err := pool.QueryRow(ctx, `SELECT balance_nanoton FROM channel_ton_balances WHERE channel_id=$1`, created.Channel.ID).Scan(&channelTON); err != nil || channelTON != 900 { t.Fatalf("channel TON proceeds after replay = %d err %v", channelTON, err) } + toUser, err := lifecycle.TransferStarGift(ctx, domain.StarGiftTransferRequest{ + ActorUserID: actor.ID, + Ref: domain.SavedStarGiftRef{ + Owner: targetChannelPeer, SavedID: resold.Saved.SavedID, + }, + To: domain.Peer{Type: domain.PeerTypeUser, ID: actor.ID}, ChargeStars: resold.Saved.TransferStars, + CommandKey: "channel-craft-entitlement-to-user-" + suffix, Date: now + 8, + }) + if err != nil || toUser.Saved.Owner.Type != domain.PeerTypeUser || toUser.Saved.Owner.ID != actor.ID || + toUser.Saved.CanCraftAt != upgraded.Saved.CanCraftAt || + toUser.Unique.CraftChancePermille != upgraded.Unique.CraftChancePermille { + t.Fatalf("channel-to-user Craft entitlement transfer = %+v err %v", toUser, err) + } + toUserAction := toUser.Send.SenderMessage.Media.ServiceAction.StarGiftUnique + if toUserAction == nil || toUserAction.CanCraftAt != upgraded.Saved.CanCraftAt { + t.Fatalf("channel-to-user action did not restore Craft readiness: %+v", toUserAction) + } + backToChannel, err := lifecycle.TransferStarGift(ctx, domain.StarGiftTransferRequest{ + ActorUserID: actor.ID, + Ref: domain.SavedStarGiftRef{ + Owner: toUser.Saved.Owner, MsgID: toUser.Saved.MsgID, + }, + To: channelPeer, ChargeStars: toUser.Saved.TransferStars, + CommandKey: "user-craft-entitlement-to-channel-" + suffix, Date: now + 9, + }) + if err != nil || backToChannel.Saved.Owner != channelPeer || + backToChannel.Saved.CanCraftAt != upgraded.Saved.CanCraftAt || + backToChannel.Unique.CraftChancePermille != upgraded.Unique.CraftChancePermille { + t.Fatalf("user-to-channel Craft entitlement transfer = %+v err %v", backToChannel, err) + } var remainsBefore int if err := pool.QueryRow(ctx, `SELECT availability_remains FROM star_gift_catalog WHERE gift_id=$1`, entry.Gift.ID).Scan(&remainsBefore); err != nil { @@ -890,6 +1054,119 @@ WHERE channel_id=$1 AND message::text LIKE '%auction_acquired%'`, created.Channe } } +func TestStarGiftCraftFailureConsumesThreeInputsPostgres(t *testing.T) { + pool := testPool(t) + ctx := context.Background() + suffix := randomSuffix(t) + now := int(time.Now().Unix()) + users := NewUserStore(pool) + buyer := createTestUser(t, ctx, users, "+1883"+suffix+"01", "CraftBuyer", "") + owner := createTestUser(t, ctx, users, "+1883"+suffix+"02", "CraftOwner", "") + ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID} + stars := NewStarsStore(pool) + for _, userID := range []int64{buyer.ID, owner.ID} { + if _, _, err := stars.EnsureGrant(ctx, userID, 10000, now); err != nil { + t.Fatalf("grant craft stars to %d: %v", userID, err) + } + } + + gifts := NewStarGiftStore(pool) + baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000 + entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + Title: "Three Input Craft " + suffix, Stars: 50, ConvertStars: 20, Enabled: true, + Document: collectibleTestDocument(baseDocumentID, "three-input.tgs"), + Blob: collectibleTestBlob(baseDocumentID, "three-input"), Animation: collectibleTestAnimation("three-input.tgs"), + Actor: "integration", CommandID: "three-input-catalog-" + suffix, + }) + if err != nil { + t.Fatalf("create three-input catalog: %v", err) + } + if _, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ + GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "three-" + suffix, + Models: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleModel, Name: "Base", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+1, "base.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "base"), Animation: collectibleTestAnimationPtr("base.tgs")}, + {Kind: domain.StarGiftCollectibleModel, Name: "Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true, + Document: collectibleTestDocumentPtr(baseDocumentID+2, "crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "crafted"), Animation: collectibleTestAnimationPtr("crafted.tgs")}, + }, + Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", + RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}}, + Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 88, + CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, + RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}}, + Actor: "integration", CommandID: "three-input-pool-" + suffix, + }); err != nil { + t.Fatalf("publish three-input collectible: %v", err) + } + + messages := NewMessageStore(pool) + lifecycle := NewStarGiftLifecycleStore(pool, messages, 1_000_000, + WithStarGiftCraftDraw(func(upper int) (int, error) { return upper - 1, nil })) + upgrades := NewStarGiftUpgradeStore(pool, messages, WithStarGiftLifecyclePolicy(domain.StarGiftLifecyclePolicy{ + TransferStars: 25, DropOriginalDetailsStars: 25, OfferMinStars: 1, CraftChancePermille: 250, + })) + refs := make([]domain.SavedStarGiftRef, 0, 3) + uniqueIDs := make([]int64, 0, 3) + for i := 0; i < 3; i++ { + purchaseReq := issueLifecyclePurchaseForm(t, ctx, lifecycle, domain.StarGiftPurchaseRequest{ + BuyerUserID: buyer.ID, To: ownerPeer, GiftID: entry.Gift.ID, IncludeUpgrade: true, + CommandKey: fmt.Sprintf("three-input-purchase-%s-%d", suffix, i), Date: now + i, + }) + purchased, err := lifecycle.PurchaseStarGift(ctx, purchaseReq) + if err != nil { + t.Fatalf("purchase three-input gift %d: %v", i, err) + } + upgraded, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{UserID: owner.ID, + Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID}, RequirePrepaid: true, + CommandKey: fmt.Sprintf("three-input-upgrade-%s-%d", suffix, i), Date: now + 10 + i, + }) + if err != nil { + t.Fatalf("upgrade three-input gift %d: %v", i, err) + } + refs = append(refs, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: upgraded.Saved.UpgradeMsgID}) + uniqueIDs = append(uniqueIDs, upgraded.Unique.ID) + } + req := domain.StarGiftCraftRequest{UserID: owner.ID, Refs: refs, + CommandKey: "three-input-craft-fail-" + suffix, Date: now + 20} + failed, err := lifecycle.CraftStarGift(ctx, req) + if err != nil || failed.Success || failed.Chance != 750 || failed.Gift != nil { + t.Fatalf("three-input craft failure = %+v err=%v", failed, err) + } + for _, uniqueID := range uniqueIDs { + edit := craftedSourceEditForUserAndGift(failed, owner.ID, uniqueID) + action := starGiftUniqueActionFromEdit(edit) + if edit.Event.Pts <= 0 || action == nil || !action.Gift.Burned || action.Gift.CraftChancePermille != 0 || + action.Saved || action.CanCraftAt != 0 { + t.Fatalf("three-input terminal edit for %d = %+v", uniqueID, edit) + } + var burned bool + var status string + if err := pool.QueryRow(ctx, `SELECT u.burned,p.lifecycle_status +FROM unique_star_gifts u JOIN peer_star_gifts p ON p.unique_gift_id=u.id WHERE u.id=$1`, uniqueID). + Scan(&burned, &status); err != nil || !burned || status != "burned" { + t.Fatalf("three-input terminal aggregate %d burned=%v status=%q err=%v", uniqueID, burned, status, err) + } + } + var sourcePTS []int32 + var outputMedia, outputFingerprint []byte + if err := pool.QueryRow(ctx, `SELECT source_edit_pts,output_media,output_fingerprint +FROM star_gift_craft_commands WHERE user_id=$1 AND command_key=$2`, owner.ID, req.CommandKey). + Scan(&sourcePTS, &outputMedia, &outputFingerprint); err != nil || len(sourcePTS) != 3 || + len(outputMedia) != 0 || len(outputFingerprint) != 0 { + t.Fatalf("three-input failure receipt pts=%v media=%d fingerprint=%d err=%v", sourcePTS, len(outputMedia), len(outputFingerprint), err) + } + replay, err := lifecycle.CraftStarGift(ctx, req) + if err != nil || !replay.Duplicate || replay.Success || replay.Chance != 750 { + t.Fatalf("three-input failure replay = %+v err=%v", replay, err) + } + for i, uniqueID := range uniqueIDs { + if edit := craftedSourceEditForUserAndGift(replay, owner.ID, uniqueID); edit.Event.Pts != int(sourcePTS[i]) { + t.Fatalf("three-input replay pts for %d = %d want %d", uniqueID, edit.Event.Pts, sourcePTS[i]) + } + } +} + func issueLifecyclePurchaseForm(t *testing.T, ctx context.Context, lifecycle *StarGiftLifecycleStore, req domain.StarGiftPurchaseRequest) domain.StarGiftPurchaseRequest { t.Helper() diff --git a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go index a66c4fb5..f908b6d3 100644 --- a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go @@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) { if err != nil { t.Fatalf("migrate star gift lifecycle schema: %v", err) } - if status.Dirty || status.Empty || status.Version != 125 { - t.Fatalf("migration status = %+v, want clean version 125", status) + if status.Dirty || status.Empty || status.Version != 129 { + t.Fatalf("migration status = %+v, want clean version 129", status) } } diff --git a/internal/store/postgres/star_gift_lifecycle_projection.go b/internal/store/postgres/star_gift_lifecycle_projection.go new file mode 100644 index 00000000..86923ca5 --- /dev/null +++ b/internal/store/postgres/star_gift_lifecycle_projection.go @@ -0,0 +1,196 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "sort" + + "github.com/jackc/pgx/v5" + + "telesrv/internal/domain" + "telesrv/internal/store/postgres/sqlcgen" +) + +// retireUserStarGiftMessagesTx closes every user-scoped unique-gift action +// emitted for the source ownership epoch. Ownership moves and terminal export +// must not leave an older chat card with Craft/transfer/resale capabilities. +// The aggregate mutation and all message edits share one transaction and each +// visible box receives its own durable pts/event/outbox entry. +func (s *StarGiftLifecycleStore) retireUserStarGiftMessagesTx( + ctx context.Context, + tx pgx.Tx, + source domain.SavedStarGift, + current domain.UniqueStarGift, + date int, +) ([]domain.EditedMessageForUser, error) { + if s == nil || s.messages == nil || source.Owner.Type != domain.PeerTypeUser || source.Owner.ID <= 0 || + source.ID <= 0 || source.UniqueGiftID <= 0 || current.ID != source.UniqueGiftID || date <= 0 { + return nil, domain.ErrStarGiftTransferUnavailable + } + + messageIDs := map[int]struct{}{} + if source.MsgID > 0 { + messageIDs[source.MsgID] = struct{}{} + } + if source.UpgradeMsgID > 0 { + messageIDs[source.UpgradeMsgID] = struct{}{} + } + rows, err := tx.Query(ctx, ` +SELECT msg_id FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND saved_gift_id=$2 +ORDER BY msg_id`, source.Owner.ID, source.ID) + if err != nil { + return nil, fmt.Errorf("list star gift message projections: %w", err) + } + for rows.Next() { + var msgID int + if err := rows.Scan(&msgID); err != nil { + rows.Close() + return nil, fmt.Errorf("scan star gift message projection: %w", err) + } + if msgID > 0 { + messageIDs[msgID] = struct{}{} + } + } + if err := rows.Err(); err != nil { + rows.Close() + return nil, fmt.Errorf("iterate star gift message projections: %w", err) + } + rows.Close() + + ids := make([]int, 0, len(messageIDs)) + for msgID := range messageIDs { + ids = append(ids, msgID) + } + sort.Ints(ids) + + q := sqlcgen.New(tx) + edits := make([]domain.EditedMessageForUser, 0, len(ids)*2) + seenPrivateMessages := make(map[string]struct{}, len(ids)) + for _, msgID := range ids { + var peerType string + var peerID int64 + err := tx.QueryRow(ctx, ` +SELECT peer_type,peer_id FROM message_boxes +WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted +FOR UPDATE`, source.Owner.ID, msgID).Scan(&peerType, &peerID) + if errors.Is(err, pgx.ErrNoRows) { + continue + } + if err != nil { + return nil, fmt.Errorf("lock star gift message projection: %w", err) + } + if peerType != string(domain.PeerTypeUser) || peerID <= 0 { + return nil, fmt.Errorf("star gift message projection %d is not private", msgID) + } + target, err := q.GetMessageBoxForEdit(ctx, sqlcgen.GetMessageBoxForEditParams{ + OwnerUserID: source.Owner.ID, BoxID: int32(msgID), PeerType: peerType, PeerID: peerID, + }) + if err != nil { + return nil, fmt.Errorf("load star gift message projection: %w", err) + } + logicalKey := fmt.Sprintf("%d:%d", target.MessageSenderID, target.PrivateMessageID) + if _, duplicate := seenPrivateMessages[logicalKey]; duplicate { + continue + } + seenPrivateMessages[logicalKey] = struct{}{} + + boxes, err := q.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{ + OwnerUserIds: privateMessageOwnerIDs(source.Owner.ID, peerID), + MessageSenderID: target.MessageSenderID, PrivateMessageID: target.PrivateMessageID, + }) + if err != nil { + return nil, fmt.Errorf("list visible star gift message projections: %w", err) + } + var privateMediaJSON []byte + matched := false + for _, box := range boxes { + media, err := decodeMessageMedia(box.MediaJson) + if err != nil { + return nil, fmt.Errorf("decode star gift message projection: %w", err) + } + if media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil || + media.ServiceAction.Kind != domain.MessageServiceActionStarGiftUnique || media.ServiceAction.StarGiftUnique == nil || + media.ServiceAction.StarGiftUnique.Gift.ID != current.ID { + continue + } + matched = true + action := media.ServiceAction.StarGiftUnique + retiredGift := current + retiredGift.CraftChancePermille = 0 + retiredGift.ResellAmount = nil + action.Gift = retiredGift + action.Peer = domain.Peer{} + action.SavedID = 0 + action.Saved = false + if validLifecyclePeer(current.Owner) && current.Owner != source.Owner { + action.Transferred = true + } + action.CanExportAt = 0 + action.TransferStars = 0 + action.ResaleAmount = nil + action.CanTransferAt = 0 + action.CanResellAt = 0 + action.DropOriginalDetailsStars = 0 + action.CanCraftAt = 0 + + mediaJSON, err := encodeMessageMedia(media) + if err != nil { + return nil, fmt.Errorf("encode retired star gift projection: %w", err) + } + pts, err := s.messages.reservePts(ctx, tx, box.OwnerUserID) + if err != nil { + return nil, fmt.Errorf("allocate retired star gift pts: %w", err) + } + tag, err := tx.Exec(ctx, ` +UPDATE message_boxes SET media=$3,pts=$4 +WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, box.OwnerUserID, box.BoxID, mediaJSON, int32(pts)) + if err != nil { + return nil, fmt.Errorf("update retired star gift projection: %w", err) + } + if tag.RowsAffected() != 1 { + return nil, fmt.Errorf("update retired star gift projection lost row") + } + msg, err := messageFromVisibleBoxRow(box) + if err != nil { + return nil, err + } + msg.Media = media + msg.Pts = pts + if err := replaceMessageBoxMediaIndexTx(ctx, tx, msg.OwnerUserID, msg.Peer.ID, msg.ID, msg.Date, msg.Media, msg.Entities); err != nil { + return nil, err + } + event := domain.UpdateEvent{UserID: msg.OwnerUserID, Type: domain.UpdateEventEditMessage, + Pts: pts, PtsCount: 1, Date: date, Message: msg} + if err := appendUserUpdateEvent(ctx, tx, q, msg.OwnerUserID, event); err != nil { + return nil, fmt.Errorf("append retired star gift edit event: %w", err) + } + if err := enqueueDispatch(ctx, q, sqlcgen.EnqueueDispatchParams{ + TargetUserID: msg.OwnerUserID, Pts: int32(pts), EventType: string(domain.UpdateEventEditMessage), + ExcludeAuthKeyID: 0, ExcludeSessionID: 0, + }); err != nil { + return nil, fmt.Errorf("enqueue retired star gift edit: %w", err) + } + if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 { + privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media) + if err != nil { + return nil, err + } + } + edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event}) + } + if !matched { + continue + } + if len(privateMediaJSON) == 0 { + return nil, fmt.Errorf("retired star gift projection missing shared media") + } + if _, err := tx.Exec(ctx, ` +UPDATE private_messages SET media=$3 +WHERE sender_user_id=$1 AND id=$2`, target.MessageSenderID, target.PrivateMessageID, privateMediaJSON); err != nil { + return nil, fmt.Errorf("update retired star gift private media: %w", err) + } + } + return edits, nil +} diff --git a/internal/store/postgres/star_gift_lifecycle_test.go b/internal/store/postgres/star_gift_lifecycle_test.go index 04190537..7e844cc7 100644 --- a/internal/store/postgres/star_gift_lifecycle_test.go +++ b/internal/store/postgres/star_gift_lifecycle_test.go @@ -7,19 +7,38 @@ import ( ) func TestTransferUniqueActionSavedIDNamespace(t *testing.T) { - saved := domain.SavedStarGift{SavedID: 42} + saved := domain.SavedStarGift{SavedID: 42, CanCraftAt: 1_780_000_123} unique := domain.UniqueStarGift{ID: 7} user := domain.Peer{Type: domain.PeerTypeUser, ID: 100} channel := domain.Peer{Type: domain.PeerTypeChannel, ID: 200} - if action := transferUniqueAction(unique, 1, user, saved); action.SavedID != 0 { + if action := transferUniqueAction(unique, 1, user, saved); action.SavedID != 0 || action.CanCraftAt != saved.CanCraftAt { t.Fatalf("user transfer action leaked channel saved_id: %+v", action) } - if action := transferUniqueAction(unique, 1, channel, saved); action.SavedID != saved.SavedID { + if action := transferUniqueAction(unique, 1, channel, saved); action.SavedID != saved.SavedID || action.CanCraftAt != 0 { t.Fatalf("channel transfer action lost channel saved_id: %+v", action) } } +func TestStarGiftCraftReadyAt(t *testing.T) { + const date = 1_780_000_000 + if got := starGiftCraftReadyAt(date, 0); got != date { + t.Fatalf("zero-delay craft ready_at = %d, want %d", got, date) + } + if got := starGiftCraftReadyAt(date, 60); got != date+60 { + t.Fatalf("delayed craft ready_at = %d, want %d", got, date+60) + } + if got := starGiftCraftReadyAt(0, 0); got != 0 { + t.Fatalf("invalid-date craft ready_at = %d, want 0", got) + } + if got := starGiftCraftReadyAt(1<<31-10, 60); got != 1<<31-1 { + t.Fatalf("overflow craft ready_at = %d, want max int32", got) + } + if got := starGiftCraftReadyAt(1<<31+10, 0); got != 1<<31-1 { + t.Fatalf("oversized-date craft ready_at = %d, want max int32", got) + } +} + func TestEncodeSharedPrivateStarGiftMediaOmitsUserBoxLocalRefs(t *testing.T) { ordinary := &domain.MessageMedia{ Kind: domain.MessageMediaKindService, diff --git a/internal/store/postgres/star_gift_private_projection.go b/internal/store/postgres/star_gift_private_projection.go index 2fcfc665..8ed76d18 100644 --- a/internal/store/postgres/star_gift_private_projection.go +++ b/internal/store/postgres/star_gift_private_projection.go @@ -11,11 +11,10 @@ import ( // projectPrivateStarGiftSourceRef exposes a user-owned gift's stable source // message identity only in the gift owner's message-box projection. Telegram -// defines gift_msg_id as receiver-only, and TDesktop also treats user unique -// saved_id as an inputSavedStarGiftUser identity. A non-owner counterpart box -// id is therefore not a valid substitute: it could resolve to an unrelated -// gift owned by that viewer. The shared private_messages row omits the local -// reference for the same reason. +// defines gift_msg_id as receiver-only. A non-owner counterpart box id is not +// a valid substitute: it could resolve to an unrelated gift owned by that +// viewer. User unique actions do not use channel-only peer/saved_id fields; +// their owner-scoped message ids are registered separately at write time. func projectPrivateStarGiftSourceRef( _ context.Context, _ pgx.Tx, @@ -60,25 +59,6 @@ func projectPrivateStarGiftSourceRef( } else { recipientAction.GiftMsgID = sourceOwnerBoxID } - case privateStarGiftUniqueAction(shared) != nil: - sharedAction := privateStarGiftUniqueAction(shared) - senderAction := privateStarGiftUniqueAction(sender) - recipientAction := privateStarGiftUniqueAction(recipient) - if sharedAction.Peer.Type != domain.PeerTypeUser || sharedAction.Peer.ID != sourceOwnerUserID || - sharedAction.SavedID != int64(sourceOwnerBoxID) { - return privateSendMediaProjection{}, fmt.Errorf( - "project private unique star gift source: saved_id %d does not match owner box %d", - sharedAction.SavedID, sourceOwnerBoxID, - ) - } - sharedAction.SavedID = 0 - senderAction.SavedID = 0 - recipientAction.SavedID = 0 - if req.SenderUserID == sourceOwnerUserID { - senderAction.SavedID = int64(sourceOwnerBoxID) - } else { - recipientAction.SavedID = int64(sourceOwnerBoxID) - } default: return privateSendMediaProjection{}, fmt.Errorf("project private star gift source: unsupported media") } diff --git a/internal/store/postgres/star_gift_upgrade.go b/internal/store/postgres/star_gift_upgrade.go index 56c89d93..b786e289 100644 --- a/internal/store/postgres/star_gift_upgrade.go +++ b/internal/store/postgres/star_gift_upgrade.go @@ -122,9 +122,15 @@ WHERE collectible_revision_id=$1 AND crafted } craftChancePermille := 0 canCraftAt := 0 + // Keep the durable Craft entitlement attached to the collectible across + // user/channel ownership moves. The RPC projection suppresses the + // readiness marker for channel owners until channel Craft execution is + // implemented, without destroying the official gift property. if craftable { craftChancePermille = s.lifecycle.CraftChancePermille - canCraftAt = starGiftReadyAt(req.Date, s.lifecycle.CraftDelaySeconds) + if craftChancePermille > 0 { + canCraftAt = starGiftCraftReadyAt(req.Date, s.lifecycle.CraftDelaySeconds) + } } if revision.Issued >= revision.SupplyTotal { return domain.ErrStarGiftCollectibleSoldOut @@ -230,12 +236,6 @@ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, req.UserID, commandKey, locked.ID, req.For } return nil }, - projectMedia: func(ctx context.Context, tx pgx.Tx, messageReq *domain.SendPrivateTextRequest) (privateSendMediaProjection, error) { - if result.Saved.Owner.Type != domain.PeerTypeUser { - return privateSendMediaProjection{Shared: messageReq.Media, Sender: messageReq.Media, Recipient: messageReq.Media}, nil - } - return projectPrivateStarGiftSourceRef(ctx, tx, messageReq, result.Saved.Owner.ID, result.Saved.MsgID) - }, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { ownerMessageID := sent.RecipientMessage.ID if saved.FromUserID == req.UserID { @@ -251,6 +251,12 @@ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, req.UserID, commandKey, locked.ID, req.For if tag.RowsAffected() != 1 { return fmt.Errorf("save star gift upgrade message id lost aggregate row") } + if result.Saved.Owner.Type == domain.PeerTypeUser { + if err := registerUserStarGiftMessageRef(ctx, tx, result.Saved.Owner.ID, ownerMessageID, + result.Saved.ID, result.Unique.ID); err != nil { + return err + } + } result.Saved.UpgradeMsgID = ownerMessageID if result.Saved.Owner.Type == domain.PeerTypeUser { edits, err := s.markPrivateStarGiftSourceUpgradedTx(ctx, tx, req, result.Saved, sent) @@ -311,19 +317,27 @@ func starGiftUpgradeUniqueAction(saved domain.SavedStarGift, unique domain.Uniqu // peer plus action.peer=channel and action.saved_id. fromUserID = messageSenderID } + peer := saved.Owner savedID := saved.SavedID + canCraftAt := saved.CanCraftAt if saved.Owner.Type == domain.PeerTypeUser { - // For user-owned gifts messageActionStarGiftUnique.saved_id is the - // stable source gift message id. TDesktop uses this back-reference as - // inputSavedStarGiftUser.msg_id for crafting and later lifecycle RPCs. - savedID = int64(saved.MsgID) + // peer and saved_id share one TL flag and are defined for channel gifts. + // For user gifts both must be absent; official clients use the emitted + // service-message id (registered owner-locally by the send transaction). + peer = domain.Peer{} + savedID = 0 + } else { + // The current Craft state machine is user-owned only. Android treats a + // positive can_craft_at as the channel Craft entry marker, so do not + // advertise a write path that the server cannot execute yet. + canCraftAt = 0 } return &domain.MessageStarGiftUniqueAction{ - Gift: unique, FromUserID: fromUserID, Peer: saved.Owner, SavedID: savedID, + Gift: unique, FromUserID: fromUserID, Peer: peer, SavedID: savedID, Upgrade: true, Saved: !saved.Unsaved, PrepaidUpgrade: req.RequirePrepaid, CanExportAt: saved.CanExportAt, TransferStars: saved.TransferStars, CanTransferAt: saved.CanTransferAt, CanResellAt: saved.CanResellAt, - DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: saved.CanCraftAt, + DropOriginalDetailsStars: saved.DropOriginalDetailsStars, CanCraftAt: canCraftAt, } } @@ -469,6 +483,27 @@ func starGiftReadyAt(date, delaySeconds int) int { return date + delaySeconds } +// starGiftCraftReadyAt differs intentionally from the other lifecycle delay +// fields. Official Android clients use a positive can_craft_at both as the +// capability marker and as the readiness boundary, so an immediately +// craftable gift must carry its upgrade date instead of omitting the field. +func starGiftCraftReadyAt(date, delaySeconds int) int { + if date <= 0 || delaySeconds < 0 { + return 0 + } + const maxProtocolDate = int(1<<31 - 1) + if date >= maxProtocolDate { + return maxProtocolDate + } + if delaySeconds == 0 { + return date + } + if delaySeconds > maxProtocolDate-date { + return maxProtocolDate + } + return date + delaySeconds +} + func lockSavedStarGiftForUpgrade(ctx context.Context, tx pgx.Tx, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) { where, args := savedStarGiftRefWhere(ref) return lockSavedStarGiftWhere(ctx, tx, where, args...) diff --git a/internal/store/postgres/star_gift_user_message_ref.go b/internal/store/postgres/star_gift_user_message_ref.go new file mode 100644 index 00000000..b1a4da81 --- /dev/null +++ b/internal/store/postgres/star_gift_user_message_ref.go @@ -0,0 +1,59 @@ +package postgres + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5" +) + +// registerUserStarGiftMessageRef records an owner-scoped service-message alias +// for a user-owned gift. Official clients may continue from a freshly emitted +// messageActionStarGiftUnique and pass that message id to a lifecycle RPC, +// while payments.getSavedStarGifts may still expose the original received gift +// message as the aggregate's primary msg_id. +func registerUserStarGiftMessageRef( + ctx context.Context, + tx pgx.Tx, + ownerUserID int64, + msgID int, + savedGiftID int64, + uniqueGiftID int64, +) error { + if ownerUserID <= 0 || msgID <= 0 || savedGiftID <= 0 || uniqueGiftID <= 0 { + return fmt.Errorf("register user star gift message ref: invalid identity") + } + tag, err := tx.Exec(ctx, ` +INSERT INTO star_gift_user_message_refs(owner_user_id,msg_id,saved_gift_id) +SELECT $1,$2,p.id +FROM peer_star_gifts p +WHERE p.id=$3 AND p.owner_peer_type='user' AND p.owner_peer_id=$1 + AND p.unique_gift_id=$4 AND p.lifecycle_status='active' +ON CONFLICT(owner_user_id,msg_id) DO UPDATE +SET saved_gift_id=EXCLUDED.saved_gift_id +WHERE star_gift_user_message_refs.saved_gift_id=EXCLUDED.saved_gift_id`, ownerUserID, msgID, savedGiftID, uniqueGiftID) + if err != nil { + return fmt.Errorf("register user star gift message ref: %w", err) + } + if tag.RowsAffected() != 1 { + return fmt.Errorf("register user star gift message ref: identity collision") + } + return nil +} + +func userStarGiftMessageRefMatches( + ctx context.Context, + db interface { + QueryRow(context.Context, string, ...any) pgx.Row + }, + ownerUserID int64, + msgID int, + savedGiftID int64, +) (bool, error) { + var matches bool + err := db.QueryRow(ctx, `SELECT EXISTS ( +SELECT 1 FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND msg_id=$2 AND saved_gift_id=$3 +)`, ownerUserID, msgID, savedGiftID).Scan(&matches) + return matches, err +} From 40743dfb09b3b459bf6b04db55cd63b5df9b46d5 Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 15:48:33 +0800 Subject: [PATCH 07/28] fix: sync contact note projection --- internal/app/userprojection/contact_cache.go | 3 + .../app/userprojection/contact_cache_test.go | 17 +- internal/app/userprojection/projection.go | 32 +-- .../app/userprojection/projection_test.go | 9 +- internal/domain/user.go | 5 + internal/rpc/contacts.go | 142 ++++++++++--- internal/rpc/contacts_users_rpc_test.go | 197 ++++++++++++++++++ internal/rpc/users.go | 50 +++++ 8 files changed, 395 insertions(+), 60 deletions(-) diff --git a/internal/app/userprojection/contact_cache.go b/internal/app/userprojection/contact_cache.go index a89caf7c..64946a2e 100644 --- a/internal/app/userprojection/contact_cache.go +++ b/internal/app/userprojection/contact_cache.go @@ -451,6 +451,9 @@ func cloneCachedUser(in domain.User) domain.User { if in.PhotoStripped != nil { in.PhotoStripped = append([]byte(nil), in.PhotoStripped...) } + if in.ContactNoteEntities != nil { + in.ContactNoteEntities = append([]domain.MessageEntity(nil), in.ContactNoteEntities...) + } return in } diff --git a/internal/app/userprojection/contact_cache_test.go b/internal/app/userprojection/contact_cache_test.go index f272f3a6..ed88e3f4 100644 --- a/internal/app/userprojection/contact_cache_test.go +++ b/internal/app/userprojection/contact_cache_test.go @@ -116,7 +116,13 @@ func (s *countingContactStore) SetPersonalPhoto(ctx context.Context, userID, con func TestCachedContactStoreCachesProjectionReads(t *testing.T) { ctx := context.Background() base := memory.NewContactStore() - if _, err := base.Upsert(ctx, 1, domain.ContactInput{ContactUserID: 2, FirstName: "Alice", Phone: "111"}); err != nil { + if _, err := base.Upsert(ctx, 1, domain.ContactInput{ + ContactUserID: 2, + FirstName: "Alice", + Phone: "111", + Note: "private note", + NoteEntities: []domain.MessageEntity{{Type: domain.MessageEntityBold, Offset: 0, Length: 7}}, + }); err != nil { t.Fatalf("upsert contact: %v", err) } counting := &countingContactStore{ContactStore: base} @@ -126,15 +132,16 @@ func TestCachedContactStoreCachesProjectionReads(t *testing.T) { if err != nil { t.Fatalf("get many first: %v", err) } - if first[2].FirstName != "Alice" { - t.Fatalf("first contact = %+v, want Alice", first[2]) + if first[2].FirstName != "Alice" || first[2].Note != "private note" || len(first[2].NoteEntities) != 1 { + t.Fatalf("first contact = %+v, want Alice with private note", first[2]) } + first[2].NoteEntities[0].Length = 99 second, err := cached.GetMany(ctx, 1, []int64{2, 3}) if err != nil { t.Fatalf("get many second: %v", err) } - if second[2].FirstName != "Alice" { - t.Fatalf("second contact = %+v, want Alice", second[2]) + if second[2].FirstName != "Alice" || second[2].Note != "private note" || len(second[2].NoteEntities) != 1 || second[2].NoteEntities[0].Length != 7 { + t.Fatalf("second contact = %+v, want isolated cached Alice note", second[2]) } if counting.listCalls != 1 { t.Fatalf("ListByUser calls = %d, want 1 account snapshot load", counting.listCalls) diff --git a/internal/app/userprojection/projection.go b/internal/app/userprojection/projection.go index 60fe7d19..ded9a3b5 100644 --- a/internal/app/userprojection/projection.go +++ b/internal/app/userprojection/projection.go @@ -260,6 +260,9 @@ func cloneUsers(users []domain.User) []domain.User { } out := make([]domain.User, len(users)) copy(out, users) + for i := range out { + out[i].ContactNoteEntities = append([]domain.MessageEntity(nil), out[i].ContactNoteEntities...) + } return out } @@ -499,30 +502,7 @@ func projectOne(ctx context.Context, contacts store.ContactStore, viewerUserID i if err != nil { return domain.User{}, err } - if !found { - user.Phone = "" - user.Contact = false - user.Mutual = false - user.CloseFriend = false - return user, nil - } - projected := user - projected.Contact = true - projected.Mutual = contact.Mutual || contact.User.Mutual - projected.CloseFriend = contact.CloseFriend || contact.User.CloseFriend - if contact.User.Phone != "" { - projected.Phone = contact.User.Phone - } else { - projected.Phone = contact.Phone - } - if contact.User.FirstName != "" || contact.User.LastName != "" { - projected.FirstName = contact.User.FirstName - projected.LastName = contact.User.LastName - } else if contact.FirstName != "" || contact.LastName != "" { - projected.FirstName = contact.FirstName - projected.LastName = contact.LastName - } - return projected, nil + return applyContactProjection(user, contact, found), nil } func uniqueUserIDs(users []domain.User) []int64 { @@ -575,11 +555,15 @@ func applyContactProjection(user domain.User, contact domain.Contact, found bool user.Contact = false user.Mutual = false user.CloseFriend = false + user.ContactNote = "" + user.ContactNoteEntities = nil return user } user.Contact = true user.Mutual = contact.Mutual || contact.User.Mutual user.CloseFriend = contact.CloseFriend || contact.User.CloseFriend + user.ContactNote = contact.Note + user.ContactNoteEntities = append([]domain.MessageEntity(nil), contact.NoteEntities...) if contact.User.Phone != "" { user.Phone = contact.User.Phone } else { diff --git a/internal/app/userprojection/projection_test.go b/internal/app/userprojection/projection_test.go index f6013c3c..7f17dfc2 100644 --- a/internal/app/userprojection/projection_test.go +++ b/internal/app/userprojection/projection_test.go @@ -21,6 +21,8 @@ func TestProjectorCombinesProfilePhotosAndViewerContacts(t *testing.T) { Phone: "1111", FirstName: "Alice", LastName: "Contact", + Note: "private note", + NoteEntities: []domain.MessageEntity{{Type: domain.MessageEntityBold, Offset: 0, Length: 7}}, }); err != nil { t.Fatalf("upsert contact: %v", err) } @@ -47,12 +49,15 @@ func TestProjectorCombinesProfilePhotosAndViewerContacts(t *testing.T) { if friend.FirstName != "Alice" || friend.LastName != "Contact" || friend.Phone != "1111" || !friend.Contact { t.Fatalf("friend projection = %+v, want contact name/phone", friend) } + if friend.ContactNote != "private note" || len(friend.ContactNoteEntities) != 1 || friend.ContactNoteEntities[0].Type != domain.MessageEntityBold { + t.Fatalf("friend contact note = %q %+v, want owner-scoped note", friend.ContactNote, friend.ContactNoteEntities) + } if friend.PhotoID != 9001 || friend.PhotoDCID != 2 || string(friend.PhotoStripped) != string([]byte{1, 2}) { t.Fatalf("friend photo = id %d dc %d stripped %v, want 9001/2/[1 2]", friend.PhotoID, friend.PhotoDCID, friend.PhotoStripped) } stranger := projectionUser(t, users, strangerID) - if stranger.Phone != "" || stranger.Contact { - t.Fatalf("stranger projection = %+v, want hidden phone and non-contact", stranger) + if stranger.Phone != "" || stranger.Contact || stranger.ContactNote != "" || len(stranger.ContactNoteEntities) != 0 { + t.Fatalf("stranger projection = %+v, want hidden phone and no contact note", stranger) } if stranger.PhotoID != 9002 || stranger.PhotoDCID != 3 { t.Fatalf("stranger photo = id %d dc %d, want 9002/3", stranger.PhotoID, stranger.PhotoDCID) diff --git a/internal/domain/user.go b/internal/domain/user.go index 372ff3f3..22413065 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -104,6 +104,11 @@ type User struct { Contact bool Mutual bool CloseFriend bool + // ContactNote/ContactNoteEntities are transient viewer-scoped contact + // projection fields. They must never be persisted into users or a + // viewer-independent base-user cache. + ContactNote string + ContactNoteEntities []MessageEntity // Bot 标识 bot 账号;置位时 BotInfoVersion 必须 ≥1(TDesktop 只认 // user TL 是否携带 bot_info_version 字段,且与 bot flag 共用 bit14)。 Bot bool diff --git a/internal/rpc/contacts.go b/internal/rpc/contacts.go index e77940b5..026c3dc1 100644 --- a/internal/rpc/contacts.go +++ b/internal/rpc/contacts.go @@ -594,7 +594,11 @@ func (r *Router) onContactsImportContacts(ctx context.Context, input []tg.InputP } items := make([]domain.ContactInput, 0, len(input)) for _, item := range input { - note, entities := contactNote(item.GetNote()) + rawNote, hasNote := item.GetNote() + note, entities, err := contactNote(userID, rawNote, hasNote) + if err != nil { + return nil, err + } if !validContactInput(item.Phone, item.FirstName, item.LastName, note, len(entities)) { return nil, limitInvalidErr() } @@ -666,7 +670,11 @@ func (r *Router) onContactsAddContact(ctx context.Context, req *tg.ContactsAddCo if !found { return nil, contactIDInvalidErr() } - note, entities := contactNote(req.GetNote()) + rawNote, hasNote := req.GetNote() + note, entities, err := contactNote(userID, rawNote, hasNote) + if err != nil { + return nil, err + } if !validContactInput(req.Phone, req.FirstName, req.LastName, note, len(entities)) { return nil, limitInvalidErr() } @@ -682,22 +690,20 @@ func (r *Router) onContactsAddContact(ctx context.Context, req *tg.ContactsAddCo if err != nil { return nil, contactErr(err) } - peerUser := contact.User - peerUser.Contact = true - peerUser.Mutual = contact.Mutual || contact.User.Mutual - if contact.Phone != "" { - peerUser.Phone = contact.Phone - } - if contact.FirstName != "" || contact.LastName != "" { - peerUser.FirstName = contact.FirstName - peerUser.LastName = contact.LastName - } + peerUser := contactUserForUpdates(contact) peer := domain.Peer{Type: domain.PeerTypeUser, ID: contact.User.ID} settings, err := r.deps.Contacts.GetPeerSettings(ctx, userID, peer) if err != nil { return nil, internalErr() } updates := r.contactPeerSettingsUpdates(ctx, userID, peerUser, settings, true) + if hasNote { + // TDesktop does not copy the submitted note into Data::User after + // contacts.addContact. updateUser is the lightweight full-info refresh + // signal; the private note itself remains available only from + // users.getFullUser for this viewer. + updates.Updates = append(updates.Updates, &tg.UpdateUser{UserID: peerUser.ID}) + } updates.Updates = append(updates.Updates, &tg.UpdateContactsReset{}) if err := r.recordPeerSettings(ctx, userID, peer, settings); err != nil { return nil, internalErr() @@ -712,6 +718,9 @@ func (r *Router) onContactsAddContact(ctx context.Context, req *tg.ContactsAddCo } r.invalidateRPCProjectionForViewer(userID) r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, updates) + if hasNote { + r.pushContactNoteRefreshIfReliableDispatch(ctx, userID, peerUser) + } return updates, nil } @@ -739,16 +748,7 @@ func (r *Router) onContactsAcceptContact(ctx context.Context, id tg.InputUserCla if err != nil { return nil, internalErr() } - peerUser := contact.User - peerUser.Contact = true - peerUser.Mutual = contact.Mutual || contact.User.Mutual - if contact.Phone != "" { - peerUser.Phone = contact.Phone - } - if contact.FirstName != "" || contact.LastName != "" { - peerUser.FirstName = contact.FirstName - peerUser.LastName = contact.LastName - } + peerUser := contactUserForUpdates(contact) updates := r.contactPeerSettingsUpdates(ctx, userID, peerUser, settings, true) updates.Updates = append(updates.Updates, &tg.UpdateContactsReset{}) if err := r.recordPeerSettings(ctx, userID, peer, settings); err != nil { @@ -844,17 +844,27 @@ func (r *Router) onContactsUpdateContactNote(ctx context.Context, req *tg.Contac if !found { return false, contactIDInvalidErr() } - if utf8.RuneCountInString(req.Note.Text) > maxContactNoteLength || len(req.Note.Entities) > maxMessageEntityCount { - return false, limitInvalidErr() + note, entities, err := contactNote(userID, req.Note, true) + if err != nil { + return false, err } - if _, err := r.deps.Contacts.UpdateContactNote(ctx, userID, target.ID, req.Note.Text, domainMessageEntities(req.Note.Entities)); err != nil { + contact, err := r.deps.Contacts.UpdateContactNote(ctx, userID, target.ID, note, entities) + if err != nil { return false, contactErr(err) } if err := r.recordContactsReset(ctx, userID); err != nil { return false, internalErr() } r.invalidateRPCProjectionForViewer(userID) - r.pushContactsReset(ctx, userID) + peerUser := contactUserForUpdates(contact) + if r.hasReliableUpdateDispatch() { + // contactsReset is already delivered by the durable outbox. updateUser + // is intentionally a transient online refresh hint and must not copy a + // private note into the shared update log. + r.pushContactNoteRefreshIfReliableDispatch(ctx, userID, peerUser) + } else { + r.pushUserUpdates(ctx, userID, r.contactNoteRefreshUpdates(peerUser, int(r.clock.Now().Unix()), true)) + } return true, nil } @@ -989,11 +999,85 @@ func validContactInput(phone, firstName, lastName, note string, entities int) bo return true } -func contactNote(note tg.TextWithEntities, ok bool) (string, []domain.MessageEntity) { +func contactNote(ownerUserID int64, note tg.TextWithEntities, ok bool) (string, []domain.MessageEntity, error) { if !ok { - return "", nil + return "", nil, nil } - return note.Text, domainMessageEntities(note.Entities) + if !utf8.ValidString(note.Text) || utf8.RuneCountInString(note.Text) > maxContactNoteLength || len(note.Entities) > maxMessageEntityCount { + return "", nil, limitInvalidErr() + } + limit := utf16CodeUnitLen(note.Text) + for _, entity := range note.Entities { + if messageEntityClassNil(entity) || !storyCaptionEntitySupported(entity) { + return "", nil, entityBoundsInvalidErr() + } + offset, length := entity.GetOffset(), entity.GetLength() + if offset < 0 || length <= 0 || offset > limit || length > limit-offset { + return "", nil, entityBoundsInvalidErr() + } + switch typed := entity.(type) { + case *tg.MessageEntityCustomEmoji: + if typed.DocumentID <= 0 { + return "", nil, entityBoundsInvalidErr() + } + case *tg.MessageEntityMentionName: + if typed.UserID <= 0 { + return "", nil, entityBoundsInvalidErr() + } + case *tg.InputMessageEntityMentionName: + if inputUserClassNil(typed.UserID) { + return "", nil, entityBoundsInvalidErr() + } + } + } + entities := domainMessageEntitiesForViewer(ownerUserID, note.Entities) + if len(entities) != len(note.Entities) || !validEphemeralEntityBounds(note.Text, entities) { + return "", nil, entityBoundsInvalidErr() + } + return note.Text, entities, nil +} + +func contactUserForUpdates(contact domain.Contact) domain.User { + peerUser := contact.User + peerUser.Contact = true + peerUser.Mutual = contact.Mutual || contact.User.Mutual + if contact.Phone != "" { + peerUser.Phone = contact.Phone + } + if contact.FirstName != "" || contact.LastName != "" { + peerUser.FirstName = contact.FirstName + peerUser.LastName = contact.LastName + } + return peerUser +} + +func (r *Router) contactNoteRefreshUpdates(peerUser domain.User, date int, includeContactsReset bool) *tg.Updates { + updates := make([]tg.UpdateClass, 0, 2) + if includeContactsReset { + updates = append(updates, &tg.UpdateContactsReset{}) + } + updates = append(updates, &tg.UpdateUser{UserID: peerUser.ID}) + return &tg.Updates{ + Updates: updates, + Users: []tg.UserClass{r.tgUser(peerUser)}, + Date: date, + } +} + +// pushContactNoteRefreshIfReliableDispatch complements the durable +// contactsReset event. Reliable dispatch already owns the reset, while this +// best-effort online nudge makes other loaded TDesktop profiles refetch +// users.getFullUser immediately. Offline correctness does not depend on it. +func (r *Router) pushContactNoteRefreshIfReliableDispatch(ctx context.Context, userID int64, peerUser domain.User) { + if !r.hasReliableUpdateDispatch() || peerUser.ID == 0 { + return + } + r.pushUserMessageTransient( + ctx, + userID, + "push contact note full-user refresh", + r.contactNoteRefreshUpdates(peerUser, int(r.clock.Now().Unix()), false), + ) } func (r *Router) contactPeerSettingsUpdates(ctx context.Context, userID int64, peerUser domain.User, settings domain.PeerSettings, includeSelf bool) *tg.Updates { diff --git a/internal/rpc/contacts_users_rpc_test.go b/internal/rpc/contacts_users_rpc_test.go index 16095d7b..6c44e580 100644 --- a/internal/rpc/contacts_users_rpc_test.go +++ b/internal/rpc/contacts_users_rpc_test.go @@ -13,6 +13,7 @@ import ( appprivacy "telesrv/internal/app/privacy" appstories "telesrv/internal/app/stories" appupdates "telesrv/internal/app/updates" + "telesrv/internal/app/userprojection" appusers "telesrv/internal/app/users" "telesrv/internal/domain" "telesrv/internal/store/memory" @@ -724,6 +725,202 @@ func TestAccountUpdateProfileRPC(t *testing.T) { } } +func TestUsersGetFullUserProjectsOwnerScopedContactNoteAcrossCacheUpdates(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + rawContacts := memory.NewContactStore() + cachedContacts := userprojection.NewCachedContactStore(rawContacts, time.Hour) + owner, err := userStore.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000001", FirstName: "Owner"}) + if err != nil { + t.Fatalf("create owner: %v", err) + } + altOwner, err := userStore.Create(ctx, domain.User{AccessHash: 2, Phone: "15550000002", FirstName: "Alt"}) + if err != nil { + t.Fatalf("create alternate owner: %v", err) + } + friend, err := userStore.Create(ctx, domain.User{AccessHash: 3, Phone: "15550000003", FirstName: "Friend"}) + if err != nil { + t.Fatalf("create friend: %v", err) + } + contactsService := appcontacts.NewService(cachedContacts, userStore) + usersService := appusers.NewService(userStore, appusers.WithContactStore(cachedContacts)) + sessions := &captureSessions{} + r := New(Config{}, Deps{Users: usersService, Contacts: contactsService, Sessions: sessions}, zaptest.NewLogger(t), clock.System) + hasUserRefresh := func(updates *tg.Updates, userID int64) bool { + t.Helper() + if updates == nil { + return false + } + for _, update := range updates.Updates { + if changed, ok := update.(*tg.UpdateUser); ok && changed.UserID == userID { + return true + } + } + return false + } + add := &tg.ContactsAddContactRequest{ + ID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}, + FirstName: "Friend", + } + add.SetNote(tg.TextWithEntities{ + Text: "owner note", + Entities: []tg.MessageEntityClass{&tg.MessageEntityBold{Offset: 0, Length: 5}}, + }) + addedClass, err := r.onContactsAddContact(WithUserID(ctx, owner.ID), add) + if err != nil { + t.Fatalf("add owner contact through RPC: %v", err) + } + added, ok := addedClass.(*tg.Updates) + if !ok || !hasUserRefresh(added, friend.ID) { + t.Fatalf("add contact updates = %T %+v, want updateUser refresh for note", addedClass, addedClass) + } + pushed, ok := sessions.lastUserPush().(*tg.Updates) + if !ok || !hasUserRefresh(pushed, friend.ID) { + t.Fatalf("add contact push = %T %+v, want other-session updateUser refresh", sessions.lastUserPush(), sessions.lastUserPush()) + } + if _, err := contactsService.AddContact(ctx, altOwner.ID, domain.ContactInput{ + ContactUserID: friend.ID, + FirstName: "Friend", + Note: "alternate note", + }); err != nil { + t.Fatalf("add alternate owner contact: %v", err) + } + getNote := func(viewer domain.User) (tg.TextWithEntities, bool) { + t.Helper() + full, err := r.onUsersGetFullUser(WithUserID(ctx, viewer.ID), &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}) + if err != nil { + t.Fatalf("get full user for viewer %d: %v", viewer.ID, err) + } + return full.FullUser.GetNote() + } + + note, ok := getNote(owner) + if !ok || note.Text != "owner note" || len(note.Entities) != 1 { + t.Fatalf("owner note = %+v present=%v, want owner note with entity", note, ok) + } + bold, ok := note.Entities[0].(*tg.MessageEntityBold) + if !ok || bold.Offset != 0 || bold.Length != 5 { + t.Fatalf("owner note entity = %T %+v, want bold 0/5", note.Entities[0], note.Entities[0]) + } + // The large UserFull LRU intentionally excludes private notes; every response + // overlays one from the already-loaded viewer contact projection. + cachedFull, ok := r.userFullProjectionCache.Lookup(owner.ID, friend.ID) + if !ok { + t.Fatal("user full projection was not cached") + } + if cachedNote, present := cachedFull.GetNote(); present { + t.Fatalf("cached user full leaked private note: %+v", cachedNote) + } + // Mutating one response must not leak through the cache or contact snapshot. + bold.Length = 99 + note, ok = getNote(owner) + if !ok || note.Entities[0].(*tg.MessageEntityBold).Length != 5 { + t.Fatalf("owner note after response mutation = %+v present=%v", note, ok) + } + + altNote, ok := getNote(altOwner) + if !ok || altNote.Text != "alternate note" { + t.Fatalf("alternate owner note = %+v present=%v, want isolated value", altNote, ok) + } + if ok, err := r.onContactsUpdateContactNote(WithUserID(ctx, owner.ID), &tg.ContactsUpdateContactNoteRequest{ + ID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}, + Note: tg.TextWithEntities{ + Text: "bad", + Entities: []tg.MessageEntityClass{&tg.MessageEntityBold{Offset: 3, Length: 1}}, + }, + }); err == nil || ok || !strings.Contains(err.Error(), "ENTITY_BOUNDS_INVALID") { + t.Fatalf("invalid contact note ok=%v err=%v, want ENTITY_BOUNDS_INVALID", ok, err) + } + note, ok = getNote(owner) + if !ok || note.Text != "owner note" { + t.Fatalf("invalid update mutated owner note: %+v present=%v", note, ok) + } + + updated := &tg.ContactsUpdateContactNoteRequest{ + ID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}, + Note: tg.TextWithEntities{ + Text: "fresh note", + Entities: []tg.MessageEntityClass{&tg.MessageEntityItalic{Offset: 0, Length: 5}}, + }, + } + sessions.clearMessages() + if ok, err := r.onContactsUpdateContactNote(WithUserID(ctx, owner.ID), updated); err != nil || !ok { + t.Fatalf("update contact note ok=%v err=%v", ok, err) + } + pushed, ok = sessions.lastUserPush().(*tg.Updates) + if !ok || !hasUserRefresh(pushed, friend.ID) { + t.Fatalf("update contact note push = %T %+v, want updateUser refresh", sessions.lastUserPush(), sessions.lastUserPush()) + } + hasReset := false + for _, update := range pushed.Updates { + if _, ok := update.(*tg.UpdateContactsReset); ok { + hasReset = true + } + } + if !hasReset { + t.Fatalf("update contact note push = %+v, want contactsReset for non-reliable dispatch", pushed) + } + note, ok = getNote(owner) + if !ok || note.Text != "fresh note" || len(note.Entities) != 1 { + t.Fatalf("fresh owner note = %+v present=%v", note, ok) + } + if _, ok := note.Entities[0].(*tg.MessageEntityItalic); !ok { + t.Fatalf("fresh owner note entity = %T, want italic", note.Entities[0]) + } + altNote, ok = getNote(altOwner) + if !ok || altNote.Text != "alternate note" { + t.Fatalf("alternate note changed with owner update: %+v present=%v", altNote, ok) + } + + if ok, err := r.onContactsUpdateContactNote(WithUserID(ctx, owner.ID), &tg.ContactsUpdateContactNoteRequest{ + ID: &tg.InputUser{UserID: friend.ID, AccessHash: friend.AccessHash}, + Note: tg.TextWithEntities{}, + }); err != nil || !ok { + t.Fatalf("clear contact note ok=%v err=%v", ok, err) + } + if note, present := getNote(owner); present { + t.Fatalf("cleared contact note still present: %+v", note) + } + + // Simulate a write committed by another instance: both the shared contact + // snapshot and RPC projection receive the existing contact_account NOTIFY. + if _, found, err := rawContacts.UpdateNote(ctx, owner.ID, friend.ID, "remote note", nil); err != nil || !found { + t.Fatalf("remote update found=%v err=%v", found, err) + } + cachedContacts.InvalidateViewers(owner.ID) + r.InvalidateRPCProjectionReadModelForViewer(owner.ID) + note, ok = getNote(owner) + if !ok || note.Text != "remote note" { + t.Fatalf("note after cross-instance invalidation = %+v present=%v", note, ok) + } +} + +func TestContactNoteReliableDispatchPushesOnlyTransientUserRefresh(t *testing.T) { + sessions := &captureSessions{} + r := New(Config{}, Deps{ + Sessions: sessions, + Updates: &captureUpdates{reliableDispatch: true}, + }, zaptest.NewLogger(t), clock.System) + peer := domain.User{ID: 1000000002, AccessHash: 22, FirstName: "Friend", Contact: true} + + r.pushContactNoteRefreshIfReliableDispatch(WithUserID(context.Background(), 1000000001), 1000000001, peer) + + pushed, ok := sessions.lastUserPush().(*tg.Updates) + if !ok { + t.Fatalf("contact note refresh = %T, want *tg.Updates", sessions.lastUserPush()) + } + if len(pushed.Updates) != 1 { + t.Fatalf("contact note refresh updates = %+v, want one updateUser without duplicate contactsReset", pushed.Updates) + } + changed, ok := pushed.Updates[0].(*tg.UpdateUser) + if !ok || changed.UserID != peer.ID { + t.Fatalf("contact note refresh update = %T %+v, want updateUser(%d)", pushed.Updates[0], pushed.Updates[0], peer.ID) + } + if len(pushed.Users) != 1 || pushed.Users[0].GetID() != peer.ID { + t.Fatalf("contact note refresh users = %+v, want peer companion", pushed.Users) + } +} + func TestUsersSavedMusicStubsValidateInput(t *testing.T) { ctx := context.Background() userStore := memory.NewUserStore() diff --git a/internal/rpc/users.go b/internal/rpc/users.go index bfdc0857..a6e0f70e 100644 --- a/internal/rpc/users.go +++ b/internal/rpc/users.go @@ -3,6 +3,7 @@ package rpc import ( "context" "errors" + "unicode/utf8" "github.com/iamxvbaba/td/tg" @@ -156,6 +157,9 @@ func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) ( r.applyStoryMaxIDsToPeerObjects(ctx, currentUserID, []tg.UserClass{user}, nil) loadEpoch := r.userFullProjectionCache.LoadEpoch() if full, ok := r.userFullProjectionCache.Lookup(currentUserID, u.ID); ok { + if !applyContactNoteToUserFull(u, &full) { + return nil, internalErr() + } if err := r.applyTranslationDisabledToUserFull(ctx, currentUserID, u.ID, &full); err != nil { return nil, err } @@ -173,6 +177,9 @@ func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) ( return nil, err } r.userFullProjectionCache.StoreIfEpoch(currentUserID, u.ID, full, loadEpoch) + if !applyContactNoteToUserFull(u, &full) { + return nil, internalErr() + } if err := r.applyTranslationDisabledToUserFull(ctx, currentUserID, u.ID, &full); err != nil { return nil, err } @@ -186,6 +193,49 @@ func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) ( }, nil } +// applyContactNoteToUserFull overlays the viewer-scoped contact note after the +// expensive UserFull projection cache. This keeps private notes out of the +// large LRU while reusing the contact projection already loaded by Users.ByID, +// so users.getFullUser adds neither a PostgreSQL query nor an N+1 read. +func applyContactNoteToUserFull(user domain.User, full *tg.UserFull) bool { + if full == nil { + return false + } + full.Flags2.Unset(22) + full.Note = tg.TextWithEntities{} + if !user.Contact { + return user.ContactNote == "" && len(user.ContactNoteEntities) == 0 + } + if user.ContactNote == "" { + return len(user.ContactNoteEntities) == 0 + } + if !utf8.ValidString(user.ContactNote) || utf8.RuneCountInString(user.ContactNote) > maxContactNoteLength || + len(user.ContactNoteEntities) > maxMessageEntityCount || !validEphemeralEntityBounds(user.ContactNote, user.ContactNoteEntities) { + return false + } + entities := tgMessageEntities(user.ContactNoteEntities) + if len(entities) != len(user.ContactNoteEntities) { + return false + } + for _, entity := range user.ContactNoteEntities { + switch entity.Type { + case domain.MessageEntityCustomEmoji: + if entity.DocumentID <= 0 { + return false + } + case domain.MessageEntityMentionName: + if entity.UserID <= 0 { + return false + } + } + } + full.SetNote(tg.TextWithEntities{ + Text: user.ContactNote, + Entities: entities, + }) + return true +} + func (r *Router) buildUserFullProjection(ctx context.Context, currentUserID int64, u domain.User) (tg.UserFull, error) { about := u.About if r.deps.Privacy != nil && u.ID != currentUserID { From 27ec9dce331140000026dc4cc9317266513d8615 Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 15:50:40 +0800 Subject: [PATCH 08/28] chore: sanitize public sync references --- cmd/bots/bedolagaformat/README.md | 4 ++-- internal/config/config_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/bots/bedolagaformat/README.md b/cmd/bots/bedolagaformat/README.md index 5287ee5c..f7163360 100644 --- a/cmd/bots/bedolagaformat/README.md +++ b/cmd/bots/bedolagaformat/README.md @@ -89,7 +89,7 @@ enable `/setlogin` 首次创建 client 时只展示一次 OIDC Client Secret;不要写进仓库。可用 `/logininfo` 查看 Client ID 和登记结果,或用 `/resetloginsecret` 轮换 secret。 -loopback HTTP 仅应配合 telesrv 的显式开发开关使用;testserver/生产必须换成精确 +loopback HTTP 仅应配合 telesrv 的显式开发开关使用;测试部署/生产必须换成精确 HTTPS origin。 把一次性 secret 和 Client ID 放入进程环境,再启动: @@ -118,7 +118,7 @@ code flow 会明确禁用。 `/auth/status` 配 wildcard CORS,也不要在 RP 中记录 browser token 或 ID token。 demo 的 flow/state/nonce 只保存在单进程内存中,带 10 分钟过期和 256 条上限,专用于 -本地与 testserver 端到端验证,不是生产 relying-party 实现。官方 iOS/Android SDK +本地与受控测试部署端到端验证,不是生产 relying-party 实现。官方 iOS/Android SDK 目前把 `https://oauth.telegram.org` 写死;验证自建 issuer 时需使用项目记录的最小 base-URL patch 或等价测试构建,不能把官方生产 SDK 未修改的结果误判为自建服务结果。 diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b8e4c0f2..7d9b35cf 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -35,13 +35,13 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) { func TestLoadUsesExplicitAdvertiseIP(t *testing.T) { disableDefaultConfigFile(t) - t.Setenv("TELESRV_ADVERTISE_IP", "10.172.61.102") + t.Setenv("TELESRV_ADVERTISE_IP", "203.0.113.10") cfg, err := Load() if err != nil { t.Fatalf("Load: %v", err) } - if cfg.AdvertiseIP != "10.172.61.102" { + if cfg.AdvertiseIP != "203.0.113.10" { t.Fatalf("AdvertiseIP = %q, want explicit env", cfg.AdvertiseIP) } } From bf72c246b6a0a0f92219554b78b63b40f1f58f3e Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 17:25:53 +0800 Subject: [PATCH 09/28] fix: sync call ringing device scope --- internal/rpc/phone_calls.go | 5 ++- internal/rpc/phone_push.go | 30 +++++++++++++ internal/rpc/phone_rpc_test.go | 80 +++++++++++++++++++++++++++++----- 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/internal/rpc/phone_calls.go b/internal/rpc/phone_calls.go index 4f303776..a59c8e0a 100644 --- a/internal/rpc/phone_calls.go +++ b/internal/rpc/phone_calls.go @@ -135,7 +135,10 @@ func (r *Router) onPhoneReceivedCall(ctx context.Context, peer tg.InputPhoneCall if transitioned { // ⚠ P1-2:receiveDate 推送必须在 P1 就位。主叫只有收到带 receive_date 的 // phoneCallWaiting 才会把 20s receive 定时器换成 90s ring 定时器。 - r.pushPhoneCall(ctx, call.AdminID, call, "phone call ringing") + // 这条主叫视角更新只属于 requestCall 的来源设备;账号级广播会在 DrKLO + // 多账号同机时按相同 call_id 覆盖被叫的 pending phoneCallRequested,丢失 + // g_a_hash 并在接听时触发 Ga hash mismatch。 + r.pushPhoneCallToDevice(ctx, call.AdminID, call.CallerDevice, call, "phone call ringing") } return true, nil } diff --git a/internal/rpc/phone_push.go b/internal/rpc/phone_push.go index f6fe9b88..e829f2cb 100644 --- a/internal/rpc/phone_push.go +++ b/internal/rpc/phone_push.go @@ -5,6 +5,7 @@ import ( "github.com/iamxvbaba/td/proto" "github.com/iamxvbaba/td/tg" + "go.uber.org/zap" "telesrv/internal/domain" ) @@ -34,6 +35,35 @@ func (r *Router) pushPhoneCall(ctx context.Context, targetUserID int64, call dom return r.pushUserMessage(ctx, targetUserID, logMessage, r.phoneCallUpdates(ctx, call, targetUserID)) } +// pushPhoneCallToDevice 只把 phoneCall 状态推给一台精确的物理 session。 +// +// 这是 fail-closed 路径:目标锚点缺失、session 已断开或编码/发送失败时都不得 +// 回退为 user 级广播。呼出通话的 ringing 更新只属于 requestCall 来源设备; +// 扩大投递范围会在 DrKLO 多账号同机时污染另一账号的全局 pending 来电对象。 +func (r *Router) pushPhoneCallToDevice(ctx context.Context, targetUserID int64, device domain.SessionRef, call domain.PhoneCall, logMessage string) { + if targetUserID == 0 || device.RawAuthKeyID == ([8]byte{}) || device.SessionID == 0 || r.deps.Sessions == nil { + if r.log != nil { + r.log.Debug(logMessage, + zap.Int64("target_user_id", targetUserID), + zap.Int64("call_id", call.ID), + zap.Int64("target_session_id", device.SessionID), + zap.String("delivery", "skipped_invalid_device_anchor"), + ) + } + return + } + + updates := r.phoneCallUpdates(ctx, call, targetUserID) + if err := r.deps.Sessions.PushToSessionForAuthKey(ctx, device.RawAuthKeyID, device.SessionID, proto.MessageFromServer, updates); err != nil && r.log != nil { + r.log.Debug(logMessage, + zap.Int64("target_user_id", targetUserID), + zap.Int64("call_id", call.ID), + zap.Int64("target_session_id", device.SessionID), + zap.Error(err), + ) + } +} + // pushPhoneCallStopRinging 向被叫其它设备推合成 phoneCallDiscarded 停振铃(P0-1 修正)。 // ctx 必须是接听设备的请求上下文:except 语义恰好把赢家排除在外。 func (r *Router) pushPhoneCallStopRinging(ctx context.Context, call domain.PhoneCall) int { diff --git a/internal/rpc/phone_rpc_test.go b/internal/rpc/phone_rpc_test.go index 97f53331..f96e579f 100644 --- a/internal/rpc/phone_rpc_test.go +++ b/internal/rpc/phone_rpc_test.go @@ -25,6 +25,7 @@ import ( // phonePushRecord 记录一次定向推送(目标用户、被排除的 session、载荷)。 type phonePushRecord struct { userID int64 + rawAuthKeyID [8]byte targetSession int64 excludeSession int64 msg bin.Encoder @@ -32,8 +33,9 @@ type phonePushRecord struct { // phoneCaptureSessions 是带完整推送日志的 SessionBinder fake(captureSessions 只留最后一条)。 type phoneCaptureSessions struct { - mu sync.Mutex - log []phonePushRecord + mu sync.Mutex + log []phonePushRecord + pushErr error } func (s *phoneCaptureSessions) BindAuthKeyForSession([8]byte, int64, [8]byte) {} @@ -47,11 +49,11 @@ func (s *phoneCaptureSessions) UserIDResolvedForAuthKey([8]byte, int64) (int64, func (s *phoneCaptureSessions) UnbindAuthKey([8]byte) int { return 0 } func (s *phoneCaptureSessions) SetReceivesUpdatesForAuthKey([8]byte, int64, bool) {} -func (s *phoneCaptureSessions) PushToSessionForAuthKey(_ context.Context, _ [8]byte, sessionID int64, _ proto.MessageType, msg tg.UpdatesClass) error { +func (s *phoneCaptureSessions) PushToSessionForAuthKey(_ context.Context, rawAuthKeyID [8]byte, sessionID int64, _ proto.MessageType, msg tg.UpdatesClass) error { s.mu.Lock() defer s.mu.Unlock() - s.log = append(s.log, phonePushRecord{targetSession: sessionID, msg: msg}) - return nil + s.log = append(s.log, phonePushRecord{rawAuthKeyID: rawAuthKeyID, targetSession: sessionID, msg: msg}) + return s.pushErr } func (s *phoneCaptureSessions) PushToUserExceptAuthKeySession(_ context.Context, userID int64, _ [8]byte, excludeSessionID int64, _ proto.MessageType, msg tg.UpdatesClass) (int, error) { @@ -73,6 +75,12 @@ func (s *phoneCaptureSessions) reset() { s.log = nil } +func (s *phoneCaptureSessions) setPushError(err error) { + s.mu.Lock() + defer s.mu.Unlock() + s.pushErr = err +} + // stubPrivacy 只为 CanSee 服务;其余接口方法不在通话链路使用。 type stubPrivacy struct { deny map[domain.PrivacyKey]bool @@ -104,8 +112,15 @@ type phoneFixture struct { } const ( - phoneCallerSession = int64(101) - phoneCalleeSession = int64(202) + phoneCallerSession = int64(101) + phoneCalleeSession = int64(202) + phoneOtherCalleeSession = int64(303) +) + +var ( + phoneCallerRawAuthKey = [8]byte{0x11, 0x01} + phoneCalleeRawAuthKey = [8]byte{0x22, 0x02} + phoneOtherCalleeRawAuthKey = [8]byte{0x33, 0x03} ) func newPhoneFixture(t *testing.T, privacy PrivacyService) *phoneFixture { @@ -133,11 +148,16 @@ func newPhoneFixture(t *testing.T, privacy PrivacyService) *phoneFixture { } func (f *phoneFixture) callerCtx() context.Context { - return WithSessionID(WithUserID(f.ctx, f.caller.ID), phoneCallerSession) + return WithSessionID(WithRawAuthKeyID(WithUserID(f.ctx, f.caller.ID), phoneCallerRawAuthKey), phoneCallerSession) } func (f *phoneFixture) calleeCtx() context.Context { - return WithSessionID(WithUserID(f.ctx, f.callee.ID), phoneCalleeSession) + return WithSessionID(WithRawAuthKeyID(WithUserID(f.ctx, f.callee.ID), phoneCalleeRawAuthKey), phoneCalleeSession) + +} + +func (f *phoneFixture) otherCalleeCtx() context.Context { + return WithSessionID(WithRawAuthKeyID(WithUserID(f.ctx, f.callee.ID), phoneOtherCalleeRawAuthKey), phoneOtherCalleeSession) } func phoneTestProtocol() tg.PhoneCallProtocol { @@ -226,8 +246,8 @@ func TestPhoneCallRPCHappyPath(t *testing.T) { t.Fatalf("receivedCall = %v err=%v", ok, err) } pushes = f.sessions.records() - if len(pushes) != 1 || pushes[0].userID != f.caller.ID { - t.Fatalf("receivedCall pushes = %+v, want one to caller", pushes) + if len(pushes) != 1 || pushes[0].rawAuthKeyID != phoneCallerRawAuthKey || pushes[0].targetSession != phoneCallerSession { + t.Fatalf("receivedCall pushes = %+v, want caller device %x/%d", pushes, phoneCallerRawAuthKey, phoneCallerSession) } ringing, ok := phoneCallPayload(t, pushes[0]).(*tg.PhoneCallWaiting) if !ok || ringing.ReceiveDate == 0 { @@ -236,6 +256,15 @@ func TestPhoneCallRPCHappyPath(t *testing.T) { } f.sessions.reset() + // 其它被叫设备晚到的 receivedCall 幂等成功,但不得再次推 ringing。 + if ok, err := f.router.onPhoneReceivedCall(f.otherCalleeCtx(), tg.InputPhoneCall{ID: callID, AccessHash: accessHash}); err != nil || !ok { + t.Fatalf("duplicate receivedCall = %v err=%v", ok, err) + } + if pushes := f.sessions.records(); len(pushes) != 0 { + t.Fatalf("duplicate receivedCall pushes = %+v, want none", pushes) + } + f.sessions.reset() + // --- acceptCall(被叫赢家设备) --- acceptRes, err := f.router.onPhoneAcceptCall(f.calleeCtx(), &tg.PhoneAcceptCallRequest{ Peer: tg.InputPhoneCall{ID: callID, AccessHash: accessHash}, @@ -388,6 +417,35 @@ func TestPhoneCallRPCHappyPath(t *testing.T) { } } +func TestPhoneReceivedCallDevicePushFailureDoesNotBroadcast(t *testing.T) { + f := newPhoneFixture(t, stubPrivacy{}) + _, gaHash, _ := phoneTestKeys() + + res, err := f.router.onPhoneRequestCall(f.callerCtx(), &tg.PhoneRequestCallRequest{ + UserID: &tg.InputUser{UserID: f.callee.ID, AccessHash: f.callee.AccessHash}, + RandomID: 43, + GAHash: gaHash, + Protocol: phoneTestProtocol(), + }) + if err != nil { + t.Fatalf("requestCall: %v", err) + } + waiting := res.PhoneCall.(*tg.PhoneCallWaiting) + f.sessions.reset() + f.sessions.setPushError(errors.New("caller session gone")) + + if ok, err := f.router.onPhoneReceivedCall(f.calleeCtx(), tg.InputPhoneCall{ID: waiting.ID, AccessHash: waiting.AccessHash}); err != nil || !ok { + t.Fatalf("receivedCall = %v err=%v", ok, err) + } + pushes := f.sessions.records() + if len(pushes) != 1 || pushes[0].rawAuthKeyID != phoneCallerRawAuthKey || pushes[0].targetSession != phoneCallerSession { + t.Fatalf("receivedCall pushes = %+v, want one failed attempt to caller device", pushes) + } + if pushes[0].userID != 0 { + t.Fatalf("receivedCall failure fell back to user broadcast: %+v", pushes) + } +} + func TestPhoneCallRPCValidation(t *testing.T) { f := newPhoneFixture(t, stubPrivacy{}) _, gaHash, gb := phoneTestKeys() From f53579416ec6af4c5f96b1efdb9359bd92daa92c Mon Sep 17 00:00:00 2001 From: A Date: Tue, 21 Jul 2026 18:18:33 +0800 Subject: [PATCH 10/28] feat: sync HTTP callback OIDC setup --- .env.example | 4 +- cmd/bots/aiogramecho/README.md | 8 +- cmd/bots/aiogramecho/echo.py | 2 +- cmd/bots/bedolagaformat/README.md | 4 +- cmd/bots/bedolagaformat/login_demo.py | 6 - cmd/bots/bedolagaformat/test_login_demo.py | 10 +- cmd/telesrv/main.go | 6 +- docs/configuration.en.md | 201 ++++++++++++++++++++- docs/configuration.zh-CN.md | 193 +++++++++++++++++++- internal/app/bots/botfather_login_test.go | 8 +- internal/app/telegramlogin/jose.go | 9 +- internal/app/telegramlogin/jose_test.go | 15 ++ internal/app/telegramlogin/native.go | 4 +- internal/app/telegramlogin/service.go | 48 ++--- internal/app/telegramlogin/service_test.go | 2 +- internal/app/telegramlogin/url.go | 20 +- internal/app/telegramlogin/url_test.go | 15 +- internal/botapi/server.go | 13 +- internal/botapi/server_test.go | 23 ++- internal/config/config.go | 17 +- internal/config/config_test.go | 30 ++- internal/domain/message_markup.go | 17 +- internal/domain/message_markup_test.go | 3 +- internal/rpc/telegram_login_rpc_test.go | 6 +- internal/telegramloginhttp/handler.go | 24 +-- internal/telegramloginhttp/handler_test.go | 2 +- 26 files changed, 557 insertions(+), 133 deletions(-) diff --git a/.env.example b/.env.example index c706c601..35abb6c8 100644 --- a/.env.example +++ b/.env.example @@ -164,7 +164,9 @@ TELESRV_PUBLIC_LINK_WEB_ADDR=127.0.0.1:2401 # have been generated with `go run ./cmd/telegramloginkeygen -mode init`. TELESRV_TELEGRAM_LOGIN_ENABLE=false TELESRV_TELEGRAM_LOGIN_ISSUER=https://telesrv.net -TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP=false +# Set true to permit an HTTP issuer and HTTP registered origins/redirect URIs +# on any hostname or IP address. HTTPS remains the default when false. +TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=false TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE=data/telegram-login/signing-keys.json TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE=data/telegram-login/code-keys.json TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE=data/telegram-login/client-secret-pepper diff --git a/cmd/bots/aiogramecho/README.md b/cmd/bots/aiogramecho/README.md index 0febb3a7..5c06a384 100644 --- a/cmd/bots/aiogramecho/README.md +++ b/cmd/bots/aiogramecho/README.md @@ -56,10 +56,10 @@ python .\cmd\bots\aiogramecho\echo.py ` ## Webhook 模式 telesrv 现在会持久化 webhook 配置,通过跨实例租约投递,并且只在目标返回 2xx -后推进 `update_id`。aiogram 可监听本机 HTTP,由 Caddy/Nginx/Tunnel 提供公网 HTTPS: +后推进 `update_id`。aiogram 可直接登记 HTTP/HTTPS 域名或 IP,也可以由 Caddy/Nginx/Tunnel 提供公网 HTTPS: ```powershell -$env:TELESRV_BOT_WEBHOOK_URL = "https://bot.example.com/webhook" +$env:TELESRV_BOT_WEBHOOK_URL = "http://192.0.2.25:8080/webhook" $env:TELESRV_BOT_WEBHOOK_SECRET = "replace_with_a_random_secret" python .\cmd\bots\aiogramecho\echo.py ` --mode webhook ` @@ -69,8 +69,8 @@ python .\cmd\bots\aiogramecho\echo.py ` --drop-pending ``` -公网 URL 必须是 HTTPS,端口限 Telegram 标准的 443/80/88/8443;本机监听地址 -可以是 HTTP,因为 TLS 通常在反向代理终止。`secret_token` 会由 telesrv 放入 +Webhook URL 可使用任意合法 HTTP/HTTPS 域名或 IP 及 `1..65535` 端口;本机监听地址 +也可以直接使用 HTTP。`secret_token` 会由 telesrv 放入 `X-Telegram-Bot-Api-Secret-Token`,aiogram 会自动校验。若希望进程退出时删除配置, 再加 `--delete-webhook-on-exit`;默认保留配置,以免普通重启造成更新丢窗。 diff --git a/cmd/bots/aiogramecho/echo.py b/cmd/bots/aiogramecho/echo.py index 78b0d172..400786cc 100644 --- a/cmd/bots/aiogramecho/echo.py +++ b/cmd/bots/aiogramecho/echo.py @@ -52,7 +52,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--webhook-url", default=os.getenv("TELESRV_BOT_WEBHOOK_URL", ""), - help="Public HTTPS URL including the webhook path", + help="Public HTTP(S) URL including the webhook path", ) parser.add_argument( "--webhook-path", diff --git a/cmd/bots/bedolagaformat/README.md b/cmd/bots/bedolagaformat/README.md index f7163360..ef46c504 100644 --- a/cmd/bots/bedolagaformat/README.md +++ b/cmd/bots/bedolagaformat/README.md @@ -89,8 +89,8 @@ enable `/setlogin` 首次创建 client 时只展示一次 OIDC Client Secret;不要写进仓库。可用 `/logininfo` 查看 Client ID 和登记结果,或用 `/resetloginsecret` 轮换 secret。 -loopback HTTP 仅应配合 telesrv 的显式开发开关使用;测试部署/生产必须换成精确 -HTTPS origin。 +使用 HTTP 域名/IP 时,在 telesrv 配置 `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true`; +demo 会接受任意合法 HTTP(S) issuer/public origin,不再限制为 loopback。 把一次性 secret 和 Client ID 放入进程环境,再启动: diff --git a/cmd/bots/bedolagaformat/login_demo.py b/cmd/bots/bedolagaformat/login_demo.py index c0cae522..185cd165 100644 --- a/cmd/bots/bedolagaformat/login_demo.py +++ b/cmd/bots/bedolagaformat/login_demo.py @@ -68,10 +68,6 @@ class PendingFlow: code_verifier: str = "" -def _is_loopback(host: str | None) -> bool: - return host in {"127.0.0.1", "::1", "localhost"} - - def normalize_web_base(value: str, *, name: str) -> str: raw = value.strip().rstrip("/") parsed = urlsplit(raw) @@ -85,8 +81,6 @@ def normalize_web_base(value: str, *, name: str) -> str: or parsed.path not in {"", "/"} ): raise ValueError(f"{name} must be an absolute origin without path, query, or fragment") - if parsed.scheme != "https" and not _is_loopback(parsed.hostname): - raise ValueError(f"{name} must use HTTPS except on loopback") return f"{parsed.scheme}://{parsed.netloc}" diff --git a/cmd/bots/bedolagaformat/test_login_demo.py b/cmd/bots/bedolagaformat/test_login_demo.py index 7ba9bb54..cd136f70 100644 --- a/cmd/bots/bedolagaformat/test_login_demo.py +++ b/cmd/bots/bedolagaformat/test_login_demo.py @@ -49,8 +49,14 @@ class LoginDemoHelpersTest(unittest.TestCase): demo.normalize_web_base("http://127.0.0.1:3000", name="RP"), "http://127.0.0.1:3000", ) - with self.assertRaises(ValueError): - demo.normalize_web_base("http://rp.example", name="RP") + self.assertEqual( + demo.normalize_web_base("http://192.0.2.25:3000", name="RP"), + "http://192.0.2.25:3000", + ) + self.assertEqual( + demo.normalize_web_base("http://rp.example:18080", name="RP"), + "http://rp.example:18080", + ) with self.assertRaises(ValueError): demo.normalize_web_base("https://rp.example/callback", name="RP") self.assertEqual(demo.parse_listen("127.0.0.1:3000"), ("127.0.0.1", 3000)) diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index b078525d..d5efb734 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -366,7 +366,7 @@ func run(logger *zap.Logger) error { } telegramLoginService, err = telegramloginapp.NewService(postgres.NewTelegramLoginStore(pool), codeSealer, telegramloginapp.Config{ Issuer: cfg.TelegramLoginIssuer, AppScheme: cfg.PublicAppScheme, - AllowLoopbackHTTP: cfg.TelegramLoginAllowLoopbackHTTP, + AllowHTTP: cfg.TelegramLoginAllowHTTP, ClientSecretPepper: clientSecretPepper, SupportedSigningAlgorithms: signingKeys.ActiveAlgorithms(), RequestTTL: cfg.TelegramLoginRequestTTL, CodeTTL: cfg.TelegramLoginCodeTTL, @@ -375,7 +375,7 @@ func run(logger *zap.Logger) error { return fmt.Errorf("initialize telegram login service: %w", err) } telegramLoginIDTokens, err = telegramloginapp.NewIDTokenIssuer(signingKeys, telegramloginapp.IDTokenIssuerConfig{ - Issuer: cfg.TelegramLoginIssuer, TTL: cfg.TelegramLoginIDTokenTTL, + Issuer: cfg.TelegramLoginIssuer, TTL: cfg.TelegramLoginIDTokenTTL, AllowHTTP: cfg.TelegramLoginAllowHTTP, }) if err != nil { return fmt.Errorf("initialize telegram login ID-token issuer: %w", err) @@ -393,7 +393,7 @@ func run(logger *zap.Logger) error { Service: telegramLoginService, Tokens: telegramLoginIDTokens, Limiter: redisstore.NewRateLimiter(rdb), AppName: cfg.PublicAppName, Logger: logger.Named("telegram-login-http"), TrustedProxyCIDRs: cfg.TelegramLoginTrustedProxyCIDRs, - AllowLoopbackHTTP: cfg.TelegramLoginAllowLoopbackHTTP, + AllowHTTP: cfg.TelegramLoginAllowHTTP, }) if err != nil { return fmt.Errorf("initialize telegram login HTTP provider: %w", err) diff --git a/docs/configuration.en.md b/docs/configuration.en.md index 20f35c0b..aaa23531 100644 --- a/docs/configuration.en.md +++ b/docs/configuration.en.md @@ -51,7 +51,7 @@ This document describes every setting loaded by `internal/config`. Defaults and | Setting | Type / code default | Description and constraints | |---|---|---| | `TELESRV_DEBUG_ADDR` | nullable address / `127.0.0.1:6060` | pprof/debug listener. Empty disables it. Keep loopback-only; use an SSH tunnel for production profiling. | -| `TELESRV_BOT_API_ADDR` | nullable address / empty | Minimal HTTP Bot API listener. Empty disables it. It shares MTProto app/store facts. | +| `TELESRV_BOT_API_ADDR` | nullable address / empty | Minimal HTTP Bot API listener. Empty disables it. It shares MTProto app/store facts. `setWebhook` accepts any valid `http://` or `https://` host/IP and port in `1..65535`. | | `TELESRV_ADMIN_API_ADDR` | nullable address / empty | In-process Admin write API listener. Empty disables it; production should bind loopback. | | `TELESRV_ADMIN_API_TOKEN` | secret string / empty | Admin API bearer token. Required when the Admin API is enabled and must match the Admin UI token configuration. | | `TELESRV_ADMIN_UI_ADDR` | address / `127.0.0.1:2600` | Standalone `cmd/telesrv-admin` listen address. | @@ -62,7 +62,189 @@ This document describes every setting loaded by `internal/config`. Defaults and | `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | Automatic app-open scheme on landing pages. Must match patched client registration. `tg`, `http`, and `https` are rejected. | | `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | Web-client root used by public username pages. Same URL validation as `TELESRV_PUBLIC_BASE_URL`. | | `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | Public landing-page product name; trimmed, non-empty, no control characters, maximum 64 Unicode characters. | -| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / empty | Read-only username/avatar/sticker/emoji/chatlist landing-page listener. Empty disables it. Production should bind loopback behind exact nginx routes. `.env.example` enables `127.0.0.1:2401` for development. | +| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / empty | Read-only username/avatar/sticker/emoji/chatlist/collectible-gift landing-page listener. Empty disables it. Production should bind loopback behind exact nginx routes. `.env.example` enables `127.0.0.1:2401` for development. | +| `TELESRV_TELEGRAM_LOGIN_ENABLE` | bool / `false` | Mount the self-hosted Telegram Login/OIDC provider on `TELESRV_PUBLIC_LINK_WEB_ADDR`. Enabling it requires that listener and all key files below. | +| `TELESRV_TELEGRAM_LOGIN_ISSUER` | absolute origin URL / `TELESRV_PUBLIC_BASE_URL` | Exact public issuer used in discovery and tokens. HTTPS is required by default; paths, credentials, query, and fragment are rejected. The next setting permits any HTTP host/IP. | +| `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP` | bool / `false` | When enabled, permits any valid HTTP issuer, BotFather Web origin, redirect URI, and native HTTP callback, without loopback, subnet, or port restrictions. When disabled, those Web URLs still require HTTPS. | +| `TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE` | path / `data/telegram-login/signing-keys.json` | JOSE private-key ring generated by `cmd/telegramloginkeygen`; active plus retiring public keys are published through JWKS. | +| `TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE` | path / `data/telegram-login/code-keys.json` | AES-256-GCM envelope-key ring for recoverable, one-time authorization codes. | +| `TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE` | path / `data/telegram-login/client-secret-pepper` | Deployment pepper for HMAC-SHA-256 client-secret hashes. The file must contain a base64 encoding of exactly 32 random bytes. | +| `TELESRV_TELEGRAM_LOGIN_REQUEST_TTL` | duration / `5m` | Pending authorization lifetime; bounded to `1m..15m`. | +| `TELESRV_TELEGRAM_LOGIN_CODE_TTL` | duration / `2m` | One-time code lifetime; bounded to `30s..10m`. | +| `TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL` | duration / `1h` | Signed ID-token lifetime; bounded to `1m..24h`. Retiring signing keys must cover this window. | +| `TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS` | comma-separated CIDRs / empty | Only requests whose direct peer is in this list may supply `Forwarded`/`X-Forwarded-*` client metadata. The documented nginx deployment uses `127.0.0.1/32,::1/128`. | +| `TELESRV_TELEGRAM_LOGIN_RETENTION` | duration / `168h` | Retention after terminal request/code/revocation state; bounded to `1h..90d`. | +| `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | Retention worker interval; bounded to `10s..1h`. | +| `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | Maximum rows per retention pass; bounded to `1..1000`. | + +### 3.1 Complete Telegram Login / OIDC setup + +#### 1. Generate `data/telegram-login` once + +Run this from the `telesrv` repository root: + +```powershell +go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login +Get-ChildItem .\data\telegram-login +``` + +The same command works on Linux; restrict the generated directory afterward: + +```bash +go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login +chmod 0700 data/telegram-login +chmod 0600 data/telegram-login/* +``` + +Initialization creates the following private files. It never prints key material and refuses to +overwrite an existing `signing-keys.json`, `code-keys.json`, or `client-secret-pepper`: + +- `signing-keys.json` plus three `signing-*.pem` files: the manifest and private keys for RS256, + ES256, and EdDSA ID-token signatures; +- `code-keys.json`: the AES-256-GCM envelope-key ring for one-time authorization codes; +- `client-secret-pepper`: a 32-byte deployment pepper used to store and verify OIDC Client Secret + digests. + +The repository ignores `data/*` by default. Never put this directory in Git, release archives, +logs, or ordinary backups. All instances must mount the same protected files and restart together +after rotation. Losing the pepper invalidates existing Client Secret verification. Losing a signing +key that is still in its publication window invalidates otherwise-live ID tokens against JWKS. + +#### 2. Configure and start the Provider + +This example exposes OIDC directly at `http://192.0.2.25:2401`; replace it with the server address +that clients can actually reach. Bind `0.0.0.0:2401` for direct LAN/public access, or keep +`127.0.0.1:2401` when an on-host reverse proxy is the only caller: + +```env +TELESRV_PUBLIC_BASE_URL=http://192.0.2.25:2401 +TELESRV_PUBLIC_LINK_WEB_ADDR=0.0.0.0:2401 +TELESRV_PUBLIC_APP_SCHEME=telesrv + +TELESRV_TELEGRAM_LOGIN_ENABLE=true +TELESRV_TELEGRAM_LOGIN_ISSUER=http://192.0.2.25:2401 +TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true +TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE=data/telegram-login/signing-keys.json +TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE=data/telegram-login/code-keys.json +TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE=data/telegram-login/client-secret-pepper +``` + +For HTTPS, set the issuer and public base to the exact HTTPS origin and leave +`TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=false`. The issuer becomes the token `iss` and the root of all +discovery endpoints, so its scheme, host, and port must exactly match the address used by relying +parties. Start or restart `telesrv`, then verify the public endpoints: + +```powershell +curl.exe http://192.0.2.25:2401/.well-known/openid-configuration +curl.exe http://192.0.2.25:2401/.well-known/jwks.json +curl.exe -I http://192.0.2.25:2401/js/telegram-login.js +``` + +The discovery `issuer` must equal the configured value, and its `authorization_endpoint`, +`token_endpoint`, and `jwks_uri` must be reachable by the relying party. A reverse proxy must pass +through `/.well-known/openid-configuration`, `/.well-known/jwks.json`, `/auth`, `/auth/status`, +`/token`, `/crossapp`, `/inapp`, `/telegram-login.js`, and `/js/telegram-login.js` unchanged. + +#### 3. Create an OIDC Client with the local `@BotFather` + +Create a bot with `/newbot` or select an existing bot. In the local `@BotFather`, run `/setlogin` +and choose that bot. Initial setup returns: + +- `Client ID`: the bot user ID as a decimal string; +- `Client Secret`: shown once, separate from the Bot API token, and meant to be saved immediately + in a secret manager. + +Send each configuration command separately. This example runs the relying party at +`http://192.0.2.30:3000`: + +```text +add origin http://192.0.2.30:3000 +add redirect http://192.0.2.30:3000/oauth/callback +algorithm RS256 +enable +``` + +An `origin` is an exact Web origin without a path, query, or fragment; it authorizes the JS SDK, +popup CORS, and legacy `login_url`. A `redirect` is the exact full URI that receives an +Authorization Code. Wildcards and prefix matching are not supported. Use `/logininfo` to inspect +status and registrations; use `/setlogin` to add/remove URLs, change the algorithm, or disable the +client; use `/resetloginsecret` to rotate the Client Secret. Available algorithms are RS256, +ES256, EdDSA, and ES256K only when its build/key ring is present. EdDSA and ES256K accept only the +`openid` scope. + +#### 4. Integrate a relying party with standard OIDC + +Start by loading: + +```text +http://192.0.2.25:2401/.well-known/openid-configuration +``` + +The standard flow is Authorization Code with PKCE S256: + +1. Generate random `state`, `nonce`, and PKCE `code_verifier`; derive the S256 `code_challenge`. +2. Open the discovery `authorization_endpoint` with `client_id`, the exact `redirect_uri`, + `response_type=code`, a `scope` containing `openid`, `state`, `nonce`, `code_challenge`, and + `code_challenge_method=S256`. +3. After the user approves in TDesktop/Android, verify `state` at the relying-party callback and + read the one-time code. +4. Server-side, POST `grant_type=authorization_code`, the code, the same `redirect_uri`, and + `code_verifier` to the discovery `token_endpoint`. Confidential clients authenticate with HTTP + Basic or `client_secret_post`. +5. Verify the ID-token signature with the discovery `jwks_uri`, then strictly validate `iss`, + `aud`, `exp`, `nonce`, and a non-empty `sub`. Decoding without signature verification is not + sufficient. + +Supported scopes are `openid`, `profile`, `phone`, and `telegram:bot_access`. The provider does not +currently expose UserInfo, refresh tokens, or an introspection endpoint. Browser applications may +load `/js/telegram-login.js` for the local JS SDK. A Client Secret must remain server-side. + +#### 5. Verify the complete path with the Bedolaga demo + +Install the demo dependencies: + +```powershell +python -m venv "$env:TEMP\telesrv-bedolaga-demo-venv" +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" -m pip install ` + -r .\cmd\bots\bedolagaformat\requirements.txt +``` + +Put the Client ID/Secret from step 3 and the same bot's Bot API token only in process environment: + +```powershell +$env:TELESRV_BOT_TOKEN = ":" +$env:TELESRV_BOT_API_SERVER = "http://192.0.2.25:8081" +$env:TELESRV_BOT_LOGIN_DEMO = "1" +$env:TELESRV_BOT_LOGIN_ISSUER = "http://192.0.2.25:2401" +$env:TELESRV_BOT_LOGIN_CLIENT_ID = "" +$env:TELESRV_BOT_LOGIN_CLIENT_SECRET = "" +$env:TELESRV_BOT_LOGIN_PUBLIC_URL = "http://192.0.2.30:3000" +$env:TELESRV_BOT_LOGIN_LISTEN = "0.0.0.0:3000" + +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" ` + .\cmd\bots\bedolagaformat\demo.py --drop-pending --login-demo +``` + +The BotFather origin must equal `TELESRV_BOT_LOGIN_PUBLIC_URL`, and the redirect must equal +`/oauth/callback`. Send `/logindemo` to the bot. The first button +tests Bot API `login_url` plus the HMAC callback; the second page tests the local JS SDK popup and +Authorization Code + PKCE/JWKS. Omitting the Client Secret leaves JS popup verification available +but explicitly disables the server-side code flow. + +#### 6. Rotate keys + +When rotating a signing key, retain the old public key for at least the configured ID-token TTL +plus ten minutes. Restart all instances together after the operation: + +```powershell +go run ./cmd/telegramloginkeygen -mode rotate-signing -algorithm RS256 ` + -id-token-ttl 1h -publish-for 2h -dir data/telegram-login +go run ./cmd/telegramloginkeygen -mode rotate-code -dir data/telegram-login +``` + +Run `rotate-signing` separately for RS256, ES256, or EdDSA. `rotate-code` retains old code keys and +adds a new active key. Do not edit manifests or PEM files manually, and never generate divergent +key rings independently on different instances. ## 4. PostgreSQL, Redis, files, and seed data @@ -95,7 +277,7 @@ The language-pack file manifest is authoritative. To add a language, place `data | `TELESRV_AUTH_CODE_RATE_WINDOW` | duration / `10m` | Shared window for phone and auth-key issuance limits. | | `TELESRV_PHONE_CODE_DELIVERY_PROVIDER` | enum / `development` | `development` uses fixed codes; `webhook` generates random SMS codes for login, registration, and phone changes. Both modes first commit the same code to the durable 777000 dialog for existing accounts; Webhook is additive. | | `TELESRV_EMAIL_CODE_DELIVERY_PROVIDER` | enum / `smtp` | Delivery implementation for login-email and email setup/change codes: `smtp` or `webhook`. Existing-account login-email codes are first mirrored to 777000; setup/change remains provider-only. | -| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / empty | Required when any provider selects `webhook`; see [otp-delivery.md](otp-delivery.md) for the fixed v1 contract. Must use `http`/`https` and contain no userinfo. | +| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / empty | Required when any provider selects `webhook`; see [otp-delivery.md](otp-delivery.md) for the fixed v1 contract. Any valid `http://` or `https://` host/IP and port is accepted; userinfo is rejected. | | `TELESRV_OTP_WEBHOOK_SECRET` | secret string / empty | Optional HMAC-SHA256 signing secret; enables `X-Telesrv-Signature` when non-empty. | | `TELESRV_OTP_WEBHOOK_TIMEOUT` | duration / `5s` | Webhook HTTP timeout; must be positive when Webhook delivery is enabled. | | `TELESRV_LOGIN_EMAIL_ENABLE` | bool / `false` | Enables login-email verification. SMTP settings are required only when the email provider is `smtp`. | @@ -207,6 +389,19 @@ The following fallback keys are accepted from the **process environment only**. | `TELESRV_STARS_STARTING_GRANT` | int64 / `1000` | Idempotent lazy starting Stars balance for all accounts; `0` disables automatic grant. | | `TELESRV_PREMIUM_SWEEP_INTERVAL` | duration / `1m` | Expired-premium cleanup/push interval. Read paths derive expiry independently. | | `TELESRV_PREMIUM_SWEEP_BATCH` | int / `500` | Maximum expired premium rows processed per sweep. | +| `TELESRV_STARGIFT_SWEEP_INTERVAL` | duration / `15s` | Local Star Gift offer/auction lifecycle sweep interval; no blockchain connection is made. | +| `TELESRV_STARGIFT_SWEEP_BATCH` | int / `1000` | Maximum offer/auction/outbox work claimed per lifecycle sweep. | +| `TELESRV_STARGIFT_TON_STARTING_GRANT` | int64 / `10000000000` | Nanoton granted idempotently on a user's first access to the internal telesrv TON ledger; `0` disables it. This is not an on-chain asset. | +| `TELESRV_STARGIFT_TRANSFER_STARS` | int64 / `25` | Stars charged for a collectible transfer; `0` enables the free-transfer RPC. | +| `TELESRV_STARGIFT_DROP_DETAILS_STARS` | int64 / `25` | Stars charged to remove a collectible's original sender/message details. | +| `TELESRV_STARGIFT_OFFER_MIN_STARS` | int / `1` | Minimum Stars offer snapshotted for user-owned collectibles; `0` disables the offer entry point. | +| `TELESRV_STARGIFT_STARS_PROCEEDS_PERMILLE` | int / `1000` | Seller share in Stars sales, in permille; the remainder is recorded as platform commission. | +| `TELESRV_STARGIFT_TON_PROCEEDS_PERMILLE` | int / `1000` | Seller share in internal-TON sales, in permille; this affects only the local ledger. | +| `TELESRV_STARGIFT_EXPORT_DELAY` | duration / `0s` | Delay snapshotted into `can_export_at` when a collectible is issued. | +| `TELESRV_STARGIFT_TRANSFER_DELAY` | duration / `0s` | Delay snapshotted into `can_transfer_at`. | +| `TELESRV_STARGIFT_RESELL_DELAY` | duration / `0s` | Delay snapshotted into `can_resell_at`. | +| `TELESRV_STARGIFT_CRAFT_DELAY` | duration / `0s` | Delay snapshotted into `can_craft_at`. | +| `TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE` | int / `250` | Per-input local craft success contribution, capped at 1000 permille. | ## 11. Private calls, group calls, TURN, SFU, and livestream diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md index e08a2291..4c2e6af2 100644 --- a/docs/configuration.zh-CN.md +++ b/docs/configuration.zh-CN.md @@ -51,7 +51,7 @@ | 参数 | 类型 / 代码默认值 | 说明与约束 | |---|---|---| | `TELESRV_DEBUG_ADDR` | nullable address / `127.0.0.1:6060` | pprof/debug 监听;空值关闭。生产必须保持 loopback,通过 SSH 隧道抓取。 | -| `TELESRV_BOT_API_ADDR` | nullable address / 空 | 最小 HTTP Bot API 监听;空值关闭,与 MTProto 共用 app/store 事实。 | +| `TELESRV_BOT_API_ADDR` | nullable address / 空 | 最小 HTTP Bot API 监听;空值关闭,与 MTProto 共用 app/store 事实。`setWebhook` 接受任意合法 `http://` 或 `https://` 主机/IP 与 `1..65535` 端口。 | | `TELESRV_ADMIN_API_ADDR` | nullable address / 空 | 进程内 Admin 写 API;空值关闭,生产应只监听 loopback。 | | `TELESRV_ADMIN_API_TOKEN` | secret string / 空 | Admin API bearer token;启用 Admin API 时必须显式配置,并与 Admin UI 使用的 token 一致。 | | `TELESRV_ADMIN_UI_ADDR` | address / `127.0.0.1:2600` | 独立 `cmd/telesrv-admin` 监听地址。 | @@ -62,7 +62,181 @@ | `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | 落地页自动唤起客户端的 scheme,必须与 patched 客户端注册值一致;禁止 `tg`、`http`、`https`。 | | `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | username 页面 Web 客户端入口,校验规则同 `TELESRV_PUBLIC_BASE_URL`。 | | `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | 公开落地页产品名;trim 后非空、无控制字符、最多 64 个 Unicode 字符。 | -| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / 空 | 只读 username/avatar/sticker/emoji/chatlist 落地页监听;空值关闭。生产应 loopback + nginx 精确反代;`.env.example` 为开发启用 `127.0.0.1:2401`。 | +| `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / 空 | 只读 username/avatar/sticker/emoji/chatlist/collectible gift 落地页监听;空值关闭。生产应 loopback + nginx 精确反代;`.env.example` 为开发启用 `127.0.0.1:2401`。 | +| `TELESRV_TELEGRAM_LOGIN_ENABLE` | bool / `false` | 在 `TELESRV_PUBLIC_LINK_WEB_ADDR` 上挂载自建 Telegram Login/OIDC Provider;启用时必须同时配置该 listener 与下列全部密钥文件。 | +| `TELESRV_TELEGRAM_LOGIN_ISSUER` | 绝对 origin URL / `TELESRV_PUBLIC_BASE_URL` | discovery 与 token 使用的精确公开 issuer;默认必须 HTTPS,禁止 path、credentials、query、fragment。开启下一项后可直接配置任意 HTTP 域名/IP。 | +| `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP` | bool / `false` | 开启后允许任意合法 HTTP issuer、BotFather Web origin、redirect URI 和 native HTTP callback,不限制为 loopback,也不限制 IP 网段或端口。关闭时这些 Web URL 仍必须 HTTPS。 | +| `TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE` | path / `data/telegram-login/signing-keys.json` | 由 `cmd/telegramloginkeygen` 生成的 JOSE 私钥环;JWKS 会发布 active 和仍在退役窗口内的公钥。 | +| `TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE` | path / `data/telegram-login/code-keys.json` | 用于可恢复一次性 authorization code 的 AES-256-GCM envelope key ring。 | +| `TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE` | path / `data/telegram-login/client-secret-pepper` | HMAC-SHA-256 Client Secret 摘要的部署 pepper 文件,内容必须是恰好 32 个随机字节的 base64 编码。 | +| `TELESRV_TELEGRAM_LOGIN_REQUEST_TTL` | duration / `5m` | pending authorization 生命周期,限定 `1m..15m`。 | +| `TELESRV_TELEGRAM_LOGIN_CODE_TTL` | duration / `2m` | 一次性 code 生命周期,限定 `30s..10m`。 | +| `TELESRV_TELEGRAM_LOGIN_ID_TOKEN_TTL` | duration / `1h` | ID token 生命周期,限定 `1m..24h`;退役签名公钥必须覆盖该窗口。 | +| `TELESRV_TELEGRAM_LOGIN_TRUSTED_PROXY_CIDRS` | 逗号分隔 CIDR / 空 | 只有直连 peer 落在该列表时才信任 `Forwarded`/`X-Forwarded-*` 客户端元数据;文档中的单机 nginx 部署使用 `127.0.0.1/32,::1/128`。 | +| `TELESRV_TELEGRAM_LOGIN_RETENTION` | duration / `168h` | terminal request/code/revocation 后的保留期,限定 `1h..90d`。 | +| `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | retention worker 周期,限定 `10s..1h`。 | +| `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | 每轮最大清理行数,限定 `1..1000`。 | + +### 3.1 Telegram Login / OIDC 完整启用流程 + +#### 1. 一次性生成 `data/telegram-login` + +在 `telesrv` 仓库根目录执行: + +```powershell +go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login +Get-ChildItem .\data\telegram-login +``` + +Linux 部署也可使用同一命令;生成后应限制目录权限: + +```bash +go run ./cmd/telegramloginkeygen -mode init -dir data/telegram-login +chmod 0700 data/telegram-login +chmod 0600 data/telegram-login/* +``` + +初始化会生成以下私密文件,命令不会把密钥内容输出到终端,并会拒绝覆盖已经存在的 +`signing-keys.json`、`code-keys.json` 或 `client-secret-pepper`: + +- `signing-keys.json` 和三个 `signing-*.pem`:RS256、ES256、EdDSA ID token 签名私钥及清单; +- `code-keys.json`:一次性 authorization code 使用的 AES-256-GCM envelope key ring; +- `client-secret-pepper`:保存和校验 OIDC Client Secret 摘要时使用的 32 字节部署 pepper。 + +`data/*` 默认已被仓库 `.gitignore` 排除。不要把该目录放入 Git、发布压缩包、日志或 +普通备份;多实例必须挂载同一份受保护的文件,并在轮换后一起重启。丢失 pepper 会让 +现有 Client Secret 无法验证,丢失仍在发布窗口内的签名私钥会让尚未过期的 ID token +无法继续通过 JWKS 验证。 + +#### 2. 配置并启动 Provider + +以下示例直接通过 `http://192.0.2.25:2401` 对外提供 OIDC;请替换成客户端实际可达的 +服务器 IP。直接监听局域网/公网网卡时使用 `0.0.0.0:2401`,仅由同机反向代理转发时 +应改回 `127.0.0.1:2401`: + +```env +TELESRV_PUBLIC_BASE_URL=http://192.0.2.25:2401 +TELESRV_PUBLIC_LINK_WEB_ADDR=0.0.0.0:2401 +TELESRV_PUBLIC_APP_SCHEME=telesrv + +TELESRV_TELEGRAM_LOGIN_ENABLE=true +TELESRV_TELEGRAM_LOGIN_ISSUER=http://192.0.2.25:2401 +TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true +TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE=data/telegram-login/signing-keys.json +TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE=data/telegram-login/code-keys.json +TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE=data/telegram-login/client-secret-pepper +``` + +使用 HTTPS 时,把 `TELESRV_TELEGRAM_LOGIN_ISSUER` 和公开根地址改成精确 HTTPS +origin,并保持 `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=false`。issuer 是 token 的 `iss` +以及 discovery 中所有端点的根地址,scheme、host 和 port 必须与依赖方访问的地址完全 +一致。启动或重启 `telesrv` 后,先验证公开端点: + +```powershell +curl.exe http://192.0.2.25:2401/.well-known/openid-configuration +curl.exe http://192.0.2.25:2401/.well-known/jwks.json +curl.exe -I http://192.0.2.25:2401/js/telegram-login.js +``` + +discovery 返回的 `issuer` 必须等于配置值,`authorization_endpoint`、`token_endpoint` +和 `jwks_uri` 必须可从依赖方访问。使用反向代理时需原样转发 +`/.well-known/openid-configuration`、`/.well-known/jwks.json`、`/auth`、`/auth/status`、 +`/token`、`/crossapp`、`/inapp`、`/telegram-login.js` 和 `/js/telegram-login.js`。 + +#### 3. 用本服 `@BotFather` 创建 OIDC Client + +先用 `/newbot` 创建或选择已有 bot,然后在本服 `@BotFather` 中执行 `/setlogin` 并选择 +该 bot。首次配置会返回: + +- `Client ID`:bot user ID 的十进制字符串; +- `Client Secret`:只显示一次,与 Bot API token 不同,必须立即保存到密钥管理系统。 + +接着逐条发送配置命令。下面假设依赖方页面运行在 `http://192.0.2.30:3000`: + +```text +add origin http://192.0.2.30:3000 +add redirect http://192.0.2.30:3000/oauth/callback +algorithm RS256 +enable +``` + +`origin` 只能是无 path/query/fragment 的精确 Web origin,用于 JS SDK、popup CORS 和 +legacy `login_url`;`redirect` 是 Authorization Code Flow 返回 code 的精确完整 URI。 +不支持 wildcard 或 prefix 匹配。用 `/logininfo` 检查状态和登记值;用 `/setlogin` +增删 URL、切换签名算法或 disable;用 `/resetloginsecret` 轮换 Client Secret。可用的 +签名算法为 RS256、ES256、EdDSA,以及仅在对应构建和 key ring 已提供时可选的 ES256K; +EdDSA/ES256K 只允许 `openid` scope。 + +#### 4. 依赖方接入标准 OIDC + +依赖方应首先读取: + +```text +http://192.0.2.25:2401/.well-known/openid-configuration +``` + +标准流程为 Authorization Code + PKCE S256: + +1. 生成随机 `state`、`nonce` 和 PKCE `code_verifier`,计算 S256 `code_challenge`; +2. 浏览器打开 discovery 中的 `authorization_endpoint`,携带 `client_id`、精确 + `redirect_uri`、`response_type=code`、包含 `openid` 的 `scope`、`state`、`nonce`、 + `code_challenge` 和 `code_challenge_method=S256`; +3. 用户在 TDesktop/Android 中确认后,依赖方 callback 校验 `state` 并取得一次性 code; +4. 服务端向 discovery 中的 `token_endpoint` POST `grant_type=authorization_code`、code、 + 同一 `redirect_uri` 和 `code_verifier`,机密 client 使用 HTTP Basic 或 + `client_secret_post` 提交 Client Secret; +5. 用 discovery 的 `jwks_uri` 验证 ID token 签名,并严格校验 `iss`、`aud`、`exp`、 + `nonce` 和非空 `sub`。不要只解码而不验签。 + +支持的 scope 为 `openid`、`profile`、`phone`、`telegram:bot_access`。当前不提供 +UserInfo、refresh token 或 introspection endpoint。浏览器前端可以加载 +`/js/telegram-login.js` 使用本地 JS SDK;Client Secret 只能留在服务端。 + +#### 5. 使用 Bedolaga demo 验证完整链路 + +安装 demo 依赖: + +```powershell +python -m venv "$env:TEMP\telesrv-bedolaga-demo-venv" +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" -m pip install ` + -r .\cmd\bots\bedolagaformat\requirements.txt +``` + +将第 3 步得到的 Client ID/Secret 和同一个 Bot API token 仅放入进程环境: + +```powershell +$env:TELESRV_BOT_TOKEN = ":" +$env:TELESRV_BOT_API_SERVER = "http://192.0.2.25:8081" +$env:TELESRV_BOT_LOGIN_DEMO = "1" +$env:TELESRV_BOT_LOGIN_ISSUER = "http://192.0.2.25:2401" +$env:TELESRV_BOT_LOGIN_CLIENT_ID = "" +$env:TELESRV_BOT_LOGIN_CLIENT_SECRET = "<只显示一次的 OIDC Client Secret>" +$env:TELESRV_BOT_LOGIN_PUBLIC_URL = "http://192.0.2.30:3000" +$env:TELESRV_BOT_LOGIN_LISTEN = "0.0.0.0:3000" + +& "$env:TEMP\telesrv-bedolaga-demo-venv\Scripts\python.exe" ` + .\cmd\bots\bedolagaformat\demo.py --drop-pending --login-demo +``` + +确保 BotFather 登记的 origin 等于 `TELESRV_BOT_LOGIN_PUBLIC_URL`,redirect 等于 +`/oauth/callback`。在客户端向 bot 发送 `/logindemo`:第一颗 +按钮验证 Bot API `login_url` 和 HMAC 回调,第二颗按钮页面分别验证本地 JS SDK popup +以及 Authorization Code + PKCE/JWKS。省略 Client Secret 时只能验证 JS popup,服务端 +code flow 会明确禁用。 + +#### 6. 密钥轮换 + +签名 key 轮换时,旧公钥发布窗口必须至少覆盖配置的 ID token TTL 再加 10 分钟;操作 +完成后所有实例一起重启: + +```powershell +go run ./cmd/telegramloginkeygen -mode rotate-signing -algorithm RS256 ` + -id-token-ttl 1h -publish-for 2h -dir data/telegram-login +go run ./cmd/telegramloginkeygen -mode rotate-code -dir data/telegram-login +``` + +`rotate-signing` 可分别用于 RS256、ES256、EdDSA;`rotate-code` 保留旧 code key 并新增 +active key。不要手工编辑 manifest 或 PEM,不要在各实例上分别生成不一致的 key ring。 ## 4. PostgreSQL、Redis、文件与 seed @@ -95,7 +269,7 @@ | `TELESRV_AUTH_CODE_RATE_WINDOW` | duration / `10m` | 手机号与 auth-key 发码限流共用窗口。 | | `TELESRV_PHONE_CODE_DELIVERY_PROVIDER` | enum / `development` | `development` 使用固定码;`webhook` 为登录、注册、改号生成随机 SMS code 并调用 OTP Webhook。已有账号在两种模式下都先 durable 写入同码 777000 消息,Webhook 只是附加渠道。 | | `TELESRV_EMAIL_CODE_DELIVERY_PROVIDER` | enum / `smtp` | 登录邮箱、邮箱 setup/change 的投递实现:`smtp` 或 `webhook`。已有账号的登录邮箱码会先同码镜像到 777000;邮箱 setup/change 仍只走 provider。 | -| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / 空 | 任一 provider 选择 `webhook` 时必填;固定 v1 协议见 [otp-delivery.md](otp-delivery.md)。只允许 `http`/`https` 且不得含 userinfo。 | +| `TELESRV_OTP_WEBHOOK_URL` | absolute URL / 空 | 任一 provider 选择 `webhook` 时必填;固定 v1 协议见 [otp-delivery.md](otp-delivery.md)。允许任意合法 `http://` 或 `https://` 主机/IP 与端口,不得含 userinfo。 | | `TELESRV_OTP_WEBHOOK_SECRET` | secret string / 空 | 可选 HMAC-SHA256 签名密钥;非空时发送 `X-Telesrv-Signature`。 | | `TELESRV_OTP_WEBHOOK_TIMEOUT` | duration / `5s` | Webhook HTTP 请求超时,启用 Webhook 时必须为正数。 | | `TELESRV_LOGIN_EMAIL_ENABLE` | bool / `false` | 启用登录邮箱验证码;email provider 为 `smtp` 时要求 SMTP 配置,`webhook` 时不依赖 SMTP。 | @@ -207,6 +381,19 @@ | `TELESRV_STARS_STARTING_GRANT` | int64 / `1000` | 对所有账号幂等惰性授予的 Stars 起始余额;`0` 关闭自动赠送。 | | `TELESRV_PREMIUM_SWEEP_INTERVAL` | duration / `1m` | 过期 Premium 清理/推送周期;读取路径独立即时派生到期状态。 | | `TELESRV_PREMIUM_SWEEP_BATCH` | int / `500` | 单次 sweep 最大处理行数。 | +| `TELESRV_STARGIFT_SWEEP_INTERVAL` | duration / `15s` | Star Gift 报价/竞拍本地生命周期清扫周期;不会连接区块链。 | +| `TELESRV_STARGIFT_SWEEP_BATCH` | int / `1000` | 单次礼物生命周期清扫最多处理的报价、竞拍与 outbox 工作量。 | +| `TELESRV_STARGIFT_TON_STARTING_GRANT` | int64 / `10000000000` | 每个用户首次访问 telesrv 内部 TON 账本时幂等授予的 nanoton;`0` 关闭赠送。它不是链上资产。 | +| `TELESRV_STARGIFT_TRANSFER_STARS` | int64 / `25` | collectible 转赠费用;设为 `0` 时使用免费转赠 RPC。 | +| `TELESRV_STARGIFT_DROP_DETAILS_STARS` | int64 / `25` | 移除 collectible 原始发送者/附言信息所需 Stars。 | +| `TELESRV_STARGIFT_OFFER_MIN_STARS` | int / `1` | collectible 签发时固化的用户持有礼物最低 Stars 报价;`0` 不开放报价入口。 | +| `TELESRV_STARGIFT_STARS_PROCEEDS_PERMILLE` | int / `1000` | Stars 成交时卖方实收比例(千分比);差额作为平台佣金写入成交记录。 | +| `TELESRV_STARGIFT_TON_PROCEEDS_PERMILLE` | int / `1000` | 内部 TON 成交时卖方实收比例(千分比);只影响本地账本。 | +| `TELESRV_STARGIFT_EXPORT_DELAY` | duration / `0s` | collectible 签发时固化到 `can_export_at` 的等待期。 | +| `TELESRV_STARGIFT_TRANSFER_DELAY` | duration / `0s` | 签发时固化到 `can_transfer_at` 的等待期。 | +| `TELESRV_STARGIFT_RESELL_DELAY` | duration / `0s` | 签发时固化到 `can_resell_at` 的等待期。 | +| `TELESRV_STARGIFT_CRAFT_DELAY` | duration / `0s` | 签发时固化到 `can_craft_at` 的等待期;可 Craft 礼物即使为 `0s` 也写升级时间这一正数能力边界,0 只表示不具备 Craft 能力或已终结。 | +| `TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE` | int / `250` | 每份输入礼物贡献的本地合成成功概率,累计上限 1000‰。 | ## 11. 私聊通话、群通话、TURN、SFU 与直播 diff --git a/internal/app/bots/botfather_login_test.go b/internal/app/bots/botfather_login_test.go index 8a77f63c..f48543fe 100644 --- a/internal/app/bots/botfather_login_test.go +++ b/internal/app/bots/botfather_login_test.go @@ -22,7 +22,7 @@ func newBotFatherTelegramLoginService(t *testing.T) *telegramloginapp.Service { pepper := make([]byte, 32) pepper[0] = 2 service, err := telegramloginapp.NewService(memory.NewTelegramLoginStore(nil), sealer, telegramloginapp.Config{ - Issuer: "http://localhost:2404", AppScheme: "telesrv", AllowLoopbackHTTP: true, + Issuer: "http://192.0.2.25:2404", AppScheme: "telesrv", AllowHTTP: true, ClientSecretPepper: pepper, Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() }, }) if err != nil { @@ -52,13 +52,13 @@ func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) { if len(secret) < 32 { t.Fatalf("client secret is unexpectedly short: %q", secret) } - if reply := sendToBotFather(t, svc, messages, owner, "add origin http://localhost:3000"); !strings.Contains(reply, "Success!") { + if reply := sendToBotFather(t, svc, messages, owner, "add origin http://rp.example.test:3000"); !strings.Contains(reply, "Success!") { t.Fatalf("add origin reply = %q", reply) } sendToBotFather(t, svc, messages, owner, "/setlogin") sendToBotFather(t, svc, messages, owner, "login_demo_bot") - if reply := sendToBotFather(t, svc, messages, owner, "add redirect http://localhost:3000/auth/callback"); !strings.Contains(reply, "Success!") { + if reply := sendToBotFather(t, svc, messages, owner, "add redirect http://192.0.2.26:3000/auth/callback"); !strings.Contains(reply, "Success!") { t.Fatalf("add redirect reply = %q", reply) } @@ -83,7 +83,7 @@ func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) { sendToBotFather(t, svc, messages, owner, "/logininfo") info := sendToBotFather(t, svc, messages, owner, "login_demo_bot") - for _, want := range []string{"Signing algorithm: ES256", "web_origin http://localhost:3000", "redirect_uri http://localhost:3000/auth/callback", "dev.bedolaga.demo", "Bedolaga iOS Demo", "Bedolaga Android Demo"} { + for _, want := range []string{"Signing algorithm: ES256", "web_origin http://rp.example.test:3000", "redirect_uri http://192.0.2.26:3000/auth/callback", "dev.bedolaga.demo", "Bedolaga iOS Demo", "Bedolaga Android Demo"} { if !strings.Contains(info, want) { t.Fatalf("login info = %q, missing %q", info, want) } diff --git a/internal/app/telegramlogin/jose.go b/internal/app/telegramlogin/jose.go index 60986af6..a05424f2 100644 --- a/internal/app/telegramlogin/jose.go +++ b/internal/app/telegramlogin/jose.go @@ -293,9 +293,10 @@ func (r *SigningKeyRing) sign(algorithm domain.TelegramLoginSigningAlgorithm, to } type IDTokenIssuerConfig struct { - Issuer string - TTL time.Duration - Now func() time.Time + Issuer string + TTL time.Duration + Now func() time.Time + AllowHTTP bool } type IDTokenIssuer struct { @@ -337,7 +338,7 @@ func NewIDTokenIssuer(keys *SigningKeyRing, cfg IDTokenIssuerConfig) (*IDTokenIs if keys == nil { return nil, errors.New("telegram login signing key ring is required") } - issuer, err := NormalizeWebOrigin(cfg.Issuer, true) + issuer, err := NormalizeWebOrigin(cfg.Issuer, cfg.AllowHTTP) if err != nil { return nil, fmt.Errorf("telegram login ID token issuer: %w", err) } diff --git a/internal/app/telegramlogin/jose_test.go b/internal/app/telegramlogin/jose_test.go index 108cd20a..eaea7b1f 100644 --- a/internal/app/telegramlogin/jose_test.go +++ b/internal/app/telegramlogin/jose_test.go @@ -167,6 +167,21 @@ func TestIDTokenIssuerScopeProjectionAndVerification(t *testing.T) { } } +func TestIDTokenIssuerAcceptsHTTPIPOnlyWhenEnabled(t *testing.T) { + now := time.Date(2026, 7, 21, 10, 0, 0, 0, time.UTC) + ring := telegramLoginTestSigningKeys(t, &now) + if _, err := NewIDTokenIssuer(ring, IDTokenIssuerConfig{Issuer: "http://192.0.2.25:2401"}); err == nil { + t.Fatal("HTTP issuer was accepted while AllowHTTP was false") + } + issuer, err := NewIDTokenIssuer(ring, IDTokenIssuerConfig{Issuer: "http://192.0.2.25:2401", AllowHTTP: true}) + if err != nil { + t.Fatal(err) + } + if issuer.Issuer() != "http://192.0.2.25:2401" { + t.Fatalf("issuer=%q", issuer.Issuer()) + } +} + func TestSigningKeyRingRejectsWrongCurveAndDuplicateActiveKey(t *testing.T) { p384, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { diff --git a/internal/app/telegramlogin/native.go b/internal/app/telegramlogin/native.go index 97d74282..565a4352 100644 --- a/internal/app/telegramlogin/native.go +++ b/internal/app/telegramlogin/native.go @@ -45,7 +45,7 @@ func normalizeNativeVerificationID(platform domain.TelegramLoginNativePlatform, // non-web custom scheme registered for a native application. Query and // fragment components are forbidden because OAuth response fields are // appended by the provider and must not collide with application input. -func NormalizeNativeCallbackURI(raw string, allowLoopbackHTTP bool) (string, error) { +func NormalizeNativeCallbackURI(raw string, allowHTTP bool) (string, error) { if raw == "" || len(raw) > maxTelegramLoginURLLength || raw != strings.TrimSpace(raw) || strings.IndexFunc(raw, unicode.IsControl) >= 0 { return "", domain.ErrTelegramLoginURLInvalid } @@ -54,7 +54,7 @@ func NormalizeNativeCallbackURI(raw string, allowLoopbackHTTP bool) (string, err return "", domain.ErrTelegramLoginURLInvalid } if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") { - normalized, _, err := NormalizeRedirectURI(raw, allowLoopbackHTTP) + normalized, _, err := NormalizeRedirectURI(raw, allowHTTP) return normalized, err } scheme := strings.ToLower(u.Scheme) diff --git a/internal/app/telegramlogin/service.go b/internal/app/telegramlogin/service.go index 77c34f97..dd725a03 100644 --- a/internal/app/telegramlogin/service.go +++ b/internal/app/telegramlogin/service.go @@ -36,7 +36,7 @@ var telegramLoginMatchCodePool = []string{ type Config struct { Issuer string AppScheme string - AllowLoopbackHTTP bool + AllowHTTP bool ClientSecretPepper []byte SupportedSigningAlgorithms []domain.TelegramLoginSigningAlgorithm RequestTTL time.Duration @@ -49,7 +49,7 @@ type Service struct { sealer *CodeSealer issuer string appScheme string - allowLoopbackHTTP bool + allowHTTP bool clientSecretPepper []byte signingAlgorithms []domain.TelegramLoginSigningAlgorithm signingAlgorithmSet map[domain.TelegramLoginSigningAlgorithm]struct{} @@ -62,7 +62,7 @@ func NewService(loginStore store.TelegramLoginStore, sealer *CodeSealer, cfg Con if loginStore == nil || sealer == nil || len(cfg.ClientSecretPepper) < 32 { return nil, fmt.Errorf("telegram login dependencies are incomplete") } - issuer, err := NormalizeWebOrigin(cfg.Issuer, cfg.AllowLoopbackHTTP) + issuer, err := NormalizeWebOrigin(cfg.Issuer, cfg.AllowHTTP) if err != nil { return nil, fmt.Errorf("telegram login issuer: %w", err) } @@ -93,7 +93,7 @@ func NewService(loginStore store.TelegramLoginStore, sealer *CodeSealer, cfg Con } return &Service{ store: loginStore, sealer: sealer, issuer: issuer, appScheme: strings.ToLower(cfg.AppScheme), - allowLoopbackHTTP: cfg.AllowLoopbackHTTP, + allowHTTP: cfg.AllowHTTP, clientSecretPepper: append([]byte(nil), cfg.ClientSecretPepper...), signingAlgorithms: append([]domain.TelegramLoginSigningAlgorithm(nil), cfg.SupportedSigningAlgorithms...), signingAlgorithmSet: signingAlgorithmSet, @@ -248,9 +248,9 @@ func (s *Service) AddAllowedURL(ctx context.Context, botUserID int64, kind domai var err error switch kind { case domain.TelegramLoginAllowedWebOrigin: - normalized, err = NormalizeWebOrigin(raw, s.allowLoopbackHTTP) + normalized, err = NormalizeWebOrigin(raw, s.allowHTTP) case domain.TelegramLoginAllowedRedirectURI: - normalized, _, err = NormalizeRedirectURI(raw, s.allowLoopbackHTTP) + normalized, _, err = NormalizeRedirectURI(raw, s.allowHTTP) default: err = domain.ErrTelegramLoginURLInvalid } @@ -267,9 +267,9 @@ func (s *Service) DeleteAllowedURL(ctx context.Context, botUserID int64, kind do var err error switch kind { case domain.TelegramLoginAllowedWebOrigin: - normalized, err = NormalizeWebOrigin(raw, s.allowLoopbackHTTP) + normalized, err = NormalizeWebOrigin(raw, s.allowHTTP) case domain.TelegramLoginAllowedRedirectURI: - normalized, _, err = NormalizeRedirectURI(raw, s.allowLoopbackHTTP) + normalized, _, err = NormalizeRedirectURI(raw, s.allowHTTP) default: err = domain.ErrTelegramLoginURLInvalid } @@ -311,7 +311,7 @@ func (s *Service) AddNativeApp(ctx context.Context, botUserID int64, platform do if err != nil { return domain.TelegramLoginNativeApp{}, err } - callbackURI, err = NormalizeNativeCallbackURI(callbackURI, s.allowLoopbackHTTP) + callbackURI, err = NormalizeNativeCallbackURI(callbackURI, s.allowHTTP) if err != nil { return domain.TelegramLoginNativeApp{}, err } @@ -335,7 +335,7 @@ func (s *Service) DeleteNativeApp(ctx context.Context, botUserID, appID int64) ( } func (s *Service) matchNativeApp(ctx context.Context, botUserID int64, platform domain.TelegramLoginNativePlatform, rawCallbackURI string) (domain.TelegramLoginNativeApp, string, bool, error) { - callbackURI, err := NormalizeNativeCallbackURI(rawCallbackURI, s.allowLoopbackHTTP) + callbackURI, err := NormalizeNativeCallbackURI(rawCallbackURI, s.allowHTTP) if err != nil { return domain.TelegramLoginNativeApp{}, "", false, nil } @@ -362,7 +362,7 @@ func (s *Service) ValidateMessageButton(ctx context.Context, botUserID int64, ra if !found || !client.Enabled { return "", "", domain.ErrTelegramLoginClientDisabled } - normalizedURL, domainName, err = NormalizeRedirectURI(rawURL, s.allowLoopbackHTTP) + normalizedURL, domainName, err = NormalizeRedirectURI(rawURL, s.allowHTTP) if err != nil { return "", "", err } @@ -370,7 +370,7 @@ func (s *Service) ValidateMessageButton(ctx context.Context, botUserID int64, ra if err != nil { return "", "", domain.ErrTelegramLoginURLInvalid } - origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowLoopbackHTTP) + origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowHTTP) if err != nil { return "", "", err } @@ -415,7 +415,7 @@ func (s *Service) AuthorizeMessageButton(ctx context.Context, params domain.Tele if err != nil { return domain.TelegramLoginMessageButtonResult{}, domain.ErrTelegramLoginURLInvalid } - origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowLoopbackHTTP) + origin, err := NormalizeWebOrigin(u.Scheme+"://"+u.Host, s.allowHTTP) if err != nil { return domain.TelegramLoginMessageButtonResult{}, err } @@ -552,7 +552,7 @@ func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthoriz } var allowed, isApp bool var nativeApp domain.TelegramLoginNativeApp - redirectURI, domainName, redirectErr := NormalizeRedirectURI(params.RedirectURI, s.allowLoopbackHTTP) + redirectURI, domainName, redirectErr := NormalizeRedirectURI(params.RedirectURI, s.allowHTTP) if params.ResponseType == "code" && redirectErr == nil && !params.NativePlatform.Valid() { allowed, err = s.store.IsTelegramLoginURLAllowed(ctx, client.BotUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI) if err != nil { @@ -603,14 +603,14 @@ func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthoriz u, _ := url.Parse(redirectURI) origin = u.Scheme + "://" + u.Host } - origin, err = NormalizeWebOrigin(origin, s.allowLoopbackHTTP) + origin, err = NormalizeWebOrigin(origin, s.allowHTTP) if err != nil { return CreatedAuthorization{}, err } } if params.ResponseType == "post_message" { redirectURL, _ := url.Parse(redirectURI) - redirectOrigin, redirectOriginErr := NormalizeWebOrigin(redirectURL.Scheme+"://"+redirectURL.Host, s.allowLoopbackHTTP) + redirectOrigin, redirectOriginErr := NormalizeWebOrigin(redirectURL.Scheme+"://"+redirectURL.Host, s.allowHTTP) if redirectOriginErr != nil || redirectOrigin != origin { return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed } @@ -627,7 +627,7 @@ func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthoriz if isApp { return CreatedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed } - inAppOrigin, err = NormalizeWebOrigin(params.InAppOrigin, s.allowLoopbackHTTP) + inAppOrigin, err = NormalizeWebOrigin(params.InAppOrigin, s.allowHTTP) if err != nil { return CreatedAuthorization{}, err } @@ -693,7 +693,7 @@ func (s *Service) ResolveAuthorizationErrorTarget(ctx context.Context, clientID, redirectURI, safe, err := s.safeCodeRedirect(ctx, client.BotUserID, rawRedirectURI) return AuthorizationErrorTarget{ResponseType: responseType, RedirectURI: redirectURI}, safe, err case "post_message": - redirectURI, _, err := NormalizeRedirectURI(rawRedirectURI, s.allowLoopbackHTTP) + redirectURI, _, err := NormalizeRedirectURI(rawRedirectURI, s.allowHTTP) if err != nil { return AuthorizationErrorTarget{}, false, nil } @@ -702,12 +702,12 @@ func (s *Service) ResolveAuthorizationErrorTarget(ctx context.Context, clientID, redirect, _ := url.Parse(redirectURI) origin = redirect.Scheme + "://" + redirect.Host } - origin, err = NormalizeWebOrigin(origin, s.allowLoopbackHTTP) + origin, err = NormalizeWebOrigin(origin, s.allowHTTP) if err != nil { return AuthorizationErrorTarget{}, false, nil } redirect, _ := url.Parse(redirectURI) - redirectOrigin, err := NormalizeWebOrigin(redirect.Scheme+"://"+redirect.Host, s.allowLoopbackHTTP) + redirectOrigin, err := NormalizeWebOrigin(redirect.Scheme+"://"+redirect.Host, s.allowHTTP) if err != nil || redirectOrigin != origin { return AuthorizationErrorTarget{}, false, nil } @@ -725,7 +725,7 @@ func (s *Service) ResolveAuthorizationErrorTarget(ctx context.Context, clientID, } func (s *Service) safeCodeRedirect(ctx context.Context, botUserID int64, raw string) (string, bool, error) { - if redirectURI, _, err := NormalizeRedirectURI(raw, s.allowLoopbackHTTP); err == nil { + if redirectURI, _, err := NormalizeRedirectURI(raw, s.allowHTTP); err == nil { allowed, err := s.store.IsTelegramLoginURLAllowed(ctx, botUserID, domain.TelegramLoginAllowedRedirectURI, redirectURI) if err != nil || allowed { return redirectURI, allowed, err @@ -856,7 +856,7 @@ func (s *Service) RequestByDeepLinkForOrigin(ctx context.Context, rawURL, rawOri if rawOrigin == "" || request.InAppOrigin == "" { return domain.TelegramLoginRequest{}, domain.ErrTelegramLoginOriginNotAllowed } - origin, err := NormalizeWebOrigin(rawOrigin, s.allowLoopbackHTTP) + origin, err := NormalizeWebOrigin(rawOrigin, s.allowHTTP) if err != nil { return domain.TelegramLoginRequest{}, err } @@ -1067,7 +1067,7 @@ func (s *Service) ExchangeInAppTokenAndIssue(ctx context.Context, token, rawOrig if issuer == nil || len(token) < 16 || len(token) > 1024 || strings.IndexFunc(token, func(r rune) bool { return r <= 0x20 || r == 0x7f }) >= 0 { return IssuedAuthorization{}, domain.ErrTelegramLoginCodeInvalid } - origin, err := NormalizeWebOrigin(rawOrigin, s.allowLoopbackHTTP) + origin, err := NormalizeWebOrigin(rawOrigin, s.allowHTTP) if err != nil { return IssuedAuthorization{}, domain.ErrTelegramLoginOriginNotAllowed } @@ -1294,7 +1294,7 @@ func (s *Service) exchangeAuthorizationCode(ctx context.Context, params Exchange return ExchangedAuthorization{}, "", domain.ErrTelegramLoginCodeInvalid } } else { - redirectURI, _, err = NormalizeRedirectURI(params.RedirectURI, s.allowLoopbackHTTP) + redirectURI, _, err = NormalizeRedirectURI(params.RedirectURI, s.allowHTTP) if err != nil { return ExchangedAuthorization{}, "", err } diff --git a/internal/app/telegramlogin/service_test.go b/internal/app/telegramlogin/service_test.go index d0d02216..dc98e407 100644 --- a/internal/app/telegramlogin/service_test.go +++ b/internal/app/telegramlogin/service_test.go @@ -94,7 +94,7 @@ func newTelegramLoginTestServiceWithAlgorithms(t *testing.T, now *time.Time, alg pepper[0] = 9 service, err := NewService(loginStore, sealer, Config{ Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv", - AllowLoopbackHTTP: true, ClientSecretPepper: pepper, + AllowHTTP: true, ClientSecretPepper: pepper, SupportedSigningAlgorithms: algorithms, Now: func() time.Time { return *now }, }) diff --git a/internal/app/telegramlogin/url.go b/internal/app/telegramlogin/url.go index dfc9a21a..62ec11ec 100644 --- a/internal/app/telegramlogin/url.go +++ b/internal/app/telegramlogin/url.go @@ -14,8 +14,8 @@ import ( const maxTelegramLoginURLLength = 4096 -func NormalizeRedirectURI(raw string, allowLoopbackHTTP bool) (normalized, domainName string, err error) { - u, err := parseWebURL(raw, allowLoopbackHTTP) +func NormalizeRedirectURI(raw string, allowHTTP bool) (normalized, domainName string, err error) { + u, err := parseWebURL(raw, allowHTTP) if err != nil { return "", "", err } @@ -34,8 +34,8 @@ func NormalizeRedirectURI(raw string, allowLoopbackHTTP bool) (normalized, domai return u.String(), u.Hostname(), nil } -func NormalizeWebOrigin(raw string, allowLoopbackHTTP bool) (string, error) { - u, err := parseWebURL(raw, allowLoopbackHTTP) +func NormalizeWebOrigin(raw string, allowHTTP bool) (string, error) { + u, err := parseWebURL(raw, allowHTTP) if err != nil { return "", err } @@ -46,7 +46,7 @@ func NormalizeWebOrigin(raw string, allowLoopbackHTTP bool) (string, error) { return u.String(), nil } -func parseWebURL(raw string, allowLoopbackHTTP bool) (*url.URL, error) { +func parseWebURL(raw string, allowHTTP bool) (*url.URL, error) { if raw == "" || len(raw) > maxTelegramLoginURLLength || raw != strings.TrimSpace(raw) || strings.IndexFunc(raw, unicode.IsControl) >= 0 { return nil, domain.ErrTelegramLoginURLInvalid } @@ -78,7 +78,7 @@ func parseWebURL(raw string, allowLoopbackHTTP bool) (*url.URL, error) { port = "" } case "http": - if !allowLoopbackHTTP || !isLoopbackHost(host) { + if !allowHTTP { return nil, domain.ErrTelegramLoginURLInvalid } if port == "80" { @@ -99,14 +99,6 @@ func parseWebURL(raw string, allowLoopbackHTTP bool) (*url.URL, error) { return u, nil } -func isLoopbackHost(host string) bool { - if host == "localhost" { - return true - } - ip := net.ParseIP(host) - return ip != nil && ip.IsLoopback() -} - func AppendAuthorizationResult(redirectURI, code, state string) (string, error) { u, err := url.Parse(redirectURI) if err != nil || !u.IsAbs() || code == "" { diff --git a/internal/app/telegramlogin/url_test.go b/internal/app/telegramlogin/url_test.go index 17dc4549..49f35aa7 100644 --- a/internal/app/telegramlogin/url_test.go +++ b/internal/app/telegramlogin/url_test.go @@ -18,8 +18,9 @@ func TestNormalizeRedirectURIIsExactAndRejectsOpenRedirectShapes(t *testing.T) { }{ {name: "https canonical", raw: "https://EXAMPLE.com:443/callback?tenant=one", want: "https://example.com/callback?tenant=one", valid: true}, {name: "idna", raw: "https://例子.测试/callback", want: "https://xn--fsqu00a.xn--0zwm56d/callback", valid: true}, - {name: "loopback dev", raw: "http://127.0.0.1:8080/callback", allowHTTP: true, want: "http://127.0.0.1:8080/callback", valid: true}, - {name: "http production", raw: "http://example.com/callback"}, + {name: "http hostname enabled", raw: "http://example.com:8080/callback", allowHTTP: true, want: "http://example.com:8080/callback", valid: true}, + {name: "http ipv4 enabled", raw: "http://192.0.2.25:3000/callback", allowHTTP: true, want: "http://192.0.2.25:3000/callback", valid: true}, + {name: "http disabled", raw: "http://example.com/callback"}, {name: "userinfo", raw: "https://user@example.com/callback"}, {name: "fragment", raw: "https://example.com/callback#token"}, {name: "reserved code", raw: "https://example.com/callback?code=attacker"}, @@ -64,19 +65,19 @@ func TestNormalizeWebOriginRejectsPathAndQuery(t *testing.T) { } } -func TestNormalizeLoopbackIPv6PreservesURLBrackets(t *testing.T) { - origin, err := NormalizeWebOrigin("http://[0:0:0:0:0:0:0:1]:80/", true) +func TestNormalizeHTTPIPv6PreservesURLBrackets(t *testing.T) { + origin, err := NormalizeWebOrigin("http://[2001:db8::25]:80/", true) if err != nil { t.Fatal(err) } - if origin != "http://[0:0:0:0:0:0:0:1]" { + if origin != "http://[2001:db8::25]" { t.Fatalf("origin=%q", origin) } - redirect, domainName, err := NormalizeRedirectURI("http://[::1]/callback", true) + redirect, domainName, err := NormalizeRedirectURI("http://[2001:db8::26]:3000/callback", true) if err != nil { t.Fatal(err) } - if redirect != "http://[::1]/callback" || domainName != "::1" { + if redirect != "http://[2001:db8::26]:3000/callback" || domainName != "2001:db8::26" { t.Fatalf("redirect=%q domain=%q", redirect, domainName) } } diff --git a/internal/botapi/server.go b/internal/botapi/server.go index 333fe288..ff3b3df8 100644 --- a/internal/botapi/server.go +++ b/internal/botapi/server.go @@ -1077,11 +1077,18 @@ func validateWebhookURL(raw string) error { return errors.New("WEBHOOK_URL_INVALID") } u, err := neturl.ParseRequestURI(raw) - if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.Fragment != "" { + if err != nil { return errors.New("WEBHOOK_URL_INVALID") } - if port := u.Port(); port != "" && port != "443" && port != "80" && port != "88" && port != "8443" { - return errors.New("WEBHOOK_PORT_NOT_ALLOWED") + scheme := strings.ToLower(u.Scheme) + if (scheme != "http" && scheme != "https") || u.Hostname() == "" || u.User != nil || u.Fragment != "" { + return errors.New("WEBHOOK_URL_INVALID") + } + if port := u.Port(); port != "" { + n, err := strconv.Atoi(port) + if err != nil || n < 1 || n > 65535 { + return errors.New("WEBHOOK_URL_INVALID") + } } return nil } diff --git a/internal/botapi/server_test.go b/internal/botapi/server_test.go index 1bbc014a..b4b36fdc 100644 --- a/internal/botapi/server_test.go +++ b/internal/botapi/server_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "mime/multipart" "net/http" "net/http/httptest" @@ -977,6 +978,23 @@ func TestSetWebhookPersistsConfigReportsInfoAndConflictsWithPolling(t *testing.T } } +func TestSetWebhookAcceptsHTTPHostIPAndArbitraryPort(t *testing.T) { + bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} + for _, rawURL := range []string{ + "http://bot.example.test:3000/hook", + "http://192.0.2.25:18080/hook", + "http://[2001:db8::25]:28080/hook", + "HTTP://bot.example.test:3100/hook", + } { + gateway := &fakeBotAPIGateway{} + h := (&handler{bots: bots, gateway: gateway}).routes() + rec := performBotAPIRequest(t, h, bots.profile, "setWebhook", fmt.Sprintf(`{"url":%q}`, rawURL)) + if rec.Code != http.StatusOK || !gateway.webhookFound || gateway.webhook.URL != rawURL { + t.Fatalf("setWebhook url=%q status=%d body=%s config=%#v", rawURL, rec.Code, rec.Body.String(), gateway.webhook) + } + } +} + func TestSetWebhookRejectsUnsafeParameters(t *testing.T) { bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} h := (&handler{bots: bots, gateway: &fakeBotAPIGateway{}}).routes() @@ -984,8 +1002,9 @@ func TestSetWebhookRejectsUnsafeParameters(t *testing.T) { body string want string }{ - {`{"url":"http://example.test/hook"}`, "WEBHOOK_URL_INVALID"}, - {`{"url":"https://example.test:444/hook"}`, "WEBHOOK_PORT_NOT_ALLOWED"}, + {`{"url":"ftp://example.test/hook"}`, "WEBHOOK_URL_INVALID"}, + {`{"url":"http://user@example.test/hook"}`, "WEBHOOK_URL_INVALID"}, + {`{"url":"http://example.test:0/hook"}`, "WEBHOOK_URL_INVALID"}, {`{"url":"https://example.test/hook","secret_token":"bad secret"}`, "SECRET_TOKEN_INVALID"}, {`{"url":"https://example.test/hook","max_connections":101}`, "MAX_CONNECTIONS_INVALID"}, } diff --git a/internal/config/config.go b/internal/config/config.go index 3c08d7dd..72fcbd47 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,7 +4,6 @@ package config import ( "bufio" "fmt" - "net" "net/netip" "net/url" "os" @@ -93,9 +92,11 @@ type Config struct { // TelegramLoginEnabled mounts the self-hosted Telegram Login/OIDC provider // on PublicLinkWebAddr. Secrets are file-backed so they are not exposed in // process listings or accidentally copied into tracked .env templates. - TelegramLoginEnabled bool - TelegramLoginIssuer string - TelegramLoginAllowLoopbackHTTP bool + TelegramLoginEnabled bool + TelegramLoginIssuer string + // TelegramLoginAllowHTTP permits HTTP issuers and registered Login URLs on + // any valid host/IP and port. HTTPS remains mandatory when false. + TelegramLoginAllowHTTP bool TelegramLoginSigningKeysFile string TelegramLoginCodeKeysFile string TelegramLoginSecretPepperFile string @@ -491,7 +492,7 @@ func Load() (Config, error) { PublicLinkWebAddr: envAllowEmptyOr("TELESRV_PUBLIC_LINK_WEB_ADDR", ""), TelegramLoginEnabled: envBoolOr("TELESRV_TELEGRAM_LOGIN_ENABLE", false), TelegramLoginIssuer: strings.TrimSuffix(envOr("TELESRV_TELEGRAM_LOGIN_ISSUER", publicBaseURL), "/"), - TelegramLoginAllowLoopbackHTTP: envBoolOr("TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP", false), + TelegramLoginAllowHTTP: envBoolOr("TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP", false), TelegramLoginSigningKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "data/telegram-login/signing-keys.json"), TelegramLoginCodeKeysFile: envOr("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "data/telegram-login/code-keys.json"), TelegramLoginSecretPepperFile: envOr("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "data/telegram-login/client-secret-pepper"), @@ -681,10 +682,8 @@ func validateTelegramLoginConfig(cfg Config) error { switch issuer.Scheme { case "https": case "http": - host := issuer.Hostname() - ip := net.ParseIP(host) - if !cfg.TelegramLoginAllowLoopbackHTTP || (host != "localhost" && (ip == nil || !ip.IsLoopback())) { - return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER http is allowed only for explicit loopback development") + if !cfg.TelegramLoginAllowHTTP { + return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER http requires TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true") } default: return fmt.Errorf("TELESRV_TELEGRAM_LOGIN_ISSUER must use https") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7d9b35cf..0339fb40 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -435,8 +435,8 @@ func TestLoadTelegramLoginConfig(t *testing.T) { disableDefaultConfigFile(t) t.Setenv("TELESRV_PUBLIC_LINK_WEB_ADDR", "127.0.0.1:2401") t.Setenv("TELESRV_TELEGRAM_LOGIN_ENABLE", "true") - t.Setenv("TELESRV_TELEGRAM_LOGIN_ISSUER", "http://127.0.0.1:2401/") - t.Setenv("TELESRV_TELEGRAM_LOGIN_ALLOW_LOOPBACK_HTTP", "true") + t.Setenv("TELESRV_TELEGRAM_LOGIN_ISSUER", "http://192.0.2.25:2401/") + t.Setenv("TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP", "true") t.Setenv("TELESRV_TELEGRAM_LOGIN_SIGNING_KEYS_FILE", "secrets/signing.json") t.Setenv("TELESRV_TELEGRAM_LOGIN_CODE_KEYS_FILE", "secrets/codes.json") t.Setenv("TELESRV_TELEGRAM_LOGIN_SECRET_PEPPER_FILE", "secrets/pepper") @@ -452,8 +452,8 @@ func TestLoadTelegramLoginConfig(t *testing.T) { if err != nil { t.Fatalf("Load: %v", err) } - if !cfg.TelegramLoginEnabled || cfg.TelegramLoginIssuer != "http://127.0.0.1:2401" || !cfg.TelegramLoginAllowLoopbackHTTP { - t.Fatalf("telegram login endpoint config = enabled:%v issuer:%q loopback:%v", cfg.TelegramLoginEnabled, cfg.TelegramLoginIssuer, cfg.TelegramLoginAllowLoopbackHTTP) + if !cfg.TelegramLoginEnabled || cfg.TelegramLoginIssuer != "http://192.0.2.25:2401" || !cfg.TelegramLoginAllowHTTP { + t.Fatalf("telegram login endpoint config = enabled:%v issuer:%q allow_http:%v", cfg.TelegramLoginEnabled, cfg.TelegramLoginIssuer, cfg.TelegramLoginAllowHTTP) } if cfg.TelegramLoginSigningKeysFile != "secrets/signing.json" || cfg.TelegramLoginCodeKeysFile != "secrets/codes.json" || cfg.TelegramLoginSecretPepperFile != "secrets/pepper" { t.Fatalf("telegram login secret files = %q / %q / %q", cfg.TelegramLoginSigningKeysFile, cfg.TelegramLoginCodeKeysFile, cfg.TelegramLoginSecretPepperFile) @@ -484,11 +484,7 @@ func TestValidateTelegramLoginConfigRejectsUnsafeOrUnboundedSettings(t *testing. }{ {name: "missing listener", mutate: func(c *Config) { c.PublicLinkWebAddr = "" }}, {name: "issuer path", mutate: func(c *Config) { c.TelegramLoginIssuer = "https://login.example.test/oauth" }}, - {name: "public http", mutate: func(c *Config) { - c.TelegramLoginIssuer = "http://login.example.test" - c.TelegramLoginAllowLoopbackHTTP = true - }}, - {name: "loopback http disabled", mutate: func(c *Config) { c.TelegramLoginIssuer = "http://127.0.0.1:2401" }}, + {name: "http disabled", mutate: func(c *Config) { c.TelegramLoginIssuer = "http://192.0.2.25:2401" }}, {name: "missing key file", mutate: func(c *Config) { c.TelegramLoginSigningKeysFile = "" }}, {name: "request ttl too long", mutate: func(c *Config) { c.TelegramLoginRequestTTL = 16 * time.Minute }}, {name: "code ttl too short", mutate: func(c *Config) { c.TelegramLoginCodeTTL = 29 * time.Second }}, @@ -508,6 +504,22 @@ func TestValidateTelegramLoginConfigRejectsUnsafeOrUnboundedSettings(t *testing. } } +func TestValidateTelegramLoginConfigAcceptsHTTPHostAndIPWhenEnabled(t *testing.T) { + valid := Config{ + TelegramLoginEnabled: true, TelegramLoginAllowHTTP: true, PublicLinkWebAddr: "127.0.0.1:2401", + TelegramLoginSigningKeysFile: "signing.json", TelegramLoginCodeKeysFile: "codes.json", TelegramLoginSecretPepperFile: "pepper", + TelegramLoginRequestTTL: 5 * time.Minute, TelegramLoginCodeTTL: 2 * time.Minute, TelegramLoginIDTokenTTL: time.Hour, + TelegramLoginRetention: 7 * 24 * time.Hour, TelegramLoginSweepInterval: 5 * time.Minute, TelegramLoginSweepBatch: 500, + } + for _, issuer := range []string{"http://login.example.test:3000", "http://192.0.2.25:2401", "http://[2001:db8::25]:2401"} { + cfg := valid + cfg.TelegramLoginIssuer = issuer + if err := validateTelegramLoginConfig(cfg); err != nil { + t.Fatalf("issuer %q was rejected: %v", issuer, err) + } + } +} + func TestLoadRejectsInvalidPublicBaseURL(t *testing.T) { disableDefaultConfigFile(t) t.Setenv("TELESRV_PUBLIC_BASE_URL", "https://links.example.test/root?tenant=one") diff --git a/internal/domain/message_markup.go b/internal/domain/message_markup.go index 9196d574..da638c88 100644 --- a/internal/domain/message_markup.go +++ b/internal/domain/message_markup.go @@ -2,7 +2,6 @@ package domain import ( "errors" - "net" "net/url" "strings" "unicode/utf8" @@ -397,8 +396,9 @@ func validateButtonURL(raw string) error { // validateLoginButtonURL performs only the protocol-shape validation shared by // Bot API and MTProto input buttons. The Telegram Login service remains the -// authority for the deployment policy: it rejects loopback HTTP unless the -// explicit development switch is enabled and the exact origin is registered. +// authority for the deployment policy: HTTP is accepted here as a protocol +// shape, then allowed only when the Login HTTP switch is enabled and the exact +// origin is registered. func validateLoginButtonURL(raw string) error { raw = strings.TrimSpace(raw) if raw == "" || len(raw) > MaxBotMenuButtonURLLen { @@ -408,15 +408,8 @@ func validateLoginButtonURL(raw string) error { if err != nil || u.Host == "" || u.User != nil { return ErrButtonURLInvalid } - if u.Scheme == "https" { - return nil - } - if u.Scheme != "http" { - return ErrButtonURLInvalid - } - host := strings.ToLower(u.Hostname()) - ip := net.ParseIP(host) - if host != "localhost" && (ip == nil || !ip.IsLoopback()) { + scheme := strings.ToLower(u.Scheme) + if scheme != "http" && scheme != "https" { return ErrButtonURLInvalid } return nil diff --git a/internal/domain/message_markup_test.go b/internal/domain/message_markup_test.go index 8ff348ed..76a27755 100644 --- a/internal/domain/message_markup_test.go +++ b/internal/domain/message_markup_test.go @@ -28,7 +28,8 @@ func TestValidateReplyMarkup(t *testing.T) { {"url empty bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonURL, Text: "go", URL: ""}}}}, ErrButtonURLInvalid}, {"login url loopback http ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://127.0.0.1:8080/login"}}}}, nil}, {"login url localhost http ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://localhost:8080/login"}}}}, nil}, - {"login url public http bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://example.com/login"}}}}, ErrButtonURLInvalid}, + {"login url public http host ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://example.com:3000/login"}}}}, nil}, + {"login url public http ip ok", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "http://192.0.2.25:18080/login"}}}}, nil}, {"login url credentials bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: MarkupButtonLoginURL, Text: "login", URL: "https://user@example.com/login"}}}}, ErrButtonURLInvalid}, {"unknown type bad", &MessageReplyMarkup{Inline: [][]MarkupButton{{{Type: "rainbow", Text: "x"}}}}, ErrButtonTypeInvalid}, {"reply keyboard ok", &MessageReplyMarkup{Type: MessageReplyMarkupKeyboard, Keyboard: [][]MarkupButton{{{Type: MarkupButtonText, Text: "Help"}}}, Resize: true, Persistent: true, Placeholder: "Choose"}, nil}, diff --git a/internal/rpc/telegram_login_rpc_test.go b/internal/rpc/telegram_login_rpc_test.go index 6b22ce70..dc1c31f6 100644 --- a/internal/rpc/telegram_login_rpc_test.go +++ b/internal/rpc/telegram_login_rpc_test.go @@ -261,7 +261,7 @@ func TestTelegramLoginMessageButtonRereadSignsAndGrantsWriteAccess(t *testing.T) pepper[0] = 8 loginStore := memory.NewTelegramLoginStore(telegramLoginBotPermissionAdapter{bots: f.router.deps.Bots}) login, err := telegramloginapp.NewService(loginStore, sealer, telegramloginapp.Config{ - Issuer: "https://oauth.test", AppScheme: "telesrv", ClientSecretPepper: pepper, + Issuer: "http://192.0.2.25:2401", AppScheme: "telesrv", AllowHTTP: true, ClientSecretPepper: pepper, Now: func() time.Time { return time.Unix(1_780_000_000, 0).UTC() }, }) if err != nil { @@ -270,13 +270,13 @@ func TestTelegramLoginMessageButtonRereadSignsAndGrantsWriteAccess(t *testing.T) if _, err := login.CreateClient(f.ctx, f.bot.ID, domain.TelegramLoginSigningRS256); err != nil { t.Fatal(err) } - if _, err := login.AddAllowedURL(f.ctx, f.bot.ID, domain.TelegramLoginAllowedWebOrigin, "https://rp.test"); err != nil { + if _, err := login.AddAllowedURL(f.ctx, f.bot.ID, domain.TelegramLoginAllowedWebOrigin, "http://rp.test:3000"); err != nil { t.Fatal(err) } f.router.deps.TelegramLogin = login markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: [][]domain.MarkupButton{{{ - Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "https://rp.test/login?next=%2Fhome", RequestWriteAccess: true, + Type: domain.MarkupButtonLoginURL, Text: "Log in", URL: "http://rp.test:3000/login?next=%2Fhome", RequestWriteAccess: true, }}}} if _, err := f.router.BotAPISendMessage(f.ctx, f.bot.ID, f.owner.ID, "Authorize", nil, markup, false, false, 0); err != nil { t.Fatalf("BotAPISendMessage: %v", err) diff --git a/internal/telegramloginhttp/handler.go b/internal/telegramloginhttp/handler.go index b977b76d..feda7894 100644 --- a/internal/telegramloginhttp/handler.go +++ b/internal/telegramloginhttp/handler.go @@ -40,18 +40,18 @@ type Config struct { AppName string Logger *zap.Logger TrustedProxyCIDRs []string - AllowLoopbackHTTP bool + AllowHTTP bool } type Handler struct { - service *loginapp.Service - tokens *loginapp.IDTokenIssuer - appName string - logger *zap.Logger - limiter RateLimiter - trustedProxies []netip.Prefix - allowLoopbackHTTP bool - mux *http.ServeMux + service *loginapp.Service + tokens *loginapp.IDTokenIssuer + appName string + logger *zap.Logger + limiter RateLimiter + trustedProxies []netip.Prefix + allowHTTP bool + mux *http.ServeMux } type RateLimiter interface { @@ -76,7 +76,7 @@ func NewHandler(cfg Config) (*Handler, error) { } trustedProxies = append(trustedProxies, prefix.Masked()) } - h := &Handler{service: cfg.Service, tokens: cfg.Tokens, appName: strings.TrimSpace(cfg.AppName), logger: cfg.Logger, limiter: cfg.Limiter, trustedProxies: trustedProxies, allowLoopbackHTTP: cfg.AllowLoopbackHTTP} + h := &Handler{service: cfg.Service, tokens: cfg.Tokens, appName: strings.TrimSpace(cfg.AppName), logger: cfg.Logger, limiter: cfg.Limiter, trustedProxies: trustedProxies, allowHTTP: cfg.AllowHTTP} mux := http.NewServeMux() mux.HandleFunc("GET /.well-known/openid-configuration", h.discovery) mux.HandleFunc("GET /.well-known/jwks.json", h.jwks) @@ -333,7 +333,7 @@ func (h *Handler) inApp(w http.ResponseWriter, r *http.Request) { writeOAuthError(w, http.StatusBadRequest, "unsupported_response_type", "only id_token is supported") return } - origin, err := loginapp.NormalizeWebOrigin(values["origin"], h.allowLoopbackHTTP) + origin, err := loginapp.NormalizeWebOrigin(values["origin"], h.allowHTTP) if err != nil || r.Header.Get("Origin") != origin { writeOAuthError(w, http.StatusBadRequest, "invalid_request", "in-app origin is invalid") return @@ -511,7 +511,7 @@ func (h *Handler) authorizeStatusOrigin(w http.ResponseWriter, r *http.Request, if origin == "" { return true } - issuerOrigin, err := loginapp.NormalizeWebOrigin(h.tokens.Issuer(), h.allowLoopbackHTTP) + issuerOrigin, err := loginapp.NormalizeWebOrigin(h.tokens.Issuer(), h.allowHTTP) if err == nil && origin == issuerOrigin { return true } diff --git a/internal/telegramloginhttp/handler_test.go b/internal/telegramloginhttp/handler_test.go index 2563de45..9b7abb16 100644 --- a/internal/telegramloginhttp/handler_test.go +++ b/internal/telegramloginhttp/handler_test.go @@ -109,7 +109,7 @@ func newTelegramLoginHTTPFixture(t *testing.T) telegramLoginHTTPFixture { if err != nil { t.Fatal(err) } - handler, err := NewHandler(Config{Service: service, Tokens: tokens, AppName: "Telesrv", AllowLoopbackHTTP: true}) + handler, err := NewHandler(Config{Service: service, Tokens: tokens, AppName: "Telesrv", AllowHTTP: true}) if err != nil { t.Fatal(err) } From d9875b5caa48047dbfed1513ac00dc87a3044a15 Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 00:51:52 +0800 Subject: [PATCH 11/28] feat: sync bot setup and webhook diagnostics --- cmd/bots/bedolagaformat/README.md | 6 +- .../0130_botfather_done_command.down.sql | 13 ++ .../0130_botfather_done_command.up.sql | 22 +++ docs/configuration.en.md | 131 ++++++++++++++++- docs/configuration.zh-CN.md | 121 +++++++++++++++- internal/app/bots/botfather.go | 133 +++++++++++++++++- internal/app/bots/botfather_login_test.go | 93 ++++++++++-- internal/botapi/webhook.go | 2 +- internal/botapi/webhook_test.go | 18 ++- internal/store/memory/bot.go | 1 + .../store/postgres/bot_integration_test.go | 10 ++ 11 files changed, 521 insertions(+), 29 deletions(-) create mode 100644 deploy/migrations/0130_botfather_done_command.down.sql create mode 100644 deploy/migrations/0130_botfather_done_command.up.sql diff --git a/cmd/bots/bedolagaformat/README.md b/cmd/bots/bedolagaformat/README.md index ef46c504..55432536 100644 --- a/cmd/bots/bedolagaformat/README.md +++ b/cmd/bots/bedolagaformat/README.md @@ -78,8 +78,8 @@ $env:TELESRV_BOT_API_SERVER = "http://127.0.0.1:8081" 3. 服务端 Authorization Code + PKCE S256 → `/token` Basic Client Secret → JWKS 验签和 `issuer/audience/nonce/subject` 复核。 -先在 telesrv 的 @BotFather 中对目标 bot 运行 `/setlogin`。选择 bot 后逐条登记 demo -的精确 origin 和 callback(本机示例): +先在 telesrv 的 @BotFather 中对目标 bot 运行 `/setlogin`。选择一次 bot 后,可逐条发送, +也可把下面三行作为一条多行消息粘贴,无需每次重新运行 `/setlogin` 或重选 bot: ```text add origin http://127.0.0.1:3000 @@ -87,6 +87,8 @@ add redirect http://127.0.0.1:3000/oauth/callback enable ``` +发送 `/done` 退出配置会话并查看最终摘要。`/cancel` 只退出,不回滚已经成功应用的命令。 + `/setlogin` 首次创建 client 时只展示一次 OIDC Client Secret;不要写进仓库。可用 `/logininfo` 查看 Client ID 和登记结果,或用 `/resetloginsecret` 轮换 secret。 使用 HTTP 域名/IP 时,在 telesrv 配置 `TELESRV_TELEGRAM_LOGIN_ALLOW_HTTP=true`; diff --git a/deploy/migrations/0130_botfather_done_command.down.sql b/deploy/migrations/0130_botfather_done_command.down.sql new file mode 100644 index 00000000..0b672f17 --- /dev/null +++ b/deploy/migrations/0130_botfather_done_command.down.sql @@ -0,0 +1,13 @@ +UPDATE public.bots +SET commands = COALESCE(( + SELECT jsonb_agg(command ORDER BY ordinal) + FROM jsonb_array_elements(commands) WITH ORDINALITY AS item(command, ordinal) + WHERE command->>'command' <> 'done' + ), '[]'::jsonb), + updated_at = now() +WHERE bot_user_id = 93372553; + +UPDATE public.users +SET bot_info_version = bot_info_version + 1, + updated_at = now() +WHERE id = 93372553; diff --git a/deploy/migrations/0130_botfather_done_command.up.sql b/deploy/migrations/0130_botfather_done_command.up.sql new file mode 100644 index 00000000..73d562e8 --- /dev/null +++ b/deploy/migrations/0130_botfather_done_command.up.sql @@ -0,0 +1,22 @@ +-- /setlogin remains active across multiple configuration messages. Publish +-- /done in BotFather's command menu so clients can discover the explicit +-- finish action without reopening /help. +UPDATE public.bots +SET commands = commands || '[ + {"command":"done","description":"finish Telegram Login configuration"} + ]'::jsonb, + updated_at = now() +WHERE bot_user_id = 93372553 + AND NOT EXISTS ( + SELECT 1 + FROM jsonb_array_elements(commands) AS item(command) + WHERE item.command->>'command' = 'done' + ); + +-- Bot command menus are cached by bot_info_version. Bump it even when an +-- operator already added /done manually, making the migration convergent and +-- forcing connected clients to refresh the authoritative command list. +UPDATE public.users +SET bot_info_version = bot_info_version + 1, + updated_at = now() +WHERE id = 93372553; diff --git a/docs/configuration.en.md b/docs/configuration.en.md index aaa23531..71de0b4b 100644 --- a/docs/configuration.en.md +++ b/docs/configuration.en.md @@ -77,7 +77,124 @@ This document describes every setting loaded by `internal/config`. Defaults and | `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | Retention worker interval; bounded to `10s..1h`. | | `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | Maximum rows per retention pass; bounded to `1..1000`. | -### 3.1 Complete Telegram Login / OIDC setup +### 3.1 Bot API webhook troubleshooting + +Start by separating the three addresses below. Never use the webhook receiver domain as the Bot +API endpoint unless an explicit reverse-proxy route maps that domain to telesrv: + +| Name | Setting/source | Direction and purpose | +|---|---|---| +| Bot API listener | telesrv `TELESRV_BOT_API_ADDR` | The telesrv bind address; empty disables the gateway. `0.0.0.0` is valid only for binding and is not a client request target. | +| Bot API base URL | the bot application's `TELEGRAM_API_URL` or equivalent | A client-reachable address for telesrv, for example `http://172.17.0.1:8088`. Method URLs are `/bot/` and file URLs are `/file/bot/`. | +| Webhook receiver URL | the bot application's `WEBHOOK_URL + WEBHOOK_PATH`, registered by `setWebhook` | The target to which telesrv actively POSTs updates, for example `https://bot.example.com/webhook`. It is not the Bot API base URL. | + +The network direction is different too: polling is `bot application -> telesrv Bot API`, while +webhook delivery is `telesrv -> bot application webhook receiver`. Working polling proves only the +first path. It does not prove webhook DNS, outbound TCP, TLS, reverse proxy, or Docker hairpin +connectivity. + +#### 1. Query the authoritative webhook state from the Bot API + +Run this inside the bot application container with its actual Bot API base URL. Do not expand and +paste the token into chat, tickets, or screenshots: + +```sh +curl -sS -X POST \ + "${TELEGRAM_API_URL%/}/bot${BOT_TOKEN}/getWebhookInfo" | jq +``` + +If the application uses a differently named variable, replace `TELEGRAM_API_URL` with the +**client-reachable address** corresponding to `TELESRV_BOT_API_ADDR`. For example, if telesrv binds +`0.0.0.0:8088`, a container on the same host might use `http://172.17.0.1:8088`; it must not request +`http://0.0.0.0:8088`. + +Interpret the result as follows: + +| Result | Conclusion and next step | +|---|---| +| Empty `url` | No webhook is registered on this telesrv instance. Verify that the application uses this Bot API base URL and that startup `setWebhook` succeeded. | +| Increasing `pending_update_count` | Updates reached the telesrv durable queue but are not being delivered successfully. Inspect `last_error_message`. | +| HTTP `401`/`403` in `last_error_message` | The receiver is reachable, but its webhook secret differs or an authentication layer rejected the request. | +| `dial tcp ... i/o timeout` | telesrv cannot connect to the target IP/port. Check outbound firewall rules, Docker networking, loopback/hairpin NAT, and security groups. | +| `connection refused` | The address is reachable, but nothing listens on that port or the port mapping/reverse-proxy upstream is wrong. | +| DNS/`no such host` | The webhook hostname cannot be resolved from the telesrv runtime environment. | +| TLS/`x509` error | The certificate chain, hostname, SNI, or container CA trust is wrong. HTTPS uses the system trust store. | +| Target type absent from `allowed_updates` | Newly produced updates of that type are not queued. A normal `/start` requires at least `message`. | +| Pending reaches zero but the app does not react | telesrv received a 2xx response. Inspect the receiver's internal queue, workers, dispatcher, and handlers. | + +`getWebhookInfo` reports telesrv's persisted delivery facts. An application `/health` endpoint only +proves that its receiver route and workers started; it cannot replace this check. + +#### 2. Validate the receiver with the correct header + +The Telegram webhook secret is distinct from the Bot token, OIDC Client Secret, and other API +keys. The receiver validates `X-Telegram-Bot-Api-Secret-Token`, not `Authorization: Bearer`: + +```sh +curl -i -X POST "${WEBHOOK_URL%/}${WEBHOOK_PATH}" \ + -H 'Content-Type: application/json' \ + -H "X-Telegram-Bot-Api-Secret-Token: ${WEBHOOK_SECRET_TOKEN}" \ + -d '{"update_id":2147483000}' +``` + +Expect an HTTP 2xx response. `401 invalid_secret_token` proves that the request reached the +application but the header was absent or did not match. Recreate/restart the application after +editing `.env`; changing the file alone neither updates the secret already registered in telesrv +nor the receiver process's startup-time secret. + +#### 3. Test from the actual telesrv network namespace + +A browser or official Telegram reaching the public webhook proves only public inbound +connectivity. Repeat the test from the host, container, or network namespace that actually runs +telesrv: + +```sh +docker exec sh -lc \ + 'getent hosts bot.example.com; curl -vk --connect-timeout 10 https://bot.example.com/health/unified' +``` + +If public clients work but this returns `dial tcp ...:443: i/o timeout`, a same-host public-IP +hairpin failure is a common cause. Prefer split DNS or a container host mapping so the public +hostname resolves to the reverse proxy's internal entry point inside the telesrv container while +preserving the hostname, HTTPS SNI, and certificate validation. If the reverse proxy publishes +443 on the Docker host, test first with: + +```sh +curl -vk --resolve bot.example.com:443:172.17.0.1 \ + https://bot.example.com/health/unified +``` + +After that succeeds, a deployment may use a network-appropriate Compose entry such as: + +```yaml +extra_hosts: + - "bot.example.com:host-gateway" +``` + +Other fixes include attaching telesrv to the reverse proxy's Docker network, allowing the Docker +subnet to reach host port 443, or correcting cloud security-group/NAT hairpin rules. telesrv allows +an internal HTTP receiver, but use one only on a controlled shared network and only when the +application's `WEBHOOK_URL` is not also its public OIDC, payment, or media callback base. Do not +blindly replace a global public URL with an internal address to mask a routing problem. + +#### 4. Close the loop after the fix + +1. Restart the bot application so it calls `setWebhook` again with the current URL, secret, and + `allowed_updates`. +2. Send a new `/start` or press a callback button. +3. Call `getWebhookInfo` again. `pending_update_count` should fall to `0`, with no new + `last_error_date`. +4. Inspect telesrv Warning logs for `bot api webhook delivery failed`. The record contains + `bot_user_id`, `retry_in`, and the failure reason, but must not contain the webhook URL, Bot + token, or secret. +5. Confirm that the receiver recorded and processed the `update_id`. Delivery is at-least-once, so + the application must safely handle duplicate updates caused by retries. + +Immediately rotate any Bot token, webhook secret, OIDC Client Secret, API key, or database +password exposed in shell history, chat, or screenshots. Keep only redacted diagnostics in support +material. + +### 3.2 Complete Telegram Login / OIDC setup #### 1. Generate `data/telegram-login` once @@ -154,8 +271,10 @@ and choose that bot. Initial setup returns: - `Client Secret`: shown once, separate from the Bot API token, and meant to be saved immediately in a secret manager. -Send each configuration command separately. This example runs the relying party at -`http://192.0.2.30:3000`: +After selecting a bot once, BotFather keeps that configuration session active; there is no need to +repeat `/setlogin` and the bot username for every change. Send commands one at a time or paste them +as separate lines in one message (up to 32 lines per message). This example runs the relying party +at `http://192.0.2.30:3000`: ```text add origin http://192.0.2.30:3000 @@ -164,6 +283,12 @@ algorithm RS256 enable ``` +Send `/done` after the changes succeed. BotFather closes the session and returns the final +configuration summary. Every successful change takes effect immediately, so `/cancel` only closes +the session and does not roll back changes. If a multi-line message fails partway through, +BotFather identifies the applied lines, the failed line, and the later lines that were skipped, +then keeps the selected bot active for a corrected command. + An `origin` is an exact Web origin without a path, query, or fragment; it authorizes the JS SDK, popup CORS, and legacy `login_url`. A `redirect` is the exact full URI that receives an Authorization Code. Wildcards and prefix matching are not supported. Use `/logininfo` to inspect diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md index 4c2e6af2..69433930 100644 --- a/docs/configuration.zh-CN.md +++ b/docs/configuration.zh-CN.md @@ -77,7 +77,117 @@ | `TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL` | duration / `5m` | retention worker 周期,限定 `10s..1h`。 | | `TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH` | int / `500` | 每轮最大清理行数,限定 `1..1000`。 | -### 3.1 Telegram Login / OIDC 完整启用流程 +### 3.1 Bot API webhook 故障排查 + +先区分三个地址,禁止把 webhook 接收域名当成 Bot API 地址: + +| 名称 | 配置/来源 | 方向与用途 | +|---|---|---| +| Bot API listener | telesrv 的 `TELESRV_BOT_API_ADDR` | telesrv 的监听地址;空值表示关闭。`0.0.0.0` 只能用于 bind,不能作为客户端请求目标。 | +| Bot API base URL | bot 应用的 `TELEGRAM_API_URL` 等配置 | bot 应用访问 telesrv 的可达地址,例如 `http://172.17.0.1:8088`。方法地址为 `/bot/`,文件地址为 `/file/bot/`。 | +| Webhook receiver URL | bot 应用的 `WEBHOOK_URL + WEBHOOK_PATH`,经 `setWebhook` 登记 | telesrv 主动 POST update 的目标,例如 `https://bot.example.com/webhook`。它不是 Bot API base URL。 | + +网络方向也不同:polling 是 `bot 应用 -> telesrv Bot API`,webhook 是 +`telesrv -> bot 应用 webhook receiver`。因此 polling 正常只能证明前一条路径可达, +不能证明 webhook 的 DNS、出站 TCP、TLS、反向代理或 Docker hairpin 路径正常。 + +#### 1. 从 Bot API 查询真实 webhook 状态 + +应在 bot 应用容器中使用它实际配置的 Bot API base URL;不要把 token 展开后粘贴到 +聊天、工单或截图: + +```sh +curl -sS -X POST \ + "${TELEGRAM_API_URL%/}/bot${BOT_TOKEN}/getWebhookInfo" | jq +``` + +若没有 `TELEGRAM_API_URL` 这个变量,就把它替换成与 +`TELESRV_BOT_API_ADDR` 对应的**客户端可达地址**。例如 telesrv 监听 +`0.0.0.0:8088`,同宿主 Docker 容器可能使用 `http://172.17.0.1:8088`;不要请求 +`http://0.0.0.0:8088`。 + +按下表判读响应: + +| 结果 | 结论与下一步 | +|---|---| +| `url` 为空 | webhook 没有登记到这台 telesrv;检查 bot 应用是否确实使用该 Bot API base URL,以及启动时 `setWebhook` 是否成功。 | +| `pending_update_count` 增长 | update 已进入 telesrv durable queue,但没有成功交付;继续看 `last_error_message`。 | +| `last_error_message` 为 HTTP `401`/`403` | 接收端已可达,但 webhook secret 不一致或请求被认证层拒绝。 | +| `dial tcp ... i/o timeout` | telesrv 到目标 IP/端口的连接超时;检查出站防火墙、Docker 网络、回环 NAT/hairpin 和安全组。 | +| `connection refused` | 目标地址可达,但相应端口没有监听或端口映射/反代 upstream 错误。 | +| DNS/`no such host` | telesrv 所在运行环境无法解析 webhook hostname。 | +| TLS/`x509` 错误 | 证书链、hostname、SNI 或容器 CA trust 有问题。HTTPS 使用系统信任链。 | +| `allowed_updates` 不含目标类型 | 新产生的该类型 update 不会入队;普通 `/start` 至少需要 `message`。 | +| pending 归零但应用无响应 | telesrv 已收到 2xx;转查接收应用内部 queue、worker、dispatcher 和 handler 日志。 | + +`getWebhookInfo` 查询的是 telesrv 持久化的交付事实;应用自己的 `/health` 只能证明 +接收路由和 worker 已启动,不能代替这一步。 + +#### 2. 用正确请求头验证接收端 + +Telegram webhook secret 与 Bot token、OIDC Client Secret、API key 都是不同凭据。 +接收端校验的标准请求头是 `X-Telegram-Bot-Api-Secret-Token`,不是 +`Authorization: Bearer`: + +```sh +curl -i -X POST "${WEBHOOK_URL%/}${WEBHOOK_PATH}" \ + -H 'Content-Type: application/json' \ + -H "X-Telegram-Bot-Api-Secret-Token: ${WEBHOOK_SECRET_TOKEN}" \ + -d '{"update_id":2147483000}' +``` + +预期为 HTTP 2xx。`401 invalid_secret_token` 表示请求已经到达应用,但 header 缺失或 +值不匹配。编辑 `.env` 后必须重建/重启读取该配置的应用;只修改磁盘文件不会更新 +已经登记到 telesrv 的 secret,也不会更新接收进程启动时捕获的 secret。 + +#### 3. 从 telesrv 的实际网络命名空间测试 + +浏览器或官方 Telegram 能访问公网 webhook,只能证明公网入站正常。必须从实际运行 +telesrv 的宿主机、容器或 network namespace 再测一次: + +```sh +docker exec sh -lc \ + 'getent hosts bot.example.com; curl -vk --connect-timeout 10 https://bot.example.com/health/unified' +``` + +如果公网客户端正常而这里 `dial tcp ...:443: i/o timeout`,常见原因是同机公网 IP +回环失败。优先使用 split DNS 或容器 host mapping,让公网 hostname 在 telesrv 容器 +内解析到反向代理的内部入口,同时保留原 hostname、HTTPS SNI 和证书校验。例如反代 +的 443 已发布到 Docker 宿主机时,可先验证: + +```sh +curl -vk --resolve bot.example.com:443:172.17.0.1 \ + https://bot.example.com/health/unified +``` + +验证通过后,可在 telesrv Compose 中使用与实际网络匹配的配置: + +```yaml +extra_hosts: + - "bot.example.com:host-gateway" +``` + +其它可选修复包括:把 telesrv 接入反向代理所在 Docker network、为 Docker subnet +放行宿主机 443,或修正云安全组/NAT hairpin。telesrv 允许登记内部 HTTP receiver, +但只有在两端共享受控内网且调用方的 `WEBHOOK_URL` 不同时承担 OIDC、支付或公开媒体 +回调时才应使用;不要为绕过网络问题盲目把应用的全局公开 URL 改成内部地址。 + +#### 4. 修复后的闭环验证 + +1. 重新启动 bot 应用,让它用当前 URL、secret 和 `allowed_updates` 再次调用 + `setWebhook`。 +2. 发送一条新的 `/start` 或点击 callback 按钮。 +3. 再次调用 `getWebhookInfo`;`pending_update_count` 应下降到 `0`,且不再出现新的 + `last_error_date`。 +4. 检查 telesrv Warning 日志中的 `bot api webhook delivery failed`。日志包含 + `bot_user_id`、`retry_in` 和失败原因,但不得记录 webhook URL、Bot token 或 secret。 +5. 检查接收应用是否记录并处理该 `update_id`。webhook 是 at-least-once,应用必须能 + 安全处理失败重试带来的重复 update。 + +若凭据曾出现在命令历史、聊天或截图中,立即轮换 Bot token、webhook secret、OIDC +Client Secret 及同屏暴露的其它 API key/数据库密码;排查资料只保留脱敏结果。 + +### 3.2 Telegram Login / OIDC 完整启用流程 #### 1. 一次性生成 `data/telegram-login` @@ -151,7 +261,9 @@ discovery 返回的 `issuer` 必须等于配置值,`authorization_endpoint`、 - `Client ID`:bot user ID 的十进制字符串; - `Client Secret`:只显示一次,与 Bot API token 不同,必须立即保存到密钥管理系统。 -接着逐条发送配置命令。下面假设依赖方页面运行在 `http://192.0.2.30:3000`: +选择一次 bot 后会持续停留在它的配置会话中,无需为每项修改重复 `/setlogin` 和 bot +username。可以逐条发送,也可以像下面这样在一条消息中粘贴多行命令(每条消息最多 +32 行)。下面假设依赖方页面运行在 `http://192.0.2.30:3000`: ```text add origin http://192.0.2.30:3000 @@ -160,6 +272,11 @@ algorithm RS256 enable ``` +全部修改成功后发送 `/done`,BotFather 会退出配置会话并返回最终配置摘要。各条修改会 +立即生效;`/cancel` 只关闭当前会话,不会回滚已经成功的修改。多行消息若中途失败, +BotFather 会明确列出已应用项、失败行以及未执行的后续行,并保留当前 bot 选择供修正 +后继续操作。 + `origin` 只能是无 path/query/fragment 的精确 Web origin,用于 JS SDK、popup CORS 和 legacy `login_url`;`redirect` 是 Authorization Code Flow 返回 code 的精确完整 URI。 不支持 wildcard 或 prefix 匹配。用 `/logininfo` 检查状态和登记值;用 `/setlogin` diff --git a/internal/app/bots/botfather.go b/internal/app/bots/botfather.go index d37d492f..26b1ab5b 100644 --- a/internal/app/bots/botfather.go +++ b/internal/app/bots/botfather.go @@ -37,6 +37,7 @@ const ( botFatherCmdSetLogin = "setlogin" botFatherCmdLoginInfo = "logininfo" botFatherCmdResetLogin = "resetloginsecret" + botFatherCmdDone = "done" botFatherStepName = "name" botFatherStepUsername = "username" @@ -45,6 +46,8 @@ const ( botFatherDraftBotID = "bot_id" botFatherDraftBotUsername = "bot_username" + + maxTelegramLoginCommandsPerMessage = 32 ) const botFatherHelpText = `I can help you create and manage ` + branding.ProductName + ` bots. @@ -67,6 +70,7 @@ You can control me by sending these commands: /setlogin - configure Telegram Login allowed URLs and signing /logininfo - show a bot's Telegram Login configuration /resetloginsecret - rotate a bot's OIDC Client Secret +/done - finish the active Telegram Login configuration /cancel - cancel the current operation /help - show this message` @@ -176,7 +180,7 @@ func (s *Service) botReplyRandomID() int64 { // 必须作为原始内容透传给状态机,否则 /setcommands 的 /empty 永不可达、且首行 // 带斜杠的命令列表会被截成命令名 "start" 静默销毁整个流程。 var botFatherGlobalCommands = map[string]bool{ - "start": true, "help": true, "cancel": true, + "start": true, "help": true, "cancel": true, botFatherCmdDone: true, botFatherCmdNewBot: true, "mybots": true, botFatherCmdToken: true, botFatherCmdRevoke: true, botFatherCmdSetName: true, botFatherCmdSetDescription: true, botFatherCmdSetAbout: true, @@ -288,7 +292,7 @@ func (s *Service) handleBotFatherCommand(ctx context.Context, userID int64, cmd _ = s.bots.DeleteBotChatState(ctx, domain.BotFatherUserID, userID) return botReply{Text: botFatherHelpText} case "cancel": - _, found, err := s.bots.GetBotChatState(ctx, domain.BotFatherUserID, userID) + state, found, err := s.bots.GetBotChatState(ctx, domain.BotFatherUserID, userID) if err != nil { s.log.Error("botfather: get chat state", zap.Int64("user_id", userID), zap.Error(err)) return internalReply() @@ -300,7 +304,12 @@ func (s *Service) handleBotFatherCommand(ctx context.Context, userID int64, cmd s.log.Error("botfather: delete chat state", zap.Int64("user_id", userID), zap.Error(err)) return internalReply() } + if state.Command == botFatherCmdSetLogin && state.Step == botFatherStepValue { + return botReply{Text: "Telegram Login configuration closed. Changes that were already applied have been kept."} + } return botReply{Text: "The command has been cancelled. Anything else I can do for you? Send /help for a list of commands."} + case botFatherCmdDone: + return s.finishTelegramLoginConfiguration(ctx, userID) case botFatherCmdNewBot: count, err := s.bots.CountBotsByOwner(ctx, userID) if err != nil { @@ -577,7 +586,7 @@ func (s *Service) handleSetValue(ctx context.Context, state domain.BotChatState, case botFatherCmdSetPrivacy: reply, err = s.applyToggle(ctx, botID, text, false) case botFatherCmdSetLogin: - reply, err = s.applyTelegramLoginConfiguration(ctx, botID, username, text) + return s.handleTelegramLoginConfigurationInput(ctx, state, botID, username, text) default: s.clearState(ctx, state.UserID) return internalReply() @@ -655,7 +664,7 @@ func (s *Service) applySetInlineGeo(ctx context.Context, botID int64, text strin } func telegramLoginConfigurationPrompt(username string) string { - return fmt.Sprintf(`Send one configuration command for @%s: + return fmt.Sprintf(`Configure Telegram Login for @%s. Send commands one at a time or paste up to %d commands on separate lines: add origin https://example.com add redirect https://example.com/auth/callback @@ -668,7 +677,117 @@ algorithm RS256|ES256|EdDSA|ES256K enable disable -Origins authorize the JS SDK and legacy login_url buttons. Redirects are exact OIDC callbacks. Run /logininfo to inspect the result or /cancel to stop.`, username) +Origins authorize the JS SDK and legacy login_url buttons. Redirects are exact OIDC callbacks. Changes apply immediately. Send /done to finish, or /cancel to close this session without undoing changes already applied.`, username, maxTelegramLoginCommandsPerMessage) +} + +func telegramLoginConfigurationContinuePrompt(username string) string { + return fmt.Sprintf("Still configuring @%s. Send another command, paste multiple commands on separate lines, or send /done to finish.", username) +} + +func (s *Service) finishTelegramLoginConfiguration(ctx context.Context, userID int64) botReply { + state, found, err := s.bots.GetBotChatState(ctx, domain.BotFatherUserID, userID) + if err != nil { + s.log.Error("botfather: get telegram login state", zap.Int64("user_id", userID), zap.Error(err)) + return internalReply() + } + if !found || state.Command != botFatherCmdSetLogin || state.Step != botFatherStepValue { + return botReply{Text: "There is no active Telegram Login configuration to finish. Send /setlogin to start one."} + } + botID, _ := strconv.ParseInt(state.Draft[botFatherDraftBotID], 10, 64) + username := state.Draft[botFatherDraftBotUsername] + if botID == 0 || username == "" { + s.clearState(ctx, userID) + return botReply{Text: "Something went wrong, I forgot which bot we were editing. Send /setlogin to start again."} + } + owns, err := s.OwnsBot(ctx, userID, botID) + if err != nil { + s.log.Error("botfather: verify telegram login owner", zap.Int64("user_id", userID), zap.Int64("bot_user_id", botID), zap.Error(err)) + return internalReply() + } + if !owns { + s.clearState(ctx, userID) + return botReply{Text: "That bot is no longer available."} + } + if s.telegramLogin == nil { + s.clearState(ctx, userID) + return botReply{Text: "Telegram Login is not enabled on this server."} + } + configuration, configured, err := s.telegramLogin.ClientConfiguration(ctx, botID) + if err != nil { + s.log.Error("botfather: get telegram login configuration", zap.Int64("bot_user_id", botID), zap.Error(err)) + return internalReply() + } + if !configured { + s.clearState(ctx, userID) + return botReply{Text: fmt.Sprintf("Telegram Login is not configured for @%s. Send /setlogin to create it.", username)} + } + if err := s.bots.DeleteBotChatState(ctx, domain.BotFatherUserID, userID); err != nil { + s.log.Error("botfather: finish telegram login state", zap.Int64("user_id", userID), zap.Error(err)) + return internalReply() + } + return botReply{Text: fmt.Sprintf("Finished configuring Telegram Login for @%s.\n\n%s", username, formatTelegramLoginConfiguration(username, configuration))} +} + +func (s *Service) handleTelegramLoginConfigurationInput( + ctx context.Context, + state domain.BotChatState, + botID int64, + username string, + text string, +) botReply { + if strings.EqualFold(strings.TrimSpace(text), "done") { + return s.finishTelegramLoginConfiguration(ctx, state.UserID) + } + lines := make([]string, 0, 4) + for _, raw := range strings.Split(text, "\n") { + if line := strings.TrimSpace(raw); line != "" { + lines = append(lines, line) + } + } + if len(lines) == 0 { + return botReply{Text: "Send a Telegram Login configuration command.\n\n" + telegramLoginConfigurationContinuePrompt(username)} + } + if len(lines) > maxTelegramLoginCommandsPerMessage { + return botReply{Text: fmt.Sprintf("Too many commands in one message. Send at most %d lines at a time.\n\n%s", maxTelegramLoginCommandsPerMessage, telegramLoginConfigurationContinuePrompt(username))} + } + + applied := make([]string, 0, len(lines)) + for i, line := range lines { + reply, err := s.applyTelegramLoginConfiguration(ctx, botID, username, line) + if err != nil { + if len(lines) == 1 { + if reply.Text == "" { + return internalReply() + } + return botReply{Text: reply.Text + "\n\n" + telegramLoginConfigurationContinuePrompt(username)} + } + failure := reply.Text + if failure == "" { + failure = "Something went wrong on my side. Please try that line again later." + } + var out strings.Builder + if len(applied) > 0 { + fmt.Fprintf(&out, "Applied %d command(s) before the error:\n%s\n\n", len(applied), strings.Join(applied, "\n")) + } + fmt.Fprintf(&out, "Stopped at line %d:\n%s\n\n", i+1, failure) + if i+1 < len(lines) { + fmt.Fprintf(&out, "%d later command(s) were not applied.\n\n", len(lines)-i-1) + } + out.WriteString(telegramLoginConfigurationContinuePrompt(username)) + return botReply{Text: out.String()} + } + applied = append(applied, fmt.Sprintf("Line %d: %s", i+1, reply.Text)) + } + + var out strings.Builder + if len(lines) == 1 { + out.WriteString(strings.TrimPrefix(applied[0], "Line 1: ")) + } else { + fmt.Fprintf(&out, "Applied all %d commands:\n%s", len(applied), strings.Join(applied, "\n")) + } + out.WriteString("\n\n") + out.WriteString(telegramLoginConfigurationContinuePrompt(username)) + return botReply{Text: out.String()} } func formatTelegramLoginConfiguration(username string, configuration telegramloginapp.ClientConfiguration) string { @@ -733,7 +852,7 @@ func (s *Service) applyTelegramLoginConfiguration(ctx context.Context, botID int if err := s.telegramLogin.SetClientEnabled(ctx, botID, true); err != nil { return botReply{}, err } - return botReply{Text: fmt.Sprintf("Telegram Login is enabled for @%s. Use /setlogin for another change or /logininfo to review it.", username)}, nil + return botReply{Text: fmt.Sprintf("Telegram Login is enabled for @%s.", username)}, nil case "disable": if err := s.telegramLogin.SetClientEnabled(ctx, botID, false); err != nil { return botReply{}, err @@ -763,7 +882,7 @@ func (s *Service) applyTelegramLoginConfiguration(ctx context.Context, botID int if strings.EqualFold(fields[0], "add") { allowed, err := s.telegramLogin.AddAllowedURL(ctx, botID, kind, fields[2]) if err != nil { - return botReply{Text: "That URL is not allowed. Use an exact HTTPS URL without credentials, fragments or reserved OAuth query fields."}, err + return botReply{Text: "That URL is not allowed. Use an exact HTTP(S) URL permitted by this server without credentials, fragments or reserved OAuth query fields."}, err } return botReply{Text: fmt.Sprintf("Success! Added %s for @%s:\n%s", allowed.Kind, username, allowed.NormalizedURL)}, nil } diff --git a/internal/app/bots/botfather_login_test.go b/internal/app/bots/botfather_login_test.go index f48543fe..72360081 100644 --- a/internal/app/bots/botfather_login_test.go +++ b/internal/app/bots/botfather_login_test.go @@ -8,6 +8,7 @@ import ( "time" telegramloginapp "telesrv/internal/app/telegramlogin" + "telesrv/internal/domain" "telesrv/internal/store/memory" ) @@ -32,7 +33,7 @@ func newBotFatherTelegramLoginService(t *testing.T) *telegramloginapp.Service { } func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) { - svc, users, _, messages := newTestService(t) + svc, users, bots, messages := newTestService(t) svc.telegramLogin = newBotFatherTelegramLoginService(t) owner := newOwner(t, users, "+1090") bot, _, err := svc.CreateBot(context.Background(), owner.ID, "Login Demo", "login_demo_bot") @@ -55,31 +56,30 @@ func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) { if reply := sendToBotFather(t, svc, messages, owner, "add origin http://rp.example.test:3000"); !strings.Contains(reply, "Success!") { t.Fatalf("add origin reply = %q", reply) } - - sendToBotFather(t, svc, messages, owner, "/setlogin") - sendToBotFather(t, svc, messages, owner, "login_demo_bot") + state, found, err := bots.GetBotChatState(context.Background(), domain.BotFatherUserID, owner.ID) + if err != nil || !found || state.Step != botFatherStepValue || state.Draft[botFatherDraftBotID] != strconv.FormatInt(bot.ID, 10) { + t.Fatalf("state after first command = %+v, found=%v err=%v", state, found, err) + } if reply := sendToBotFather(t, svc, messages, owner, "add redirect http://192.0.2.26:3000/auth/callback"); !strings.Contains(reply, "Success!") { t.Fatalf("add redirect reply = %q", reply) } - - sendToBotFather(t, svc, messages, owner, "/setlogin") - sendToBotFather(t, svc, messages, owner, "login_demo_bot") if reply := sendToBotFather(t, svc, messages, owner, "algorithm ES256"); !strings.Contains(reply, "ES256") { t.Fatalf("algorithm reply = %q", reply) } - - sendToBotFather(t, svc, messages, owner, "/setlogin") - sendToBotFather(t, svc, messages, owner, "login_demo_bot") if reply := sendToBotFather(t, svc, messages, owner, "add ios dev.bedolaga.demo ABCDE12345 bedolaga://telegram-login Bedolaga iOS Demo"); !strings.Contains(reply, "Registered native app #") { t.Fatalf("add iOS app reply = %q", reply) } - - sendToBotFather(t, svc, messages, owner, "/setlogin") - sendToBotFather(t, svc, messages, owner, "login_demo_bot") fingerprint := strings.Repeat("A", 64) if reply := sendToBotFather(t, svc, messages, owner, "add android dev.bedolaga.demo "+fingerprint+" bedolaga://android-login Bedolaga Android Demo"); !strings.Contains(reply, "Registered native app #") { t.Fatalf("add Android app reply = %q", reply) } + done := sendToBotFather(t, svc, messages, owner, "/done") + if !strings.Contains(done, "Finished configuring") || !strings.Contains(done, "Signing algorithm: ES256") { + t.Fatalf("/done reply = %q", done) + } + if _, found, err := bots.GetBotChatState(context.Background(), domain.BotFatherUserID, owner.ID); err != nil || found { + t.Fatalf("state after /done: found=%v err=%v", found, err) + } sendToBotFather(t, svc, messages, owner, "/logininfo") info := sendToBotFather(t, svc, messages, owner, "login_demo_bot") @@ -98,3 +98,70 @@ func TestBotFatherTelegramLoginConfigurationFlow(t *testing.T) { t.Fatalf("rotate reply = %q", rotated) } } + +func TestBotFatherTelegramLoginBatchAndCancelFlow(t *testing.T) { + svc, users, bots, messages := newTestService(t) + svc.telegramLogin = newBotFatherTelegramLoginService(t) + owner := newOwner(t, users, "+1091") + bot, _, err := svc.CreateBot(context.Background(), owner.ID, "Batch Login Demo", "batch_login_bot") + if err != nil { + t.Fatal(err) + } + + if reply := sendToBotFather(t, svc, messages, owner, "/done"); !strings.Contains(reply, "no active") { + t.Fatalf("inactive /done reply = %q", reply) + } + sendToBotFather(t, svc, messages, owner, "/setlogin") + sendToBotFather(t, svc, messages, owner, "@batch_login_bot") + tooMany := strings.TrimSuffix(strings.Repeat("enable\n", maxTelegramLoginCommandsPerMessage+1), "\n") + if reply := sendToBotFather(t, svc, messages, owner, tooMany); !strings.Contains(reply, "at most 32 lines") { + t.Fatalf("oversized batch reply = %q", reply) + } + oversizedConfiguration, found, err := svc.telegramLogin.ClientConfiguration(context.Background(), bot.ID) + if err != nil || !found || len(oversizedConfiguration.AllowedURLs) != 0 || oversizedConfiguration.Client.SigningAlgorithm != "RS256" { + t.Fatalf("configuration after oversized batch = %+v, found=%v err=%v", oversizedConfiguration, found, err) + } + batch := strings.Join([]string{ + "add origin http://batch.example.test:3000", + "add redirect http://batch.example.test:3000/auth/telegram/callback", + "algorithm ES256", + "enable", + }, "\n") + if reply := sendToBotFather(t, svc, messages, owner, batch); !strings.Contains(reply, "Applied all 4 commands") || !strings.Contains(reply, "/done") { + t.Fatalf("batch reply = %q", reply) + } + configuration, found, err := svc.telegramLogin.ClientConfiguration(context.Background(), bot.ID) + if err != nil || !found || !configuration.Client.Enabled || configuration.Client.SigningAlgorithm != "ES256" || len(configuration.AllowedURLs) != 2 { + t.Fatalf("configuration after batch = %+v, found=%v err=%v", configuration, found, err) + } + + partial := strings.Join([]string{ + "add origin http://second.example.test:3001", + "add redirect not-a-url", + "disable", + }, "\n") + partialReply := sendToBotFather(t, svc, messages, owner, partial) + for _, want := range []string{"Applied 1 command(s) before the error", "Stopped at line 2", "1 later command(s) were not applied", "/done"} { + if !strings.Contains(partialReply, want) { + t.Fatalf("partial batch reply = %q, missing %q", partialReply, want) + } + } + configuration, found, err = svc.telegramLogin.ClientConfiguration(context.Background(), bot.ID) + if err != nil || !found || !configuration.Client.Enabled || len(configuration.AllowedURLs) != 3 { + t.Fatalf("configuration after partial batch = %+v, found=%v err=%v", configuration, found, err) + } + if _, found, err := bots.GetBotChatState(context.Background(), domain.BotFatherUserID, owner.ID); err != nil || !found { + t.Fatalf("state after partial batch: found=%v err=%v", found, err) + } + + if reply := sendToBotFather(t, svc, messages, owner, "/cancel"); !strings.Contains(reply, "already applied have been kept") { + t.Fatalf("/cancel reply = %q", reply) + } + if _, found, err := bots.GetBotChatState(context.Background(), domain.BotFatherUserID, owner.ID); err != nil || found { + t.Fatalf("state after /cancel: found=%v err=%v", found, err) + } + configuration, found, err = svc.telegramLogin.ClientConfiguration(context.Background(), bot.ID) + if err != nil || !found || len(configuration.AllowedURLs) != 3 { + t.Fatalf("configuration after /cancel = %+v, found=%v err=%v", configuration, found, err) + } +} diff --git a/internal/botapi/webhook.go b/internal/botapi/webhook.go index 8a480258..b2a6b52b 100644 --- a/internal/botapi/webhook.go +++ b/internal/botapi/webhook.go @@ -255,5 +255,5 @@ func (d *webhookDispatcher) fail(ctx context.Context, config domain.BotAPIWebhoo d.logger.Warn("record bot api webhook failure", zap.Int64("bot_user_id", config.BotUserID), zap.Error(err)) return } - d.logger.Debug("bot api webhook delivery failed", zap.Int64("bot_user_id", config.BotUserID), zap.Duration("retry_in", delay), zap.String("reason", message)) + d.logger.Warn("bot api webhook delivery failed", zap.Int64("bot_user_id", config.BotUserID), zap.Duration("retry_in", delay), zap.String("reason", message)) } diff --git a/internal/botapi/webhook_test.go b/internal/botapi/webhook_test.go index f31ad385..b6a8482a 100644 --- a/internal/botapi/webhook_test.go +++ b/internal/botapi/webhook_test.go @@ -10,6 +10,7 @@ import ( "time" "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" "telesrv/internal/domain" ) @@ -115,7 +116,8 @@ func TestWebhookDispatcherOnlyConfirmsSuccessfulPrefixAndSchedulesRetry(t *testi webhookFound: true, } gateway := &recordingWebhookGateway{fakeBotAPIGateway: base} - d := &webhookDispatcher{control: gateway, gateway: gateway, client: server.Client(), logger: zap.NewNop(), botSem: make(chan struct{}, 1), httpSem: make(chan struct{}, 8)} + logCore, observedLogs := observer.New(zap.WarnLevel) + d := &webhookDispatcher{control: gateway, gateway: gateway, client: server.Client(), logger: zap.New(logCore), botSem: make(chan struct{}, 1), httpSem: make(chan struct{}, 8)} d.deliver(context.Background(), base.webhook) gateway.mu.Lock() @@ -124,4 +126,18 @@ func TestWebhookDispatcherOnlyConfirmsSuccessfulPrefixAndSchedulesRetry(t *testi if base.webhookConfirmed != 21 || failure != "webhook returned HTTP 503" || !retryAt.After(time.Now()) { t.Fatalf("confirmed=%d failure=%q retry=%v", base.webhookConfirmed, failure, retryAt) } + entries := observedLogs.FilterMessage("bot api webhook delivery failed").All() + if len(entries) != 1 { + t.Fatalf("delivery failure warning count = %d, want 1", len(entries)) + } + fields := entries[0].ContextMap() + if fields["bot_user_id"] != int64(1001) || fields["reason"] != "webhook returned HTTP 503" { + t.Fatalf("delivery failure warning fields = %#v", fields) + } + if _, ok := fields["url"]; ok { + t.Fatalf("delivery failure warning must not include webhook URL: %#v", fields) + } + if _, ok := fields["secret_token"]; ok { + t.Fatalf("delivery failure warning must not include webhook secret: %#v", fields) + } } diff --git a/internal/store/memory/bot.go b/internal/store/memory/bot.go index 63f457d1..ef0b31bc 100644 --- a/internal/store/memory/bot.go +++ b/internal/store/memory/bot.go @@ -78,6 +78,7 @@ func botFatherSeedProfile() domain.BotProfile { {Command: "setlogin", Description: "configure Telegram Login"}, {Command: "logininfo", Description: "show Telegram Login configuration"}, {Command: "resetloginsecret", Description: "rotate an OIDC Client Secret"}, + {Command: "done", Description: "finish Telegram Login configuration"}, {Command: "cancel", Description: "cancel the current operation"}, {Command: "help", Description: "show help"}, }, diff --git a/internal/store/postgres/bot_integration_test.go b/internal/store/postgres/bot_integration_test.go index 12bdf74f..d9dca476 100644 --- a/internal/store/postgres/bot_integration_test.go +++ b/internal/store/postgres/bot_integration_test.go @@ -35,6 +35,16 @@ func TestBotStoreRoundTripPostgres(t *testing.T) { if bfProfile.TokenSecret != "" || len(bfProfile.Commands) == 0 { t.Fatalf("BotFather profile = %+v, want empty token with seeded commands", bfProfile) } + hasDone := false + for _, command := range bfProfile.Commands { + if command.Command == "done" { + hasDone = true + break + } + } + if !hasDone { + t.Fatalf("BotFather commands = %+v, want /done for persistent /setlogin sessions", bfProfile.Commands) + } // 空 phone 查询不得命中任何行。 if _, found, err := users.ByPhone(ctx, ""); err != nil || found { t.Fatalf("ByPhone('') found=%v err=%v, want not found", found, err) From eba402946a8c2acea23df324e2ab66c7bf41b85d Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 02:03:00 +0800 Subject: [PATCH 12/28] fix: sync account freeze peer visibility --- cmd/telesrv/main.go | 7 +- .../0131_account_freeze_visibility.down.sql | 5 + .../0131_account_freeze_visibility.up.sql | 37 ++++ internal/admin/service.go | 101 +++++++++++ internal/admin/service_test.go | 63 ++++++- internal/app/contacts/read_model_cache.go | 3 + internal/app/contacts/service.go | 6 + internal/app/dialogs/read_model_cache.go | 3 + internal/app/dialogs/service.go | 6 + internal/app/messages/service.go | 6 + internal/app/userprojection/contact_cache.go | 3 + internal/app/userprojection/projection.go | 48 +++++- .../app/userprojection/projection_test.go | 72 ++++++++ internal/app/users/service.go | 6 + internal/domain/admin.go | 13 ++ internal/domain/user.go | 21 +++ internal/rpc/account_freeze_worker.go | 107 ++++++++++++ internal/rpc/account_freeze_worker_test.go | 136 +++++++++++++++ internal/rpc/admin_hooks.go | 17 ++ internal/rpc/convert_users.go | 24 +++ .../rpc/convert_users_restriction_test.go | 62 +++++++ internal/rpc/router.go | 3 +- internal/store/postgres/admin.go | 163 +++++++++++++++++- .../postgres/admin_freeze_integration_test.go | 96 ++++++++++- .../store/postgres/read_model_listener.go | 9 + .../postgres/story_peer_read_model_test.go | 36 ++++ 26 files changed, 1034 insertions(+), 19 deletions(-) create mode 100644 deploy/migrations/0131_account_freeze_visibility.down.sql create mode 100644 deploy/migrations/0131_account_freeze_visibility.up.sql create mode 100644 internal/rpc/account_freeze_worker.go create mode 100644 internal/rpc/account_freeze_worker_test.go create mode 100644 internal/rpc/convert_users_restriction_test.go diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index d5efb734..35556cab 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -550,6 +550,7 @@ func run(logger *zap.Logger) error { contactsService := contacts.NewService(contactStore, userStore).Configure( contacts.WithPhotoProvider(cachedPhotos), contacts.WithPrivacyEvaluator(privacyService), + contacts.WithAccountFreezeProvider(adminService), contacts.WithReadModelVersions(readModelVersionStore), ) if seeded, err := langPackService.SeedDirectory(ctx, cfg.LangPackSeedDir); err != nil { @@ -751,13 +752,14 @@ func run(logger *zap.Logger) error { passkeyapp.WithAllowedOrigins(cfg.PasskeyAllowedOrigins)) // 自定义云主题(Create a New Theme):主题目录与每用户已安装列表均持久化到 postgres。 themeService := themesapp.NewService(postgres.NewThemeStore(pool)) - usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService)) + usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(adminService)) aiComposeService := aiapp.NewService(aiComposeStore, newAIComposeOptions(cfg, rateLimiter, usersService.PremiumActive, logger)...) botsService.SetAIChatGenerator(aiComposeService) dialogsService := dialogs.NewService(dialogStore, channelStore).Configure( dialogs.WithContactStore(contactStore), dialogs.WithPhotoProvider(cachedPhotos), dialogs.WithPrivacyEvaluator(privacyService), + dialogs.WithAccountFreezeProvider(adminService), dialogs.WithPremiumChecker(usersService.PremiumActive), dialogs.WithReadModelVersions(readModelVersionStore), ) @@ -782,6 +784,7 @@ func run(logger *zap.Logger) error { messageapp.WithContactStore(contactStore), messageapp.WithPhotoProvider(cachedPhotos), messageapp.WithPrivacyEvaluator(privacyService), + messageapp.WithAccountFreezeProvider(adminService), messageapp.WithReadModelVersions(readModelVersionStore), messageapp.WithBotResponder(botsService), messageapp.WithSendPermissionChecker(adminService), @@ -911,6 +914,7 @@ func run(logger *zap.Logger) error { Stars: starsService, StarsNotifier: router, UserNotifier: router, + FreezeNotifier: router, Channels: channelsService, ChannelNotifier: router, Messages: messagesService, @@ -938,6 +942,7 @@ func run(logger *zap.Logger) error { go activeSessions.RunPendingSweeper(ctx, time.Minute) go router.RunPremiumSweeper(ctx, cfg.PremiumSweepInterval, cfg.PremiumSweepBatch) go router.RunAccountLifecycle(ctx, time.Minute, 500) + go router.RunAccountFreezeNotifications(ctx, time.Minute, 500) if telegramLoginService != nil { go runTelegramLoginRetention(ctx, telegramLoginService, cfg.TelegramLoginRetention, cfg.TelegramLoginSweepInterval, cfg.TelegramLoginSweepBatch, logger.Named("telegram-login-retention")) } diff --git a/deploy/migrations/0131_account_freeze_visibility.down.sql b/deploy/migrations/0131_account_freeze_visibility.down.sql new file mode 100644 index 00000000..fd2eaf81 --- /dev/null +++ b/deploy/migrations/0131_account_freeze_visibility.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS public.account_freeze_notifications; + +ALTER TABLE public.account_restrictions + DROP CONSTRAINT IF EXISTS account_restrictions_version_check, + DROP COLUMN IF EXISTS version; diff --git a/deploy/migrations/0131_account_freeze_visibility.up.sql b/deploy/migrations/0131_account_freeze_visibility.up.sql new file mode 100644 index 00000000..60993ff5 --- /dev/null +++ b/deploy/migrations/0131_account_freeze_visibility.up.sql @@ -0,0 +1,37 @@ +-- A freeze/unfreeze is a viewer-visible user projection change. Version the +-- durable fact so a claimed old nudge can never acknowledge a newer state. +ALTER TABLE public.account_restrictions + ADD COLUMN version bigint DEFAULT 1 NOT NULL, + ADD CONSTRAINT account_restrictions_version_check CHECK (version > 0); + +-- updateUser has no pts. This coalesced queue is only a crash-safe online +-- nudge; offline clients reconstruct the current restriction from the +-- authoritative account_restrictions row during normal user hydration. +CREATE TABLE public.account_freeze_notifications ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + target_user_id bigint NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + frozen_user_id bigint NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + version bigint NOT NULL, + frozen boolean NOT NULL, + status text DEFAULT 'pending' NOT NULL, + attempts integer DEFAULT 0 NOT NULL, + next_attempt_at timestamp with time zone DEFAULT now() NOT NULL, + lease_until timestamp with time zone, + last_error text DEFAULT '' NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT account_freeze_notifications_status_check + CHECK (status IN ('pending', 'dispatching', 'delivered')), + CONSTRAINT account_freeze_notifications_attempts_check CHECK (attempts >= 0), + CONSTRAINT account_freeze_notifications_version_check CHECK (version > 0), + CONSTRAINT account_freeze_notifications_not_self_check CHECK (target_user_id <> frozen_user_id), + UNIQUE (target_user_id, frozen_user_id) +); + +CREATE INDEX account_freeze_notifications_ready_idx + ON public.account_freeze_notifications(next_attempt_at, id) + WHERE status = 'pending'; + +CREATE INDEX account_freeze_notifications_lease_idx + ON public.account_freeze_notifications(lease_until, id) + WHERE status = 'dispatching'; diff --git a/internal/admin/service.go b/internal/admin/service.go index 386f1591..356c02b2 100644 --- a/internal/admin/service.go +++ b/internal/admin/service.go @@ -52,6 +52,15 @@ type RestrictionStore interface { SetAccountFreeze(ctx context.Context, freeze domain.AccountFreeze) (domain.AccountFreeze, error) } +type accountFreezeBatchStore interface { + GetAccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error) +} + +type accountFreezeNotificationStore interface { + ClaimAccountFreezeNotifications(ctx context.Context, now time.Time, limit int, lease time.Duration) ([]domain.AccountFreezeNotification, error) + CompleteAccountFreezeNotification(ctx context.Context, id, version int64, now time.Time) error +} + type AuthService interface { ListAuthorizations(ctx context.Context, userID int64) ([]domain.Authorization, error) ResetAuthorization(ctx context.Context, userID, hash int64) (domain.Authorization, bool, error) @@ -80,6 +89,10 @@ type UserNotifier interface { NotifyUserChanged(ctx context.Context, u domain.User) error } +type AccountFreezeNotifier interface { + NotifyAccountFreezeChanged(ctx context.Context, freeze domain.AccountFreeze) error +} + type ChannelsService interface { GetChannelByID(ctx context.Context, channelID int64) (domain.Channel, error) SetVerified(ctx context.Context, channelID int64, verified bool) (domain.Channel, error) @@ -123,6 +136,7 @@ type Dependencies struct { Stars StarsService StarsNotifier StarsNotifier UserNotifier UserNotifier + FreezeNotifier AccountFreezeNotifier Channels ChannelsService ChannelNotifier ChannelNotifier Messages MessagesService @@ -140,6 +154,7 @@ type Service struct { stars StarsService starsNotifier StarsNotifier userNotifier UserNotifier + freezeNotifier AccountFreezeNotifier channels ChannelsService channelNotifier ChannelNotifier messages MessagesService @@ -178,6 +193,9 @@ func (s *Service) Configure(deps Dependencies) *Service { if deps.UserNotifier != nil { s.userNotifier = deps.UserNotifier } + if deps.FreezeNotifier != nil { + s.freezeNotifier = deps.FreezeNotifier + } if deps.Channels != nil { s.channels = deps.Channels } @@ -373,6 +391,78 @@ func (s *Service) AccountFreeze(ctx context.Context, userID int64) (domain.Accou return freeze, true, nil } +// AccountFreezes is the bounded-query projection API used by user hydration. +// Production stores use array batches; lightweight test stores keep the exact +// same semantics through the single-row fallback. +func (s *Service) AccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error) { + out := make(map[int64]domain.AccountFreeze) + if s == nil || s.restrictions == nil || len(userIDs) == 0 { + return out, nil + } + ids := uniqueFreezeUserIDs(userIDs) + if batch, ok := s.restrictions.(accountFreezeBatchStore); ok { + const batchSize = 1000 + for start := 0; start < len(ids); start += batchSize { + end := min(start+batchSize, len(ids)) + items, err := batch.GetAccountFreezes(ctx, ids[start:end]) + if err != nil { + return nil, err + } + for id, freeze := range items { + if err := validateAccountFreeze(freeze); err != nil { + return nil, fmt.Errorf("invalid durable account freeze for user %d: %w", id, err) + } + if freeze.Frozen { + out[id] = freeze + } + } + } + return out, nil + } + for _, id := range ids { + freeze, found, err := s.AccountFreeze(ctx, id) + if err != nil { + return nil, err + } + if found && freeze.Frozen { + out[id] = freeze + } + } + return out, nil +} + +func uniqueFreezeUserIDs(userIDs []int64) []int64 { + out := make([]int64, 0, len(userIDs)) + seen := make(map[int64]struct{}, len(userIDs)) + for _, id := range userIDs { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + out = append(out, id) + } + return out +} + +func (s *Service) ClaimAccountFreezeNotifications(ctx context.Context, now time.Time, limit int, lease time.Duration) ([]domain.AccountFreezeNotification, error) { + store, ok := s.restrictions.(accountFreezeNotificationStore) + if !ok { + return nil, nil + } + return store.ClaimAccountFreezeNotifications(ctx, now, limit, lease) +} + +func (s *Service) CompleteAccountFreezeNotification(ctx context.Context, id, version int64, now time.Time) error { + store, ok := s.restrictions.(accountFreezeNotificationStore) + if !ok { + return nil + } + return store.CompleteAccountFreezeNotification(ctx, id, version, now) +} + func validateAccountFreeze(freeze domain.AccountFreeze) error { if !freeze.Frozen { if !freeze.Since.IsZero() || !freeze.Until.IsZero() || freeze.AppealURL != "" { @@ -476,6 +566,10 @@ func (s *Service) SetAccountFrozen(ctx context.Context, req SetAccountFrozenRequ return CommandResult{}, err } details["updated_at"] = updated.UpdatedAt.UTC().Format(time.RFC3339) + details["version"] = updated.Version + if err := s.notifyAccountFreezeChanged(ctx, updated); err != nil { + details["notify_error"] = err.Error() + } return CommandResult{Message: "account freeze updated", Details: details}, nil }) } @@ -1321,6 +1415,13 @@ func (s *Service) notifyUserChanged(ctx context.Context, u domain.User) error { return s.userNotifier.NotifyUserChanged(ctx, u) } +func (s *Service) notifyAccountFreezeChanged(ctx context.Context, freeze domain.AccountFreeze) error { + if s == nil || s.freezeNotifier == nil { + return nil + } + return s.freezeNotifier.NotifyAccountFreezeChanged(ctx, freeze) +} + func (s *Service) notifyStarsBalanceChanged(ctx context.Context, balance domain.StarsBalance) error { if s == nil || s.starsNotifier == nil { return nil diff --git a/internal/admin/service_test.go b/internal/admin/service_test.go index f48c4182..46da9654 100644 --- a/internal/admin/service_test.go +++ b/internal/admin/service_test.go @@ -21,10 +21,12 @@ func TestSetAccountFrozenDryRunExecuteAndIdempotency(t *testing.T) { ctx := context.Background() repo := newMemoryCommandRepo() restrictions := &fakeRestrictionStore{} + notifier := &fakeAccountFreezeNotifier{} svc := NewService(Dependencies{ - Commands: repo, - Restrictions: restrictions, - Now: fixedNow, + Commands: repo, + Restrictions: restrictions, + FreezeNotifier: notifier, + Now: fixedNow, }) dry, err := svc.SetAccountFrozen(ctx, SetAccountFrozenRequest{ @@ -55,6 +57,9 @@ func TestSetAccountFrozenDryRunExecuteAndIdempotency(t *testing.T) { if exec.Status != string(domain.AdminCommandCompleted) || restrictions.setCalls != 1 { t.Fatalf("execute result=%+v setCalls=%d", exec, restrictions.setCalls) } + if len(notifier.items) != 1 || notifier.items[0].UserID != 1001 || !notifier.items[0].Frozen || notifier.items[0].Version != 1 { + t.Fatalf("freeze notifications = %+v, want one versioned frozen state", notifier.items) + } if err := svc.CanSendMessages(ctx, 1001); !errors.Is(err, domain.ErrUserFrozen) { t.Fatalf("CanSendMessages err=%v, want ErrUserFrozen", err) } @@ -70,6 +75,32 @@ func TestSetAccountFrozenDryRunExecuteAndIdempotency(t *testing.T) { if !again.AlreadyExecuted || restrictions.setCalls != 1 { t.Fatalf("duplicate result=%+v setCalls=%d, want idempotent replay", again, restrictions.setCalls) } + if len(notifier.items) != 1 { + t.Fatalf("idempotent replay emitted duplicate notification: %+v", notifier.items) + } +} + +func TestAccountFreezesBatchesAndReturnsOnlyActiveFacts(t *testing.T) { + now := fixedNow() + store := &fakeBatchRestrictionStore{fakeRestrictionStore: fakeRestrictionStore{items: map[int64]domain.AccountFreeze{ + 1001: { + UserID: 1001, Frozen: true, Version: 2, Since: now, + Until: now.Add(time.Hour), AppealURL: "https://appeals.example.test/1001", + }, + 1002: {UserID: 1002, Frozen: false, Version: 4}, + }}} + svc := NewService(Dependencies{Restrictions: store, Now: fixedNow}) + + got, err := svc.AccountFreezes(context.Background(), []int64{1001, 1001, 0, 1002}) + if err != nil { + t.Fatalf("AccountFreezes: %v", err) + } + if len(store.requests) != 1 || !reflect.DeepEqual(store.requests[0], []int64{1001, 1002}) { + t.Fatalf("batch requests = %v, want one deduplicated request", store.requests) + } + if len(got) != 1 || !got[1001].Frozen || got[1001].Version != 2 { + t.Fatalf("AccountFreezes = %+v, want active user 1001 only", got) + } } func TestSetAccountFrozenRejectsIncompleteStateAndUnfreezeClearsOverlay(t *testing.T) { @@ -492,11 +523,37 @@ func (f *fakeRestrictionStore) SetAccountFreeze(_ context.Context, r domain.Acco f.items = map[int64]domain.AccountFreeze{} } f.setCalls++ + r.Version = f.items[r.UserID].Version + 1 r.UpdatedAt = fixedNow() f.items[r.UserID] = r return r, nil } +type fakeBatchRestrictionStore struct { + fakeRestrictionStore + requests [][]int64 +} + +func (f *fakeBatchRestrictionStore) GetAccountFreezes(_ context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error) { + f.requests = append(f.requests, append([]int64(nil), userIDs...)) + out := make(map[int64]domain.AccountFreeze) + for _, id := range userIDs { + if freeze, ok := f.items[id]; ok && freeze.Frozen { + out[id] = freeze + } + } + return out, nil +} + +type fakeAccountFreezeNotifier struct { + items []domain.AccountFreeze +} + +func (f *fakeAccountFreezeNotifier) NotifyAccountFreezeChanged(_ context.Context, freeze domain.AccountFreeze) error { + f.items = append(f.items, freeze) + return nil +} + type fakeMessagesService struct { byID []domain.Message deleteCalls int diff --git a/internal/app/contacts/read_model_cache.go b/internal/app/contacts/read_model_cache.go index 205d1027..d22adb80 100644 --- a/internal/app/contacts/read_model_cache.go +++ b/internal/app/contacts/read_model_cache.go @@ -132,5 +132,8 @@ func cloneUser(in domain.User) domain.User { if in.PhotoStripped != nil { in.PhotoStripped = append([]byte(nil), in.PhotoStripped...) } + if in.RestrictionReasons != nil { + in.RestrictionReasons = append([]domain.UserRestrictionReason(nil), in.RestrictionReasons...) + } return in } diff --git a/internal/app/contacts/service.go b/internal/app/contacts/service.go index 2fe8b5de..e73a8139 100644 --- a/internal/app/contacts/service.go +++ b/internal/app/contacts/service.go @@ -31,6 +31,7 @@ type Service struct { users store.UserStore photos userprojection.ProfilePhotoProvider privacy phonePrivacyService + freezes userprojection.AccountFreezeProvider projector *userprojection.Projector versions store.ReadModelVersionStore cache *contactListReadModelCache @@ -49,6 +50,10 @@ func WithPrivacyEvaluator(p phonePrivacyService) Option { return func(s *Service) { s.privacy = p } } +func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option { + return func(s *Service) { s.freezes = p } +} + // WithReadModelVersions enables durable hash-token fast paths for NotModified RPCs. func WithReadModelVersions(v store.ReadModelVersionStore) Option { return func(s *Service) { s.versions = v } @@ -84,6 +89,7 @@ func (s *Service) rebuildProjector() { userprojection.WithContactStore(s.contacts), userprojection.WithPhotoProvider(s.photos), userprojection.WithPrivacyEvaluator(s.privacy), + userprojection.WithAccountFreezeProvider(s.freezes), ) } diff --git a/internal/app/dialogs/read_model_cache.go b/internal/app/dialogs/read_model_cache.go index 299aab67..9178c6b2 100644 --- a/internal/app/dialogs/read_model_cache.go +++ b/internal/app/dialogs/read_model_cache.go @@ -502,6 +502,9 @@ func cloneDialogUser(in domain.User) domain.User { if in.PhotoStripped != nil { in.PhotoStripped = append([]byte(nil), in.PhotoStripped...) } + if in.RestrictionReasons != nil { + in.RestrictionReasons = append([]domain.UserRestrictionReason(nil), in.RestrictionReasons...) + } return in } diff --git a/internal/app/dialogs/service.go b/internal/app/dialogs/service.go index 56dbb858..aae8c85a 100644 --- a/internal/app/dialogs/service.go +++ b/internal/app/dialogs/service.go @@ -24,6 +24,7 @@ type Service struct { contacts store.ContactStore photos userprojection.ProfilePhotoProvider privacy userprojection.PrivacyEvaluator + freezes userprojection.AccountFreezeProvider premium PremiumChecker projector *userprojection.Projector versions store.ReadModelVersionStore @@ -54,6 +55,10 @@ func WithPrivacyEvaluator(p userprojection.PrivacyEvaluator) Option { return func(s *Service) { s.privacy = p } } +func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option { + return func(s *Service) { s.freezes = p } +} + // WithReadModelVersions enables durable version-token backed peer dialog caching. func WithReadModelVersions(v store.ReadModelVersionStore) Option { return func(s *Service) { s.versions = v } @@ -93,6 +98,7 @@ func (s *Service) rebuildProjector() { userprojection.WithContactStore(s.contacts), userprojection.WithPhotoProvider(s.photos), userprojection.WithPrivacyEvaluator(s.privacy), + userprojection.WithAccountFreezeProvider(s.freezes), ) } diff --git a/internal/app/messages/service.go b/internal/app/messages/service.go index 8099b7b5..82ee42fb 100644 --- a/internal/app/messages/service.go +++ b/internal/app/messages/service.go @@ -16,6 +16,7 @@ type Service struct { contacts store.ContactStore photos userprojection.ProfilePhotoProvider privacy userprojection.PrivacyEvaluator + freezes userprojection.AccountFreezeProvider versions store.ReadModelVersionStore projector *userprojection.Projector botResponder BotResponder @@ -57,6 +58,10 @@ func WithPrivacyEvaluator(p userprojection.PrivacyEvaluator) Option { return func(s *Service) { s.privacy = p } } +func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option { + return func(s *Service) { s.freezes = p } +} + // WithBotResponder 启用服务端内置 bot(BotFather)对私聊消息的自动应答。 func WithBotResponder(r BotResponder) Option { return func(s *Service) { s.botResponder = r } @@ -85,6 +90,7 @@ func NewService(messages store.MessageStore, dialogs store.DialogStore, opts ... userprojection.WithContactStore(s.contacts), userprojection.WithPhotoProvider(s.photos), userprojection.WithPrivacyEvaluator(s.privacy), + userprojection.WithAccountFreezeProvider(s.freezes), ) return s } diff --git a/internal/app/userprojection/contact_cache.go b/internal/app/userprojection/contact_cache.go index 64946a2e..412d66d4 100644 --- a/internal/app/userprojection/contact_cache.go +++ b/internal/app/userprojection/contact_cache.go @@ -454,6 +454,9 @@ func cloneCachedUser(in domain.User) domain.User { if in.ContactNoteEntities != nil { in.ContactNoteEntities = append([]domain.MessageEntity(nil), in.ContactNoteEntities...) } + if in.RestrictionReasons != nil { + in.RestrictionReasons = append([]domain.UserRestrictionReason(nil), in.RestrictionReasons...) + } return in } diff --git a/internal/app/userprojection/projection.go b/internal/app/userprojection/projection.go index ded9a3b5..5c654aca 100644 --- a/internal/app/userprojection/projection.go +++ b/internal/app/userprojection/projection.go @@ -24,6 +24,12 @@ type PrivacyEvaluator interface { CanSee(ctx context.Context, ownerUserID, viewerUserID int64, key domain.PrivacyKey) (bool, error) } +// AccountFreezeProvider returns durable account freeze facts for a bounded +// batch. The projector only exposes them to viewers other than the frozen user. +type AccountFreezeProvider interface { + AccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error) +} + // BatchPrivacyEvaluator 批量评估多 owner 对单 viewer 的可见性,消除 projectBatch / fan-out // 投影里 per-user 3×CanSee 的 N+1。可选:实现了它的 evaluator(privacy.Service)会被 // projectBatch 优先用批量预取,否则回退逐 CanSee。结果必须与逐 CanSee 字节等价。 @@ -52,6 +58,7 @@ type Projector struct { contacts store.ContactStore photos ProfilePhotoProvider privacy PrivacyEvaluator + freezes AccountFreezeProvider } // Option configures a Projector. @@ -72,6 +79,11 @@ func WithPrivacyEvaluator(privacy PrivacyEvaluator) Option { return func(p *Projector) { p.privacy = privacy } } +// WithAccountFreezeProvider enables viewer-scoped frozen-account visibility. +func WithAccountFreezeProvider(provider AccountFreezeProvider) Option { + return func(p *Projector) { p.freezes = provider } +} + // New creates a user projector. func New(opts ...Option) *Projector { p := &Projector{} @@ -87,7 +99,7 @@ func (p *Projector) ForViewer(ctx context.Context, viewerUserID int64, users []d if p == nil { return users, nil } - return projectBatch(ctx, p.contacts, p.photos, p.privacy, viewerUserID, users) + return projectBatch(ctx, p.contacts, p.photos, p.privacy, p.freezes, viewerUserID, users) } // One applies ForViewer to a single user. @@ -136,6 +148,7 @@ func (p *Projector) ForViewers(ctx context.Context, viewerUserIDs []int64, users fallbackRefs map[int64]domain.ProfilePhotoRef contactsByViewer map[int64]map[int64]domain.Contact matrix map[int64]map[int64]map[domain.PrivacyKey]bool + freezes map[int64]domain.AccountFreeze ) g, gctx := errgroup.WithContext(ctx) // 1) 共享头像:profile/fallback 一次批量,跨全部 viewer 复用;personal photo v1 跳过(见 doc)。 @@ -159,6 +172,13 @@ func (p *Projector) ForViewers(ctx context.Context, viewerUserIDs []int64, users return err }) } + if p.freezes != nil && len(ids) > 0 { + g.Go(func() error { + var err error + freezes, err = p.freezes.AccountFreezes(gctx, ids) + return err + }) + } if err := g.Wait(); err != nil { return nil, err } @@ -194,6 +214,7 @@ func (p *Projector) ForViewers(ctx context.Context, viewerUserIDs []int64, users return nil, perr } } + pj = applyAccountFreezeProjection(pj, viewer, freezes[u.ID]) cache[u.ID] = pj projected[i] = pj } @@ -262,6 +283,7 @@ func cloneUsers(users []domain.User) []domain.User { copy(out, users) for i := range out { out[i].ContactNoteEntities = append([]domain.MessageEntity(nil), out[i].ContactNoteEntities...) + out[i].RestrictionReasons = append([]domain.UserRestrictionReason(nil), out[i].RestrictionReasons...) } return out } @@ -357,7 +379,7 @@ func One(ctx context.Context, contacts store.ContactStore, viewerUserID int64, u return projected[0], nil } -func projectBatch(ctx context.Context, contacts store.ContactStore, photos ProfilePhotoProvider, privacy PrivacyEvaluator, viewerUserID int64, users []domain.User) ([]domain.User, error) { +func projectBatch(ctx context.Context, contacts store.ContactStore, photos ProfilePhotoProvider, privacy PrivacyEvaluator, freezesProvider AccountFreezeProvider, viewerUserID int64, users []domain.User) ([]domain.User, error) { if len(users) == 0 { return users, nil } @@ -371,6 +393,7 @@ func projectBatch(ctx context.Context, contacts store.ContactStore, photos Profi personalRefs = map[int64]domain.ProfilePhotoRef{} contactsByID map[int64]domain.Contact visibility map[int64]map[domain.PrivacyKey]bool + freezes map[int64]domain.AccountFreeze ) // 这些预取查询互不依赖(头像 profile/fallback、联系人 GetMany/PersonalPhotos、privacy 可见性), // 并发执行把 ~6 次串行 round-trip 收敛成一波;每个 goroutine 只写自己那一个变量,组装循环在 @@ -433,6 +456,16 @@ func projectBatch(ctx context.Context, contacts store.ContactStore, photos Profi visibility = v return nil }) + if freezesProvider != nil && len(ids) > 0 { + g.Go(func() error { + m, err := freezesProvider.AccountFreezes(gctx, ids) + if err != nil { + return err + } + freezes = m + return nil + }) + } if err := g.Wait(); err != nil { return nil, err } @@ -462,12 +495,23 @@ func projectBatch(ctx context.Context, contacts store.ContactStore, photos Profi return nil, err } } + projected = applyAccountFreezeProjection(projected, viewerUserID, freezes[u.ID]) cache[u.ID] = projected out[i] = projected } return out, nil } +func applyAccountFreezeProjection(user domain.User, viewerUserID int64, freeze domain.AccountFreeze) domain.User { + // Base users and self users must never retain a viewer-scoped restriction. + user.RestrictionReasons = nil + if user.Deleted || viewerUserID == 0 || user.ID == 0 || user.ID == viewerUserID || !freeze.Frozen { + return user + } + user.RestrictionReasons = domain.AccountFrozenRestrictionReasons() + return user +} + func prefetchPrivacyVisibility(ctx context.Context, privacy PrivacyEvaluator, viewerUserID int64, users []domain.User) (map[int64]map[domain.PrivacyKey]bool, error) { if privacy == nil || viewerUserID == 0 { return nil, nil diff --git a/internal/app/userprojection/projection_test.go b/internal/app/userprojection/projection_test.go index 7f17dfc2..6be85d50 100644 --- a/internal/app/userprojection/projection_test.go +++ b/internal/app/userprojection/projection_test.go @@ -119,6 +119,64 @@ func TestProjectorUsesFallbackWhenProfilePhotoHidden(t *testing.T) { } } +func TestProjectorAccountFreezeIsViewerScopedAndReversible(t *testing.T) { + ctx := context.Background() + const ( + frozenUserID = int64(4001) + otherViewer = int64(4002) + ) + freezes := &fakeAccountFreezes{items: map[int64]domain.AccountFreeze{ + frozenUserID: {UserID: frozenUserID, Frozen: true, Version: 3}, + }} + projector := New(WithAccountFreezeProvider(freezes)) + base := []domain.User{{ + ID: frozenUserID, + FirstName: "Frozen", + // Viewer-scoped fields must never be trusted from a reused base object. + RestrictionReasons: []domain.UserRestrictionReason{{Platform: "all", Reason: "stale", Text: "stale"}}, + }} + + otherView, err := projector.ForViewer(ctx, otherViewer, base) + if err != nil { + t.Fatalf("ForViewer(other): %v", err) + } + got := projectionUser(t, otherView, frozenUserID) + if !reflect.DeepEqual(got.RestrictionReasons, domain.AccountFrozenRestrictionReasons()) { + t.Fatalf("other-view restriction = %+v, want frozen restriction", got.RestrictionReasons) + } + if base[0].RestrictionReasons[0].Reason != "stale" { + t.Fatalf("projection mutated base user: %+v", base[0]) + } + + selfView, err := projector.ForViewer(ctx, frozenUserID, base) + if err != nil { + t.Fatalf("ForViewer(self): %v", err) + } + if reasons := projectionUser(t, selfView, frozenUserID).RestrictionReasons; len(reasons) != 0 { + t.Fatalf("self-view restriction = %+v, want none", reasons) + } + + batch, err := projector.ForViewers(ctx, []int64{otherViewer, frozenUserID}, base) + if err != nil { + t.Fatalf("ForViewers: %v", err) + } + if reasons := projectionUser(t, batch[otherViewer], frozenUserID).RestrictionReasons; !reflect.DeepEqual(reasons, domain.AccountFrozenRestrictionReasons()) { + t.Fatalf("batch other-view restriction = %+v", reasons) + } + if reasons := projectionUser(t, batch[frozenUserID], frozenUserID).RestrictionReasons; len(reasons) != 0 { + t.Fatalf("batch self-view restriction = %+v, want none", reasons) + } + + freezes.items = nil + unfrozenView, err := projector.ForViewer(ctx, otherViewer, otherView) + if err != nil { + t.Fatalf("ForViewer(after unfreeze): %v", err) + } + if reasons := projectionUser(t, unfrozenView, frozenUserID).RestrictionReasons; len(reasons) != 0 { + t.Fatalf("unfrozen projection retained restriction = %+v", reasons) + } +} + // TestForViewersEquivalentToForViewer 锁定 fan-out 模板化的核心安全网:ForViewers(viewers, users) // 的每个 viewer 切片必须与逐 viewer 的 ForViewer(viewer, users) 字节等价(隐私/改名/头像投影 // 不能因 O(owner) 模板化而漂移泄漏)。**唯一允许的差异是 personal photo overlay**:v1 模板不做 @@ -243,6 +301,20 @@ type fakeProfilePhotos struct { fallback map[int64]domain.ProfilePhotoRef } +type fakeAccountFreezes struct { + items map[int64]domain.AccountFreeze +} + +func (f *fakeAccountFreezes) AccountFreezes(_ context.Context, ids []int64) (map[int64]domain.AccountFreeze, error) { + out := make(map[int64]domain.AccountFreeze) + for _, id := range ids { + if freeze, ok := f.items[id]; ok { + out[id] = freeze + } + } + return out, nil +} + func (p fakeProfilePhotos) CurrentProfilePhotos(_ context.Context, _ domain.PeerType, ids []int64) (map[int64]domain.ProfilePhotoRef, error) { return p.CurrentProfilePhotosKind(context.Background(), domain.PeerTypeUser, ids, domain.ProfilePhotoKindProfile) } diff --git a/internal/app/users/service.go b/internal/app/users/service.go index 993727f2..eadf49d3 100644 --- a/internal/app/users/service.go +++ b/internal/app/users/service.go @@ -25,6 +25,7 @@ type Service struct { contacts store.ContactStore photos ProfilePhotoProvider privacy userprojection.PrivacyEvaluator + freezes userprojection.AccountFreezeProvider projector *userprojection.Projector } @@ -55,6 +56,10 @@ func WithPrivacyEvaluator(p userprojection.PrivacyEvaluator) Option { return func(s *Service) { s.privacy = p } } +func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option { + return func(s *Service) { s.freezes = p } +} + const ( minUsernameLen = 5 maxUsernameLen = 32 @@ -77,6 +82,7 @@ func NewService(users store.UserStore, opts ...Option) *Service { userprojection.WithContactStore(s.contacts), userprojection.WithPhotoProvider(s.photos), userprojection.WithPrivacyEvaluator(s.privacy), + userprojection.WithAccountFreezeProvider(s.freezes), ) return s } diff --git a/internal/domain/admin.go b/internal/domain/admin.go index 6bf6d651..70aa900a 100644 --- a/internal/domain/admin.go +++ b/internal/domain/admin.go @@ -32,6 +32,7 @@ type AdminCommand struct { type AccountFreeze struct { UserID int64 Frozen bool + Version int64 Since time.Time Until time.Time AppealURL string @@ -40,3 +41,15 @@ type AccountFreeze struct { CommandID string UpdatedAt time.Time } + +// AccountFreezeNotification is a durable, coalesced online refresh for one +// viewer. UpdateUser itself has no pts; offline clients always recover from the +// authoritative viewer-scoped user projection instead of replaying this row. +type AccountFreezeNotification struct { + ID int64 + TargetUserID int64 + FrozenUserID int64 + Version int64 + Frozen bool + Attempts int +} diff --git a/internal/domain/user.go b/internal/domain/user.go index 22413065..fa2e1a0b 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -104,6 +104,10 @@ type User struct { Contact bool Mutual bool CloseFriend bool + // RestrictionReasons are transient, viewer-scoped unavailability reasons. + // They are produced after loading the viewer-independent base user and must + // never be persisted in users or the base-user cache. + RestrictionReasons []UserRestrictionReason // ContactNote/ContactNoteEntities are transient viewer-scoped contact // projection fields. They must never be persisted into users or a // viewer-independent base-user cache. @@ -153,6 +157,23 @@ type User struct { AccountDeleteAt time.Time } +// UserRestrictionReason is the protocol-neutral form of Telegram's +// restrictionReason. Platform "all" applies to TDesktop and official mobile +// clients; Text is intentionally server supplied and directly user-visible. +type UserRestrictionReason struct { + Platform string + Reason string + Text string +} + +func AccountFrozenRestrictionReasons() []UserRestrictionReason { + return []UserRestrictionReason{{ + Platform: "all", + Reason: "frozen", + Text: "This account is frozen.", + }} +} + // PremiumActiveAt 报告用户在 now(Unix 秒)时刻是否为有效会员。 // bot 永不为会员(官方语义;授予路径同样排除 bot,这里是双保险)。 func (u User) PremiumActiveAt(now int64) bool { diff --git a/internal/rpc/account_freeze_worker.go b/internal/rpc/account_freeze_worker.go new file mode 100644 index 00000000..80ea01cf --- /dev/null +++ b/internal/rpc/account_freeze_worker.go @@ -0,0 +1,107 @@ +package rpc + +import ( + "context" + "time" + + "github.com/iamxvbaba/td/tg" + "go.uber.org/zap" + + "telesrv/internal/domain" +) + +type accountFreezeNotificationService interface { + ClaimAccountFreezeNotifications(ctx context.Context, now time.Time, limit int, lease time.Duration) ([]domain.AccountFreezeNotification, error) + CompleteAccountFreezeNotification(ctx context.Context, id, version int64, now time.Time) error +} + +// RunAccountFreezeNotifications drains the crash-safe, coalesced non-pts +// updateUser queue. One attempt is enough for online delivery; offline clients +// recover the current state from viewer-scoped user hydration. +func (r *Router) RunAccountFreezeNotifications(ctx context.Context, interval time.Duration, batch int) { + if interval <= 0 { + interval = time.Minute + } + if batch <= 0 { + batch = 500 + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + r.drainAccountFreezeNotifications(ctx, batch) + select { + case <-ctx.Done(): + return + case <-ticker.C: + case <-r.accountFreezeWake: + } + } +} + +func (r *Router) drainAccountFreezeNotifications(ctx context.Context, batch int) { + svc, ok := r.deps.AccountFreeze.(accountFreezeNotificationService) + if !ok || r.deps.Users == nil { + return + } + for { + now := r.clock.Now().UTC() + claimCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + notifications, err := svc.ClaimAccountFreezeNotifications(claimCtx, now, batch, 2*time.Minute) + cancel() + if err != nil { + r.log.Warn("claim account freeze notifications failed", zap.Error(err)) + return + } + for _, notification := range notifications { + r.dispatchAccountFreezeNotification(ctx, svc, notification) + } + if len(notifications) < batch { + return + } + } +} + +func (r *Router) dispatchAccountFreezeNotification(ctx context.Context, svc accountFreezeNotificationService, notification domain.AccountFreezeNotification) { + peer := domain.Peer{Type: domain.PeerTypeUser, ID: notification.FrozenUserID} + if contacts, ok := r.deps.Contacts.(interface{ InvalidateViewers(...int64) }); ok { + contacts.InvalidateViewers(notification.TargetUserID) + } + if dialogs, ok := r.deps.Dialogs.(interface { + InvalidateDialog(int64, domain.Peer) + }); ok { + dialogs.InvalidateDialog(notification.TargetUserID, peer) + } + r.invalidateRPCProjectionForPeer(notification.TargetUserID, peer) + + loadCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + user, found, err := r.deps.Users.ByID(loadCtx, notification.TargetUserID, notification.FrozenUserID) + cancel() + if err != nil { + r.log.Warn("load frozen user projection for notification failed", + zap.Int64("target_user_id", notification.TargetUserID), + zap.Int64("frozen_user_id", notification.FrozenUserID), + zap.Int64("version", notification.Version), + zap.Error(err)) + return + } + if !found { + user = domain.User{ID: notification.FrozenUserID, Deleted: true} + } + pushCtx, pushCancel := context.WithTimeout(ctx, 10*time.Second) + r.pushUserUpdates(pushCtx, notification.TargetUserID, &tg.Updates{ + Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: notification.FrozenUserID}}, + Users: r.tgUsersForViewer(notification.TargetUserID, []domain.User{user}), + Date: int(r.clock.Now().Unix()), + }) + pushCancel() + + completeCtx, completeCancel := context.WithTimeout(ctx, 10*time.Second) + err = svc.CompleteAccountFreezeNotification(completeCtx, notification.ID, notification.Version, r.clock.Now().UTC()) + completeCancel() + if err != nil { + r.log.Warn("complete account freeze notification failed", + zap.Int64("notification_id", notification.ID), + zap.Int64("version", notification.Version), + zap.Error(err)) + } +} diff --git a/internal/rpc/account_freeze_worker_test.go b/internal/rpc/account_freeze_worker_test.go new file mode 100644 index 00000000..c9f39f42 --- /dev/null +++ b/internal/rpc/account_freeze_worker_test.go @@ -0,0 +1,136 @@ +package rpc + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/iamxvbaba/td/clock" + "github.com/iamxvbaba/td/tg" + "go.uber.org/zap/zaptest" + + "telesrv/internal/domain" +) + +func TestAccountFreezeNotificationPushesCurrentViewerProjection(t *testing.T) { + const ( + viewerID = int64(1001) + frozenID = int64(1002) + ) + sessions := &captureSessions{} + freezeSvc := &freezeWorkerService{} + users := &freezeWorkerUsers{user: domain.User{ + ID: frozenID, + FirstName: "Frozen", + RestrictionReasons: domain.AccountFrozenRestrictionReasons(), + }} + r := New(Config{}, Deps{ + AccountFreeze: freezeSvc, + Users: users, + Sessions: sessions, + }, zaptest.NewLogger(t), clock.System) + + r.dispatchAccountFreezeNotification(context.Background(), freezeSvc, domain.AccountFreezeNotification{ + ID: 7, TargetUserID: viewerID, FrozenUserID: frozenID, Version: 4, Frozen: true, + }) + + if len(freezeSvc.completed) != 1 || freezeSvc.completed[0] != [2]int64{7, 4} { + t.Fatalf("completed = %v, want [[7 4]]", freezeSvc.completed) + } + if got := sessions.pushedUserIDs(); len(got) != 1 || got[0] != viewerID { + t.Fatalf("pushed user IDs = %v, want [%d]", got, viewerID) + } + updates, ok := sessions.lastUserPush().(*tg.Updates) + if !ok || len(updates.Updates) != 1 || len(updates.Users) != 1 { + t.Fatalf("push = %#v, want updateUser plus projected user", sessions.lastUserPush()) + } + if update, ok := updates.Updates[0].(*tg.UpdateUser); !ok || update.UserID != frozenID { + t.Fatalf("update = %#v, want updateUser(%d)", updates.Updates[0], frozenID) + } + projected, ok := updates.Users[0].(*tg.User) + if !ok || !projected.Restricted { + t.Fatalf("projected user = %#v, want restricted user", updates.Users[0]) + } + reasons, ok := projected.GetRestrictionReason() + if !ok || len(reasons) != 1 || reasons[0].Reason != "frozen" { + t.Fatalf("projected restriction = %+v ok=%v", reasons, ok) + } +} + +func TestAccountFreezeNotificationLoadsCurrentStateAndRetriesLoadFailure(t *testing.T) { + const ( + viewerID = int64(2001) + frozenID = int64(2002) + ) + sessions := &captureSessions{} + freezeSvc := &freezeWorkerService{} + users := &freezeWorkerUsers{err: errors.New("projection unavailable")} + r := New(Config{}, Deps{ + AccountFreeze: freezeSvc, + Users: users, + Sessions: sessions, + }, zaptest.NewLogger(t), clock.System) + notification := domain.AccountFreezeNotification{ + ID: 8, TargetUserID: viewerID, FrozenUserID: frozenID, Version: 5, Frozen: true, + } + + r.dispatchAccountFreezeNotification(context.Background(), freezeSvc, notification) + if len(freezeSvc.completed) != 0 || len(sessions.pushedUserIDs()) != 0 { + t.Fatalf("failed load completed=%v pushes=%v, want retry without push", freezeSvc.completed, sessions.pushedUserIDs()) + } + + // The queued payload may say frozen, but delivery must hydrate the latest + // viewer projection so a newer unfreeze can never be overwritten by stale work. + users.err = nil + users.user = domain.User{ID: frozenID, FirstName: "Active"} + r.dispatchAccountFreezeNotification(context.Background(), freezeSvc, notification) + updates, ok := sessions.lastUserPush().(*tg.Updates) + if !ok || len(updates.Users) != 1 { + t.Fatalf("push = %#v", sessions.lastUserPush()) + } + projected, ok := updates.Users[0].(*tg.User) + if !ok || projected.Restricted { + t.Fatalf("latest projected user = %#v, want unrestricted", updates.Users[0]) + } + if len(freezeSvc.completed) != 1 || freezeSvc.completed[0] != [2]int64{8, 5} { + t.Fatalf("completed = %v, want [[8 5]]", freezeSvc.completed) + } +} + +type freezeWorkerService struct { + completed [][2]int64 +} + +func (*freezeWorkerService) AccountFreeze(context.Context, int64) (domain.AccountFreeze, bool, error) { + return domain.AccountFreeze{}, false, nil +} + +func (*freezeWorkerService) ClaimAccountFreezeNotifications(context.Context, time.Time, int, time.Duration) ([]domain.AccountFreezeNotification, error) { + return nil, nil +} + +func (s *freezeWorkerService) CompleteAccountFreezeNotification(_ context.Context, id, version int64, _ time.Time) error { + s.completed = append(s.completed, [2]int64{id, version}) + return nil +} + +type freezeWorkerUsers struct { + user domain.User + err error +} + +func (s *freezeWorkerUsers) Self(context.Context, int64) (domain.User, error) { + return s.user, s.err +} + +func (s *freezeWorkerUsers) ByID(context.Context, int64, int64) (domain.User, bool, error) { + return s.user, s.err == nil, s.err +} + +func (s *freezeWorkerUsers) ByIDs(context.Context, int64, []int64) ([]domain.User, error) { + if s.err != nil { + return nil, s.err + } + return []domain.User{s.user}, nil +} diff --git a/internal/rpc/admin_hooks.go b/internal/rpc/admin_hooks.go index 51caf393..a85e365c 100644 --- a/internal/rpc/admin_hooks.go +++ b/internal/rpc/admin_hooks.go @@ -46,3 +46,20 @@ func (r *Router) NotifyStarsBalanceChanged(ctx context.Context, balance domain.S }) return nil } + +// NotifyAccountFreezeChanged invalidates target-scoped projections immediately +// and wakes the durable audience nudge worker. Cross-instance cache invalidation +// is also carried by the committed user_visibility read-model notification. +func (r *Router) NotifyAccountFreezeChanged(_ context.Context, freeze domain.AccountFreeze) error { + if r == nil || freeze.UserID == 0 { + return nil + } + r.invalidateRPCProjectionForUser(freeze.UserID) + if r.accountFreezeWake != nil { + select { + case r.accountFreezeWake <- struct{}{}: + default: + } + } + return nil +} diff --git a/internal/rpc/convert_users.go b/internal/rpc/convert_users.go index 1093453b..66f05805 100644 --- a/internal/rpc/convert_users.go +++ b/internal/rpc/convert_users.go @@ -30,6 +30,7 @@ func tgSelfUser(u domain.User) *tg.User { applyTgUserBotFields(out, u) applyTgUserPremiumFields(out, u) applyTgUserColorFields(out, u) + applyTgUserRestrictionFields(out, u) if u.LinkedCommunityID != 0 { out.SetLinkedCommunityID(u.LinkedCommunityID) } @@ -60,6 +61,7 @@ func tgUser(u domain.User) *tg.User { applyTgUserBotFields(out, u) applyTgUserPremiumFields(out, u) applyTgUserColorFields(out, u) + applyTgUserRestrictionFields(out, u) if u.LinkedCommunityID != 0 { out.SetLinkedCommunityID(u.LinkedCommunityID) } @@ -69,6 +71,28 @@ func tgUser(u domain.User) *tg.User { return out } +func applyTgUserRestrictionFields(out *tg.User, u domain.User) { + if out == nil || len(u.RestrictionReasons) == 0 { + return + } + reasons := make([]tg.RestrictionReason, 0, len(u.RestrictionReasons)) + for _, reason := range u.RestrictionReasons { + if reason.Platform == "" || reason.Reason == "" || reason.Text == "" { + continue + } + reasons = append(reasons, tg.RestrictionReason{ + Platform: reason.Platform, + Reason: reason.Reason, + Text: reason.Text, + }) + } + if len(reasons) == 0 { + return + } + out.Restricted = true + out.SetRestrictionReason(reasons) +} + // applyTgUserPremiumFields 由到期时间即时派生 premium flag(bit28,独立位)与 // emoji status。判断用真实时钟:premium 的权威来源是 premium_expires_at 本身, // 到期即停发,正确性不依赖后台 sweeper(它只负责清理与 updateUser 通知); diff --git a/internal/rpc/convert_users_restriction_test.go b/internal/rpc/convert_users_restriction_test.go new file mode 100644 index 00000000..d7280f4f --- /dev/null +++ b/internal/rpc/convert_users_restriction_test.go @@ -0,0 +1,62 @@ +package rpc + +import ( + "testing" + + "github.com/iamxvbaba/td/bin" + "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tlprofile" + + "telesrv/internal/domain" +) + +func TestTgUserEncodesFrozenRestriction(t *testing.T) { + user := tgUser(domain.User{ + ID: 1001, + FirstName: "Frozen", + RestrictionReasons: domain.AccountFrozenRestrictionReasons(), + }) + if !user.Restricted { + t.Fatal("tg user restricted=false, want true") + } + reasons, ok := user.GetRestrictionReason() + if !ok || len(reasons) != 1 { + t.Fatalf("restriction_reason = %+v ok=%v, want one reason", reasons, ok) + } + if got := reasons[0]; got.Platform != "all" || got.Reason != "frozen" || got.Text != "This account is frozen." { + t.Fatalf("restriction_reason = %+v", got) + } + + for profile := tlprofile.Profile225; profile <= tlprofile.Profile228; profile++ { + wire := &bin.Buffer{} + if err := tlprofile.EncodeObject(profile, user, wire); err != nil { + t.Fatalf("encode layer %d frozen user: %v", profile, err) + } + decoded, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: wire.Copy()}, tlprofile.Limits{}) + if err != nil { + t.Fatalf("decode layer %d frozen user: %v", profile, err) + } + exact, ok := decoded.(*tg.User) + if !ok || !exact.Restricted { + t.Fatalf("layer %d user = %#v, want restricted", profile, decoded) + } + exactReasons, ok := exact.GetRestrictionReason() + if !ok || len(exactReasons) != 1 || exactReasons[0].Reason != "frozen" { + t.Fatalf("layer %d restriction = %+v ok=%v", profile, exactReasons, ok) + } + } +} + +func TestTgUserSkipsIncompleteRestriction(t *testing.T) { + user := tgUser(domain.User{ + ID: 1001, + FirstName: "Active", + RestrictionReasons: []domain.UserRestrictionReason{{Platform: "all", Reason: "frozen"}}, + }) + if user.Restricted { + t.Fatal("incomplete restriction was encoded") + } + if reasons, ok := user.GetRestrictionReason(); ok || len(reasons) != 0 { + t.Fatalf("restriction_reason = %+v ok=%v, want omitted", reasons, ok) + } +} diff --git a/internal/rpc/router.go b/internal/rpc/router.go index 97a560b8..1bf9b11e 100644 --- a/internal/rpc/router.go +++ b/internal/rpc/router.go @@ -164,6 +164,7 @@ type Router struct { stickerCatalog *stickerCatalogCache transientPrivateBigReactions transientPrivateBigReactionCache accountSettings *accountSettingsCache + accountFreezeWake chan struct{} // webPageResolveSem 是链接预览异步解析的并发信号量(有界):发送后把 pending 占位 // 解析为卡片并就地替换。满则丢弃任务(消息留 pending)。nil=未启用(测试可直接调 // resolvePendingWebPage 同步验证)。 @@ -237,7 +238,7 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router { if instanceID == "" { instanceID = fmt.Sprintf("%016x", randomNonZeroInt64()) } - r := &Router{cfg: cfg, log: log, clock: clk, deps: deps, exactProfiles: make(map[clientInfoSessionKey]exactSessionProfileEntry), authLayerEvidence: make(map[[8]byte]authLayerDefaultEvidence), presence: newPresenceTracker(), callbacks: newCallbackRegistry(deps.BotCallbacks), inlines: newInlineRegistry(botInlineQueryTTL, deps.Inline), webviews: newWebViewRegistry(webViewSessionTTL, deps.Inline), loginTokens: newLoginTokenRegistry(), botAPIUpdates: newBotAPIUpdateNotifier(), tempKeyResolveCache: newTempKeyResolveCache(cfg.TempKeyResolveCacheMaxEntries), storyProjectionCache: newStoryProjectionCache(clk.Now), storyPinnedCache: newStoryPinnedAvailableCache(clk.Now), storyPinnedListCache: newStoryPinnedStoriesCache(clk.Now), channelFullBotCache: newChannelFullBotInfoCache(clk.Now), userFullProjectionCache: newUserFullProjectionCache(clk.Now), peerSettingsProjectionCache: newPeerSettingsProjectionCache(clk.Now), channelFullProjectionCache: newChannelFullProjectionCache(clk.Now), emojiStickers: newEmojiStickerIndex(clk.Now), notifySettings: newNotifySettingsCache(clk.Now), stickerCatalog: newStickerCatalogCache(clk.Now), accountSettings: newAccountSettingsCache(clk.Now), instanceID: instanceID} + r := &Router{cfg: cfg, log: log, clock: clk, deps: deps, exactProfiles: make(map[clientInfoSessionKey]exactSessionProfileEntry), authLayerEvidence: make(map[[8]byte]authLayerDefaultEvidence), presence: newPresenceTracker(), callbacks: newCallbackRegistry(deps.BotCallbacks), inlines: newInlineRegistry(botInlineQueryTTL, deps.Inline), webviews: newWebViewRegistry(webViewSessionTTL, deps.Inline), loginTokens: newLoginTokenRegistry(), botAPIUpdates: newBotAPIUpdateNotifier(), tempKeyResolveCache: newTempKeyResolveCache(cfg.TempKeyResolveCacheMaxEntries), storyProjectionCache: newStoryProjectionCache(clk.Now), storyPinnedCache: newStoryPinnedAvailableCache(clk.Now), storyPinnedListCache: newStoryPinnedStoriesCache(clk.Now), channelFullBotCache: newChannelFullBotInfoCache(clk.Now), userFullProjectionCache: newUserFullProjectionCache(clk.Now), peerSettingsProjectionCache: newPeerSettingsProjectionCache(clk.Now), channelFullProjectionCache: newChannelFullProjectionCache(clk.Now), emojiStickers: newEmojiStickerIndex(clk.Now), notifySettings: newNotifySettingsCache(clk.Now), stickerCatalog: newStickerCatalogCache(clk.Now), accountSettings: newAccountSettingsCache(clk.Now), accountFreezeWake: make(chan struct{}, 1), instanceID: instanceID} r.channelFanout = newChannelFanoutDispatcher(r, defaultChannelFanoutShards, defaultChannelFanoutBuffer) r.botAPIEnqueueQueue = newBotAPIEnqueueDispatcher(log, defaultBotAPIEnqueueBuffer) r.webPageResolveSem = make(chan struct{}, webPageResolveConcurrency) diff --git a/internal/store/postgres/admin.go b/internal/store/postgres/admin.go index 497754d6..f4641685 100644 --- a/internal/store/postgres/admin.go +++ b/internal/store/postgres/admin.go @@ -168,7 +168,7 @@ func scanAdminCommand(row pgx.Row) (domain.AdminCommand, error) { func (s *AdminStore) GetAccountFreeze(ctx context.Context, userID int64) (domain.AccountFreeze, bool, error) { row := s.db.QueryRow(ctx, ` -SELECT user_id, frozen, frozen_since, frozen_until, appeal_url, reason, actor, command_id, updated_at +SELECT user_id, frozen, version, frozen_since, frozen_until, appeal_url, reason, actor, command_id, updated_at FROM account_restrictions WHERE user_id = $1`, userID) r, err := scanAccountFreeze(row) @@ -181,13 +181,80 @@ WHERE user_id = $1`, userID) return r, true, nil } +func (s *AdminStore) GetAccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error) { + out := make(map[int64]domain.AccountFreeze) + if s == nil || s.db == nil || len(userIDs) == 0 { + return out, nil + } + rows, err := s.db.Query(ctx, ` +SELECT user_id, frozen, version, frozen_since, frozen_until, appeal_url, reason, actor, command_id, updated_at +FROM account_restrictions +WHERE user_id = ANY($1::bigint[]) AND frozen = true`, userIDs) + if err != nil { + return nil, fmt.Errorf("get account freezes: %w", err) + } + defer rows.Close() + for rows.Next() { + freeze, err := scanAccountFreeze(rows) + if err != nil { + return nil, fmt.Errorf("scan account freeze: %w", err) + } + out[freeze.UserID] = freeze + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate account freezes: %w", err) + } + return out, nil +} + func (s *AdminStore) SetAccountFreeze(ctx context.Context, freeze domain.AccountFreeze) (domain.AccountFreeze, error) { + beginner, ok := s.db.(txBeginner) + if !ok { + return setAccountFreezeRow(ctx, s.db, freeze) + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.AccountFreeze{}, fmt.Errorf("begin set account freeze: %w", err) + } + committed := false + defer func() { + if !committed { + _ = tx.Rollback(ctx) + } + }() + out, err := setAccountFreezeRow(ctx, tx, freeze) + if err != nil { + return domain.AccountFreeze{}, err + } + if err := enqueueAccountFreezeNotifications(ctx, tx, out); err != nil { + return domain.AccountFreeze{}, err + } + // User visibility participates in the same cache/version invalidation spine + // as profile and dialog changes. These functions emit cross-instance NOTIFY + // events only after the surrounding transaction commits. + if _, err := tx.Exec(ctx, `SELECT telesrv_bump_contact_accounts_for_user($1)`, out.UserID); err != nil { + return domain.AccountFreeze{}, fmt.Errorf("bump frozen user contact projections: %w", err) + } + if _, err := tx.Exec(ctx, `SELECT telesrv_bump_private_dialog_light_for_user($1)`, out.UserID); err != nil { + return domain.AccountFreeze{}, fmt.Errorf("bump frozen user dialog projections: %w", err) + } + if _, err := tx.Exec(ctx, `SELECT telesrv_bump_read_model_version('user_visibility', 0, 'user', $1)`, out.UserID); err != nil { + return domain.AccountFreeze{}, fmt.Errorf("bump frozen user visibility: %w", err) + } + if err := tx.Commit(ctx); err != nil { + return domain.AccountFreeze{}, fmt.Errorf("commit set account freeze: %w", err) + } + committed = true + return out, nil +} + +func setAccountFreezeRow(ctx context.Context, db sqlcgen.DBTX, freeze domain.AccountFreeze) (domain.AccountFreeze, error) { var since, until any if freeze.Frozen { since = freeze.Since until = freeze.Until } - row := s.db.QueryRow(ctx, ` + row := db.QueryRow(ctx, ` INSERT INTO account_restrictions ( user_id, frozen, frozen_since, frozen_until, appeal_url, reason, actor, command_id, updated_at ) @@ -200,8 +267,9 @@ ON CONFLICT (user_id) DO UPDATE SET reason = EXCLUDED.reason, actor = EXCLUDED.actor, command_id = EXCLUDED.command_id, + version = account_restrictions.version + 1, updated_at = now() -RETURNING user_id, frozen, frozen_since, frozen_until, appeal_url, reason, actor, command_id, updated_at`, +RETURNING user_id, frozen, version, frozen_since, frozen_until, appeal_url, reason, actor, command_id, updated_at`, freeze.UserID, freeze.Frozen, since, until, freeze.AppealURL, freeze.Reason, freeze.Actor, freeze.CommandID, ) out, err := scanAccountFreeze(row) @@ -211,12 +279,16 @@ RETURNING user_id, frozen, frozen_since, frozen_until, appeal_url, reason, actor return out, nil } -func scanAccountFreeze(row pgx.Row) (domain.AccountFreeze, error) { +type accountFreezeScanner interface { + Scan(dest ...any) error +} + +func scanAccountFreeze(row accountFreezeScanner) (domain.AccountFreeze, error) { var r domain.AccountFreeze var since, until pgtype.Timestamptz var updated time.Time if err := row.Scan( - &r.UserID, &r.Frozen, &since, &until, &r.AppealURL, + &r.UserID, &r.Frozen, &r.Version, &since, &until, &r.AppealURL, &r.Reason, &r.Actor, &r.CommandID, &updated, ); err != nil { return domain.AccountFreeze{}, err @@ -230,3 +302,84 @@ func scanAccountFreeze(row pgx.Row) (domain.AccountFreeze, error) { r.UpdatedAt = updated return r, nil } + +func enqueueAccountFreezeNotifications(ctx context.Context, tx pgx.Tx, freeze domain.AccountFreeze) error { + const maxAccountFreezeNotificationAudience = 4096 + _, err := tx.Exec(ctx, ` +INSERT INTO account_freeze_notifications (target_user_id, frozen_user_id, version, frozen) +SELECT audience.user_id, $1, $2, $3 +FROM ( + SELECT user_id + FROM ( + SELECT contact_user_id AS user_id, 0 AS priority, 0 AS activity + FROM contacts WHERE user_id = $1 + UNION ALL + SELECT user_id, 0, 0 FROM contacts WHERE contact_user_id = $1 + UNION ALL + SELECT peer_id, 1, top_message_date + FROM dialogs WHERE user_id = $1 AND peer_type = 'user' + UNION ALL + SELECT user_id, 1, top_message_date + FROM dialogs WHERE peer_type = 'user' AND peer_id = $1 + ) candidates + GROUP BY user_id + ORDER BY min(priority), max(activity) DESC, user_id + LIMIT $4 +) audience +JOIN users u ON u.id = audience.user_id +WHERE audience.user_id <> $1 AND u.deleted_at IS NULL +ON CONFLICT (target_user_id, frozen_user_id) DO UPDATE SET + version = EXCLUDED.version, + frozen = EXCLUDED.frozen, + status = 'pending', + attempts = 0, + next_attempt_at = now(), + lease_until = NULL, + last_error = '', + updated_at = now()`, freeze.UserID, freeze.Version, freeze.Frozen, maxAccountFreezeNotificationAudience) + if err != nil { + return fmt.Errorf("enqueue account freeze notifications: %w", err) + } + return nil +} + +func (s *AdminStore) ClaimAccountFreezeNotifications(ctx context.Context, now time.Time, limit int, lease time.Duration) ([]domain.AccountFreezeNotification, error) { + if s == nil || s.db == nil || limit <= 0 || lease <= 0 { + return nil, nil + } + rows, err := s.db.Query(ctx, ` +WITH claim AS ( + SELECT id FROM account_freeze_notifications + WHERE (status = 'pending' AND next_attempt_at <= $1) + OR (status = 'dispatching' AND lease_until <= $1) + ORDER BY next_attempt_at, id FOR UPDATE SKIP LOCKED LIMIT $2 +) +UPDATE account_freeze_notifications n +SET status = 'dispatching', attempts = attempts + 1, lease_until = $3, updated_at = $1 +FROM claim WHERE n.id = claim.id +RETURNING n.id, n.target_user_id, n.frozen_user_id, n.version, n.frozen, n.attempts`, now, limit, now.Add(lease)) + if err != nil { + return nil, fmt.Errorf("claim account freeze notifications: %w", err) + } + defer rows.Close() + out := make([]domain.AccountFreezeNotification, 0) + for rows.Next() { + var n domain.AccountFreezeNotification + if err := rows.Scan(&n.ID, &n.TargetUserID, &n.FrozenUserID, &n.Version, &n.Frozen, &n.Attempts); err != nil { + return nil, fmt.Errorf("scan account freeze notification: %w", err) + } + out = append(out, n) + } + return out, rows.Err() +} + +func (s *AdminStore) CompleteAccountFreezeNotification(ctx context.Context, id, version int64, now time.Time) error { + _, err := s.db.Exec(ctx, ` +UPDATE account_freeze_notifications +SET status = 'delivered', lease_until = NULL, last_error = '', updated_at = $3 +WHERE id = $1 AND version = $2`, id, version, now) + if err != nil { + return fmt.Errorf("complete account freeze notification: %w", err) + } + return nil +} diff --git a/internal/store/postgres/admin_freeze_integration_test.go b/internal/store/postgres/admin_freeze_integration_test.go index b49fac19..299cfbec 100644 --- a/internal/store/postgres/admin_freeze_integration_test.go +++ b/internal/store/postgres/admin_freeze_integration_test.go @@ -33,11 +33,12 @@ func TestAccountFreezeMigrationAndStoreRoundTrip(t *testing.T) { const ( frozenUserID = int64(1999999881) activeUserID = int64(1999999882) + observerID = int64(1999999883) ) for _, user := range []struct { id int64 phone string - }{{frozenUserID, "1999999881"}, {activeUserID, "1999999882"}} { + }{{frozenUserID, "1999999881"}, {activeUserID, "1999999882"}, {observerID, "1999999883"}} { if _, err := tx.Exec(ctx, ` INSERT INTO users (id, access_hash, phone, first_name) VALUES ($1, $1, $2, 'Freeze migration test')`, user.id, user.phone); err != nil { @@ -60,10 +61,32 @@ VALUES ($1, true, 'legacy freeze', 'ops', 'legacy-freeze', $2)`, frozenUserID, l t.Fatalf("GetAccountFreeze migrated = %+v found=%v err=%v", migrated, found, err) } if !migrated.Frozen || !migrated.Since.Equal(legacyUpdatedAt) || - !migrated.Until.Equal(legacyUpdatedAt.Add(7*24*time.Hour)) || migrated.AppealURL != "https://t.me/SpamBot" { + !migrated.Until.Equal(legacyUpdatedAt.Add(7*24*time.Hour)) || migrated.AppealURL != "https://t.me/SpamBot" || migrated.Version != 1 { t.Fatalf("migrated freeze = %+v", migrated) } + if _, err := tx.Exec(ctx, ` +INSERT INTO contacts (user_id, contact_user_id, contact_first_name) +VALUES ($1, $2, 'Visible frozen peer')`, observerID, activeUserID); err != nil { + t.Fatalf("insert observer contact: %v", err) + } + if _, err := tx.Exec(ctx, ` +INSERT INTO dialogs (user_id, peer_type, peer_id, top_message_id, top_message_date) +VALUES ($1, 'user', $2, 1, 100)`, observerID, activeUserID); err != nil { + t.Fatalf("insert observer dialog: %v", err) + } + var contactVersionBefore, dialogVersionBefore int64 + if err := tx.QueryRow(ctx, ` +SELECT version FROM read_model_versions +WHERE model = 'contact_account' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $1`, observerID).Scan(&contactVersionBefore); err != nil { + t.Fatalf("read initial contact projection version: %v", err) + } + if err := tx.QueryRow(ctx, ` +SELECT version FROM read_model_versions +WHERE model = 'dialog_light' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $2`, observerID, activeUserID).Scan(&dialogVersionBefore); err != nil { + t.Fatalf("read initial dialog projection version: %v", err) + } + since := time.Date(2026, 7, 15, 2, 0, 0, 0, time.UTC) want := domain.AccountFreeze{ UserID: activeUserID, @@ -75,21 +98,80 @@ VALUES ($1, true, 'legacy freeze', 'ops', 'legacy-freeze', $2)`, frozenUserID, l Actor: "ops", CommandID: "freeze-round-trip", } - if _, err := store.SetAccountFreeze(ctx, want); err != nil { + updated, err := store.SetAccountFreeze(ctx, want) + if err != nil { t.Fatalf("SetAccountFreeze active: %v", err) } + if updated.Version != 1 { + t.Fatalf("first freeze version = %d, want 1", updated.Version) + } got, found, err := store.GetAccountFreeze(ctx, activeUserID) if err != nil || !found || !got.Frozen || !got.Since.Equal(want.Since) || - !got.Until.Equal(want.Until) || got.AppealURL != want.AppealURL { + !got.Until.Equal(want.Until) || got.AppealURL != want.AppealURL || got.Version != 1 { t.Fatalf("active round trip = %+v found=%v err=%v", got, found, err) } - if _, err := store.SetAccountFreeze(ctx, domain.AccountFreeze{ + var contactVersionAfter, dialogVersionAfter, visibilityVersion int64 + if err := tx.QueryRow(ctx, ` +SELECT version FROM read_model_versions +WHERE model = 'contact_account' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $1`, observerID).Scan(&contactVersionAfter); err != nil { + t.Fatalf("read frozen contact projection version: %v", err) + } + if err := tx.QueryRow(ctx, ` +SELECT version FROM read_model_versions +WHERE model = 'dialog_light' AND owner_user_id = $1 AND peer_type = 'user' AND peer_id = $2`, observerID, activeUserID).Scan(&dialogVersionAfter); err != nil { + t.Fatalf("read frozen dialog projection version: %v", err) + } + if contactVersionAfter <= contactVersionBefore || dialogVersionAfter <= dialogVersionBefore { + t.Fatalf("projection versions contact %d->%d dialog %d->%d, want increments", + contactVersionBefore, contactVersionAfter, dialogVersionBefore, dialogVersionAfter) + } + if err := tx.QueryRow(ctx, ` +SELECT version FROM read_model_versions +WHERE model = 'user_visibility' AND owner_user_id = 0 AND peer_type = 'user' AND peer_id = $1`, activeUserID).Scan(&visibilityVersion); err != nil || visibilityVersion != 1 { + t.Fatalf("user visibility version = %d err=%v, want 1", visibilityVersion, err) + } + + claimAt := time.Now().UTC().Add(time.Minute) + claimed, err := store.ClaimAccountFreezeNotifications(ctx, claimAt, 10, time.Minute) + if err != nil || len(claimed) != 1 { + t.Fatalf("claim frozen notification = %+v err=%v, want one", claimed, err) + } + oldNotification := claimed[0] + if oldNotification.TargetUserID != observerID || oldNotification.FrozenUserID != activeUserID || !oldNotification.Frozen || oldNotification.Version != 1 { + t.Fatalf("frozen notification = %+v", oldNotification) + } + + updated, err = store.SetAccountFreeze(ctx, domain.AccountFreeze{ UserID: activeUserID, Reason: "appeal accepted", Actor: "ops", CommandID: "unfreeze-round-trip", - }); err != nil { + }) + if err != nil { t.Fatalf("SetAccountFreeze inactive: %v", err) } + if updated.Version != 2 { + t.Fatalf("unfreeze version = %d, want 2", updated.Version) + } + // A worker that claimed v1 before the unfreeze cannot acknowledge the + // coalesced v2 row and suppress its online refresh. + if err := store.CompleteAccountFreezeNotification(ctx, oldNotification.ID, oldNotification.Version, claimAt); err != nil { + t.Fatalf("complete stale notification: %v", err) + } + claimed, err = store.ClaimAccountFreezeNotifications(ctx, claimAt.Add(time.Minute), 10, time.Minute) + if err != nil || len(claimed) != 1 { + t.Fatalf("claim unfreeze notification = %+v err=%v, want one", claimed, err) + } + newNotification := claimed[0] + if newNotification.ID != oldNotification.ID || newNotification.Version != 2 || newNotification.Frozen { + t.Fatalf("coalesced unfreeze notification = %+v, previous=%+v", newNotification, oldNotification) + } + if err := store.CompleteAccountFreezeNotification(ctx, newNotification.ID, newNotification.Version, claimAt.Add(2*time.Minute)); err != nil { + t.Fatalf("complete unfreeze notification: %v", err) + } + var notificationStatus string + if err := tx.QueryRow(ctx, `SELECT status FROM account_freeze_notifications WHERE id = $1`, newNotification.ID).Scan(¬ificationStatus); err != nil || notificationStatus != "delivered" { + t.Fatalf("notification status = %q err=%v, want delivered", notificationStatus, err) + } got, found, err = store.GetAccountFreeze(ctx, activeUserID) - if err != nil || !found || got.Frozen || !got.Since.IsZero() || !got.Until.IsZero() || got.AppealURL != "" { + if err != nil || !found || got.Frozen || !got.Since.IsZero() || !got.Until.IsZero() || got.AppealURL != "" || got.Version != 2 { t.Fatalf("inactive round trip = %+v found=%v err=%v", got, found, err) } diff --git a/internal/store/postgres/read_model_listener.go b/internal/store/postgres/read_model_listener.go index 0bf27c2a..0a4b0cbf 100644 --- a/internal/store/postgres/read_model_listener.go +++ b/internal/store/postgres/read_model_listener.go @@ -348,6 +348,15 @@ func (l *ReadModelChangeListener) handlePayload(payload string) { l.caches.BotProfiles.InvalidateBotProfileReadModel(evt.PeerID) } } + case "user_visibility": + if evt.PeerType == "user" && evt.PeerID != 0 { + if l.caches.RPCProjections != nil { + l.caches.RPCProjections.InvalidateRPCProjectionReadModelForUser(evt.PeerID) + } + if l.caches.Stories != nil { + l.caches.Stories.InvalidateStoryReadModelPeer(domain.Peer{Type: domain.PeerTypeUser, ID: evt.PeerID}) + } + } case "bot_full": // bot 资料(name/about/description/commands/menu_button)变更经 bot_info_version // bump 触发(迁移 0013)。channelFullBotInfoCache 按 (viewer,channel) 键、无法按 botID diff --git a/internal/store/postgres/story_peer_read_model_test.go b/internal/store/postgres/story_peer_read_model_test.go index de7af269..4e1adb18 100644 --- a/internal/store/postgres/story_peer_read_model_test.go +++ b/internal/store/postgres/story_peer_read_model_test.go @@ -17,6 +17,19 @@ type fakeStoryReadModelCache struct { flushes int } +type fakeRPCProjectionReadModelCache struct { + users []int64 +} + +func (*fakeRPCProjectionReadModelCache) InvalidateRPCProjectionReadModelForViewer(int64) {} +func (f *fakeRPCProjectionReadModelCache) InvalidateRPCProjectionReadModelForUser(id int64) { + f.users = append(f.users, id) +} +func (*fakeRPCProjectionReadModelCache) InvalidateRPCProjectionReadModelForPeer(int64, domain.Peer) { +} +func (*fakeRPCProjectionReadModelCache) InvalidateRPCProjectionReadModelForChannel(int64) {} +func (*fakeRPCProjectionReadModelCache) FlushRPCProjectionReadModel() {} + func (f *fakeStoryReadModelCache) InvalidateStoryReadModelViewers(ids ...int64) { f.mu.Lock() defer f.mu.Unlock() @@ -78,6 +91,29 @@ func TestReadModelChangeListenerRoutesStoryPeer(t *testing.T) { } } +func TestReadModelChangeListenerRoutesUserVisibility(t *testing.T) { + stories := &fakeStoryReadModelCache{} + rpcProjections := &fakeRPCProjectionReadModelCache{} + listener := NewReadModelChangeListener("", ReadModelCacheSet{ + Stories: stories, + RPCProjections: rpcProjections, + }, nil) + + listener.handlePayload(`{"model":"user_visibility","owner_user_id":0,"peer_type":"user","peer_id":777,"version":2}`) + if len(rpcProjections.users) != 1 || rpcProjections.users[0] != 777 { + t.Fatalf("RPC projection invalidations = %v, want [777]", rpcProjections.users) + } + if peers := stories.peersSnapshot(); len(peers) != 1 || peers[0] != (domain.Peer{Type: domain.PeerTypeUser, ID: 777}) { + t.Fatalf("story projection invalidations = %+v, want user 777", peers) + } + + listener.handlePayload(`{"model":"user_visibility","owner_user_id":0,"peer_type":"channel","peer_id":888,"version":3}`) + listener.handlePayload(`{"model":"user_visibility","owner_user_id":0,"peer_type":"user","peer_id":0,"version":4}`) + if len(rpcProjections.users) != 1 || len(stories.peersSnapshot()) != 1 { + t.Fatalf("invalid visibility events were not ignored: users=%v peers=%+v", rpcProjections.users, stories.peersSnapshot()) + } +} + // TestStoryPeerReadModelNotifyInvalidatesOnStoryWrite 验证 0135 触发器:写 stories / // story_hidden_peers → story_peer bump → 统一 read-model NOTIFY → 按 owner peer 失效故事投影。 func TestStoryPeerReadModelNotifyInvalidatesOnStoryWrite(t *testing.T) { From de233a4c18451ecf4abedeb8451b0833669fb841 Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 13:06:49 +0800 Subject: [PATCH 13/28] fix: sync collectible upgrade preview pools --- .../{index-D9dH2J7N.js => index-Duge82ST.js} | 4 +- cmd/telesrv-admin/web/dist/index.html | 2 +- cmd/telesrv-admin/web/src/i18n.tsx | 18 ++- .../web/src/pages/GiftCollectiblesModal.tsx | 51 ++++-- ...32_star_gift_upgrade_preview_pool.down.sql | 10 ++ ...0132_star_gift_upgrade_preview_pool.up.sql | 88 ++++++++++ internal/admin/service_test.go | 92 ++++++++--- internal/adminapi/server_test.go | 11 +- internal/app/stargifts/animation_test.go | 26 +-- internal/domain/star_gift.go | 64 +++++++- internal/domain/star_gift_collectible_test.go | 57 ++++++- internal/rpc/payments_star_gifts_rpc_test.go | 28 +++- ...star_gift_collectibles_integration_test.go | 150 +++++++++++++----- .../star_gift_lifecycle_integration_test.go | 52 ++++-- ...ft_lifecycle_migration_integration_test.go | 4 +- ...r_gift_official_import_integration_test.go | 31 +++- 16 files changed, 552 insertions(+), 136 deletions(-) rename cmd/telesrv-admin/web/dist/assets/{index-D9dH2J7N.js => index-Duge82ST.js} (71%) create mode 100644 deploy/migrations/0132_star_gift_upgrade_preview_pool.down.sql create mode 100644 deploy/migrations/0132_star_gift_upgrade_preview_pool.up.sql diff --git a/cmd/telesrv-admin/web/dist/assets/index-D9dH2J7N.js b/cmd/telesrv-admin/web/dist/assets/index-Duge82ST.js similarity index 71% rename from cmd/telesrv-admin/web/dist/assets/index-D9dH2J7N.js rename to cmd/telesrv-admin/web/dist/assets/index-Duge82ST.js index c42628b3..3c2389cf 100644 --- a/cmd/telesrv-admin/web/dist/assets/index-D9dH2J7N.js +++ b/cmd/telesrv-admin/web/dist/assets/index-Duge82ST.js @@ -5,5 +5,5 @@ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r= `+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 V(e,t){var n=t.checked;return R({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function fe(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 pe(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function me(e,t){pe(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`)?ge(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ge(e,t.type,ce(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function he(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 ge(e,t,n){(t!==`number`||B(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var _e=Array.isArray;function ve(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Ce.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(_e(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`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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`}]]),V=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`}]]),fe=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`}]]),pe=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`}]]),me=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`}]]),he=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`}]]),ge=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`}]]),_e=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`}]]),ve=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),ye=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),H=o(((e,t)=>{t.exports=ye()}))(),U=`telesrv.admin.lang`,be={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":`中文`,"language.ru":`RU`,"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.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and 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":`Permille values are relative regular-upgrade weights; their total does not need to equal 1000.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`中文`,"language.ru":`RU`,"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.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和稀有度总和,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`Permille 是普通升级的相对权重,不要求每类合计正好为 1000。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка...`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтвержден`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.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":`中文`,"language.ru":`RU`,"login.heading":`Панель администратора`,"login.body":`Введите учетные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход...`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, премиум, верификация, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, количество участников, статус верификации.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтвержден`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звезд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество звёзд`,"account.starsAmountAria":`Указать количество начисляемых звёзд`,"account.grantStars":`Начислить звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновленные`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждено`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Указать и удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звездных подарков`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звездного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звездах`,"gifts.convertStars":`Звезд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звездные подарки еще не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и итоговые показатели редкости перед тем, как версия станет активной.`,"collectibles.upgradeStars":`Цена улучшения в Звездах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Разлогинить все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтвержденные`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Запустить тестовый запуск снова`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},xe=(0,g.createContext)(null);function Se({children:e}){let[t,n]=(0,g.useState)(()=>Ee());(0,g.useEffect)(()=>{try{localStorage.setItem(U,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Te(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Te(t,e,n)}),[t]);return(0,H.jsx)(xe.Provider,{value:r,children:e})}function Ce(){let e=(0,g.useContext)(xe);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function we(){let{lang:e,setLang:t,t:n}=Ce();return(0,H.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,H.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Te(e,t,n){let r=be[e][t]??be.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(U));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`:t===`ru`||t.startsWith(`ru-`)?`ru`: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,H.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Me(){let{t:e}=Ce();return(0,H.jsxs)(`div`,{className:`boot-screen`,children:[(0,H.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`loader-bar`})]})}function Ne({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=Ce(),o=t.path.startsWith(`/messages`),[s,c]=(0,g.useState)(o);(0,g.useEffect)(()=>{o&&c(!0)},[o]);async function l(){await x.logout().catch(()=>void 0),r()}return(0,H.jsxs)(`div`,{className:`shell`,children:[(0,H.jsxs)(`aside`,{className:`sidebar`,children:[(0,H.jsxs)(je,{className:`brand`,href:`/`,navigate:n,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,H.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,H.jsx)(Pe,{icon:(0,H.jsx)(oe,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(_e,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(fe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(re,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,H.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,H.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,H.jsx)(ce,{size:16}),(0,H.jsx)(`span`,{children:a(`layout.messages`)}),(0,H.jsx)(F,{className:`nav-section-chevron`,size:15})]}),s&&(0,H.jsxs)(`div`,{className:`nav-children`,children:[(0,H.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,H.jsx)(Pe,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,H.jsxs)(`div`,{className:`sidebar-status`,children:[(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(V,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,H.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(ee,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,H.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(pe,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,H.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,H.jsxs)(`div`,{className:`workspace`,children:[(0,H.jsxs)(`header`,{className:`topbar`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:Ae(t.path,a)}),(0,H.jsx)(`h1`,{children:ke(t.path,a)})]}),(0,H.jsxs)(`div`,{className:`topbar-actions`,children:[(0,H.jsx)(we,{}),(0,H.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,H.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,H.jsx)(se,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,H.jsx)(`main`,{className:`content`,children:i})]})]})}function Pe({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,H.jsxs)(je,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,H.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,H.jsx)(`span`,{children:i})]})}function 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,H.jsxs)(`div`,{className:`page-frame`,children:[(0,H.jsxs)(`div`,{className:`page-title-row`,children:[(0,H.jsxs)(`div`,{children:[t&&(0,H.jsx)(`div`,{className:`eyebrow`,children:t}),(0,H.jsx)(`h2`,{children:e})]}),r&&(0,H.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ue({children:e}){return(0,H.jsx)(`div`,{className:`query-panel`,children:e})}function We({main:e,side:t}){return(0,H.jsxs)(`div`,{className:`split-layout`,children:[(0,H.jsx)(`div`,{className:`split-main`,children:e}),(0,H.jsx)(`aside`,{className:`split-side`,children:t})]})}function Ge({title:e,text:t,action:n}){return(0,H.jsxs)(`div`,{className:`section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`h2`,{children:e}),t&&(0,H.jsx)(`p`,{children:t})]}),n&&(0,H.jsx)(`div`,{className:`section-action`,children:n})]})}function Ke({children:e}){return(0,H.jsxs)(`div`,{className:`alert`,children:[(0,H.jsx)(O,{size:16}),` `,(0,H.jsx)(`span`,{children:e})]})}function G({children:e,tone:t=`neutral`}){return(0,H.jsx)(`span`,{className:`badge ${t}`,children:e})}function K({label:e,value:t,tone:n}){return(0,H.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{children:t})]})}function q({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,H.jsxs)(`div`,{className:`metric ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function J({label:e,value:t,mono:n=!1}){return(0,H.jsxs)(`div`,{className:`summary-item`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function qe({rows:e}){let{t}=Ce();return(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`audit.id`)}),(0,H.jsx)(`th`,{children:t(`audit.commandID`)}),(0,H.jsx)(`th`,{children:t(`audit.action`)}),(0,H.jsx)(`th`,{children:t(`audit.actor`)}),(0,H.jsx)(`th`,{children:t(`audit.status`)}),(0,H.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,H.jsx)(`th`,{children:t(`audit.reason`)}),(0,H.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[e.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,H.jsx)(`td`,{children:e.Action}),(0,H.jsx)(`td`,{children:e.Actor}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,H.jsx)(`td`,{children:ze(e.CreatedAt)})]},e.ID)),e.length===0&&(0,H.jsx)(Je,{colSpan:8})]})]})})}function Je({colSpan:e}){let{t}=Ce();return(0,H.jsx)(`tr`,{children:(0,H.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function Ye({label:e}){return(0,H.jsx)(`section`,{className:`surface`,children:(0,H.jsx)(`div`,{className:`loading-line`,children:e})})}function Xe({value:e}){return(0,H.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function Ze({onLogin:e}){let{t}=Ce(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1);async function c(t){t.preventDefault(),s(!0),a(``);try{e((await x.login(n)).actor)}catch(e){a(b(e))}finally{s(!1)}}return(0,H.jsx)(`main`,{className:`login-page`,children:(0,H.jsxs)(`section`,{className:`login-panel`,children:[(0,H.jsxs)(`div`,{className:`login-head`,children:[(0,H.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,H.jsxs)(`div`,{className:`login-head-actions`,children:[(0,H.jsx)(we,{}),(0,H.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,H.jsxs)(`div`,{className:`login-copy`,children:[(0,H.jsx)(`h1`,{children:t(`login.heading`)}),(0,H.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,H.jsx)(Ke,{children:i}),(0,H.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:t(`login.secret`)}),(0,H.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,H.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var Qe=m();function $e({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=Ce(),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1);function y(){d(``),p(null),h(``)}async function S(e){if(!u.trim()){h(s(`action.reasonRequired`));return}v(!0),h(``);try{let r={...n(),reason:u,confirm:e};p(await x.action(t,r)),e&&o?.()}catch(e){h(b(e))}finally{v(!1)}}let C=f?.dry_run&&!f.error,w=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,T=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:b(e)}}},[c,n]);return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,H.jsx)(`h2`,{children:e})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:s(`action.reason`)}),(0,H.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,H.jsxs)(`div`,{className:`command-preview`,children:[(0,H.jsxs)(`div`,{className:`preview-head`,children:[(0,H.jsx)(te,{size:14}),` `,s(`action.requestPreview`)]}),(0,H.jsx)(Xe,{value:JSON.stringify(T,null,2)})]}),m&&(0,H.jsx)(Ke,{children:m}),f&&(0,H.jsxs)(`div`,{className:`result-box`,children:[(0,H.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,H.jsx)(O,{size:16}):(0,H.jsx)(k,{size:16}),(0,H.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.commandID`)}),(0,H.jsx)(`strong`,{children:f.command_id})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.status`)}),(0,H.jsx)(`strong`,{children:f.status})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.dryRun`)}),(0,H.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,H.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,H.jsx)(Xe,{value:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(ue,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,H.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,H.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function et({rows:e,userID:t,onDone:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{a(new Set)},[t]);let o=(0,g.useMemo)(()=>e.filter(e=>!i.has(e.Hash)),[e,i]);function s(e){a(t=>e(t)),n()}return(0,H.jsxs)(`div`,{className:`authorization-block`,children:[(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:r(`auth.device`)}),(0,H.jsx)(`th`,{children:r(`auth.platform`)}),(0,H.jsx)(`th`,{children:r(`auth.ip`)}),(0,H.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,H.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[o.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,H.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,H.jsx)(`td`,{children:n.IP}),(0,H.jsx)(`td`,{children:ze(n.ActiveAt)}),(0,H.jsx)(`td`,{className:`device-actions-cell`,children:(0,H.jsxs)(`div`,{className:`device-actions`,children:[(0,H.jsx)($e,{label:r(`auth.revokeCurrent`),icon:(0,H.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,H.jsx)($e,{label:r(`auth.keepCurrent`),icon:(0,H.jsx)(fe,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>s(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),o.length===0&&(0,H.jsx)(Je,{colSpan:5})]})]})}),(0,H.jsx)(`div`,{className:`danger-zone`,children:(0,H.jsx)($e,{label:r(`auth.revokeAll`),icon:(0,H.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}=Ce(),[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,H.jsx)(Ke,{children:a});if(!r)return(0,H.jsx)(Ye,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,H.jsx)(He,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,H.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:Le(y)}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(y.Username)||n(`account.noUsername`),` · `,Fe(y.Phone)||n(`account.noPhone`)]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,H.jsx)(G,{tone:`good`,children:n(`account.premium`)}):(0,H.jsx)(G,{children:n(`account.notPremium`)}),r.Verified?(0,H.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(G,{children:n(`account.notVerified`)}),y.Frozen?(0,H.jsx)(G,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,H.jsx)(G,{children:n(`account.accountActive`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,H.jsx)(J,{label:n(`account.lastActive`),value:Be(r.LastSeenAt)||`-`}),(0,H.jsx)(J,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?Be(y.PremiumUntil):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,H.jsx)(J,{label:n(`common.updatedAt`),value:ze(y.UpdatedAt)||`-`}),(0,H.jsx)(J,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,H.jsx)(J,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,H.jsx)(J,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeSince`),value:r.Restriction.Since?ze(r.Restriction.Since):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeUntil`),value:r.Restriction.Until?ze(r.Restriction.Until):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.createdAt`),value:ze(y.CreatedAt)||`-`})]}),r.About&&(0,H.jsx)(`p`,{className:`about-text`,children:r.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,H.jsx)(et,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(qe,{rows:r.AuditLogs})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,H.jsx)($e,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!0,freeze_until:new Date(p).toISOString(),freeze_appeal_url:h.trim()}),onDone:v}),y.Frozen&&(0,H.jsx)($e,{label:n(`account.unfreezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)($e,{label:n(`account.setPremium`),icon:(0,H.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:W(l)}),onDone:v}),(0,H.jsx)($e,{label:n(`account.clearPremium`),icon:(0,H.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,H.jsx)($e,{label:n(`account.grantStars`),icon:(0,H.jsx)(me,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:W(d)}),onDone:v}),(0,H.jsx)($e,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]})]})})})}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}=Ce(),[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,H.jsxs)(He,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(q,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,H.jsx)(q,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,H.jsx)(q,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`account.userID`)}),(0,H.jsx)(`th`,{children:t(`account.phone`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.name`)}),(0,H.jsx)(`th`,{children:t(`common.device`)}),(0,H.jsx)(`th`,{children:t(`account.lastActive`)}),(0,H.jsx)(`th`,{children:t(`account.premium`)}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`account.frozen`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Fe(n.Phone)}),(0,H.jsx)(`td`,{children:Ie(n.Username)}),(0,H.jsx)(`td`,{children:Le(n)}),(0,H.jsx)(`td`,{children:n.DeviceCount}),(0,H.jsx)(`td`,{children:ze(n.LastActiveAt)}),(0,H.jsx)(`td`,{children:n.PremiumUntil>0?(0,H.jsxs)(G,{tone:`good`,children:[t(`account.premium`),` `,Be(n.PremiumUntil)]}):(0,H.jsx)(G,{children:t(`common.none`)})}),(0,H.jsx)(`td`,{children:n.Verified?(0,H.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(G,{children:t(`account.notVerified`)})}),(0,H.jsx)(`td`,{children:n.Frozen?(0,H.jsx)(G,{tone:`danger`,children:t(`account.frozen`)}):(0,H.jsx)(G,{children:t(`common.normal`)})}),(0,H.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Je,{colSpan:11})]})]})})]})}function ot({id:e,navigate:t}){let{t:n}=Ce(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await x.channel(e))}catch(e){o(b(e))}}if((0,g.useEffect)(()=>{s()},[e]),a)return(0,H.jsx)(Ke,{children:a});if(!r)return(0,H.jsx)(Ye,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,H.jsx)(He,{title:`${Re(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,H.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(G,{children:Re(c,n)}),c.Verified?(0,H.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(G,{children:n(`account.notVerified`)}),c.Deleted?(0,H.jsx)(G,{tone:`danger`,children:n(`common.deleted`)}):(0,H.jsx)(G,{children:n(`common.valid`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,H.jsx)(J,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,H.jsx)(J,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,H.jsx)(J,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,H.jsx)(J,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,H.jsx)(J,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,H.jsx)(J,{label:n(`account.createdAt`),value:Be(c.Date)||`-`}),(0,H.jsx)(J,{label:n(`common.updatedAt`),value:ze(c.UpdatedAt)||`-`})]}),c.About&&(0,H.jsx)(`p`,{className:`about-text`,children:c.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(qe,{rows:r.AuditLogs})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,H.jsx)(Xe,{value:r.ChannelJSON})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,H.jsx)($e,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s})]})})})}function st({navigate:e}){let{t}=Ce(),[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,H.jsxs)(He,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(q,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,H.jsx)(q,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,H.jsx)(q,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`channel.channelID`)}),(0,H.jsx)(`th`,{children:t(`channel.kind`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`channel.title`)}),(0,H.jsx)(`th`,{children:t(`common.members`)}),(0,H.jsx)(`th`,{children:t(`common.admins`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Re(n,t)}),(0,H.jsx)(`td`,{children:Ie(n.Username)}),(0,H.jsx)(`td`,{children:n.Title}),(0,H.jsx)(`td`,{children:n.ParticipantsCount}),(0,H.jsx)(`td`,{children:n.AdminsCount}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Verified?(0,H.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(G,{children:t(`account.notVerified`)})}),(0,H.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Je,{colSpan:10})]})]})})]})}function ct({navigate:e}){let{t}=Ce();return(0,H.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,H.jsxs)(`section`,{className:`overview-band`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,H.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,H.jsxs)(`div`,{className:`overview-metrics`,children:[(0,H.jsx)(K,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,H.jsx)(K,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,H.jsx)(K,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,H.jsxs)(`div`,{className:`command-grid`,children:[(0,H.jsx)(lt,{icon:(0,H.jsx)(_e,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,H.jsx)(lt,{icon:(0,H.jsx)(fe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,H.jsx)(lt,{icon:(0,H.jsx)(ce,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,H.jsxs)(`section`,{className:`work-strip`,children:[(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(k,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(ae,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(L,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(te,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function lt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,H.jsxs)(je,{className:`launcher`,href:r,navigate:i,children:[(0,H.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,H.jsxs)(`span`,{className:`launcher-copy`,children:[(0,H.jsx)(`strong`,{children:t}),(0,H.jsx)(`span`,{children:n})]}),(0,H.jsx)(I,{size:16})]})}function ut({channelID:e,msgID:t,navigate:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.groupMessage(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(Ke,{children:o});if(!i)return(0,H.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(He,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,H.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:Be(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(G,{children:r(`common.survived`)}),l.Pinned&&(0,H.jsx)(G,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,H.jsx)(G,{children:r(`messages.channelPost`)}),(0,H.jsxs)(G,{children:[`pts `,l.PTS]})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,H.jsx)(J,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,H.jsx)(J,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,H.jsx)(Xe,{value:i.MessageJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,H.jsx)(Xe,{value:i.ChannelJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.messageId`)}),(0,H.jsx)(`th`,{children:r(`common.sender`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:e.MessageID}),(0,H.jsx)(`td`,{children:e.SenderUserID}),(0,H.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Je,{colSpan:6})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.eventJson`)}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,H.jsx)(Xe,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,H.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function dt({label:e,value:t,onChange:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.accounts(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(ve,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(P,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:Le(t)}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:Ie(t.Username)||Fe(t.Phone)||`-`})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:Le(e)}),(0,H.jsx)(`span`,{children:Ie(e.Username)||Fe(e.Phone)||`-`}),e.Verified?(0,H.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(G,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function ft({label:e,value:t,onChange:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.channels(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(ve,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(P,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:t.Title||`-`}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:Ie(t.Username)||Re(t,r)})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:e.Title||`-`}),(0,H.jsx)(`span`,{children:Ie(e.Username)||Re(e,r)}),e.Verified?(0,H.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(G,{children:Re(e,r)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function pt({navigate:e}){let{t}=Ce(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`100`),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``);async function m(e=!1){if(p(``),!n){p(t(`messages.selectChannel`));return}let r=new URLSearchParams({channel_id:String(n.ID),limit:c});if(e&&u?.rows.length){let e=u.rows[u.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.ID)),a(String(e.Date)),s(String(e.ID))}else i&&r.set(`before_date`,i),o&&r.set(`before_id`,o);try{d(await x.groupMessages(r))}catch(e){p(b(e))}}function h(e){r(e),a(``),s(``),d(null)}let _=u?.rows??[];return(0,H.jsxs)(He,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(Ue,{children:[(0,H.jsx)(`div`,{className:`message-selector-grid single`,children:(0,H.jsx)(ft,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`messages.currentPage`),value:String(_.length)}),(0,H.jsx)(q,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,H.jsx)(q,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,H.jsx)(q,{label:t(`messages.channelGroup`),value:n?`${n.Title||Re(n,t)} (${n.ID})`:`-`})]}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:`From Peer`}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.views`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[_.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Be(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,H.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.ViewsCount}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,H.jsx)(G,{tone:`warn`,children:t(`messages.pinned`)}):(0,H.jsx)(G,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,H.jsx)(Je,{colSpan:9})]})]})})]})}function mt({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.message(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(Ke,{children:o});if(!i)return(0,H.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(He,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,H.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:Be(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(G,{children:r(`common.survived`)}),(0,H.jsxs)(G,{children:[`pts `,l.PTS]}),(0,H.jsx)(G,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,H.jsx)(J,{label:r(`common.time`),value:Be(l.Date)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,H.jsx)(Xe,{value:i.MessageJSON})]}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,H.jsx)(Xe,{value:i.DialogJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,H.jsx)(Xe,{value:i.PrivateJSON})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Je,{colSpan:4})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`ID`}),(0,H.jsx)(`th`,{children:r(`account.userID`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.status`)}),(0,H.jsx)(`th`,{children:r(`messages.attempts`)}),(0,H.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{children:e.TargetUserID}),(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.EventType}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.Attempts}),(0,H.jsx)(`td`,{children:ze(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,H.jsx)(Je,{colSpan:7})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,H.jsx)($e,{label:r(`messages.deleteThis`),icon:(0,H.jsx)(he,{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}=Ce(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`100`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,S]=(0,g.useState)(``),[C,w]=(0,g.useState)(`1`),[T,E]=(0,g.useState)(null),[D,O]=(0,g.useState)(``);async function k(e=!1){if(O(``),!n||!i){O(t(`messages.selectPrivatePeers`));return}let r=new URLSearchParams({owner_user_id:String(n.ID),peer_id:String(i.ID),limit:u});if(e&&T?.rows.length){let e=T.rows[T.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.BoxID)),s(String(e.Date)),l(String(e.BoxID))}else o&&r.set(`before_date`,o),c&&r.set(`before_id`,c);try{E(await x.messages(r))}catch(e){O(b(e))}}function A(e){r(e),s(``),l(``),E(null)}function j(e){a(e),s(``),l(``),E(null)}return(0,H.jsxs)(He,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,H.jsx)(Ke,{children:D}),(0,H.jsxs)(Ue,{children:[(0,H.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,H.jsx)(dt,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,H.jsx)(dt,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,H.jsx)(q,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,H.jsx)(q,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,H.jsx)(q,{label:t(`messages.ownerPeer`),value:n&&i?`${Le(n)} / ${Le(i)}`:`-`})]}),(0,H.jsxs)(`div`,{className:`operation-row`,children:[(0,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(he,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,H.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsx)($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,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(ie,{size:15}),` `,t(`messages.clearHistory`)]}),(0,H.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,H.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,H.jsx)($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,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:t(`messages.direction`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,H.jsx)(`td`,{children:Be(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,H.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):(0,H.jsx)(G,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,H.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`)},H.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)}},H.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`)))},H.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())},H.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},H.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},H.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`))}},H.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`)},H.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)},H.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),ye(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}(),xe=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Se=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=xe.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Ce=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return Se(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=Ce.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=be.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?U.searchAnimations(Je,qe,Ye):U.searchAnimations()}function Qe(e){re(e)}function $e(e){ue(e)}function et(e){return qe===!0&&(e.animationData=JSON.parse(Je)),U.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=U.play,J.pause=U.pause,J.setLocationHref=Xe,J.togglePause=U.togglePause,J.setSpeed=U.setSpeed,J.setDirection=U.setDirection,J.stop=U.stop,J.searchAnimations=Ze,J.registerAnimation=U.registerAnimation,J.loadAnimation=et,J.setSubframeRendering=Qe,J.resize=U.resize,J.goToAndStop=U.goToAndStop,J.destroy=U.destroy,J.setQuality=tt,J.inBrowser=nt,J.installPlugin=rt,J.freeze=U.freeze,J.unfreeze=U.unfreeze,J.setVolume=U.setVolume,J.mute=U.mute,J.unmute=U.unmute,J.getRegisteredAnimations=U.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=be.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+xe||!x?(T=(m+xe-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)},ge(`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,H.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,H.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,H.jsx)(xt,{data:n,compact:!0}):(0,H.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,H.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),Tt=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function Et({gift:e,onClose:t,onPublished:n}){let{t:r}=Ce(),[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: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,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,H.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(G,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n([...t,yt(e===`models`?`model`:`pattern`)]),F()},children:[(0,H.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,H.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,H.jsxs)(`label`,{className:`collectible-file`,children:[(0,H.jsx)(`span`,{children:r(`gifts.animation`)}),(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:t=>void L(e,i,t.target.files?.[0]??null)}),(0,H.jsxs)(`em`,{children:[(0,H.jsx)(R,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,H.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,H.jsx)(xt,{data:i.animation,compact:!0}):(0,H.jsx)(j,{size:16})}),(0,H.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,H.jsx)(he,{size:14})}),i.fileError&&(0,H.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,H.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,H.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,H.jsxs)(`div`,{className:`collectible-loading`,children:[(0,H.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,H.jsxs)(`section`,{className:`collectible-active`,children:[(0,H.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(ne,{size:18}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,H.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,H.jsx)(G,{tone:`good`,children:r(`collectibles.published`)})]}),(0,H.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(St,{giftID:e.GiftID,attribute:t}),(0,H.jsxs)(`div`,{children:[(0,H.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,H.jsx)(G,{children:`crafted`})]}),(0,H.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,Tt(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, #${(e.center_color??0).toString(16).padStart(6,`0`)}, #${(e.edge_color??0).toString(16).padStart(6,`0`)})`,color:`#${(e.text_color??16777215).toString(16).padStart(6,`0`)}`},children:`Aa`}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e.name}),(0,H.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,Tt(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,H.jsxs)(`div`,{className:`collectible-empty`,children:[(0,H.jsx)(ne,{size:22}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,H.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,H.jsxs)(`section`,{className:`collectible-definition`,children:[(0,H.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,H.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.reason`)}),(0,H.jsx)(`input`,{value:C,maxLength:1e3,placeholder:r(`gifts.reasonPlaceholder`),onChange:e=>w(e.target.value)})]})]}),ie(`models`,T,E),ie(`patterns`,D,O),(0,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,H.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(G,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N([...M,bt()]),F()},children:[(0,H.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,H.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:e.name,maxLength:128,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,name:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:e.rarity,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,rarity:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:e.sortOrder,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,sortOrder:t.target.value}:n)),F()}})]}),[`center`,`edge`,`pattern`,`text`].map(t=>(0,H.jsxs)(`label`,{className:`collectible-color`,children:[(0,H.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,H.jsx)(`input`,{type:`color`,value:e[t],onChange:n=>{N(M.map(r=>r.key===e.key?{...r,[t]:n.target.value}:r)),F()}})]},t)),(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length===1,onClick:()=>{N(M.filter(t=>t.key!==e.key)),F()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(he,{size:14})})]},e.key))})]})]}),u&&(0,H.jsx)(Ke,{children:u}),f&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,H.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:te,disabled:c,children:[c?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(fe,{size:15}),r(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:re,disabled:c||!f,children:[(0,H.jsx)(ge,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function Dt(e){return e.model_count+e.pattern_count+e.backdrop_count}function Ot(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function kt({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,H.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,H.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,H.jsx)(`span`,{children:s})}),(0,H.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,H.jsx)(le,{size:14}):(0,H.jsx)(ue,{size:14})})]})}function At({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=gt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,H.jsx)(`div`,{className:`gift-animation-shell`,children:(0,H.jsx)(`div`,{className:`gift-animation`,ref:t})})}function jt(){let{t:e}=Ce(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,te]=(0,g.useState)(`50`),[re,ie]=(0,g.useState)(`50`),[ae,oe]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[V,pe]=(0,g.useState)(null),[me,he]=(0,g.useState)(!1),[_e,ye]=(0,g.useState)(``),[U,be]=(0,g.useState)(``);async function xe(){ye(``);try{n((await x.gifts()).Gifts??[])}catch(e){ye(b(e))}}(0,g.useEffect)(()=>{xe()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>be(b(e)))},[a,d,p.length]);let Se=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),we=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),Te=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),Ee=(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 De(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae)})),r.set(`file`,l,l.name),r}function Oe(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function ke(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),te(String(t.stars)),ie(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),pe(null)}async function Ae(){he(!0),be(``),pe(null);try{pe(d===`official`?await x.importOfficialGift(Oe(!1)):await x.importGift(De(!1)))}catch(e){be(b(e))}finally{he(!1)}}async function je(){if(V){he(!0),be(``);try{d===`official`?await x.importOfficialGift(Oe(!0,V.command_id)):await x.importGift(De(!0,V.command_id)),pe(null),u(null),F(`0`),L(``),C(``),await xe(),o(!1)}catch(e){be(b(e))}finally{he(!1)}}}function Me(){F(`0`),L(``),te(`50`),ie(`50`),oe(`0`),ce(!0),ue(``),u(null),pe(null),be(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Ne(e){F(e.GiftID),L(e.Title),te(String(e.Stars)),ie(String(e.ConvertStars)),oe(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),pe(null),be(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,H.jsxs)(He,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>xe(),disabled:me,children:[(0,H.jsx)(de,{size:15}),` `,e(`common.refresh`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Me,children:[(0,H.jsx)(z,{size:15}),` `,e(`gifts.add`)]})]}),children:[_e&&(0,H.jsx)(Ke,{children:_e}),(0,H.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,H.jsx)(q,{label:e(`gifts.total`),value:String(t.length)}),(0,H.jsx)(q,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,H.jsx)(q,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,H.jsx)(q,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`div`,{className:`toolbar`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,H.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Ee.length,total:t.length})})]})}),(0,H.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:e(`gifts.animation`)}),(0,H.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,H.jsx)(`th`,{children:e(`gifts.title`)}),(0,H.jsx)(`th`,{children:e(`gifts.price`)}),(0,H.jsx)(`th`,{children:e(`gifts.source`)}),(0,H.jsx)(`th`,{children:e(`gifts.received`)}),(0,H.jsx)(`th`,{children:e(`common.status`)}),(0,H.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,H.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[Ee.map(t=>(0,H.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,H.jsx)(`td`,{children:(0,H.jsx)(kt,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,H.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,H.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,H.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(G,{children:t.SourceFormat}),(0,H.jsx)(`span`,{className:`gift-source-size`,children:Ot(t.AnimationSize)})]}),(0,H.jsx)(`td`,{children:t.ReceivedCount}),(0,H.jsx)(`td`,{children:(0,H.jsx)(G,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,H.jsx)(`td`,{children:ze(t.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,H.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,H.jsx)(ne,{size:13}),e(`collectibles.manage`)]}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Ne(t),children:e(`gifts.replace`)}),(0,H.jsx)($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 xe()})]})})]},t.GiftID)),Ee.length===0&&(0,H.jsx)(Je,{colSpan:9})]})]})}),a&&(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,H.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:me,"aria-label":e(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${V?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${V?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,H.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),pe(null)},children:e(`gifts.officialSource`)}),(0,H.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),pe(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,H.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:p.length}),(0,H.jsx)(`span`,{children:`SHA-256`})]})]}),(0,H.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,H.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:Te.length,total:p.length})})]}),(0,H.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,H.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,H.jsx)(`span`,{children:we[t]})]},t))}),(0,H.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[Te.map(t=>{let n=t.source_gift_id===S;return(0,H.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>ke(t),children:[(0,H.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,H.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,H.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,H.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,H.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:Dt(t)})})]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),Te.length===0&&(0,H.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),Se&&(0,H.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,H.jsx)(At,{sourceGiftID:Se.source_gift_id}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:Se.title||e(`gifts.officialUnnamed`,{id:Se.source_gift_id})}),(0,H.jsx)(`span`,{className:`mono`,children:Se.source_gift_id}),(0,H.jsxs)(`small`,{children:[Se.model_count,` `,e(`collectibles.models`),` · `,Se.pattern_count,` `,e(`collectibles.patterns`),` · `,Se.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:Se.can_upgrade?`yes`:`no`,children:Se.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:Se.can_craft?`craft`:`no`,children:Se.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),Se?.can_upgrade&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),pe(null)}})]})]})]})]}):(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-file-icon`,children:(0,H.jsx)(R,{size:22})}),(0,H.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,H.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,H.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,H.jsx)(`small`,{children:l?Ot(l.size):e(`gifts.fileHint`)})]}),(0,H.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.title`)}),(0,H.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.stars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{te(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:re,onChange:e=>{ie(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:ae,onChange:e=>{oe(e.target.value),pe(null)}})]})]}),(0,H.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,H.jsx)(`span`,{children:e(`gifts.reason`)}),(0,H.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),U&&(0,H.jsx)(Ke,{children:U}),V&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,H.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(V.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:me,children:e(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:Ae,disabled:me,children:[me?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(fe,{size:15}),e(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,disabled:me||!V,children:[(0,H.jsx)(ge,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,H.jsx)(Et,{gift:s,onClose:()=>c(null),onPublished:()=>void xe()})]})}function Mt({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1];return n?(0,H.jsx)(tt,{id:Number(n),navigate:t}):r?(0,H.jsx)(ot,{id:Number(r),navigate:t}):e.path===`/accounts`?(0,H.jsx)(at,{navigate:t}):e.path===`/channels`?(0,H.jsx)(st,{navigate:t}):e.path===`/gifts`?(0,H.jsx)(jt,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,H.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,H.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,H.jsx)(pt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,H.jsx)(ht,{navigate:t}):(0,H.jsx)(ct,{navigate:t})}function Y(){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,H.jsx)(Me,{}):e===null?(0,H.jsx)(Ze,{onLogin:t}):(0,H.jsx)(Ne,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,H.jsx)(Mt,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,H.jsx)(g.StrictMode,{children:(0,H.jsx)(Se,{children:(0,H.jsx)(Y,{})})})); \ No newline at end of file +`+e.stack}return{value:e,source:t,stack:i,digest:null}}function Cs(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function ws(e,t){try{console.error(t.value)}catch(e){setTimeout(function(){throw e})}}var Ts=typeof WeakMap==`function`?WeakMap:Map;function Es(e,t,n){n=Za(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){nl||(nl=!0,rl=r),ws(e,t)},n}function Ds(e,t,n){n=Za(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r==`function`){var i=t.value;n.payload=function(){return r(i)},n.callback=function(){ws(e,t)}}var a=e.stateNode;return a!==null&&typeof a.componentDidCatch==`function`&&(n.callback=function(){ws(e,t),typeof r!=`function`&&(il===null?il=new Set([this]):il.add(this));var n=t.stack;this.componentDidCatch(t.value,{componentStack:n===null?``:n})}),n}function Os(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Ts;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(i.add(n),e=zl.bind(null,e,t,n),t.then(e,e))}function ks(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t===null?!0:t.dehydrated!==null),t)return e;e=e.return}while(e!==null);return null}function As(e,t,n,r,i){return e.mode&1?(e.flags|=65536,e.lanes=i,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=Za(-1,1),t.tag=2,Qa(n,t,1))),n.lanes|=1),e)}var js=C.ReactCurrentOwner,Ms=!1;function Ns(e,t,n,r){t.child=e===null?Pa(t,null,n,r):Na(t,e.child,n,r)}function Ps(e,t,n,r,i){n=n.render;var a=t.ref;return Ha(t,i),r=ko(e,t,n,r,a,i),n=Ao(),e!==null&&!Ms?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~i,ec(e,t,i)):(_a&&n&&pa(t),t.flags|=1,Ns(e,t,r,i),t.child)}function Fs(e,t,n,r,i){if(e===null){var a=n.type;return typeof a==`function`&&!ql(a)&&a.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=a,Is(e,t,a,r,i)):(e=Xl(n.type,null,r,t,t.mode,i),e.ref=t.ref,e.return=t,t.child=e)}if(a=e.child,(e.lanes&i)===0){var o=a.memoizedProps;if(n=n.compare,n=n===null?br:n,n(o,r)&&e.ref===t.ref)return ec(e,t,i)}return t.flags|=1,e=Yl(a,r),e.ref=t.ref,e.return=t,t.child=e}function Is(e,t,n,r,i){if(e!==null){var a=e.memoizedProps;if(br(a,r)&&e.ref===t.ref)if(Ms=!1,t.pendingProps=r=a,(e.lanes&i)!==0)e.flags&131072&&(Ms=!0);else return t.lanes=e.lanes,ec(e,t,i)}return zs(e,t,n,r,i)}function Ls(e,t,n){var r=t.pendingProps,i=r.children,a=e===null?null:e.memoizedState;if(r.mode===`hidden`)if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},Ri(Gc,Wc),Wc|=n;else{if(!(n&1073741824))return e=a===null?n:a.baseLanes|n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,Ri(Gc,Wc),Wc|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=a===null?n:a.baseLanes,Ri(Gc,Wc),Wc|=r}else a===null?r=n:(r=a.baseLanes|n,t.memoizedState=null),Ri(Gc,Wc),Wc|=r;return Ns(e,t,i,n),t.child}function Rs(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function zs(e,t,n,r,i){var a=Wi(n)?Hi:Bi.current;return a=Ui(t,a),Ha(t,i),n=ko(e,t,n,r,a,i),r=Ao(),e!==null&&!Ms?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~i,ec(e,t,i)):(_a&&r&&pa(t),t.flags|=1,Ns(e,t,n,i),t.child)}function Bs(e,t,n,r,i){if(Wi(n)){var a=!0;Ji(t)}else a=!1;if(Ha(t,i),t.stateNode===null)$s(e,t),ys(t,n,r),xs(t,n,r,i),r=!0;else if(e===null){var o=t.stateNode,s=t.memoizedProps;o.props=s;var c=o.context,l=n.contextType;typeof l==`object`&&l?l=Ua(l):(l=Wi(n)?Hi:Bi.current,l=Ui(t,l));var u=n.getDerivedStateFromProps,d=typeof u==`function`||typeof o.getSnapshotBeforeUpdate==`function`;d||typeof o.UNSAFE_componentWillReceiveProps!=`function`&&typeof o.componentWillReceiveProps!=`function`||(s!==r||c!==l)&&bs(t,o,r,l),Ja=!1;var f=t.memoizedState;o.state=f,to(t,r,o,i),c=t.memoizedState,s!==r||f!==c||Vi.current||Ja?(typeof u==`function`&&(gs(t,n,u,r),c=t.memoizedState),(s=Ja||vs(t,n,s,r,f,c,l))?(d||typeof o.UNSAFE_componentWillMount!=`function`&&typeof o.componentWillMount!=`function`||(typeof o.componentWillMount==`function`&&o.componentWillMount(),typeof o.UNSAFE_componentWillMount==`function`&&o.UNSAFE_componentWillMount()),typeof o.componentDidMount==`function`&&(t.flags|=4194308)):(typeof o.componentDidMount==`function`&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=c),o.props=r,o.state=c,o.context=l,r=s):(typeof o.componentDidMount==`function`&&(t.flags|=4194308),r=!1)}else{o=t.stateNode,Xa(e,t),s=t.memoizedProps,l=t.type===t.elementType?s:hs(t.type,s),o.props=l,d=t.pendingProps,f=o.context,c=n.contextType,typeof c==`object`&&c?c=Ua(c):(c=Wi(n)?Hi:Bi.current,c=Ui(t,c));var p=n.getDerivedStateFromProps;(u=typeof p==`function`||typeof o.getSnapshotBeforeUpdate==`function`)||typeof o.UNSAFE_componentWillReceiveProps!=`function`&&typeof o.componentWillReceiveProps!=`function`||(s!==d||f!==c)&&bs(t,o,r,c),Ja=!1,f=t.memoizedState,o.state=f,to(t,r,o,i);var m=t.memoizedState;s!==d||f!==m||Vi.current||Ja?(typeof p==`function`&&(gs(t,n,p,r),m=t.memoizedState),(l=Ja||vs(t,n,l,r,f,m,c)||!1)?(u||typeof o.UNSAFE_componentWillUpdate!=`function`&&typeof o.componentWillUpdate!=`function`||(typeof o.componentWillUpdate==`function`&&o.componentWillUpdate(r,m,c),typeof o.UNSAFE_componentWillUpdate==`function`&&o.UNSAFE_componentWillUpdate(r,m,c)),typeof o.componentDidUpdate==`function`&&(t.flags|=4),typeof o.getSnapshotBeforeUpdate==`function`&&(t.flags|=1024)):(typeof o.componentDidUpdate!=`function`||s===e.memoizedProps&&f===e.memoizedState||(t.flags|=4),typeof o.getSnapshotBeforeUpdate!=`function`||s===e.memoizedProps&&f===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=m),o.props=r,o.state=m,o.context=c,r=l):(typeof o.componentDidUpdate!=`function`||s===e.memoizedProps&&f===e.memoizedState||(t.flags|=4),typeof o.getSnapshotBeforeUpdate!=`function`||s===e.memoizedProps&&f===e.memoizedState||(t.flags|=1024),r=!1)}return Vs(e,t,n,r,a,i)}function Vs(e,t,n,r,i,a){Rs(e,t);var o=(t.flags&128)!=0;if(!r&&!o)return i&&Yi(t,n,!1),ec(e,t,a);r=t.stateNode,js.current=t;var s=o&&typeof n.getDerivedStateFromError!=`function`?null:r.render();return t.flags|=1,e!==null&&o?(t.child=Na(t,e.child,null,a),t.child=Na(t,null,s,a)):Ns(e,t,s,a),t.memoizedState=r.state,i&&Yi(t,n,!0),t.child}function Hs(e){var t=e.stateNode;t.pendingContext?Ki(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Ki(e,t.context,!1),co(e,t.containerInfo)}function Us(e,t,n,r,i){return Ea(),Da(i),t.flags|=256,Ns(e,t,n,r),t.child}var Ws={dehydrated:null,treeContext:null,retryLane:0};function Gs(e){return{baseLanes:e,cachePool:null,transitions:null}}function Ks(e,t,n){var r=t.pendingProps,i=po.current,a=!1,o=(t.flags&128)!=0,s;if((s=o)||(s=e!==null&&e.memoizedState===null?!1:(i&2)!=0),s?(a=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(i|=1),Ri(po,i&1),e===null)return Sa(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?(t.mode&1?e.data===`$!`?t.lanes=8:t.lanes=1073741824:t.lanes=1,null):(o=r.children,e=r.fallback,a?(r=t.mode,a=t.child,o={mode:`hidden`,children:o},!(r&1)&&a!==null?(a.childLanes=0,a.pendingProps=o):a=Ql(o,r,0,null),e=Zl(e,r,n,null),a.return=t,e.return=t,a.sibling=e,t.child=a,t.child.memoizedState=Gs(n),t.memoizedState=Ws,e):qs(t,o));if(i=e.memoizedState,i!==null&&(s=i.dehydrated,s!==null))return Ys(e,t,o,r,s,i,n);if(a){a=r.fallback,o=t.mode,i=e.child,s=i.sibling;var c={mode:`hidden`,children:r.children};return!(o&1)&&t.child!==i?(r=t.child,r.childLanes=0,r.pendingProps=c,t.deletions=null):(r=Yl(i,c),r.subtreeFlags=i.subtreeFlags&14680064),s===null?(a=Zl(a,o,n,null),a.flags|=2):a=Yl(s,a),a.return=t,r.return=t,r.sibling=a,t.child=r,r=a,a=t.child,o=e.child.memoizedState,o=o===null?Gs(n):{baseLanes:o.baseLanes|n,cachePool:null,transitions:o.transitions},a.memoizedState=o,a.childLanes=e.childLanes&~n,t.memoizedState=Ws,r}return a=e.child,e=a.sibling,r=Yl(a,{mode:`visible`,children:r.children}),!(t.mode&1)&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function qs(e,t){return t=Ql({mode:`visible`,children:t},e.mode,0,null),t.return=e,e.child=t}function Js(e,t,n,r){return r!==null&&Da(r),Na(t,e.child,null,n),e=qs(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Ys(e,t,n,i,a,o,s){if(n)return t.flags&256?(t.flags&=-257,i=Cs(Error(r(422))),Js(e,t,s,i)):t.memoizedState===null?(o=i.fallback,a=t.mode,i=Ql({mode:`visible`,children:i.children},a,0,null),o=Zl(o,a,s,null),o.flags|=2,i.return=t,o.return=t,i.sibling=o,t.child=i,t.mode&1&&Na(t,e.child,null,s),t.child.memoizedState=Gs(s),t.memoizedState=Ws,o):(t.child=e.child,t.flags|=128,null);if(!(t.mode&1))return Js(e,t,s,null);if(a.data===`$!`){if(i=a.nextSibling&&a.nextSibling.dataset,i)var c=i.dgst;return i=c,o=Error(r(419)),i=Cs(o,i,void 0),Js(e,t,s,i)}if(c=(s&e.childLanes)!==0,Ms||c){if(i=Vc,i!==null){switch(s&-s){case 4:a=2;break;case 16:a=8;break;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:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:a=32;break;case 536870912:a=268435456;break;default:a=0}a=(a&(i.suspendedLanes|s))===0?a:0,a!==0&&a!==o.retryLane&&(o.retryLane=a,qa(e,a),ml(i,e,a,-1))}return Ol(),i=Cs(Error(r(421))),Js(e,t,s,i)}return a.data===`$?`?(t.flags|=128,t.child=e.child,t=Vl.bind(null,e),a._reactRetry=t,null):(e=o.treeContext,ga=xi(a.nextSibling),ha=t,_a=!0,va=null,e!==null&&(oa[sa++]=la,oa[sa++]=ua,oa[sa++]=ca,la=e.id,ua=e.overflow,ca=t),t=qs(t,i.children),t.flags|=4096,t)}function Xs(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),Va(e.return,t,n)}function Zs(e,t,n,r,i){var a=e.memoizedState;a===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:i}:(a.isBackwards=t,a.rendering=null,a.renderingStartTime=0,a.last=r,a.tail=n,a.tailMode=i)}function Qs(e,t,n){var r=t.pendingProps,i=r.revealOrder,a=r.tail;if(Ns(e,t,r.children,n),r=po.current,r&2)r=r&1|2,t.flags|=128;else{if(e!==null&&e.flags&128)a:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Xs(e,n,t);else if(e.tag===19)Xs(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break a;for(;e.sibling===null;){if(e.return===null||e.return===t)break a;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(Ri(po,r),!(t.mode&1))t.memoizedState=null;else switch(i){case`forwards`:for(n=t.child,i=null;n!==null;)e=n.alternate,e!==null&&mo(e)===null&&(i=n),n=n.sibling;n=i,n===null?(i=t.child,t.child=null):(i=n.sibling,n.sibling=null),Zs(t,!1,i,n,a);break;case`backwards`:for(n=null,i=t.child,t.child=null;i!==null;){if(e=i.alternate,e!==null&&mo(e)===null){t.child=i;break}e=i.sibling,i.sibling=n,n=i,i=e}Zs(t,!0,n,null,a);break;case`together`:Zs(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function $s(e,t){!(t.mode&1)&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function ec(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),Jc|=t.lanes,(n&t.childLanes)===0)return null;if(e!==null&&t.child!==e.child)throw Error(r(153));if(t.child!==null){for(e=t.child,n=Yl(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Yl(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function tc(e,t,n){switch(t.tag){case 3:Hs(t),Ea();break;case 5:uo(t);break;case 1:Wi(t.type)&&Ji(t);break;case 4:co(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,i=t.memoizedProps.value;Ri(Fa,r._currentValue),r._currentValue=i;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated===null?(n&t.child.childLanes)===0?(Ri(po,po.current&1),e=ec(e,t,n),e===null?null:e.sibling):Ks(e,t,n):(Ri(po,po.current&1),t.flags|=128,null);Ri(po,po.current&1);break;case 19:if(r=(n&t.childLanes)!==0,e.flags&128){if(r)return Qs(e,t,n);t.flags|=128}if(i=t.memoizedState,i!==null&&(i.rendering=null,i.tail=null,i.lastEffect=null),Ri(po,po.current),r)break;return null;case 22:case 23:return t.lanes=0,Ls(e,t,n)}return ec(e,t,n)}var nc=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}},rc=function(e,t,n,r){var i=e.memoizedProps;if(i!==r){e=t.stateNode,so(io.current);var o=null;switch(n){case`input`:i=V(e,i),r=V(e,r),o=[];break;case`select`:i=R({},i,{value:void 0}),r=R({},r,{value:void 0}),o=[];break;case`textarea`:i=ye(e,i),r=ye(e,r),o=[];break;default:typeof i.onClick!=`function`&&typeof r.onClick==`function`&&(e.onclick=di)}je(n,r);var s;for(u in n=null,i)if(!r.hasOwnProperty(u)&&i.hasOwnProperty(u)&&i[u]!=null)if(u===`style`){var c=i[u];for(s in c)c.hasOwnProperty(s)&&(n||={},n[s]=``)}else u!==`dangerouslySetInnerHTML`&&u!==`children`&&u!==`suppressContentEditableWarning`&&u!==`suppressHydrationWarning`&&u!==`autoFocus`&&(a.hasOwnProperty(u)?o||=[]:(o||=[]).push(u,null));for(u in r){var l=r[u];if(c=i?.[u],r.hasOwnProperty(u)&&l!==c&&(l!=null||c!=null))if(u===`style`)if(c){for(s in c)!c.hasOwnProperty(s)||l&&l.hasOwnProperty(s)||(n||={},n[s]=``);for(s in l)l.hasOwnProperty(s)&&c[s]!==l[s]&&(n||={},n[s]=l[s])}else n||(o||=[],o.push(u,n)),n=l;else u===`dangerouslySetInnerHTML`?(l=l?l.__html:void 0,c=c?c.__html:void 0,l!=null&&c!==l&&(o||=[]).push(u,l)):u===`children`?typeof l!=`string`&&typeof l!=`number`||(o||=[]).push(u,``+l):u!==`suppressContentEditableWarning`&&u!==`suppressHydrationWarning`&&(a.hasOwnProperty(u)?(l!=null&&u===`onScroll`&&Zr(`scroll`,e),o||c===l||(o=[])):(o||=[]).push(u,l))}n&&(o||=[]).push(`style`,n);var u=o;(t.updateQueue=u)&&(t.flags|=4)}},ic=function(e,t,n,r){n!==r&&(t.flags|=4)};function ac(e,t){if(!_a)switch(e.tailMode){case`hidden`:t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case`collapsed`:n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function oc(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var i=e.child;i!==null;)n|=i.lanes|i.childLanes,r|=i.subtreeFlags&14680064,r|=i.flags&14680064,i.return=e,i=i.sibling;else for(i=e.child;i!==null;)n|=i.lanes|i.childLanes,r|=i.subtreeFlags,r|=i.flags,i.return=e,i=i.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function sc(e,t,n){var i=t.pendingProps;switch(ma(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return oc(t),null;case 1:return Wi(t.type)&&Gi(),oc(t),null;case 3:return i=t.stateNode,lo(),Li(Vi),Li(Bi),go(),i.pendingContext&&(i.context=i.pendingContext,i.pendingContext=null),(e===null||e.child===null)&&(wa(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&!(t.flags&256)||(t.flags|=1024,va!==null&&(vl(va),va=null))),oc(t),null;case 5:fo(t);var o=so(oo.current);if(n=t.type,e!==null&&t.stateNode!=null)rc(e,t,n,i,o),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!i){if(t.stateNode===null)throw Error(r(166));return oc(t),null}if(e=so(io.current),wa(t)){i=t.stateNode,n=t.type;var s=t.memoizedProps;switch(i[wi]=t,i[Ti]=s,e=(t.mode&1)!=0,n){case`dialog`:Zr(`cancel`,i),Zr(`close`,i);break;case`iframe`:case`object`:case`embed`:Zr(`load`,i);break;case`video`:case`audio`:for(o=0;o<\/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`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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`}]]),V=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`}]]),fe=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`}]]),pe=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`}]]),me=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`}]]),he=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`}]]),ge=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`}]]),_e=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`}]]),ve=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),ye=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),H=o(((e,t)=>{t.exports=ye()}))(),U=`telesrv.admin.lang`,be={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":`中文`,"language.ru":`RU`,"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.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`中文`,"language.ru":`RU`,"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.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка...`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтвержден`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.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":`中文`,"language.ru":`RU`,"login.heading":`Панель администратора`,"login.body":`Введите учетные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход...`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, премиум, верификация, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, количество участников, статус верификации.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтвержден`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звезд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество звёзд`,"account.starsAmountAria":`Указать количество начисляемых звёзд`,"account.grantStars":`Начислить звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновленные`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждено`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Указать и удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звездных подарков`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звездного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звездах`,"gifts.convertStars":`Звезд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звездные подарки еще не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звездах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. При добавлении и удалении веса permille перераспределяются до 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Разлогинить все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтвержденные`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Запустить тестовый запуск снова`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},xe=(0,g.createContext)(null);function Se({children:e}){let[t,n]=(0,g.useState)(()=>Ee());(0,g.useEffect)(()=>{try{localStorage.setItem(U,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Te(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Te(t,e,n)}),[t]);return(0,H.jsx)(xe.Provider,{value:r,children:e})}function Ce(){let e=(0,g.useContext)(xe);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function we(){let{lang:e,setLang:t,t:n}=Ce();return(0,H.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,H.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Te(e,t,n){let r=be[e][t]??be.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(U));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`:t===`ru`||t.startsWith(`ru-`)?`ru`: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,H.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Me(){let{t:e}=Ce();return(0,H.jsxs)(`div`,{className:`boot-screen`,children:[(0,H.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`loader-bar`})]})}function Ne({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=Ce(),o=t.path.startsWith(`/messages`),[s,c]=(0,g.useState)(o);(0,g.useEffect)(()=>{o&&c(!0)},[o]);async function l(){await x.logout().catch(()=>void 0),r()}return(0,H.jsxs)(`div`,{className:`shell`,children:[(0,H.jsxs)(`aside`,{className:`sidebar`,children:[(0,H.jsxs)(je,{className:`brand`,href:`/`,navigate:n,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,H.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,H.jsx)(Pe,{icon:(0,H.jsx)(oe,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(_e,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(fe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(re,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,H.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,H.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,H.jsx)(ce,{size:16}),(0,H.jsx)(`span`,{children:a(`layout.messages`)}),(0,H.jsx)(F,{className:`nav-section-chevron`,size:15})]}),s&&(0,H.jsxs)(`div`,{className:`nav-children`,children:[(0,H.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,H.jsx)(Pe,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,H.jsxs)(`div`,{className:`sidebar-status`,children:[(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(V,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,H.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(ee,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,H.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(pe,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,H.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,H.jsxs)(`div`,{className:`workspace`,children:[(0,H.jsxs)(`header`,{className:`topbar`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:Ae(t.path,a)}),(0,H.jsx)(`h1`,{children:ke(t.path,a)})]}),(0,H.jsxs)(`div`,{className:`topbar-actions`,children:[(0,H.jsx)(we,{}),(0,H.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,H.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,H.jsx)(se,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,H.jsx)(`main`,{className:`content`,children:i})]})]})}function Pe({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,H.jsxs)(je,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,H.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,H.jsx)(`span`,{children:i})]})}function 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,H.jsxs)(`div`,{className:`page-frame`,children:[(0,H.jsxs)(`div`,{className:`page-title-row`,children:[(0,H.jsxs)(`div`,{children:[t&&(0,H.jsx)(`div`,{className:`eyebrow`,children:t}),(0,H.jsx)(`h2`,{children:e})]}),r&&(0,H.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ue({children:e}){return(0,H.jsx)(`div`,{className:`query-panel`,children:e})}function We({main:e,side:t}){return(0,H.jsxs)(`div`,{className:`split-layout`,children:[(0,H.jsx)(`div`,{className:`split-main`,children:e}),(0,H.jsx)(`aside`,{className:`split-side`,children:t})]})}function Ge({title:e,text:t,action:n}){return(0,H.jsxs)(`div`,{className:`section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`h2`,{children:e}),t&&(0,H.jsx)(`p`,{children:t})]}),n&&(0,H.jsx)(`div`,{className:`section-action`,children:n})]})}function Ke({children:e}){return(0,H.jsxs)(`div`,{className:`alert`,children:[(0,H.jsx)(O,{size:16}),` `,(0,H.jsx)(`span`,{children:e})]})}function G({children:e,tone:t=`neutral`}){return(0,H.jsx)(`span`,{className:`badge ${t}`,children:e})}function K({label:e,value:t,tone:n}){return(0,H.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{children:t})]})}function q({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,H.jsxs)(`div`,{className:`metric ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function J({label:e,value:t,mono:n=!1}){return(0,H.jsxs)(`div`,{className:`summary-item`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function qe({rows:e}){let{t}=Ce();return(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`audit.id`)}),(0,H.jsx)(`th`,{children:t(`audit.commandID`)}),(0,H.jsx)(`th`,{children:t(`audit.action`)}),(0,H.jsx)(`th`,{children:t(`audit.actor`)}),(0,H.jsx)(`th`,{children:t(`audit.status`)}),(0,H.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,H.jsx)(`th`,{children:t(`audit.reason`)}),(0,H.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[e.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,H.jsx)(`td`,{children:e.Action}),(0,H.jsx)(`td`,{children:e.Actor}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,H.jsx)(`td`,{children:ze(e.CreatedAt)})]},e.ID)),e.length===0&&(0,H.jsx)(Je,{colSpan:8})]})]})})}function Je({colSpan:e}){let{t}=Ce();return(0,H.jsx)(`tr`,{children:(0,H.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function Ye({label:e}){return(0,H.jsx)(`section`,{className:`surface`,children:(0,H.jsx)(`div`,{className:`loading-line`,children:e})})}function Xe({value:e}){return(0,H.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function Ze({onLogin:e}){let{t}=Ce(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1);async function c(t){t.preventDefault(),s(!0),a(``);try{e((await x.login(n)).actor)}catch(e){a(b(e))}finally{s(!1)}}return(0,H.jsx)(`main`,{className:`login-page`,children:(0,H.jsxs)(`section`,{className:`login-panel`,children:[(0,H.jsxs)(`div`,{className:`login-head`,children:[(0,H.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,H.jsxs)(`div`,{className:`login-head-actions`,children:[(0,H.jsx)(we,{}),(0,H.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,H.jsxs)(`div`,{className:`login-copy`,children:[(0,H.jsx)(`h1`,{children:t(`login.heading`)}),(0,H.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,H.jsx)(Ke,{children:i}),(0,H.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:t(`login.secret`)}),(0,H.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,H.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var Qe=m();function $e({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=Ce(),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1);function y(){d(``),p(null),h(``)}async function S(e){if(!u.trim()){h(s(`action.reasonRequired`));return}v(!0),h(``);try{let r={...n(),reason:u,confirm:e};p(await x.action(t,r)),e&&o?.()}catch(e){h(b(e))}finally{v(!1)}}let C=f?.dry_run&&!f.error,w=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,T=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:b(e)}}},[c,n]);return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,H.jsx)(`h2`,{children:e})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:s(`action.reason`)}),(0,H.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,H.jsxs)(`div`,{className:`command-preview`,children:[(0,H.jsxs)(`div`,{className:`preview-head`,children:[(0,H.jsx)(te,{size:14}),` `,s(`action.requestPreview`)]}),(0,H.jsx)(Xe,{value:JSON.stringify(T,null,2)})]}),m&&(0,H.jsx)(Ke,{children:m}),f&&(0,H.jsxs)(`div`,{className:`result-box`,children:[(0,H.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,H.jsx)(O,{size:16}):(0,H.jsx)(k,{size:16}),(0,H.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.commandID`)}),(0,H.jsx)(`strong`,{children:f.command_id})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.status`)}),(0,H.jsx)(`strong`,{children:f.status})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.dryRun`)}),(0,H.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,H.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,H.jsx)(Xe,{value:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(ue,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,H.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,H.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function et({rows:e,userID:t,onDone:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{a(new Set)},[t]);let o=(0,g.useMemo)(()=>e.filter(e=>!i.has(e.Hash)),[e,i]);function s(e){a(t=>e(t)),n()}return(0,H.jsxs)(`div`,{className:`authorization-block`,children:[(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:r(`auth.device`)}),(0,H.jsx)(`th`,{children:r(`auth.platform`)}),(0,H.jsx)(`th`,{children:r(`auth.ip`)}),(0,H.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,H.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[o.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,H.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,H.jsx)(`td`,{children:n.IP}),(0,H.jsx)(`td`,{children:ze(n.ActiveAt)}),(0,H.jsx)(`td`,{className:`device-actions-cell`,children:(0,H.jsxs)(`div`,{className:`device-actions`,children:[(0,H.jsx)($e,{label:r(`auth.revokeCurrent`),icon:(0,H.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,H.jsx)($e,{label:r(`auth.keepCurrent`),icon:(0,H.jsx)(fe,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>s(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),o.length===0&&(0,H.jsx)(Je,{colSpan:5})]})]})}),(0,H.jsx)(`div`,{className:`danger-zone`,children:(0,H.jsx)($e,{label:r(`auth.revokeAll`),icon:(0,H.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}=Ce(),[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,H.jsx)(Ke,{children:a});if(!r)return(0,H.jsx)(Ye,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,H.jsx)(He,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,H.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:Le(y)}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(y.Username)||n(`account.noUsername`),` · `,Fe(y.Phone)||n(`account.noPhone`)]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,H.jsx)(G,{tone:`good`,children:n(`account.premium`)}):(0,H.jsx)(G,{children:n(`account.notPremium`)}),r.Verified?(0,H.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(G,{children:n(`account.notVerified`)}),y.Frozen?(0,H.jsx)(G,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,H.jsx)(G,{children:n(`account.accountActive`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,H.jsx)(J,{label:n(`account.lastActive`),value:Be(r.LastSeenAt)||`-`}),(0,H.jsx)(J,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?Be(y.PremiumUntil):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,H.jsx)(J,{label:n(`common.updatedAt`),value:ze(y.UpdatedAt)||`-`}),(0,H.jsx)(J,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,H.jsx)(J,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,H.jsx)(J,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeSince`),value:r.Restriction.Since?ze(r.Restriction.Since):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeUntil`),value:r.Restriction.Until?ze(r.Restriction.Until):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.createdAt`),value:ze(y.CreatedAt)||`-`})]}),r.About&&(0,H.jsx)(`p`,{className:`about-text`,children:r.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,H.jsx)(et,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(qe,{rows:r.AuditLogs})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,H.jsx)($e,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!0,freeze_until:new Date(p).toISOString(),freeze_appeal_url:h.trim()}),onDone:v}),y.Frozen&&(0,H.jsx)($e,{label:n(`account.unfreezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)($e,{label:n(`account.setPremium`),icon:(0,H.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:W(l)}),onDone:v}),(0,H.jsx)($e,{label:n(`account.clearPremium`),icon:(0,H.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,H.jsx)($e,{label:n(`account.grantStars`),icon:(0,H.jsx)(me,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:W(d)}),onDone:v}),(0,H.jsx)($e,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]})]})})})}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}=Ce(),[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,H.jsxs)(He,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(q,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,H.jsx)(q,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,H.jsx)(q,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`account.userID`)}),(0,H.jsx)(`th`,{children:t(`account.phone`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.name`)}),(0,H.jsx)(`th`,{children:t(`common.device`)}),(0,H.jsx)(`th`,{children:t(`account.lastActive`)}),(0,H.jsx)(`th`,{children:t(`account.premium`)}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`account.frozen`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Fe(n.Phone)}),(0,H.jsx)(`td`,{children:Ie(n.Username)}),(0,H.jsx)(`td`,{children:Le(n)}),(0,H.jsx)(`td`,{children:n.DeviceCount}),(0,H.jsx)(`td`,{children:ze(n.LastActiveAt)}),(0,H.jsx)(`td`,{children:n.PremiumUntil>0?(0,H.jsxs)(G,{tone:`good`,children:[t(`account.premium`),` `,Be(n.PremiumUntil)]}):(0,H.jsx)(G,{children:t(`common.none`)})}),(0,H.jsx)(`td`,{children:n.Verified?(0,H.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(G,{children:t(`account.notVerified`)})}),(0,H.jsx)(`td`,{children:n.Frozen?(0,H.jsx)(G,{tone:`danger`,children:t(`account.frozen`)}):(0,H.jsx)(G,{children:t(`common.normal`)})}),(0,H.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Je,{colSpan:11})]})]})})]})}function ot({id:e,navigate:t}){let{t:n}=Ce(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await x.channel(e))}catch(e){o(b(e))}}if((0,g.useEffect)(()=>{s()},[e]),a)return(0,H.jsx)(Ke,{children:a});if(!r)return(0,H.jsx)(Ye,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,H.jsx)(He,{title:`${Re(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,H.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(G,{children:Re(c,n)}),c.Verified?(0,H.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(G,{children:n(`account.notVerified`)}),c.Deleted?(0,H.jsx)(G,{tone:`danger`,children:n(`common.deleted`)}):(0,H.jsx)(G,{children:n(`common.valid`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,H.jsx)(J,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,H.jsx)(J,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,H.jsx)(J,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,H.jsx)(J,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,H.jsx)(J,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,H.jsx)(J,{label:n(`account.createdAt`),value:Be(c.Date)||`-`}),(0,H.jsx)(J,{label:n(`common.updatedAt`),value:ze(c.UpdatedAt)||`-`})]}),c.About&&(0,H.jsx)(`p`,{className:`about-text`,children:c.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(qe,{rows:r.AuditLogs})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,H.jsx)(Xe,{value:r.ChannelJSON})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,H.jsx)($e,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s})]})})})}function st({navigate:e}){let{t}=Ce(),[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,H.jsxs)(He,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(q,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,H.jsx)(q,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,H.jsx)(q,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`channel.channelID`)}),(0,H.jsx)(`th`,{children:t(`channel.kind`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`channel.title`)}),(0,H.jsx)(`th`,{children:t(`common.members`)}),(0,H.jsx)(`th`,{children:t(`common.admins`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Re(n,t)}),(0,H.jsx)(`td`,{children:Ie(n.Username)}),(0,H.jsx)(`td`,{children:n.Title}),(0,H.jsx)(`td`,{children:n.ParticipantsCount}),(0,H.jsx)(`td`,{children:n.AdminsCount}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Verified?(0,H.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(G,{children:t(`account.notVerified`)})}),(0,H.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Je,{colSpan:10})]})]})})]})}function ct({navigate:e}){let{t}=Ce();return(0,H.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,H.jsxs)(`section`,{className:`overview-band`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,H.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,H.jsxs)(`div`,{className:`overview-metrics`,children:[(0,H.jsx)(K,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,H.jsx)(K,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,H.jsx)(K,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,H.jsxs)(`div`,{className:`command-grid`,children:[(0,H.jsx)(lt,{icon:(0,H.jsx)(_e,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,H.jsx)(lt,{icon:(0,H.jsx)(fe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,H.jsx)(lt,{icon:(0,H.jsx)(ce,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,H.jsxs)(`section`,{className:`work-strip`,children:[(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(k,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(ae,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(L,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(te,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function lt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,H.jsxs)(je,{className:`launcher`,href:r,navigate:i,children:[(0,H.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,H.jsxs)(`span`,{className:`launcher-copy`,children:[(0,H.jsx)(`strong`,{children:t}),(0,H.jsx)(`span`,{children:n})]}),(0,H.jsx)(I,{size:16})]})}function ut({channelID:e,msgID:t,navigate:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.groupMessage(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(Ke,{children:o});if(!i)return(0,H.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(He,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,H.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:Be(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(G,{children:r(`common.survived`)}),l.Pinned&&(0,H.jsx)(G,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,H.jsx)(G,{children:r(`messages.channelPost`)}),(0,H.jsxs)(G,{children:[`pts `,l.PTS]})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,H.jsx)(J,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,H.jsx)(J,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,H.jsx)(Xe,{value:i.MessageJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,H.jsx)(Xe,{value:i.ChannelJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.messageId`)}),(0,H.jsx)(`th`,{children:r(`common.sender`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:e.MessageID}),(0,H.jsx)(`td`,{children:e.SenderUserID}),(0,H.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Je,{colSpan:6})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.eventJson`)}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,H.jsx)(Xe,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,H.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function dt({label:e,value:t,onChange:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.accounts(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(ve,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(P,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:Le(t)}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:Ie(t.Username)||Fe(t.Phone)||`-`})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:Le(e)}),(0,H.jsx)(`span`,{children:Ie(e.Username)||Fe(e.Phone)||`-`}),e.Verified?(0,H.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(G,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function ft({label:e,value:t,onChange:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.channels(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(ve,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(P,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:t.Title||`-`}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:Ie(t.Username)||Re(t,r)})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:e.Title||`-`}),(0,H.jsx)(`span`,{children:Ie(e.Username)||Re(e,r)}),e.Verified?(0,H.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(G,{children:Re(e,r)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function pt({navigate:e}){let{t}=Ce(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`100`),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``);async function m(e=!1){if(p(``),!n){p(t(`messages.selectChannel`));return}let r=new URLSearchParams({channel_id:String(n.ID),limit:c});if(e&&u?.rows.length){let e=u.rows[u.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.ID)),a(String(e.Date)),s(String(e.ID))}else i&&r.set(`before_date`,i),o&&r.set(`before_id`,o);try{d(await x.groupMessages(r))}catch(e){p(b(e))}}function h(e){r(e),a(``),s(``),d(null)}let _=u?.rows??[];return(0,H.jsxs)(He,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(Ue,{children:[(0,H.jsx)(`div`,{className:`message-selector-grid single`,children:(0,H.jsx)(ft,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`messages.currentPage`),value:String(_.length)}),(0,H.jsx)(q,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,H.jsx)(q,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,H.jsx)(q,{label:t(`messages.channelGroup`),value:n?`${n.Title||Re(n,t)} (${n.ID})`:`-`})]}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:`From Peer`}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.views`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[_.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Be(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,H.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.ViewsCount}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,H.jsx)(G,{tone:`warn`,children:t(`messages.pinned`)}):(0,H.jsx)(G,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,H.jsx)(Je,{colSpan:9})]})]})})]})}function mt({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.message(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(Ke,{children:o});if(!i)return(0,H.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(He,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,H.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:Be(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(G,{children:r(`common.survived`)}),(0,H.jsxs)(G,{children:[`pts `,l.PTS]}),(0,H.jsx)(G,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,H.jsx)(J,{label:r(`common.time`),value:Be(l.Date)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,H.jsx)(Xe,{value:i.MessageJSON})]}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,H.jsx)(Xe,{value:i.DialogJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,H.jsx)(Xe,{value:i.PrivateJSON})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Je,{colSpan:4})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`ID`}),(0,H.jsx)(`th`,{children:r(`account.userID`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.status`)}),(0,H.jsx)(`th`,{children:r(`messages.attempts`)}),(0,H.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{children:e.TargetUserID}),(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.EventType}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.Attempts}),(0,H.jsx)(`td`,{children:ze(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,H.jsx)(Je,{colSpan:7})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,H.jsx)($e,{label:r(`messages.deleteThis`),icon:(0,H.jsx)(he,{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}=Ce(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`100`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,S]=(0,g.useState)(``),[C,w]=(0,g.useState)(`1`),[T,E]=(0,g.useState)(null),[D,O]=(0,g.useState)(``);async function k(e=!1){if(O(``),!n||!i){O(t(`messages.selectPrivatePeers`));return}let r=new URLSearchParams({owner_user_id:String(n.ID),peer_id:String(i.ID),limit:u});if(e&&T?.rows.length){let e=T.rows[T.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.BoxID)),s(String(e.Date)),l(String(e.BoxID))}else o&&r.set(`before_date`,o),c&&r.set(`before_id`,c);try{E(await x.messages(r))}catch(e){O(b(e))}}function A(e){r(e),s(``),l(``),E(null)}function j(e){a(e),s(``),l(``),E(null)}return(0,H.jsxs)(He,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,H.jsx)(Ke,{children:D}),(0,H.jsxs)(Ue,{children:[(0,H.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,H.jsx)(dt,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,H.jsx)(dt,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,H.jsx)(q,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,H.jsx)(q,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,H.jsx)(q,{label:t(`messages.ownerPeer`),value:n&&i?`${Le(n)} / ${Le(i)}`:`-`})]}),(0,H.jsxs)(`div`,{className:`operation-row`,children:[(0,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(he,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,H.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsx)($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,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(ie,{size:15}),` `,t(`messages.clearHistory`)]}),(0,H.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,H.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,H.jsx)($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,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:t(`messages.direction`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,H.jsx)(`td`,{children:Be(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,H.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):(0,H.jsx)(G,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,H.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`)},H.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)}},H.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`)))},H.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())},H.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},H.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},H.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`))}},H.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`)},H.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)},H.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),ye(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}(),xe=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Se=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=xe.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Ce=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return Se(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=Ce.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=be.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?U.searchAnimations(Je,qe,Ye):U.searchAnimations()}function Qe(e){re(e)}function $e(e){ue(e)}function et(e){return qe===!0&&(e.animationData=JSON.parse(Je)),U.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=U.play,J.pause=U.pause,J.setLocationHref=Xe,J.togglePause=U.togglePause,J.setSpeed=U.setSpeed,J.setDirection=U.setDirection,J.stop=U.stop,J.searchAnimations=Ze,J.registerAnimation=U.registerAnimation,J.loadAnimation=et,J.setSubframeRendering=Qe,J.resize=U.resize,J.goToAndStop=U.goToAndStop,J.destroy=U.destroy,J.setQuality=tt,J.inBrowser=nt,J.installPlugin=rt,J.freeze=U.freeze,J.unfreeze=U.unfreeze,J.setVolume=U.setVolume,J.mute=U.mute,J.unmute=U.unmute,J.getRegisteredAnimations=U.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=be.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+xe||!x?(T=(m+xe-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)},ge(`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=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function bt(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:vt(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function St(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=yt[e.length%yt.length];return{key:vt(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var Ct=e=>bt([xt(e,0),xt(e,1)]),wt=()=>{let e=St([]);return bt([e,St([e])])};function Tt({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,H.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function Et({giftID:e,attribute:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(!1);return(0,g.useEffect)(()=>{let n=!1;return a(!1),x.giftCollectibleAnimation(e,t.kind,t.id).then(e=>{n||r(e)}).catch(()=>{n||a(!0)}),()=>{n=!0}},[e,t.id,t.kind]),i?(0,H.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,H.jsx)(Tt,{data:n,compact:!0}):(0,H.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,H.jsx)(A,{className:`spin`,size:15})})}async function Dt(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 Ot=e=>Number.parseInt(e.replace(`#`,``),16),kt=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function At({gift:e,onClose:t,onPublished:n}){let{t:r}=Ce(),[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)(()=>Ct(`model`)),[D,O]=(0,g.useState)(()=>Ct(`pattern`)),[M,N]=(0,g.useState)(wt);(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 Dt(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function ee(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||M.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=M.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:M.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:Ot(e.center),edge_color:Ot(e.edge),pattern_color:Ot(e.pattern),text_color:Ot(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}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,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,H.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(G,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(bt([...t,xt(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,H.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,H.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,H.jsxs)(`label`,{className:`collectible-file`,children:[(0,H.jsx)(`span`,{children:r(`gifts.animation`)}),(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:t=>void L(e,i,t.target.files?.[0]??null)}),(0,H.jsxs)(`em`,{children:[(0,H.jsx)(R,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,H.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,H.jsx)(Tt,{data:i.animation,compact:!0}):(0,H.jsx)(j,{size:16})}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(bt(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(he,{size:14})}),i.fileError&&(0,H.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,H.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,H.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,H.jsxs)(`div`,{className:`collectible-loading`,children:[(0,H.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,H.jsxs)(`section`,{className:`collectible-active`,children:[(0,H.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(ne,{size:18}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,H.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,H.jsx)(G,{tone:`good`,children:r(`collectibles.published`)})]}),(0,H.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(Et,{giftID:e.GiftID,attribute:t}),(0,H.jsxs)(`div`,{children:[(0,H.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,H.jsx)(G,{children:`crafted`})]}),(0,H.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,kt(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, #${(e.center_color??0).toString(16).padStart(6,`0`)}, #${(e.edge_color??0).toString(16).padStart(6,`0`)})`,color:`#${(e.text_color??16777215).toString(16).padStart(6,`0`)}`},children:`Aa`}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e.name}),(0,H.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,kt(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,H.jsxs)(`div`,{className:`collectible-empty`,children:[(0,H.jsx)(ne,{size:22}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,H.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,H.jsxs)(`section`,{className:`collectible-definition`,children:[(0,H.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,H.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.reason`)}),(0,H.jsx)(`input`,{value:C,maxLength:1e3,placeholder:r(`gifts.reasonPlaceholder`),onChange:e=>w(e.target.value)})]})]}),ie(`models`,T,E),ie(`patterns`,D,O),(0,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,H.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(G,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N(bt([...M,St(M)])),F()},children:[(0,H.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,H.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:e.name,maxLength:128,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,name:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:e.rarity,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,rarity:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:e.sortOrder,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,sortOrder:t.target.value}:n)),F()}})]}),[`center`,`edge`,`pattern`,`text`].map(t=>(0,H.jsxs)(`label`,{className:`collectible-color`,children:[(0,H.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,H.jsx)(`input`,{type:`color`,value:e[t],onChange:n=>{N(M.map(r=>r.key===e.key?{...r,[t]:n.target.value}:r)),F()}})]},t)),(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length<=2,onClick:()=>{N(bt(M.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(he,{size:14})})]},e.key))})]})]}),u&&(0,H.jsx)(Ke,{children:u}),f&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,H.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:te,disabled:c,children:[c?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(fe,{size:15}),r(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:re,disabled:c||!f,children:[(0,H.jsx)(ge,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function jt(e){return e.model_count+e.pattern_count+e.backdrop_count}function Mt(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Y({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,H.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,H.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,H.jsx)(`span`,{children:s})}),(0,H.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,H.jsx)(le,{size:14}):(0,H.jsx)(ue,{size:14})})]})}function Nt({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=gt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,H.jsx)(`div`,{className:`gift-animation-shell`,children:(0,H.jsx)(`div`,{className:`gift-animation`,ref:t})})}function Pt(){let{t:e}=Ce(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,te]=(0,g.useState)(`50`),[re,ie]=(0,g.useState)(`50`),[ae,oe]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[V,pe]=(0,g.useState)(null),[me,he]=(0,g.useState)(!1),[_e,ye]=(0,g.useState)(``),[U,be]=(0,g.useState)(``);async function xe(){ye(``);try{n((await x.gifts()).Gifts??[])}catch(e){ye(b(e))}}(0,g.useEffect)(()=>{xe()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>be(b(e)))},[a,d,p.length]);let Se=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),we=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),Te=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),Ee=(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 De(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae)})),r.set(`file`,l,l.name),r}function Oe(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function ke(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),te(String(t.stars)),ie(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),pe(null)}async function Ae(){he(!0),be(``),pe(null);try{pe(d===`official`?await x.importOfficialGift(Oe(!1)):await x.importGift(De(!1)))}catch(e){be(b(e))}finally{he(!1)}}async function je(){if(V){he(!0),be(``);try{d===`official`?await x.importOfficialGift(Oe(!0,V.command_id)):await x.importGift(De(!0,V.command_id)),pe(null),u(null),F(`0`),L(``),C(``),await xe(),o(!1)}catch(e){be(b(e))}finally{he(!1)}}}function Me(){F(`0`),L(``),te(`50`),ie(`50`),oe(`0`),ce(!0),ue(``),u(null),pe(null),be(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Ne(e){F(e.GiftID),L(e.Title),te(String(e.Stars)),ie(String(e.ConvertStars)),oe(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),pe(null),be(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,H.jsxs)(He,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>xe(),disabled:me,children:[(0,H.jsx)(de,{size:15}),` `,e(`common.refresh`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Me,children:[(0,H.jsx)(z,{size:15}),` `,e(`gifts.add`)]})]}),children:[_e&&(0,H.jsx)(Ke,{children:_e}),(0,H.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,H.jsx)(q,{label:e(`gifts.total`),value:String(t.length)}),(0,H.jsx)(q,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,H.jsx)(q,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,H.jsx)(q,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`div`,{className:`toolbar`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,H.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Ee.length,total:t.length})})]})}),(0,H.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:e(`gifts.animation`)}),(0,H.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,H.jsx)(`th`,{children:e(`gifts.title`)}),(0,H.jsx)(`th`,{children:e(`gifts.price`)}),(0,H.jsx)(`th`,{children:e(`gifts.source`)}),(0,H.jsx)(`th`,{children:e(`gifts.received`)}),(0,H.jsx)(`th`,{children:e(`common.status`)}),(0,H.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,H.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[Ee.map(t=>(0,H.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,H.jsx)(`td`,{children:(0,H.jsx)(Y,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,H.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,H.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,H.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(G,{children:t.SourceFormat}),(0,H.jsx)(`span`,{className:`gift-source-size`,children:Mt(t.AnimationSize)})]}),(0,H.jsx)(`td`,{children:t.ReceivedCount}),(0,H.jsx)(`td`,{children:(0,H.jsx)(G,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,H.jsx)(`td`,{children:ze(t.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,H.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,H.jsx)(ne,{size:13}),e(`collectibles.manage`)]}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Ne(t),children:e(`gifts.replace`)}),(0,H.jsx)($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 xe()})]})})]},t.GiftID)),Ee.length===0&&(0,H.jsx)(Je,{colSpan:9})]})]})}),a&&(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,H.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:me,"aria-label":e(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${V?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${V?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,H.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),pe(null)},children:e(`gifts.officialSource`)}),(0,H.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),pe(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,H.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:p.length}),(0,H.jsx)(`span`,{children:`SHA-256`})]})]}),(0,H.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,H.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:Te.length,total:p.length})})]}),(0,H.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,H.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,H.jsx)(`span`,{children:we[t]})]},t))}),(0,H.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[Te.map(t=>{let n=t.source_gift_id===S;return(0,H.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>ke(t),children:[(0,H.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,H.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,H.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,H.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,H.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:jt(t)})})]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),Te.length===0&&(0,H.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),Se&&(0,H.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,H.jsx)(Nt,{sourceGiftID:Se.source_gift_id}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:Se.title||e(`gifts.officialUnnamed`,{id:Se.source_gift_id})}),(0,H.jsx)(`span`,{className:`mono`,children:Se.source_gift_id}),(0,H.jsxs)(`small`,{children:[Se.model_count,` `,e(`collectibles.models`),` · `,Se.pattern_count,` `,e(`collectibles.patterns`),` · `,Se.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:Se.can_upgrade?`yes`:`no`,children:Se.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:Se.can_craft?`craft`:`no`,children:Se.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),Se?.can_upgrade&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),pe(null)}})]})]})]})]}):(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-file-icon`,children:(0,H.jsx)(R,{size:22})}),(0,H.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,H.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,H.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,H.jsx)(`small`,{children:l?Mt(l.size):e(`gifts.fileHint`)})]}),(0,H.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.title`)}),(0,H.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.stars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{te(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:re,onChange:e=>{ie(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:ae,onChange:e=>{oe(e.target.value),pe(null)}})]})]}),(0,H.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,H.jsx)(`span`,{children:e(`gifts.reason`)}),(0,H.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),U&&(0,H.jsx)(Ke,{children:U}),V&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,H.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(V.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:me,children:e(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:Ae,disabled:me,children:[me?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(fe,{size:15}),e(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,disabled:me||!V,children:[(0,H.jsx)(ge,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,H.jsx)(At,{gift:s,onClose:()=>c(null),onPublished:()=>void xe()})]})}function Ft({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1];return n?(0,H.jsx)(tt,{id:Number(n),navigate:t}):r?(0,H.jsx)(ot,{id:Number(r),navigate:t}):e.path===`/accounts`?(0,H.jsx)(at,{navigate:t}):e.path===`/channels`?(0,H.jsx)(st,{navigate:t}):e.path===`/gifts`?(0,H.jsx)(Pt,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,H.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,H.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,H.jsx)(pt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,H.jsx)(ht,{navigate:t}):(0,H.jsx)(ct,{navigate:t})}function It(){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,H.jsx)(Me,{}):e===null?(0,H.jsx)(Ze,{onLogin:t}):(0,H.jsx)(Ne,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,H.jsx)(Ft,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,H.jsx)(g.StrictMode,{children:(0,H.jsx)(Se,{children:(0,H.jsx)(It,{})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/index.html b/cmd/telesrv-admin/web/dist/index.html index 788b9ebd..18651f58 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -4,7 +4,7 @@ telesrv admin - + diff --git a/cmd/telesrv-admin/web/src/i18n.tsx b/cmd/telesrv-admin/web/src/i18n.tsx index d1723df8..34842c3d 100644 --- a/cmd/telesrv-admin/web/src/i18n.tsx +++ b/cmd/telesrv-admin/web/src/i18n.tsx @@ -316,7 +316,7 @@ const translations: Record> = { "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.immutableHint": "Dry-run checks every file and the attribute-pool structure before the revision becomes active.", "collectibles.upgradeStars": "Upgrade price in Stars", "collectibles.supply": "Unique supply", "collectibles.slug": "Public slug prefix", @@ -327,7 +327,9 @@ const translations: Record> = { "collectibles.pattern": "Pattern", "collectibles.backdrop": "Backdrop", "collectibles.rarity": "Rarity ‰", - "collectibles.rarityHint": "Permille values are relative regular-upgrade weights; their total does not need to equal 1000.", + "collectibles.rarityHint": "Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.", + "collectibles.minimumAttributes": "Models, patterns, and backdrops must each contain at least two attributes.", + "collectibles.duplicateBackdropID": "Backdrop IDs must be unique within the pool.", "collectibles.colorHint": "Colors are stored as 24-bit RGB values.", "collectibles.addAttribute": "Add", "collectibles.remove": "Remove attribute", @@ -686,7 +688,7 @@ const translations: Record> = { "collectibles.noPool": "尚未发布 Collectibles 属性池", "collectibles.noPoolHint": "发布模型、图案与背景后,客户端即可升级为唯一礼物。", "collectibles.publishNew": "发布新的不可变版本", - "collectibles.immutableHint": "Dry-run 会校验全部文件和稀有度总和,通过后才切换为当前版本。", + "collectibles.immutableHint": "Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。", "collectibles.upgradeStars": "升级价格 Stars", "collectibles.supply": "唯一礼物总量", "collectibles.slug": "公开 Slug 前缀", @@ -697,7 +699,9 @@ const translations: Record> = { "collectibles.pattern": "图案", "collectibles.backdrop": "背景", "collectibles.rarity": "稀有度 ‰", - "collectibles.rarityHint": "Permille 是普通升级的相对权重,不要求每类合计正好为 1000。", + "collectibles.rarityHint": "每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。", + "collectibles.minimumAttributes": "模型、图案和背景每类都必须至少包含两个属性。", + "collectibles.duplicateBackdropID": "同一属性池中的背景 ID 必须唯一。", "collectibles.colorHint": "颜色会按 24 位 RGB 数值保存。", "collectibles.addAttribute": "添加", "collectibles.remove": "删除属性", @@ -1056,7 +1060,7 @@ const translations: Record> = { "collectibles.noPool": "Нет опубликованного пула коллекционных предметов", "collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.", "collectibles.publishNew": "Опубликовать новую неизменяемую версию", - "collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и итоговые показатели редкости перед тем, как версия станет активной.", + "collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.", "collectibles.upgradeStars": "Цена улучшения в Звездах", "collectibles.supply": "Уникальный тираж", "collectibles.slug": "Публичный префикс ссылки (slug)", @@ -1067,7 +1071,9 @@ const translations: Record> = { "collectibles.pattern": "Узор", "collectibles.backdrop": "Фон", "collectibles.rarity": "Редкость ‰", - "collectibles.rarityHint": "Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.", + "collectibles.rarityHint": "В каждой категории должно быть не менее двух атрибутов. При добавлении и удалении веса permille перераспределяются до 1000.", + "collectibles.minimumAttributes": "Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.", + "collectibles.duplicateBackdropID": "ID фонов в одном наборе должны быть уникальными.", "collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.", "collectibles.addAttribute": "Добавить", "collectibles.remove": "Удалить атрибут", diff --git a/cmd/telesrv-admin/web/src/pages/GiftCollectiblesModal.tsx b/cmd/telesrv-admin/web/src/pages/GiftCollectiblesModal.tsx index c6bbe6ff..fa09130c 100644 --- a/cmd/telesrv-admin/web/src/pages/GiftCollectiblesModal.tsx +++ b/cmd/telesrv-admin/web/src/pages/GiftCollectiblesModal.tsx @@ -31,8 +31,38 @@ type BackdropDraft = { let draftSequence = 0; const nextKey = (kind: string) => `${kind}-${++draftSequence}`; -const newAnimated = (kind: string): AnimatedDraft => ({ key: nextKey(kind), name: "", rarity: "1000", sortOrder: "0", file: null, animation: null, fileError: "" }); -const newBackdrop = (): BackdropDraft => ({ key: nextKey("backdrop"), name: "", backdropID: "1", rarity: "1000", sortOrder: "0", center: "#6f5bea", edge: "#34278f", pattern: "#a89df5", text: "#ffffff" }); +const backdropPalettes = [ + { center: "#6f5bea", edge: "#34278f", pattern: "#a89df5", text: "#ffffff" }, + { center: "#32a86b", edge: "#17613e", pattern: "#8ee0b3", text: "#ffffff" }, + { center: "#df8d2f", edge: "#8c421e", pattern: "#ffd08a", text: "#ffffff" }, + { center: "#d95878", edge: "#7b2944", pattern: "#f5a1b6", text: "#ffffff" } +]; + +function rebalanceRarity(rows: T[]): T[] { + if (!rows.length) return rows; + const base = Math.floor(1000 / rows.length); + const remainder = 1000 % rows.length; + return rows.map((row, index) => ({ ...row, rarity: String(base + (index < remainder ? 1 : 0)) })); +} + +const newAnimated = (kind: string, sortOrder: number): AnimatedDraft => ({ + key: nextKey(kind), name: "", rarity: "1", sortOrder: String(sortOrder), file: null, animation: null, fileError: "" +}); + +function newBackdrop(rows: BackdropDraft[]): BackdropDraft { + const backdropID = rows.reduce((maximum, row) => { + const value = Number(row.backdropID); + return Number.isInteger(value) ? Math.max(maximum, value) : maximum; + }, 0) + 1; + const colors = backdropPalettes[rows.length % backdropPalettes.length]; + return { key: nextKey("backdrop"), name: "", backdropID: String(backdropID), rarity: "1", sortOrder: String(rows.length), ...colors }; +} + +const initialAnimated = (kind: string) => rebalanceRarity([newAnimated(kind, 0), newAnimated(kind, 1)]); +const initialBackdrops = () => { + const first = newBackdrop([]); + return rebalanceRarity([first, newBackdrop([first])]); +}; function AnimationPreview({ data, compact = false }: { data: AnimationData; compact?: boolean }) { const host = useRef(null); @@ -87,9 +117,9 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St const [supplyTotal, setSupplyTotal] = useState("1000"); const [slugPrefix, setSlugPrefix] = useState(`gift-${gift.GiftID}`); const [reason, setReason] = useState(""); - const [models, setModels] = useState([newAnimated("model")]); - const [patterns, setPatterns] = useState([newAnimated("pattern")]); - const [backdrops, setBackdrops] = useState([newBackdrop()]); + const [models, setModels] = useState(() => initialAnimated("model")); + const [patterns, setPatterns] = useState(() => initialAnimated("pattern")); + const [backdrops, setBackdrops] = useState(initialBackdrops); useEffect(() => { let cancelled = false; @@ -131,6 +161,9 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St function buildForm(confirm: boolean, commandID = "") { if (!reason.trim()) throw new Error(t("action.reasonRequired")); + if (models.length < 2 || patterns.length < 2 || backdrops.length < 2) throw new Error(t("collectibles.minimumAttributes")); + const backdropIDs = backdrops.map((row) => Number(row.backdropID)); + if (new Set(backdropIDs).size !== backdropIDs.length) throw new Error(t("collectibles.duplicateBackdropID")); for (const row of [...models, ...patterns]) if (!row.file) throw new Error(t("collectibles.fileRequired")); const form = new FormData(); const animatedMetadata = (rows: AnimatedDraft[]) => rows.map((row) => ({ name: row.name.trim(), rarity_permille: Number(row.rarity), sort_order: Number(row.sortOrder), file_key: row.key })); @@ -168,7 +201,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
{t(`collectibles.${kind}`)}{t("collectibles.rarityHint")}
-
0 ? "good" : "neutral"}>{rarityTotals[kind]}‰
+
0 ? "good" : "neutral"}>{rarityTotals[kind]}‰
{rows.map((row, index) =>
@@ -178,7 +211,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St
{row.animation ? : }
- + {row.fileError && {row.fileError}}
)}
@@ -211,7 +244,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St {renderAnimatedRows("models", models, setModels)} {renderAnimatedRows("patterns", patterns, setPatterns)}
-
{t("collectibles.backdrops")}{t("collectibles.colorHint")}
0 ? "good" : "neutral"}>{rarityTotals.backdrops}‰
+
{t("collectibles.backdrops")}{t("collectibles.colorHint")}
0 ? "good" : "neutral"}>{rarityTotals.backdrops}‰
{backdrops.map((row, index) =>
{index + 1}
@@ -220,7 +253,7 @@ export function GiftCollectiblesModal({ gift, onClose, onPublished }: { gift: St {(["center", "edge", "pattern", "text"] as const).map((field) => )}
Aa
- +
)}
diff --git a/deploy/migrations/0132_star_gift_upgrade_preview_pool.down.sql b/deploy/migrations/0132_star_gift_upgrade_preview_pool.down.sql new file mode 100644 index 00000000..9495ef47 --- /dev/null +++ b/deploy/migrations/0132_star_gift_upgrade_preview_pool.down.sql @@ -0,0 +1,10 @@ +DROP TRIGGER IF EXISTS star_gift_catalog_collectible_preview_activation ON public.star_gift_catalog; +DROP FUNCTION IF EXISTS public.telesrv_validate_collectible_preview_activation(); + +UPDATE public.star_gift_catalog c +SET collectible_revision_id = repair.collectible_revision_id, updated_at = now() +FROM public.star_gift_collectible_preview_repairs repair +WHERE c.gift_id = repair.gift_id + AND c.collectible_revision_id IS NULL; + +DROP TABLE public.star_gift_collectible_preview_repairs; diff --git a/deploy/migrations/0132_star_gift_upgrade_preview_pool.up.sql b/deploy/migrations/0132_star_gift_upgrade_preview_pool.up.sql new file mode 100644 index 00000000..2e0855df --- /dev/null +++ b/deploy/migrations/0132_star_gift_upgrade_preview_pool.up.sql @@ -0,0 +1,88 @@ +-- TDesktop deduplicates upgrade-preview models/patterns by document identity and can only +-- finish each attribute spinner after it has a non-target item. Detach previously published +-- pools that cannot satisfy that client contract; the immutable revisions remain available +-- for audit and for already-issued unique gifts. +CREATE TABLE public.star_gift_collectible_preview_repairs ( + gift_id bigint PRIMARY KEY REFERENCES public.star_gift_catalog(gift_id) ON DELETE CASCADE, + collectible_revision_id bigint UNIQUE NOT NULL + REFERENCES public.star_gift_collectible_revisions(id) ON DELETE RESTRICT, + reason text DEFAULT 'insufficient distinct upgrade preview attributes' NOT NULL, + repaired_at timestamp with time zone DEFAULT now() NOT NULL +); + +INSERT INTO public.star_gift_collectible_preview_repairs (gift_id, collectible_revision_id) +SELECT c.gift_id, c.collectible_revision_id +FROM public.star_gift_catalog c +JOIN public.star_gift_collectible_revisions r ON r.id = c.collectible_revision_id +WHERE c.collectible_revision_id IS NOT NULL + AND ( + r.status <> 'published' OR r.gift_id <> c.gift_id OR + (SELECT count(DISTINCT m.document_id) + FROM public.star_gift_collectible_models m + WHERE m.collectible_revision_id = r.id + AND m.rarity_kind = 'permille' AND NOT m.crafted) < 2 OR + (SELECT count(DISTINCT p.document_id) + FROM public.star_gift_collectible_patterns p + WHERE p.collectible_revision_id = r.id + AND p.rarity_kind = 'permille') < 2 OR + (SELECT count(DISTINCT b.backdrop_id) + FROM public.star_gift_collectible_backdrops b + WHERE b.collectible_revision_id = r.id + AND b.rarity_kind = 'permille') < 2 + ); + +UPDATE public.star_gift_catalog c +SET collectible_revision_id = NULL, updated_at = now() +FROM public.star_gift_collectible_preview_repairs repair +WHERE c.gift_id = repair.gift_id + AND c.collectible_revision_id = repair.collectible_revision_id; + +-- Keep the same invariant at the final activation boundary. Application validation gives the +-- operator a precise error first; this trigger also protects imports or maintenance SQL that +-- attempts to expose a malformed published revision directly. +CREATE FUNCTION public.telesrv_validate_collectible_preview_activation() RETURNS trigger + LANGUAGE plpgsql AS $$ +DECLARE + revision_gift_id bigint; + revision_status text; +BEGIN + IF NEW.collectible_revision_id IS NULL THEN + RETURN NEW; + END IF; + + SELECT gift_id, status INTO revision_gift_id, revision_status + FROM public.star_gift_collectible_revisions + WHERE id = NEW.collectible_revision_id; + + IF NOT FOUND OR revision_gift_id <> NEW.gift_id OR revision_status <> 'published' THEN + RAISE EXCEPTION 'collectible preview revision must be published for the same gift' + USING ERRCODE = '23514'; + END IF; + IF (SELECT count(DISTINCT document_id) + FROM public.star_gift_collectible_models + WHERE collectible_revision_id = NEW.collectible_revision_id + AND rarity_kind = 'permille' AND NOT crafted) < 2 THEN + RAISE EXCEPTION 'collectible model preview requires two distinct documents' + USING ERRCODE = '23514'; + END IF; + IF (SELECT count(DISTINCT document_id) + FROM public.star_gift_collectible_patterns + WHERE collectible_revision_id = NEW.collectible_revision_id + AND rarity_kind = 'permille') < 2 THEN + RAISE EXCEPTION 'collectible pattern preview requires two distinct documents' + USING ERRCODE = '23514'; + END IF; + IF (SELECT count(DISTINCT backdrop_id) + FROM public.star_gift_collectible_backdrops + WHERE collectible_revision_id = NEW.collectible_revision_id + AND rarity_kind = 'permille') < 2 THEN + RAISE EXCEPTION 'collectible backdrop preview requires two distinct IDs' + USING ERRCODE = '23514'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER star_gift_catalog_collectible_preview_activation + BEFORE INSERT OR UPDATE OF collectible_revision_id ON public.star_gift_catalog + FOR EACH ROW EXECUTE FUNCTION public.telesrv_validate_collectible_preview_activation(); diff --git a/internal/admin/service_test.go b/internal/admin/service_test.go index 46da9654..2fda7b69 100644 --- a/internal/admin/service_test.go +++ b/internal/admin/service_test.go @@ -798,9 +798,18 @@ func TestPublishStarGiftCollectiblesDryRunThenConfirm(t *testing.T) { 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}}, + Models: []StarGiftCollectibleAnimationUpload{ + {Name: "Ruby", RarityPermille: 500, FileKey: "model-0", FileName: "ruby.lottie", Data: []byte("model")}, + {Name: "Sapphire", RarityPermille: 500, FileKey: "model-1", FileName: "sapphire.lottie", Data: []byte("model-1")}, + }, + Patterns: []StarGiftCollectibleAnimationUpload{ + {Name: "Stars", RarityPermille: 500, FileKey: "pattern-0", FileName: "stars.tgs", Data: []byte("pattern")}, + {Name: "Moons", RarityPermille: 500, FileKey: "pattern-1", FileName: "moons.tgs", Data: []byte("pattern-1")}, + }, + Backdrops: []StarGiftCollectibleBackdropInput{ + {Name: "Night", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityPermille: 500}, + {Name: "Day", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityPermille: 500}, + }, } base.CommandMeta = CommandMeta{CommandID: "dry-collectibles", Actor: "ops", Reason: "pool", DryRun: true} preview, err := svc.PublishStarGiftCollectibles(context.Background(), base) @@ -814,6 +823,45 @@ func TestPublishStarGiftCollectiblesDryRunThenConfirm(t *testing.T) { } } +func TestPublishStarGiftCollectiblesRejectsUnsafeClientPreviewPool(t *testing.T) { + valid := func() PublishStarGiftCollectiblesRequest { + return PublishStarGiftCollectiblesRequest{ + CommandMeta: CommandMeta{CommandID: "unsafe-pool", Actor: "ops", Reason: "regression", DryRun: true}, + GiftID: 11, UpgradeStars: 125, SupplyTotal: 100, SlugPrefix: "cake", + Models: []StarGiftCollectibleAnimationUpload{ + {Name: "Ruby", RarityPermille: 500, FileName: "ruby.lottie", Data: []byte("ruby")}, + {Name: "Sapphire", RarityPermille: 500, FileName: "sapphire.lottie", Data: []byte("sapphire")}, + }, + Patterns: []StarGiftCollectibleAnimationUpload{ + {Name: "Stars", RarityPermille: 500, FileName: "stars.lottie", Data: []byte("stars")}, + {Name: "Moons", RarityPermille: 500, FileName: "moons.lottie", Data: []byte("moons")}, + }, + Backdrops: []StarGiftCollectibleBackdropInput{ + {Name: "Night", BackdropID: 1, RarityPermille: 500}, + {Name: "Day", BackdropID: 2, RarityPermille: 500}, + }, + } + } + tests := map[string]func(*PublishStarGiftCollectiblesRequest){ + "single model": func(req *PublishStarGiftCollectiblesRequest) { req.Models = req.Models[:1] }, + "single pattern": func(req *PublishStarGiftCollectiblesRequest) { req.Patterns = req.Patterns[:1] }, + "single backdrop": func(req *PublishStarGiftCollectiblesRequest) { req.Backdrops = req.Backdrops[:1] }, + "duplicate backdrop id": func(req *PublishStarGiftCollectiblesRequest) { + req.Backdrops[1].BackdropID = req.Backdrops[0].BackdropID + }, + } + for name, mutate := range tests { + t.Run(name, func(t *testing.T) { + req := valid() + mutate(&req) + svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: &fakeGiftsService{}, Now: fixedNow}) + if _, err := svc.PublishStarGiftCollectibles(context.Background(), req); !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) { + t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err) + } + }) + } +} + func TestImportOfficialStarGiftPreservesCraftedRarityAndPublishesBundle(t *testing.T) { permille := 922 source := &fakeOfficialGiftsSource{bundle: officialgifts.Bundle{ @@ -828,10 +876,17 @@ func TestImportOfficialStarGiftPreservesCraftedRarityAndPublishesBundle(t *testi Collectible: &officialgifts.CollectibleSet{ Models: []officialgifts.Model{ {Name: "Regular", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 2, FileName: "regular.tgs", SHA256: strings.Repeat("b", 64), Data: []byte("regular")}}, + {Name: "Regular Two", DocumentID: 5, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 5, FileName: "regular-two.tgs", SHA256: strings.Repeat("e", 64), Data: []byte("regular-two")}}, {Name: "Crafted", DocumentID: 3, Crafted: true, Rarity: officialgifts.Rarity{Kind: "legendary"}, Document: officialgifts.Document{ID: 3, FileName: "crafted.tgs", SHA256: strings.Repeat("c", 64), Data: []byte("crafted")}}, }, - Patterns: []officialgifts.Pattern{{Name: "Pattern", DocumentID: 4, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 4, FileName: "pattern.tgs", SHA256: strings.Repeat("d", 64), Data: []byte("pattern")}}}, - Backdrops: []officialgifts.Backdrop{{Name: "Black", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}}}, + Patterns: []officialgifts.Pattern{ + {Name: "Pattern", DocumentID: 4, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 4, FileName: "pattern.tgs", SHA256: strings.Repeat("d", 64), Data: []byte("pattern")}}, + {Name: "Pattern Two", DocumentID: 6, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: officialgifts.Document{ID: 6, FileName: "pattern-two.tgs", SHA256: strings.Repeat("f", 64), Data: []byte("pattern-two")}}, + }, + Backdrops: []officialgifts.Backdrop{ + {Name: "Black", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}}, + {Name: "White", BackdropID: 1, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}}, + }, }, }} gifts := &fakeGiftsService{} @@ -848,7 +903,7 @@ func TestImportOfficialStarGiftPreservesCraftedRarityAndPublishesBundle(t *testi t.Fatalf("result=%+v err=%v create=%d", result, err, gifts.createCalls) } models := gifts.lastBundle.Collectible.Models - if len(models) != 2 || !models[1].Crafted || models[1].RarityKind != domain.StarGiftRarityLegendary || models[1].RarityPermille != 0 || + if len(models) != 3 || !models[2].Crafted || models[2].RarityKind != domain.StarGiftRarityLegendary || models[2].RarityPermille != 0 || models[0].RarityPermille != 922 || gifts.lastBundle.Collectible.Backdrops[0].BackdropID != 0 { t.Fatalf("imported models=%+v backdrops=%+v", models, gifts.lastBundle.Collectible.Backdrops) } @@ -880,17 +935,18 @@ func TestImportOfficialStarGiftPublishesThroughRealGiftService(t *testing.T) { }, BaseDocument: document(1, "gift.json"), Collectible: &officialgifts.CollectibleSet{ - Models: []officialgifts.Model{{ - Name: "Model", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, - Document: document(2, "model.json"), - }}, - Patterns: []officialgifts.Pattern{{ - Name: "Pattern", DocumentID: 3, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, - Document: document(3, "pattern.json"), - }}, - Backdrops: []officialgifts.Backdrop{{ - Name: "Backdrop", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, - }}, + Models: []officialgifts.Model{ + {Name: "Model", DocumentID: 2, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(2, "model.json")}, + {Name: "Model Two", DocumentID: 4, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(4, "model-two.json")}, + }, + Patterns: []officialgifts.Pattern{ + {Name: "Pattern", DocumentID: 3, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(3, "pattern.json")}, + {Name: "Pattern Two", DocumentID: 5, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}, Document: document(5, "pattern-two.json")}, + }, + Backdrops: []officialgifts.Backdrop{ + {Name: "Backdrop", BackdropID: 0, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}}, + {Name: "Backdrop Two", BackdropID: 1, Rarity: officialgifts.Rarity{Kind: "permille", Permille: &permille}}, + }, }, }} ctx := context.Background() @@ -911,7 +967,7 @@ func TestImportOfficialStarGiftPublishesThroughRealGiftService(t *testing.T) { t.Fatalf("catalog=%+v err=%v, want one imported gift", catalog, err) } preview, ok, err := giftService.CollectiblePreview(ctx, catalog[0].ID) - if err != nil || !ok || len(preview.Models) != 1 || len(preview.Patterns) != 1 { + if err != nil || !ok || len(preview.Models) != 2 || len(preview.Patterns) != 2 { t.Fatalf("preview=%+v ok=%v err=%v", preview, ok, err) } model := preview.Models[0].Document diff --git a/internal/adminapi/server_test.go b/internal/adminapi/server_test.go index eb97efd4..b4591396 100644 --- a/internal/adminapi/server_test.go +++ b/internal/adminapi/server_test.go @@ -120,11 +120,14 @@ func TestAdminAPIImportStarGiftMultipart(t *testing.T) { 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}]}` + 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":500,"sort_order":0,"file_key":"model-0"},{"name":"Sapphire","rarity_permille":500,"sort_order":1,"file_key":"model-1"}],"patterns":[{"name":"Stars","rarity_permille":500,"sort_order":0,"file_key":"pattern-0"},{"name":"Moons","rarity_permille":500,"sort_order":1,"file_key":"pattern-1"}],"backdrops":[{"name":"Night","backdrop_id":1,"center_color":1122867,"edge_color":2241348,"pattern_color":3359829,"text_color":16777215,"rarity_permille":500,"sort_order":0},{"name":"Day","backdrop_id":2,"center_color":11189196,"edge_color":7833753,"pattern_color":14544639,"text_color":1118481,"rarity_permille":500,"sort_order":1}]}` 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"} { + for key, name := range map[string]string{ + "model-0": "ruby.lottie", "model-1": "sapphire.lottie", + "pattern-0": "stars.tgs", "pattern-1": "moons.tgs", + } { part, err := writer.CreateFormFile(key, name) if err != nil { t.Fatal(err) @@ -146,8 +149,8 @@ func TestAdminAPIPublishStarGiftCollectiblesMultipart(t *testing.T) { 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 { + if svc.req.GiftID != 11 || len(svc.req.Models) != 2 || svc.req.Models[0].FileName != "ruby.lottie" || + string(svc.req.Patterns[0].Data) != "pattern-0" || len(svc.req.Backdrops) != 2 || svc.req.Backdrops[1].BackdropID != 2 { t.Fatalf("decoded collectible request = %+v", svc.req) } } diff --git a/internal/app/stargifts/animation_test.go b/internal/app/stargifts/animation_test.go index 3e56141a..65f2cc47 100644 --- a/internal/app/stargifts/animation_test.go +++ b/internal/app/stargifts/animation_test.go @@ -132,18 +132,18 @@ func TestCreateCatalogBundleMaterializesPublishableCollectibleDocuments(t *testi }, Collectible: &domain.StarGiftCollectibleWrite{ UpgradeStars: 100, SupplyTotal: 1000, SlugPrefix: "official-10", - Models: []domain.StarGiftCollectibleAttribute{{ - Kind: domain.StarGiftCollectibleModel, Name: "Model", RarityKind: domain.StarGiftRarityPermille, - RarityPermille: 1000, Animation: &animation, - }}, - Patterns: []domain.StarGiftCollectibleAttribute{{ - Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, - RarityPermille: 1000, Animation: &animation, - }}, - Backdrops: []domain.StarGiftCollectibleAttribute{{ - Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", RarityKind: domain.StarGiftRarityPermille, - RarityPermille: 1000, - }}, + Models: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleModel, Name: "Model", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation}, + {Kind: domain.StarGiftCollectibleModel, Name: "Model Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation}, + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation}, + {Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, Animation: &animation}, + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 1, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500}, + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 2, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500}, + }, Actor: "test", CommandID: "official-pool", OfficialGiftID: 10, SourceManifestSHA256: manifestSHA, }, @@ -151,7 +151,7 @@ func TestCreateCatalogBundleMaterializesPublishableCollectibleDocuments(t *testi if err != nil { t.Fatalf("create official collectible bundle: %v", err) } - if result.Collectible == nil || len(result.Collectible.Models) != 1 || len(result.Collectible.Patterns) != 1 { + if result.Collectible == nil || len(result.Collectible.Models) != 2 || len(result.Collectible.Patterns) != 2 { t.Fatalf("collectible result = %+v", result.Collectible) } model := result.Collectible.Models[0].Document diff --git a/internal/domain/star_gift.go b/internal/domain/star_gift.go index 5a8a54f1..06357481 100644 --- a/internal/domain/star_gift.go +++ b/internal/domain/star_gift.go @@ -3,6 +3,7 @@ package domain import ( "encoding/base64" "errors" + "fmt" "regexp" "strconv" "strings" @@ -139,6 +140,7 @@ func (k StarGiftAttributeRarityKind) Valid() bool { // StarGiftCollectibleAttribute 是已发布属性池的一项。RarityKind/RarityPermille // 是客户端展示事实;普通升级把非 crafted 的 permille 值当相对权重,不要求合计为 1000。 +// 每类仍必须提供至少两个客户端可区分的普通升级属性,否则 TDesktop 的升级滚动无法结束。 type StarGiftCollectibleAttribute struct { ID int64 CollectibleRevisionID int64 @@ -935,7 +937,10 @@ func ValidateStarGiftCollectibleDraft(write StarGiftCollectibleWrite) error { if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, false); err != nil { return err } - return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, false) + if err := validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, false); err != nil { + return err + } + return validateStarGiftUpgradePreviewPool(write, false) } // ValidateStarGiftCollectibleWrite validates a complete publish command. Published pools are @@ -950,7 +955,62 @@ func ValidateStarGiftCollectibleWrite(write StarGiftCollectibleWrite) error { if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, true); err != nil { return err } - return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, true) + if err := validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, true); err != nil { + return err + } + return validateStarGiftUpgradePreviewPool(write, true) +} + +// validateStarGiftUpgradePreviewPool protects the official-client animation contract. The +// preview response includes the target attribute plus the published selectable pool; TDesktop +// deduplicates models and patterns by document identity and needs a non-target item in every +// category before its spinner can transition to the finished state. +func validateStarGiftUpgradePreviewPool(write StarGiftCollectibleWrite, requireStoredAsset bool) error { + validateAnimated := func(kind StarGiftCollectibleAttributeKind, attributes []StarGiftCollectibleAttribute) error { + selectable := 0 + documents := make(map[int64]struct{}, len(attributes)) + for _, attribute := range attributes { + if attribute.RarityKind != StarGiftRarityPermille || attribute.Crafted { + continue + } + selectable++ + if requireStoredAsset { + if attribute.Document == nil { + return fmt.Errorf("%w: %s preview attribute has no document", ErrStarGiftCollectibleInvalid, kind) + } + documents[attribute.Document.ID] = struct{}{} + } + } + if selectable < 2 { + return fmt.Errorf("%w: %s preview requires at least two selectable attributes", ErrStarGiftCollectibleInvalid, kind) + } + if requireStoredAsset && len(documents) < 2 { + return fmt.Errorf("%w: %s preview requires at least two distinct documents", ErrStarGiftCollectibleInvalid, kind) + } + return nil + } + if err := validateAnimated(StarGiftCollectibleModel, write.Models); err != nil { + return err + } + if err := validateAnimated(StarGiftCollectiblePattern, write.Patterns); err != nil { + return err + } + seenBackdropIDs := make(map[int]struct{}, len(write.Backdrops)) + selectableBackdrops := 0 + for _, attribute := range write.Backdrops { + if attribute.RarityKind != StarGiftRarityPermille || attribute.Crafted { + continue + } + selectableBackdrops++ + if _, exists := seenBackdropIDs[attribute.BackdropID]; exists { + return fmt.Errorf("%w: duplicate backdrop_id %d", ErrStarGiftCollectibleInvalid, attribute.BackdropID) + } + seenBackdropIDs[attribute.BackdropID] = struct{}{} + } + if selectableBackdrops < 2 { + return fmt.Errorf("%w: backdrop preview requires at least two selectable attributes", ErrStarGiftCollectibleInvalid) + } + return nil } func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind StarGiftCollectibleAttributeKind, requireStoredAsset bool) error { diff --git a/internal/domain/star_gift_collectible_test.go b/internal/domain/star_gift_collectible_test.go index e8c37eb8..32ffb61c 100644 --- a/internal/domain/star_gift_collectible_test.go +++ b/internal/domain/star_gift_collectible_test.go @@ -48,13 +48,16 @@ func validCollectibleDraft() StarGiftCollectibleWrite { GiftID: 1, UpgradeStars: 25, SupplyTotal: 100, SlugPrefix: "official-1", CommandID: "test", Models: []StarGiftCollectibleAttribute{ {Kind: StarGiftCollectibleModel, Name: "Regular", RarityKind: StarGiftRarityPermille, RarityPermille: 922, Animation: animation}, + {Kind: StarGiftCollectibleModel, Name: "Regular Two", RarityKind: StarGiftRarityPermille, RarityPermille: 78, Animation: animation}, {Kind: StarGiftCollectibleModel, Name: "Crafted", RarityKind: StarGiftRarityLegendary, Crafted: true, Animation: animation}, }, Patterns: []StarGiftCollectibleAttribute{ {Kind: StarGiftCollectiblePattern, Name: "Pattern", RarityKind: StarGiftRarityPermille, RarityPermille: 989, Animation: animation}, + {Kind: StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: StarGiftRarityPermille, RarityPermille: 11, Animation: animation}, }, Backdrops: []StarGiftCollectibleAttribute{ {Kind: StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 0, RarityKind: StarGiftRarityPermille, RarityPermille: 999}, + {Kind: StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 1, RarityKind: StarGiftRarityPermille, RarityPermille: 1}, }, } } @@ -103,15 +106,59 @@ func storedCollectibleWrite() StarGiftCollectibleWrite { } write.Models[i].Blob = &FileBlob{LocationKey: "model"} } - write.Patterns[0].Document = &Document{ - ID: 200, MimeType: "application/x-tgsticker", - Attributes: []DocumentAttribute{{Kind: DocAttrCustomEmoji, Alt: "🎁", TextColor: true}}, - Thumbs: []PhotoSize{{Kind: PhotoSizeKindPath, Type: "j", Bytes: []byte{1}}}, + for i := range write.Patterns { + write.Patterns[i].Document = &Document{ + ID: int64(200 + i), MimeType: "application/x-tgsticker", + Attributes: []DocumentAttribute{{Kind: DocAttrCustomEmoji, Alt: "🎁", TextColor: true}}, + Thumbs: []PhotoSize{{Kind: PhotoSizeKindPath, Type: "j", Bytes: []byte{1}}}, + } + write.Patterns[i].Blob = &FileBlob{LocationKey: "pattern"} } - write.Patterns[0].Blob = &FileBlob{LocationKey: "pattern"} return write } +func TestValidateStarGiftCollectibleDraftRequiresClientSafePreviewPool(t *testing.T) { + tests := map[string]func(*StarGiftCollectibleWrite){ + "one selectable model": func(write *StarGiftCollectibleWrite) { + write.Models = append(write.Models[:1], write.Models[2:]...) + }, + "one selectable pattern": func(write *StarGiftCollectibleWrite) { + write.Patterns = write.Patterns[:1] + }, + "one selectable backdrop": func(write *StarGiftCollectibleWrite) { + write.Backdrops = write.Backdrops[:1] + }, + "duplicate backdrop id": func(write *StarGiftCollectibleWrite) { + write.Backdrops[1].BackdropID = write.Backdrops[0].BackdropID + }, + } + for name, mutate := range tests { + t.Run(name, func(t *testing.T) { + write := validCollectibleDraft() + mutate(&write) + if err := ValidateStarGiftCollectibleDraft(write); !errors.Is(err, ErrStarGiftCollectibleInvalid) { + t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err) + } + }) + } +} + +func TestValidateStarGiftCollectibleWriteRequiresDistinctPreviewDocuments(t *testing.T) { + for _, kind := range []StarGiftCollectibleAttributeKind{StarGiftCollectibleModel, StarGiftCollectiblePattern} { + t.Run(string(kind), func(t *testing.T) { + write := storedCollectibleWrite() + if kind == StarGiftCollectibleModel { + write.Models[1].Document = write.Models[0].Document + } else { + write.Patterns[1].Document = write.Patterns[0].Document + } + if err := ValidateStarGiftCollectibleWrite(write); !errors.Is(err, ErrStarGiftCollectibleInvalid) { + t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err) + } + }) + } +} + func TestValidateStarGiftCollectibleWriteRequiresExactDocumentRoles(t *testing.T) { if err := ValidateStarGiftCollectibleWrite(storedCollectibleWrite()); err != nil { t.Fatalf("valid stored collectible: %v", err) diff --git a/internal/rpc/payments_star_gifts_rpc_test.go b/internal/rpc/payments_star_gifts_rpc_test.go index 8e615e9e..8468e396 100644 --- a/internal/rpc/payments_star_gifts_rpc_test.go +++ b/internal/rpc/payments_star_gifts_rpc_test.go @@ -594,16 +594,19 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test t.Fatalf("gift service = %T", r.deps.Gifts) } model := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8101, "Aurora") + modelTwo := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8104, "Aurora Two") crafted := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8103, "Crafted Aurora") crafted.Crafted = true crafted.RarityKind = domain.StarGiftRarityLegendary crafted.RarityPermille = 0 pattern := collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8102, "Orbit") + patternTwo := collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8105, "Orbit Two") backdrop := collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 1, "Midnight") + backdropTwo := collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 2, "Daylight") if _, err := giftService.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 500, SlugPrefix: "cake", - Models: []domain.StarGiftCollectibleAttribute{model, crafted}, Patterns: []domain.StarGiftCollectibleAttribute{pattern}, - Backdrops: []domain.StarGiftCollectibleAttribute{backdrop}, Actor: "test", CommandID: "collectible-rpc", + Models: []domain.StarGiftCollectibleAttribute{model, crafted, modelTwo}, Patterns: []domain.StarGiftCollectibleAttribute{pattern, patternTwo}, + Backdrops: []domain.StarGiftCollectibleAttribute{backdrop, backdropTwo}, Actor: "test", CommandID: "collectible-rpc", }); err != nil { t.Fatalf("publish collectible pool: %v", err) } @@ -615,11 +618,11 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test } preview, err := r.onPaymentsGetStarGiftUpgradePreview(ownerCtx, gift.ID) - if err != nil || len(preview.SampleAttributes) != 3 { + if err != nil || len(preview.SampleAttributes) != 6 { t.Fatalf("upgrade preview = %#v err %v", preview, err) } attributes, err := r.onPaymentsGetStarGiftUpgradeAttributes(ownerCtx, gift.ID) - if err != nil || len(attributes.Attributes) != 4 { + if err != nil || len(attributes.Attributes) != 7 { t.Fatalf("upgrade attributes = %#v err %v", attributes, err) } craftedTG, ok := attributes.Attributes[1].(*tg.StarGiftAttributeModel) @@ -1004,10 +1007,19 @@ func TestStarGiftChannelSaga(t *testing.T) { 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", + Models: []domain.StarGiftCollectibleAttribute{ + collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8201, "Aurora"), + collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8204, "Aurora Two"), + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8202, "Orbit"), + collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8205, "Orbit Two"), + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 2, "Midnight"), + collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 3, "Daylight"), + }, + Actor: "test", CommandID: "channel-collectible-rpc", }); err != nil { t.Fatalf("publish channel collectible pool: %v", err) } diff --git a/internal/store/postgres/star_gift_collectibles_integration_test.go b/internal/store/postgres/star_gift_collectibles_integration_test.go index b73f8106..9d049f20 100644 --- a/internal/store/postgres/star_gift_collectibles_integration_test.go +++ b/internal/store/postgres/star_gift_collectibles_integration_test.go @@ -43,24 +43,29 @@ func TestStarGiftCollectibleUpgradeAggregatePostgres(t *testing.T) { Document: collectibleTestDocumentPtr(baseDocumentID+3, "crafted-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "crafted-model"), Animation: collectibleTestAnimationPtr("crafted-model.tgs"), OfficialDocumentID: 5100000000000000003, + }, { + Kind: domain.StarGiftCollectibleModel, Name: "Solar", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 78, + Document: collectibleTestDocumentPtr(baseDocumentID+4, "model-two.tgs"), + Blob: collectibleTestBlobPtr(baseDocumentID+4, "model-two"), Animation: collectibleTestAnimationPtr("model-two.tgs"), + OfficialDocumentID: 5100000000000000004, }}, - Patterns: []domain.StarGiftCollectibleAttribute{{ - Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 989, - Document: collectibleTestPatternDocumentPtr(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, - RarityKind: domain.StarGiftRarityPermille, RarityPermille: 999, - }}, + Patterns: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 989, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}, + {Kind: domain.StarGiftCollectiblePattern, Name: "Rings", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 11, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "pattern-two"), Animation: collectibleTestAnimationPtr("pattern-two.tgs")}, + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Midnight", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 999}, + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Daylight", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1}, + }, Actor: "integration", CommandID: "collectibles-" + suffix, OfficialGiftID: 5170145012310081615, SourceManifestSHA256: make([]byte, 32), }) if err != nil { t.Fatalf("publish collectible pool: %v", err) } - if !poolRevision.Published || poolRevision.Issued != 0 || len(poolRevision.Models) != 2 || len(poolRevision.Patterns) != 1 || len(poolRevision.Backdrops) != 1 || + if !poolRevision.Published || poolRevision.Issued != 0 || len(poolRevision.Models) != 3 || len(poolRevision.Patterns) != 2 || len(poolRevision.Backdrops) != 2 || !poolRevision.Models[1].Crafted || poolRevision.Models[1].RarityKind != domain.StarGiftRarityLegendary || poolRevision.Models[1].RarityPermille != 0 || poolRevision.Models[0].OfficialDocumentID != 5100000000000000001 { t.Fatalf("published pool = %+v", poolRevision) @@ -353,21 +358,22 @@ WHERE owner_user_id=$1 AND msg_id=$2`, sender.ID, ownerMessage.ID).Scan(&senderA } 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", RarityKind: domain.StarGiftRarityPermille, 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", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestPatternDocumentPtr(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, - RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - }}, + Models: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleModel, Name: "Nova", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestDocumentPtr(baseDocumentID+101, "nova-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+101, "nova-model"), Animation: collectibleTestAnimationPtr("nova-model.tgs")}, + {Kind: domain.StarGiftCollectibleModel, Name: "Nova Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestDocumentPtr(baseDocumentID+103, "nova-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+103, "nova-model-two"), Animation: collectibleTestAnimationPtr("nova-model-two.tgs")}, + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectiblePattern, Name: "Ray", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+102, "nova-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+102, "nova-pattern"), Animation: collectibleTestAnimationPtr("nova-pattern.tgs")}, + {Kind: domain.StarGiftCollectiblePattern, Name: "Ray Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+104, "nova-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+104, "nova-pattern-two"), Animation: collectibleTestAnimationPtr("nova-pattern-two.tgs")}, + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Void", BackdropID: 2, CenterColor: 0x101010, EdgeColor: 0x202020, PatternColor: 0x303030, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500}, + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Light", BackdropID: 3, CenterColor: 0xeeeeee, EdgeColor: 0xcccccc, PatternColor: 0xaaaaaa, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500}, + }, Actor: "integration", CommandID: "soldout-pool-" + suffix, }) if err != nil { @@ -437,6 +443,65 @@ WHERE owner_user_id=$1 AND msg_id=$2`, sender.ID, ownerMessage.ID).Scan(&senderA } } +func TestStarGiftCollectiblePreviewActivationGuardPostgres(t *testing.T) { + pool := testPool(t) + ctx := context.Background() + suffix := randomSuffix(t) + baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000 + gifts := NewStarGiftStore(pool) + entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + Title: "Unsafe Preview " + suffix, Stars: 10, ConvertStars: 5, Enabled: true, + Document: collectibleTestDocument(baseDocumentID, "unsafe-preview.tgs"), Blob: collectibleTestBlob(baseDocumentID, "unsafe-preview"), + Animation: collectibleTestAnimation("unsafe-preview.tgs"), Actor: "integration", CommandID: "unsafe-preview-catalog-" + suffix, + }) + if err != nil { + t.Fatalf("create unsafe-preview catalog: %v", err) + } + var revisionID int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO star_gift_collectible_revisions + (gift_id, revision, upgrade_stars, supply_total, slug_prefix, status, created_by, command_id) +VALUES ($1, 1, 10, 10, $2, 'draft', 'integration', $3) +RETURNING id`, entry.Gift.ID, "unsafe-"+suffix, "unsafe-preview-pool-"+suffix).Scan(&revisionID); err != nil { + t.Fatalf("insert unsafe-preview revision: %v", err) + } + animation := collectibleTestAnimation("unsafe-preview-attribute.tgs") + if _, err := pool.Exec(ctx, ` +INSERT INTO star_gift_collectible_models + (collectible_revision_id, name, document_id, animation_json, animation_sha256, source_name, source_format, + width, height, frame_rate, in_point, out_point, rarity_kind, rarity_permille, crafted, sort_order) +VALUES ($1, 'Only Model', $2, $3::jsonb, $4, 'model.tgs', 'tgs', 512, 512, 30, 0, 60, 'permille', 1000, false, 0)`, + revisionID, baseDocumentID, string(animation.JSON), animation.SHA256); err != nil { + t.Fatalf("insert unsafe-preview model: %v", err) + } + if _, err := pool.Exec(ctx, ` +INSERT INTO star_gift_collectible_patterns + (collectible_revision_id, name, document_id, animation_json, animation_sha256, source_name, source_format, + width, height, frame_rate, in_point, out_point, rarity_kind, rarity_permille, sort_order) +VALUES ($1, 'Only Pattern', $2, $3::jsonb, $4, 'pattern.tgs', 'tgs', 512, 512, 30, 0, 60, 'permille', 1000, 0)`, + revisionID, baseDocumentID, string(animation.JSON), animation.SHA256); err != nil { + t.Fatalf("insert unsafe-preview pattern: %v", err) + } + if _, err := pool.Exec(ctx, ` +INSERT INTO star_gift_collectible_backdrops + (collectible_revision_id, name, backdrop_id, center_color, edge_color, pattern_color, text_color, + rarity_kind, rarity_permille, sort_order) +VALUES ($1, 'Only Backdrop', 1, 1, 2, 3, 4, 'permille', 1000, 0)`, revisionID); err != nil { + t.Fatalf("insert unsafe-preview backdrop: %v", err) + } + if _, err := pool.Exec(ctx, ` +UPDATE star_gift_collectible_revisions SET status='published', published_at=now() WHERE id=$1`, revisionID); err != nil { + t.Fatalf("publish unsafe-preview revision directly: %v", err) + } + if _, err := pool.Exec(ctx, `UPDATE star_gift_catalog SET collectible_revision_id=$2 WHERE gift_id=$1`, entry.Gift.ID, revisionID); err == nil { + t.Fatal("database activated a collectible preview pool with one client-distinct item per category") + } + var activeRevisionID *int64 + if err := pool.QueryRow(ctx, `SELECT collectible_revision_id FROM star_gift_catalog WHERE gift_id=$1`, entry.Gift.ID).Scan(&activeRevisionID); err != nil || activeRevisionID != nil { + t.Fatalf("unsafe preview activation pointer=%v err=%v, want null", activeRevisionID, err) + } +} + func TestStarGiftUpgradeWithoutCraftedModelDoesNotAdvertiseCraft(t *testing.T) { pool := testPool(t) ctx := context.Background() @@ -460,27 +525,28 @@ func TestStarGiftUpgradeWithoutCraftedModelDoesNotAdvertiseCraft(t *testing.T) { } revision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "no-craft-" + suffix, - Models: []domain.StarGiftCollectibleAttribute{{ - Kind: domain.StarGiftCollectibleModel, Name: "Ordinary", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestDocumentPtr(baseDocumentID+1, "no-craft-model.tgs"), - Blob: collectibleTestBlobPtr(baseDocumentID+1, "no-craft-model"), Animation: collectibleTestAnimationPtr("no-craft-model.tgs"), - }}, - Patterns: []domain.StarGiftCollectibleAttribute{{ - Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "no-craft-pattern.tgs"), - Blob: collectibleTestBlobPtr(baseDocumentID+2, "no-craft-pattern"), Animation: collectibleTestAnimationPtr("no-craft-pattern.tgs"), - }}, - Backdrops: []domain.StarGiftCollectibleAttribute{{ - Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 1, - CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, - RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - }}, + Models: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleModel, Name: "Ordinary", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestDocumentPtr(baseDocumentID+1, "no-craft-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "no-craft-model"), Animation: collectibleTestAnimationPtr("no-craft-model.tgs")}, + {Kind: domain.StarGiftCollectibleModel, Name: "Ordinary Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestDocumentPtr(baseDocumentID+3, "no-craft-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "no-craft-model-two"), Animation: collectibleTestAnimationPtr("no-craft-model-two.tgs")}, + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "no-craft-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "no-craft-pattern"), Animation: collectibleTestAnimationPtr("no-craft-pattern.tgs")}, + {Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+4, "no-craft-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "no-craft-pattern-two"), Animation: collectibleTestAnimationPtr("no-craft-pattern-two.tgs")}, + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500}, + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500}, + }, Actor: "integration", CommandID: "no-craft-pool-" + suffix, }) if err != nil { t.Fatalf("publish no-craft pool: %v", err) } - if len(revision.Models) != 1 || revision.Models[0].Crafted { + if len(revision.Models) != 2 || revision.Models[0].Crafted || revision.Models[1].Crafted { t.Fatalf("no-craft pool models = %+v", revision.Models) } diff --git a/internal/store/postgres/star_gift_lifecycle_integration_test.go b/internal/store/postgres/star_gift_lifecycle_integration_test.go index 2102abc6..d91cb45f 100644 --- a/internal/store/postgres/star_gift_lifecycle_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_integration_test.go @@ -48,12 +48,19 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) { Document: collectibleTestDocumentPtr(baseDocumentID+1, "model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "model"), Animation: collectibleTestAnimationPtr("model.tgs")}, {Kind: domain.StarGiftCollectibleModel, Name: "Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true, Document: collectibleTestDocumentPtr(baseDocumentID+2, "crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "crafted"), Animation: collectibleTestAnimationPtr("crafted.tgs")}, + {Kind: domain.StarGiftCollectibleModel, Name: "Base Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+4, "model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "model-two"), Animation: collectibleTestAnimationPtr("model-two.tgs")}, + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}, + {Kind: domain.StarGiftCollectiblePattern, Name: "Orbit Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "pattern-two"), Animation: collectibleTestAnimationPtr("pattern-two.tgs")}, + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Night", BackdropID: 77, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}, + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Day", BackdropID: 78, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}, }, - Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}}, - Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Night", BackdropID: 77, - CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, - RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}}, Actor: "integration", CommandID: "lifecycle-pool-" + suffix, }); err != nil { t.Fatalf("publish lifecycle pool: %v", err) @@ -760,12 +767,19 @@ func TestStarGiftChannelLifecycleAtomicPostgres(t *testing.T) { Document: collectibleTestDocumentPtr(baseDocumentID+1, "channel-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "channel-model"), Animation: collectibleTestAnimationPtr("channel-model.tgs")}, {Kind: domain.StarGiftCollectibleModel, Name: "Channel Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true, Document: collectibleTestDocumentPtr(baseDocumentID+2, "channel-crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "channel-crafted"), Animation: collectibleTestAnimationPtr("channel-crafted.tgs")}, + {Kind: domain.StarGiftCollectibleModel, Name: "Channel Model Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+4, "channel-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "channel-model-two"), Animation: collectibleTestAnimationPtr("channel-model-two.tgs")}, + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectiblePattern, Name: "Channel Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "channel-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "channel-pattern"), Animation: collectibleTestAnimationPtr("channel-pattern.tgs")}, + {Kind: domain.StarGiftCollectiblePattern, Name: "Channel Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "channel-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "channel-pattern-two"), Animation: collectibleTestAnimationPtr("channel-pattern-two.tgs")}, + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Channel Backdrop", BackdropID: 88, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}, + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Channel Backdrop Two", BackdropID: 89, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}, }, - Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Channel Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "channel-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "channel-pattern"), Animation: collectibleTestAnimationPtr("channel-pattern.tgs")}}, - Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Channel Backdrop", BackdropID: 88, - CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, - RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}}, Actor: "integration", CommandID: "channel-gift-pool-" + suffix, }); err != nil { t.Fatalf("publish channel gift pool: %v", err) @@ -1088,13 +1102,19 @@ func TestStarGiftCraftFailureConsumesThreeInputsPostgres(t *testing.T) { Document: collectibleTestDocumentPtr(baseDocumentID+1, "base.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "base"), Animation: collectibleTestAnimationPtr("base.tgs")}, {Kind: domain.StarGiftCollectibleModel, Name: "Crafted", RarityKind: domain.StarGiftRarityLegendary, Crafted: true, Document: collectibleTestDocumentPtr(baseDocumentID+2, "crafted.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "crafted"), Animation: collectibleTestAnimationPtr("crafted.tgs")}, + {Kind: domain.StarGiftCollectibleModel, Name: "Base Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+4, "base-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "base-two"), Animation: collectibleTestAnimationPtr("base-two.tgs")}, + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}, + {Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, + Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "pattern-two"), Animation: collectibleTestAnimationPtr("pattern-two.tgs")}, + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 88, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}, + {Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 89, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}, }, - Patterns: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", - RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000, - Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")}}, - Backdrops: []domain.StarGiftCollectibleAttribute{{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 88, - CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, - RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000}}, Actor: "integration", CommandID: "three-input-pool-" + suffix, }); err != nil { t.Fatalf("publish three-input collectible: %v", err) diff --git a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go index f908b6d3..eb0a5382 100644 --- a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go @@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) { if err != nil { t.Fatalf("migrate star gift lifecycle schema: %v", err) } - if status.Dirty || status.Empty || status.Version != 129 { - t.Fatalf("migration status = %+v, want clean version 129", status) + if status.Dirty || status.Empty || status.Version != 132 { + t.Fatalf("migration status = %+v, want clean version 132", status) } } diff --git a/internal/store/postgres/star_gift_official_import_integration_test.go b/internal/store/postgres/star_gift_official_import_integration_test.go index e4baffc9..da526ed7 100644 --- a/internal/store/postgres/star_gift_official_import_integration_test.go +++ b/internal/store/postgres/star_gift_official_import_integration_test.go @@ -22,7 +22,7 @@ func TestOfficialStarGiftBundleIsAtomicPostgres(t *testing.T) { attribute := func(kind domain.StarGiftCollectibleAttributeKind, id int64, name string) domain.StarGiftCollectibleAttribute { value := domain.StarGiftCollectibleAttribute{Kind: kind, Name: name, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 918} if kind == domain.StarGiftCollectibleBackdrop { - value.BackdropID = 0 + value.BackdropID = int(id) value.CenterColor, value.EdgeColor, value.PatternColor, value.TextColor = 1, 2, 3, 4 return value } @@ -46,10 +46,19 @@ func TestOfficialStarGiftBundleIsAtomicPostgres(t *testing.T) { }, Collectible: &domain.StarGiftCollectibleWrite{ UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "official-" + suffix, - Models: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectibleModel, baseID+1, "model")}, - Patterns: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectiblePattern, baseID+2, "pattern")}, - Backdrops: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop")}, - Actor: "integration", CommandID: "official-pool-" + suffix, + Models: []domain.StarGiftCollectibleAttribute{ + attribute(domain.StarGiftCollectibleModel, baseID+1, "model"), + attribute(domain.StarGiftCollectibleModel, baseID+3, "model-two"), + }, + Patterns: []domain.StarGiftCollectibleAttribute{ + attribute(domain.StarGiftCollectiblePattern, baseID+2, "pattern"), + attribute(domain.StarGiftCollectiblePattern, baseID+4, "pattern-two"), + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop"), + attribute(domain.StarGiftCollectibleBackdrop, 1, "backdrop-two"), + }, + Actor: "integration", CommandID: "official-pool-" + suffix, OfficialGiftID: 5170145012310081615, SourceManifestSHA256: manifestSHA, }, } @@ -81,9 +90,15 @@ FROM star_gift_catalog_revisions WHERE id=$1`, result.Catalog.Gift.RevisionID).S attribute(domain.StarGiftCollectibleModel, baseID+101, "duplicate"), attribute(domain.StarGiftCollectibleModel, baseID+102, "duplicate"), }, - Patterns: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectiblePattern, baseID+103, "pattern")}, - Backdrops: []domain.StarGiftCollectibleAttribute{attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop")}, - Actor: "integration", CommandID: "rollback-pool-" + suffix, + Patterns: []domain.StarGiftCollectibleAttribute{ + attribute(domain.StarGiftCollectiblePattern, baseID+103, "pattern"), + attribute(domain.StarGiftCollectiblePattern, baseID+104, "pattern-two"), + }, + Backdrops: []domain.StarGiftCollectibleAttribute{ + attribute(domain.StarGiftCollectibleBackdrop, 0, "backdrop"), + attribute(domain.StarGiftCollectibleBackdrop, 1, "backdrop-two"), + }, + Actor: "integration", CommandID: "rollback-pool-" + suffix, } if _, err := store.CreateCatalogBundle(ctx, failing); !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) { t.Fatalf("failing bundle err=%v", err) From 8e00a581122d9118dfb15aaca3d87a2b00b12e1b Mon Sep 17 00:00:00 2001 From: A
Date: Wed, 22 Jul 2026 13:07:37 +0800 Subject: [PATCH 14/28] fix: sync owner-only megagroup creation --- internal/rpc/channels_legacy_chat.go | 20 ++- internal/rpc/errors.go | 2 - internal/rpc/messages_create_chat_test.go | 209 ++++++++++++++++++++-- 3 files changed, 205 insertions(+), 26 deletions(-) diff --git a/internal/rpc/channels_legacy_chat.go b/internal/rpc/channels_legacy_chat.go index 2ec6cbec..a051a51b 100644 --- a/internal/rpc/channels_legacy_chat.go +++ b/internal/rpc/channels_legacy_chat.go @@ -35,9 +35,6 @@ func (r *Router) onMessagesCreateChat(ctx context.Context, req *tg.MessagesCreat zap.Int("member_ids", len(memberIDs)), zap.Int64s("member_user_ids", memberIDs), ) - if len(memberIDs) == 0 { - return nil, usersTooFewErr() - } createRes, err := r.deps.Channels.CreateMegagroupFromCreateChat(ctx, userID, domain.CreateChannelRequest{ CreatorUserID: userID, Title: req.Title, @@ -63,16 +60,25 @@ func (r *Router) onMessagesCreateChat(ctx context.Context, req *tg.MessagesCreat } cache := newViewerPeerCache(r) - updates := r.channelOperationUpdatesWithPeerCache(ctx, userID, responseRes, cache) + canonicalUpdates := r.channelOperationUpdatesWithPeerCache(ctx, userID, responseRes, cache) + var inviteUpdates *tg.Updates + if inviteRes.Event.Pts != 0 { + inviteUpdates = r.channelOperationUpdatesWithPeerCache(ctx, userID, inviteRes, cache) + if inviteUpdates != nil { + canonicalUpdates.Updates = append(canonicalUpdates.Updates, inviteUpdates.Updates...) + } + } + updates := canonicalUpdates if createChatNeedsLegacyChat(ctx) { updates = r.tdesktopCreateChatUpdatesWithPeerCache(ctx, userID, responseRes, cache) - } - if inviteRes.Event.Pts != 0 { - inviteUpdates := r.channelOperationUpdatesWithPeerCache(ctx, userID, inviteRes, cache) if inviteUpdates != nil { updates.Updates = append(updates.Updates, inviteUpdates.Updates...) } } + // The rpc_result reaches only the calling session. Keep the creator's other + // sessions in sync with the same canonical channel state; compatibility-only + // legacy chat projection is needed solely by the synchronous create callback. + r.pushUserUpdates(ctx, userID, canonicalUpdates) if inviteRes.Event.Pts != 0 { r.pushChannelExplicitUpdates(ctx, userID, inviteRes.Channel.ID, memberIDs, func(viewerUserID int64) *tg.Updates { return r.channelOperationUpdatesWithPeerCache(ctx, viewerUserID, inviteRes, cache) diff --git a/internal/rpc/errors.go b/internal/rpc/errors.go index 9a969948..fb129b09 100644 --- a/internal/rpc/errors.go +++ b/internal/rpc/errors.go @@ -243,8 +243,6 @@ func authTokenExceptionErr() error { return tgerr.New(400, "AUTH_TOKEN_EXCEPTION func userIDInvalidErr() error { return tgerr.New(400, "USER_ID_INVALID") } -func usersTooFewErr() error { return tgerr.New(400, "USERS_TOO_FEW") } - func firstNameInvalidErr() error { return tgerr.New(400, "FIRSTNAME_INVALID") } func aboutTooLongErr() error { return tgerr.New(400, "ABOUT_TOO_LONG") } diff --git a/internal/rpc/messages_create_chat_test.go b/internal/rpc/messages_create_chat_test.go index a7f08eb1..53108baa 100644 --- a/internal/rpc/messages_create_chat_test.go +++ b/internal/rpc/messages_create_chat_test.go @@ -6,7 +6,6 @@ import ( "github.com/iamxvbaba/td/clock" "github.com/iamxvbaba/td/tg" "go.uber.org/zap/zaptest" - "strings" appchannels "telesrv/internal/app/channels" appdialogs "telesrv/internal/app/dialogs" appusers "telesrv/internal/app/users" @@ -120,22 +119,198 @@ func TestMessagesCreateChatCreatesMegagroupAndDialogsRPC(t *testing.T) { } } -func TestMessagesCreateChatRejectsEmptyInviteListRPC(t *testing.T) { - ctx := context.Background() - userStore := memory.NewUserStore() - owner, err := userStore.Create(ctx, domain.User{AccessHash: 21, Phone: "15550001021", FirstName: "Owner"}) - if err != nil { - t.Fatalf("create owner: %v", err) +func TestMessagesCreateChatCreatesOwnerOnlyMegagroupRPC(t *testing.T) { + tests := []struct { + name string + phone string + users func(domain.User) []tg.InputUserClass + }{ + { + name: "empty vector", + phone: "15550001021", + users: func(domain.User) []tg.InputUserClass { return nil }, + }, + { + name: "self references normalize to empty", + phone: "15550001022", + users: func(owner domain.User) []tg.InputUserClass { + return []tg.InputUserClass{ + &tg.InputUserSelf{}, + &tg.InputUser{UserID: owner.ID, AccessHash: owner.AccessHash}, + &tg.InputUserSelf{}, + } + }, + }, } - r := New(Config{}, Deps{ - Users: appusers.NewService(userStore), - Channels: appchannels.NewService(memory.NewChannelStore()), - }, zaptest.NewLogger(t), clock.System) - if _, err := r.onMessagesCreateChat(WithUserID(ctx, owner.ID), &tg.MessagesCreateChatRequest{ - Title: "No Invitees", - }); err == nil || !strings.Contains(err.Error(), "USERS_TOO_FEW") { - t.Fatalf("create chat without users err = %v, want USERS_TOO_FEW", err) + for index, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, err := userStore.Create(ctx, domain.User{AccessHash: int64(21 + index), Phone: tc.phone, FirstName: "Owner"}) + if err != nil { + t.Fatalf("create owner: %v", err) + } + channelStore := memory.NewChannelStore() + channels := appchannels.NewService(channelStore) + sessions := &captureScopedSessions{captureSessions: &captureSessions{}} + r := New(Config{}, Deps{ + Users: appusers.NewService(userStore), + Channels: channels, + Dialogs: appdialogs.NewService(memory.NewDialogStore(), channelStore), + Sessions: sessions, + }, zaptest.NewLogger(t), clock.System) + + authKeyID := [8]byte{0x60, byte(index + 1)} + sessionID := int64(70 + index) + requestCtx := WithClientInfo( + WithSessionID(WithAuthKeyID(WithUserID(ctx, owner.ID), authKeyID), sessionID), + ClientInfo{DeviceModel: "Android", AppVersion: "12.7.3"}, + ) + invited, err := r.onMessagesCreateChat(requestCtx, &tg.MessagesCreateChatRequest{ + Users: tc.users(owner), + Title: "Owner Only Group", + }) + if err != nil { + t.Fatalf("create owner-only chat: %v", err) + } + if len(invited.MissingInvitees) != 0 { + t.Fatalf("missing invitees = %+v, want empty", invited.MissingInvitees) + } + + updates, ok := invited.Updates.(*tg.Updates) + if !ok || len(updates.Chats) != 2 { + t.Fatalf("updates = %T %+v, want legacy chat + channel", invited.Updates, invited.Updates) + } + legacy, ok := updates.Chats[0].(*tg.Chat) + if !ok || !legacy.Deactivated || !legacy.Creator || legacy.ParticipantsCount != 1 { + t.Fatalf("legacy chat = %#v, want migrated creator-only chat", updates.Chats[0]) + } + channel, ok := updates.Chats[1].(*tg.Channel) + if !ok || !channel.Megagroup || channel.Broadcast || !channel.Creator || channel.ParticipantsCount != 1 { + t.Fatalf("channel = %#v, want owner-only megagroup", updates.Chats[1]) + } + migrated, ok := legacy.GetMigratedTo() + if !ok { + t.Fatal("legacy chat missing migrated_to") + } + migratedChannel, ok := migrated.(*tg.InputChannel) + if !ok || migratedChannel.ChannelID != channel.ID || migratedChannel.AccessHash != channel.AccessHash { + t.Fatalf("migrated_to = %#v, want channel %d/%d", migrated, channel.ID, channel.AccessHash) + } + if len(updates.Updates) != 2 { + t.Fatalf("updates len = %d, want create service message + channel refresh only", len(updates.Updates)) + } + created, ok := updates.Updates[0].(*tg.UpdateNewChannelMessage) + if !ok || created.Pts != 1 || created.PtsCount != 1 { + t.Fatalf("create update = %#v, want pts=1/count=1", updates.Updates[0]) + } + createdMessage, ok := created.Message.(*tg.MessageService) + if !ok { + t.Fatalf("create message = %T, want messageService", created.Message) + } + if _, ok := createdMessage.Action.(*tg.MessageActionChannelCreate); !ok { + t.Fatalf("create action = %T, want messageActionChannelCreate", createdMessage.Action) + } + if refresh, ok := updates.Updates[1].(*tg.UpdateChannel); !ok || refresh.ChannelID != channel.ID { + t.Fatalf("refresh = %#v, want channel %d", updates.Updates[1], channel.ID) + } + if len(updates.Users) != 1 { + t.Fatalf("updates users len = %d, want creator only", len(updates.Users)) + } + if user, ok := updates.Users[0].(*tg.User); !ok || user.ID != owner.ID { + t.Fatalf("updates user = %#v, want owner %d", updates.Users[0], owner.ID) + } + + pushedUserIDs := sessions.pushedUserIDs() + if len(pushedUserIDs) != 1 || pushedUserIDs[0] != owner.ID { + t.Fatalf("push user ids = %v, want creator's other sessions", pushedUserIDs) + } + push := sessions.snapshot() + if push.sessionID != sessionID || sessions.scopedAuthKey() != authKeyID { + t.Fatalf("push exclusion = auth_key %x session %d, want %x/%d", sessions.scopedAuthKey(), push.sessionID, authKeyID, sessionID) + } + canonicalPush, ok := sessions.userMessage.(*tg.Updates) + if !ok || len(canonicalPush.Chats) != 1 { + t.Fatalf("creator push = %T %+v, want canonical channel updates", sessions.userMessage, sessions.userMessage) + } + if pushedChannel, ok := canonicalPush.Chats[0].(*tg.Channel); !ok || pushedChannel.ID != channel.ID { + t.Fatalf("creator pushed chat = %#v, want channel %d", canonicalPush.Chats[0], channel.ID) + } + + participants, err := r.onChannelsGetParticipants(requestCtx, &tg.ChannelsGetParticipantsRequest{ + Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}, + Filter: &tg.ChannelParticipantsRecent{}, + Limit: 10, + }) + if err != nil { + t.Fatalf("get participants: %v", err) + } + participantList, ok := participants.(*tg.ChannelsChannelParticipants) + if !ok || participantList.Count != 1 || len(participantList.Participants) != 1 || len(participantList.Users) != 1 { + t.Fatalf("participants = %T %+v, want creator only", participants, participants) + } + if creator, ok := participantList.Participants[0].(*tg.ChannelParticipantCreator); !ok || creator.UserID != owner.ID { + t.Fatalf("participant = %#v, want creator %d", participantList.Participants[0], owner.ID) + } + + view, err := channels.GetChannel(ctx, owner.ID, channel.ID) + if err != nil { + t.Fatalf("get created channel: %v", err) + } + if view.Self.Role != domain.ChannelRoleCreator || view.Self.Status != domain.ChannelMemberActive { + t.Fatalf("self membership = %+v, want active creator", view.Self) + } + if view.Dialog.TopMessageID != createdMessage.ID || view.Dialog.ReadInboxMaxID != createdMessage.ID || view.Dialog.UnreadCount != 0 { + t.Fatalf("creator dialog = %+v, want creation message %d read", view.Dialog, createdMessage.ID) + } + + var dialogsBuffer bin.Buffer + if err := (&tg.MessagesGetDialogsRequest{OffsetPeer: &tg.InputPeerEmpty{}, Limit: 20}).Encode(&dialogsBuffer); err != nil { + t.Fatalf("encode getDialogs: %v", err) + } + dialogsResult, err := r.Dispatch(requestCtx, authKeyID, sessionID, &dialogsBuffer) + if err != nil { + t.Fatalf("dispatch getDialogs: %v", err) + } + dialogs, ok := dialogsResult.(*tg.MessagesDialogs) + if !ok || len(dialogs.Dialogs) != 1 || len(dialogs.Chats) != 1 || len(dialogs.Messages) != 1 { + t.Fatalf("dialogs = %T %+v, want persisted owner-only group", dialogsResult, dialogsResult) + } + + var historyBuffer bin.Buffer + if err := (&tg.MessagesGetHistoryRequest{ + Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}, + Limit: 20, + }).Encode(&historyBuffer); err != nil { + t.Fatalf("encode getHistory: %v", err) + } + historyResult, err := r.Dispatch(requestCtx, authKeyID, sessionID, &historyBuffer) + if err != nil { + t.Fatalf("dispatch getHistory: %v", err) + } + history, ok := historyResult.(*tg.MessagesChannelMessages) + if !ok || len(history.Messages) != 1 { + t.Fatalf("history = %T %+v, want creation service message", historyResult, historyResult) + } + if message, ok := history.Messages[0].(*tg.MessageService); !ok || message.ID != createdMessage.ID { + t.Fatalf("history message = %#v, want creation service %d", history.Messages[0], createdMessage.ID) + } + + difference, err := r.onUpdatesGetChannelDifference(requestCtx, &tg.UpdatesGetChannelDifferenceRequest{ + Channel: &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}, + Filter: &tg.ChannelMessagesFilterEmpty{}, + Pts: 0, + Limit: 10, + }) + if err != nil { + t.Fatalf("getChannelDifference from pts=0: %v", err) + } + fullDifference, ok := difference.(*tg.UpdatesChannelDifference) + if !ok || fullDifference.Pts != 1 || len(fullDifference.NewMessages) != 1 { + t.Fatalf("difference = %T %+v, want creation event at pts=1", difference, difference) + } + }) } } @@ -346,8 +521,8 @@ func TestMessagesCreateChatDispatchRemembersTDesktopClientInfo(t *testing.T) { sessions.mu.Lock() pushUserIDs := append([]int64(nil), sessions.pushUserIDs...) sessions.mu.Unlock() - if len(pushUserIDs) != 1 || pushUserIDs[0] != friend.ID { - t.Fatalf("push user ids = %v, want only invited friend %d", pushUserIDs, friend.ID) + if len(pushUserIDs) != 2 || pushUserIDs[0] != owner.ID || pushUserIDs[1] != friend.ID { + t.Fatalf("push user ids = %v, want creator then invited friend %d/%d", pushUserIDs, owner.ID, friend.ID) } } From 511f25efc29cc3fbb805dd4e0b4e00d532744b5e Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 13:08:55 +0800 Subject: [PATCH 15/28] fix: sync Android join RPC admission --- go.mod | 2 +- go.sum | 4 +- internal/compat/android/layer_rpc_test.go | 2 +- .../rpc/android_private_layer_gate_test.go | 43 ++++++++++++++++--- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 21f7e95f..e1366f58 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/golang-migrate/migrate/v4 v4.19.1 github.com/gotd/ige v0.2.2 github.com/gotd/log/logzap v0.1.1 - github.com/iamxvbaba/td v1.1.3 + github.com/iamxvbaba/td v1.1.4 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jackc/pgx/v5 v5.9.2 github.com/lestrrat-go/jwx/v3 v3.1.1 diff --git a/go.sum b/go.sum index d3c16d1d..5369e059 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/iamxvbaba/td v1.1.3 h1:g9GE2jZVB1U1N8HuaRy707EzYfcsEuXIC//wI23VYsg= -github.com/iamxvbaba/td v1.1.3/go.mod h1:oG/fu7sqGC7NznoBD8f3fmTy9NFR42+DMNtdCPStX04= +github.com/iamxvbaba/td v1.1.4 h1:N0WIo6pqBJdMb+9vQiqlqHfZDV1FmGgf+YH/m7uEwuQ= +github.com/iamxvbaba/td v1.1.4/go.mod h1:oG/fu7sqGC7NznoBD8f3fmTy9NFR42+DMNtdCPStX04= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/internal/compat/android/layer_rpc_test.go b/internal/compat/android/layer_rpc_test.go index a9989325..d699f73a 100644 --- a/internal/compat/android/layer_rpc_test.go +++ b/internal/compat/android/layer_rpc_test.go @@ -39,7 +39,7 @@ func TestUpgradePrivateLayerRPCOnlyAcceptsAuditedAndroidConstructors(t *testing. } func TestGeneratedPrivateLayerRPCOverlayHasAllAuditedMethods(t *testing.T) { - if got, want := tlprofile.ClientRPCOverlayMethodCount(tlprofile.ClientRPCOverlayDrkloAndroid), 15; got != want { + if got, want := tlprofile.ClientRPCOverlayMethodCount(tlprofile.ClientRPCOverlayDrkloAndroid), 17; got != want { t.Fatalf("generated DrKLO method count = %d, want %d", got, want) } } diff --git a/internal/rpc/android_private_layer_gate_test.go b/internal/rpc/android_private_layer_gate_test.go index 92859cbb..6b748118 100644 --- a/internal/rpc/android_private_layer_gate_test.go +++ b/internal/rpc/android_private_layer_gate_test.go @@ -15,11 +15,12 @@ import ( ) type androidPrivateLayerFixture struct { - name string - privateID uint32 - semantic tlprofile.SemanticID - method string - wire func(*testing.T) []byte + name string + privateID uint32 + semantic tlprofile.SemanticID + method string + currentJoinResult bool + wire func(*testing.T) []byte } // TestAndroidPrivateLayerRPCsAdaptAcrossCanonicalBoundary is the production @@ -34,7 +35,7 @@ type androidPrivateLayerFixture struct { func TestAndroidPrivateLayerRPCsAdaptAcrossCanonicalBoundary(t *testing.T) { r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System) fixtures := androidPrivateLayerFixtures() - if got, want := len(fixtures), 15; got != want { + if got, want := len(fixtures), 17; got != want { t.Fatalf("private fixture count = %d, want %d", got, want) } @@ -86,6 +87,17 @@ func TestAndroidPrivateLayerRPCsAdaptAcrossCanonicalBoundary(t *testing.T) { if !ok || call.WireID() != wantWireID { t.Fatalf("admitted exact id = %#x, want %#x (ok=%v)", call.WireID(), wantWireID, ok) } + if fixture.currentJoinResult { + var result bin.Buffer + if err := call.EncodeResult(&tg.MessagesChatInviteJoinResultOk{ + Updates: &tg.UpdatesTooLong{}, + }, &result); err != nil { + t.Fatalf("encode current join result: %v", err) + } + if wireID, err := result.PeekID(); err != nil || wireID != 0x445663a7 { + t.Fatalf("result wire = %#x err=%v, want chatInviteJoinResultOk#445663a7", wireID, err) + } + } }) } }) @@ -109,6 +121,24 @@ func androidPrivateLayerFixtures() []androidPrivateLayerFixture { }) }, }, + { + name: "messages.importChatInvite_alias", privateID: 0x6c50051c, + semantic: tlprofile.SemanticMethodMessagesImportChatInvite, method: "messages.importChatInvite", + currentJoinResult: true, + wire: func(t *testing.T) []byte { + return androidPrivateAliasWire(t, 0x6c50051c, &tg.MessagesImportChatInviteRequest{Hash: "private-invite"}) + }, + }, + { + name: "channels.joinChannel_alias", privateID: 0x24b524c5, + semantic: tlprofile.SemanticMethodChannelsJoinChannel, method: "channels.joinChannel", + currentJoinResult: true, + wire: func(t *testing.T) []byte { + return androidPrivateAliasWire(t, 0x24b524c5, &tg.ChannelsJoinChannelRequest{ + Channel: &tg.InputChannel{ChannelID: 45, AccessHash: 46}, + }) + }, + }, { name: "updates.getDifference_alias", privateID: 0x25939651, semantic: tlprofile.SemanticMethodUpdatesGetDifference, method: "updates.getDifference", @@ -121,7 +151,6 @@ func androidPrivateLayerFixtures() []androidPrivateLayerFixture { semantic: tlprofile.SemanticMethodMessagesCreateChat, method: "messages.createChat", wire: func(t *testing.T) []byte { return androidPrivateAliasWire(t, 0x0034a818, &tg.MessagesCreateChatRequest{ - Users: []tg.InputUserClass{&tg.InputUser{UserID: 51, AccessHash: 52}}, Title: "private group", }) }, From 401a8f148ed1996f74697a17e7db032a7cdf714b Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 16:08:44 +0800 Subject: [PATCH 16/28] fix: sync monoforum suggested post flow --- cmd/telesrv/main.go | 1 + .../0133_suggested_post_lifecycle.down.sql | 1 + .../0133_suggested_post_lifecycle.up.sql | 39 + ...ested_post_effective_publish_date.down.sql | 17 + ...ggested_post_effective_publish_date.up.sql | 93 +++ .../app/channels/service_suggested_post.go | 34 + internal/domain/channel.go | 87 ++- internal/domain/channel_errors.go | 67 +- internal/domain/stars.go | 29 +- internal/rpc/convert_channels_core.go | 52 +- internal/rpc/messages_monoforum.go | 36 +- internal/rpc/messages_monoforum_rpc_test.go | 24 +- internal/rpc/messages_register.go | 3 + internal/rpc/messages_suggested_post.go | 73 +- .../rpc/messages_suggested_post_rpc_test.go | 158 ++++ .../rpc/messages_suggested_post_updates.go | 65 ++ internal/rpc/suggested_post_dispatcher.go | 59 ++ internal/store/memory/channel_dialogs.go | 4 +- internal/store/memory/channel_helpers.go | 4 +- .../store/memory/channel_message_helpers.go | 4 + .../store/memory/channel_message_history.go | 2 +- internal/store/memory/channel_monoforum.go | 9 +- internal/store/memory/channel_store.go | 116 +-- .../store/memory/channel_suggested_post.go | 421 +++++++++++ .../memory/channel_suggested_post_test.go | 276 +++++++ internal/store/memory/channel_updates.go | 6 +- internal/store/postgres/channel_dialogs.go | 2 +- internal/store/postgres/channel_helpers.go | 6 +- .../store/postgres/channel_member_helpers.go | 2 +- .../store/postgres/channel_message_history.go | 2 +- internal/store/postgres/channel_monoforum.go | 14 +- .../store/postgres/channel_suggested_post.go | 708 ++++++++++++++++++ ...channel_suggested_post_integration_test.go | 135 ++++ internal/store/postgres/channel_updates.go | 6 +- ...ft_lifecycle_migration_integration_test.go | 4 +- 35 files changed, 2415 insertions(+), 144 deletions(-) create mode 100644 deploy/migrations/0133_suggested_post_lifecycle.down.sql create mode 100644 deploy/migrations/0133_suggested_post_lifecycle.up.sql create mode 100644 deploy/migrations/0134_suggested_post_effective_publish_date.down.sql create mode 100644 deploy/migrations/0134_suggested_post_effective_publish_date.up.sql create mode 100644 internal/app/channels/service_suggested_post.go create mode 100644 internal/rpc/messages_suggested_post_rpc_test.go create mode 100644 internal/rpc/messages_suggested_post_updates.go create mode 100644 internal/rpc/suggested_post_dispatcher.go create mode 100644 internal/store/memory/channel_suggested_post.go create mode 100644 internal/store/memory/channel_suggested_post_test.go create mode 100644 internal/store/postgres/channel_suggested_post.go create mode 100644 internal/store/postgres/channel_suggested_post_integration_test.go diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index 35556cab..a78c8ca0 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -933,6 +933,7 @@ func run(logger *zap.Logger) error { ).Run(ctx) go rpc.NewBootstrapUpdateDispatcher(router, logger.Named("rpc").Named("bootstrap")).Run(ctx) go rpc.NewScheduledDispatcher(router, logger.Named("rpc").Named("scheduled")).Run(ctx) + go rpc.NewSuggestedPostDispatcher(router, logger.Named("rpc").Named("suggested-post")).Run(ctx) go rpc.NewExpiryDispatcher(router, logger.Named("rpc").Named("expiry")).Run(ctx) go rpc.NewPhoneExpiryDispatcher(router, logger.Named("rpc").Named("phone-expiry"), cfg.CallExpiryInterval).Run(ctx) go rpc.NewGroupCallSweepDispatcher(router, logger.Named("rpc").Named("groupcall-sweep"), cfg.GroupCallSweepInterval, cfg.GroupCallCheckTTL).Run(ctx) diff --git a/deploy/migrations/0133_suggested_post_lifecycle.down.sql b/deploy/migrations/0133_suggested_post_lifecycle.down.sql new file mode 100644 index 00000000..5ef581c2 --- /dev/null +++ b/deploy/migrations/0133_suggested_post_lifecycle.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS public.suggested_post_approvals; diff --git a/deploy/migrations/0133_suggested_post_lifecycle.up.sql b/deploy/migrations/0133_suggested_post_lifecycle.up.sql new file mode 100644 index 00000000..1ae81ae9 --- /dev/null +++ b/deploy/migrations/0133_suggested_post_lifecycle.up.sql @@ -0,0 +1,39 @@ +-- Durable suggested-post approval/payment/publication state. The row is the +-- idempotency key for a monoforum suggestion; message/update rows remain the +-- client-visible source of truth and are written in the same transaction. +CREATE TABLE public.suggested_post_approvals ( + monoforum_id bigint NOT NULL, + suggestion_message_id integer NOT NULL, + parent_channel_id bigint NOT NULL, + actor_user_id bigint NOT NULL, + payer_user_id bigint NOT NULL, + state text NOT NULL, + price_kind text NOT NULL DEFAULT '', + price_amount bigint NOT NULL DEFAULT 0, + price_nanos integer NOT NULL DEFAULT 0, + schedule_date integer NOT NULL DEFAULT 0, + approval_service_message_id integer NOT NULL DEFAULT 0, + published_message_id integer NOT NULL DEFAULT 0, + settlement_due integer NOT NULL DEFAULT 0, + final_service_message_id integer NOT NULL DEFAULT 0, + created_at integer NOT NULL, + updated_at integer NOT NULL, + PRIMARY KEY (monoforum_id, suggestion_message_id), + CONSTRAINT suggested_post_approvals_shape_check CHECK ( + monoforum_id>0 AND suggestion_message_id>0 AND parent_channel_id>0 AND + actor_user_id>0 AND payer_user_id>0 AND created_at>0 AND updated_at>=created_at AND + state IN ('balance_low','rejected','scheduled','published','completed','refunded') AND + price_kind IN ('','stars','ton') AND price_amount>=0 AND price_nanos BETWEEN 0 AND 999999999 AND + ((price_kind='' AND price_amount=0 AND price_nanos=0) OR + (price_kind='stars' AND price_amount>0) OR + (price_kind='ton' AND price_amount>0 AND price_nanos=0)) AND + schedule_date>=0 AND approval_service_message_id>=0 AND published_message_id>=0 AND + settlement_due>=0 AND final_service_message_id>=0) +); + +CREATE INDEX suggested_post_approvals_schedule_idx + ON public.suggested_post_approvals(schedule_date,monoforum_id,suggestion_message_id) + WHERE state='scheduled'; +CREATE INDEX suggested_post_approvals_settlement_idx + ON public.suggested_post_approvals(settlement_due,monoforum_id,suggestion_message_id) + WHERE state='published'; diff --git a/deploy/migrations/0134_suggested_post_effective_publish_date.down.sql b/deploy/migrations/0134_suggested_post_effective_publish_date.down.sql new file mode 100644 index 00000000..3d4020cb --- /dev/null +++ b/deploy/migrations/0134_suggested_post_effective_publish_date.down.sql @@ -0,0 +1,17 @@ +-- The data backfill is intentionally retained on rollback. Restore only the +-- pre-0134 shape constraint, which allowed zero schedule_date in every state. +ALTER TABLE suggested_post_approvals + DROP CONSTRAINT suggested_post_approvals_shape_check; + +ALTER TABLE suggested_post_approvals + ADD CONSTRAINT suggested_post_approvals_shape_check CHECK ( + monoforum_id>0 AND suggestion_message_id>0 AND parent_channel_id>0 AND + actor_user_id>0 AND payer_user_id>0 AND created_at>0 AND updated_at>=created_at AND + state IN ('balance_low','rejected','scheduled','published','completed','refunded') AND + price_kind IN ('','stars','ton') AND price_amount>=0 AND price_nanos BETWEEN 0 AND 999999999 AND + ((price_kind='' AND price_amount=0 AND price_nanos=0) OR + (price_kind='stars' AND price_amount>0) OR + (price_kind='ton' AND price_amount>0 AND price_nanos=0)) AND + schedule_date>=0 AND approval_service_message_id>=0 AND published_message_id>=0 AND + settlement_due>=0 AND final_service_message_id>=0 + ); diff --git a/deploy/migrations/0134_suggested_post_effective_publish_date.up.sql b/deploy/migrations/0134_suggested_post_effective_publish_date.up.sql new file mode 100644 index 00000000..c76172fa --- /dev/null +++ b/deploy/migrations/0134_suggested_post_effective_publish_date.up.sql @@ -0,0 +1,93 @@ +-- TDesktop omits schedule_date for "Publish Now", while the approval action +-- renderer always formats an absolute publication date. Backfill rows written +-- by the initial lifecycle implementation and keep current/history/difference +-- projections on the same effective timestamp. +UPDATE channel_messages m +SET suggested_post = jsonb_set( + m.suggested_post, + '{ScheduleDate}', + to_jsonb(a.created_at), + true + ) +FROM suggested_post_approvals a +WHERE a.schedule_date = 0 + AND a.state IN ('scheduled', 'published', 'completed', 'refunded') + AND m.channel_id = a.monoforum_id + AND m.id = a.suggestion_message_id + AND COALESCE((m.suggested_post->>'Accepted')::boolean, false) + AND COALESCE((m.suggested_post->>'ScheduleDate')::integer, 0) = 0; + +UPDATE channel_messages m +SET action = jsonb_set( + m.action, + '{SuggestedPostScheduleDate}', + to_jsonb(a.created_at), + true + ) +FROM suggested_post_approvals a +WHERE a.schedule_date = 0 + AND a.state IN ('scheduled', 'published', 'completed', 'refunded') + AND m.channel_id = a.monoforum_id + AND m.id = a.approval_service_message_id + AND m.action->>'Type' = 'suggested_post_approval' + AND NOT COALESCE((m.action->>'SuggestedPostRejected')::boolean, false) + AND NOT COALESCE((m.action->>'SuggestedPostBalanceTooLow')::boolean, false) + AND COALESCE((m.action->>'SuggestedPostScheduleDate')::integer, 0) = 0; + +UPDATE channel_update_events e +SET payload = jsonb_set( + e.payload, + '{message,SuggestedPost,ScheduleDate}', + to_jsonb(a.created_at), + true + ) +FROM suggested_post_approvals a +WHERE a.schedule_date = 0 + AND a.state IN ('scheduled', 'published', 'completed', 'refunded') + AND e.channel_id = a.monoforum_id + AND e.message_id = a.suggestion_message_id + AND e.event_type = 'edit_channel_message' + AND COALESCE((e.payload #>> '{message,SuggestedPost,Accepted}')::boolean, false) + AND COALESCE((e.payload #>> '{message,SuggestedPost,ScheduleDate}')::integer, 0) = 0; + +UPDATE channel_update_events e +SET payload = jsonb_set( + e.payload, + '{message,Action,SuggestedPostScheduleDate}', + to_jsonb(a.created_at), + true + ) +FROM suggested_post_approvals a +WHERE a.schedule_date = 0 + AND a.state IN ('scheduled', 'published', 'completed', 'refunded') + AND e.channel_id = a.monoforum_id + AND e.message_id = a.approval_service_message_id + AND e.event_type = 'new_channel_message' + AND e.payload #>> '{message,Action,Type}' = 'suggested_post_approval' + AND NOT COALESCE((e.payload #>> '{message,Action,SuggestedPostRejected}')::boolean, false) + AND NOT COALESCE((e.payload #>> '{message,Action,SuggestedPostBalanceTooLow}')::boolean, false) + AND COALESCE((e.payload #>> '{message,Action,SuggestedPostScheduleDate}')::integer, 0) = 0; + +UPDATE suggested_post_approvals +SET schedule_date = created_at, + updated_at = GREATEST(updated_at, created_at) +WHERE schedule_date = 0 + AND state IN ('scheduled', 'published', 'completed', 'refunded'); + +ALTER TABLE suggested_post_approvals + DROP CONSTRAINT suggested_post_approvals_shape_check; + +ALTER TABLE suggested_post_approvals + ADD CONSTRAINT suggested_post_approvals_shape_check CHECK ( + monoforum_id>0 AND suggestion_message_id>0 AND parent_channel_id>0 AND + actor_user_id>0 AND payer_user_id>0 AND created_at>0 AND updated_at>=created_at AND + state IN ('balance_low','rejected','scheduled','published','completed','refunded') AND + price_kind IN ('','stars','ton') AND price_amount>=0 AND price_nanos BETWEEN 0 AND 999999999 AND + ((price_kind='' AND price_amount=0 AND price_nanos=0) OR + (price_kind='stars' AND price_amount>0) OR + (price_kind='ton' AND price_amount>0 AND price_nanos=0)) AND + schedule_date>=0 AND + (state IN ('balance_low','rejected') OR schedule_date>0) AND + approval_service_message_id>=0 AND published_message_id>=0 AND + settlement_due>=0 AND final_service_message_id>=0 + ); diff --git a/internal/app/channels/service_suggested_post.go b/internal/app/channels/service_suggested_post.go new file mode 100644 index 00000000..c6bed8cc --- /dev/null +++ b/internal/app/channels/service_suggested_post.go @@ -0,0 +1,34 @@ +package channels + +import ( + "context" + + "telesrv/internal/domain" +) + +type suggestedPostStore interface { + ToggleSuggestedPostApproval(context.Context, domain.ToggleSuggestedPostApprovalRequest) (domain.ToggleSuggestedPostApprovalResult, error) + ProcessSuggestedPostLifecycle(context.Context, domain.SuggestedPostLifecycleRequest) ([]domain.ToggleSuggestedPostApprovalResult, error) +} + +func (s *Service) ToggleSuggestedPostApproval(ctx context.Context, req domain.ToggleSuggestedPostApprovalRequest) (domain.ToggleSuggestedPostApprovalResult, error) { + if s == nil || s.channels == nil || req.UserID == 0 || req.MonoforumID == 0 || req.MessageID <= 0 { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + store, ok := s.channels.(suggestedPostStore) + if !ok { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + return store.ToggleSuggestedPostApproval(ctx, req) +} + +func (s *Service) ProcessSuggestedPostLifecycle(ctx context.Context, req domain.SuggestedPostLifecycleRequest) ([]domain.ToggleSuggestedPostApprovalResult, error) { + if s == nil || s.channels == nil { + return nil, domain.ErrSuggestedPostInvalid + } + store, ok := s.channels.(suggestedPostStore) + if !ok { + return nil, domain.ErrSuggestedPostInvalid + } + return store.ProcessSuggestedPostLifecycle(ctx, req) +} diff --git a/internal/domain/channel.go b/internal/domain/channel.go index 027956a6..941b356b 100644 --- a/internal/domain/channel.go +++ b/internal/domain/channel.go @@ -505,6 +505,26 @@ type ChannelMember struct { Guest bool } +// CanManageDirectMessages reports whether this active parent-channel member may +// see and address every subscriber topic in the linked direct-messages +// monoforum. Telegram deliberately does not grant this capability to an +// ordinary channel administrator: the explicit manage_direct_messages right is +// required (creators have the capability implicitly). +func (m ChannelMember) CanManageDirectMessages() bool { + return m.Status == ChannelMemberActive && + (m.Role == ChannelRoleCreator || + (m.Role == ChannelRoleAdmin && m.AdminRights.ManageDirectMessages)) +} + +// CanPostChannelMessages reports whether this active member may publish a post +// to a broadcast channel. Suggested-post managers need this in addition to +// CanManageDirectMessages when approving a subscriber-authored suggestion. +func (m ChannelMember) CanPostChannelMessages() bool { + return m.Status == ChannelMemberActive && + (m.Role == ChannelRoleCreator || + (m.Role == ChannelRoleAdmin && m.AdminRights.PostMessages)) +} + // ChannelDialog is the current user's owner-view dialog state for a channel. type ChannelDialog struct { UserID int64 @@ -570,7 +590,10 @@ const ( ChannelActionSetChatWallpaper ChannelMessageActionType = "set_chat_wallpaper" // ChannelActionChangeCommunity maps messageActionChangeCommunity. A non-zero // CommunityID means linked; zero means unlinked. - ChannelActionChangeCommunity ChannelMessageActionType = "change_community" + ChannelActionChangeCommunity ChannelMessageActionType = "change_community" + ChannelActionSuggestedPostApproval ChannelMessageActionType = "suggested_post_approval" + ChannelActionSuggestedPostSuccess ChannelMessageActionType = "suggested_post_success" + ChannelActionSuggestedPostRefund ChannelMessageActionType = "suggested_post_refund" ) // ChannelMessageAction describes a service action without depending on tg.*. @@ -609,6 +632,15 @@ type ChannelMessageAction struct { Wallpaper *Wallpaper // Photo 仅 chat_edit_photo 服务消息使用。 Photo *Photo + // Suggested-post lifecycle actions share the immutable price snapshot. The + // approval action additionally uses the reject/balance/schedule fields; + // refund uses PayerInitiated. + SuggestedPostRejected bool + SuggestedPostBalanceTooLow bool + SuggestedPostRejectComment string + SuggestedPostScheduleDate int + SuggestedPostPrice *SuggestedPostPrice + SuggestedPostPayerInitiated bool } // ChannelMessage is a single stored message in a channel/supergroup. @@ -1491,6 +1523,59 @@ type SendMonoforumMessageRequest struct { Date int } +// ToggleSuggestedPostApprovalRequest is the domain command behind +// messages.toggleSuggestedPostApproval. MessageID addresses the immutable +// suggestion in one monoforum subscriber sub-dialog. +type ToggleSuggestedPostApprovalRequest struct { + UserID int64 + MonoforumID int64 + MessageID int + Reject bool + RejectComment string + ScheduleDate int + Date int +} + +// SuggestedPostLifecycleState is persisted so approval, scheduled publication, +// settlement and refund remain idempotent across restarts. +type SuggestedPostLifecycleState string + +const ( + SuggestedPostStateBalanceLow SuggestedPostLifecycleState = "balance_low" + SuggestedPostStateRejected SuggestedPostLifecycleState = "rejected" + SuggestedPostStateScheduled SuggestedPostLifecycleState = "scheduled" + SuggestedPostStatePublished SuggestedPostLifecycleState = "published" + SuggestedPostStateCompleted SuggestedPostLifecycleState = "completed" + SuggestedPostStateRefunded SuggestedPostLifecycleState = "refunded" +) + +// ToggleSuggestedPostApprovalResult contains every durable update produced by +// one command or lifecycle transition. OriginalEvent is an edit in the +// monoforum; ServiceEvent is the approval/success/refund service message; an +// optional Published result is the broadcast post. +type ToggleSuggestedPostApprovalResult struct { + Monoforum Channel + Parent Channel + SavedPeer Peer + State SuggestedPostLifecycleState + OriginalMessage ChannelMessage + OriginalEvent ChannelUpdateEvent + ServiceMessage ChannelMessage + ServiceEvent ChannelUpdateEvent + Published *SendChannelMessageResult + Recipients []int64 + PayerStarsBalance *StarsBalance + PayerTONBalance *int64 + Duplicate bool +} + +// SuggestedPostLifecycleRequest bounds one worker pass; stores must use an +// indexed seek and row locks rather than scanning every approval. +type SuggestedPostLifecycleRequest struct { + Now int + Limit int +} + // ChannelSendReplayRequest addresses either a regular channel send (SavedPeer is zero) or one // monoforum sub-dialog send (SavedPeer is the subscriber scope). Lookup is read-only and must // never re-run membership/permission checks or allocate pts/message ids. diff --git a/internal/domain/channel_errors.go b/internal/domain/channel_errors.go index 92c2f2d0..7a747af5 100644 --- a/internal/domain/channel_errors.go +++ b/internal/domain/channel_errors.go @@ -6,38 +6,41 @@ import ( ) var ( - ErrChannelInvalid = errors.New("channel invalid") - ErrChannelPrivate = errors.New("channel private") - ErrChannelTitleInvalid = errors.New("channel title invalid") - ErrChannelUserBanned = errors.New("user banned in channel") - ErrChannelWriteForbidden = errors.New("chat write forbidden") - ErrChannelAdminRequired = errors.New("chat admin required") - ErrChannelNotModified = errors.New("chat not modified") - ErrChannelForumMissing = errors.New("channel forum missing") - ErrChannelMonoforumUnsupported = errors.New("channel monoforum unsupported") - ErrLinkNotModified = errors.New("discussion link not modified") - ErrChatDiscussionUnallowed = errors.New("chat discussion unallowed") - ErrBroadcastIDInvalid = errors.New("broadcast id invalid") - ErrMegagroupIDInvalid = errors.New("megagroup id invalid") - ErrMegagroupPrehistoryHidden = errors.New("megagroup prehistory hidden") - ErrChatPublicRequired = errors.New("chat public required") - ErrChannelUserCreator = errors.New("channel user creator") - ErrChannelRightForbidden = errors.New("channel right forbidden") - ErrPersistentTimestamp = errors.New("persistent timestamp invalid") - ErrInviteHashEmpty = errors.New("invite hash empty") - ErrInviteHashInvalid = errors.New("invite hash invalid") - ErrInviteHashExpired = errors.New("invite hash expired") - ErrInvitePermanent = errors.New("chat invite permanent") - ErrInviteRevokedMissing = errors.New("invite revoked missing") - ErrInviteRequestSent = errors.New("invite request sent") - ErrHideRequesterMissing = errors.New("hide requester missing") - ErrUsersTooMuch = errors.New("users too much") - ErrUserAlreadyParticipant = errors.New("user already participant") - ErrUserKicked = errors.New("user kicked") - ErrUserNotParticipant = errors.New("user not participant") - ErrBotGroupsBlocked = errors.New("bot groups blocked") - ErrReactionInvalid = errors.New("reaction invalid") - ErrReactionsTooMany = errors.New("reactions too many") + ErrChannelInvalid = errors.New("channel invalid") + ErrChannelPrivate = errors.New("channel private") + ErrChannelTitleInvalid = errors.New("channel title invalid") + ErrChannelUserBanned = errors.New("user banned in channel") + ErrChannelWriteForbidden = errors.New("chat write forbidden") + ErrChannelAdminRequired = errors.New("chat admin required") + ErrChannelNotModified = errors.New("chat not modified") + ErrChannelForumMissing = errors.New("channel forum missing") + ErrChannelMonoforumUnsupported = errors.New("channel monoforum unsupported") + ErrLinkNotModified = errors.New("discussion link not modified") + ErrChatDiscussionUnallowed = errors.New("chat discussion unallowed") + ErrBroadcastIDInvalid = errors.New("broadcast id invalid") + ErrMegagroupIDInvalid = errors.New("megagroup id invalid") + ErrMegagroupPrehistoryHidden = errors.New("megagroup prehistory hidden") + ErrChatPublicRequired = errors.New("chat public required") + ErrChannelUserCreator = errors.New("channel user creator") + ErrChannelRightForbidden = errors.New("channel right forbidden") + ErrPersistentTimestamp = errors.New("persistent timestamp invalid") + ErrInviteHashEmpty = errors.New("invite hash empty") + ErrInviteHashInvalid = errors.New("invite hash invalid") + ErrInviteHashExpired = errors.New("invite hash expired") + ErrInvitePermanent = errors.New("chat invite permanent") + ErrInviteRevokedMissing = errors.New("invite revoked missing") + ErrInviteRequestSent = errors.New("invite request sent") + ErrHideRequesterMissing = errors.New("hide requester missing") + ErrUsersTooMuch = errors.New("users too much") + ErrUserAlreadyParticipant = errors.New("user already participant") + ErrUserKicked = errors.New("user kicked") + ErrUserNotParticipant = errors.New("user not participant") + ErrBotGroupsBlocked = errors.New("bot groups blocked") + ErrReactionInvalid = errors.New("reaction invalid") + ErrReactionsTooMany = errors.New("reactions too many") + ErrSuggestedPostInvalid = errors.New("suggested post invalid") + ErrSuggestedPostAlreadyHandled = errors.New("suggested post already handled") + ErrSuggestedPostApprovalForbidden = errors.New("suggested post approval forbidden") ) // SlowModeWaitError carries the remaining wait seconds for a channel slow mode violation. diff --git a/internal/domain/stars.go b/internal/domain/stars.go index dc3f1b60..0d2f45f3 100644 --- a/internal/domain/stars.go +++ b/internal/domain/stars.go @@ -21,20 +21,21 @@ type StarsBalance struct { type StarsTransactionReason string const ( - StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予 - StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造) - StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费 - StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取 - StarsReasonGiftUpgrade StarsTransactionReason = "gift_upgrade" // 普通礼物升级为唯一礼物 - StarsReasonGiftTransfer StarsTransactionReason = "gift_transfer" - StarsReasonGiftResale StarsTransactionReason = "gift_resale" - StarsReasonGiftOffer StarsTransactionReason = "gift_offer" - StarsReasonGiftAuction StarsTransactionReason = "gift_auction" - StarsReasonGiftPrepaid StarsTransactionReason = "gift_prepaid_upgrade" - StarsReasonGiftDrop StarsTransactionReason = "gift_drop_original_details" - StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁 - StarsReasonPaidMessage StarsTransactionReason = "paid_message" // 频道 Direct Message 花费 - StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整 + StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予 + StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造) + StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费 + StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取 + StarsReasonGiftUpgrade StarsTransactionReason = "gift_upgrade" // 普通礼物升级为唯一礼物 + StarsReasonGiftTransfer StarsTransactionReason = "gift_transfer" + StarsReasonGiftResale StarsTransactionReason = "gift_resale" + StarsReasonGiftOffer StarsTransactionReason = "gift_offer" + StarsReasonGiftAuction StarsTransactionReason = "gift_auction" + StarsReasonGiftPrepaid StarsTransactionReason = "gift_prepaid_upgrade" + StarsReasonGiftDrop StarsTransactionReason = "gift_drop_original_details" + StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁 + StarsReasonPaidMessage StarsTransactionReason = "paid_message" // 频道 Direct Message 花费 + StarsReasonSuggestedPost StarsTransactionReason = "suggested_post" + StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整 ) // StarsTransaction 是一条账本流水。amount 带符号:贷记 > 0(含 refund/收取),借记 < 0。 diff --git a/internal/rpc/convert_channels_core.go b/internal/rpc/convert_channels_core.go index 44ae66fe..5d116f3d 100644 --- a/internal/rpc/convert_channels_core.go +++ b/internal/rpc/convert_channels_core.go @@ -113,6 +113,9 @@ func tgChannelMessage(viewerUserID int64, m domain.ChannelMessage) tg.MessageCla if msg.Action == nil { msg.Action = &tg.MessageActionEmpty{} } + if m.SavedPeer.ID != 0 { + msg.SetSavedPeerID(tgPeer(m.SavedPeer)) + } if reply := tgMessageReplyHeader(domain.Message{ Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: m.ChannelID}, ReplyTo: m.ReplyTo, @@ -140,7 +143,18 @@ func tgChannelMessage(viewerUserID int64, m domain.ChannelMessage) tg.MessageCla msg.SetSavedPeerID(tgPeer(m.SavedPeer)) } if suggested, ok := tgSuggestedPost(m.SuggestedPost); ok { - msg.SetSuggestedPost(suggested) + if m.Post { + if m.SuggestedPost != nil && m.SuggestedPost.Accepted && m.SuggestedPost.Price != nil { + switch m.SuggestedPost.Price.Kind { + case domain.SuggestedPostPriceStars: + msg.SetPaidSuggestedPostStars(true) + case domain.SuggestedPostPriceTON: + msg.SetPaidSuggestedPostTon(true) + } + } + } else { + msg.SetSuggestedPost(suggested) + } } if m.PaidMessageStars > 0 { msg.SetPaidMessageStars(m.PaidMessageStars) @@ -303,6 +317,42 @@ func tgChannelMessageAction(action domain.ChannelMessageAction) tg.MessageAction out.SetCommunityID(action.CommunityID) } return out + case domain.ChannelActionSuggestedPostApproval: + out := &tg.MessageActionSuggestedPostApproval{ + Rejected: action.SuggestedPostRejected, + BalanceTooLow: action.SuggestedPostBalanceTooLow, + } + if action.SuggestedPostRejectComment != "" { + out.SetRejectComment(action.SuggestedPostRejectComment) + } + if action.SuggestedPostScheduleDate > 0 { + out.SetScheduleDate(action.SuggestedPostScheduleDate) + } + if price := tgSuggestedPostPrice(action.SuggestedPostPrice); price != nil { + out.SetPrice(price) + } + return out + case domain.ChannelActionSuggestedPostSuccess: + if price := tgSuggestedPostPrice(action.SuggestedPostPrice); price != nil { + return &tg.MessageActionSuggestedPostSuccess{Price: price} + } + return nil + case domain.ChannelActionSuggestedPostRefund: + return &tg.MessageActionSuggestedPostRefund{PayerInitiated: action.SuggestedPostPayerInitiated} + default: + return nil + } +} + +func tgSuggestedPostPrice(price *domain.SuggestedPostPrice) tg.StarsAmountClass { + if price == nil { + return nil + } + switch price.Kind { + case domain.SuggestedPostPriceStars: + return &tg.StarsAmount{Amount: price.Amount, Nanos: price.Nanos} + case domain.SuggestedPostPriceTON: + return &tg.StarsTonAmount{Amount: price.Amount} default: return nil } diff --git a/internal/rpc/messages_monoforum.go b/internal/rpc/messages_monoforum.go index fabfb9ce..8943da19 100644 --- a/internal/rpc/messages_monoforum.go +++ b/internal/rpc/messages_monoforum.go @@ -9,11 +9,14 @@ import ( "telesrv/internal/domain" ) -// resolveMonoforumForAdmin 解析 parent_peer 指向的 monoforum 虚拟频道,并校验当前用户是其母广播频道 -// 的管理员/创建者(频道私信只有频道管理员可读/回复)。monoforum 是私有零成员频道,管理员并非其成员, -// 故走 store 的 membership-agnostic 解析,在母频道上做授权。 -// 返回 (monoforum频道, isMonoforum, err):parent 是有效频道但非 monoforum 时返回 (零, false, nil), -// 由调用方回退良性空响应(兼容对普通频道传 parent_peer 的被动探测);是 monoforum 但非管理员→CHAT_ADMIN_REQUIRED。 +// resolveMonoforumForAdmin 解析 TDesktop messages.getSavedDialogs/getSavedHistory 的 parent_peer, +// 并校验当前用户可管理母广播频道的 Direct Messages。TDesktop 的 SavedSublist 实际会把 +// parentChat()->input() 作为 parent_peer;根据客户端 materialize 路径,它既可能是 monoforum +// 虚拟频道,也可能是与之关联的母广播频道。因此这里把两种 wire peer 归一到同一个 monoforum, +// 授权仍只认母频道的 creator / ManageDirectMessages,绝不能把普通 admin 放进管理者视图。 +// +// 返回 (monoforum频道, isMonoforum, err):parent 是有效但未关联 Direct Messages 的普通频道时 +// 返回 (零, false, nil),由调用方保留良性空响应;关联频道的非管理者返回 CHAT_ADMIN_REQUIRED。 func (r *Router) resolveMonoforumForAdmin(ctx context.Context, userID int64, parent domain.Peer) (domain.Channel, bool, error) { if r.deps.Channels == nil { return domain.Channel{}, false, notImplementedErr() @@ -22,9 +25,30 @@ func (r *Router) resolveMonoforumForAdmin(ctx context.Context, userID int64, par return domain.Channel{}, false, parentPeerInvalidErr() } mono, isAdmin, err := r.deps.Channels.ResolveMonoforumSend(ctx, userID, parent.ID) + if errors.Is(err, domain.ErrChannelInvalid) { + // TDesktop 当前的 Direct Messages subsection 会传母广播频道。只接受显式的 + // linked_monoforum 关系,不能把任意普通频道猜成 monoforum。 + views, viewErr := r.deps.Channels.GetChannels(ctx, userID, []int64{parent.ID}) + if viewErr != nil { + return domain.Channel{}, false, internalErr() + } + if len(views) != 1 { + return domain.Channel{}, false, nil + } + parentChannel := views[0].Channel + if parentChannel.ID != parent.ID || parentChannel.Deleted || parentChannel.Monoforum || parentChannel.LinkedMonoforumID == 0 { + return domain.Channel{}, false, nil + } + mono, isAdmin, err = r.deps.Channels.ResolveMonoforumSend(ctx, userID, parentChannel.LinkedMonoforumID) + if err != nil { + // A visible parent that advertises linked_monoforum_id but cannot resolve + // that target violates the durable channel-link invariant. Do not disguise + // it as an ordinary channel probe. + return domain.Channel{}, false, internalErr() + } + } if err != nil { if errors.Is(err, domain.ErrChannelInvalid) { - // 非 monoforum 频道(或不存在):非错误,交由调用方回退良性空响应。 return domain.Channel{}, false, nil } return domain.Channel{}, false, internalErr() diff --git a/internal/rpc/messages_monoforum_rpc_test.go b/internal/rpc/messages_monoforum_rpc_test.go index fd9057bc..3480ffad 100644 --- a/internal/rpc/messages_monoforum_rpc_test.go +++ b/internal/rpc/messages_monoforum_rpc_test.go @@ -17,9 +17,9 @@ import ( "telesrv/internal/store/memory" ) -// TestMonoforumSavedDialogsAndHistory 验证频道私信(monoforum)读侧 RPC:管理员经 -// getSavedDialogs(parent_peer=monoforum) 看订阅者子会话列表、经 getSavedHistory 看某订阅者历史 -// (消息带 saved_peer_id);订阅者经普通 getHistory 只看自己的子会话。 +// TestMonoforumSavedDialogsAndHistory 验证频道私信(monoforum)读侧 RPC:管理员经 +// getSavedDialogs/getSavedHistory 看订阅者子会话,parent_peer 同时兼容 TDesktop 实际发送的 +// 母广播频道和虚拟 monoforum;订阅者经普通 getHistory 只看自己的子会话。 func TestMonoforumSavedDialogsAndHistory(t *testing.T) { ctx := context.Background() userStore := memory.NewUserStore() @@ -64,6 +64,7 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { t.Fatalf("get monoforum: %v", err) } monoInput := &tg.InputPeerChannel{ChannelID: monoID, AccessHash: mono.AccessHash} + parentInput := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash} // TDesktop 点 Direct Messages 入口会先按 monoforum peer 拉普通 channel history。 // 主历史只应返回 monoforum 自身的 service messages,不能混入 saved_peer 子会话消息。 @@ -129,7 +130,8 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { // 管理员看私信列表。 dreq := &tg.MessagesGetSavedDialogsRequest{} - dreq.SetParentPeer(monoInput) + // TDesktop SavedSublist::loadAround() 的 parentChat()->input() 是母广播频道。 + dreq.SetParentPeer(parentInput) dres, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, owner.ID), dreq) if err != nil { t.Fatalf("getSavedDialogs(monoforum): %v", err) @@ -160,7 +162,7 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { // 管理员看某订阅者会话历史。 hreq := &tg.MessagesGetSavedHistoryRequest{Peer: &tg.InputPeerUser{UserID: sub.ID}} - hreq.SetParentPeer(monoInput) + hreq.SetParentPeer(parentInput) hres, err := r.onMessagesGetSavedHistory(WithUserID(ctx, owner.ID), hreq) if err != nil { t.Fatalf("getSavedHistory(monoforum): %v", err) @@ -194,6 +196,18 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { t.Fatalf("saved_peer_id = %#v, want sub %d", sp, sub.ID) } + // 虚拟 monoforum peer 仍是合法的等价入口,两个 parent 不能落到不同数据集。 + directMonoReq := &tg.MessagesGetSavedHistoryRequest{Peer: &tg.InputPeerUser{UserID: sub.ID}} + directMonoReq.SetParentPeer(monoInput) + directMonoRes, err := r.onMessagesGetSavedHistory(WithUserID(ctx, owner.ID), directMonoReq) + if err != nil { + t.Fatalf("getSavedHistory(direct monoforum): %v", err) + } + directMonoSlice, ok := directMonoRes.(*tg.MessagesMessagesSlice) + if !ok || len(directMonoSlice.Messages) != 1 { + t.Fatalf("getSavedHistory(direct monoforum) = %#v, want same single-message topic", directMonoRes) + } + // 非管理员(订阅者本人)经管理员入口看列表被拒。 if _, err := r.onMessagesGetSavedDialogs(WithUserID(ctx, sub.ID), dreq); err == nil { t.Fatalf("non-admin getSavedDialogs(monoforum) = nil err, want denied") diff --git a/internal/rpc/messages_register.go b/internal/rpc/messages_register.go index 809f1c5a..8e025d05 100644 --- a/internal/rpc/messages_register.go +++ b/internal/rpc/messages_register.go @@ -91,6 +91,9 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) { registerRPC[*tg.MessagesSendMessageRequest](d, tlprofile.SemanticMethodMessagesSendMessage, func(ctx context.Context, layerRequest *tg.MessagesSendMessageRequest) (any, error) { return r.onMessagesSendMessage(ctx, layerRequest) }) + registerRPC[*tg.MessagesToggleSuggestedPostApprovalRequest](d, tlprofile.SemanticMethodMessagesToggleSuggestedPostApproval, func(ctx context.Context, layerRequest *tg.MessagesToggleSuggestedPostApprovalRequest) (any, error) { + return r.onMessagesToggleSuggestedPostApproval(ctx, layerRequest) + }) registerRPC[*tg.MessagesForwardMessagesRequest](d, tlprofile.SemanticMethodMessagesForwardMessages, func(ctx context.Context, layerRequest *tg.MessagesForwardMessagesRequest) (any, error) { return r.onMessagesForwardMessages(ctx, layerRequest) }) diff --git a/internal/rpc/messages_suggested_post.go b/internal/rpc/messages_suggested_post.go index 8cb59754..dc009224 100644 --- a/internal/rpc/messages_suggested_post.go +++ b/internal/rpc/messages_suggested_post.go @@ -1,6 +1,11 @@ package rpc import ( + "context" + "errors" + "strings" + "unicode/utf8" + "github.com/iamxvbaba/td/tg" "telesrv/internal/domain" @@ -13,6 +18,71 @@ const ( maxSuggestedPostNanoTON int64 = 10_000_000_000_000 ) +const ( + minSuggestedPostScheduleDelay = 5 * 60 + maxSuggestedPostScheduleDelay = 31 * 24 * 60 * 60 + maxSuggestedPostRejectComment = 1024 +) + +type suggestedPostApprovalService interface { + ToggleSuggestedPostApproval(context.Context, domain.ToggleSuggestedPostApprovalRequest) (domain.ToggleSuggestedPostApprovalResult, error) + ProcessSuggestedPostLifecycle(context.Context, domain.SuggestedPostLifecycleRequest) ([]domain.ToggleSuggestedPostApprovalResult, error) +} + +func (r *Router) onMessagesToggleSuggestedPostApproval(ctx context.Context, req *tg.MessagesToggleSuggestedPostApprovalRequest) (tg.UpdatesClass, error) { + userID, _, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + if req == nil || req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID { + return nil, messageIDInvalidErr() + } + comment, hasComment := req.GetRejectComment() + if (!req.Reject && hasComment) || utf8.RuneCountInString(comment) > maxSuggestedPostRejectComment { + return nil, tgerr400("SUGGESTED_POST_INVALID") + } + peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer) + if err != nil { + return nil, err + } + if peer.Type != domain.PeerTypeChannel || peer.ID == 0 { + return nil, peerIDInvalidErr() + } + service, ok := r.deps.Channels.(suggestedPostApprovalService) + if !ok { + return nil, notImplementedErr() + } + now := int(r.clock.Now().Unix()) + scheduleDate, hasScheduleDate := req.GetScheduleDate() + if hasScheduleDate && (req.Reject || scheduleDate < now+minSuggestedPostScheduleDelay || scheduleDate > now+maxSuggestedPostScheduleDelay) { + return nil, scheduleDateInvalidErr() + } + result, err := service.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{ + UserID: userID, MonoforumID: peer.ID, MessageID: req.MsgID, Reject: req.Reject, + RejectComment: strings.TrimSpace(comment), ScheduleDate: scheduleDate, Date: now, + }) + if err != nil { + return nil, suggestedPostApprovalErr(err) + } + if !result.Duplicate { + r.enqueueSuggestedPostApprovalFanout(ctx, userID, result) + } + return r.suggestedPostApprovalUpdates(ctx, userID, result), nil +} + +func suggestedPostApprovalErr(err error) error { + switch { + case errors.Is(err, domain.ErrSuggestedPostApprovalForbidden): + return tgerr400("CHAT_ADMIN_REQUIRED") + case errors.Is(err, domain.ErrSuggestedPostAlreadyHandled): + return tgerr400("SUGGESTED_POST_ALREADY_HANDLED") + case errors.Is(err, domain.ErrSuggestedPostInvalid), errors.Is(err, domain.ErrChannelInvalid), errors.Is(err, domain.ErrMessageIDInvalid): + return tgerr400("SUGGESTED_POST_INVALID") + default: + return internalErr() + } +} + func domainSuggestedPost(input tg.SuggestedPost, present bool) (*domain.SuggestedPost, error) { if !present { return nil, nil @@ -30,8 +100,7 @@ func domainSuggestedPost(input tg.SuggestedPost, present bool) (*domain.Suggeste if price, ok := input.GetPrice(); ok { switch value := price.(type) { case *tg.StarsAmount: - if value == nil || value.Amount < minSuggestedPostStars || value.Amount > maxSuggestedPostStars || - value.Nanos < 0 || value.Nanos >= 1_000_000_000 || value.Amount == maxSuggestedPostStars && value.Nanos != 0 { + if value == nil || value.Amount < minSuggestedPostStars || value.Amount > maxSuggestedPostStars || value.Nanos != 0 { return nil, tgerr400("SUGGESTED_POST_AMOUNT_INVALID") } out.Price = &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: value.Amount, Nanos: value.Nanos} diff --git a/internal/rpc/messages_suggested_post_rpc_test.go b/internal/rpc/messages_suggested_post_rpc_test.go new file mode 100644 index 00000000..2cfb9cf0 --- /dev/null +++ b/internal/rpc/messages_suggested_post_rpc_test.go @@ -0,0 +1,158 @@ +package rpc + +import ( + "context" + "testing" + + "github.com/iamxvbaba/td/bin" + "github.com/iamxvbaba/td/clock" + "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tlprofile" + "go.uber.org/zap/zaptest" + + appchannels "telesrv/internal/app/channels" + appusers "telesrv/internal/app/users" + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +func TestMessagesToggleSuggestedPostApprovalRegisteredAndProjectsLifecycle(t *testing.T) { + ctx := context.Background() + users := memory.NewUserStore() + owner, err := users.Create(ctx, domain.User{AccessHash: 101, Phone: "15551110001", FirstName: "Owner"}) + if err != nil { + t.Fatal(err) + } + subscriber, err := users.Create(ctx, domain.User{AccessHash: 102, Phone: "15551110002", FirstName: "Subscriber"}) + if err != nil { + t.Fatal(err) + } + channelsStore := memory.NewChannelStore() + channels := appchannels.NewService(channelsStore) + created, err := channels.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{Title: "Suggested", Broadcast: true, Date: 1_700_000_000}) + if err != nil { + t.Fatal(err) + } + enabled, err := channelsStore.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 0, true) + if err != nil { + t.Fatal(err) + } + mono, err := channelsStore.GetChannelByID(ctx, enabled.Channel.LinkedMonoforumID) + if err != nil { + t.Fatal(err) + } + saved := domain.Peer{Type: domain.PeerTypeUser, ID: subscriber.ID} + suggestion, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: saved, RandomID: 91, Message: "RPC suggestion", SuggestedPost: &domain.SuggestedPost{}, Date: 1_700_000_100}) + if err != nil { + t.Fatal(err) + } + router := New(Config{}, Deps{Users: appusers.NewService(users), Channels: channels}, zaptest.NewLogger(t), clock.System) + req := &tg.MessagesToggleSuggestedPostApprovalRequest{Peer: &tg.InputPeerChannel{ChannelID: mono.ID, AccessHash: mono.AccessHash}, MsgID: suggestion.Message.ID} + var raw bin.Buffer + if err := req.Encode(&raw); err != nil { + t.Fatal(err) + } + response, err := router.Dispatch(WithLayer(WithUserID(ctx, owner.ID), 228), [8]byte{}, 0, &raw) + if err != nil { + t.Fatalf("dispatch toggleSuggestedPostApproval: %v", err) + } + updates, ok := response.(*tg.Updates) + if !ok { + t.Fatalf("response=%T, want *tg.Updates", response) + } + var edited, approval, published bool + for _, update := range updates.Updates { + switch item := update.(type) { + case *tg.UpdateEditChannelMessage: + message, ok := item.Message.(*tg.Message) + if ok && message.ID == suggestion.Message.ID { + post, present := message.GetSuggestedPost() + edited = present && post.GetAccepted() + } + case *tg.UpdateNewChannelMessage: + switch message := item.Message.(type) { + case *tg.MessageService: + action, ok := message.Action.(*tg.MessageActionSuggestedPostApproval) + if ok { + scheduleDate, hasScheduleDate := action.GetScheduleDate() + approval = !action.Rejected && !action.BalanceTooLow && hasScheduleDate && scheduleDate > 0 + } + if savedPeer, present := message.GetSavedPeerID(); !present { + t.Fatalf("approval service missing saved_peer_id") + } else if peer, ok := savedPeer.(*tg.PeerUser); !ok || peer.UserID != subscriber.ID { + t.Fatalf("approval saved_peer=%#v", savedPeer) + } + case *tg.Message: + published = message.PeerID.(*tg.PeerChannel).ChannelID == created.Channel.ID && message.Post && message.Message == "RPC suggestion" + } + } + } + if !edited || !approval || !published { + t.Fatalf("updates missing edit/approval/publish: %#v", updates.Updates) + } + + var retryRaw bin.Buffer + if err := req.Encode(&retryRaw); err != nil { + t.Fatal(err) + } + retry, err := router.Dispatch(WithLayer(WithUserID(ctx, owner.ID), 227), [8]byte{}, 0, &retryRaw) + if err != nil { + t.Fatalf("layer 227 retry: %v", err) + } + got, ok := retry.(*tg.Updates) + if !ok { + t.Fatalf("layer 227 response=%T", retry) + } + // Duplicate replay returns the persisted approval + published update to + // the caller but is never fanned out again. + if len(got.Updates) != 3 { + t.Fatalf("layer 227 duplicate updates=%d, want 3", len(got.Updates)) + } +} + +func TestSuggestedPostTLProjectionSeparatesSuggestionAndPublishedPaymentFlags(t *testing.T) { + original := domain.ChannelMessage{ChannelID: 10, ID: 1, SenderUserID: 20, From: domain.Peer{Type: domain.PeerTypeUser, ID: 20}, SavedPeer: domain.Peer{Type: domain.PeerTypeUser, ID: 20}, Date: 100, Body: "proposal", SuggestedPost: &domain.SuggestedPost{Accepted: true, Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}} + proposal := tgChannelMessage(20, original).(*tg.Message) + if _, present := proposal.GetSuggestedPost(); !present || proposal.GetPaidSuggestedPostStars() { + t.Fatalf("proposal flags=%+v", proposal) + } + published := original + published.ChannelID, published.ID, published.Post, published.SavedPeer = 11, 2, true, domain.Peer{} + post := tgChannelMessage(20, published).(*tg.Message) + if !post.GetPaidSuggestedPostStars() { + t.Fatalf("published Stars post missing paid flag") + } + if _, present := post.GetSuggestedPost(); present { + t.Fatalf("published post leaked suggested_post") + } + published.SuggestedPost.Price = &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceTON, Amount: 10_000_000} + ton := tgChannelMessage(20, published).(*tg.Message) + if !ton.GetPaidSuggestedPostTon() || ton.GetPaidSuggestedPostStars() { + t.Fatalf("published TON flags=%+v", ton) + } +} + +func TestSuggestedPostApprovalScheduleDateSurvivesExactProfiles(t *testing.T) { + action := tgChannelMessageAction(domain.ChannelMessageAction{ + Type: domain.ChannelActionSuggestedPostApproval, + SuggestedPostScheduleDate: 1_700_000_200, + }) + for _, profile := range []tlprofile.Profile{tlprofile.Profile227, tlprofile.Profile228} { + wire := &bin.Buffer{} + if err := tlprofile.EncodeObject(profile, action, wire); err != nil { + t.Fatalf("encode Layer %d approval action: %v", profile, err) + } + decodedObject, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: wire.Copy()}, tlprofile.Limits{}) + if err != nil { + t.Fatalf("decode Layer %d approval action: %v", profile, err) + } + decoded, ok := decodedObject.(*tg.MessageActionSuggestedPostApproval) + if !ok { + t.Fatalf("decode Layer %d approval action = %T", profile, decodedObject) + } + date, present := decoded.GetScheduleDate() + if !present || date != 1_700_000_200 { + t.Fatalf("Layer %d approval date=%d/%v, want 1700000200/true", profile, date, present) + } + } +} diff --git a/internal/rpc/messages_suggested_post_updates.go b/internal/rpc/messages_suggested_post_updates.go new file mode 100644 index 00000000..cdf84a15 --- /dev/null +++ b/internal/rpc/messages_suggested_post_updates.go @@ -0,0 +1,65 @@ +package rpc + +import ( + "context" + + "github.com/iamxvbaba/td/tg" + + "telesrv/internal/domain" +) + +func (r *Router) suggestedPostApprovalUpdates(ctx context.Context, viewerUserID int64, result domain.ToggleSuggestedPostApprovalResult) *tg.Updates { + updates := make([]tg.UpdateClass, 0, 4) + if result.OriginalEvent.Pts > 0 { + if update := tgChannelUpdate(viewerUserID, result.OriginalEvent); update != nil { + updates = append(updates, update) + } + } + if result.ServiceEvent.Pts > 0 { + if update := tgChannelUpdate(viewerUserID, result.ServiceEvent); update != nil { + updates = append(updates, update) + } + } + if result.Published != nil && result.Published.Event.Pts > 0 { + if update := tgChannelUpdate(viewerUserID, result.Published.Event); update != nil { + updates = append(updates, update) + } + } + if result.PayerStarsBalance != nil && result.PayerStarsBalance.UserID == viewerUserID { + updates = append(updates, &tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: result.PayerStarsBalance.Balance}}) + } + chats := r.monoforumChats(ctx, viewerUserID, result.Monoforum) + if result.Parent.ID != 0 { + chats = appendUniqueTGChats(chats, tgChannelChatMin(viewerUserID, result.Parent)) + } + messages := make([]domain.ChannelMessage, 0, 3) + if result.OriginalMessage.ID != 0 { + messages = append(messages, result.OriginalMessage) + } + if result.ServiceMessage.ID != 0 { + messages = append(messages, result.ServiceMessage) + } + if result.Published != nil { + messages = append(messages, result.Published.Message) + } + return &tg.Updates{ + Updates: updates, + Chats: chats, + Users: r.monoforumSubscriberUsers(ctx, viewerUserID, []domain.MonoforumDialog{{SavedPeer: result.SavedPeer}}, messages), + Date: int(r.clock.Now().Unix()), + } +} + +func (r *Router) enqueueSuggestedPostApprovalFanout(ctx context.Context, originUserID int64, result domain.ToggleSuggestedPostApprovalResult) { + monoOnly := result + monoOnly.Published = nil + nudge := max(result.OriginalEvent.Pts, result.ServiceEvent.Pts) + if nudge > 0 { + r.enqueueChannelFanout(ctx, channelFanoutExplicit, originUserID, result.Monoforum.ID, nudge, result.Recipients, func(bgCtx context.Context, viewerUserID int64) *tg.Updates { + return r.suggestedPostApprovalUpdates(bgCtx, viewerUserID, monoOnly) + }) + } + if result.Published != nil && result.Published.Event.Pts > 0 { + r.enqueueChannelMessageFanout(ctx, originUserID, *result.Published, nil) + } +} diff --git a/internal/rpc/suggested_post_dispatcher.go b/internal/rpc/suggested_post_dispatcher.go new file mode 100644 index 00000000..0ef6ae07 --- /dev/null +++ b/internal/rpc/suggested_post_dispatcher.go @@ -0,0 +1,59 @@ +package rpc + +import ( + "context" + "time" + + "go.uber.org/zap" + + "telesrv/internal/domain" +) + +// SuggestedPostDispatcher publishes scheduled suggestions and resolves paid +// escrow after the minimum live age (or refunds it when the post is deleted). +// Store-side row locks make multiple server instances safe. +type SuggestedPostDispatcher struct { + router *Router + log *zap.Logger + interval time.Duration + batch int +} + +func NewSuggestedPostDispatcher(router *Router, log *zap.Logger) *SuggestedPostDispatcher { + if log == nil { + log = zap.NewNop() + } + return &SuggestedPostDispatcher{router: router, log: log, interval: time.Second, batch: 50} +} + +func (d *SuggestedPostDispatcher) Run(ctx context.Context) { + if d == nil || d.router == nil { + return + } + ticker := time.NewTicker(d.interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + d.DispatchOnce(ctx) + } + } +} + +func (d *SuggestedPostDispatcher) DispatchOnce(ctx context.Context) bool { + service, ok := d.router.deps.Channels.(suggestedPostApprovalService) + if !ok { + return false + } + results, err := service.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: int(d.router.clock.Now().Unix()), Limit: d.batch}) + if err != nil { + d.log.Warn("process suggested post lifecycle", zap.Error(err)) + return false + } + for _, result := range results { + d.router.enqueueSuggestedPostApprovalFanout(ctx, 0, result) + } + return len(results) > 0 +} diff --git a/internal/store/memory/channel_dialogs.go b/internal/store/memory/channel_dialogs.go index a1de0199..dfbd19cc 100644 --- a/internal/store/memory/channel_dialogs.go +++ b/internal/store/memory/channel_dialogs.go @@ -42,7 +42,7 @@ func (s *ChannelStore) ListChannelDialogs(_ context.Context, viewerUserID int64, continue } parentMember, ok := s.members[channel.LinkedMonoforumID][viewerUserID] - if !ok || parentMember.Status != domain.ChannelMemberActive || !isChannelAdmin(parentMember) { + if !ok || !parentMember.CanManageDirectMessages() { continue } channelIDs = append(channelIDs, channelID) @@ -131,7 +131,7 @@ func (s *ChannelStore) GetChannelDialogs(_ context.Context, viewerUserID int64, continue } parentMember, ok := s.members[channel.LinkedMonoforumID][viewerUserID] - if !ok || parentMember.Status != domain.ChannelMemberActive || !isChannelAdmin(parentMember) { + if !ok || !parentMember.CanManageDirectMessages() { continue } member = syntheticMonoforumAdminMember(channel, parentMember) diff --git a/internal/store/memory/channel_helpers.go b/internal/store/memory/channel_helpers.go index bda20e6c..0d79ef0b 100644 --- a/internal/store/memory/channel_helpers.go +++ b/internal/store/memory/channel_helpers.go @@ -363,7 +363,7 @@ func (s *ChannelStore) monoforumVisibleToUserLocked(mono domain.Channel, userID if !ok || parent.Deleted || !parent.BroadcastMessagesAllowed || parent.LinkedMonoforumID != mono.ID { return false } - if member, ok := s.members[parent.ID][userID]; ok && member.Status == domain.ChannelMemberActive && isChannelAdmin(member) { + if member, ok := s.members[parent.ID][userID]; ok && member.CanManageDirectMessages() { return true } for _, msg := range s.messages[mono.ID] { @@ -418,7 +418,7 @@ func (s *ChannelStore) channelForViewerLocked(userID, channelID int64) (domain.C } if channel.Monoforum && channel.LinkedMonoforumID != 0 { parentMember, ok := s.members[channel.LinkedMonoforumID][userID] - if ok && parentMember.Status == domain.ChannelMemberActive && isChannelAdmin(parentMember) { + if ok && parentMember.CanManageDirectMessages() { return channel, syntheticMonoforumAdminMember(channel, parentMember), true, nil } parent, ok := s.channels[channel.LinkedMonoforumID] diff --git a/internal/store/memory/channel_message_helpers.go b/internal/store/memory/channel_message_helpers.go index d42ea089..897d063a 100644 --- a/internal/store/memory/channel_message_helpers.go +++ b/internal/store/memory/channel_message_helpers.go @@ -77,6 +77,10 @@ func cloneChannelMessageAction(in *domain.ChannelMessageAction) *domain.ChannelM } out.StarGift = &g } + if in.SuggestedPostPrice != nil { + price := *in.SuggestedPostPrice + out.SuggestedPostPrice = &price + } out.Wallpaper = domain.CloneWallpaperPtr(in.Wallpaper) out.Photo = domain.ClonePhotoPtr(in.Photo) return &out diff --git a/internal/store/memory/channel_message_history.go b/internal/store/memory/channel_message_history.go index 0a881f6d..d62bfd7c 100644 --- a/internal/store/memory/channel_message_history.go +++ b/internal/store/memory/channel_message_history.go @@ -24,7 +24,7 @@ func (s *ChannelStore) ListChannelHistory(_ context.Context, viewerUserID int64, // 静态过滤(不含 offset 锚点的方向条件),结果保持 id 降序。 query := strings.ToLower(strings.TrimSpace(filter.Query)) matched := make([]domain.ChannelMessage, 0, len(items)) - monoforumUserView := channel.Monoforum && !isChannelAdmin(member) + monoforumUserView := channel.Monoforum && !member.CanManageDirectMessages() for _, msg := range items { if msg.Deleted { continue diff --git a/internal/store/memory/channel_monoforum.go b/internal/store/memory/channel_monoforum.go index 8634f7dc..1df62b24 100644 --- a/internal/store/memory/channel_monoforum.go +++ b/internal/store/memory/channel_monoforum.go @@ -54,7 +54,7 @@ func (s *ChannelStore) SendMonoforumMessage(_ context.Context, req domain.SendMo return domain.SendChannelMessageResult{}, domain.ErrChannelPrivate } parentMember, parentMemberOK := s.members[parent.ID][req.SenderUserID] - isAdmin := parentMemberOK && parentMember.Status == domain.ChannelMemberActive && isChannelAdmin(parentMember) + isAdmin := parentMemberOK && parentMember.CanManageDirectMessages() if req.SenderUserID != req.SavedPeer.ID && !isAdmin { return domain.SendChannelMessageResult{}, domain.ErrChannelAdminRequired } @@ -157,7 +157,7 @@ func (s *ChannelStore) SendMonoforumMessage(_ context.Context, req domain.SendMo s.channels[req.MonoforumID] = channel recipients := []int64{req.SavedPeer.ID} for userID, member := range s.members[parent.ID] { - if member.Status == domain.ChannelMemberActive && isChannelAdmin(member) { + if member.CanManageDirectMessages() { recipients = append(recipients, userID) } } @@ -214,7 +214,7 @@ func (s *ChannelStore) ListMonoforumHistory(_ context.Context, filter domain.Mon } // ResolveMonoforumSend 按 id 取 monoforum 频道(不要求调用者是 monoforum 成员——订阅者私信频道时 -// 并非 monoforum 成员),并返回调用者是否为其母广播频道的创建者/管理员。非 monoforum/不存在 → ErrChannelInvalid。 +// 并非 monoforum 成员),并返回调用者是否可管理其母广播频道的 Direct Messages。非 monoforum/不存在 → ErrChannelInvalid。 func (s *ChannelStore) ResolveMonoforumSend(_ context.Context, viewerUserID, monoforumID int64) (domain.Channel, bool, error) { if viewerUserID == 0 || monoforumID == 0 { return domain.Channel{}, false, domain.ErrChannelInvalid @@ -226,8 +226,7 @@ func (s *ChannelStore) ResolveMonoforumSend(_ context.Context, viewerUserID, mon return domain.Channel{}, false, domain.ErrChannelInvalid } member, ok := s.members[mono.LinkedMonoforumID][viewerUserID] - isAdmin := ok && member.Status == domain.ChannelMemberActive && - (member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin) + isAdmin := ok && member.CanManageDirectMessages() return cloneChannel(mono), isAdmin, nil } diff --git a/internal/store/memory/channel_store.go b/internal/store/memory/channel_store.go index abcc0033..77788026 100644 --- a/internal/store/memory/channel_store.go +++ b/internal/store/memory/channel_store.go @@ -73,29 +73,32 @@ type ChannelStore struct { messages map[int64][]domain.ChannelMessage reactions map[int64]map[int]map[int64][]domain.ChannelMessagePeerReaction // paidReactions 是 per-(channel,message,user) 付费 reaction 累计星数 + 匿名标志。 - paidReactions map[int64]map[int]map[int64]memoryPaidReaction - top map[int64]map[string]domain.TopMessageReaction - recent map[int64]map[string]domain.RecentMessageReaction - savedTags map[int64]map[string]domain.SavedReactionTag - mentions map[int64]map[int64]map[int]memoryMention - msgViews map[int64]map[int]int - msgViewers map[int64]map[int]map[int64]struct{} - events map[int64][]domain.ChannelUpdateEvent - retention map[int64]domain.ChannelUpdateRetentionCheckpoint - adminLogs map[int64][]domain.ChannelAdminLogEvent - invites map[string]domain.ChannelInvite - importers map[int64]map[int64]domain.ChannelInviteImporter - msgSeq map[int64]int - ptsSeq map[int64]int - logSeq map[int64]int64 - randomToID map[channelRandomKey]int - sendSnapshots map[channelMessageReplayKey][]byte - sendFingerprints map[channelMessageReplayKey][]byte - deleteReceipts map[channelMessageReplayKey]*domain.ChannelUpdateEvent - starsBalances map[int64]int64 - channelStarsBalances map[int64]int64 - boostSlots map[boostSlotKey]domain.PremiumBoostSlot - readMarks map[int64]channelReadWatermark + paidReactions map[int64]map[int]map[int64]memoryPaidReaction + top map[int64]map[string]domain.TopMessageReaction + recent map[int64]map[string]domain.RecentMessageReaction + savedTags map[int64]map[string]domain.SavedReactionTag + mentions map[int64]map[int64]map[int]memoryMention + msgViews map[int64]map[int]int + msgViewers map[int64]map[int]map[int64]struct{} + events map[int64][]domain.ChannelUpdateEvent + retention map[int64]domain.ChannelUpdateRetentionCheckpoint + adminLogs map[int64][]domain.ChannelAdminLogEvent + invites map[string]domain.ChannelInvite + importers map[int64]map[int64]domain.ChannelInviteImporter + msgSeq map[int64]int + ptsSeq map[int64]int + logSeq map[int64]int64 + randomToID map[channelRandomKey]int + sendSnapshots map[channelMessageReplayKey][]byte + sendFingerprints map[channelMessageReplayKey][]byte + deleteReceipts map[channelMessageReplayKey]*domain.ChannelUpdateEvent + starsBalances map[int64]int64 + channelStarsBalances map[int64]int64 + tonBalances map[int64]int64 + channelTONBalances map[int64]int64 + suggestedPostApprovals map[memorySuggestedPostKey]memorySuggestedPostApproval + boostSlots map[boostSlotKey]domain.PremiumBoostSlot + readMarks map[int64]channelReadWatermark // topicReads 是 per-(channel,user,topic) 已读水位(forum 话题独立已读,不碰频道级 member 水位)。 topicReads map[int64]map[int64]map[int]memoryTopicRead // polls 是共享 poll 权威(与 MessageStore 同一实例);nil 时 poll 链路按未接入处理。 @@ -110,37 +113,40 @@ func (s *ChannelStore) AttachPollStore(polls *PollStore) { // NewChannelStore creates an in-memory ChannelStore. func NewChannelStore() *ChannelStore { return &ChannelStore{ - nextID: firstMemoryChannelID, - nextHash: 900000000000, - channels: make(map[int64]domain.Channel), - members: make(map[int64]map[int64]domain.ChannelMember), - dialogs: make(map[int64]map[int64]domain.ChannelDialog), - topics: make(map[int64]map[int]domain.ChannelForumTopic), - messages: make(map[int64][]domain.ChannelMessage), - reactions: make(map[int64]map[int]map[int64][]domain.ChannelMessagePeerReaction), - paidReactions: make(map[int64]map[int]map[int64]memoryPaidReaction), - top: make(map[int64]map[string]domain.TopMessageReaction), - recent: make(map[int64]map[string]domain.RecentMessageReaction), - savedTags: make(map[int64]map[string]domain.SavedReactionTag), - mentions: make(map[int64]map[int64]map[int]memoryMention), - msgViews: make(map[int64]map[int]int), - msgViewers: make(map[int64]map[int]map[int64]struct{}), - events: make(map[int64][]domain.ChannelUpdateEvent), - retention: make(map[int64]domain.ChannelUpdateRetentionCheckpoint), - adminLogs: make(map[int64][]domain.ChannelAdminLogEvent), - invites: make(map[string]domain.ChannelInvite), - importers: make(map[int64]map[int64]domain.ChannelInviteImporter), - msgSeq: make(map[int64]int), - ptsSeq: make(map[int64]int), - logSeq: make(map[int64]int64), - randomToID: make(map[channelRandomKey]int), - sendSnapshots: make(map[channelMessageReplayKey][]byte), - sendFingerprints: make(map[channelMessageReplayKey][]byte), - deleteReceipts: make(map[channelMessageReplayKey]*domain.ChannelUpdateEvent), - starsBalances: make(map[int64]int64), - channelStarsBalances: make(map[int64]int64), - boostSlots: make(map[boostSlotKey]domain.PremiumBoostSlot), - readMarks: make(map[int64]channelReadWatermark), - topicReads: make(map[int64]map[int64]map[int]memoryTopicRead), + nextID: firstMemoryChannelID, + nextHash: 900000000000, + channels: make(map[int64]domain.Channel), + members: make(map[int64]map[int64]domain.ChannelMember), + dialogs: make(map[int64]map[int64]domain.ChannelDialog), + topics: make(map[int64]map[int]domain.ChannelForumTopic), + messages: make(map[int64][]domain.ChannelMessage), + reactions: make(map[int64]map[int]map[int64][]domain.ChannelMessagePeerReaction), + paidReactions: make(map[int64]map[int]map[int64]memoryPaidReaction), + top: make(map[int64]map[string]domain.TopMessageReaction), + recent: make(map[int64]map[string]domain.RecentMessageReaction), + savedTags: make(map[int64]map[string]domain.SavedReactionTag), + mentions: make(map[int64]map[int64]map[int]memoryMention), + msgViews: make(map[int64]map[int]int), + msgViewers: make(map[int64]map[int]map[int64]struct{}), + events: make(map[int64][]domain.ChannelUpdateEvent), + retention: make(map[int64]domain.ChannelUpdateRetentionCheckpoint), + adminLogs: make(map[int64][]domain.ChannelAdminLogEvent), + invites: make(map[string]domain.ChannelInvite), + importers: make(map[int64]map[int64]domain.ChannelInviteImporter), + msgSeq: make(map[int64]int), + ptsSeq: make(map[int64]int), + logSeq: make(map[int64]int64), + randomToID: make(map[channelRandomKey]int), + sendSnapshots: make(map[channelMessageReplayKey][]byte), + sendFingerprints: make(map[channelMessageReplayKey][]byte), + deleteReceipts: make(map[channelMessageReplayKey]*domain.ChannelUpdateEvent), + starsBalances: make(map[int64]int64), + channelStarsBalances: make(map[int64]int64), + tonBalances: make(map[int64]int64), + channelTONBalances: make(map[int64]int64), + suggestedPostApprovals: make(map[memorySuggestedPostKey]memorySuggestedPostApproval), + boostSlots: make(map[boostSlotKey]domain.PremiumBoostSlot), + readMarks: make(map[int64]channelReadWatermark), + topicReads: make(map[int64]map[int64]map[int]memoryTopicRead), } } diff --git a/internal/store/memory/channel_suggested_post.go b/internal/store/memory/channel_suggested_post.go new file mode 100644 index 00000000..37dec1f5 --- /dev/null +++ b/internal/store/memory/channel_suggested_post.go @@ -0,0 +1,421 @@ +package memory + +import ( + "context" + "fmt" + "strings" + "time" + + "telesrv/internal/domain" +) + +const suggestedPostSettlementAge = 24 * 60 * 60 + +type memorySuggestedPostKey struct { + monoforumID int64 + messageID int +} + +type memorySuggestedPostApproval struct { + actorUserID int64 + parentID int64 + savedPeer domain.Peer + state domain.SuggestedPostLifecycleState + price *domain.SuggestedPostPrice + scheduleDate int + publishedMessageID int + settlementDue int + lastResult domain.ToggleSuggestedPostApprovalResult +} + +func (s *ChannelStore) ToggleSuggestedPostApproval(_ context.Context, req domain.ToggleSuggestedPostApprovalRequest) (domain.ToggleSuggestedPostApprovalResult, error) { + if req.UserID == 0 || req.MonoforumID == 0 || req.MessageID <= 0 || (!req.Reject && strings.TrimSpace(req.RejectComment) != "") { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + if req.Date == 0 { + req.Date = int(time.Now().Unix()) + } + s.mu.Lock() + defer s.mu.Unlock() + return s.toggleSuggestedPostApprovalLocked(req) +} + +func (s *ChannelStore) toggleSuggestedPostApprovalLocked(req domain.ToggleSuggestedPostApprovalRequest) (domain.ToggleSuggestedPostApprovalResult, error) { + mono, ok := s.channels[req.MonoforumID] + if !ok || mono.Deleted || !mono.Monoforum || mono.LinkedMonoforumID == 0 { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + parent, ok := s.channels[mono.LinkedMonoforumID] + if !ok || parent.Deleted || !parent.Broadcast || parent.LinkedMonoforumID != mono.ID { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + idx := -1 + var original domain.ChannelMessage + for i := range s.messages[mono.ID] { + candidate := s.messages[mono.ID][i] + if candidate.ID == req.MessageID && !candidate.Deleted { + idx, original = i, cloneChannelMessage(candidate) + break + } + } + if idx < 0 || original.SavedPeer.Type != domain.PeerTypeUser || original.SavedPeer.ID == 0 || original.SuggestedPost == nil { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + manager := s.members[parent.ID][req.UserID] + fromSubscriber := original.From.Type == domain.PeerTypeUser + if fromSubscriber { + if !manager.CanManageDirectMessages() || (!req.Reject && !manager.CanPostChannelMessages()) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostApprovalForbidden + } + } else if req.UserID != original.SavedPeer.ID { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostApprovalForbidden + } + key := memorySuggestedPostKey{monoforumID: mono.ID, messageID: original.ID} + approval, exists := s.suggestedPostApprovals[key] + if exists && approval.state != domain.SuggestedPostStateBalanceLow { + out := cloneSuggestedPostResult(approval.lastResult) + out.Duplicate = true + return out, nil + } + if original.SuggestedPost.Accepted || original.SuggestedPost.Rejected { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostAlreadyHandled + } + price := cloneSuggestedPostPrice(original.SuggestedPost.Price) + scheduleDate := original.SuggestedPost.ScheduleDate + if req.ScheduleDate > 0 { + scheduleDate = req.ScheduleDate + } + if !req.Reject && scheduleDate > 0 && (scheduleDate < req.Date+5*60 || scheduleDate > req.Date+31*24*60*60) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + recipients := s.monoforumRecipientsLocked(parent.ID, original.SavedPeer.ID) + base := domain.ToggleSuggestedPostApprovalResult{ + Monoforum: cloneChannel(mono), Parent: cloneChannel(parent), SavedPeer: original.SavedPeer, + State: domain.SuggestedPostStateBalanceLow, Recipients: recipients, + } + if req.Reject { + original.SuggestedPost.Rejected = true + original.SuggestedPost.Accepted = false + original.Pts = s.nextChannelPtsLocked(mono.ID) + s.messages[mono.ID][idx] = cloneChannelMessage(original) + edit := domain.ChannelUpdateEvent{ChannelID: mono.ID, Type: domain.ChannelUpdateEditMessage, Pts: original.Pts, PtsCount: 1, Date: req.Date, Message: cloneChannelMessage(original), SenderUserID: req.UserID} + s.appendChannelEventLocked(edit) + service, serviceEvent := s.appendSuggestedPostServiceLocked(mono, parent, req.UserID, original.SavedPeer, original.ID, req.Date, domain.ChannelMessageAction{ + Type: domain.ChannelActionSuggestedPostApproval, SuggestedPostRejected: true, + SuggestedPostRejectComment: strings.TrimSpace(req.RejectComment), SuggestedPostPrice: price, + }) + mono = s.channels[mono.ID] + base.Monoforum, base.State = cloneChannel(mono), domain.SuggestedPostStateRejected + base.OriginalMessage, base.OriginalEvent = cloneChannelMessage(original), cloneChannelEvent(edit) + base.ServiceMessage, base.ServiceEvent = cloneChannelMessage(service), cloneChannelEvent(serviceEvent) + approval = memorySuggestedPostApproval{actorUserID: req.UserID, parentID: parent.ID, savedPeer: original.SavedPeer, state: base.State, price: price, lastResult: cloneSuggestedPostResult(base)} + s.suggestedPostApprovals[key] = approval + return base, nil + } + + starsBalance, tonBalance, enough := s.reserveSuggestedPostPaymentLocked(original.SavedPeer.ID, parent.ID, price) + if !enough { + if exists { + out := cloneSuggestedPostResult(approval.lastResult) + out.PayerStarsBalance, out.PayerTONBalance = starsBalance, tonBalance + out.Duplicate = true + return out, nil + } + service, serviceEvent := s.appendSuggestedPostServiceLocked(mono, parent, req.UserID, original.SavedPeer, original.ID, req.Date, domain.ChannelMessageAction{ + Type: domain.ChannelActionSuggestedPostApproval, SuggestedPostBalanceTooLow: true, + SuggestedPostScheduleDate: scheduleDate, SuggestedPostPrice: price, + }) + base.Monoforum = cloneChannel(s.channels[mono.ID]) + base.ServiceMessage, base.ServiceEvent = cloneChannelMessage(service), cloneChannelEvent(serviceEvent) + base.PayerStarsBalance, base.PayerTONBalance = starsBalance, tonBalance + approval = memorySuggestedPostApproval{actorUserID: req.UserID, parentID: parent.ID, savedPeer: original.SavedPeer, state: base.State, price: price, scheduleDate: scheduleDate, lastResult: cloneSuggestedPostResult(base)} + s.suggestedPostApprovals[key] = approval + return base, nil + } + + original.SuggestedPost.Accepted = true + original.SuggestedPost.Rejected = false + effectivePublishDate := scheduleDate + if effectivePublishDate == 0 { + // TDesktop deliberately omits schedule_date for "Publish Now", but + // renders the approval service action as an absolute date. Persist one + // effective publication timestamp across the edited suggestion, action + // and approval record instead of leaking an accepted zero date. + effectivePublishDate = req.Date + } + original.SuggestedPost.ScheduleDate = effectivePublishDate + original.Pts = s.nextChannelPtsLocked(mono.ID) + s.messages[mono.ID][idx] = cloneChannelMessage(original) + edit := domain.ChannelUpdateEvent{ChannelID: mono.ID, Type: domain.ChannelUpdateEditMessage, Pts: original.Pts, PtsCount: 1, Date: req.Date, Message: cloneChannelMessage(original), SenderUserID: req.UserID} + s.appendChannelEventLocked(edit) + service, serviceEvent := s.appendSuggestedPostServiceLocked(mono, parent, req.UserID, original.SavedPeer, original.ID, req.Date, domain.ChannelMessageAction{ + Type: domain.ChannelActionSuggestedPostApproval, SuggestedPostScheduleDate: effectivePublishDate, SuggestedPostPrice: price, + }) + base.Monoforum, base.OriginalMessage, base.OriginalEvent = cloneChannel(s.channels[mono.ID]), cloneChannelMessage(original), cloneChannelEvent(edit) + base.ServiceMessage, base.ServiceEvent = cloneChannelMessage(service), cloneChannelEvent(serviceEvent) + base.PayerStarsBalance, base.PayerTONBalance = starsBalance, tonBalance + base.State = domain.SuggestedPostStateScheduled + approval = memorySuggestedPostApproval{actorUserID: req.UserID, parentID: parent.ID, savedPeer: original.SavedPeer, state: base.State, price: price, scheduleDate: effectivePublishDate} + if effectivePublishDate <= req.Date { + published := s.publishSuggestedPostLocked(parent, original, req.UserID, req.Date) + base.Published = &published + approval.publishedMessageID = published.Message.ID + if price == nil { + base.State = domain.SuggestedPostStateCompleted + } else { + base.State = domain.SuggestedPostStatePublished + approval.settlementDue = req.Date + suggestedPostSettlementAge + } + approval.state = base.State + } + approval.lastResult = cloneSuggestedPostResult(base) + s.suggestedPostApprovals[key] = approval + return base, nil +} + +func (s *ChannelStore) ProcessSuggestedPostLifecycle(_ context.Context, req domain.SuggestedPostLifecycleRequest) ([]domain.ToggleSuggestedPostApprovalResult, error) { + if req.Now == 0 { + req.Now = int(time.Now().Unix()) + } + if req.Limit <= 0 || req.Limit > 100 { + req.Limit = 100 + } + s.mu.Lock() + defer s.mu.Unlock() + out := make([]domain.ToggleSuggestedPostApprovalResult, 0) + for key, approval := range s.suggestedPostApprovals { + if len(out) >= req.Limit { + break + } + if approval.state != domain.SuggestedPostStateScheduled && approval.state != domain.SuggestedPostStatePublished { + continue + } + mono, monoOK := s.channels[key.monoforumID] + parent, parentOK := s.channels[approval.parentID] + if !monoOK || !parentOK { + return out, fmt.Errorf("suggested post lifecycle invariant: missing monoforum %d or parent %d", key.monoforumID, approval.parentID) + } + if mono.Deleted || !mono.Monoforum || mono.LinkedMonoforumID != parent.ID || parent.Deleted || !parent.Broadcast || parent.LinkedMonoforumID != mono.ID { + return out, fmt.Errorf("suggested post lifecycle invariant: broken monoforum link %d <-> %d", mono.ID, parent.ID) + } + if approval.scheduleDate <= 0 { + return out, fmt.Errorf("suggested post lifecycle invariant: state %s has zero publish date", approval.state) + } + var original domain.ChannelMessage + originalFound := false + for _, message := range s.messages[mono.ID] { + if message.ID == key.messageID { + original = cloneChannelMessage(message) + originalFound = true + break + } + } + if !originalFound || original.SuggestedPost == nil || !original.SuggestedPost.Accepted || original.SuggestedPost.Rejected { + return out, fmt.Errorf("suggested post lifecycle invariant: missing or invalid accepted suggestion %d/%d", mono.ID, key.messageID) + } + result := domain.ToggleSuggestedPostApprovalResult{Monoforum: cloneChannel(mono), Parent: cloneChannel(parent), SavedPeer: approval.savedPeer, State: approval.state, Recipients: s.monoforumRecipientsLocked(parent.ID, approval.savedPeer.ID)} + changed := false + if approval.state == domain.SuggestedPostStateScheduled && original.Deleted { + if approval.price != nil { + s.refundSuggestedPostPaymentLocked(approval.savedPeer.ID, approval.price) + service, event := s.appendSuggestedPostServiceLocked(mono, parent, approval.actorUserID, approval.savedPeer, key.messageID, req.Now, domain.ChannelMessageAction{Type: domain.ChannelActionSuggestedPostRefund}) + result.ServiceMessage, result.ServiceEvent = service, event + } + approval.state, result.State, changed = domain.SuggestedPostStateRefunded, domain.SuggestedPostStateRefunded, true + } + if approval.state == domain.SuggestedPostStateScheduled && approval.scheduleDate <= req.Now { + published := s.publishSuggestedPostLocked(parent, original, approval.actorUserID, req.Now) + result.Published = &published + approval.publishedMessageID = published.Message.ID + if approval.price == nil { + approval.state = domain.SuggestedPostStateCompleted + } else { + approval.state = domain.SuggestedPostStatePublished + approval.settlementDue = req.Now + suggestedPostSettlementAge + } + result.State, changed = approval.state, true + } + if approval.state == domain.SuggestedPostStatePublished { + if approval.price == nil || approval.publishedMessageID <= 0 || approval.settlementDue <= 0 { + return out, fmt.Errorf("suggested post lifecycle invariant: incomplete published state %d/%d", mono.ID, key.messageID) + } + deleted := false + publishedFound := false + for _, message := range s.messages[parent.ID] { + if message.ID == approval.publishedMessageID { + deleted = message.Deleted + publishedFound = true + break + } + } + if !publishedFound { + return out, fmt.Errorf("suggested post lifecycle invariant: missing published message %d/%d", parent.ID, approval.publishedMessageID) + } + deleteDate := s.channelMessageDeleteDateLocked(parent.ID, approval.publishedMessageID) + if deleted && (deleteDate == 0 || deleteDate < approval.settlementDue) { + s.refundSuggestedPostPaymentLocked(approval.savedPeer.ID, approval.price) + service, event := s.appendSuggestedPostServiceLocked(mono, parent, approval.actorUserID, approval.savedPeer, key.messageID, req.Now, domain.ChannelMessageAction{Type: domain.ChannelActionSuggestedPostRefund}) + result.ServiceMessage, result.ServiceEvent = service, event + approval.state, result.State, changed = domain.SuggestedPostStateRefunded, domain.SuggestedPostStateRefunded, true + } else if approval.settlementDue <= req.Now { + s.settleSuggestedPostPaymentLocked(parent.ID, approval.price) + service, event := s.appendSuggestedPostServiceLocked(mono, parent, approval.actorUserID, approval.savedPeer, key.messageID, req.Now, domain.ChannelMessageAction{Type: domain.ChannelActionSuggestedPostSuccess, SuggestedPostPrice: cloneSuggestedPostPrice(approval.price)}) + result.ServiceMessage, result.ServiceEvent = service, event + approval.state, result.State, changed = domain.SuggestedPostStateCompleted, domain.SuggestedPostStateCompleted, true + } + } + if changed { + result.Monoforum, result.Parent = cloneChannel(s.channels[mono.ID]), cloneChannel(s.channels[parent.ID]) + approval.lastResult = cloneSuggestedPostResult(result) + s.suggestedPostApprovals[key] = approval + out = append(out, result) + } + } + return out, nil +} + +func (s *ChannelStore) channelMessageDeleteDateLocked(channelID int64, messageID int) int { + for i := len(s.events[channelID]) - 1; i >= 0; i-- { + event := s.events[channelID][i] + if event.Type != domain.ChannelUpdateDeleteMessages { + continue + } + for _, id := range event.MessageIDs { + if id == messageID { + return event.Date + } + } + } + return 0 +} + +func (s *ChannelStore) reserveSuggestedPostPaymentLocked(payerID, parentID int64, price *domain.SuggestedPostPrice) (*domain.StarsBalance, *int64, bool) { + if price == nil { + return nil, nil, true + } + switch price.Kind { + case domain.SuggestedPostPriceStars: + current, ok := s.starsBalances[payerID] + if !ok { + current = domain.DefaultStarsStartingGrant + } + balance := &domain.StarsBalance{UserID: payerID, Balance: current, Granted: true} + if price.Nanos != 0 || current < price.Amount { + return balance, nil, false + } + current -= price.Amount + s.starsBalances[payerID] = current + balance.Balance = current + return balance, nil, true + case domain.SuggestedPostPriceTON: + current := s.tonBalances[payerID] + balance := current + if current < price.Amount { + return nil, &balance, false + } + current -= price.Amount + s.tonBalances[payerID] = current + balance = current + return nil, &balance, true + default: + return nil, nil, false + } +} + +func (s *ChannelStore) refundSuggestedPostPaymentLocked(payerID int64, price *domain.SuggestedPostPrice) { + if price == nil { + return + } + if price.Kind == domain.SuggestedPostPriceStars { + s.starsBalances[payerID] += price.Amount + } else if price.Kind == domain.SuggestedPostPriceTON { + s.tonBalances[payerID] += price.Amount + } +} + +func (s *ChannelStore) settleSuggestedPostPaymentLocked(parentID int64, price *domain.SuggestedPostPrice) { + if price == nil { + return + } + credit := price.Amount * paidMessageChannelCommissionPermille / 1000 + if price.Kind == domain.SuggestedPostPriceStars { + s.channelStarsBalances[parentID] += credit + } else if price.Kind == domain.SuggestedPostPriceTON { + s.channelTONBalances[parentID] += credit + } +} + +func (s *ChannelStore) appendSuggestedPostServiceLocked(mono, parent domain.Channel, actor int64, saved domain.Peer, replyID, date int, action domain.ChannelMessageAction) (domain.ChannelMessage, domain.ChannelUpdateEvent) { + pts := s.nextChannelPtsLocked(mono.ID) + from := domain.Peer{Type: domain.PeerTypeUser, ID: actor} + if member, ok := s.members[parent.ID][actor]; ok && member.CanManageDirectMessages() { + from = domain.Peer{Type: domain.PeerTypeChannel, ID: parent.ID} + } + msg := domain.ChannelMessage{ChannelID: mono.ID, ID: s.nextChannelMessageIDLocked(mono.ID), SenderUserID: actor, From: from, SavedPeer: saved, Date: date, Action: cloneChannelMessageAction(&action), ReplyTo: &domain.MessageReply{Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: mono.ID}, MessageID: replyID}, Pts: pts} + event := domain.ChannelUpdateEvent{ChannelID: mono.ID, Type: domain.ChannelUpdateNewMessage, Pts: pts, PtsCount: 1, Date: date, Message: cloneChannelMessage(msg), SenderUserID: actor} + s.messages[mono.ID] = append(s.messages[mono.ID], cloneChannelMessage(msg)) + s.appendChannelEventLocked(event) + mono.TopMessageID, mono.Pts = msg.ID, pts + s.channels[mono.ID] = mono + return cloneChannelMessage(msg), cloneChannelEvent(event) +} + +func (s *ChannelStore) publishSuggestedPostLocked(parent domain.Channel, original domain.ChannelMessage, actor int64, date int) domain.SendChannelMessageResult { + msg := cloneChannelMessage(original) + msg.ChannelID, msg.ID, msg.RandomID, msg.SenderUserID = parent.ID, s.nextChannelMessageIDLocked(parent.ID), 0, actor + msg.From, msg.SavedPeer, msg.Date, msg.EditDate, msg.Post = domain.Peer{Type: domain.PeerTypeChannel, ID: parent.ID}, domain.Peer{}, date, 0, true + msg.ReplyTo, msg.PaidMessageStars, msg.Pts, msg.Deleted = nil, 0, s.nextChannelPtsLocked(parent.ID), false + event := domain.ChannelUpdateEvent{ChannelID: parent.ID, Type: domain.ChannelUpdateNewMessage, Pts: msg.Pts, PtsCount: 1, Date: date, Message: cloneChannelMessage(msg), SenderUserID: actor} + s.messages[parent.ID] = append(s.messages[parent.ID], cloneChannelMessage(msg)) + s.appendChannelEventLocked(event) + parent.TopMessageID, parent.Pts = msg.ID, msg.Pts + s.channels[parent.ID] = parent + recipients := make([]int64, 0, len(s.members[parent.ID])) + for id, member := range s.members[parent.ID] { + if member.Status == domain.ChannelMemberActive { + recipients = append(recipients, id) + } + } + return domain.SendChannelMessageResult{Channel: cloneChannel(parent), Message: cloneChannelMessage(msg), Event: cloneChannelEvent(event), Recipients: uniqueNonZero(recipients, 0)} +} + +func (s *ChannelStore) monoforumRecipientsLocked(parentID, subscriberID int64) []int64 { + ids := []int64{subscriberID} + for id, member := range s.members[parentID] { + if member.CanManageDirectMessages() { + ids = append(ids, id) + } + } + return uniqueNonZero(ids, 0) +} + +func cloneSuggestedPostPrice(in *domain.SuggestedPostPrice) *domain.SuggestedPostPrice { + if in == nil { + return nil + } + out := *in + return &out +} + +func cloneSuggestedPostResult(in domain.ToggleSuggestedPostApprovalResult) domain.ToggleSuggestedPostApprovalResult { + in.Monoforum, in.Parent = cloneChannel(in.Monoforum), cloneChannel(in.Parent) + in.OriginalMessage, in.ServiceMessage = cloneChannelMessage(in.OriginalMessage), cloneChannelMessage(in.ServiceMessage) + in.OriginalEvent, in.ServiceEvent = cloneChannelEvent(in.OriginalEvent), cloneChannelEvent(in.ServiceEvent) + in.Recipients = append([]int64(nil), in.Recipients...) + if in.Published != nil { + p := *in.Published + p.Message = cloneChannelMessage(p.Message) + p.Event = cloneChannelEvent(p.Event) + p.Recipients = append([]int64(nil), p.Recipients...) + in.Published = &p + } + if in.PayerStarsBalance != nil { + b := *in.PayerStarsBalance + in.PayerStarsBalance = &b + } + if in.PayerTONBalance != nil { + b := *in.PayerTONBalance + in.PayerTONBalance = &b + } + return in +} diff --git a/internal/store/memory/channel_suggested_post_test.go b/internal/store/memory/channel_suggested_post_test.go new file mode 100644 index 00000000..3e4d3c80 --- /dev/null +++ b/internal/store/memory/channel_suggested_post_test.go @@ -0,0 +1,276 @@ +package memory + +import ( + "context" + "errors" + "testing" + + "telesrv/internal/domain" +) + +func newSuggestedPostMemoryFixture(t *testing.T) (*ChannelStore, domain.Channel, domain.Channel, domain.Peer) { + t.Helper() + ctx := context.Background() + store := NewChannelStore() + created, err := store.CreateChannel(ctx, domain.CreateChannelRequest{CreatorUserID: 1, Title: "Suggestions", Broadcast: true, Date: 1_700_000_000}) + if err != nil { + t.Fatal(err) + } + enabled, err := store.SetPaidMessagesPrice(ctx, 1, created.Channel.ID, 0, true) + if err != nil { + t.Fatal(err) + } + mono := store.channels[enabled.Channel.LinkedMonoforumID] + return store, store.channels[created.Channel.ID], mono, domain.Peer{Type: domain.PeerTypeUser, ID: 42} +} + +func TestMonoforumManagerRequiresManageDirectMessages(t *testing.T) { + ctx := context.Background() + store, parent, mono, subscriber := newSuggestedPostMemoryFixture(t) + store.mu.Lock() + store.members[parent.ID][2] = domain.ChannelMember{ChannelID: parent.ID, UserID: 2, Role: domain.ChannelRoleAdmin, Status: domain.ChannelMemberActive, AdminRights: domain.ChannelAdminRights{PostMessages: true}} + store.mu.Unlock() + if _, manager, err := store.ResolveMonoforumSend(ctx, 2, mono.ID); err != nil || manager { + t.Fatalf("ordinary admin resolved as manager: manager=%v err=%v", manager, err) + } + if _, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: 2, SavedPeer: subscriber, RandomID: 1, Message: "must not send", Date: 1_700_000_010}); !errors.Is(err, domain.ErrChannelAdminRequired) { + t.Fatalf("ordinary admin send err=%v, want admin required", err) + } + fromSubscriber, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, RandomID: 10, Message: "private", Date: 1_700_000_010}) + if err != nil { + t.Fatal(err) + } + if containsInt64(fromSubscriber.Recipients, 2) { + t.Fatalf("ordinary admin leaked into recipients: %v", fromSubscriber.Recipients) + } + dialogs, err := store.ListChannelDialogs(ctx, 2, domain.DialogFilter{Limit: 20}) + if err != nil { + t.Fatal(err) + } + for _, dialog := range dialogs.Dialogs { + if dialog.Peer.ID == mono.ID { + t.Fatalf("ordinary admin received monoforum dialog") + } + } + store.mu.Lock() + member := store.members[parent.ID][2] + member.AdminRights.ManageDirectMessages = true + store.members[parent.ID][2] = member + store.mu.Unlock() + if _, manager, err := store.ResolveMonoforumSend(ctx, 2, mono.ID); err != nil || !manager { + t.Fatalf("DM manager not resolved: manager=%v err=%v", manager, err) + } + dialogs, err = store.ListChannelDialogs(ctx, 2, domain.DialogFilter{Limit: 20}) + if err != nil { + t.Fatal(err) + } + foundMono := false + for _, dialog := range dialogs.Dialogs { + foundMono = foundMono || dialog.Peer.ID == mono.ID + } + if !foundMono { + t.Fatalf("DM manager missing monoforum dialog") + } + if _, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: 2, SavedPeer: subscriber, RandomID: 2, Message: "allowed", Date: 1_700_000_011}); err != nil { + t.Fatalf("DM manager send: %v", err) + } +} + +func TestSuggestedPostStarsApprovalRefundAndSettlement(t *testing.T) { + ctx := context.Background() + store, parent, mono, subscriber := newSuggestedPostMemoryFixture(t) + store.starsBalances[subscriber.ID] = 100 + + suggestion, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, RandomID: 11, Message: "publish me", SuggestedPost: &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}, Date: 1_700_000_100}) + if err != nil { + t.Fatal(err) + } + approved, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, Date: 1_700_000_200}) + if err != nil { + t.Fatal(err) + } + if approved.State != domain.SuggestedPostStatePublished || approved.OriginalEvent.Type != domain.ChannelUpdateEditMessage || approved.ServiceMessage.Action == nil || approved.ServiceMessage.Action.Type != domain.ChannelActionSuggestedPostApproval || approved.Published == nil { + t.Fatalf("approval result=%+v", approved) + } + if approved.OriginalMessage.SuggestedPost.ScheduleDate != 1_700_000_200 || approved.ServiceMessage.Action.SuggestedPostScheduleDate != 1_700_000_200 { + t.Fatalf("immediate approval dates original/action=%d/%d, want commit date", approved.OriginalMessage.SuggestedPost.ScheduleDate, approved.ServiceMessage.Action.SuggestedPostScheduleDate) + } + if store.starsBalances[subscriber.ID] != 90 || store.channelStarsBalances[parent.ID] != 0 { + t.Fatalf("escrow/channel balances=%d/%d, want 90/0", store.starsBalances[subscriber.ID], store.channelStarsBalances[parent.ID]) + } + duplicate, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, Date: 1_700_000_201}) + if err != nil || !duplicate.Duplicate || store.starsBalances[subscriber.ID] != 90 { + t.Fatalf("duplicate=%+v err=%v balance=%d", duplicate, err, store.starsBalances[subscriber.ID]) + } + if duplicate.OriginalMessage.SuggestedPost.ScheduleDate != 1_700_000_200 || duplicate.ServiceMessage.Action.SuggestedPostScheduleDate != 1_700_000_200 { + t.Fatalf("duplicate changed immediate approval date: %+v", duplicate) + } + store.mu.Lock() + for i := range store.messages[parent.ID] { + if store.messages[parent.ID][i].ID == approved.Published.Message.ID { + store.messages[parent.ID][i].Deleted = true + } + } + store.mu.Unlock() + lifecycle, err := store.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: 1_700_000_300, Limit: 10}) + if err != nil || len(lifecycle) != 1 || lifecycle[0].State != domain.SuggestedPostStateRefunded || lifecycle[0].ServiceMessage.Action.Type != domain.ChannelActionSuggestedPostRefund { + t.Fatalf("refund lifecycle=%+v err=%v", lifecycle, err) + } + if store.starsBalances[subscriber.ID] != 100 || store.channelStarsBalances[parent.ID] != 0 { + t.Fatalf("refund balances=%d/%d, want 100/0", store.starsBalances[subscriber.ID], store.channelStarsBalances[parent.ID]) + } + + second, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, RandomID: 12, Message: "settle me", SuggestedPost: &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 20}}, Date: 1_700_000_400}) + if err != nil { + t.Fatal(err) + } + settling, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: second.Message.ID, Date: 1_700_000_500}) + if err != nil || settling.State != domain.SuggestedPostStatePublished { + t.Fatalf("second approval=%+v err=%v", settling, err) + } + lifecycle, err = store.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: 1_700_000_500 + suggestedPostSettlementAge, Limit: 10}) + if err != nil || len(lifecycle) != 1 || lifecycle[0].State != domain.SuggestedPostStateCompleted || lifecycle[0].ServiceMessage.Action.Type != domain.ChannelActionSuggestedPostSuccess { + t.Fatalf("success lifecycle=%+v err=%v", lifecycle, err) + } + if store.starsBalances[subscriber.ID] != 80 || store.channelStarsBalances[parent.ID] != 17 { + t.Fatalf("settled balances=%d/%d, want 80/17", store.starsBalances[subscriber.ID], store.channelStarsBalances[parent.ID]) + } +} + +func TestSuggestedPostLowBalanceRetryScheduleAndRoleMatrix(t *testing.T) { + ctx := context.Background() + store, parent, mono, subscriber := newSuggestedPostMemoryFixture(t) + store.starsBalances[subscriber.ID] = 5 + suggestion, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, RandomID: 21, Message: "later", SuggestedPost: &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}, Date: 1_700_001_000}) + if err != nil { + t.Fatal(err) + } + low, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, ScheduleDate: 1_700_001_400, Date: 1_700_001_000}) + if err != nil || low.State != domain.SuggestedPostStateBalanceLow || low.ServiceMessage.Action == nil || !low.ServiceMessage.Action.SuggestedPostBalanceTooLow { + t.Fatalf("low=%+v err=%v", low, err) + } + again, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, ScheduleDate: 1_700_001_400, Date: 1_700_001_001}) + if err != nil || !again.Duplicate { + t.Fatalf("low retry=%+v err=%v", again, err) + } + store.starsBalances[subscriber.ID] = 20 + accepted, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, ScheduleDate: 1_700_001_400, Date: 1_700_001_050}) + if err != nil || accepted.State != domain.SuggestedPostStateScheduled || accepted.Published != nil { + t.Fatalf("scheduled=%+v err=%v", accepted, err) + } + due, err := store.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: 1_700_001_400, Limit: 10}) + if err != nil || len(due) != 1 || due[0].Published == nil || due[0].State != domain.SuggestedPostStatePublished { + t.Fatalf("due=%+v err=%v", due, err) + } + + store.mu.Lock() + store.members[parent.ID][2] = domain.ChannelMember{ChannelID: parent.ID, UserID: 2, Role: domain.ChannelRoleAdmin, Status: domain.ChannelMemberActive, AdminRights: domain.ChannelAdminRights{ManageDirectMessages: true}} + store.mu.Unlock() + third, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, RandomID: 22, Message: "decline only", SuggestedPost: &domain.SuggestedPost{}, Date: 1_700_002_000}) + if err != nil { + t.Fatal(err) + } + if _, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 2, MonoforumID: mono.ID, MessageID: third.Message.ID, Date: 1_700_002_100}); !errors.Is(err, domain.ErrSuggestedPostApprovalForbidden) { + t.Fatalf("manager without post right approve err=%v", err) + } + rejected, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 2, MonoforumID: mono.ID, MessageID: third.Message.ID, Reject: true, RejectComment: "no", Date: 1_700_002_100}) + if err != nil || rejected.State != domain.SuggestedPostStateRejected { + t.Fatalf("decline=%+v err=%v", rejected, err) + } +} + +func TestChannelAuthoredSuggestedPostAcceptedBySubscriber(t *testing.T) { + ctx := context.Background() + store, _, mono, subscriber := newSuggestedPostMemoryFixture(t) + fromChannel, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: 1, SavedPeer: subscriber, RandomID: 31, Message: "channel proposal", SuggestedPost: &domain.SuggestedPost{}, Date: 1_700_003_000}) + if err != nil { + t.Fatal(err) + } + result, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: subscriber.ID, MonoforumID: mono.ID, MessageID: fromChannel.Message.ID, Date: 1_700_003_100}) + if err != nil || result.State != domain.SuggestedPostStateCompleted || result.Published == nil { + t.Fatalf("subscriber approval=%+v err=%v", result, err) + } +} + +func TestScheduledSuggestedPostDeletionRefundsBeforePublication(t *testing.T) { + ctx := context.Background() + store, parent, mono, subscriber := newSuggestedPostMemoryFixture(t) + store.starsBalances[subscriber.ID] = 30 + suggestion, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, RandomID: 41, Message: "cancel scheduled", SuggestedPost: &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}, Date: 1_700_004_000}) + if err != nil { + t.Fatal(err) + } + accepted, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, ScheduleDate: 1_700_004_600, Date: 1_700_004_000}) + if err != nil || accepted.State != domain.SuggestedPostStateScheduled { + t.Fatalf("accepted=%+v err=%v", accepted, err) + } + store.mu.Lock() + for i := range store.messages[mono.ID] { + if store.messages[mono.ID][i].ID == suggestion.Message.ID { + store.messages[mono.ID][i].Deleted = true + } + } + store.mu.Unlock() + resolved, err := store.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: 1_700_004_100, Limit: 10}) + if err != nil || len(resolved) != 1 || resolved[0].State != domain.SuggestedPostStateRefunded || resolved[0].Published != nil { + t.Fatalf("resolved=%+v err=%v", resolved, err) + } + if store.starsBalances[subscriber.ID] != 30 || store.channelStarsBalances[parent.ID] != 0 { + t.Fatalf("balances=%d/%d", store.starsBalances[subscriber.ID], store.channelStarsBalances[parent.ID]) + } +} + +func TestSuggestedPostLifecycleFailsFastOnCorruptAcceptedState(t *testing.T) { + ctx := context.Background() + store, _, mono, subscriber := newSuggestedPostMemoryFixture(t) + suggestion, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{ + MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, + RandomID: 61, Message: "must fail fast", SuggestedPost: &domain.SuggestedPost{}, Date: 1_700_006_000, + }) + if err != nil { + t.Fatal(err) + } + if _, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{ + UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, + ScheduleDate: 1_700_006_600, Date: 1_700_006_000, + }); err != nil { + t.Fatal(err) + } + store.mu.Lock() + for i, message := range store.messages[mono.ID] { + if message.ID == suggestion.Message.ID { + store.messages[mono.ID] = append(store.messages[mono.ID][:i], store.messages[mono.ID][i+1:]...) + break + } + } + store.mu.Unlock() + if _, err := store.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: 1_700_006_100, Limit: 10}); err == nil { + t.Fatal("corrupt accepted suggestion was silently skipped") + } +} + +func TestSuggestedPostDeletedAfterMinimumAgeStillSettles(t *testing.T) { + ctx := context.Background() + store, parent, mono, subscriber := newSuggestedPostMemoryFixture(t) + store.starsBalances[subscriber.ID] = 30 + suggestion, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: mono.ID, SenderUserID: subscriber.ID, SavedPeer: subscriber, RandomID: 51, Message: "late delete", SuggestedPost: &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}, Date: 1_700_005_000}) + if err != nil { + t.Fatal(err) + } + approvedAt := 1_700_005_100 + approved, err := store.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: 1, MonoforumID: mono.ID, MessageID: suggestion.Message.ID, Date: approvedAt}) + if err != nil || approved.Published == nil { + t.Fatalf("approved=%+v err=%v", approved, err) + } + due := approvedAt + suggestedPostSettlementAge + if _, err := store.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{UserID: 1, ChannelID: parent.ID, IDs: []int{approved.Published.Message.ID}, Date: due + 1}); err != nil { + t.Fatal(err) + } + resolved, err := store.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: due + 2, Limit: 10}) + if err != nil || len(resolved) != 1 || resolved[0].State != domain.SuggestedPostStateCompleted || resolved[0].ServiceMessage.Action == nil || resolved[0].ServiceMessage.Action.Type != domain.ChannelActionSuggestedPostSuccess { + t.Fatalf("resolved=%+v err=%v", resolved, err) + } + if store.starsBalances[subscriber.ID] != 20 || store.channelStarsBalances[parent.ID] != 8 { + t.Fatalf("balances=%d/%d, want 20/8", store.starsBalances[subscriber.ID], store.channelStarsBalances[parent.ID]) + } +} diff --git a/internal/store/memory/channel_updates.go b/internal/store/memory/channel_updates.go index 8f626a96..033daca0 100644 --- a/internal/store/memory/channel_updates.go +++ b/internal/store/memory/channel_updates.go @@ -49,7 +49,7 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann if msg.Deleted { continue } - if channel.Monoforum && !isChannelAdmin(member) && msg.SavedPeer != (domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}) { + if channel.Monoforum && !member.CanManageDirectMessages() && msg.SavedPeer != (domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}) { continue } if msg.ID <= member.AvailableMinID { @@ -72,7 +72,7 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann events := make([]domain.ChannelUpdateEvent, 0, limit) lastPts := req.Pts var visibleMonoforumMessageIDs map[int]struct{} - if channel.Monoforum && !isChannelAdmin(member) { + if channel.Monoforum && !member.CanManageDirectMessages() { visibleMonoforumMessageIDs = make(map[int]struct{}) savedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID} for _, message := range s.messages[req.ChannelID] { @@ -90,7 +90,7 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann if !ok { continue } - if channel.Monoforum && !isChannelAdmin(member) { + if channel.Monoforum && !member.CanManageDirectMessages() { visible, ok = filterMonoforumEventForUser(visible, req.UserID, visibleMonoforumMessageIDs) if !ok { continue diff --git a/internal/store/postgres/channel_dialogs.go b/internal/store/postgres/channel_dialogs.go index d4dcda87..50a5baa7 100644 --- a/internal/store/postgres/channel_dialogs.go +++ b/internal/store/postgres/channel_dialogs.go @@ -267,7 +267,7 @@ WHERE i.user_id = $1 AND NOT i.deleted AND i.role IN ('creator','admin') AND pm.status = 'active' - AND pm.role IN ('creator','admin') + AND (pm.role = 'creator' OR (pm.role = 'admin' AND COALESCE((pm.admin_rights->>'ManageDirectMessages')::boolean, false))) ORDER BY COALESCE(d.pinned, false) DESC, COALESCE(d.pinned_order, 0) DESC, COALESCE(top_msg.message_date, d.top_message_date, c.date) DESC, diff --git a/internal/store/postgres/channel_helpers.go b/internal/store/postgres/channel_helpers.go index c3650cab..87c03e93 100644 --- a/internal/store/postgres/channel_helpers.go +++ b/internal/store/postgres/channel_helpers.go @@ -310,7 +310,8 @@ WITH visible_channels AS ( WHERE mono.monoforum AND NOT mono.deleted AND (EXISTS ( SELECT 1 FROM channel_members admin - WHERE admin.channel_id = parent.id AND admin.user_id = $1 AND admin.status = 'active' AND admin.role IN ('creator', 'admin') + WHERE admin.channel_id = parent.id AND admin.user_id = $1 AND admin.status = 'active' + AND (admin.role = 'creator' OR (admin.role = 'admin' AND COALESCE((admin.admin_rights->>'ManageDirectMessages')::boolean, false))) ) OR EXISTS ( SELECT 1 FROM channel_messages message WHERE message.channel_id = mono.id AND message.saved_peer_type = 'user' AND message.saved_peer_id = $1 AND NOT message.deleted @@ -355,7 +356,8 @@ WITH visible_channels AS ( WHERE mono.monoforum AND NOT mono.deleted AND (EXISTS ( SELECT 1 FROM channel_members admin - WHERE admin.channel_id = parent.id AND admin.user_id = $1 AND admin.status = 'active' AND admin.role IN ('creator', 'admin') + WHERE admin.channel_id = parent.id AND admin.user_id = $1 AND admin.status = 'active' + AND (admin.role = 'creator' OR (admin.role = 'admin' AND COALESCE((admin.admin_rights->>'ManageDirectMessages')::boolean, false))) ) OR EXISTS ( SELECT 1 FROM channel_messages message WHERE message.channel_id = mono.id AND message.saved_peer_type = 'user' AND message.saved_peer_id = $1 AND NOT message.deleted diff --git a/internal/store/postgres/channel_member_helpers.go b/internal/store/postgres/channel_member_helpers.go index ff26a5bb..9d4f9360 100644 --- a/internal/store/postgres/channel_member_helpers.go +++ b/internal/store/postgres/channel_member_helpers.go @@ -454,7 +454,7 @@ func (s *ChannelStore) monoforumAdminPreview(ctx context.Context, db sqlcgen.DBT } return domain.ChannelMember{}, domain.Channel{}, false, err } - if !isChannelAdmin(parentMember) { + if !parentMember.CanManageDirectMessages() { return domain.ChannelMember{}, domain.Channel{}, false, nil } return syntheticMonoforumAdminMember(mono, parentMember), parent, true, nil diff --git a/internal/store/postgres/channel_message_history.go b/internal/store/postgres/channel_message_history.go index 8b5a9d94..61971770 100644 --- a/internal/store/postgres/channel_message_history.go +++ b/internal/store/postgres/channel_message_history.go @@ -25,7 +25,7 @@ func (s *ChannelStore) ListChannelHistory(ctx context.Context, viewerUserID int6 base := "channel_id = $1 AND NOT deleted" extraChannels := []domain.Channel(nil) if channel.Monoforum { - if isChannelAdmin(member) { + if member.CanManageDirectMessages() { base += " AND saved_peer_id = 0" } else { baseArgs = append(baseArgs, viewerUserID) diff --git a/internal/store/postgres/channel_monoforum.go b/internal/store/postgres/channel_monoforum.go index ff728a92..7baf770c 100644 --- a/internal/store/postgres/channel_monoforum.go +++ b/internal/store/postgres/channel_monoforum.go @@ -99,7 +99,7 @@ FOR SHARE OF m, p`, channel.ID).Scan( if parentMemberErr != nil && !errors.Is(parentMemberErr, domain.ErrChannelPrivate) { return domain.SendChannelMessageResult{}, parentMemberErr } - isAdmin := parentMemberErr == nil && parentMember.Status == domain.ChannelMemberActive && isChannelAdmin(parentMember) + isAdmin := parentMemberErr == nil && parentMember.CanManageDirectMessages() if req.SenderUserID != req.SavedPeer.ID && !isAdmin { return domain.SendChannelMessageResult{}, domain.ErrChannelAdminRequired } @@ -237,7 +237,12 @@ SELECT EXISTS ( return domain.SendChannelMessageResult{}, fmt.Errorf("update monoforum top: %w", err) } recipients := []int64{req.SavedPeer.ID} - rows, err := tx.Query(ctx, `SELECT user_id FROM channel_members WHERE channel_id = $1 AND status = 'active' AND role IN ('creator', 'admin') ORDER BY user_id`, parent.ID) + rows, err := tx.Query(ctx, ` +SELECT user_id +FROM channel_members +WHERE channel_id = $1 AND status = 'active' + AND (role = 'creator' OR (role = 'admin' AND COALESCE((admin_rights->>'ManageDirectMessages')::boolean, false))) +ORDER BY user_id`, parent.ID) if err != nil { return domain.SendChannelMessageResult{}, fmt.Errorf("list monoforum recipients: %w", err) } @@ -313,7 +318,7 @@ func (s *ChannelStore) ListMonoforumHistory(ctx context.Context, filter domain.M } // ResolveMonoforumSend 按 id 取 monoforum 频道(不要求调用者是 monoforum 成员),并返回调用者是否为 -// 其母广播频道的创建者/管理员。非 monoforum/不存在 → ErrChannelInvalid。 +// 其母广播频道 Direct Messages 管理者。非 monoforum/不存在 → ErrChannelInvalid。 func (s *ChannelStore) ResolveMonoforumSend(ctx context.Context, viewerUserID, monoforumID int64) (domain.Channel, bool, error) { if viewerUserID == 0 || monoforumID == 0 { return domain.Channel{}, false, domain.ErrChannelInvalid @@ -330,8 +335,7 @@ func (s *ChannelStore) ResolveMonoforumSend(ctx context.Context, viewerUserID, m } isAdmin := false if _, member, memberErr := s.getChannelForMember(ctx, s.db, viewerUserID, mono.LinkedMonoforumID); memberErr == nil { - isAdmin = member.Status == domain.ChannelMemberActive && - (member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin) + isAdmin = member.CanManageDirectMessages() } else if !errors.Is(memberErr, domain.ErrChannelPrivate) { return domain.Channel{}, false, memberErr } diff --git a/internal/store/postgres/channel_suggested_post.go b/internal/store/postgres/channel_suggested_post.go new file mode 100644 index 00000000..9cde2a95 --- /dev/null +++ b/internal/store/postgres/channel_suggested_post.go @@ -0,0 +1,708 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/jackc/pgx/v5" + + "telesrv/internal/domain" +) + +const suggestedPostSettlementAge = 24 * 60 * 60 + +type persistedSuggestedPostApproval struct { + monoforumID, parentID, actorID, payerID int64 + messageID, scheduleDate, approvalServiceID, publishedMessageID, settlementDue, finalServiceID int + state domain.SuggestedPostLifecycleState + price *domain.SuggestedPostPrice +} + +func (s *ChannelStore) ToggleSuggestedPostApproval(ctx context.Context, req domain.ToggleSuggestedPostApprovalRequest) (domain.ToggleSuggestedPostApprovalResult, error) { + if req.UserID == 0 || req.MonoforumID == 0 || req.MessageID <= 0 || (!req.Reject && strings.TrimSpace(req.RejectComment) != "") { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + if req.Date == 0 { + req.Date = nowUnix() + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.ToggleSuggestedPostApprovalResult{}, fmt.Errorf("toggle suggested post: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, fmt.Errorf("begin toggle suggested post: %w", err) + } + committed := false + defer func() { + if !committed { + _ = tx.Rollback(ctx) + } + }() + + mono, err := getChannelByID(ctx, tx, req.MonoforumID) + if err != nil { + if errors.Is(err, domain.ErrChannelInvalid) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + return domain.ToggleSuggestedPostApprovalResult{}, err + } + if mono.Deleted || !mono.Monoforum || mono.LinkedMonoforumID == 0 { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + parent, err := getChannelByID(ctx, tx, mono.LinkedMonoforumID) + if err != nil { + if errors.Is(err, domain.ErrChannelInvalid) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + return domain.ToggleSuggestedPostApprovalResult{}, err + } + if parent.Deleted || !parent.Broadcast || parent.LinkedMonoforumID != mono.ID { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + if _, err := tx.Exec(ctx, `SELECT 1 FROM channel_messages WHERE channel_id=$1 AND id=$2 FOR UPDATE`, mono.ID, req.MessageID); err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + original, err := s.getChannelMessage(ctx, tx, mono.ID, req.MessageID) + if err != nil { + if errors.Is(err, domain.ErrMessageIDInvalid) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + return domain.ToggleSuggestedPostApprovalResult{}, err + } + if original.Deleted || original.SavedPeer.Type != domain.PeerTypeUser || original.SavedPeer.ID == 0 || original.SuggestedPost == nil { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + fromSubscriber := original.From.Type == domain.PeerTypeUser + manager := domain.ChannelMember{} + if fromSubscriber { + manager, err = s.getChannelMember(ctx, tx, parent.ID, req.UserID) + if err != nil { + if errors.Is(err, domain.ErrChannelPrivate) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostApprovalForbidden + } + return domain.ToggleSuggestedPostApprovalResult{}, err + } + if !manager.CanManageDirectMessages() || (!req.Reject && !manager.CanPostChannelMessages()) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostApprovalForbidden + } + } else if req.UserID != original.SavedPeer.ID { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostApprovalForbidden + } + + existing, found, err := loadSuggestedPostApprovalTx(ctx, tx, mono.ID, original.ID, true) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + if found && existing.state != domain.SuggestedPostStateBalanceLow { + result, err := s.loadSuggestedPostResultTx(ctx, tx, existing, true) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + if err := tx.Commit(ctx); err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + committed = true + return result, nil + } + if original.SuggestedPost.Accepted || original.SuggestedPost.Rejected { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostAlreadyHandled + } + price := cloneSuggestedPricePG(original.SuggestedPost.Price) + if price != nil && price.Kind == domain.SuggestedPostPriceStars && price.Nanos != 0 { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + scheduleDate := original.SuggestedPost.ScheduleDate + if req.ScheduleDate > 0 { + scheduleDate = req.ScheduleDate + } + if !req.Reject && scheduleDate > 0 && (scheduleDate < req.Date+5*60 || scheduleDate > req.Date+31*24*60*60) { + return domain.ToggleSuggestedPostApprovalResult{}, domain.ErrSuggestedPostInvalid + } + recipients, err := monoforumManagerRecipientsTx(ctx, tx, parent.ID, original.SavedPeer.ID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result := domain.ToggleSuggestedPostApprovalResult{Monoforum: mono, Parent: parent, SavedPeer: original.SavedPeer, Recipients: recipients} + + if req.Reject { + original.SuggestedPost.Accepted, original.SuggestedPost.Rejected = false, true + original, result.OriginalEvent, err = s.persistSuggestedPostEditTx(ctx, tx, original, req.UserID, req.Date) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result.OriginalMessage = original + result.ServiceMessage, result.ServiceEvent, err = s.insertSuggestedPostServiceTx(ctx, tx, mono, parent, req.UserID, fromSubscriber && manager.CanManageDirectMessages(), original.SavedPeer, original.ID, req.Date, domain.ChannelMessageAction{ + Type: domain.ChannelActionSuggestedPostApproval, SuggestedPostRejected: true, + SuggestedPostRejectComment: strings.TrimSpace(req.RejectComment), SuggestedPostPrice: price, + }) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result.State = domain.SuggestedPostStateRejected + if err := upsertSuggestedPostApprovalTx(ctx, tx, persistedSuggestedPostApproval{monoforumID: mono.ID, messageID: original.ID, parentID: parent.ID, actorID: req.UserID, payerID: original.SavedPeer.ID, state: result.State, price: price, scheduleDate: scheduleDate, approvalServiceID: result.ServiceMessage.ID}, req.Date); err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + } else { + stars, ton, enough, err := reserveSuggestedPostPaymentTx(ctx, tx, original.SavedPeer.ID, parent.ID, price, req.Date) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result.PayerStarsBalance, result.PayerTONBalance = stars, ton + if !enough { + if found { + result, err = s.loadSuggestedPostResultTx(ctx, tx, existing, true) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result.PayerStarsBalance, result.PayerTONBalance = stars, ton + } else { + result.ServiceMessage, result.ServiceEvent, err = s.insertSuggestedPostServiceTx(ctx, tx, mono, parent, req.UserID, fromSubscriber && manager.CanManageDirectMessages(), original.SavedPeer, original.ID, req.Date, domain.ChannelMessageAction{ + Type: domain.ChannelActionSuggestedPostApproval, SuggestedPostBalanceTooLow: true, + SuggestedPostScheduleDate: scheduleDate, SuggestedPostPrice: price, + }) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result.State = domain.SuggestedPostStateBalanceLow + if err := upsertSuggestedPostApprovalTx(ctx, tx, persistedSuggestedPostApproval{monoforumID: mono.ID, messageID: original.ID, parentID: parent.ID, actorID: req.UserID, payerID: original.SavedPeer.ID, state: result.State, price: price, scheduleDate: scheduleDate, approvalServiceID: result.ServiceMessage.ID}, req.Date); err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + } + } else { + effectivePublishDate := scheduleDate + if effectivePublishDate == 0 { + // TDesktop's "Publish Now" request has no schedule_date flag, but + // its approval service renderer always expects an absolute date. + effectivePublishDate = req.Date + } + original.SuggestedPost.Accepted, original.SuggestedPost.Rejected, original.SuggestedPost.ScheduleDate = true, false, effectivePublishDate + original, result.OriginalEvent, err = s.persistSuggestedPostEditTx(ctx, tx, original, req.UserID, req.Date) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result.OriginalMessage = original + result.ServiceMessage, result.ServiceEvent, err = s.insertSuggestedPostServiceTx(ctx, tx, mono, parent, req.UserID, fromSubscriber && manager.CanManageDirectMessages(), original.SavedPeer, original.ID, req.Date, domain.ChannelMessageAction{Type: domain.ChannelActionSuggestedPostApproval, SuggestedPostScheduleDate: effectivePublishDate, SuggestedPostPrice: price}) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + record := persistedSuggestedPostApproval{monoforumID: mono.ID, messageID: original.ID, parentID: parent.ID, actorID: req.UserID, payerID: original.SavedPeer.ID, state: domain.SuggestedPostStateScheduled, price: price, scheduleDate: effectivePublishDate, approvalServiceID: result.ServiceMessage.ID} + if effectivePublishDate <= req.Date { + published, publishErr := s.publishSuggestedPostTx(ctx, tx, parent, original, req.UserID, req.Date) + if publishErr != nil { + return domain.ToggleSuggestedPostApprovalResult{}, publishErr + } + result.Published = &published + record.publishedMessageID = published.Message.ID + if price == nil { + record.state = domain.SuggestedPostStateCompleted + } else { + record.state = domain.SuggestedPostStatePublished + record.settlementDue = req.Date + suggestedPostSettlementAge + } + } + result.State = record.state + if err := upsertSuggestedPostApprovalTx(ctx, tx, record, req.Date); err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + } + } + if err := tx.Commit(ctx); err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, fmt.Errorf("commit toggle suggested post: %w", err) + } + committed = true + result.Monoforum, err = getChannelByID(ctx, s.db, mono.ID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, fmt.Errorf("reload suggested post monoforum after commit: %w", err) + } + result.Parent, err = getChannelByID(ctx, s.db, parent.ID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, fmt.Errorf("reload suggested post parent after commit: %w", err) + } + return result, nil +} + +func (s *ChannelStore) persistSuggestedPostEditTx(ctx context.Context, tx pgx.Tx, msg domain.ChannelMessage, actor int64, date int) (domain.ChannelMessage, domain.ChannelUpdateEvent, error) { + pts, err := s.reserveChannelPts(ctx, tx, msg.ChannelID) + if err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + encoded, err := marshalJSON(msg.SuggestedPost, "{}") + if err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + msg.Pts = pts + if _, err := tx.Exec(ctx, `UPDATE channel_messages SET suggested_post=$3, pts=$4, updated_at=now() WHERE channel_id=$1 AND id=$2 AND NOT deleted`, msg.ChannelID, msg.ID, encoded, pts); err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, fmt.Errorf("update suggested post message: %w", err) + } + event := domain.ChannelUpdateEvent{ChannelID: msg.ChannelID, Type: domain.ChannelUpdateEditMessage, Pts: pts, PtsCount: 1, Date: date, Message: msg, SenderUserID: actor} + if err := insertChannelEventTx(ctx, tx, event); err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + return msg, event, nil +} + +func (s *ChannelStore) insertSuggestedPostServiceTx(ctx context.Context, tx pgx.Tx, mono, parent domain.Channel, actor int64, fromChannel bool, saved domain.Peer, replyID, date int, action domain.ChannelMessageAction) (domain.ChannelMessage, domain.ChannelUpdateEvent, error) { + msgID, err := s.msgIDs.NextChannelMessageID(ctx, mono.ID) + if err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + pts, err := s.reserveChannelPts(ctx, tx, mono.ID) + if err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + from := domain.Peer{Type: domain.PeerTypeUser, ID: actor} + if fromChannel { + from = domain.Peer{Type: domain.PeerTypeChannel, ID: parent.ID} + } + msg := domain.ChannelMessage{ChannelID: mono.ID, ID: msgID, SenderUserID: actor, From: from, SavedPeer: saved, Date: date, ReplyTo: &domain.MessageReply{Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: mono.ID}, MessageID: replyID}, Action: &action, Pts: pts} + event := domain.ChannelUpdateEvent{ChannelID: mono.ID, Type: domain.ChannelUpdateNewMessage, Pts: pts, PtsCount: 1, Date: date, Message: msg, SenderUserID: actor} + if err := insertChannelMessageTx(ctx, tx, msg); err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + if err := insertChannelEventTx(ctx, tx, event); err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + if _, err := tx.Exec(ctx, `UPDATE channels SET top_message_id=$2, pts=$3, updated_at=now() WHERE id=$1`, mono.ID, msgID, pts); err != nil { + return domain.ChannelMessage{}, domain.ChannelUpdateEvent{}, err + } + return msg, event, nil +} + +func (s *ChannelStore) publishSuggestedPostTx(ctx context.Context, tx pgx.Tx, parent domain.Channel, original domain.ChannelMessage, actor int64, date int) (domain.SendChannelMessageResult, error) { + msgID, err := s.msgIDs.NextChannelMessageID(ctx, parent.ID) + if err != nil { + return domain.SendChannelMessageResult{}, err + } + pts, err := s.reserveChannelPts(ctx, tx, parent.ID) + if err != nil { + return domain.SendChannelMessageResult{}, err + } + msg := original + msg.ChannelID, msg.ID, msg.RandomID, msg.SenderUserID = parent.ID, msgID, 0, actor + msg.From, msg.SendAs, msg.SavedPeer, msg.Date, msg.EditDate, msg.Post = domain.Peer{Type: domain.PeerTypeChannel, ID: parent.ID}, nil, domain.Peer{}, date, 0, true + msg.ReplyTo, msg.PaidMessageStars, msg.Pts, msg.Deleted = nil, 0, pts, false + event := domain.ChannelUpdateEvent{ChannelID: parent.ID, Type: domain.ChannelUpdateNewMessage, Pts: pts, PtsCount: 1, Date: date, Message: msg, SenderUserID: actor} + if err := insertChannelMessageTx(ctx, tx, msg); err != nil { + return domain.SendChannelMessageResult{}, err + } + if err := insertChannelEventTx(ctx, tx, event); err != nil { + return domain.SendChannelMessageResult{}, err + } + if _, err := tx.Exec(ctx, `UPDATE channels SET top_message_id=$2, pts=$3, updated_at=now() WHERE id=$1`, parent.ID, msgID, pts); err != nil { + return domain.SendChannelMessageResult{}, err + } + recipients, err := s.listActiveChannelMemberIDs(ctx, tx, parent.ID, 0) + if err != nil { + return domain.SendChannelMessageResult{}, err + } + parent.TopMessageID, parent.Pts = msgID, pts + return domain.SendChannelMessageResult{Channel: parent, Message: msg, Event: event, Recipients: recipients}, nil +} + +func reserveSuggestedPostPaymentTx(ctx context.Context, tx pgx.Tx, payerID, parentID int64, price *domain.SuggestedPostPrice, date int) (*domain.StarsBalance, *int64, bool, error) { + if price == nil { + return nil, nil, true, nil + } + switch price.Kind { + case domain.SuggestedPostPriceStars: + if price.Nanos != 0 || price.Amount <= 0 { + return nil, nil, false, domain.ErrSuggestedPostInvalid + } + balance := domain.StarsBalance{UserID: payerID} + err := tx.QueryRow(ctx, `SELECT balance,granted FROM stars_balances WHERE user_id=$1 FOR UPDATE`, payerID).Scan(&balance.Balance, &balance.Granted) + if errors.Is(err, pgx.ErrNoRows) { + return &balance, nil, false, nil + } + if err != nil { + return nil, nil, false, err + } + if balance.Balance < price.Amount { + return &balance, nil, false, nil + } + if err := tx.QueryRow(ctx, `UPDATE stars_balances SET balance=balance-$2,updated_at=now() WHERE user_id=$1 RETURNING balance`, payerID, price.Amount).Scan(&balance.Balance); err != nil { + return nil, nil, false, err + } + if err := insertStarsTxn(ctx, tx, payerID, -price.Amount, domain.StarsReasonSuggestedPost, domain.Peer{Type: domain.PeerTypeChannel, ID: parentID}, date, "Suggested post escrow", ""); err != nil { + return nil, nil, false, err + } + return &balance, nil, true, nil + case domain.SuggestedPostPriceTON: + var balance int64 + err := tx.QueryRow(ctx, `SELECT balance_nanoton FROM ton_balances WHERE user_id=$1 FOR UPDATE`, payerID).Scan(&balance) + if errors.Is(err, pgx.ErrNoRows) { + return nil, &balance, false, nil + } + if err != nil { + return nil, nil, false, err + } + if balance < price.Amount { + return nil, &balance, false, nil + } + if err := tx.QueryRow(ctx, `UPDATE ton_balances SET balance_nanoton=balance_nanoton-$2,updated_at=now() WHERE user_id=$1 RETURNING balance_nanoton`, payerID, price.Amount).Scan(&balance); err != nil { + return nil, nil, false, err + } + if _, err := tx.Exec(ctx, `INSERT INTO ton_transactions(user_id,amount_nanoton,reason,peer_type,peer_id,date) VALUES($1,$2,$3,'channel',$4,$5)`, payerID, -price.Amount, string(domain.StarsReasonSuggestedPost), parentID, date); err != nil { + return nil, nil, false, err + } + return nil, &balance, true, nil + default: + return nil, nil, false, domain.ErrSuggestedPostInvalid + } +} + +func monoforumManagerRecipientsTx(ctx context.Context, tx pgx.Tx, parentID, subscriberID int64) ([]int64, error) { + rows, err := tx.Query(ctx, `SELECT user_id FROM channel_members WHERE channel_id=$1 AND status='active' AND (role='creator' OR (role='admin' AND COALESCE((admin_rights->>'ManageDirectMessages')::boolean,false))) ORDER BY user_id`, parentID) + if err != nil { + return nil, err + } + defer rows.Close() + ids := []int64{subscriberID} + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + ids = append(ids, id) + } + return uniqueChannelUserIDs(ids, 0), rows.Err() +} + +func upsertSuggestedPostApprovalTx(ctx context.Context, tx pgx.Tx, row persistedSuggestedPostApproval, date int) error { + kind, amount, nanos := "", int64(0), 0 + if row.price != nil { + kind, amount, nanos = string(row.price.Kind), row.price.Amount, row.price.Nanos + } + _, err := tx.Exec(ctx, `INSERT INTO suggested_post_approvals(monoforum_id,suggestion_message_id,parent_channel_id,actor_user_id,payer_user_id,state,price_kind,price_amount,price_nanos,schedule_date,approval_service_message_id,published_message_id,settlement_due,final_service_message_id,created_at,updated_at) +VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$15) +ON CONFLICT(monoforum_id,suggestion_message_id) DO UPDATE SET actor_user_id=EXCLUDED.actor_user_id,state=EXCLUDED.state,price_kind=EXCLUDED.price_kind,price_amount=EXCLUDED.price_amount,price_nanos=EXCLUDED.price_nanos,schedule_date=EXCLUDED.schedule_date,approval_service_message_id=EXCLUDED.approval_service_message_id,published_message_id=EXCLUDED.published_message_id,settlement_due=EXCLUDED.settlement_due,final_service_message_id=EXCLUDED.final_service_message_id,updated_at=EXCLUDED.updated_at`, + row.monoforumID, row.messageID, row.parentID, row.actorID, row.payerID, string(row.state), kind, amount, nanos, row.scheduleDate, row.approvalServiceID, row.publishedMessageID, row.settlementDue, row.finalServiceID, date) + return err +} + +func loadSuggestedPostApprovalTx(ctx context.Context, tx pgx.Tx, monoID int64, messageID int, lock bool) (persistedSuggestedPostApproval, bool, error) { + q := `SELECT parent_channel_id,actor_user_id,payer_user_id,state,price_kind,price_amount,price_nanos,schedule_date,approval_service_message_id,published_message_id,settlement_due,final_service_message_id FROM suggested_post_approvals WHERE monoforum_id=$1 AND suggestion_message_id=$2` + if lock { + q += ` FOR UPDATE` + } + var row persistedSuggestedPostApproval + row.monoforumID, row.messageID = monoID, messageID + var state, kind string + var amount int64 + var nanos int + err := tx.QueryRow(ctx, q, monoID, messageID).Scan(&row.parentID, &row.actorID, &row.payerID, &state, &kind, &amount, &nanos, &row.scheduleDate, &row.approvalServiceID, &row.publishedMessageID, &row.settlementDue, &row.finalServiceID) + if errors.Is(err, pgx.ErrNoRows) { + return row, false, nil + } + if err != nil { + return row, false, err + } + row.state = domain.SuggestedPostLifecycleState(state) + if kind != "" { + row.price = &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceKind(kind), Amount: amount, Nanos: nanos} + } + return row, true, nil +} + +func (s *ChannelStore) loadSuggestedPostResultTx(ctx context.Context, tx pgx.Tx, row persistedSuggestedPostApproval, duplicate bool) (domain.ToggleSuggestedPostApprovalResult, error) { + mono, err := getChannelByID(ctx, tx, row.monoforumID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + parent, err := getChannelByID(ctx, tx, row.parentID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + original, err := s.getChannelMessage(ctx, tx, row.monoforumID, row.messageID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + recipients, err := monoforumManagerRecipientsTx(ctx, tx, row.parentID, row.payerID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, err + } + result := domain.ToggleSuggestedPostApprovalResult{Monoforum: mono, Parent: parent, SavedPeer: domain.Peer{Type: domain.PeerTypeUser, ID: row.payerID}, State: row.state, OriginalMessage: original, Recipients: recipients, Duplicate: duplicate} + if original.SuggestedPost != nil && (original.SuggestedPost.Accepted || original.SuggestedPost.Rejected) && original.Pts > 0 { + eventDate, senderUserID := original.Date, row.actorID + // The event row is the exact durable replay source. Retention may have + // pruned an old event, in which case the message snapshot still provides + // a safe replay with its original date and lifecycle actor. + if err := tx.QueryRow(ctx, `SELECT date,sender_user_id FROM channel_update_events WHERE channel_id=$1 AND pts=$2 AND event_type='edit_channel_message'`, row.monoforumID, original.Pts).Scan(&eventDate, &senderUserID); err != nil && !errors.Is(err, pgx.ErrNoRows) { + return domain.ToggleSuggestedPostApprovalResult{}, fmt.Errorf("load suggested post edit event: %w", err) + } + result.OriginalEvent = domain.ChannelUpdateEvent{ChannelID: row.monoforumID, Type: domain.ChannelUpdateEditMessage, Pts: original.Pts, PtsCount: 1, Date: eventDate, Message: original, SenderUserID: senderUserID} + } + if row.approvalServiceID > 0 { + result.ServiceMessage, err = s.getChannelMessage(ctx, tx, row.monoforumID, row.approvalServiceID) + if err != nil { + return result, err + } + result.ServiceEvent = domain.ChannelUpdateEvent{ChannelID: row.monoforumID, Type: domain.ChannelUpdateNewMessage, Pts: result.ServiceMessage.Pts, PtsCount: 1, Date: result.ServiceMessage.Date, Message: result.ServiceMessage, SenderUserID: row.actorID} + } + if row.publishedMessageID > 0 { + msg, e := s.getChannelMessage(ctx, tx, row.parentID, row.publishedMessageID) + if e != nil { + return result, e + } + event := domain.ChannelUpdateEvent{ChannelID: row.parentID, Type: domain.ChannelUpdateNewMessage, Pts: msg.Pts, PtsCount: 1, Date: msg.Date, Message: msg, SenderUserID: row.actorID} + result.Published = &domain.SendChannelMessageResult{Channel: parent, Message: msg, Event: event} + } + return result, nil +} + +func cloneSuggestedPricePG(in *domain.SuggestedPostPrice) *domain.SuggestedPostPrice { + if in == nil { + return nil + } + out := *in + return &out +} + +func (s *ChannelStore) ProcessSuggestedPostLifecycle(ctx context.Context, req domain.SuggestedPostLifecycleRequest) ([]domain.ToggleSuggestedPostApprovalResult, error) { + if req.Now == 0 { + req.Now = nowUnix() + } + if req.Limit <= 0 || req.Limit > 100 { + req.Limit = 100 + } + rows, err := s.db.Query(ctx, ` +SELECT monoforum_id,suggestion_message_id +FROM suggested_post_approvals a +WHERE (a.state='scheduled' AND a.schedule_date <= $1) + OR (a.state='scheduled' AND EXISTS ( + SELECT 1 FROM channel_messages sm + WHERE sm.channel_id=a.monoforum_id AND sm.id=a.suggestion_message_id AND sm.deleted)) + OR (a.state='published' AND (a.settlement_due <= $1 OR EXISTS ( + SELECT 1 FROM channel_messages m + WHERE m.channel_id=a.parent_channel_id AND m.id=a.published_message_id AND m.deleted))) +ORDER BY CASE WHEN a.state='scheduled' THEN a.schedule_date ELSE a.settlement_due END, + a.monoforum_id,a.suggestion_message_id +LIMIT $2`, req.Now, req.Limit) + if err != nil { + return nil, fmt.Errorf("list due suggested posts: %w", err) + } + type key struct { + mono int64 + message int + } + keys := make([]key, 0, req.Limit) + for rows.Next() { + var k key + if err := rows.Scan(&k.mono, &k.message); err != nil { + rows.Close() + return nil, err + } + keys = append(keys, k) + } + if err := rows.Err(); err != nil { + rows.Close() + return nil, err + } + rows.Close() + out := make([]domain.ToggleSuggestedPostApprovalResult, 0, len(keys)) + for _, k := range keys { + result, changed, err := s.processSuggestedPostLifecycleOne(ctx, k.mono, k.message, req.Now) + if err != nil { + return out, err + } + if changed { + out = append(out, result) + } + } + return out, nil +} + +func (s *ChannelStore) processSuggestedPostLifecycleOne(ctx context.Context, monoID int64, messageID, now int) (domain.ToggleSuggestedPostApprovalResult, bool, error) { + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.ToggleSuggestedPostApprovalResult{}, false, fmt.Errorf("suggested post lifecycle: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, false, err + } + committed := false + defer func() { + if !committed { + _ = tx.Rollback(ctx) + } + }() + row, found, err := loadSuggestedPostApprovalTx(ctx, tx, monoID, messageID, true) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, false, err + } + if !found { + return domain.ToggleSuggestedPostApprovalResult{}, false, fmt.Errorf("suggested post lifecycle invariant: approval row disappeared for monoforum %d message %d", monoID, messageID) + } + if row.state != domain.SuggestedPostStateScheduled && row.state != domain.SuggestedPostStatePublished { + if err := tx.Commit(ctx); err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, false, err + } + committed = true + return domain.ToggleSuggestedPostApprovalResult{}, false, nil + } + mono, err := getChannelByID(ctx, tx, row.monoforumID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, false, err + } + parent, err := getChannelByID(ctx, tx, row.parentID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, false, err + } + original, err := s.getChannelMessage(ctx, tx, row.monoforumID, row.messageID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, false, err + } + recipients, err := monoforumManagerRecipientsTx(ctx, tx, parent.ID, row.payerID) + if err != nil { + return domain.ToggleSuggestedPostApprovalResult{}, false, err + } + result := domain.ToggleSuggestedPostApprovalResult{Monoforum: mono, Parent: parent, SavedPeer: domain.Peer{Type: domain.PeerTypeUser, ID: row.payerID}, State: row.state, Recipients: recipients} + changed := false + if row.state == domain.SuggestedPostStateScheduled && original.Deleted { + if row.price != nil { + stars, ton, err := refundSuggestedPostPaymentTx(ctx, tx, row.payerID, row.parentID, row.price, now) + if err != nil { + return result, false, err + } + result.PayerStarsBalance, result.PayerTONBalance = stars, ton + service, event, err := s.insertSuggestedPostServiceTx(ctx, tx, mono, parent, row.actorID, true, row.savedPeer(), row.messageID, now, domain.ChannelMessageAction{Type: domain.ChannelActionSuggestedPostRefund}) + if err != nil { + return result, false, err + } + result.ServiceMessage, result.ServiceEvent, row.finalServiceID = service, event, service.ID + } + row.state, result.State, changed = domain.SuggestedPostStateRefunded, domain.SuggestedPostStateRefunded, true + } + if row.state == domain.SuggestedPostStateScheduled && row.scheduleDate <= now { + published, err := s.publishSuggestedPostTx(ctx, tx, parent, original, row.actorID, now) + if err != nil { + return result, false, err + } + result.Published = &published + row.publishedMessageID = published.Message.ID + if row.price == nil { + row.state = domain.SuggestedPostStateCompleted + } else { + row.state = domain.SuggestedPostStatePublished + row.settlementDue = now + suggestedPostSettlementAge + } + result.State = row.state + changed = true + } + if row.state == domain.SuggestedPostStatePublished { + var deleted bool + var deleteDate int + if err := tx.QueryRow(ctx, `SELECT deleted,delete_date FROM channel_messages WHERE channel_id=$1 AND id=$2 FOR SHARE`, row.parentID, row.publishedMessageID).Scan(&deleted, &deleteDate); err != nil { + return result, false, err + } + if deleted && (deleteDate == 0 || deleteDate < row.settlementDue) { + stars, ton, err := refundSuggestedPostPaymentTx(ctx, tx, row.payerID, row.parentID, row.price, now) + if err != nil { + return result, false, err + } + result.PayerStarsBalance, result.PayerTONBalance = stars, ton + service, event, err := s.insertSuggestedPostServiceTx(ctx, tx, mono, parent, row.actorID, true, row.savedPeer(), row.messageID, now, domain.ChannelMessageAction{Type: domain.ChannelActionSuggestedPostRefund}) + if err != nil { + return result, false, err + } + result.ServiceMessage, result.ServiceEvent = service, event + row.state, row.finalServiceID, result.State = domain.SuggestedPostStateRefunded, service.ID, domain.SuggestedPostStateRefunded + changed = true + } else if row.settlementDue <= now { + if err := settleSuggestedPostPaymentTx(ctx, tx, row.actorID, row.payerID, row.parentID, row.price, now); err != nil { + return result, false, err + } + service, event, err := s.insertSuggestedPostServiceTx(ctx, tx, mono, parent, row.actorID, true, row.savedPeer(), row.messageID, now, domain.ChannelMessageAction{Type: domain.ChannelActionSuggestedPostSuccess, SuggestedPostPrice: cloneSuggestedPricePG(row.price)}) + if err != nil { + return result, false, err + } + result.ServiceMessage, result.ServiceEvent = service, event + row.state, row.finalServiceID, result.State = domain.SuggestedPostStateCompleted, service.ID, domain.SuggestedPostStateCompleted + changed = true + } + } + if !changed { + if err := tx.Commit(ctx); err != nil { + return result, false, err + } + committed = true + return result, false, nil + } + if err := upsertSuggestedPostApprovalTx(ctx, tx, row, now); err != nil { + return result, false, err + } + if err := tx.Commit(ctx); err != nil { + return result, false, err + } + committed = true + result.Monoforum, err = getChannelByID(ctx, s.db, row.monoforumID) + if err != nil { + return result, true, fmt.Errorf("reload lifecycle monoforum after commit: %w", err) + } + result.Parent, err = getChannelByID(ctx, s.db, row.parentID) + if err != nil { + return result, true, fmt.Errorf("reload lifecycle parent after commit: %w", err) + } + return result, true, nil +} + +func (r persistedSuggestedPostApproval) savedPeer() domain.Peer { + return domain.Peer{Type: domain.PeerTypeUser, ID: r.payerID} +} + +func refundSuggestedPostPaymentTx(ctx context.Context, tx pgx.Tx, payerID, parentID int64, price *domain.SuggestedPostPrice, date int) (*domain.StarsBalance, *int64, error) { + if price == nil { + return nil, nil, nil + } + switch price.Kind { + case domain.SuggestedPostPriceStars: + balance := domain.StarsBalance{UserID: payerID, Granted: true} + if err := tx.QueryRow(ctx, `INSERT INTO stars_balances(user_id,balance,granted) VALUES($1,$2,true) ON CONFLICT(user_id) DO UPDATE SET balance=stars_balances.balance+EXCLUDED.balance,updated_at=now() RETURNING balance,granted`, payerID, price.Amount).Scan(&balance.Balance, &balance.Granted); err != nil { + return nil, nil, err + } + if err := insertStarsTxn(ctx, tx, payerID, price.Amount, domain.StarsReasonSuggestedPost, domain.Peer{Type: domain.PeerTypeChannel, ID: parentID}, date, "Suggested post refund", ""); err != nil { + return nil, nil, err + } + return &balance, nil, nil + case domain.SuggestedPostPriceTON: + var balance int64 + if err := tx.QueryRow(ctx, `INSERT INTO ton_balances(user_id,balance_nanoton,granted) VALUES($1,$2,true) ON CONFLICT(user_id) DO UPDATE SET balance_nanoton=ton_balances.balance_nanoton+EXCLUDED.balance_nanoton,updated_at=now() RETURNING balance_nanoton`, payerID, price.Amount).Scan(&balance); err != nil { + return nil, nil, err + } + if _, err := tx.Exec(ctx, `INSERT INTO ton_transactions(user_id,amount_nanoton,reason,peer_type,peer_id,date) VALUES($1,$2,$3,'channel',$4,$5)`, payerID, price.Amount, string(domain.StarsReasonSuggestedPost), parentID, date); err != nil { + return nil, nil, err + } + return nil, &balance, nil + default: + return nil, nil, domain.ErrSuggestedPostInvalid + } +} + +func settleSuggestedPostPaymentTx(ctx context.Context, tx pgx.Tx, actorID, payerID, parentID int64, price *domain.SuggestedPostPrice, date int) error { + if price == nil { + return nil + } + credit := price.Amount * paidMessageChannelCommissionPermille / 1000 + if credit <= 0 { + return nil + } + switch price.Kind { + case domain.SuggestedPostPriceStars: + if _, err := tx.Exec(ctx, `INSERT INTO channel_stars_balances(channel_id,balance) VALUES($1,$2) ON CONFLICT(channel_id) DO UPDATE SET balance=channel_stars_balances.balance+EXCLUDED.balance,updated_at=now()`, parentID, credit); err != nil { + return err + } + _, err := tx.Exec(ctx, `INSERT INTO channel_stars_transactions(channel_id,actor_user_id,amount,reason,peer_type,peer_id,date) VALUES($1,$2,$3,$4,'user',$5,$6)`, parentID, actorID, credit, string(domain.StarsReasonSuggestedPost), payerID, date) + return err + case domain.SuggestedPostPriceTON: + if _, err := tx.Exec(ctx, `INSERT INTO channel_ton_balances(channel_id,balance_nanoton) VALUES($1,$2) ON CONFLICT(channel_id) DO UPDATE SET balance_nanoton=channel_ton_balances.balance_nanoton+EXCLUDED.balance_nanoton,updated_at=now()`, parentID, credit); err != nil { + return err + } + _, err := tx.Exec(ctx, `INSERT INTO channel_ton_transactions(channel_id,actor_user_id,amount_nanoton,reason,peer_type,peer_id,date) VALUES($1,$2,$3,$4,'user',$5,$6)`, parentID, actorID, credit, string(domain.StarsReasonSuggestedPost), payerID, date) + return err + default: + return domain.ErrSuggestedPostInvalid + } +} diff --git a/internal/store/postgres/channel_suggested_post_integration_test.go b/internal/store/postgres/channel_suggested_post_integration_test.go new file mode 100644 index 00000000..4db4f512 --- /dev/null +++ b/internal/store/postgres/channel_suggested_post_integration_test.go @@ -0,0 +1,135 @@ +package postgres + +import ( + "context" + "testing" + + "telesrv/internal/domain" +) + +// TestSuggestedPostLifecyclePostgres verifies that message state, channel pts, +// escrow and refund are committed through the real PostgreSQL transaction. +// It is gated by TELESRV_TEST_POSTGRES_DSN and testPool migrates through 0134. +func TestSuggestedPostLifecyclePostgres(t *testing.T) { + pool := testPool(t) + ctx := context.Background() + suffix := randomSuffix(t) + users := NewUserStore(pool) + owner, err := users.Create(ctx, domain.User{AccessHash: 201, Phone: "+1888" + suffix + "01", FirstName: "SuggestOwner"}) + if err != nil { + t.Fatal(err) + } + subscriber, err := users.Create(ctx, domain.User{AccessHash: 202, Phone: "+1888" + suffix + "02", FirstName: "SuggestSubscriber"}) + if err != nil { + t.Fatal(err) + } + channels := NewChannelStore(pool) + created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{CreatorUserID: owner.ID, Title: "Suggested " + suffix, Broadcast: true, Date: 1_700_000_000}) + if err != nil { + t.Fatal(err) + } + enabled, err := channels.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 0, true) + if err != nil { + t.Fatal(err) + } + monoID := enabled.Channel.LinkedMonoforumID + t.Cleanup(func() { + _, _ = pool.Exec(ctx, `DELETE FROM suggested_post_approvals WHERE monoforum_id=$1`, monoID) + _, _ = pool.Exec(ctx, `DELETE FROM channel_stars_balances WHERE channel_id=$1`, created.Channel.ID) + _, _ = pool.Exec(ctx, `DELETE FROM channels WHERE id=ANY($1::bigint[])`, []int64{monoID, created.Channel.ID}) + _, _ = pool.Exec(ctx, `DELETE FROM users WHERE id=ANY($1::bigint[])`, []int64{owner.ID, subscriber.ID}) + }) + if _, err := pool.Exec(ctx, `INSERT INTO stars_balances(user_id,balance,granted) VALUES($1,100,true) ON CONFLICT(user_id) DO UPDATE SET balance=100,granted=true`, subscriber.ID); err != nil { + t.Fatal(err) + } + saved := domain.Peer{Type: domain.PeerTypeUser, ID: subscriber.ID} + suggestion, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: subscriber.ID, SavedPeer: saved, RandomID: 71, Message: "postgres suggestion", SuggestedPost: &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}, Date: 1_700_000_100}) + if err != nil { + t.Fatal(err) + } + approved, err := channels.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: owner.ID, MonoforumID: monoID, MessageID: suggestion.Message.ID, Date: 1_700_000_200}) + if err != nil { + t.Fatal(err) + } + if approved.State != domain.SuggestedPostStatePublished || approved.Published == nil || approved.PayerStarsBalance == nil || approved.PayerStarsBalance.Balance != 90 { + t.Fatalf("approved=%+v", approved) + } + if approved.OriginalMessage.SuggestedPost.ScheduleDate != 1_700_000_200 || approved.ServiceMessage.Action.SuggestedPostScheduleDate != 1_700_000_200 { + t.Fatalf("immediate approval dates original/action=%d/%d, want commit date", approved.OriginalMessage.SuggestedPost.ScheduleDate, approved.ServiceMessage.Action.SuggestedPostScheduleDate) + } + history, err := channels.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{MonoforumID: monoID, SavedPeer: saved, Limit: 10}) + if err != nil { + t.Fatal(err) + } + var persistedApprovalDate int + for _, message := range history.Messages { + if message.ID == approved.ServiceMessage.ID && message.Action != nil { + persistedApprovalDate = message.Action.SuggestedPostScheduleDate + break + } + } + if persistedApprovalDate != 1_700_000_200 { + t.Fatalf("persisted approval history date=%d, want commit date", persistedApprovalDate) + } + replay, err := channels.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: owner.ID, MonoforumID: monoID, MessageID: suggestion.Message.ID, Date: 1_700_000_201}) + if err != nil || !replay.Duplicate || replay.OriginalEvent.Type != domain.ChannelUpdateEditMessage || replay.ServiceEvent.Type != domain.ChannelUpdateNewMessage || replay.Published == nil { + t.Fatalf("approval replay=%+v err=%v", replay, err) + } + var state string + var scheduleDate int + var debit, channelBalance int64 + if err := pool.QueryRow(ctx, `SELECT state,schedule_date FROM suggested_post_approvals WHERE monoforum_id=$1 AND suggestion_message_id=$2`, monoID, suggestion.Message.ID).Scan(&state, &scheduleDate); err != nil { + t.Fatal(err) + } + if err := pool.QueryRow(ctx, `SELECT balance FROM stars_balances WHERE user_id=$1`, subscriber.ID).Scan(&debit); err != nil { + t.Fatal(err) + } + _ = pool.QueryRow(ctx, `SELECT COALESCE((SELECT balance FROM channel_stars_balances WHERE channel_id=$1),0)`, created.Channel.ID).Scan(&channelBalance) + if state != string(domain.SuggestedPostStatePublished) || scheduleDate != 1_700_000_200 || debit != 90 || channelBalance != 0 { + t.Fatalf("state/schedule/debit/channel=%s/%d/%d/%d", state, scheduleDate, debit, channelBalance) + } + if _, err := pool.Exec(ctx, `UPDATE channel_messages SET deleted=true WHERE channel_id=$1 AND id=$2`, created.Channel.ID, approved.Published.Message.ID); err != nil { + t.Fatal(err) + } + resolved, err := channels.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: 1_700_000_300, Limit: 10}) + if err != nil || len(resolved) != 1 || resolved[0].State != domain.SuggestedPostStateRefunded || resolved[0].ServiceMessage.Action == nil || resolved[0].ServiceMessage.Action.Type != domain.ChannelActionSuggestedPostRefund { + t.Fatalf("refund=%+v err=%v", resolved, err) + } + if err := pool.QueryRow(ctx, `SELECT balance FROM stars_balances WHERE user_id=$1`, subscriber.ID).Scan(&debit); err != nil { + t.Fatal(err) + } + var txnNet int64 + if err := pool.QueryRow(ctx, `SELECT COALESCE(sum(amount),0) FROM stars_transactions WHERE user_id=$1 AND reason=$2`, subscriber.ID, string(domain.StarsReasonSuggestedPost)).Scan(&txnNet); err != nil { + t.Fatal(err) + } + if debit != 100 || txnNet != 0 { + t.Fatalf("refund balance/net=%d/%d, want 100/0", debit, txnNet) + } + + late, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: subscriber.ID, SavedPeer: saved, RandomID: 72, Message: "late deletion", SuggestedPost: &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}}, Date: 1_700_000_400}) + if err != nil { + t.Fatal(err) + } + approvedAt := 1_700_000_500 + lateApproved, err := channels.ToggleSuggestedPostApproval(ctx, domain.ToggleSuggestedPostApprovalRequest{UserID: owner.ID, MonoforumID: monoID, MessageID: late.Message.ID, Date: approvedAt}) + if err != nil || lateApproved.Published == nil { + t.Fatalf("late approval=%+v err=%v", lateApproved, err) + } + due := approvedAt + suggestedPostSettlementAge + if _, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{UserID: owner.ID, ChannelID: created.Channel.ID, IDs: []int{lateApproved.Published.Message.ID}, Date: due + 1}); err != nil { + t.Fatal(err) + } + resolved, err = channels.ProcessSuggestedPostLifecycle(ctx, domain.SuggestedPostLifecycleRequest{Now: due + 2, Limit: 10}) + if err != nil || len(resolved) != 1 || resolved[0].State != domain.SuggestedPostStateCompleted || resolved[0].ServiceMessage.Action == nil || resolved[0].ServiceMessage.Action.Type != domain.ChannelActionSuggestedPostSuccess { + t.Fatalf("late settlement=%+v err=%v", resolved, err) + } + if err := pool.QueryRow(ctx, `SELECT balance FROM stars_balances WHERE user_id=$1`, subscriber.ID).Scan(&debit); err != nil { + t.Fatal(err) + } + if err := pool.QueryRow(ctx, `SELECT COALESCE((SELECT balance FROM channel_stars_balances WHERE channel_id=$1),0)`, created.Channel.ID).Scan(&channelBalance); err != nil { + t.Fatal(err) + } + if debit != 90 || channelBalance != 8 { + t.Fatalf("late settlement balance/channel=%d/%d, want 90/8", debit, channelBalance) + } +} diff --git a/internal/store/postgres/channel_updates.go b/internal/store/postgres/channel_updates.go index bf03c14c..24bef8ae 100644 --- a/internal/store/postgres/channel_updates.go +++ b/internal/store/postgres/channel_updates.go @@ -48,7 +48,7 @@ func (s *ChannelStore) ListChannelDifference(ctx context.Context, req domain.Cha args = append(args, member.AvailableMinID) where += fmt.Sprintf(" AND id > $%d", len(args)) } - if channel.Monoforum && !isChannelAdmin(member) { + if channel.Monoforum && !member.CanManageDirectMessages() { args = append(args, req.UserID) where += fmt.Sprintf(" AND saved_peer_type = 'user' AND saved_peer_id = $%d", len(args)) } @@ -147,7 +147,7 @@ LIMIT $3`, req.ChannelID, req.Pts, limit) } rows.Close() var visibleMonoforumMessageIDs map[int]struct{} - if channel.Monoforum && !isChannelAdmin(member) { + if channel.Monoforum && !member.CanManageDirectMessages() { messageIDs := make([]int, 0) for _, row := range eventRows { messageIDs = append(messageIDs, row.event.MessageIDs...) @@ -172,7 +172,7 @@ LIMIT $3`, req.ChannelID, req.Pts, limit) continue } event = visibleEvent - if channel.Monoforum && !isChannelAdmin(member) { + if channel.Monoforum && !member.CanManageDirectMessages() { event, ok = filterMonoforumEventForUser(event, req.UserID, visibleMonoforumMessageIDs) if !ok { continue diff --git a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go index eb0a5382..5000638f 100644 --- a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go @@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) { if err != nil { t.Fatalf("migrate star gift lifecycle schema: %v", err) } - if status.Dirty || status.Empty || status.Version != 132 { - t.Fatalf("migration status = %+v, want clean version 132", status) + if status.Dirty || status.Empty || status.Version != 134 { + t.Fatalf("migration status = %+v, want clean version 134", status) } } From 6e49d83fee0934688339666749665421359c3119 Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 16:09:27 +0800 Subject: [PATCH 17/28] fix: sync typed-nil dependency validation --- cmd/telesrv/main.go | 13 ++++++- cmd/telesrv/main_test.go | 21 ++++++++++++ internal/rpc/deps_validation_test.go | 51 ++++++++++++++++++++++++++++ internal/rpc/router.go | 27 +++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 cmd/telesrv/main_test.go create mode 100644 internal/rpc/deps_validation_test.go diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index a78c8ca0..ac50180e 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -854,7 +854,7 @@ func run(logger *zap.Logger) error { EphemeralPush: ephemeralStore, EphemeralReports: ephemeralReportStore, Users: usersService, - TelegramLogin: telegramLoginService, + TelegramLogin: telegramLoginRPCDependency(telegramLoginService), Updates: updatesService, BootstrapUpdates: bootstrapUpdateStore, BotAPIUpdates: botAPIUpdateStore, @@ -1047,6 +1047,17 @@ func run(logger *zap.Logger) error { return srv.ListenAndServe(ctx, cfg.ListenAddr) } +// telegramLoginRPCDependency preserves a disabled Telegram Login service as a +// nil interface. Assigning the nil *Service directly to rpc.Deps would create a +// non-nil interface with a nil concrete pointer and bypass Router availability +// checks. +func telegramLoginRPCDependency(service *telegramloginapp.Service) rpc.TelegramLoginService { + if service == nil { + return nil + } + return service +} + func runTelegramLoginRetention(ctx context.Context, service *telegramloginapp.Service, retention, interval time.Duration, batch int, logger *zap.Logger) { run := func() { var total int64 diff --git a/cmd/telesrv/main_test.go b/cmd/telesrv/main_test.go new file mode 100644 index 00000000..4850bcf9 --- /dev/null +++ b/cmd/telesrv/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" + + telegramloginapp "telesrv/internal/app/telegramlogin" +) + +func TestTelegramLoginRPCDependencyPreservesDisabledNil(t *testing.T) { + var disabled *telegramloginapp.Service + if dependency := telegramLoginRPCDependency(disabled); dependency != nil { + t.Fatalf("disabled Telegram Login dependency = %#v, want nil interface", dependency) + } +} + +func TestTelegramLoginRPCDependencyPreservesEnabledService(t *testing.T) { + enabled := new(telegramloginapp.Service) + if dependency := telegramLoginRPCDependency(enabled); dependency != enabled { + t.Fatalf("enabled Telegram Login dependency = %#v, want %p", dependency, enabled) + } +} diff --git a/internal/rpc/deps_validation_test.go b/internal/rpc/deps_validation_test.go new file mode 100644 index 00000000..42b51678 --- /dev/null +++ b/internal/rpc/deps_validation_test.go @@ -0,0 +1,51 @@ +package rpc + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/iamxvbaba/td/clock" + "go.uber.org/zap" + + telegramloginapp "telesrv/internal/app/telegramlogin" +) + +func TestAssertNoTypedNilDepsRejectsTelegramLogin(t *testing.T) { + var service *telegramloginapp.Service + defer func() { + value := recover() + if value == nil { + t.Fatal("assertNoTypedNilDeps accepted a typed-nil Telegram Login service") + } + message := fmt.Sprint(value) + if !strings.Contains(message, "dependency TelegramLogin is a typed nil *telegramlogin.Service") { + t.Fatalf("panic = %q, want TelegramLogin typed-nil diagnostic", message) + } + }() + New(Config{}, Deps{TelegramLogin: service}, zap.NewNop(), clock.System) +} + +func TestAssertNoTypedNilDepsAcceptsAbsentTelegramLogin(t *testing.T) { + assertNoTypedNilDeps(Deps{}) +} + +func TestDisabledTelegramLoginWebAuthorizationRPCs(t *testing.T) { + router := New(Config{}, Deps{}, zap.NewNop(), clock.System) + ctx := WithUserID(context.Background(), 42) + + listed, err := router.onAccountGetWebAuthorizations(ctx) + if err != nil { + t.Fatalf("get disabled web authorizations: %v", err) + } + if len(listed.Authorizations) != 0 || len(listed.Users) != 0 { + t.Fatalf("disabled web authorizations = %#v, want empty vectors", listed) + } + if reset, err := router.onAccountResetWebAuthorization(ctx, 123); err != nil || !reset { + t.Fatalf("reset disabled web authorization = %v, %v; want true, nil", reset, err) + } + if reset, err := router.onAccountResetWebAuthorizations(ctx); err != nil || !reset { + t.Fatalf("reset all disabled web authorizations = %v, %v; want true, nil", reset, err) + } +} diff --git a/internal/rpc/router.go b/internal/rpc/router.go index 1bf9b11e..212fc3b1 100644 --- a/internal/rpc/router.go +++ b/internal/rpc/router.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "reflect" "sync" "time" @@ -234,6 +235,7 @@ type authUserCacheEntry struct { // New 创建 Router,由各业务域自行注册其 RPC handler(registerHelp/Auth/Users/Updates)。 func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router { + assertNoTypedNilDeps(deps) instanceID := cfg.InstanceID if instanceID == "" { instanceID = fmt.Sprintf("%016x", randomNonZeroInt64()) @@ -282,6 +284,31 @@ func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router { return r } +// assertNoTypedNilDeps rejects partially constructed optional dependencies at +// the composition boundary. A Go interface containing a nil concrete pointer +// is not equal to nil, so handler-level availability checks would otherwise +// admit it and panic only when the first method is invoked. +// +// This is an invariant check, not a compatibility fallback: callers must +// either inject a fully constructed implementation or leave the interface nil. +func assertNoTypedNilDeps(deps Deps) { + value := reflect.ValueOf(deps) + typeOfDeps := value.Type() + for i := 0; i < value.NumField(); i++ { + field := value.Field(i) + if field.Kind() != reflect.Interface || field.IsNil() { + continue + } + implementation := field.Elem() + switch implementation.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + if implementation.IsNil() { + panic(fmt.Sprintf("rpc: dependency %s is a typed nil %s", typeOfDeps.Field(i).Name, implementation.Type())) + } + } + } +} + func registerRPC[T bin.Object](d *tlprofile.Dispatcher, method tlprofile.SemanticID, handler func(context.Context, T) (any, error)) { if d == nil || handler == nil { panic("rpc: register nil canonical RPC handler or dispatcher") From 76e7efd6cc9f4bceab4594f2e39beac4b1f46986 Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 16:09:42 +0800 Subject: [PATCH 18/28] docs: sync OTP delivery guide --- docs/otp-delivery.md | 106 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/otp-delivery.md diff --git a/docs/otp-delivery.md b/docs/otp-delivery.md new file mode 100644 index 00000000..29b630b2 --- /dev/null +++ b/docs/otp-delivery.md @@ -0,0 +1,106 @@ +# OTP delivery providers + +`telesrv` owns OTP generation, storage, attempt limits, expiry, verification, +and consumption. A delivery provider receives an already-issued code and must +only deliver it. It must not generate a replacement code or decide whether an +authentication attempt succeeds. + +## Routing + +- `TELESRV_PHONE_CODE_DELIVERY_PROVIDER=development` preserves the local fixed + code. `webhook` generates random SMS codes for login, registration, + login-email reset fallback, and phone changes. +- `TELESRV_EMAIL_CODE_DELIVERY_PROVIDER=smtp` preserves direct SMTP delivery. + `webhook` handles login-email, login-email setup, and login-email change. +- One Webhook endpoint may handle both channels. `channel` and `purpose` in the + request select the downstream template/provider. + +For an existing account, external delivery is additive: `auth.sendCode` and +`auth.resendCode` first commit the same code as a durable incoming message from +777000, then invoke the configured SMS or login-email provider. A provider +cannot replace or invalidate that App-code. A new phone and email setup/change +have no existing login dialog to receive the code, so those flows use only the +configured external provider. + +## Webhook v1 request + +`telesrv` sends one `POST` request and does not follow redirects: + +```http +POST /v1/otp/deliveries HTTP/1.1 +Content-Type: application/json +Accept: application/json +Idempotency-Key: otp_0193f0... +X-Telesrv-Timestamp: 1784275200 +X-Telesrv-Signature: sha256=... +``` + +```json +{ + "version": "1", + "delivery_id": "otp_0193f0...", + "purpose": "login_email", + "channel": "email", + "recipient": "alice@example.test", + "code": "482913", + "expires_at": "2026-07-17T16:05:00Z", + "expires_in": 299, + "locale": "zh-CN" +} +``` + +Current purpose values are `login_email`, `login_sms`, +`login_email_setup`, `login_email_change`, and `change_phone`. Current channel +values are `email` and `sms`. + +`delivery_id` is an opaque idempotency key. Replays of the same ID must not +send a second message. A resend that creates a new code has a new delivery ID. + +When `TELESRV_OTP_WEBHOOK_SECRET` is non-empty, the signature is lowercase hex +HMAC-SHA256 over: + +```text +. +``` + +## Response + +An accepted request returns any 2xx response with this JSON shape: + +```json +{ + "accepted": true, + "message_id": "provider-message-123" +} +``` + +`204 No Content` is also accepted. Other 2xx responses must explicitly contain +`"accepted": true`; a missing or malformed acknowledgement is treated as an +unknown outcome because the provider may already have sent the code. + +An explicit rejection may use either a non-2xx status or `accepted: false`: + +```json +{ + "accepted": false, + "error_code": "RECIPIENT_INVALID", + "retryable": false +} +``` + +The response body is capped at 64 KiB. For a flow without a durable 777000 +fallback, an explicit rejection invalidates only the code attempt that +triggered that request. A transport error or invalid successful acknowledgement +preserves the code and returns its hash because the provider may already have +sent it. For an existing-account login, any provider failure is reported but +does not fail the RPC or invalidate the code: the durable 777000 copy remains +the authoritative fallback. + +Webhook logs contain the opaque delivery ID, purpose, channel, status, and +transport error only. The code and recipient are not logged. + +A runnable standard-library receiver is available at +[`cmd/otpwebhook-example`](../cmd/otpwebhook-example/README.md). It includes +signature/timestamp validation, request limits, idempotency, health checking, +and graceful shutdown. Its delivery function is intentionally a no-op adapter +and must be replaced with the user's email/SMS API call. From 10de46219110ec38f5e4cf154c8c6770a1da99b1 Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 21:42:52 +0800 Subject: [PATCH 19/28] fix: sync web page preview pending deadline --- internal/rpc/messages_compat.go | 2 +- .../rpc/messages_send_webpage_rpc_test.go | 44 +++++++++++++++++++ internal/rpc/webpage_resolver.go | 4 ++ internal/rpc/webpage_url_extract.go | 7 +-- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/internal/rpc/messages_compat.go b/internal/rpc/messages_compat.go index af120cdb..919b0948 100644 --- a/internal/rpc/messages_compat.go +++ b/internal/rpc/messages_compat.go @@ -380,7 +380,7 @@ func (r *Router) webPagePreviewMedia(ctx context.Context, message string, entiti // resolveWebPageForRequest 为交互式读 RPC 解析链接预览:先查缓存(LookupWebPage,命中即返回, // 不抓取不阻塞);未命中才同步抓取,但用受限短预算(webpageRequestResolveBudget)而非异步解析 -// 的 20s,避免慢/挂上游把 RPC worker 钉死。命中(含负缓存的 empty)返回 ok=true,调用方据 state +// 的 30s,避免慢/挂上游把 RPC worker 钉死。命中(含负缓存的 empty)返回 ok=true,调用方据 state // 决定;抓取失败返回 false。未启用返回 false。 func (r *Router) resolveWebPageForRequest(ctx context.Context, url string) (domain.MessageWebPage, bool) { if page, ok := r.resolveAIComposeStyleWebPage(ctx, url); ok { diff --git a/internal/rpc/messages_send_webpage_rpc_test.go b/internal/rpc/messages_send_webpage_rpc_test.go index 5f3be62a..47b56109 100644 --- a/internal/rpc/messages_send_webpage_rpc_test.go +++ b/internal/rpc/messages_send_webpage_rpc_test.go @@ -3,6 +3,7 @@ package rpc import ( "context" "testing" + "time" "github.com/iamxvbaba/td/tg" @@ -27,6 +28,8 @@ func TestSendMessageAttachesWebPagePending(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) r.deps.Files.(*fakeFiles).webPagePreviewOn = true + now := time.Date(2030, time.January, 2, 3, 4, 5, 0, time.UTC) + r.clock = fixedClock{now: now} updates, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, @@ -55,11 +58,52 @@ func TestSendMessageAttachesWebPagePending(t *testing.T) { if url, _ := pending.GetURL(); url != wpTestURL { t.Errorf("pending url = %q, want %q", url, wpTestURL) } + wantDeadline := int(now.Add(webPagePendingLifetime).Unix()) + if pending.Date != wantDeadline { + t.Errorf("pending date = %d, want retry deadline %d", pending.Date, wantDeadline) + } + if time.Unix(int64(pending.Date), 0).Sub(now) <= webPageResolveTimeout { + t.Errorf("pending deadline must outlive resolver timeout: deadline=%s timeout=%s", time.Unix(int64(pending.Date), 0), webPageResolveTimeout) + } if !msg.InvertMedia { t.Errorf("invert_media not projected onto message") } } +// TestSendChannelMessageUsesSameFutureWebPageDeadline 锁定频道发送也经过同一 pending +// 截止时间构造路径,避免只修私聊 echo 而频道仍被客户端立即判定过期。 +func TestSendChannelMessageUsesSameFutureWebPageDeadline(t *testing.T) { + ctx := context.Background() + r, owner, channel := newRichChannelTestRouter(t) + r.deps.Files.(*fakeFiles).webPagePreviewOn = true + // 本测试只验证发送投影,不启动异步解析 goroutine。 + r.webPageResolveSem = nil + now := time.Date(2030, time.February, 3, 4, 5, 6, 0, time.UTC) + r.clock = fixedClock{now: now} + + updates, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{ + Peer: &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}, + Message: wpTestMessage, + Entities: wpURLEntities(), + RandomID: 5106, + }) + if err != nil { + t.Fatalf("send channel message: %v", err) + } + msg := newMessageFromUpdates(t, updates) + wrap, ok := msg.Media.(*tg.MessageMediaWebPage) + if !ok { + t.Fatalf("channel media = %T, want *tg.MessageMediaWebPage", msg.Media) + } + pending, ok := wrap.Webpage.(*tg.WebPagePending) + if !ok { + t.Fatalf("channel webpage = %T, want *tg.WebPagePending", wrap.Webpage) + } + if want := int(now.Add(webPagePendingLifetime).Unix()); pending.Date != want { + t.Errorf("channel pending date = %d, want retry deadline %d", pending.Date, want) + } +} + // TestSendMessageAttachesCachedDoneCard 验证:URL 已缓存解析时,发送 echo 直接带 done 卡片 // (非 pending)——官方行为,TDesktop 据此立即渲染、不依赖异步换卡。 func TestSendMessageAttachesCachedDoneCard(t *testing.T) { diff --git a/internal/rpc/webpage_resolver.go b/internal/rpc/webpage_resolver.go index ff435f4a..7d89086b 100644 --- a/internal/rpc/webpage_resolver.go +++ b/internal/rpc/webpage_resolver.go @@ -18,6 +18,10 @@ import ( const ( webPageResolveConcurrency = 16 webPageResolveTimeout = 30 * time.Second + // webPagePendingLifetime 是客户端在重新拉取 pending 消息前等待的窗口。 + // TDesktop 与 DrKLO 都把 webPagePending.date 解释为绝对截止时间,而不是处理开始时间; + // 该窗口必须严格大于 resolver 的最大执行时间,避免正常的慢解析被客户端提前标记为失败。 + webPagePendingLifetime = 2 * time.Minute ) type webPageResolveJob struct { diff --git a/internal/rpc/webpage_url_extract.go b/internal/rpc/webpage_url_extract.go index a778c89a..155eaca0 100644 --- a/internal/rpc/webpage_url_extract.go +++ b/internal/rpc/webpage_url_extract.go @@ -135,9 +135,10 @@ func (r *Router) webPagePendingOrCachedMedia(ctx context.Context, rawURL string, State: domain.MessageWebPageStatePending, ID: domain.WebPageURLHash(normalized), URL: normalized, - // Date=「processing started」时刻:留 0(=1970)会被严格客户端判为 pending 早已 - // 过期 → 直接显示纯文本,须填发送时刻。 - Date: int(r.clock.Now().Unix()), + // date 是客户端重新拉取 pending 消息的绝对截止时间。TDesktop/DrKLO 在 + // date<=now 时立即重取;若 resolver 此时仍运行,TDesktop 会把占位记成 sticky + // failed,后到的 done update 也不会重新显示卡片。 + Date: int(r.clock.Now().Add(webPagePendingLifetime).Unix()), ForceLargeMedia: forceLarge, ForceSmallMedia: forceSmall, }, From 8cfb6f74c1b7bdfe4ecd64982ab2c9120e79008e Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 21:43:56 +0800 Subject: [PATCH 20/28] feat: sync host-based public app links --- .env.example | 6 + cmd/telesrv/main.go | 5 +- docs/configuration.en.md | 2 + docs/configuration.zh-CN.md | 2 + internal/app/bots/stickersbot.go | 10 ++ internal/app/bots/stickersbot_test.go | 16 +++ internal/app/telegramlogin/service.go | 21 ++-- internal/app/telegramlogin/service_test.go | 26 ++++- internal/config/config.go | 9 ++ internal/config/config_test.go | 11 ++ internal/links/links.go | 128 +++++++++++++++++++++ internal/links/links_test.go | 73 ++++++++++++ internal/rpc/account_business.go | 2 +- internal/rpc/public_app_links_test.go | 32 ++++++ internal/rpc/public_links.go | 4 + internal/rpc/router.go | 12 +- internal/telegramloginhttp/handler_test.go | 14 ++- internal/web/server.go | 15 ++- internal/web/server_test.go | 13 ++- 19 files changed, 375 insertions(+), 26 deletions(-) create mode 100644 internal/rpc/public_app_links_test.go diff --git a/.env.example b/.env.example index 35abb6c8..d4211299 100644 --- a/.env.example +++ b/.env.example @@ -81,6 +81,12 @@ TELESRV_PUBLIC_BASE_URL=https://telesrv.net # registered by every patched client build; tg/http/https are rejected. TELESRV_PUBLIC_APP_SCHEME=telesrv +# Optional host-based app-link root for multi-server clients. When set, public +# links use e.g. owpg://example.com/oauth and owpg://example.com/username while +# the scheme above remains accepted for existing/in-flight links. The value +# must be exactly ://, without port/path/query/fragment. +TELESRV_PUBLIC_APP_LINK_BASE= + # Web client target and display brand used by public landing pages. TELESRV_PUBLIC_WEB_BASE_URL=https://web.telesrv.net TELESRV_PUBLIC_APP_NAME=telesrv diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index ac50180e..2dd4e458 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -365,7 +365,7 @@ func run(logger *zap.Logger) error { return fmt.Errorf("load telegram login signing keys: %w", err) } telegramLoginService, err = telegramloginapp.NewService(postgres.NewTelegramLoginStore(pool), codeSealer, telegramloginapp.Config{ - Issuer: cfg.TelegramLoginIssuer, AppScheme: cfg.PublicAppScheme, + Issuer: cfg.TelegramLoginIssuer, AppScheme: cfg.PublicAppScheme, AppLinkBase: cfg.PublicAppLinkBase, AllowHTTP: cfg.TelegramLoginAllowHTTP, ClientSecretPepper: clientSecretPepper, SupportedSigningAlgorithms: signingKeys.ActiveAlgorithms(), @@ -838,6 +838,8 @@ func run(logger *zap.Logger) error { GroupCallMaxParticipants: cfg.GroupCallMaxParticipants, RtmpIngestURL: cfg.LiveStreamRtmpURL, PublicBaseURL: cfg.PublicBaseURL, + PublicAppScheme: cfg.PublicAppScheme, + PublicAppLinkBase: cfg.PublicAppLinkBase, // PFS temp→perm 解析缓存:显式撤销会清缓存并断开连接,re-bind 即时失效; // 配置 TTL 只承担跨进程/异常失效兜底,避免大连接数周期性打满 PG。 TempKeyResolveCacheTTL: cfg.TempKeyResolveCacheTTL, @@ -986,6 +988,7 @@ func run(logger *zap.Logger) error { Addr: cfg.PublicLinkWebAddr, PublicBaseURL: cfg.PublicBaseURL, AppScheme: cfg.PublicAppScheme, + AppLinkBase: cfg.PublicAppLinkBase, WebBaseURL: cfg.PublicWebBaseURL, AppName: cfg.PublicAppName, StickerSets: filesService, diff --git a/docs/configuration.en.md b/docs/configuration.en.md index 71de0b4b..911105e7 100644 --- a/docs/configuration.en.md +++ b/docs/configuration.en.md @@ -60,6 +60,7 @@ This document describes every setting loaded by `internal/config`. Defaults and | `TELESRV_ADMIN_SESSION_KEY` | secret string / empty | Encrypts/signs Admin UI session cookies. Production should use at least 32 random bytes; changing it invalidates sessions. | | `TELESRV_PUBLIC_BASE_URL` | HTTP(S) URL / `https://telesrv.net` | Client-visible canonical public-link root. Paths are allowed; credentials, query, and fragment are rejected. Local example: `http://127.0.0.1:2401`. | | `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | Automatic app-open scheme on landing pages. Must match patched client registration. `tg`, `http`, and `https` are rejected. | +| `TELESRV_PUBLIC_APP_LINK_BASE` | nullable custom URL base / empty | Optional host-based root for multi-server clients, for example `owpg://example.com`. When set, links use `owpg://example.com/oauth`, `owpg://example.com/`, and equivalent route paths. Only exact `://` values are accepted; ports, paths, queries, and fragments are rejected. `TELESRV_PUBLIC_APP_SCHEME` remains an accepted legacy input. | | `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | Web-client root used by public username pages. Same URL validation as `TELESRV_PUBLIC_BASE_URL`. | | `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | Public landing-page product name; trimmed, non-empty, no control characters, maximum 64 Unicode characters. | | `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / empty | Read-only username/avatar/sticker/emoji/chatlist/collectible-gift landing-page listener. Empty disables it. Production should bind loopback behind exact nginx routes. `.env.example` enables `127.0.0.1:2401` for development. | @@ -237,6 +238,7 @@ that clients can actually reach. Bind `0.0.0.0:2401` for direct LAN/public acces TELESRV_PUBLIC_BASE_URL=http://192.0.2.25:2401 TELESRV_PUBLIC_LINK_WEB_ADDR=0.0.0.0:2401 TELESRV_PUBLIC_APP_SCHEME=telesrv +# Optional for multi-server clients: TELESRV_PUBLIC_APP_LINK_BASE=owpg://example.com TELESRV_TELEGRAM_LOGIN_ENABLE=true TELESRV_TELEGRAM_LOGIN_ISSUER=http://192.0.2.25:2401 diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md index 69433930..51085e06 100644 --- a/docs/configuration.zh-CN.md +++ b/docs/configuration.zh-CN.md @@ -60,6 +60,7 @@ | `TELESRV_ADMIN_SESSION_KEY` | secret string / 空 | 加密/签名 Admin UI session cookie;生产至少使用 32 字节随机值,修改会使已有会话失效。 | | `TELESRV_PUBLIC_BASE_URL` | HTTP(S) URL / `https://telesrv.net` | 客户端可见的公开链接根地址;允许 path,禁止 credentials、query、fragment。本地例:`http://127.0.0.1:2401`。 | | `TELESRV_PUBLIC_APP_SCHEME` | URL scheme / `telesrv` | 落地页自动唤起客户端的 scheme,必须与 patched 客户端注册值一致;禁止 `tg`、`http`、`https`。 | +| `TELESRV_PUBLIC_APP_LINK_BASE` | nullable custom URL base / 空 | 多服务客户端可选的 host-based 根,例如 `owpg://example.com`。配置后生成 `owpg://example.com/oauth`、`owpg://example.com/` 等;只允许精确 `://`,禁止端口、path、query、fragment。`TELESRV_PUBLIC_APP_SCHEME` 仍作为旧链接输入兼容。 | | `TELESRV_PUBLIC_WEB_BASE_URL` | HTTP(S) URL / `https://web.telesrv.net` | username 页面 Web 客户端入口,校验规则同 `TELESRV_PUBLIC_BASE_URL`。 | | `TELESRV_PUBLIC_APP_NAME` | string / `telesrv` | 公开落地页产品名;trim 后非空、无控制字符、最多 64 个 Unicode 字符。 | | `TELESRV_PUBLIC_LINK_WEB_ADDR` | nullable address / 空 | 只读 username/avatar/sticker/emoji/chatlist/collectible gift 落地页监听;空值关闭。生产应 loopback + nginx 精确反代;`.env.example` 为开发启用 `127.0.0.1:2401`。 | @@ -228,6 +229,7 @@ chmod 0600 data/telegram-login/* TELESRV_PUBLIC_BASE_URL=http://192.0.2.25:2401 TELESRV_PUBLIC_LINK_WEB_ADDR=0.0.0.0:2401 TELESRV_PUBLIC_APP_SCHEME=telesrv +# 多服务客户端可选:TELESRV_PUBLIC_APP_LINK_BASE=owpg://example.com TELESRV_TELEGRAM_LOGIN_ENABLE=true TELESRV_TELEGRAM_LOGIN_ISSUER=http://192.0.2.25:2401 diff --git a/internal/app/bots/stickersbot.go b/internal/app/bots/stickersbot.go index e951cc94..1cdb5ce1 100644 --- a/internal/app/bots/stickersbot.go +++ b/internal/app/bots/stickersbot.go @@ -754,6 +754,16 @@ func normalizeStickersBotShortName(raw string) string { raw = strings.TrimPrefix(raw, "tg://addemoji?set=") if strings.Contains(raw, "://") { if parsed, err := url.Parse(raw); err == nil { + query := parsed.Query() + route := strings.Trim(parsed.Path, "/") + if route == "" { + route = strings.ToLower(parsed.Host) + } + if route == "addstickers" || route == "addemoji" { + if shortName := query.Get("set"); shortName != "" { + raw = shortName + } + } parts := strings.Split(strings.Trim(parsed.Path, "/"), "/") for i, part := range parts { if (part == "addstickers" || part == "addemoji") && i+1 < len(parts) { diff --git a/internal/app/bots/stickersbot_test.go b/internal/app/bots/stickersbot_test.go index 96c43a5d..22fe26be 100644 --- a/internal/app/bots/stickersbot_test.go +++ b/internal/app/bots/stickersbot_test.go @@ -649,3 +649,19 @@ func (h *stickersBotHookRecorder) PushStickerSetsChanged(_ context.Context, user h.userID = userID h.kind = kind } + +func TestNormalizeStickersBotShortNameAcceptsHostBasedAppLinks(t *testing.T) { + for _, tc := range []struct { + raw string + want string + }{ + {raw: "telesrv://addstickers?set=Legacy_Pack", want: "legacy_pack"}, + {raw: "owpg://tenant.example.test/addstickers?set=Hosted_Pack", want: "hosted_pack"}, + {raw: "owpg://tenant.example.test/addemoji?set=Emoji_Pack", want: "emoji_pack"}, + {raw: "https://telesrv.net/addstickers/Web_Pack", want: "web_pack"}, + } { + if got := normalizeStickersBotShortName(tc.raw); got != tc.want { + t.Fatalf("normalizeStickersBotShortName(%q) = %q, want %q", tc.raw, got, tc.want) + } + } +} diff --git a/internal/app/telegramlogin/service.go b/internal/app/telegramlogin/service.go index dd725a03..70a60fdb 100644 --- a/internal/app/telegramlogin/service.go +++ b/internal/app/telegramlogin/service.go @@ -19,6 +19,7 @@ import ( "unicode/utf8" "telesrv/internal/domain" + "telesrv/internal/links" "telesrv/internal/store" ) @@ -36,6 +37,7 @@ var telegramLoginMatchCodePool = []string{ type Config struct { Issuer string AppScheme string + AppLinkBase string AllowHTTP bool ClientSecretPepper []byte SupportedSigningAlgorithms []domain.TelegramLoginSigningAlgorithm @@ -48,7 +50,7 @@ type Service struct { store store.TelegramLoginStore sealer *CodeSealer issuer string - appScheme string + appLinks links.AppLinkBuilder allowHTTP bool clientSecretPepper []byte signingAlgorithms []domain.TelegramLoginSigningAlgorithm @@ -66,8 +68,9 @@ func NewService(loginStore store.TelegramLoginStore, sealer *CodeSealer, cfg Con if err != nil { return nil, fmt.Errorf("telegram login issuer: %w", err) } - if !validAppScheme(cfg.AppScheme) { - return nil, fmt.Errorf("telegram login app scheme is invalid") + appLinks, err := links.NewAppLinkBuilder(cfg.AppScheme, cfg.AppLinkBase) + if err != nil { + return nil, fmt.Errorf("telegram login app links: %w", err) } if cfg.RequestTTL == 0 { cfg.RequestTTL = defaultRequestTTL @@ -92,7 +95,7 @@ func NewService(loginStore store.TelegramLoginStore, sealer *CodeSealer, cfg Con } } return &Service{ - store: loginStore, sealer: sealer, issuer: issuer, appScheme: strings.ToLower(cfg.AppScheme), + store: loginStore, sealer: sealer, issuer: issuer, appLinks: appLinks, allowHTTP: cfg.AllowHTTP, clientSecretPepper: append([]byte(nil), cfg.ClientSecretPepper...), signingAlgorithms: append([]domain.TelegramLoginSigningAlgorithm(nil), cfg.SupportedSigningAlgorithms...), @@ -669,7 +672,7 @@ func (s *Service) CreateAuthorization(ctx context.Context, params CreateAuthoriz if err != nil { return CreatedAuthorization{}, err } - deepLink := s.appScheme + "://oauth?token=" + url.QueryEscape(requestToken) + deepLink := s.appLinks.Build("oauth", url.Values{"token": []string{requestToken}}) return CreatedAuthorization{Request: request, RequestToken: requestToken, BrowserToken: browserToken, DeepLink: deepLink}, nil } @@ -885,12 +888,14 @@ func (s *Service) deepLinkToken(rawURL string) (string, error) { if err != nil { return "", domain.ErrTelegramLoginURLInvalid } - customOrCanonicalScheme := strings.EqualFold(u.Scheme, s.appScheme) || strings.EqualFold(u.Scheme, "tg") var token string switch { - case customOrCanonicalScheme && strings.EqualFold(u.Host, "oauth") && u.Path == "": + case s.appLinks.MatchesRoute(u, "oauth"): token, _ = singleQueryValue(query, "token") - case customOrCanonicalScheme && strings.EqualFold(u.Host, "resolve") && u.Path == "": + case strings.EqualFold(u.Scheme, "tg") && strings.EqualFold(u.Host, "oauth") && u.Path == "": + token, _ = singleQueryValue(query, "token") + case (s.appLinks.MatchesLegacyRoute(u, "resolve") || + (strings.EqualFold(u.Scheme, "tg") && strings.EqualFold(u.Host, "resolve") && u.Path == "")): domainValue, domainOK := singleQueryValue(query, "domain") startApp, startAppOK := singleQueryValue(query, "startapp") if domainOK && startAppOK && strings.EqualFold(domainValue, "oauth") { diff --git a/internal/app/telegramlogin/service_test.go b/internal/app/telegramlogin/service_test.go index dc98e407..86df38bb 100644 --- a/internal/app/telegramlogin/service_test.go +++ b/internal/app/telegramlogin/service_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/url" + "strings" "sync" "sync/atomic" "testing" @@ -78,10 +79,18 @@ func TestServiceClientCreationAndSecretRotationAreSingleWinner(t *testing.T) { } func newTelegramLoginTestService(t *testing.T, now *time.Time) (*Service, *memory.TelegramLoginStore) { - return newTelegramLoginTestServiceWithAlgorithms(t, now, nil) + return newTelegramLoginTestServiceWithConfig(t, now, nil, "") } func newTelegramLoginTestServiceWithAlgorithms(t *testing.T, now *time.Time, algorithms []domain.TelegramLoginSigningAlgorithm) (*Service, *memory.TelegramLoginStore) { + return newTelegramLoginTestServiceWithConfig(t, now, algorithms, "") +} + +func newTelegramLoginTestServiceWithAppLinkBase(t *testing.T, now *time.Time, appLinkBase string) (*Service, *memory.TelegramLoginStore) { + return newTelegramLoginTestServiceWithConfig(t, now, nil, appLinkBase) +} + +func newTelegramLoginTestServiceWithConfig(t *testing.T, now *time.Time, algorithms []domain.TelegramLoginSigningAlgorithm, appLinkBase string) (*Service, *memory.TelegramLoginStore) { t.Helper() key := make([]byte, 32) key[0] = 7 @@ -93,7 +102,7 @@ func newTelegramLoginTestServiceWithAlgorithms(t *testing.T, now *time.Time, alg pepper := make([]byte, 32) pepper[0] = 9 service, err := NewService(loginStore, sealer, Config{ - Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv", + Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv", AppLinkBase: appLinkBase, AllowHTTP: true, ClientSecretPepper: pepper, SupportedSigningAlgorithms: algorithms, Now: func() time.Time { return *now }, @@ -107,7 +116,7 @@ func newTelegramLoginTestServiceWithAlgorithms(t *testing.T, now *time.Time, alg func TestServiceAcceptsOfficialClientCanonicalOAuthDeepLinks(t *testing.T) { ctx := context.Background() now := time.Unix(1_780_000_000, 0).UTC() - service, _ := newTelegramLoginTestService(t, &now) + service, _ := newTelegramLoginTestServiceWithAppLinkBase(t, &now, "owpg://tenant.example.test") credentials, err := service.CreateClient(ctx, 9030, domain.TelegramLoginSigningRS256) if err != nil { t.Fatal(err) @@ -132,8 +141,13 @@ func TestServiceAcceptsOfficialClientCanonicalOAuthDeepLinks(t *testing.T) { t.Fatal(err) } token := parsed.Query().Get("token") + if got, want := parsed.Scheme+"://"+parsed.Host+parsed.Path, "owpg://tenant.example.test/oauth"; got != want { + t.Fatalf("generated deep link root = %q, want %q", got, want) + } valid := []string{ created.DeepLink, + "telesrv://oauth?token=" + url.QueryEscape(token), + "telesrv://resolve?domain=oauth&startapp=" + url.QueryEscape(token), "tg://oauth?token=" + url.QueryEscape(token), "tg://resolve?domain=oauth&startapp=" + url.QueryEscape(token), "https://t.me/oauth?startapp=" + url.QueryEscape(token), @@ -146,6 +160,9 @@ func TestServiceAcceptsOfficialClientCanonicalOAuthDeepLinks(t *testing.T) { } invalid := []string{ "telegram://oauth?token=" + url.QueryEscape(token), + "owpg://other.example.test/oauth?token=" + url.QueryEscape(token), + "owpg://tenant.example.test/resolve?domain=oauth&startapp=" + url.QueryEscape(token), + "owpg://tenant.example.test/oauth/extra?token=" + url.QueryEscape(token), "tg://oauth/path?token=" + url.QueryEscape(token), "tg://oauth?token=" + url.QueryEscape(token) + "&token=other", "tg://resolve?domain=oauth&domain=other&startapp=" + url.QueryEscape(token), @@ -228,6 +245,9 @@ func TestServiceAuthorizationCodeFlowAndRevocation(t *testing.T) { if created.DeepLink == "" || created.Request.ID == 0 || len(created.Request.MatchCodes) != 5 { t.Fatalf("created authorization = %#v", created) } + if !strings.HasPrefix(created.DeepLink, "telesrv://oauth?token=") { + t.Fatalf("default deep link = %q, want legacy telesrv:// OAuth form", created.DeepLink) + } if _, err := service.CheckMatchCode(ctx, created.DeepLink, created.Request.MatchCodes[0]); err == nil && created.Request.MatchCodes[0] != created.Request.MatchCode { t.Fatal("wrong match code unexpectedly accepted") } diff --git a/internal/config/config.go b/internal/config/config.go index 72fcbd47..fc7d4ee1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -82,6 +82,10 @@ type Config struct { // PublicAppScheme 是公开落地页自动唤起自建客户端时使用的 URL scheme。 // 必须与 TDesktop/Android 客户端构建时注册的 scheme 一致,且不能占用 tg/http/https。 PublicAppScheme string + // PublicAppLinkBase 是可选的 host-based 自建客户端链接根,例如 + // owpg://example.com。为空时继续生成 PublicAppScheme://;非空时 + // 生成 /,同时保留旧 scheme 作为服务端输入兼容。 + PublicAppLinkBase string // PublicWebBaseURL 是公开 username 页面“Open in Web”按钮指向的 Web 客户端根 URL。 PublicWebBaseURL string // PublicAppName 是公开落地页展示的产品名,不参与协议路由。 @@ -434,6 +438,10 @@ func Load() (Config, error) { if err != nil { return Config{}, fmt.Errorf("TELESRV_PUBLIC_APP_SCHEME: %w", err) } + publicAppLinkBase, err := links.ValidateAppLinkBase(envAllowEmptyOr("TELESRV_PUBLIC_APP_LINK_BASE", "")) + if err != nil { + return Config{}, fmt.Errorf("TELESRV_PUBLIC_APP_LINK_BASE: %w", err) + } publicWebBaseURL, err := links.ValidateBaseURL(envOr("TELESRV_PUBLIC_WEB_BASE_URL", links.DefaultWebBaseURL)) if err != nil { return Config{}, fmt.Errorf("TELESRV_PUBLIC_WEB_BASE_URL: %w", err) @@ -487,6 +495,7 @@ func Load() (Config, error) { AdminAPIToken: envOr("TELESRV_ADMIN_API_TOKEN", ""), PublicBaseURL: publicBaseURL, PublicAppScheme: publicAppScheme, + PublicAppLinkBase: publicAppLinkBase, PublicWebBaseURL: publicWebBaseURL, PublicAppName: publicAppName, PublicLinkWebAddr: envAllowEmptyOr("TELESRV_PUBLIC_LINK_WEB_ADDR", ""), diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0339fb40..dd16f92f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -25,6 +25,9 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) { if cfg.PublicAppScheme != "telesrv" { t.Fatalf("PublicAppScheme = %q, want telesrv", cfg.PublicAppScheme) } + if cfg.PublicAppLinkBase != "" { + t.Fatalf("PublicAppLinkBase = %q, want disabled", cfg.PublicAppLinkBase) + } if cfg.PublicWebBaseURL != "https://web.telesrv.net" { t.Fatalf("PublicWebBaseURL = %q, want https://web.telesrv.net", cfg.PublicWebBaseURL) } @@ -379,6 +382,7 @@ TELESRV_WEBSOCKET_ALLOWED_ORIGINS=https://one.example, https://two.example TELESRV_CALL_RING_TIMEOUT=2m TELESRV_PUBLIC_BASE_URL=links.example.test/root TELESRV_PUBLIC_APP_SCHEME=example-chat +TELESRV_PUBLIC_APP_LINK_BASE=OWPG://Tenant.Example.Test/ TELESRV_PUBLIC_WEB_BASE_URL=web.example.test/client TELESRV_PUBLIC_APP_NAME=Example Chat TELESRV_PUBLIC_LINK_WEB_ADDR=127.0.0.1:2401 @@ -410,6 +414,9 @@ TELESRV_PUBLIC_LINK_WEB_ADDR=127.0.0.1:2401 if cfg.PublicAppScheme != "example-chat" { t.Fatalf("PublicAppScheme = %q, want example-chat", cfg.PublicAppScheme) } + if cfg.PublicAppLinkBase != "owpg://tenant.example.test" { + t.Fatalf("PublicAppLinkBase = %q, want owpg://tenant.example.test", cfg.PublicAppLinkBase) + } if cfg.PublicWebBaseURL != "https://web.example.test/client" { t.Fatalf("PublicWebBaseURL = %q, want https://web.example.test/client", cfg.PublicWebBaseURL) } @@ -537,6 +544,10 @@ func TestLoadRejectsInvalidPublicLinkClientConfig(t *testing.T) { }{ {name: "official scheme", key: "TELESRV_PUBLIC_APP_SCHEME", value: "tg"}, {name: "malformed scheme", key: "TELESRV_PUBLIC_APP_SCHEME", value: "bad scheme"}, + {name: "app link base official scheme", key: "TELESRV_PUBLIC_APP_LINK_BASE", value: "tg://links.example.test"}, + {name: "app link base missing host", key: "TELESRV_PUBLIC_APP_LINK_BASE", value: "owpg://"}, + {name: "app link base path", key: "TELESRV_PUBLIC_APP_LINK_BASE", value: "owpg://links.example.test/root"}, + {name: "app link base query", key: "TELESRV_PUBLIC_APP_LINK_BASE", value: "owpg://links.example.test?tenant=one"}, {name: "invalid web base", key: "TELESRV_PUBLIC_WEB_BASE_URL", value: "file:///tmp/client"}, {name: "empty app name after trim", key: "TELESRV_PUBLIC_APP_NAME", value: " "}, {name: "control in app name", key: "TELESRV_PUBLIC_APP_NAME", value: "bad\nname"}, diff --git a/internal/links/links.go b/internal/links/links.go index 50ac7a2f..935e6e27 100644 --- a/internal/links/links.go +++ b/internal/links/links.go @@ -14,6 +14,17 @@ const ( ) const MaxChatlistSlugBytes = 128 +// AppLinkBuilder builds client-visible custom-scheme links. Without an +// explicit base it preserves Telegram's route-as-host shape, for example +// telesrv://oauth?token=... . A configured base uses an exact server host and +// moves the route into the path, for example owpg://example.test/oauth?token=... +// . The legacy scheme remains accepted so in-flight links survive a rollout. +type AppLinkBuilder struct { + legacyScheme string + baseScheme string + baseHost string +} + // ValidateAppScheme normalizes the client-visible custom URL scheme used by // public landing pages. Standard Web schemes and Telegram's official tg scheme // are deliberately rejected: the latter remains a manual compatibility link @@ -36,6 +47,123 @@ func ValidateAppScheme(raw string) (string, error) { return scheme, nil } +// ValidateAppLinkBase validates the optional host-based custom app-link root. +// The base is deliberately limited to ://: routes, query +// parameters, and fragments are owned by the individual link builders. +func ValidateAppLinkBase(raw string) (string, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return "", nil + } + parsed, err := url.Parse(raw) + if err != nil { + return "", fmt.Errorf("parse URL: %w", err) + } + if parsed.Opaque != "" { + return "", fmt.Errorf("opaque URLs are not allowed") + } + if parsed.Scheme == "" { + return "", fmt.Errorf("scheme is required") + } + scheme, err := ValidateAppScheme(parsed.Scheme) + if err != nil { + return "", err + } + if parsed.Host == "" || parsed.Hostname() == "" { + return "", fmt.Errorf("host is required") + } + if parsed.User != nil { + return "", fmt.Errorf("credentials are not allowed") + } + if parsed.Port() != "" { + return "", fmt.Errorf("port is not allowed") + } + if (parsed.Path != "" && parsed.Path != "/") || parsed.RawPath != "" { + return "", fmt.Errorf("path is not allowed") + } + if parsed.RawQuery != "" || parsed.ForceQuery { + return "", fmt.Errorf("query parameters are not allowed") + } + if parsed.Fragment != "" { + return "", fmt.Errorf("fragment is not allowed") + } + parsed.Scheme = scheme + parsed.Host = strings.ToLower(parsed.Host) + parsed.Path = "" + return parsed.String(), nil +} + +func NewAppLinkBuilder(legacyScheme, rawBase string) (AppLinkBuilder, error) { + legacyScheme, err := ValidateAppScheme(legacyScheme) + if err != nil { + return AppLinkBuilder{}, fmt.Errorf("legacy scheme: %w", err) + } + base, err := ValidateAppLinkBase(rawBase) + if err != nil { + return AppLinkBuilder{}, fmt.Errorf("app link base: %w", err) + } + builder := AppLinkBuilder{legacyScheme: legacyScheme} + if base != "" { + parsed, _ := url.Parse(base) + builder.baseScheme = parsed.Scheme + builder.baseHost = parsed.Host + } + return builder, nil +} + +func (b AppLinkBuilder) Build(route string, query url.Values) string { + if b.baseHost != "" { + return (&url.URL{ + Scheme: b.baseScheme, + Host: b.baseHost, + Path: "/" + strings.Trim(route, "/"), + RawQuery: query.Encode(), + }).String() + } + return (&url.URL{Scheme: b.legacyScheme, Host: route, RawQuery: query.Encode()}).String() +} + +// BuildUsername preserves the official resolve query in legacy mode while a +// host-based multi-server client receives the public username as the path. +func (b AppLinkBuilder) BuildUsername(username string, query url.Values) string { + query = cloneValues(query) + if b.baseHost != "" { + query.Del("domain") + return b.Build(username, query) + } + query.Set("domain", username) + return b.Build("resolve", query) +} + +// MatchesRoute accepts the exact configured host-path form and the retained +// legacy route-as-host form. Query validation remains the caller's concern. +func (b AppLinkBuilder) MatchesRoute(parsed *url.URL, route string) bool { + if parsed == nil || parsed.Opaque != "" || parsed.User != nil || parsed.Fragment != "" || parsed.RawPath != "" { + return false + } + if b.MatchesLegacyRoute(parsed, route) { + return true + } + return b.baseHost != "" && + strings.EqualFold(parsed.Scheme, b.baseScheme) && + strings.EqualFold(parsed.Host, b.baseHost) && + parsed.Path == "/"+route +} + +func (b AppLinkBuilder) MatchesLegacyRoute(parsed *url.URL, route string) bool { + return parsed != nil && parsed.Opaque == "" && parsed.User == nil && parsed.Fragment == "" && parsed.RawPath == "" && + strings.EqualFold(parsed.Scheme, b.legacyScheme) && + strings.EqualFold(parsed.Host, route) && parsed.Path == "" +} + +func cloneValues(values url.Values) url.Values { + cloned := make(url.Values, len(values)) + for key, entries := range values { + cloned[key] = append([]string(nil), entries...) + } + return cloned +} + func ValidateAppName(raw string) (string, error) { name := strings.TrimSpace(raw) if name == "" { diff --git a/internal/links/links_test.go b/internal/links/links_test.go index 14ca808e..3ba0df30 100644 --- a/internal/links/links_test.go +++ b/internal/links/links_test.go @@ -88,6 +88,79 @@ func TestValidateAppScheme(t *testing.T) { } } +func TestValidateAppLinkBase(t *testing.T) { + tests := []struct { + name string + raw string + want string + wantErr bool + }{ + {name: "disabled", raw: "", want: ""}, + {name: "normalized", raw: " OWPG://Example.Test/ ", want: "owpg://example.test"}, + {name: "missing host", raw: "owpg://", wantErr: true}, + {name: "reserved scheme", raw: "https://example.test", wantErr: true}, + {name: "credentials", raw: "owpg://user@example.test", wantErr: true}, + {name: "port", raw: "owpg://example.test:443", wantErr: true}, + {name: "path", raw: "owpg://example.test/root", wantErr: true}, + {name: "query", raw: "owpg://example.test?tenant=one", wantErr: true}, + {name: "fragment", raw: "owpg://example.test#root", wantErr: true}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ValidateAppLinkBase(tc.raw) + if (err != nil) != tc.wantErr { + t.Fatalf("ValidateAppLinkBase(%q) error = %v, wantErr %v", tc.raw, err, tc.wantErr) + } + if got != tc.want { + t.Fatalf("ValidateAppLinkBase(%q) = %q, want %q", tc.raw, got, tc.want) + } + }) + } +} + +func TestAppLinkBuilderPreservesLegacyAndSupportsHostBase(t *testing.T) { + legacy, err := NewAppLinkBuilder("telesrv", "") + if err != nil { + t.Fatal(err) + } + if got, want := legacy.Build("oauth", url.Values{"token": {"a+b"}}), "telesrv://oauth?token=a%2Bb"; got != want { + t.Fatalf("legacy OAuth = %q, want %q", got, want) + } + if got, want := legacy.BuildUsername("Alice", url.Values{"start": {"hello"}}), "telesrv://resolve?domain=Alice&start=hello"; got != want { + t.Fatalf("legacy username = %q, want %q", got, want) + } + + hosted, err := NewAppLinkBuilder("telesrv", "owpg://links.example.test") + if err != nil { + t.Fatal(err) + } + if got, want := hosted.Build("oauth", url.Values{"token": {"a+b"}}), "owpg://links.example.test/oauth?token=a%2Bb"; got != want { + t.Fatalf("hosted OAuth = %q, want %q", got, want) + } + if got, want := hosted.BuildUsername("Alice", url.Values{"domain": {"spoofed"}, "start": {"hello"}}), "owpg://links.example.test/Alice?start=hello"; got != want { + t.Fatalf("hosted username = %q, want %q", got, want) + } + + for _, tc := range []struct { + raw string + want bool + }{ + {raw: "telesrv://oauth?token=x", want: true}, + {raw: "owpg://links.example.test/oauth?token=x", want: true}, + {raw: "owpg://other.example.test/oauth?token=x", want: false}, + {raw: "owpg://links.example.test/oauth/extra?token=x", want: false}, + {raw: "owpg://links.example.test/resolve?token=x", want: false}, + } { + parsed, err := url.Parse(tc.raw) + if err != nil { + t.Fatal(err) + } + if got := hosted.MatchesRoute(parsed, "oauth"); got != tc.want { + t.Fatalf("MatchesRoute(%q) = %v, want %v", tc.raw, got, tc.want) + } + } +} + func TestValidateAppName(t *testing.T) { if got, err := ValidateAppName(" Example Chat "); err != nil || got != "Example Chat" { t.Fatalf("ValidateAppName valid = %q, %v", got, err) diff --git a/internal/rpc/account_business.go b/internal/rpc/account_business.go index a5653f8c..629fd3f8 100644 --- a/internal/rpc/account_business.go +++ b/internal/rpc/account_business.go @@ -458,7 +458,7 @@ func (r *Router) connectedBusinessBotPeerSettings(ctx context.Context, ownerUser settings.BusinessBotManageURL = r.connectedBusinessBotManageURL(botUser) } if settings.BusinessBotManageURL == "" { - settings.BusinessBotManageURL = "telesrv://business-bot" + settings.BusinessBotManageURL = r.publicAppLink("business-bot") } return settings, nil } diff --git a/internal/rpc/public_app_links_test.go b/internal/rpc/public_app_links_test.go new file mode 100644 index 00000000..472e5285 --- /dev/null +++ b/internal/rpc/public_app_links_test.go @@ -0,0 +1,32 @@ +package rpc + +import ( + "testing" + + "github.com/iamxvbaba/td/clock" + "go.uber.org/zap" +) + +func TestRouterPublicAppLinkUsesConfiguredBaseAndLegacyDefault(t *testing.T) { + legacy := New(Config{}, Deps{}, zap.NewNop(), clock.System) + if got, want := legacy.publicAppLink("business-bot"), "telesrv://business-bot"; got != want { + t.Fatalf("legacy business bot link = %q, want %q", got, want) + } + + hosted := New(Config{ + PublicAppScheme: "telesrv", + PublicAppLinkBase: "owpg://tenant.example.test", + }, Deps{}, zap.NewNop(), clock.System) + if got, want := hosted.publicAppLink("business-bot"), "owpg://tenant.example.test/business-bot"; got != want { + t.Fatalf("hosted business bot link = %q, want %q", got, want) + } +} + +func TestRouterRejectsInvalidPublicAppLinkConfig(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("New did not fail fast for an invalid public app link base") + } + }() + _ = New(Config{PublicAppLinkBase: "owpg://tenant.example.test/root"}, Deps{}, zap.NewNop(), clock.System) +} diff --git a/internal/rpc/public_links.go b/internal/rpc/public_links.go index 6fb0e808..3207ddc1 100644 --- a/internal/rpc/public_links.go +++ b/internal/rpc/public_links.go @@ -22,6 +22,10 @@ func (r *Router) publicLinkHost() string { return links.Host(r.cfg.PublicBaseURL) } +func (r *Router) publicAppLink(route string) string { + return r.appLinks.Build(route, nil) +} + func publicLinkWithBaseURL(baseURL, path string) string { return links.Build(baseURL, path, nil) } diff --git a/internal/rpc/router.go b/internal/rpc/router.go index 212fc3b1..0575646f 100644 --- a/internal/rpc/router.go +++ b/internal/rpc/router.go @@ -20,6 +20,7 @@ import ( "github.com/iamxvbaba/td/tlprofile" compatandroid "telesrv/internal/compat/android" "telesrv/internal/domain" + "telesrv/internal/links" "telesrv/internal/observability/dbtrace" ) @@ -83,6 +84,10 @@ type Config struct { RtmpIngestURL string // PublicBaseURL 是所有客户端可见 telesrv 链接的公开根 URL。 PublicBaseURL string + // PublicAppScheme/PublicAppLinkBase 控制客户端 deep link;base 为空时 + // 保持 ://,非空时生成 /。 + PublicAppScheme string + PublicAppLinkBase string // TempKeyResolveCacheTTL 是 PFS temp→perm auth key 解析的进程内缓存有效期。>0 时同一 temp key // 在 TTL 内复用上次解析、跳过每帧 ResolveAuthKey 的 PG 查询;0(默认/测试)关闭=每帧重校验。 // 显式撤销会删除协议 auth key、清缓存并断开活跃连接;TTL 只影响自然过期或异常路径下的 @@ -99,6 +104,7 @@ type Config struct { // 剥离 invokeWithLayer / initConnection / invokeWithoutUpdates / invokeAfter*,并兜底未注册 RPC。 type Router struct { cfg Config + appLinks links.AppLinkBuilder log *zap.Logger clock clock.Clock deps Deps @@ -236,11 +242,15 @@ type authUserCacheEntry struct { // New 创建 Router,由各业务域自行注册其 RPC handler(registerHelp/Auth/Users/Updates)。 func New(cfg Config, deps Deps, log *zap.Logger, clk clock.Clock) *Router { assertNoTypedNilDeps(deps) + appLinks, err := links.NewAppLinkBuilder(cfg.PublicAppScheme, cfg.PublicAppLinkBase) + if err != nil { + panic(fmt.Sprintf("initialize public app links: %v", err)) + } instanceID := cfg.InstanceID if instanceID == "" { instanceID = fmt.Sprintf("%016x", randomNonZeroInt64()) } - r := &Router{cfg: cfg, log: log, clock: clk, deps: deps, exactProfiles: make(map[clientInfoSessionKey]exactSessionProfileEntry), authLayerEvidence: make(map[[8]byte]authLayerDefaultEvidence), presence: newPresenceTracker(), callbacks: newCallbackRegistry(deps.BotCallbacks), inlines: newInlineRegistry(botInlineQueryTTL, deps.Inline), webviews: newWebViewRegistry(webViewSessionTTL, deps.Inline), loginTokens: newLoginTokenRegistry(), botAPIUpdates: newBotAPIUpdateNotifier(), tempKeyResolveCache: newTempKeyResolveCache(cfg.TempKeyResolveCacheMaxEntries), storyProjectionCache: newStoryProjectionCache(clk.Now), storyPinnedCache: newStoryPinnedAvailableCache(clk.Now), storyPinnedListCache: newStoryPinnedStoriesCache(clk.Now), channelFullBotCache: newChannelFullBotInfoCache(clk.Now), userFullProjectionCache: newUserFullProjectionCache(clk.Now), peerSettingsProjectionCache: newPeerSettingsProjectionCache(clk.Now), channelFullProjectionCache: newChannelFullProjectionCache(clk.Now), emojiStickers: newEmojiStickerIndex(clk.Now), notifySettings: newNotifySettingsCache(clk.Now), stickerCatalog: newStickerCatalogCache(clk.Now), accountSettings: newAccountSettingsCache(clk.Now), accountFreezeWake: make(chan struct{}, 1), instanceID: instanceID} + r := &Router{cfg: cfg, appLinks: appLinks, log: log, clock: clk, deps: deps, exactProfiles: make(map[clientInfoSessionKey]exactSessionProfileEntry), authLayerEvidence: make(map[[8]byte]authLayerDefaultEvidence), presence: newPresenceTracker(), callbacks: newCallbackRegistry(deps.BotCallbacks), inlines: newInlineRegistry(botInlineQueryTTL, deps.Inline), webviews: newWebViewRegistry(webViewSessionTTL, deps.Inline), loginTokens: newLoginTokenRegistry(), botAPIUpdates: newBotAPIUpdateNotifier(), tempKeyResolveCache: newTempKeyResolveCache(cfg.TempKeyResolveCacheMaxEntries), storyProjectionCache: newStoryProjectionCache(clk.Now), storyPinnedCache: newStoryPinnedAvailableCache(clk.Now), storyPinnedListCache: newStoryPinnedStoriesCache(clk.Now), channelFullBotCache: newChannelFullBotInfoCache(clk.Now), userFullProjectionCache: newUserFullProjectionCache(clk.Now), peerSettingsProjectionCache: newPeerSettingsProjectionCache(clk.Now), channelFullProjectionCache: newChannelFullProjectionCache(clk.Now), emojiStickers: newEmojiStickerIndex(clk.Now), notifySettings: newNotifySettingsCache(clk.Now), stickerCatalog: newStickerCatalogCache(clk.Now), accountSettings: newAccountSettingsCache(clk.Now), accountFreezeWake: make(chan struct{}, 1), instanceID: instanceID} r.channelFanout = newChannelFanoutDispatcher(r, defaultChannelFanoutShards, defaultChannelFanoutBuffer) r.botAPIEnqueueQueue = newBotAPIEnqueueDispatcher(log, defaultBotAPIEnqueueBuffer) r.webPageResolveSem = make(chan struct{}, webPageResolveConcurrency) diff --git a/internal/telegramloginhttp/handler_test.go b/internal/telegramloginhttp/handler_test.go index 9b7abb16..e5414c1d 100644 --- a/internal/telegramloginhttp/handler_test.go +++ b/internal/telegramloginhttp/handler_test.go @@ -65,6 +65,10 @@ func (telegramLoginHTTPDenyLimiter) Allow(context.Context, string, int, time.Dur } func newTelegramLoginHTTPFixture(t *testing.T) telegramLoginHTTPFixture { + return newTelegramLoginHTTPFixtureWithAppLinkBase(t, "") +} + +func newTelegramLoginHTTPFixtureWithAppLinkBase(t *testing.T, appLinkBase string) telegramLoginHTTPFixture { t.Helper() now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC) sealKey := make([]byte, 32) @@ -76,7 +80,7 @@ func newTelegramLoginHTTPFixture(t *testing.T) telegramLoginHTTPFixture { pepper := make([]byte, 32) pepper[0] = 2 service, err := loginapp.NewService(memory.NewTelegramLoginStore(nil), sealer, loginapp.Config{ - Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv", ClientSecretPepper: pepper, + Issuer: "https://oauth.telesrv.test", AppScheme: "telesrv", AppLinkBase: appLinkBase, ClientSecretPepper: pepper, Now: func() time.Time { return now }, }) if err != nil { @@ -162,6 +166,14 @@ func (f telegramLoginHTTPFixture) authorize(t *testing.T) (browserToken, deepLin return browserToken, deepLink } +func TestAuthorizationPageUsesConfiguredHostBasedAppLink(t *testing.T) { + f := newTelegramLoginHTTPFixtureWithAppLinkBase(t, "owpg://tenant.example.test") + _, deepLink := f.authorize(t) + if !strings.HasPrefix(deepLink, "owpg://tenant.example.test/oauth?token=") { + t.Fatalf("authorization page deep link = %q, want configured host-based OAuth URL", deepLink) + } +} + func TestAuthorizationErrorsUseOnlyPreRegisteredTargets(t *testing.T) { f := newTelegramLoginHTTPFixture(t) base := url.Values{ diff --git a/internal/web/server.go b/internal/web/server.go index f762135f..f16a5f82 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -24,6 +24,7 @@ type Config struct { Addr string PublicBaseURL string AppScheme string + AppLinkBase string WebBaseURL string AppName string StickerSets StickerSetResolver @@ -102,6 +103,7 @@ func Start(ctx context.Context, cfg Config, logger *zap.Logger) (*http.Server, e zap.String("addr", addr), zap.String("public_base_url", cfg.PublicBaseURL), zap.String("app_scheme", cfg.AppScheme), + zap.String("app_link_base", cfg.AppLinkBase), zap.String("web_base_url", cfg.WebBaseURL)) if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Warn("Public link Web endpoint exited", zap.Error(err)) @@ -134,8 +136,9 @@ func newHandler(cfg Config, logger *zap.Logger) (http.Handler, error) { if cfg.PublicBaseURL, err = links.ValidateBaseURL(cfg.PublicBaseURL); err != nil { return nil, fmt.Errorf("public base URL: %w", err) } - if cfg.AppScheme, err = links.ValidateAppScheme(cfg.AppScheme); err != nil { - return nil, fmt.Errorf("app scheme: %w", err) + appLinks, err := links.NewAppLinkBuilder(cfg.AppScheme, cfg.AppLinkBase) + if err != nil { + return nil, fmt.Errorf("app links: %w", err) } if cfg.WebBaseURL, err = links.ValidateBaseURL(cfg.WebBaseURL); err != nil { return nil, fmt.Errorf("Web base URL: %w", err) @@ -155,7 +158,7 @@ func newHandler(cfg Config, logger *zap.Logger) (http.Handler, error) { uniqueGifts: cfg.UniqueGifts, giftWithdrawals: cfg.GiftWithdrawals, publicBaseURL: cfg.PublicBaseURL, - appScheme: cfg.AppScheme, + appLinks: appLinks, webBaseURL: cfg.WebBaseURL, appName: cfg.AppName, logger: logger, @@ -195,7 +198,7 @@ type handler struct { uniqueGifts UniqueStarGiftResolver giftWithdrawals StarGiftWithdrawalResolver publicBaseURL string - appScheme string + appLinks links.AppLinkBuilder webBaseURL string appName string logger *zap.Logger @@ -376,8 +379,8 @@ func (h *handler) usernameLink(w http.ResponseWriter, r *http.Request) { h.serveUsernameNotFound(w, username) return } + app := h.appLinks.BuildUsername(peer.username, params) params.Set("domain", peer.username) - app := schemeURLValues(h.appScheme, "resolve", params) legacy := schemeURLValues("tg", "resolve", params) description := peer.about if description == "" { @@ -988,7 +991,7 @@ func itemNoun(set domain.StickerSet, count int) string { } func (h *handler) appURL(kind, key, value string) string { - return schemeURL(h.appScheme, kind, key, value) + return h.appLinks.Build(kind, url.Values{key: []string{value}}) } func legacyTgURL(kind, key, value string) string { diff --git a/internal/web/server_test.go b/internal/web/server_test.go index aca6a959..6e32f17b 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -344,6 +344,7 @@ func TestHandlerUsesConfiguredClientLinksAndBrand(t *testing.T) { }}, PublicBaseURL: "https://links.example.test", AppScheme: "example-chat", + AppLinkBase: "owpg://tenant.example.test", WebBaseURL: "https://web.example.test/client/", AppName: "Example Chat", }) @@ -357,7 +358,7 @@ func TestHandlerUsesConfiguredClientLinksAndBrand(t *testing.T) { } body := rr.Body.String() for _, want := range []string{ - "example-chat://resolve?domain=Alice&start=hello", + "owpg://tenant.example.test/Alice?start=hello", "https://web.example.test/client/#?tgaddr=", "Example Chat", "Open Example Chat to send a message to @Alice.", @@ -373,10 +374,10 @@ func TestHandlerUsesConfiguredClientLinksAndBrand(t *testing.T) { path string want string }{ - {path: "/addstickers/stickers_pack", want: "example-chat://addstickers?set=stickers_pack"}, - {path: "/addemoji/emoji_pack", want: "example-chat://addemoji?set=emoji_pack"}, - {path: "/addlist/shared-folder", want: "example-chat://addlist?slug=shared-folder"}, - {path: "/nft/gift-1", want: "example-chat://nft?slug=gift-1"}, + {path: "/addstickers/stickers_pack", want: "owpg://tenant.example.test/addstickers?set=stickers_pack"}, + {path: "/addemoji/emoji_pack", want: "owpg://tenant.example.test/addemoji?set=emoji_pack"}, + {path: "/addlist/shared-folder", want: "owpg://tenant.example.test/addlist?slug=shared-folder"}, + {path: "/nft/gift-1", want: "owpg://tenant.example.test/nft?slug=gift-1"}, } { rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, tc.path, nil)) @@ -393,6 +394,8 @@ func TestNewHandlerRejectsInvalidClientLinkConfig(t *testing.T) { }{ {name: "missing sticker resolver", cfg: Config{}}, {name: "official scheme", cfg: Config{StickerSets: fakeResolver{}, AppScheme: "tg"}}, + {name: "official app link base", cfg: Config{StickerSets: fakeResolver{}, AppLinkBase: "tg://links.example.test"}}, + {name: "app link base path", cfg: Config{StickerSets: fakeResolver{}, AppLinkBase: "owpg://links.example.test/root"}}, {name: "invalid Web base URL", cfg: Config{StickerSets: fakeResolver{}, WebBaseURL: "file:///tmp/web"}}, {name: "invalid app name", cfg: Config{StickerSets: fakeResolver{}, AppName: "bad\nname"}}, } { From 045e0f6db4ef86be67675b988aef8383d4d5eeeb Mon Sep 17 00:00:00 2001 From: A Date: Wed, 22 Jul 2026 21:44:33 +0800 Subject: [PATCH 21/28] fix: sync StarGift prepaid upgrade message refs --- ...35_star_gift_prepaid_message_refs.down.sql | 4 + ...0135_star_gift_prepaid_message_refs.up.sql | 304 ++++++++++++++ internal/store/postgres/star_gift.go | 3 +- .../store/postgres/star_gift_entitlements.go | 8 +- .../star_gift_lifecycle_integration_test.go | 189 ++++++++- ...ft_lifecycle_migration_integration_test.go | 4 +- internal/store/postgres/star_gift_upgrade.go | 377 ++++++++++++------ .../postgres/star_gift_user_message_ref.go | 13 +- 8 files changed, 754 insertions(+), 148 deletions(-) create mode 100644 deploy/migrations/0135_star_gift_prepaid_message_refs.down.sql create mode 100644 deploy/migrations/0135_star_gift_prepaid_message_refs.up.sql diff --git a/deploy/migrations/0135_star_gift_prepaid_message_refs.down.sql b/deploy/migrations/0135_star_gift_prepaid_message_refs.down.sql new file mode 100644 index 00000000..735e157c --- /dev/null +++ b/deploy/migrations/0135_star_gift_prepaid_message_refs.down.sql @@ -0,0 +1,4 @@ +-- The up migration registers protocol identities and emits durable per-user +-- edit_message events. Removing aliases, reverting snapshots or rewinding pts +-- would invalidate messages already consumed by clients and create holes in +-- updates.getDifference, so rollback intentionally preserves the repair. diff --git a/deploy/migrations/0135_star_gift_prepaid_message_refs.up.sql b/deploy/migrations/0135_star_gift_prepaid_message_refs.up.sql new file mode 100644 index 00000000..82cb120c --- /dev/null +++ b/deploy/migrations/0135_star_gift_prepaid_message_refs.up.sql @@ -0,0 +1,304 @@ +-- A separate prepaid-upgrade service message is another owner-local entry to +-- the same saved-gift aggregate. Earlier writes persisted gift_msg_id in the +-- receiver projection but did not register that message id, so clients that +-- submitted the visible card id received STARGIFT_INVALID. If the gift was +-- upgraded through the original id, the prepaid card also remained actionable. +-- +-- Repair aliases and already-upgraded projections atomically. Durable edit +-- events make history, online delivery and updates.getDifference converge on +-- the same non-actionable snapshot. Invalid persisted shapes fail the migration +-- instead of being normalized by a read path. + +LOCK TABLE public.peer_star_gifts, public.star_gift_user_message_refs, + public.message_boxes, public.private_messages IN SHARE ROW EXCLUSIVE MODE; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM public.message_boxes box + WHERE NOT box.deleted + AND box.media #>> '{service_action,kind}' = 'star_gift' + AND box.media #>> '{service_action,star_gift,prepaid_upgrade}' = 'true' + AND box.media #>> '{service_action,star_gift,upgrade_separate}' = 'true' + AND ( + jsonb_typeof(box.media #> '{service_action,star_gift,gift_id}') IS DISTINCT FROM 'number' + OR COALESCE(box.media #>> '{service_action,star_gift,gift_id}', '') !~ '^[0-9]+$' + OR (box.media #>> '{service_action,star_gift,gift_id}')::numeric <= 0 + OR (box.media #>> '{service_action,star_gift,gift_id}')::numeric > 9223372036854775807 + ) + ) THEN + RAISE EXCEPTION 'separate prepaid star gift message has malformed gift_id'; + END IF; + + -- gift_msg_id is receiver-only, so absence is valid on the payer box. If + -- present it must be a positive protocol int32 message id. + IF EXISTS ( + SELECT 1 + FROM public.message_boxes box + WHERE NOT box.deleted + AND box.media #>> '{service_action,kind}' = 'star_gift' + AND box.media #>> '{service_action,star_gift,prepaid_upgrade}' = 'true' + AND box.media #>> '{service_action,star_gift,upgrade_separate}' = 'true' + AND box.media #> '{service_action,star_gift,gift_msg_id}' IS NOT NULL + AND ( + jsonb_typeof(box.media #> '{service_action,star_gift,gift_msg_id}') <> 'number' + OR COALESCE(box.media #>> '{service_action,star_gift,gift_msg_id}', '') !~ '^[0-9]+$' + OR (box.media #>> '{service_action,star_gift,gift_msg_id}')::numeric <= 0 + OR (box.media #>> '{service_action,star_gift,gift_msg_id}')::numeric > 2147483647 + ) + ) THEN + RAISE EXCEPTION 'separate prepaid star gift message has malformed gift_msg_id'; + END IF; +END +$$; + +CREATE TEMP TABLE star_gift_prepaid_message_aliases ON COMMIT DROP AS +SELECT DISTINCT owner_box.owner_user_id, + owner_box.box_id, + gift.id AS saved_gift_id, + owner_box.message_sender_id, + owner_box.private_message_id +FROM public.message_boxes owner_box +JOIN public.peer_star_gifts gift + ON gift.owner_peer_type = 'user' + AND gift.owner_peer_id = owner_box.owner_user_id + AND gift.lifecycle_status = 'active' + AND gift.msg_id = (owner_box.media #>> '{service_action,star_gift,gift_msg_id}')::integer + AND gift.gift_id = (owner_box.media #>> '{service_action,star_gift,gift_id}')::bigint +WHERE NOT owner_box.deleted + AND owner_box.media #>> '{service_action,kind}' = 'star_gift' + AND owner_box.media #>> '{service_action,star_gift,prepaid_upgrade}' = 'true' + AND owner_box.media #>> '{service_action,star_gift,upgrade_separate}' = 'true' + AND owner_box.media #> '{service_action,star_gift,gift_msg_id}' IS NOT NULL; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM star_gift_prepaid_message_aliases + GROUP BY owner_user_id, box_id + HAVING COUNT(DISTINCT saved_gift_id) <> 1 + ) THEN + RAISE EXCEPTION 'separate prepaid star gift message resolves to multiple aggregates'; + END IF; + + IF EXISTS ( + SELECT 1 + FROM star_gift_prepaid_message_aliases alias + JOIN public.star_gift_user_message_refs ref + ON ref.owner_user_id = alias.owner_user_id + AND ref.msg_id = alias.box_id + WHERE ref.saved_gift_id <> alias.saved_gift_id + ) THEN + RAISE EXCEPTION 'separate prepaid star gift message collides with another aggregate'; + END IF; + + -- Both boxes of the logical private message must retain the same prepayment + -- identity. The receiver-only gift_msg_id may differ by design. + IF EXISTS ( + SELECT 1 + FROM star_gift_prepaid_message_aliases alias + JOIN public.peer_star_gifts gift ON gift.id = alias.saved_gift_id + JOIN public.message_boxes visible_box + ON visible_box.message_sender_id = alias.message_sender_id + AND visible_box.private_message_id = alias.private_message_id + AND NOT visible_box.deleted + WHERE visible_box.media #>> '{service_action,kind}' IS DISTINCT FROM 'star_gift' + OR visible_box.media #>> '{service_action,star_gift,prepaid_upgrade}' IS DISTINCT FROM 'true' + OR visible_box.media #>> '{service_action,star_gift,upgrade_separate}' IS DISTINCT FROM 'true' + OR visible_box.media #>> '{service_action,star_gift,gift_id}' IS DISTINCT FROM gift.gift_id::text + ) THEN + RAISE EXCEPTION 'separate prepaid star gift private projections disagree'; + END IF; +END +$$; + +CREATE UNIQUE INDEX star_gift_prepaid_message_aliases_owner_msg_idx + ON star_gift_prepaid_message_aliases(owner_user_id, box_id); + +INSERT INTO public.star_gift_user_message_refs(owner_user_id, msg_id, saved_gift_id) +SELECT owner_user_id, box_id, saved_gift_id +FROM star_gift_prepaid_message_aliases +ON CONFLICT (owner_user_id, msg_id) DO UPDATE +SET saved_gift_id = EXCLUDED.saved_gift_id +WHERE star_gift_user_message_refs.saved_gift_id = EXCLUDED.saved_gift_id; + +COMMENT ON TABLE public.star_gift_user_message_refs IS + 'Owner-local service-message aliases (unique outputs and separate prepaid-upgrade notifications) for one saved gift aggregate.'; + +CREATE TEMP TABLE star_gift_prepaid_message_repairs ( + owner_user_id bigint NOT NULL, + box_id integer NOT NULL, + peer_type text NOT NULL, + peer_id bigint NOT NULL, + message_sender_id bigint NOT NULL, + private_message_id bigint NOT NULL, + repaired_media jsonb NOT NULL, + PRIMARY KEY (owner_user_id, box_id) +) ON COMMIT DROP; + +-- Upgrade every visible copy of an already-consumed prepayment. A viewer gets +-- upgrade_msg_id only when that same viewer owns a box for the emitted unique +-- action. This covers the original sender while avoiding an owner-local link +-- on an unrelated third-party payer's card. +INSERT INTO star_gift_prepaid_message_repairs( + owner_user_id, box_id, peer_type, peer_id, + message_sender_id, private_message_id, repaired_media +) +SELECT visible_box.owner_user_id, + visible_box.box_id, + visible_box.peer_type, + visible_box.peer_id, + visible_box.message_sender_id, + visible_box.private_message_id, + CASE + WHEN unique_box.box_id IS NULL THEN + visible_box.media + #- '{service_action,star_gift,can_upgrade}' + #- '{service_action,star_gift,prepaid_upgrade_hash}' + #- '{service_action,star_gift,upgrade_msg_id}' + ELSE jsonb_set( + visible_box.media + #- '{service_action,star_gift,can_upgrade}' + #- '{service_action,star_gift,prepaid_upgrade_hash}', + '{service_action,star_gift,upgrade_msg_id}', + to_jsonb(unique_box.box_id::bigint), + true + ) + END +FROM star_gift_prepaid_message_aliases alias +JOIN public.peer_star_gifts gift + ON gift.id = alias.saved_gift_id + AND gift.lifecycle_status = 'active' + AND gift.unique_gift_id IS NOT NULL + AND gift.upgrade_msg_id > 0 +JOIN public.message_boxes owner_unique_box + ON owner_unique_box.owner_user_id = gift.owner_peer_id + AND owner_unique_box.box_id = gift.upgrade_msg_id + AND NOT owner_unique_box.deleted + AND owner_unique_box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND owner_unique_box.media #>> '{service_action,star_gift_unique,gift,ID}' = gift.unique_gift_id::text +JOIN public.message_boxes visible_box + ON visible_box.message_sender_id = alias.message_sender_id + AND visible_box.private_message_id = alias.private_message_id + AND NOT visible_box.deleted +LEFT JOIN public.message_boxes unique_box + ON unique_box.owner_user_id = visible_box.owner_user_id + AND unique_box.message_sender_id = owner_unique_box.message_sender_id + AND unique_box.private_message_id = owner_unique_box.private_message_id + AND NOT unique_box.deleted + AND unique_box.media #>> '{service_action,kind}' = 'star_gift_unique' + AND unique_box.media #>> '{service_action,star_gift_unique,gift,ID}' = gift.unique_gift_id::text; + +DO $$ +DECLARE + repair_row record; + next_pts integer; + event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer; +BEGIN + IF EXISTS ( + SELECT 1 + FROM star_gift_prepaid_message_aliases alias + JOIN public.peer_star_gifts gift + ON gift.id = alias.saved_gift_id + AND gift.lifecycle_status = 'active' + AND gift.unique_gift_id IS NOT NULL + WHERE NOT EXISTS ( + SELECT 1 + FROM star_gift_prepaid_message_repairs target_repair + WHERE target_repair.owner_user_id = alias.owner_user_id + AND target_repair.box_id = alias.box_id + ) + ) THEN + RAISE EXCEPTION 'upgraded star gift is missing its prepaid message repair'; + END IF; + + FOR repair_row IN + SELECT owner_user_id, box_id, peer_type, peer_id, repaired_media + FROM star_gift_prepaid_message_repairs + ORDER BY owner_user_id, box_id + LOOP + INSERT INTO public.user_update_watermarks(user_id, contiguous_pts) + VALUES(repair_row.owner_user_id, 0) + ON CONFLICT(user_id) DO NOTHING; + + UPDATE public.user_update_watermarks + SET contiguous_pts = contiguous_pts + 1, + updated_at = now() + WHERE user_id = repair_row.owner_user_id + RETURNING contiguous_pts INTO next_pts; + + UPDATE public.message_boxes + SET media = repair_row.repaired_media, + pts = next_pts + WHERE owner_user_id = repair_row.owner_user_id + AND box_id = repair_row.box_id + AND NOT deleted; + + INSERT INTO public.user_update_events( + user_id, pts, pts_count, date, event_type, + message_box_id, peer_type, peer_id + ) VALUES ( + repair_row.owner_user_id, next_pts, 1, event_date, 'edit_message', + repair_row.box_id, repair_row.peer_type, repair_row.peer_id + ); + + INSERT INTO public.dispatch_outbox( + target_user_id, pts, event_type, + exclude_auth_key_id, exclude_session_id + ) VALUES(repair_row.owner_user_id, next_pts, 'edit_message', 0, 0); + END LOOP; +END +$$; + +-- private_messages is a shared logical envelope and cannot retain either +-- participant's box-local gift_msg_id or upgrade_msg_id. +WITH shared_repairs AS ( + SELECT DISTINCT ON (repair.message_sender_id, repair.private_message_id) + repair.message_sender_id, + repair.private_message_id, + repair.repaired_media + #- '{service_action,star_gift,saved_id}' + #- '{service_action,star_gift,gift_msg_id}' + #- '{service_action,star_gift,upgrade_msg_id}' AS shared_media + FROM star_gift_prepaid_message_repairs repair + ORDER BY repair.message_sender_id, + repair.private_message_id, + (repair.owner_user_id = repair.message_sender_id) DESC, + repair.owner_user_id +) +UPDATE public.private_messages private_message +SET media = repair.shared_media +FROM shared_repairs repair +WHERE private_message.sender_user_id = repair.message_sender_id + AND private_message.id = repair.private_message_id; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM star_gift_prepaid_message_aliases alias + LEFT JOIN public.star_gift_user_message_refs ref + ON ref.owner_user_id = alias.owner_user_id + AND ref.msg_id = alias.box_id + AND ref.saved_gift_id = alias.saved_gift_id + WHERE ref.saved_gift_id IS NULL + ) THEN + RAISE EXCEPTION 'separate prepaid star gift alias repair did not converge'; + END IF; + + IF EXISTS ( + SELECT 1 + FROM star_gift_prepaid_message_repairs repair + JOIN public.message_boxes box + ON box.owner_user_id = repair.owner_user_id + AND box.box_id = repair.box_id + WHERE box.media IS DISTINCT FROM repair.repaired_media + OR box.media #> '{service_action,star_gift,can_upgrade}' IS NOT NULL + OR box.media #> '{service_action,star_gift,prepaid_upgrade_hash}' IS NOT NULL + ) THEN + RAISE EXCEPTION 'upgraded prepaid star gift projection repair did not converge'; + END IF; +END +$$; diff --git a/internal/store/postgres/star_gift.go b/internal/store/postgres/star_gift.go index c488f6cd..87e69b61 100644 --- a/internal/store/postgres/star_gift.go +++ b/internal/store/postgres/star_gift.go @@ -655,7 +655,8 @@ LEFT JOIN unique_star_gifts u ON u.id=p.unique_gift_id CROSS JOIN LATERAL ( SELECT p.msg_id::bigint AS msg_id UNION ALL - SELECT r.msg_id::bigint FROM star_gift_user_message_refs r WHERE r.saved_gift_id=p.id + SELECT r.msg_id::bigint FROM star_gift_user_message_refs r + WHERE r.saved_gift_id=p.id AND r.owner_user_id=p.owner_peer_id ) ref WHERE p.owner_peer_type=$1 AND p.owner_peer_id=$2 AND p.lifecycle_status='active' AND (ref.msg_id=ANY($3::bigint[]) diff --git a/internal/store/postgres/star_gift_entitlements.go b/internal/store/postgres/star_gift_entitlements.go index dd261b1a..3d951201 100644 --- a/internal/store/postgres/star_gift_entitlements.go +++ b/internal/store/postgres/star_gift_entitlements.go @@ -140,8 +140,12 @@ VALUES($1,$2,$3,$4,$5,$6,$7)`, req.PayerUserID, req.CommandKey, locked.ID, req.F } return projectPrivateStarGiftSourceRef(ctx, tx, messageReq, result.Saved.Owner.ID, result.Saved.MsgID) }, after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { - if req.Owner.Type != domain.PeerTypeChannel { - return nil + if req.Owner.Type == domain.PeerTypeUser { + ownerMessageID := sent.RecipientMessage.ID + if sent.SenderMessage.OwnerUserID == req.Owner.ID { + ownerMessageID = sent.SenderMessage.ID + } + return registerUserStarGiftMessageRef(ctx, tx, req.Owner.ID, ownerMessageID, result.Saved.ID, 0) } action := messageReq.Media.ServiceAction.StarGift return NewChannelStore(tx).appendStarGiftAdminLogTx(ctx, tx, req.Owner.ID, req.PayerUserID, diff --git a/internal/store/postgres/star_gift_lifecycle_integration_test.go b/internal/store/postgres/star_gift_lifecycle_integration_test.go index d91cb45f..62437833 100644 --- a/internal/store/postgres/star_gift_lifecycle_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_integration_test.go @@ -7,6 +7,9 @@ import ( "testing" "time" + "github.com/jackc/pgx/v5/pgxpool" + + "telesrv/deploy" "telesrv/internal/domain" ) @@ -21,10 +24,11 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) { offerBuyer := createTestUser(t, ctx, users, "+1881"+suffix+"03", "OfferBuyer", "") resaleBuyer := createTestUser(t, ctx, users, "+1881"+suffix+"04", "ResaleBuyer", "") loser := createTestUser(t, ctx, users, "+1881"+suffix+"05", "AuctionLoser", "") + prepayPayer := createTestUser(t, ctx, users, "+1881"+suffix+"06", "PrepayPayer", "") ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID} stars := NewStarsStore(pool) - for _, user := range []domain.User{buyer, owner, offerBuyer, resaleBuyer, loser} { + for _, user := range []domain.User{buyer, owner, offerBuyer, resaleBuyer, loser, prepayPayer} { if _, _, err := stars.EnsureGrant(ctx, user.ID, 10000, now); err != nil { t.Fatalf("grant stars to %d: %v", user.ID, err) } @@ -100,10 +104,10 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) { t.Fatalf("prepaid target = %+v price %d err %v", target, price, err) } prepaid, err := lifecycle.PrepayStarGiftUpgrade(ctx, domain.StarGiftPrepaidUpgradeRequest{ - PayerUserID: buyer.ID, Owner: ownerPeer, Hash: purchased.Saved.PrepaidUpgradeHash, + PayerUserID: prepayPayer.ID, Owner: ownerPeer, Hash: purchased.Saved.PrepaidUpgradeHash, ChargeStars: 100, FormID: 11002, CommandKey: "prepay-" + suffix, Date: now + 1, }) - if err != nil || prepaid.Saved.PrepaidUpgradeStars != 100 || prepaid.Saved.PrepaidUpgradeHash != "" || prepaid.Balance.Balance != 9850 { + if err != nil || prepaid.Saved.PrepaidUpgradeStars != 100 || prepaid.Saved.PrepaidUpgradeHash != "" || prepaid.Balance.Balance != 9900 { t.Fatalf("prepay upgrade = %+v err %v", prepaid, err) } prepaySenderAction := prepaid.Send.SenderMessage.Media.ServiceAction.StarGift @@ -114,7 +118,7 @@ func TestStarGiftLifecycleAggregatePostgres(t *testing.T) { t.Fatalf("prepay gift_msg_id is not owner-only: sender=%+v owner=%+v purchase=%+v", prepaySenderAction, prepayOwnerAction, purchased.Send) } - prepaySenderDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, buyer.ID, prepaid.Send.SenderMessage.Pts-1, 1) + prepaySenderDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, prepayPayer.ID, prepaid.Send.SenderMessage.Pts-1, 1) if err != nil || len(prepaySenderDifference) != 1 || prepaySenderDifference[0].Message.Media == nil || prepaySenderDifference[0].Message.Media.ServiceAction == nil || prepaySenderDifference[0].Message.Media.ServiceAction.StarGift == nil || @@ -139,10 +143,26 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, prepaid.Send.RecipientMessa sharedPrepayMedia.ServiceAction.StarGift == nil || sharedPrepayMedia.ServiceAction.StarGift.GiftMsgID != 0 { t.Fatalf("shared prepay media retained account-local gift_msg_id: media=%+v err=%v", sharedPrepayMedia, err) } - upgraded, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ - UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: purchased.Saved.MsgID}, + if byPrepay, found, err := gifts.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: prepaid.Send.RecipientMessage.ID}); err != nil || !found || byPrepay.ID != purchased.Saved.ID { + t.Fatalf("prepay owner message ref = %+v found=%v err=%v", byPrepay, found, err) + } + var ownerPrepayAlias, payerPrepayAlias int + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND msg_id=$2 AND saved_gift_id=$3`, owner.ID, prepaid.Send.RecipientMessage.ID, purchased.Saved.ID).Scan(&ownerPrepayAlias); err != nil { + t.Fatalf("load owner prepay alias: %v", err) + } + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND msg_id=$2`, prepayPayer.ID, prepaid.Send.SenderMessage.ID).Scan(&payerPrepayAlias); err != nil { + t.Fatalf("load payer prepay alias: %v", err) + } + if ownerPrepayAlias != 1 || payerPrepayAlias != 0 { + t.Fatalf("prepay aliases owner=%d payer=%d, want owner-only", ownerPrepayAlias, payerPrepayAlias) + } + upgradeReq := domain.StarGiftUpgradeRequest{ + UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: prepaid.Send.RecipientMessage.ID}, RequirePrepaid: true, KeepOriginalDetails: true, CommandKey: "upgrade-" + suffix, Date: now + 2, - }) + } + upgraded, err := upgrades.UpgradeStarGift(ctx, upgradeReq) if err != nil { t.Fatalf("upgrade prepaid gift: %v", err) } @@ -198,7 +218,9 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, prepaid.Send.RecipientMessa } upgradeAction := upgraded.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique senderUpgradeAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique - ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID) + ownerSourceEdit := upgradedSourceEditForMessage(upgraded, owner.ID, purchased.Saved.MsgID) + ownerPrepayEdit := upgradedSourceEditForMessage(upgraded, owner.ID, prepaid.Send.RecipientMessage.ID) + payerPrepayEdit := upgradedSourceEditForMessage(upgraded, prepayPayer.ID, prepaid.Send.SenderMessage.ID) if upgradeAction == nil || upgradeAction.SavedID != 0 || upgradeAction.Peer.Type != "" || upgradeAction.Peer.ID != 0 || upgradeAction.CanCraftAt != now+2 || senderUpgradeAction == nil || senderUpgradeAction.SavedID != 0 || senderUpgradeAction.CanCraftAt != now+2 || @@ -208,6 +230,36 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, prepaid.Send.RecipientMessa ownerSourceEdit.Message.Media.ServiceAction.StarGift.CanUpgrade { t.Fatalf("upgrade message linkage = action %+v source edit %+v", upgradeAction, ownerSourceEdit) } + if ownerPrepayEdit.Event.Pts <= ownerSourceEdit.Event.Pts || ownerPrepayEdit.Message.Media == nil || + ownerPrepayEdit.Message.Media.ServiceAction == nil || ownerPrepayEdit.Message.Media.ServiceAction.StarGift == nil || + ownerPrepayEdit.Message.Media.ServiceAction.StarGift.CanUpgrade || + ownerPrepayEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != upgraded.Send.RecipientMessage.ID { + t.Fatalf("owner prepay card did not converge with upgrade: %+v", ownerPrepayEdit) + } + if payerPrepayEdit.Event.Pts <= prepaid.Send.SenderMessage.Pts || payerPrepayEdit.Message.Media == nil || + payerPrepayEdit.Message.Media.ServiceAction == nil || payerPrepayEdit.Message.Media.ServiceAction.StarGift == nil || + payerPrepayEdit.Message.Media.ServiceAction.StarGift.CanUpgrade || + payerPrepayEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != 0 { + t.Fatalf("third-party payer prepay card retained an owner action/link: %+v", payerPrepayEdit) + } + ownerUpgradeDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, owner.ID, upgraded.Send.RecipientMessage.Pts-1, 3) + if err != nil || len(ownerUpgradeDifference) != 3 || + ownerUpgradeDifference[0].Type != domain.UpdateEventNewMessage || + ownerUpgradeDifference[1].Type != domain.UpdateEventEditMessage || ownerUpgradeDifference[1].Message.ID != purchased.Saved.MsgID || + ownerUpgradeDifference[2].Type != domain.UpdateEventEditMessage || ownerUpgradeDifference[2].Message.ID != prepaid.Send.RecipientMessage.ID { + t.Fatalf("owner prepaid upgrade difference = %+v err=%v", ownerUpgradeDifference, err) + } + payerUpgradeDifference, err := NewUpdateEventStore(pool).ListAfter(ctx, prepayPayer.ID, prepaid.Send.SenderMessage.Pts, 1) + if err != nil || len(payerUpgradeDifference) != 1 || payerUpgradeDifference[0].Type != domain.UpdateEventEditMessage || + payerUpgradeDifference[0].Message.ID != prepaid.Send.SenderMessage.ID { + t.Fatalf("payer prepaid upgrade difference = %+v err=%v", payerUpgradeDifference, err) + } + replayedUpgrade, err := upgrades.UpgradeStarGift(ctx, upgradeReq) + if err != nil || !replayedUpgrade.Duplicate || replayedUpgrade.Unique.ID != upgraded.Unique.ID || + upgradedSourceEditForMessage(replayedUpgrade, owner.ID, purchased.Saved.MsgID).Event.Pts != ownerSourceEdit.Event.Pts || + upgradedSourceEditForMessage(replayedUpgrade, owner.ID, prepaid.Send.RecipientMessage.ID).Event.Pts != ownerPrepayEdit.Event.Pts { + t.Fatalf("replay prepaid upgrade from notification = %+v err=%v", replayedUpgrade, err) + } var sharedUpgradeSourceMediaJSON string if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id @@ -221,6 +273,23 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, purchased.Saved.MsgID).Scan sharedUpgradeSourceMedia.ServiceAction.StarGift.GiftMsgID != 0 { t.Fatalf("shared upgraded source media retained account-local message id: media=%+v err=%v", sharedUpgradeSourceMedia, err) } + var sharedUpgradedPrepayMediaJSON string + if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p +JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id +WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, prepaid.Send.RecipientMessage.ID).Scan(&sharedUpgradedPrepayMediaJSON); err != nil { + t.Fatalf("load shared upgraded prepay media: %v", err) + } + sharedUpgradedPrepayMedia, err := decodeMessageMedia(sharedUpgradedPrepayMediaJSON) + if err != nil || sharedUpgradedPrepayMedia == nil || sharedUpgradedPrepayMedia.ServiceAction == nil || + sharedUpgradedPrepayMedia.ServiceAction.StarGift == nil || + sharedUpgradedPrepayMedia.ServiceAction.StarGift.CanUpgrade || + sharedUpgradedPrepayMedia.ServiceAction.StarGift.PrepaidUpgradeHash != "" || + sharedUpgradedPrepayMedia.ServiceAction.StarGift.UpgradeMsgID != 0 || + sharedUpgradedPrepayMedia.ServiceAction.StarGift.GiftMsgID != 0 { + t.Fatalf("shared upgraded prepay media retained an action or account-local id: media=%+v err=%v", sharedUpgradedPrepayMedia, err) + } + verifyPrepaidMessageRefMigration(t, ctx, pool, purchased.Saved.ID, owner.ID, prepayPayer.ID, + prepaid.Send.RecipientMessage.ID, prepaid.Send.SenderMessage.ID, upgraded.Send.RecipientMessage.ID) var sharedUpgradeMediaJSON string if err := pool.QueryRow(ctx, `SELECT p.media::text FROM private_messages p JOIN message_boxes b ON b.message_sender_id=p.sender_user_id AND b.private_message_id=p.id @@ -361,6 +430,16 @@ WHERE b.owner_user_id=$1 AND b.box_id=$2`, owner.ID, upgraded.Send.RecipientMess if err != nil || transferred.Unique.Owner != ownerPeer || transferred.Saved.TransferStars != 25 || transferred.Balance.Balance != 9975 { t.Fatalf("paid transfer = %+v err %v", transferred, err) } + const historicalOwnerMessageID = 2_147_483_000 + if _, err := pool.Exec(ctx, `INSERT INTO star_gift_user_message_refs(owner_user_id,msg_id,saved_gift_id) +VALUES($1,$2,$3)`, resaleBuyer.ID, historicalOwnerMessageID, transferred.Saved.ID); err != nil { + t.Fatalf("insert historical old-owner message ref: %v", err) + } + if _, err := gifts.ResolveSavedIDs(ctx, ownerPeer, []domain.SavedStarGiftRef{{ + Owner: ownerPeer, MsgID: historicalOwnerMessageID, + }}); !errors.Is(err, domain.ErrStarGiftNotFound) { + t.Fatalf("current owner resolved another owner's historical message ref: %v", err) + } var retiredSourceMediaJSON string var retiredSourcePTS int if err := pool.QueryRow(ctx, `SELECT media::text,pts FROM message_boxes @@ -1215,6 +1294,100 @@ func issueLifecyclePurchaseForm(t *testing.T, ctx context.Context, lifecycle *St return req } +func upgradedSourceEditForMessage(result domain.StarGiftUpgradeResult, userID int64, messageID int) domain.EditedMessageForUser { + for _, edit := range result.SourceEdits { + if edit.UserID == userID && edit.Message.ID == messageID { + return edit + } + } + return domain.EditedMessageForUser{UserID: userID} +} + +func verifyPrepaidMessageRefMigration( + t *testing.T, + ctx context.Context, + pool *pgxpool.Pool, + savedGiftID int64, + ownerUserID int64, + payerUserID int64, + ownerPrepayMessageID int, + payerPrepayMessageID int, + ownerUpgradeMessageID int, +) { + t.Helper() + tx, err := pool.Begin(ctx) + if err != nil { + t.Fatalf("begin prepaid message migration probe: %v", err) + } + defer func() { _ = tx.Rollback(context.Background()) }() + + var messageSenderID, privateMessageID int64 + if err := tx.QueryRow(ctx, `SELECT message_sender_id,private_message_id FROM message_boxes +WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, ownerUserID, ownerPrepayMessageID). + Scan(&messageSenderID, &privateMessageID); err != nil { + t.Fatalf("load prepaid message root for migration probe: %v", err) + } + if _, err := tx.Exec(ctx, `DELETE FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND msg_id=$2 AND saved_gift_id=$3`, ownerUserID, ownerPrepayMessageID, savedGiftID); err != nil { + t.Fatalf("remove prepaid alias for migration probe: %v", err) + } + if _, err := tx.Exec(ctx, `UPDATE message_boxes +SET media=jsonb_set(media #- '{service_action,star_gift,upgrade_msg_id}', + '{service_action,star_gift,can_upgrade}','true'::jsonb,true) +WHERE message_sender_id=$1 AND private_message_id=$2 AND NOT deleted`, messageSenderID, privateMessageID); err != nil { + t.Fatalf("restore stale prepaid message boxes for migration probe: %v", err) + } + if _, err := tx.Exec(ctx, `UPDATE private_messages +SET media=jsonb_set(media #- '{service_action,star_gift,upgrade_msg_id}', + '{service_action,star_gift,can_upgrade}','true'::jsonb,true) +WHERE sender_user_id=$1 AND id=$2`, messageSenderID, privateMessageID); err != nil { + t.Fatalf("restore stale shared prepaid message for migration probe: %v", err) + } + + migrationSQL, err := deploy.Migrations.ReadFile("migrations/0135_star_gift_prepaid_message_refs.up.sql") + if err != nil { + t.Fatalf("read prepaid message migration: %v", err) + } + if _, err := tx.Exec(ctx, string(migrationSQL)); err != nil { + t.Fatalf("apply prepaid message migration probe: %v", err) + } + + var aliasCount int + if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND msg_id=$2 AND saved_gift_id=$3`, ownerUserID, ownerPrepayMessageID, savedGiftID).Scan(&aliasCount); err != nil || aliasCount != 1 { + t.Fatalf("migrated prepaid alias count=%d err=%v", aliasCount, err) + } + assertMigratedAction := func(userID int64, messageID int, wantUpgradeMessageID int) { + t.Helper() + var mediaJSON string + var pts int + if err := tx.QueryRow(ctx, `SELECT media::text,pts FROM message_boxes +WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, userID, messageID).Scan(&mediaJSON, &pts); err != nil { + t.Fatalf("load migrated prepaid box %d/%d: %v", userID, messageID, err) + } + media, err := decodeMessageMedia(mediaJSON) + if err != nil || media == nil || media.ServiceAction == nil || media.ServiceAction.StarGift == nil || + media.ServiceAction.StarGift.CanUpgrade || media.ServiceAction.StarGift.PrepaidUpgradeHash != "" || + media.ServiceAction.StarGift.UpgradeMsgID != wantUpgradeMessageID { + t.Fatalf("migrated prepaid box %d/%d = %+v err=%v", userID, messageID, media, err) + } + var eventCount, outboxCount int + if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM user_update_events +WHERE user_id=$1 AND pts=$2 AND event_type='edit_message' AND message_box_id=$3`, userID, pts, messageID).Scan(&eventCount); err != nil { + t.Fatalf("load migrated prepaid event: %v", err) + } + if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM dispatch_outbox +WHERE target_user_id=$1 AND pts=$2 AND event_type='edit_message'`, userID, pts).Scan(&outboxCount); err != nil { + t.Fatalf("load migrated prepaid outbox: %v", err) + } + if eventCount != 1 || outboxCount != 1 { + t.Fatalf("migrated prepaid event/outbox counts=%d/%d", eventCount, outboxCount) + } + } + assertMigratedAction(ownerUserID, ownerPrepayMessageID, ownerUpgradeMessageID) + assertMigratedAction(payerUserID, payerPrepayMessageID, 0) +} + func craftedSourceEditForUserAndGift(result domain.StarGiftCraftResult, userID, uniqueGiftID int64) domain.EditedMessageForUser { for _, edit := range result.SourceEdits { if edit.UserID != userID { diff --git a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go index 5000638f..ebcfe739 100644 --- a/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go +++ b/internal/store/postgres/star_gift_lifecycle_migration_integration_test.go @@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) { if err != nil { t.Fatalf("migrate star gift lifecycle schema: %v", err) } - if status.Dirty || status.Empty || status.Version != 134 { - t.Fatalf("migration status = %+v, want clean version 134", status) + if status.Dirty || status.Empty || status.Version != 135 { + t.Fatalf("migration status = %+v, want clean version 135", status) } } diff --git a/internal/store/postgres/star_gift_upgrade.go b/internal/store/postgres/star_gift_upgrade.go index b786e289..31ea25e9 100644 --- a/internal/store/postgres/star_gift_upgrade.go +++ b/internal/store/postgres/star_gift_upgrade.go @@ -341,11 +341,45 @@ func starGiftUpgradeUniqueAction(saved domain.SavedStarGift, unique domain.Uniqu } } -// markPrivateStarGiftSourceUpgradedTx rewrites both visible copies of the -// original gift service message in the same transaction that creates the -// unique gift message. upgrade_msg_id is box-local, so each owner projection -// must point at that owner's copy of the new service message. Every rewrite is -// a durable edit_message event with its own pts and outbox row. +func userStarGiftSourceMessageIDs(ctx context.Context, db interface { + Query(context.Context, string, ...any) (pgx.Rows, error) +}, saved domain.SavedStarGift) ([]int, error) { + if saved.Owner.Type != domain.PeerTypeUser || saved.Owner.ID <= 0 || saved.ID <= 0 || saved.MsgID <= 0 { + return nil, domain.ErrStarGiftCollectibleInvalid + } + messageIDs := []int{saved.MsgID} + rows, err := db.Query(ctx, ` +SELECT msg_id FROM star_gift_user_message_refs +WHERE owner_user_id=$1 AND saved_gift_id=$2 AND msg_id<>$3 +ORDER BY msg_id`, saved.Owner.ID, saved.ID, saved.MsgID) + if err != nil { + return nil, fmt.Errorf("list star gift source message refs: %w", err) + } + defer rows.Close() + for rows.Next() { + var msgID int + if err := rows.Scan(&msgID); err != nil { + return nil, fmt.Errorf("scan star gift source message ref: %w", err) + } + if msgID <= 0 { + return nil, fmt.Errorf("star gift source message ref has invalid id") + } + messageIDs = append(messageIDs, msgID) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate star gift source message refs: %w", err) + } + return messageIDs, nil +} + +// markPrivateStarGiftSourceUpgradedTx rewrites every ordinary gift projection +// owned by the source aggregate: the original gift message and each separately +// prepaid-upgrade notification. The two visible boxes of every logical private +// message are updated together. upgrade_msg_id is box-local and is set only +// when that viewer owns a box for the emitted unique-gift message; a third-party +// payer sees the prepayment become non-actionable without receiving an invalid +// owner-local link. Every rewrite is a durable edit_message event with its own +// pts and outbox row. func (s *StarGiftUpgradeStore) markPrivateStarGiftSourceUpgradedTx( ctx context.Context, tx pgx.Tx, @@ -356,30 +390,11 @@ func (s *StarGiftUpgradeStore) markPrivateStarGiftSourceUpgradedTx( if saved.Owner.Type != domain.PeerTypeUser || saved.Owner.ID != req.UserID || saved.MsgID <= 0 { return nil, domain.ErrStarGiftCollectibleInvalid } + messageIDs, err := userStarGiftSourceMessageIDs(ctx, tx, saved) + if err != nil { + return nil, err + } q := sqlcgen.New(tx) - target, err := q.GetMessageBoxForEdit(ctx, sqlcgen.GetMessageBoxForEditParams{ - OwnerUserID: req.UserID, - BoxID: int32(saved.MsgID), - PeerType: string(domain.PeerTypeUser), - PeerID: saved.FromUserID, - }) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, domain.ErrStarGiftCollectibleInvalid - } - return nil, fmt.Errorf("lock star gift source message: %w", err) - } - boxes, err := q.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{ - OwnerUserIds: privateMessageOwnerIDs(req.UserID, saved.FromUserID), - MessageSenderID: target.MessageSenderID, - PrivateMessageID: target.PrivateMessageID, - }) - if err != nil { - return nil, fmt.Errorf("list star gift source message boxes: %w", err) - } - if len(boxes) == 0 { - return nil, domain.ErrStarGiftCollectibleInvalid - } upgradeMessageIDs := make(map[int64]int, 2) if sent.SenderMessage.OwnerUserID > 0 && sent.SenderMessage.ID > 0 { upgradeMessageIDs[sent.SenderMessage.OwnerUserID] = sent.SenderMessage.ID @@ -387,87 +402,158 @@ func (s *StarGiftUpgradeStore) markPrivateStarGiftSourceUpgradedTx( if sent.RecipientMessage.OwnerUserID > 0 && sent.RecipientMessage.ID > 0 { upgradeMessageIDs[sent.RecipientMessage.OwnerUserID] = sent.RecipientMessage.ID } - edits := make([]domain.EditedMessageForUser, 0, len(boxes)) - var privateMediaJSON []byte - for _, box := range boxes { - upgradeMessageID := upgradeMessageIDs[box.OwnerUserID] - if upgradeMessageID <= 0 { - return nil, fmt.Errorf("upgrade service message missing box for user %d", box.OwnerUserID) + edits := make([]domain.EditedMessageForUser, 0, len(messageIDs)*2) + seenPrivateMessages := make(map[string]struct{}, len(messageIDs)) + primaryRewritten := false + for _, sourceMessageID := range messageIDs { + var peerType string + var peerID int64 + err := tx.QueryRow(ctx, ` +SELECT peer_type,peer_id FROM message_boxes +WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted +FOR UPDATE`, req.UserID, sourceMessageID).Scan(&peerType, &peerID) + if errors.Is(err, pgx.ErrNoRows) { + if sourceMessageID == saved.MsgID { + return nil, domain.ErrStarGiftCollectibleInvalid + } + continue } - media, err := decodeMessageMedia(box.MediaJson) if err != nil { - return nil, fmt.Errorf("decode star gift source media: %w", err) + return nil, fmt.Errorf("lock star gift source ref %d: %w", sourceMessageID, err) } - if media == nil || media.Kind != domain.MessageMediaKindService || media.ServiceAction == nil || - media.ServiceAction.Kind != domain.MessageServiceActionStarGift || media.ServiceAction.StarGift == nil { - return nil, fmt.Errorf("star gift source message %d has invalid media", box.BoxID) + if peerType != string(domain.PeerTypeUser) || peerID <= 0 { + return nil, fmt.Errorf("star gift source ref %d is not private", sourceMessageID) } - action := media.ServiceAction.StarGift - if action.UpgradeMsgID != 0 && action.UpgradeMsgID != upgradeMessageID { - return nil, fmt.Errorf("star gift source message %d has conflicting upgrade message %d", box.BoxID, action.UpgradeMsgID) - } - action.UpgradeMsgID = upgradeMessageID - action.CanUpgrade = false - mediaJSON, err := encodeMessageMedia(media) + target, err := q.GetMessageBoxForEdit(ctx, sqlcgen.GetMessageBoxForEditParams{ + OwnerUserID: req.UserID, BoxID: int32(sourceMessageID), PeerType: peerType, PeerID: peerID, + }) if err != nil { - return nil, fmt.Errorf("encode upgraded star gift source media: %w", err) + return nil, fmt.Errorf("load star gift source ref %d: %w", sourceMessageID, err) } - pts, err := s.messages.reservePts(ctx, tx, box.OwnerUserID) + ownerMedia, err := decodeMessageMedia(target.MediaJson) if err != nil { - return nil, fmt.Errorf("allocate star gift source edit pts: %w", err) + return nil, fmt.Errorf("decode star gift source ref %d: %w", sourceMessageID, err) } - tag, err := tx.Exec(ctx, ` + ownerAction := privateStarGiftAction(ownerMedia) + if ownerAction == nil { + // The newly emitted unique action is registered before source edits in + // the same transaction and belongs to the same aggregate, but is not a + // source projection to rewrite. + if privateStarGiftUniqueAction(ownerMedia) != nil { + continue + } + return nil, fmt.Errorf("star gift source ref %d has invalid media", sourceMessageID) + } + if ownerAction.GiftID != saved.GiftID { + return nil, fmt.Errorf("star gift source ref %d points to gift %d", sourceMessageID, ownerAction.GiftID) + } + if sourceMessageID != saved.MsgID && (!ownerAction.UpgradeSeparate || !ownerAction.PrepaidUpgrade || ownerAction.GiftMsgID != saved.MsgID) { + return nil, fmt.Errorf("star gift source ref %d is not a prepaid notification for message %d", sourceMessageID, saved.MsgID) + } + logicalKey := fmt.Sprintf("%d:%d", target.MessageSenderID, target.PrivateMessageID) + if _, duplicate := seenPrivateMessages[logicalKey]; duplicate { + continue + } + seenPrivateMessages[logicalKey] = struct{}{} + boxes, err := q.ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{ + OwnerUserIds: privateMessageOwnerIDs(req.UserID, peerID), MessageSenderID: target.MessageSenderID, + PrivateMessageID: target.PrivateMessageID, + }) + if err != nil { + return nil, fmt.Errorf("list star gift source ref %d boxes: %w", sourceMessageID, err) + } + if len(boxes) == 0 { + return nil, domain.ErrStarGiftCollectibleInvalid + } + var privateMediaJSON []byte + for _, box := range boxes { + media, err := decodeMessageMedia(box.MediaJson) + if err != nil { + return nil, fmt.Errorf("decode star gift source media: %w", err) + } + action := privateStarGiftAction(media) + if action == nil || action.GiftID != saved.GiftID { + return nil, fmt.Errorf("star gift source message %d has invalid media", box.BoxID) + } + upgradeMessageID := upgradeMessageIDs[box.OwnerUserID] + if action.UpgradeMsgID != 0 && upgradeMessageID > 0 && action.UpgradeMsgID != upgradeMessageID { + return nil, fmt.Errorf("star gift source message %d has conflicting upgrade message %d", box.BoxID, action.UpgradeMsgID) + } + if upgradeMessageID > 0 { + action.UpgradeMsgID = upgradeMessageID + } else { + if box.OwnerUserID == req.UserID { + return nil, fmt.Errorf("upgrade service message missing owner box") + } + action.UpgradeMsgID = 0 + } + action.CanUpgrade = false + action.PrepaidUpgradeHash = "" + mediaJSON, err := encodeMessageMedia(media) + if err != nil { + return nil, fmt.Errorf("encode upgraded star gift source media: %w", err) + } + pts, err := s.messages.reservePts(ctx, tx, box.OwnerUserID) + if err != nil { + return nil, fmt.Errorf("allocate star gift source edit pts: %w", err) + } + tag, err := tx.Exec(ctx, ` UPDATE message_boxes SET media=$3, pts=$4 WHERE owner_user_id=$1 AND box_id=$2 AND NOT deleted`, box.OwnerUserID, box.BoxID, mediaJSON, int32(pts)) - if err != nil { - return nil, fmt.Errorf("update star gift source message box: %w", err) - } - if tag.RowsAffected() != 1 { - return nil, fmt.Errorf("update star gift source message box lost row") - } - msg, err := messageFromVisibleBoxRow(box) - if err != nil { - return nil, err - } - msg.Media = media - msg.Pts = pts - if err := replaceMessageBoxMediaIndexTx(ctx, tx, msg.OwnerUserID, msg.Peer.ID, msg.ID, msg.Date, msg.Media, msg.Entities); err != nil { - return nil, err - } - event := domain.UpdateEvent{ - UserID: msg.OwnerUserID, Type: domain.UpdateEventEditMessage, - Pts: pts, PtsCount: 1, Date: req.Date, Message: msg, - } - if err := appendUserUpdateEvent(ctx, tx, q, msg.OwnerUserID, event); err != nil { - return nil, fmt.Errorf("append star gift source edit event: %w", err) - } - dispatchAuthKeyID := [8]byte{} - dispatchSessionID := int64(0) - if msg.OwnerUserID == req.UserID { - dispatchAuthKeyID = req.OriginAuthKeyID - dispatchSessionID = req.OriginSessionID - } - if err := enqueueDispatch(ctx, q, sqlcgen.EnqueueDispatchParams{ - TargetUserID: msg.OwnerUserID, Pts: int32(pts), EventType: string(domain.UpdateEventEditMessage), - ExcludeAuthKeyID: authKeyIDToInt64(dispatchAuthKeyID), ExcludeSessionID: dispatchSessionID, - }); err != nil { - return nil, fmt.Errorf("enqueue star gift source edit: %w", err) - } - if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 { - privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media) + if err != nil { + return nil, fmt.Errorf("update star gift source message box: %w", err) + } + if tag.RowsAffected() != 1 { + return nil, fmt.Errorf("update star gift source message box lost row") + } + msg, err := messageFromVisibleBoxRow(box) if err != nil { return nil, err } + msg.Media = media + msg.Pts = pts + if err := replaceMessageBoxMediaIndexTx(ctx, tx, msg.OwnerUserID, msg.Peer.ID, msg.ID, msg.Date, msg.Media, msg.Entities); err != nil { + return nil, err + } + event := domain.UpdateEvent{UserID: msg.OwnerUserID, Type: domain.UpdateEventEditMessage, + Pts: pts, PtsCount: 1, Date: req.Date, Message: msg} + if err := appendUserUpdateEvent(ctx, tx, q, msg.OwnerUserID, event); err != nil { + return nil, fmt.Errorf("append star gift source edit event: %w", err) + } + dispatchAuthKeyID := [8]byte{} + dispatchSessionID := int64(0) + if msg.OwnerUserID == req.UserID { + dispatchAuthKeyID = req.OriginAuthKeyID + dispatchSessionID = req.OriginSessionID + } + if err := enqueueDispatch(ctx, q, sqlcgen.EnqueueDispatchParams{ + TargetUserID: msg.OwnerUserID, Pts: int32(pts), EventType: string(domain.UpdateEventEditMessage), + ExcludeAuthKeyID: authKeyIDToInt64(dispatchAuthKeyID), ExcludeSessionID: dispatchSessionID, + }); err != nil { + return nil, fmt.Errorf("enqueue star gift source edit: %w", err) + } + if box.OwnerUserID == box.MessageSenderID || len(privateMediaJSON) == 0 { + privateMediaJSON, err = encodeSharedPrivateStarGiftMedia(media) + if err != nil { + return nil, err + } + } + edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event}) } - edits = append(edits, domain.EditedMessageForUser{UserID: msg.OwnerUserID, Message: msg, Event: event}) - } - if len(privateMediaJSON) == 0 { - return nil, fmt.Errorf("upgrade source message missing private media projection") - } - if _, err := tx.Exec(ctx, ` + if len(privateMediaJSON) == 0 { + return nil, fmt.Errorf("upgrade source message missing private media projection") + } + if _, err := tx.Exec(ctx, ` UPDATE private_messages SET media=$3 WHERE sender_user_id=$1 AND id=$2`, target.MessageSenderID, target.PrivateMessageID, privateMediaJSON); err != nil { - return nil, fmt.Errorf("update star gift source private message: %w", err) + return nil, fmt.Errorf("update star gift source private message: %w", err) + } + if sourceMessageID == saved.MsgID { + primaryRewritten = true + } + } + if !primaryRewritten { + return nil, domain.ErrStarGiftCollectibleInvalid } return edits, nil } @@ -678,46 +764,77 @@ func (s *StarGiftUpgradeStore) loadUpgradeSourceReplay(ctx context.Context, req if pts <= 0 || saved.MsgID <= 0 { return nil, domain.ErrStarGiftCollectibleInvalid } - var privateMessageID, messageSenderID int64 - err := s.db.QueryRow(ctx, ` -SELECT private_message_id,message_sender_id FROM message_boxes -WHERE owner_user_id=$1 AND box_id=$2 AND peer_type='user' AND peer_id=$3 AND NOT deleted`, - req.UserID, saved.MsgID, saved.FromUserID).Scan(&privateMessageID, &messageSenderID) - if errors.Is(err, pgx.ErrNoRows) { - // A later delete event is authoritative; replaying the old edit here - // would transiently resurrect the source message. - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("load star gift source replay message: %w", err) - } - boxes, err := sqlcgen.New(s.db).ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{ - OwnerUserIds: []int64{req.UserID}, MessageSenderID: messageSenderID, PrivateMessageID: privateMessageID, - }) - if err != nil { - return nil, fmt.Errorf("load star gift source replay box: %w", err) - } - if len(boxes) != 1 || int(boxes[0].BoxID) != saved.MsgID { - return nil, domain.ErrStarGiftCollectibleInvalid - } - var eventDate int - err = s.db.QueryRow(ctx, ` -SELECT date FROM user_update_events -WHERE user_id=$1 AND pts=$2 AND event_type='edit_message' AND message_box_id=$3`, - req.UserID, pts, saved.MsgID).Scan(&eventDate) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, domain.ErrStarGiftCollectibleInvalid - } - return nil, fmt.Errorf("load star gift source replay event: %w", err) - } - msg, err := messageFromVisibleBoxRow(boxes[0]) + messageIDs, err := userStarGiftSourceMessageIDs(ctx, s.db, saved) if err != nil { return nil, err } - msg.Pts = pts - event := domain.UpdateEvent{UserID: req.UserID, Type: domain.UpdateEventEditMessage, Pts: pts, PtsCount: 1, Date: eventDate, Message: msg} - return []domain.EditedMessageForUser{{UserID: req.UserID, Message: msg, Event: event}}, nil + edits := make([]domain.EditedMessageForUser, 0, len(messageIDs)) + for _, messageID := range messageIDs { + var privateMessageID, messageSenderID int64 + err := s.db.QueryRow(ctx, ` +SELECT private_message_id,message_sender_id FROM message_boxes +WHERE owner_user_id=$1 AND box_id=$2 AND peer_type='user' AND NOT deleted`, + req.UserID, messageID).Scan(&privateMessageID, &messageSenderID) + if errors.Is(err, pgx.ErrNoRows) { + // A later delete event is authoritative; replaying the old edit here + // would transiently resurrect that source projection. + continue + } + if err != nil { + return nil, fmt.Errorf("load star gift source replay message %d: %w", messageID, err) + } + boxes, err := sqlcgen.New(s.db).ListVisibleMessageBoxesByPrivateMessage(ctx, sqlcgen.ListVisibleMessageBoxesByPrivateMessageParams{ + OwnerUserIds: []int64{req.UserID}, MessageSenderID: messageSenderID, PrivateMessageID: privateMessageID, + }) + if err != nil { + return nil, fmt.Errorf("load star gift source replay box %d: %w", messageID, err) + } + if len(boxes) != 1 || int(boxes[0].BoxID) != messageID { + return nil, domain.ErrStarGiftCollectibleInvalid + } + media, err := decodeMessageMedia(boxes[0].MediaJson) + if err != nil { + return nil, fmt.Errorf("decode star gift source replay box %d: %w", messageID, err) + } + action := privateStarGiftAction(media) + if action == nil { + if privateStarGiftUniqueAction(media) != nil { + continue + } + return nil, fmt.Errorf("star gift source replay box %d has invalid media", messageID) + } + if action.GiftID != saved.GiftID || action.CanUpgrade || action.UpgradeMsgID != saved.UpgradeMsgID { + return nil, domain.ErrStarGiftCollectibleInvalid + } + var eventPts, eventDate int + if messageID == saved.MsgID { + eventPts = pts + err = s.db.QueryRow(ctx, ` +SELECT date FROM user_update_events +WHERE user_id=$1 AND pts=$2 AND event_type='edit_message' AND message_box_id=$3`, + req.UserID, eventPts, messageID).Scan(&eventDate) + } else { + err = s.db.QueryRow(ctx, ` +SELECT pts,date FROM user_update_events +WHERE user_id=$1 AND pts>=$2 AND event_type='edit_message' AND message_box_id=$3 +ORDER BY pts LIMIT 1`, req.UserID, pts, messageID).Scan(&eventPts, &eventDate) + } + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, domain.ErrStarGiftCollectibleInvalid + } + return nil, fmt.Errorf("load star gift source replay event %d: %w", messageID, err) + } + msg, err := messageFromVisibleBoxRow(boxes[0]) + if err != nil { + return nil, err + } + msg.Pts = eventPts + event := domain.UpdateEvent{UserID: req.UserID, Type: domain.UpdateEventEditMessage, + Pts: eventPts, PtsCount: 1, Date: eventDate, Message: msg} + edits = append(edits, domain.EditedMessageForUser{UserID: req.UserID, Message: msg, Event: event}) + } + return edits, nil } func (s *StarGiftUpgradeStore) StarGiftUpgradeReceipt(ctx context.Context, userID int64, commandKey string) (domain.StarGiftUpgradeReceipt, bool, error) { diff --git a/internal/store/postgres/star_gift_user_message_ref.go b/internal/store/postgres/star_gift_user_message_ref.go index b1a4da81..e69898ec 100644 --- a/internal/store/postgres/star_gift_user_message_ref.go +++ b/internal/store/postgres/star_gift_user_message_ref.go @@ -9,9 +9,11 @@ import ( // registerUserStarGiftMessageRef records an owner-scoped service-message alias // for a user-owned gift. Official clients may continue from a freshly emitted -// messageActionStarGiftUnique and pass that message id to a lifecycle RPC, -// while payments.getSavedStarGifts may still expose the original received gift -// message as the aggregate's primary msg_id. +// messageActionStarGiftUnique or a separate prepaid-upgrade notification and +// pass that message id to a lifecycle RPC, while payments.getSavedStarGifts may +// still expose the original received gift message as the aggregate's primary +// msg_id. expectedUniqueGiftID is zero for an ordinary gift and positive for a +// unique gift; the write boundary never aliases across lifecycle states. func registerUserStarGiftMessageRef( ctx context.Context, tx pgx.Tx, @@ -20,7 +22,7 @@ func registerUserStarGiftMessageRef( savedGiftID int64, uniqueGiftID int64, ) error { - if ownerUserID <= 0 || msgID <= 0 || savedGiftID <= 0 || uniqueGiftID <= 0 { + if ownerUserID <= 0 || msgID <= 0 || savedGiftID <= 0 || uniqueGiftID < 0 { return fmt.Errorf("register user star gift message ref: invalid identity") } tag, err := tx.Exec(ctx, ` @@ -28,7 +30,8 @@ INSERT INTO star_gift_user_message_refs(owner_user_id,msg_id,saved_gift_id) SELECT $1,$2,p.id FROM peer_star_gifts p WHERE p.id=$3 AND p.owner_peer_type='user' AND p.owner_peer_id=$1 - AND p.unique_gift_id=$4 AND p.lifecycle_status='active' + AND (($4::bigint=0 AND p.unique_gift_id IS NULL) OR ($4::bigint>0 AND p.unique_gift_id=$4::bigint)) + AND p.lifecycle_status='active' ON CONFLICT(owner_user_id,msg_id) DO UPDATE SET saved_gift_id=EXCLUDED.saved_gift_id WHERE star_gift_user_message_refs.saved_gift_id=EXCLUDED.saved_gift_id`, ownerUserID, msgID, savedGiftID, uniqueGiftID) From ad9d535edc8a773ab93164598a2cdb5f7f50bd10 Mon Sep 17 00:00:00 2001 From: epilepticseizureee Date: Wed, 22 Jul 2026 23:45:09 +0300 Subject: [PATCH 22/28] feat(admin-ui): softer pre-material styling and dark theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rebuild admin console styling on semantic CSS variables (light + dark palettes) - Soften palette, shadows and corner radii for a calmer pre-material look - Add theme provider with light/dark toggle in the topbar and login header - Respect prefers-color-scheme and persist choice, with anti-FOUC inline script - Polish Russian translations (yo letters, consistent 'Звёзды' currency, wording) - Rebuild dist bundle to match the updated source --- .../web/dist/assets/index-CqgHld2y.js | 9 + .../web/dist/assets/index-DHdrFM5j.css | 1 - .../web/dist/assets/index-DuOdm70q.css | 1 + .../web/dist/assets/index-Duge82ST.js | 9 - cmd/telesrv-admin/web/dist/index.html | 43 ++- cmd/telesrv-admin/web/index.html | 17 ++ .../web/src/components/Layout.tsx | 2 + cmd/telesrv-admin/web/src/i18n.tsx | 62 +++-- cmd/telesrv-admin/web/src/main.tsx | 9 +- cmd/telesrv-admin/web/src/pages/LoginPage.tsx | 2 + .../web/src/styles/01-foundation.css | 246 +++++++++++++++--- .../web/src/styles/02-pages-and-forms.css | 132 ++++++---- .../src/styles/03-entities-and-actions.css | 165 ++++++------ .../web/src/styles/04-modal-and-login.css | 48 ++-- cmd/telesrv-admin/web/src/theme.tsx | 106 ++++++++ 15 files changed, 601 insertions(+), 251 deletions(-) create mode 100644 cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.js delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-DHdrFM5j.css create mode 100644 cmd/telesrv-admin/web/dist/assets/index-DuOdm70q.css delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-Duge82ST.js create mode 100644 cmd/telesrv-admin/web/src/theme.tsx diff --git a/cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.js b/cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.js new file mode 100644 index 00000000..4d0101b7 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.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 V(e,t){var n=t.checked;return R({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function fe(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 pe(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function me(e,t){pe(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`)?ge(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ge(e,t.type,ce(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function he(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 ge(e,t,n){(t!==`number`||B(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var _e=Array.isArray;function ve(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Se.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function we(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Te={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},Ee=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(Te).forEach(function(e){Ee.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Te[t]=Te[e]})});function De(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||Te.hasOwnProperty(e)&&Te[e]?(``+t).trim():t+`px`}function Oe(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=De(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var ke=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 Ae(e,t){if(t){if(ke[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 je(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 Me=null;function Ne(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Pe=null,Fe=null,Ie=null;function Le(e){if(e=ji(e)){if(typeof Pe!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Ni(t),Pe(e.stateNode,e.type,t))}}function Re(e){Fe?Ie?Ie.push(e):Ie=[e]:Fe=e}function ze(){if(Fe){var e=Fe,t=Ie;if(Ie=Fe=null,Le(e),t)for(e=0;e>>=0,e===0?32:31-(_t(e)/vt|0)|0}var bt=64,xt=4194304;function St(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ct(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,o=n&268435455;if(o!==0){var s=o&~i;s===0?(a&=o,a!==0&&(r=St(a))):r=St(s)}else o=n&~i,o===0?a!==0&&(r=St(a)):r=St(o);if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&a&4194240))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function kt(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-gt(t),e[t]=n}function At(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Gn),Jn=` `,Yn=!1;function Xn(e,t){switch(e){case`keyup`:return Un.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Zn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Qn=!1;function $n(e,t){switch(e){case`compositionend`:return Zn(t);case`keypress`:return t.which===32?(Yn=!0,Jn):null;case`textInput`:return e=t.data,e===Jn&&Yn?null:e;default:return null}}function er(e,t){if(Qn)return e===`compositionend`||!Wn&&Xn(e,t)?(e=mn(),pn=fn=dn=null,Qn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=xr(n)}}function Cr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Cr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wr(){for(var e=window,t=B();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=B(e.document)}return t}function Tr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}function Er(e){var t=wr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Cr(n.ownerDocument.documentElement,n)){if(r!==null&&Tr(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),`selectionStart`in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=Sr(n,a);var o=Sr(n,r);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus==`function`&&n.focus(),n=0;n=document.documentMode,Or=null,kr=null,Ar=null,jr=!1;function Mr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;jr||Or==null||Or!==B(r)||(r=Or,`selectionStart`in r&&Tr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ar&&br(Ar,r)||(Ar=r,r=ii(kr,`onSelect`),0Fi||(e.current=Pi[Fi],Pi[Fi]=null,Fi--)}function Ri(e,t){Fi++,Pi[Fi]=e.current,e.current=t}var zi={},Bi=Ii(zi),Vi=Ii(!1),Hi=zi;function Ui(e,t){var n=e.type.contextTypes;if(!n)return zi;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Wi(e){return e=e.childContextTypes,e!=null}function Gi(){Li(Vi),Li(Bi)}function Ki(e,t,n){if(Bi.current!==zi)throw Error(r(168));Ri(Bi,t),Ri(Vi,n)}function qi(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,se(e)||`Unknown`,a));return R({},n,i)}function Ji(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||zi,Hi=Bi.current,Ri(Bi,e),Ri(Vi,Vi.current),!0}function Yi(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=qi(e,t,Hi),i.__reactInternalMemoizedMergedChildContext=e,Li(Vi),Li(Bi),Ri(Bi,e)):Li(Vi),Ri(Vi,n)}var Xi=null,Zi=!1,Qi=!1;function $i(e){Xi===null?Xi=[e]:Xi.push(e)}function ea(e){Zi=!0,$i(e)}function ta(){if(!Qi&&Xi!==null){Qi=!0;var e=0,t=X;try{var n=Xi;for(X=1;e>=o,i-=o,la=1<<32-gt(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),_a&&da(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),_a&&da(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return _a&&da(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),_a&&da(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===F&&ja(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=ka(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=ka(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case F:return l=i._init,_(e,r,l(i._payload),o)}if(_e(i))return h(e,r,i,o);if(ee(i))return g(e,r,i,o);Aa(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Na=Ma(!0),Pa=Ma(!1),Fa=Ii(null),Ia=null,La=null,Ra=null;function za(){Ra=La=Ia=null}function Ba(e){var t=Fa.current;Li(Fa),e._currentValue=t}function Va(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Ha(e,t){Ia=e,Ra=La=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Ms=!0),e.firstContext=null)}function Ua(e){var t=e._currentValue;if(Ra!==e)if(e={context:e,memoizedValue:t,next:null},La===null){if(Ia===null)throw Error(r(308));La=e,Ia.dependencies={lanes:0,firstContext:e}}else La=La.next=e;return t}var Wa=null;function Ga(e){Wa===null?Wa=[e]:Wa.push(e)}function Ka(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Ga(t)):(n.next=i.next,i.next=n),t.interleaved=n,qa(e,r)}function qa(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Ja=!1;function Ya(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Za(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Qa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,$&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,qa(e,n)}return i=r.interleaved,i===null?(t.next=t,Ga(r)):(t.next=i.next,i.next=t),r.interleaved=t,qa(e,n)}function $a(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}function eo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function to(e,t,n,r){var i=e.updateQueue;Ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=R({},d,f);break a;case 2:Ja=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function no(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=vo.transition;vo.transition={};try{e(!1),t()}finally{X=n,vo.transition=r}}function as(){return Mo().memoizedState}function os(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},cs(e))ls(t,n);else if(n=Ka(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),us(n,t,r)}}function ss(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(cs(e))ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Z(s,o)){var c=t.interleaved;c===null?(i.next=i,Ga(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ka(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),us(n,t,r))}}function cs(e){var t=e.alternate;return e===bo||t!==null&&t===bo}function ls(e,t){wo=Co=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function us(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}var ds={readContext:Ua,useCallback:Do,useContext:Do,useEffect:Do,useImperativeHandle:Do,useInsertionEffect:Do,useLayoutEffect:Do,useMemo:Do,useReducer:Do,useRef:Do,useState:Do,useDebugValue:Do,useDeferredValue:Do,useTransition:Do,useMutableSource:Do,useSyncExternalStore:Do,useId:Do,unstable_isNewReconciler:!1},fs={readContext:Ua,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:Ua,useEffect:Jo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Ko(4194308,4,Qo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ko(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ko(4,2,e,t)},useMemo:function(e,t){var n=jo();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=jo();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=os.bind(null,bo,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:Uo,useDebugValue:es,useDeferredValue:function(e){return jo().memoizedState=e},useTransition:function(){var e=Uo(!1),t=e[0];return e=is.bind(null,e[1]),jo().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=bo,a=jo();if(_a){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));yo&30||Ro(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,Jo(Bo.bind(null,i,o,e),[e]),i.flags|=2048,Wo(9,zo.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=jo(),t=Vc.identifierPrefix;if(_a){var n=ua,r=la;n=(r&~(1<<32-gt(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=To++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[wi]=t,e[Ti]=i,nc(e,t,!1,!1),t.stateNode=e;a:{switch(c=je(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*ot()-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=ot(),t.sibling=null,n=po.current,Ri(po,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(oc(t),t.subtreeFlags&6&&(t.flags|=8192)):oc(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function cc(e,t){switch(ma(t),t.tag){case 1:return Wi(t.type)&&Gi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return lo(),Li(Vi),Li(Bi),go(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return fo(t),null;case 13:if(Li(po),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ea()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Li(po),null;case 4:return lo(),null;case 10:return Ba(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var lc=!1,uc=!1,dc=typeof WeakSet==`function`?WeakSet:Set,Q=null;function fc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function pc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var mc=!1;function hc(e,t){if(fi=rn,e=wr(),Tr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(pi={focusedElem:e,selectionRange:n},rn=!1,Q=t;Q!==null;)if(t=Q,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Q=e;else for(;Q!==null;){t=Q;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:hs(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,Q=e;break}Q=t.return}return h=mc,mc=!1,h}function gc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&pc(t,n,a)}i=i.next}while(i!==r)}}function _c(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function vc(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function yc(e){var t=e.alternate;t!==null&&(e.alternate=null,yc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[wi],delete t[Ti],delete t[Di],delete t[Oi],delete t[ki])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function bc(e){return e.tag===5||e.tag===3||e.tag===4}function xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||bc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=di));else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}function Cc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Cc(e,t,n),e=e.sibling;e!==null;)Cc(e,t,n),e=e.sibling}var wc=null,Tc=!1;function Ec(e,t,n){for(n=n.child;n!==null;)Dc(e,t,n),n=n.sibling}function Dc(e,t,n){if(mt&&typeof mt.onCommitFiberUnmount==`function`)try{mt.onCommitFiberUnmount(pt,n)}catch{}switch(n.tag){case 5:uc||fc(n,t);case 6:var r=wc,i=Tc;wc=null,Ec(e,t,n),wc=r,Tc=i,wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):wc.removeChild(n.stateNode));break;case 18:wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?bi(e.parentNode,n):e.nodeType===1&&bi(e,n),tn(e)):bi(wc,n.stateNode));break;case 4:r=wc,i=Tc,wc=n.stateNode.containerInfo,Tc=!0,Ec(e,t,n),wc=r,Tc=i;break;case 0:case 11:case 14:case 15:if(!uc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&pc(n,t,o),i=i.next}while(i!==r)}Ec(e,t,n);break;case 1:if(!uc&&(fc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Ec(e,t,n);break;case 21:Ec(e,t,n);break;case 22:n.mode&1?(uc=(r=uc)||n.memoizedState!==null,Ec(e,t,n),uc=r):Ec(e,t,n);break;default:Ec(e,t,n)}}function Oc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new dc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function kc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=ot()-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;lot()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=xt,xt<<=1,!(xt&130023424)&&(xt=4194304)):t=1);var n=fl();e=qa(e,t),e!==null&&(kt(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Vi.current)Ms=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return Ms=!1,tc(e,t,n);Ms=!!(e.flags&131072)}else Ms=!1,_a&&t.flags&1048576&&fa(t,aa,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;$s(e,t),e=t.pendingProps;var a=Ui(t,Bi.current);Ha(t,n),a=ko(null,t,i,e,a,n);var o=Ao();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Wi(i)?(o=!0,Ji(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ya(t),a.updater=_s,t.stateNode=a,a._reactInternals=t,xs(t,i,e,n),t=Vs(null,t,i,!0,o,n)):(t.tag=0,_a&&o&&pa(t),Ns(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch($s(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=hs(i,e),a){case 0:t=zs(null,t,i,e,n);break a;case 1:t=Bs(null,t,i,e,n);break a;case 11:t=Ps(null,t,i,e,n);break a;case 14:t=Fs(null,t,i,hs(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),zs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Bs(e,t,i,a,n);case 3:a:{if(Hs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Xa(e,t),to(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=Ss(Error(r(423)),t),t=Us(e,t,i,n,a);break a}else if(i!==a){a=Ss(Error(r(424)),t),t=Us(e,t,i,n,a);break a}else for(ga=xi(t.stateNode.containerInfo.firstChild),ha=t,_a=!0,va=null,n=Pa(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ea(),i===a){t=ec(e,t,n);break a}Ns(e,t,i,n)}t=t.child}return t;case 5:return uo(t),e===null&&Sa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,mi(i,a)?s=null:o!==null&&mi(i,o)&&(t.flags|=32),Rs(e,t),Ns(e,t,s,n),t.child;case 6:return e===null&&Sa(t),null;case 13:return Ks(e,t,n);case 4:return co(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Na(t,null,i,n):Ns(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Ps(e,t,i,a,n);case 7:return Ns(e,t,t.pendingProps,n),t.child;case 8:return Ns(e,t,t.pendingProps.children,n),t.child;case 12:return Ns(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Ri(Fa,i._currentValue),i._currentValue=s,o!==null)if(Z(o.value,s)){if(o.children===a.children&&!Vi.current){t=ec(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Za(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Va(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Va(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ns(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Ha(t,n),a=Ua(a),i=i(a),t.flags|=1,Ns(e,t,i,n),t.child;case 14:return i=t.type,a=hs(i,t.pendingProps),a=hs(i.type,a),Fs(e,t,i,a,n);case 15:return Is(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),$s(e,t),t.tag=1,Wi(i)?(e=!0,Ji(t)):e=!1,Ha(t,n),ys(t,i,a),xs(t,i,a,n),Vs(null,t,i,!0,e,n);case 19:return Qs(e,t,n);case 22:return Ls(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return nt(e,t)}function Gl(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(e,t,n,r){return new Gl(e,t,n,r)}function ql(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jl(e){if(typeof e==`function`)return+!!ql(e);if(e!=null){if(e=e.$$typeof,e===j)return 11;if(e===P)return 14}return 2}function Yl(e,t){var n=e.alternate;return n===null?(n=Kl(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xl(e,t,n,i,a,o){var s=2;if(i=e,typeof e==`function`)ql(e)&&(s=1);else if(typeof e==`string`)s=5;else a:switch(e){case E:return Zl(n.children,a,o,t);case D:s=8,a|=8;break;case O:return e=Kl(12,n,t,a|2),e.elementType=O,e.lanes=o,e;case M:return e=Kl(13,n,t,a),e.elementType=M,e.lanes=o,e;case N:return e=Kl(19,n,t,a),e.elementType=N,e.lanes=o,e;case I:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case k:s=10;break a;case A:s=9;break a;case j:s=11;break a;case P:s=14;break a;case F:s=16,i=null;break a}throw Error(r(130,e==null?e:typeof e,``))}return t=Kl(s,n,t,a),t.elementType=e,t.type=i,t.lanes=o,t}function Zl(e,t,n,r){return e=Kl(7,e,r,t),e.lanes=n,e}function Ql(e,t,n,r){return e=Kl(22,e,r,t),e.elementType=I,e.lanes=n,e.stateNode={isHidden:!1},e}function $l(e,t,n){return e=Kl(6,e,null,t),e.lanes=n,e}function eu(e,t,n){return t=Kl(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function tu(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ot(0),this.expirationTimes=Ot(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ot(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ya(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}};async function y(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData,r=await fetch(e,{credentials:`same-origin`,headers:n?t.headers:{"Content-Type":`application/json`,...t.headers??{}},...t}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function b(e){return e instanceof Error?e.message:String(e)}var x={session:()=>y(`/api/session`),login:e=>y(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})}),logout:()=>y(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>y(`/api/accounts?${e.toString()}`),account:e=>y(`/api/accounts/${e}`),channels:e=>y(`/api/channels?${e.toString()}`),channel:e=>y(`/api/channels/${e}`),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`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),ue=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`}]]),z=E(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),de=E(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),B=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`}]]),V=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(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),_e=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`}]]),ve=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`}]]),ye=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`}]]),H=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),U=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),W=o(((e,t)=>{t.exports=U()}))(),be=`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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Switch to dark theme`,"theme.switchToLight":`Switch to light theme`,"login.heading":`Operations Admin`,"login.body":`Enter credentials to open the console.`,"login.secret":`Admin password or token`,"login.submit":`Log in`,"login.submitting":`Logging in`,"dashboard.eyebrow":`Runtime Overview`,"dashboard.title":`Console Overview`,"dashboard.readPath":`Read path`,"dashboard.readPathValue":`PG read-only`,"dashboard.writePath":`Write path`,"dashboard.executionPolicy":`Execution policy`,"dashboard.dryRunFirst":`Dry-run first`,"dashboard.accountsText":`Account status, premium, verification, sessions.`,"dashboard.channelsText":`Public entities, member counts, verification state.`,"dashboard.messagesText":`Message boxes, updates, outbox state.`,"dashboard.strip.dryRun":`All dangerous actions start with dry-run`,"dashboard.strip.token":`Browser never stores internal tokens`,"dashboard.strip.pagination":`Lists use cursor pagination`,"dashboard.strip.snapshot":`Detail pages retain raw state snapshots`,"account.pageTitle":`Accounts`,"account.queryResults":`Search results`,"account.recentActive":`Recently active accounts`,"account.currentPage":`Accounts on page`,"account.onlineDevices":`Online device records`,"account.premium":`Premium`,"account.frozen":`Frozen`,"account.searchPlaceholder":`User ID / phone / username`,"account.userID":`User ID`,"account.phone":`Phone`,"account.lastActive":`Last active`,"account.notVerified":`Not verified`,"account.notPremium":`Not premium`,"account.premiumUntil":`Premium expires`,"account.starsBalance":`Stars balance`,"account.startingGrantApplied":`initial grant applied`,"account.startingGrantPending":`initial grant pending`,"account.activeSessions":`Authorized devices`,"account.accountFlags":`Account flags`,"account.restriction":`Restriction`,"account.restricted":`Restricted`,"account.createdAt":`Created`,"account.detailTitle":`Account #{id}`,"account.profile":`Account Profile`,"account.loadingDetail":`Loading account detail`,"account.waitingData":`Waiting for data`,"account.noUsername":`No username`,"account.noPhone":`No phone`,"account.accountFrozen":`Account frozen`,"account.accountActive":`Account active`,"account.authorizationsTitle":`Authorized Devices`,"account.authorizationsCount":`{count} authorizations`,"account.recentAdminOps":`Recent Admin Actions`,"account.recent30Audit":`Last 30 audit rows`,"account.actionDock":`Account Actions`,"account.freezeAccount":`Freeze account`,"account.updateFreeze":`Update freeze`,"account.unfreezeAccount":`Unfreeze account`,"account.freezeSince":`Frozen since`,"account.freezeUntil":`Appeal deadline`,"account.freezeUntilAria":`Freeze appeal deadline`,"account.freezeAppealURL":`Appeal URL`,"account.freezeAppealURLAria":`Freeze appeal URL`,"account.premiumMonths":`Premium duration (months)`,"account.premiumMonthsAria":`Set premium duration in months`,"account.setPremium":`Set premium`,"account.clearPremium":`Clear premium`,"account.starsAmount":`Stars to grant`,"account.starsAmountAria":`Set Stars amount to grant`,"account.grantStars":`Grant Stars`,"account.setVerified":`Set verified`,"account.clearVerified":`Clear verified`,"channel.pageTitle":`Supergroups and Channels`,"channel.recentUpdated":`Recently updated`,"channel.currentPage":`Entities on page`,"channel.megagroups":`Supergroups`,"channel.broadcasts":`Channels`,"channel.verifiedCount":`Verified`,"channel.searchPlaceholder":`Channel ID / username / title`,"channel.channelID":`Channel ID`,"channel.kind":`Kind`,"channel.title":`Title`,"channel.pts":`PTS`,"channel.detailProfile":`Channel Profile`,"channel.loadingDetail":`Loading channel detail`,"channel.creator":`Creator {id}`,"channel.governance":`Moderation`,"channel.governanceValue":`Banned {banned} / Kicked {kicked}`,"channel.flags":`Channel flags`,"channel.rawRow":`Channel Raw Row`,"channel.rawRowText":`Database read-only snapshot`,"channel.actionDock":`Channel Actions`,"channel.setVerified":`Set verified`,"channel.clearVerified":`Clear verified`,"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`,"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.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`中文`,"language.ru":`RU`,"theme.switchToDark":`切换到深色主题`,"theme.switchToLight":`切换到浅色主题`,"login.heading":`运维后台`,"login.body":`输入凭据后进入控制台。`,"login.secret":`管理员密码或 token`,"login.submit":`登录`,"login.submitting":`登录中`,"dashboard.eyebrow":`运行总览`,"dashboard.title":`控制台总览`,"dashboard.readPath":`读路径`,"dashboard.readPathValue":`PG 只读`,"dashboard.writePath":`写路径`,"dashboard.executionPolicy":`执行策略`,"dashboard.dryRunFirst":`先预演`,"dashboard.accountsText":`账号状态、会员、认证、会话。`,"dashboard.channelsText":`公开实体、成员计数、认证状态。`,"dashboard.messagesText":`消息盒、update、outbox 状态。`,"dashboard.strip.dryRun":`所有危险操作先预演`,"dashboard.strip.token":`浏览器不持有内部 token`,"dashboard.strip.pagination":`列表使用游标分页`,"dashboard.strip.snapshot":`详情页保留原始状态快照`,"account.pageTitle":`账号`,"account.queryResults":`查询结果`,"account.recentActive":`最近活跃账号`,"account.currentPage":`当前页账号`,"account.onlineDevices":`在线设备记录`,"account.premium":`会员`,"account.frozen":`冻结`,"account.searchPlaceholder":`用户 ID / 手机号 / 用户名`,"account.userID":`用户 ID`,"account.phone":`手机号`,"account.lastActive":`最近活跃`,"account.notVerified":`未认证`,"account.notPremium":`非会员`,"account.premiumUntil":`会员到期`,"account.starsBalance":`Stars 余额`,"account.startingGrantApplied":`初始赠送已发放`,"account.startingGrantPending":`初始赠送未触发`,"account.activeSessions":`授权设备`,"account.accountFlags":`账号标记`,"account.restriction":`限制状态`,"account.restricted":`已限制`,"account.createdAt":`创建时间`,"account.detailTitle":`账号 #{id}`,"account.profile":`账号档案`,"account.loadingDetail":`加载账号详情`,"account.waitingData":`等待数据`,"account.noUsername":`无用户名`,"account.noPhone":`无手机号`,"account.accountFrozen":`账号已冻结`,"account.accountActive":`账号正常`,"account.authorizationsTitle":`授权设备`,"account.authorizationsCount":`共 {count} 个授权`,"account.recentAdminOps":`最近后台操作`,"account.recent30Audit":`最近 30 条审计`,"account.actionDock":`账号操作`,"account.freezeAccount":`冻结账号`,"account.updateFreeze":`更新冻结信息`,"account.unfreezeAccount":`解冻账号`,"account.freezeSince":`冻结开始时间`,"account.freezeUntil":`申诉截止时间`,"account.freezeUntilAria":`账号冻结申诉截止时间`,"account.freezeAppealURL":`申诉链接`,"account.freezeAppealURLAria":`账号冻结申诉链接`,"account.premiumMonths":`会员时长(月)`,"account.premiumMonthsAria":`设置会员时长,单位月`,"account.setPremium":`设置会员`,"account.clearPremium":`取消会员`,"account.starsAmount":`赠送 Stars 数量`,"account.starsAmountAria":`设置要赠送的 Stars 数量`,"account.grantStars":`赠送 Stars`,"account.setVerified":`设置认证`,"account.clearVerified":`取消认证`,"channel.pageTitle":`超级群与频道`,"channel.recentUpdated":`最近更新`,"channel.currentPage":`当前页实体`,"channel.megagroups":`超级群`,"channel.broadcasts":`频道`,"channel.verifiedCount":`已认证`,"channel.searchPlaceholder":`频道 ID / 用户名 / 标题`,"channel.channelID":`频道 ID`,"channel.kind":`类型`,"channel.title":`标题`,"channel.pts":`PTS`,"channel.detailProfile":`频道档案`,"channel.loadingDetail":`加载频道详情`,"channel.creator":`创建者 {id}`,"channel.governance":`治理状态`,"channel.governanceValue":`封禁 {banned} / 踢出 {kicked}`,"channel.flags":`频道标记`,"channel.rawRow":`频道原始行`,"channel.rawRowText":`数据库只读快照`,"channel.actionDock":`频道操作`,"channel.setVerified":`设置认证`,"channel.clearVerified":`取消认证`,"channel.kind.broadcast":`频道`,"channel.kind.forum":`超级群/论坛`,"channel.kind.megagroup":`超级群`,"channel.kind.generic":`频道/群`,"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.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка…`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтверждён`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Тёмная тема`,"theme.switchToLight":`Светлая тема`,"login.heading":`Панель администратора`,"login.body":`Введите учётные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход…`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, Premium, подтверждение, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, число участников, статус подтверждения.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтверждён`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звёзд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество Звёзд`,"account.starsAmountAria":`Указать количество начисляемых Звёзд`,"account.grantStars":`Начислить Звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновлённые`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждённые`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звёздных подарков`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звёздного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звёздах`,"gifts.convertStars":`Звёзд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звёздные подарки ещё не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звёздах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Отозвать все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтверждённые`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Повторить тестовый запуск`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},Se=(0,g.createContext)(null);function Ce({children:e}){let[t,n]=(0,g.useState)(()=>De());(0,g.useEffect)(()=>{try{localStorage.setItem(be,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Ee(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Ee(t,e,n)}),[t]);return(0,W.jsx)(Se.Provider,{value:r,children:e})}function we(){let e=(0,g.useContext)(Se);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function Te(){let{lang:e,setLang:t,t:n}=we();return(0,W.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,W.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Ee(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 De(){try{let e=Oe(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=Oe(localStorage.getItem(be));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=Oe(t);if(e)return e}return`en`}function Oe(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:t===`ru`||t.startsWith(`ru-`)?`ru`:null}function ke(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Ae(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 je(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`)}var Me=`telesrv.admin.theme`,Ne=(0,g.createContext)(null);function Pe(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Fe({children:e}){let[t,n]=(0,g.useState)(()=>Re());(0,g.useEffect)(()=>{Pe(t);try{localStorage.setItem(Me,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(Me)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,W.jsx)(Ne.Provider,{value:a,children:e})}function Ie(){let e=(0,g.useContext)(Ne);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function Le(){let{theme:e,toggleTheme:t}=Ie(),{t:n}=we(),r=n(e===`light`?`theme.switchToDark`:`theme.switchToLight`);return(0,W.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":r,title:r,children:e===`dark`?(0,W.jsx)(ge,{size:16}):(0,W.jsx)(le,{size:16})})}function Re(){try{let e=localStorage.getItem(Me);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}function ze({href:e,navigate:t,className:n,children:r}){return(0,W.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function G(){let{t:e}=we();return(0,W.jsxs)(`div`,{className:`boot-screen`,children:[(0,W.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`telesrv`}),(0,W.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,W.jsx)(`div`,{className:`loader-bar`})]})}function Be({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=we(),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,W.jsxs)(`div`,{className:`shell`,children:[(0,W.jsxs)(`aside`,{className:`sidebar`,children:[(0,W.jsxs)(ze,{className:`brand`,href:`/`,navigate:n,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`telesrv`}),(0,W.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,W.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,W.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,W.jsx)(Ve,{icon:(0,W.jsx)(oe,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,W.jsx)(Ve,{icon:(0,W.jsx)(ye,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,W.jsx)(Ve,{icon:(0,W.jsx)(pe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,W.jsx)(Ve,{icon:(0,W.jsx)(re,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,W.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,W.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,W.jsx)(ce,{size:16}),(0,W.jsx)(`span`,{children:a(`layout.messages`)}),(0,W.jsx)(F,{className:`nav-section-chevron`,size:15})]}),s&&(0,W.jsxs)(`div`,{className:`nav-children`,children:[(0,W.jsx)(Ve,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:a(`layout.privateMessages`)}),(0,W.jsx)(Ve,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,W.jsxs)(`div`,{className:`sidebar-status`,children:[(0,W.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,W.jsxs)(`div`,{className:`runtime-row`,children:[(0,W.jsx)(fe,{size:14}),(0,W.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,W.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,W.jsxs)(`div`,{className:`runtime-row`,children:[(0,W.jsx)(ee,{size:14}),(0,W.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,W.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,W.jsxs)(`div`,{className:`runtime-row`,children:[(0,W.jsx)(me,{size:14}),(0,W.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,W.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,W.jsxs)(`div`,{className:`workspace`,children:[(0,W.jsxs)(`header`,{className:`topbar`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:je(t.path,a)}),(0,W.jsx)(`h1`,{children:Ae(t.path,a)})]}),(0,W.jsxs)(`div`,{className:`topbar-actions`,children:[(0,W.jsx)(Le,{}),(0,W.jsx)(Te,{}),(0,W.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,W.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,W.jsx)(se,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,W.jsx)(`main`,{className:`content`,children:i})]})]})}function Ve({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,W.jsxs)(ze,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,W.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,W.jsx)(`span`,{children:i})]})}function He(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function Ue(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function We(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function Ge(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 Ke(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function K(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function qe(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function q(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 Je({title:e,eyebrow:t,children:n,actions:r}){return(0,W.jsxs)(`div`,{className:`page-frame`,children:[(0,W.jsxs)(`div`,{className:`page-title-row`,children:[(0,W.jsxs)(`div`,{children:[t&&(0,W.jsx)(`div`,{className:`eyebrow`,children:t}),(0,W.jsx)(`h2`,{children:e})]}),r&&(0,W.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ye({children:e}){return(0,W.jsx)(`div`,{className:`query-panel`,children:e})}function Xe({main:e,side:t}){return(0,W.jsxs)(`div`,{className:`split-layout`,children:[(0,W.jsx)(`div`,{className:`split-main`,children:e}),(0,W.jsx)(`aside`,{className:`split-side`,children:t})]})}function Ze({title:e,text:t,action:n}){return(0,W.jsxs)(`div`,{className:`section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`h2`,{children:e}),t&&(0,W.jsx)(`p`,{children:t})]}),n&&(0,W.jsx)(`div`,{className:`section-action`,children:n})]})}function Qe({children:e}){return(0,W.jsxs)(`div`,{className:`alert`,children:[(0,W.jsx)(O,{size:16}),` `,(0,W.jsx)(`span`,{children:e})]})}function J({children:e,tone:t=`neutral`}){return(0,W.jsx)(`span`,{className:`badge ${t}`,children:e})}function $e({label:e,value:t,tone:n}){return(0,W.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{children:t})]})}function et({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,W.jsxs)(`div`,{className:`metric ${n}`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Y({label:e,value:t,mono:n=!1}){return(0,W.jsxs)(`div`,{className:`summary-item`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function tt({rows:e}){let{t}=we();return(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`audit.id`)}),(0,W.jsx)(`th`,{children:t(`audit.commandID`)}),(0,W.jsx)(`th`,{children:t(`audit.action`)}),(0,W.jsx)(`th`,{children:t(`audit.actor`)}),(0,W.jsx)(`th`,{children:t(`audit.status`)}),(0,W.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,W.jsx)(`th`,{children:t(`audit.reason`)}),(0,W.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,W.jsx)(`td`,{children:e.Action}),(0,W.jsx)(`td`,{children:e.Actor}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,W.jsx)(`td`,{children:Ke(e.CreatedAt)})]},e.ID)),e.length===0&&(0,W.jsx)(nt,{colSpan:8})]})]})})}function nt({colSpan:e}){let{t}=we();return(0,W.jsx)(`tr`,{children:(0,W.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function rt({label:e}){return(0,W.jsx)(`section`,{className:`surface`,children:(0,W.jsx)(`div`,{className:`loading-line`,children:e})})}function it({value:e}){return(0,W.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function at({onLogin:e}){let{t}=we(),[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,W.jsx)(`main`,{className:`login-page`,children:(0,W.jsxs)(`section`,{className:`login-panel`,children:[(0,W.jsxs)(`div`,{className:`login-head`,children:[(0,W.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`telesrv`}),(0,W.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,W.jsxs)(`div`,{className:`login-head-actions`,children:[(0,W.jsx)(Le,{}),(0,W.jsx)(Te,{}),(0,W.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,W.jsxs)(`div`,{className:`login-copy`,children:[(0,W.jsx)(`h1`,{children:t(`login.heading`)}),(0,W.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,W.jsx)(Qe,{children:i}),(0,W.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:t(`login.secret`)}),(0,W.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,W.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var ot=m();function st({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=we(),[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,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,ot.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,W.jsx)(`h2`,{children:e})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,W.jsx)(H,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`command-steps`,children:[(0,W.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,W.jsx)(`span`,{children:`1`}),(0,W.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`2`}),(0,W.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`3`}),(0,W.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:s(`action.reason`)}),(0,W.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,W.jsxs)(`div`,{className:`command-preview`,children:[(0,W.jsxs)(`div`,{className:`preview-head`,children:[(0,W.jsx)(te,{size:14}),` `,s(`action.requestPreview`)]}),(0,W.jsx)(it,{value:JSON.stringify(T,null,2)})]}),m&&(0,W.jsx)(Qe,{children:m}),f&&(0,W.jsxs)(`div`,{className:`result-box`,children:[(0,W.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,W.jsx)(O,{size:16}):(0,W.jsx)(k,{size:16}),(0,W.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:s(`action.commandID`)}),(0,W.jsx)(`strong`,{children:f.command_id})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:s(`action.status`)}),(0,W.jsx)(`strong`,{children:f.status})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:s(`action.dryRun`)}),(0,W.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,W.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,W.jsx)(it,{value:JSON.stringify(f.details,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,W.jsx)(A,{size:15,className:`spin`}):(0,W.jsx)(z,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,W.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,W.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function ct({rows:e,userID:t,onDone:n}){let{t:r}=we(),[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,W.jsxs)(`div`,{className:`authorization-block`,children:[(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:r(`auth.device`)}),(0,W.jsx)(`th`,{children:r(`auth.platform`)}),(0,W.jsx)(`th`,{children:r(`auth.ip`)}),(0,W.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,W.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,W.jsxs)(`tbody`,{children:[o.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,W.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,W.jsx)(`td`,{children:n.IP}),(0,W.jsx)(`td`,{children:Ke(n.ActiveAt)}),(0,W.jsx)(`td`,{className:`device-actions-cell`,children:(0,W.jsxs)(`div`,{className:`device-actions`,children:[(0,W.jsx)(st,{label:r(`auth.revokeCurrent`),icon:(0,W.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,W.jsx)(st,{label:r(`auth.keepCurrent`),icon:(0,W.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,W.jsx)(nt,{colSpan:5})]})]})}),(0,W.jsx)(`div`,{className:`danger-zone`,children:(0,W.jsx)(st,{label:r(`auth.revokeAll`),icon:(0,W.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 lt({id:e,navigate:t}){let{t:n}=we(),[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)(()=>ut(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(ut(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,W.jsx)(Qe,{children:a});if(!r)return(0,W.jsx)(rt,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,W.jsx)(Je,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,W.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,W.jsx)(Xe,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:We(y)}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[Ue(y.Username)||n(`account.noUsername`),` · `,He(y.Phone)||n(`account.noPhone`)]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,W.jsx)(J,{tone:`good`,children:n(`account.premium`)}):(0,W.jsx)(J,{children:n(`account.notPremium`)}),r.Verified?(0,W.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,W.jsx)(J,{children:n(`account.notVerified`)}),y.Frozen?(0,W.jsx)(J,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,W.jsx)(J,{children:n(`account.accountActive`)})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,W.jsx)(Y,{label:n(`account.lastActive`),value:K(r.LastSeenAt)||`-`}),(0,W.jsx)(Y,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?K(y.PremiumUntil):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,W.jsx)(Y,{label:n(`common.updatedAt`),value:Ke(y.UpdatedAt)||`-`}),(0,W.jsx)(Y,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,W.jsx)(Y,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,W.jsx)(Y,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.freezeSince`),value:r.Restriction.Since?Ke(r.Restriction.Since):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.freezeUntil`),value:r.Restriction.Until?Ke(r.Restriction.Until):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.createdAt`),value:Ke(y.CreatedAt)||`-`})]}),r.About&&(0,W.jsx)(`p`,{className:`about-text`,children:r.About}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,W.jsx)(ct,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,W.jsx)(tt,{rows:r.AuditLogs})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,W.jsx)(st,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,W.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,W.jsx)(st,{label:n(`account.unfreezeAccount`),icon:(0,W.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(st,{label:n(`account.setPremium`),icon:(0,W.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:qe(l)}),onDone:v}),(0,W.jsx)(st,{label:n(`account.clearPremium`),icon:(0,W.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,W.jsx)(st,{label:n(`account.grantStars`),icon:(0,W.jsx)(he,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:qe(d)}),onDone:v}),(0,W.jsx)(st,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,W.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]})]})})})}function ut(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function dt(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 ft(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 pt({navigate:e}){let{t}=we(),[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=dt(o?.rows??[]);return(0,W.jsxs)(Je,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,W.jsx)(B,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,W.jsx)(Qe,{children:f}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,W.jsx)(et,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,W.jsx)(et,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,W.jsx)(et,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,W.jsx)(Ye,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:t(`common.limit`)}),(0,W.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,W.jsx)(A,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`account.userID`)}),(0,W.jsx)(`th`,{children:t(`account.phone`)}),(0,W.jsx)(`th`,{children:t(`common.username`)}),(0,W.jsx)(`th`,{children:t(`common.name`)}),(0,W.jsx)(`th`,{children:t(`common.device`)}),(0,W.jsx)(`th`,{children:t(`account.lastActive`)}),(0,W.jsx)(`th`,{children:t(`account.premium`)}),(0,W.jsx)(`th`,{children:t(`common.verified`)}),(0,W.jsx)(`th`,{children:t(`account.frozen`)}),(0,W.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.ID}),(0,W.jsx)(`td`,{children:He(n.Phone)}),(0,W.jsx)(`td`,{children:Ue(n.Username)}),(0,W.jsx)(`td`,{children:We(n)}),(0,W.jsx)(`td`,{children:n.DeviceCount}),(0,W.jsx)(`td`,{children:Ke(n.LastActiveAt)}),(0,W.jsx)(`td`,{children:n.PremiumUntil>0?(0,W.jsxs)(J,{tone:`good`,children:[t(`account.premium`),` `,K(n.PremiumUntil)]}):(0,W.jsx)(J,{children:t(`common.none`)})}),(0,W.jsx)(`td`,{children:n.Verified?(0,W.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,W.jsx)(J,{children:t(`account.notVerified`)})}),(0,W.jsx)(`td`,{children:n.Frozen?(0,W.jsx)(J,{tone:`danger`,children:t(`account.frozen`)}):(0,W.jsx)(J,{children:t(`common.normal`)})}),(0,W.jsx)(`td`,{children:Ke(n.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,W.jsx)(nt,{colSpan:11})]})]})})]})}function mt({id:e,navigate:t}){let{t:n}=we(),[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,W.jsx)(Qe,{children:a});if(!r)return(0,W.jsx)(rt,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,W.jsx)(Je,{title:`${Ge(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,W.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,W.jsx)(Xe,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[Ue(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(J,{children:Ge(c,n)}),c.Verified?(0,W.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,W.jsx)(J,{children:n(`account.notVerified`)}),c.Deleted?(0,W.jsx)(J,{tone:`danger`,children:n(`common.deleted`)}):(0,W.jsx)(J,{children:n(`common.valid`)})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,W.jsx)(Y,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,W.jsx)(Y,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,W.jsx)(Y,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,W.jsx)(Y,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,W.jsx)(Y,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,W.jsx)(Y,{label:n(`account.createdAt`),value:K(c.Date)||`-`}),(0,W.jsx)(Y,{label:n(`common.updatedAt`),value:Ke(c.UpdatedAt)||`-`})]}),c.About&&(0,W.jsx)(`p`,{className:`about-text`,children:c.About}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,W.jsx)(tt,{rows:r.AuditLogs})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,W.jsx)(it,{value:r.ChannelJSON})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,W.jsx)(st,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,W.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s})]})})})}function ht({navigate:e}){let{t}=we(),[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=ft(o?.rows??[]);return(0,W.jsxs)(Je,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,W.jsx)(B,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,W.jsx)(Qe,{children:f}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,W.jsx)(et,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,W.jsx)(et,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,W.jsx)(et,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,W.jsx)(Ye,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:t(`common.limit`)}),(0,W.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,W.jsx)(A,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`channel.channelID`)}),(0,W.jsx)(`th`,{children:t(`channel.kind`)}),(0,W.jsx)(`th`,{children:t(`common.username`)}),(0,W.jsx)(`th`,{children:t(`channel.title`)}),(0,W.jsx)(`th`,{children:t(`common.members`)}),(0,W.jsx)(`th`,{children:t(`common.admins`)}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:t(`common.verified`)}),(0,W.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.ID}),(0,W.jsx)(`td`,{children:Ge(n,t)}),(0,W.jsx)(`td`,{children:Ue(n.Username)}),(0,W.jsx)(`td`,{children:n.Title}),(0,W.jsx)(`td`,{children:n.ParticipantsCount}),(0,W.jsx)(`td`,{children:n.AdminsCount}),(0,W.jsx)(`td`,{children:n.PTS}),(0,W.jsx)(`td`,{children:n.Verified?(0,W.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,W.jsx)(J,{children:t(`account.notVerified`)})}),(0,W.jsx)(`td`,{children:Ke(n.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,W.jsx)(nt,{colSpan:10})]})]})})]})}function gt({navigate:e}){let{t}=we();return(0,W.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,W.jsxs)(`section`,{className:`overview-band`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,W.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,W.jsxs)(`div`,{className:`overview-metrics`,children:[(0,W.jsx)($e,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,W.jsx)($e,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,W.jsx)($e,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,W.jsxs)(`div`,{className:`command-grid`,children:[(0,W.jsx)(_t,{icon:(0,W.jsx)(ye,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,W.jsx)(_t,{icon:(0,W.jsx)(pe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,W.jsx)(_t,{icon:(0,W.jsx)(ce,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,W.jsxs)(`section`,{className:`work-strip`,children:[(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(k,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(ae,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(L,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(te,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function _t({icon:e,title:t,text:n,href:r,navigate:i}){return(0,W.jsxs)(ze,{className:`launcher`,href:r,navigate:i,children:[(0,W.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,W.jsxs)(`span`,{className:`launcher-copy`,children:[(0,W.jsx)(`strong`,{children:t}),(0,W.jsx)(`span`,{children:n})]}),(0,W.jsx)(I,{size:16})]})}function vt({channelID:e,msgID:t,navigate:n}){let{t:r}=we(),[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,W.jsx)(Qe,{children:o});if(!i)return(0,W.jsx)(rt,{label:r(`common.loading`)});let l=i.Message;return(0,W.jsx)(Je,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,W.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:K(l.Date)})})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,W.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,W.jsx)(J,{children:r(`common.survived`)}),l.Pinned&&(0,W.jsx)(J,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,W.jsx)(J,{children:r(`messages.channelPost`)}),(0,W.jsxs)(J,{children:[`pts `,l.PTS]})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,W.jsx)(Y,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,W.jsx)(Y,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,W.jsx)(Y,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,W.jsx)(it,{value:i.MessageJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,W.jsx)(it,{value:i.ChannelJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:r(`common.count`)}),(0,W.jsx)(`th`,{children:r(`common.type`)}),(0,W.jsx)(`th`,{children:r(`common.messageId`)}),(0,W.jsx)(`th`,{children:r(`common.sender`)}),(0,W.jsx)(`th`,{children:r(`common.time`)})]})}),(0,W.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:e.MessageID}),(0,W.jsx)(`td`,{children:e.SenderUserID}),(0,W.jsx)(`td`,{children:K(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,W.jsx)(nt,{colSpan:6})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.eventJson`)}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,W.jsx)(it,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,W.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function yt({label:e,value:t,onChange:n}){let{t:r}=we(),[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,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(P,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:We(t)}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:Ue(t.Username)||He(t.Phone)||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,W.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,W.jsx)(`div`,{className:`picker-error`,children:u}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:We(e)}),(0,W.jsx)(`span`,{children:Ue(e.Username)||He(e.Phone)||`-`}),e.Verified?(0,W.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,W.jsx)(J,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,W.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function bt({label:e,value:t,onChange:n}){let{t:r}=we(),[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,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(P,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:t.Title||`-`}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:Ue(t.Username)||Ge(t,r)})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,W.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,W.jsx)(`div`,{className:`picker-error`,children:u}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:e.Title||`-`}),(0,W.jsx)(`span`,{children:Ue(e.Username)||Ge(e,r)}),e.Verified?(0,W.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,W.jsx)(J,{children:Ge(e,r)})]},e.ID)),o.length===0&&!c?(0,W.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function xt({navigate:e}){let{t}=we(),[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,W.jsxs)(Je,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,W.jsx)(Qe,{children:f}),(0,W.jsxs)(Ye,{children:[(0,W.jsx)(`div`,{className:`message-selector-grid single`,children:(0,W.jsx)(bt,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,W.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(V,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`messages.currentPage`),value:String(_.length)}),(0,W.jsx)(et,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,W.jsx)(et,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,W.jsx)(et,{label:t(`messages.channelGroup`),value:n?`${n.Title||Ge(n,t)} (${n.ID})`:`-`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`common.messageId`)}),(0,W.jsx)(`th`,{children:t(`common.time`)}),(0,W.jsx)(`th`,{children:t(`common.sender`)}),(0,W.jsx)(`th`,{children:`From Peer`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:t(`common.views`)}),(0,W.jsx)(`th`,{children:t(`common.status`)}),(0,W.jsx)(`th`,{children:t(`messages.body`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[_.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.ID}),(0,W.jsx)(`td`,{children:K(n.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,W.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,W.jsx)(`td`,{children:n.PTS}),(0,W.jsx)(`td`,{children:n.ViewsCount}),(0,W.jsx)(`td`,{children:n.Deleted?(0,W.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,W.jsx)(J,{tone:`warn`,children:t(`messages.pinned`)}):(0,W.jsx)(J,{children:t(`common.survived`)})}),(0,W.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,W.jsx)(nt,{colSpan:9})]})]})})]})}function St({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=we(),[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,W.jsx)(Qe,{children:o});if(!i)return(0,W.jsx)(rt,{label:r(`common.loading`)});let l=i.Message;return(0,W.jsx)(Je,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,W.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,W.jsx)(Xe,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:K(l.Date)})})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,W.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,W.jsx)(J,{children:r(`common.survived`)}),(0,W.jsxs)(J,{children:[`pts `,l.PTS]}),(0,W.jsx)(J,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,W.jsx)(Y,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,W.jsx)(Y,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,W.jsx)(Y,{label:r(`common.time`),value:K(l.Date)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,W.jsx)(it,{value:i.MessageJSON})]}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,W.jsx)(it,{value:i.DialogJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,W.jsx)(it,{value:i.PrivateJSON})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:r(`common.count`)}),(0,W.jsx)(`th`,{children:r(`common.type`)}),(0,W.jsx)(`th`,{children:r(`common.time`)})]})}),(0,W.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:K(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,W.jsx)(nt,{colSpan:4})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:r(`account.userID`)}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:r(`common.type`)}),(0,W.jsx)(`th`,{children:r(`common.status`)}),(0,W.jsx)(`th`,{children:r(`messages.attempts`)}),(0,W.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,W.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{children:e.TargetUserID}),(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.EventType}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.Attempts}),(0,W.jsx)(`td`,{children:Ke(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,W.jsx)(nt,{colSpan:7})]})]})})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,W.jsx)(st,{label:r(`messages.deleteThis`),icon:(0,W.jsx)(_e,{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}=we(),[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,W.jsxs)(Je,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,W.jsx)(Qe,{children:D}),(0,W.jsxs)(Ye,{children:[(0,W.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,W.jsx)(yt,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,W.jsx)(yt,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,W.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(V,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,W.jsx)(et,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,W.jsx)(et,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,W.jsx)(et,{label:t(`messages.ownerPeer`),value:n&&i?`${We(n)} / ${We(i)}`:`-`})]}),(0,W.jsxs)(`div`,{className:`operation-row`,children:[(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(_e,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,W.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,W.jsx)(st,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:q(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(ie,{size:15}),` `,t(`messages.clearHistory`)]}),(0,W.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,W.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,W.jsx)(st,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:qe(y),max_batches:qe(C),just_clear:_,revoke:m})})]})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`common.messageId`)}),(0,W.jsx)(`th`,{children:t(`common.time`)}),(0,W.jsx)(`th`,{children:t(`common.sender`)}),(0,W.jsx)(`th`,{children:t(`messages.direction`)}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:t(`common.status`)}),(0,W.jsx)(`th`,{children:t(`messages.body`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,W.jsx)(`td`,{children:K(n.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,W.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,W.jsx)(`td`,{children:n.PTS}),(0,W.jsx)(`td`,{children:n.Deleted?(0,W.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):(0,W.jsx)(J,{children:t(`common.survived`)})}),(0,W.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,W.jsx)(nt,{colSpan:8})]})]})})]})}var wt=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`)},H.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)}},H.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`)))},H.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())},H.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},H.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},H.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`))}},H.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`)},H.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)},H.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),ye(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}(),be=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),xe=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=be.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Se=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return xe(8,e)}(),Ce=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=Se.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=je(c.s),M=je(b),N=(e-y)/(v-y);Ae(r,ke(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 Ae(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 je(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 Me(){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!==Ee&&(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 Ne(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,De(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 Pe(){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]=Ve.newElement()),a[r][0]=e,a[r][1]=t},He.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)},He.prototype.reverse=function(){var e=new He;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=W.getBezierEasing(_.o.x,_.o.y,_.i.x,_.i.y).get,y.__fnct=b),d=b((e-(_.t-this.offsetTime))/(v.t-this.offsetTime-(_.t-this.offsetTime)))}a=v.s?v.s[0]:_.e[0]}i=_.s[0]}for(l=t._length,u=i.i[0].length,n.lastIndex=r,s=0;sr&&t>r)||(this._caching.lastIndex=i0||e>-1e-6&&e<0?r(e*t)/t:e}function P(){var e=this.props,t=N(e[0]),n=N(e[1]),r=N(e[4]),i=N(e[5]),a=N(e[12]),o=N(e[13]);return`matrix(`+t+`,`+n+`,`+r+`,`+i+`,`+a+`,`+o+`)`}return function(){this.reset=i,this.rotate=a,this.rotateX=o,this.rotateY=s,this.rotateZ=c,this.skew=u,this.skewFromAxis=d,this.shear=l,this.scale=f,this.setTransform=m,this.translate=h,this.transform=g,this.multiply=_,this.applyToPoint=S,this.applyToX=C,this.applyToY=w,this.applyToZ=T,this.applyToPointArray=A,this.applyToTriplePoints=k,this.applyToPointStringified=j,this.toCSS=M,this.to2dCSS=P,this.clone=b,this.cloneFromProps=x,this.equals=y,this.inversePoints=O,this.inversePoint=D,this.getInverseMatrix=E,this._t=this.transform,this.isIdentity=v,this._identity=!0,this._identityCalculated=!1,this.props=p(`float32`,16),this.reset()}}();function qe(e){"@babel/helpers - typeof";return qe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},qe(e)}var q={},Je=`__[STANDALONE]__`,Ye=`__[ANIMATIONDATA]__`,Xe=``;function Ze(e){s(e)}function Qe(){Je===!0?U.searchAnimations(Ye,Je,Xe):U.searchAnimations()}function J(e){re(e)}function $e(e){ue(e)}function et(e){return Je===!0&&(e.animationData=JSON.parse(Ye)),U.loadAnimation(e)}function Y(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 tt(){return typeof navigator<`u`}function nt(e,t){e===`expressions`&&ae(t)}function rt(e){switch(e){case`propertyFactory`:return G;case`shapePropertyFactory`:return Ke;case`matrix`:return K;default:return null}}q.play=U.play,q.pause=U.pause,q.setLocationHref=Ze,q.togglePause=U.togglePause,q.setSpeed=U.setSpeed,q.setDirection=U.setDirection,q.stop=U.stop,q.searchAnimations=Qe,q.registerAnimation=U.registerAnimation,q.loadAnimation=et,q.setSubframeRendering=J,q.resize=U.resize,q.goToAndStop=U.goToAndStop,q.destroy=U.destroy,q.setQuality=Y,q.inBrowser=tt,q.installPlugin=nt,q.freeze=U.freeze,q.unfreeze=U.unfreeze,q.setVolume=U.setVolume,q.mute=U.mute,q.unmute=U.unmute,q.getRegisteredAnimations=U.getRegisteredAnimations,q.useWebWorker=a,q.setIDPrefix=$e,q.__getFactory=rt,q.version=`5.13.0`;function it(){document.readyState===`complete`&&(clearInterval(lt),Qe())}function at(e){for(var t=ot.split(`&`),n=0;n=1?a.push({s:e-1,e:t-1}):(a.push({s:e,e:1}),a.push({s:0,e:t-1}));var o=[],s,c=a.length,l;for(s=0;sr+n)){var u=l.s*i<=r?0:(l.s*i-r)/n,d=l.e*i>=r+n?1:(l.e*i-r)/n;o.push([u,d])}return o.length||o.push([0,0]),o},ft.prototype.releasePathsData=function(e){var t,n=e.length;for(t=0;t1?1+r:this.s.v<0?0+r:this.s.v+r,n=this.e.v>1?1+r:this.e.v<0?0+r:this.e.v+r,t>n){var i=t;t=n,n=i}t=Math.round(t*1e4)*1e-4,n=Math.round(n*1e4)*1e-4,this.sValue=t,this.eValue=n}else t=this.sValue,n=this.eValue;var a,o,s=this.shapes.length,c,l,u,d,f,p=0;if(n===t)for(o=0;o=0;--o)if(h=this.shapes[o],h.shape._mdf){for(g=h.localShapeCollection,g.releaseShapes(),this.m===2&&s>1?(b=this.calculateShapeEdges(t,n,h.totalShapeLength,y,p),y+=h.totalShapeLength):b=[[_,v]],l=b.length,c=0;c=1?m.push({s:h.totalShapeLength*(_-1),e:h.totalShapeLength*(v-1)}):(m.push({s:h.totalShapeLength*_,e:h.totalShapeLength}),m.push({s:0,e:h.totalShapeLength*(v-1)}));var x=this.addShapes(h,m[0]);if(m[0].s!==m[0].e){if(m.length>1)if(h.shape.paths.shapes[h.shape.paths._length-1].c){var S=x.pop();this.addPaths(x,g),x=this.addShapes(h,m[1],S)}else this.addPaths(x,g),x=this.addShapes(h,m[1]);this.addPaths(x,g)}}h.shape.paths=g}}else if(this._mdf)for(o=0;ot.e){n.c=!1;break}else t.s<=l&&t.e>=l+u.addedLength?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[s],i[a].v[s],n,d,g),g=!1):(p=Te.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=Te.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=G.getProp(e,t.p.x,0,0,this),this.py=G.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=G.getProp(e,t.p.z,0,0,this))):this.p=G.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=G.getProp(e,t.rx,0,D,this),this.ry=G.getProp(e,t.ry,0,D,this),this.rz=G.getProp(e,t.rz,0,D,this),t.or.k[0].ti){var r,i=t.or.k.length;for(r=0;r0;)--n,this._elements.unshift(t[n]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},ht.prototype.resetElements=function(e){var t,n=e.length;for(t=0;t0?Math.floor(f):Math.ceil(f),h=this.pMatrix.props,g=this.rMatrix.props,_=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v=0;if(f>0){for(;vm;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),--v;p&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-p,!0),v-=p)}r=this.data.m===1?0:this._currentCopies-1,i=this.data.m===1?1:-1,a=this._currentCopies;for(var y,b;a;){if(t=this.elemsData[r].it,n=t[t.length-1].transform.mProps.v.props,b=n.length,t[t.length-1].transform.mProps._mdf=!0,t[t.length-1].transform.op._mdf=!0,t[t.length-1].transform.op.v=this._currentCopies===1?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),v!==0){for((r!==0&&i===1||r!==this._currentCopies-1&&i===-1)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(_[0],_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15]),this.matrix.transform(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8],h[9],h[10],h[11],h[12],h[13],h[14],h[15]),y=0;y0&&r<1?[t]:[]:[t-r,t+r].filter(function(e){return e>0&&e<1})},wt.prototype.split=function(e){if(e<=0)return[Ct(this.points[0]),this];if(e>=1)return[this,Ct(this.points[this.points.length-1])];var t=bt(this.points[0],this.points[1],e),n=bt(this.points[1],this.points[2],e),r=bt(this.points[2],this.points[3],e),i=bt(t,n,e),a=bt(n,r,e),o=bt(i,a,e);return[new wt(this.points[0],t,i,o,!0),new wt(o,a,r,this.points[3],!0)]};function Tt(e,t){var n=e.points[0][t],r=e.points[e.points.length-1][t];if(n>r){var i=r;r=n,n=i}for(var a=xt(3*e.a[t],2*e.b[t],e.c[t]),o=0;o0&&a[o]<1){var s=e.point(a[o])[t];sr&&(r=s)}return{min:n,max:r}}wt.prototype.bounds=function(){return{x:Tt(this,0),y:Tt(this,1)}},wt.prototype.boundingBox=function(){var e=this.bounds();return{left:e.x.min,right:e.x.max,top:e.y.min,bottom:e.y.max,width:e.x.max-e.x.min,height:e.y.max-e.y.min,cx:(e.x.max+e.x.min)/2,cy:(e.y.max+e.y.min)/2}};function Et(e,t,n){var r=e.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:e,t:(t+n)/2,t1:t,t2:n}}function Dt(e){var t=e.bez.split(.5);return[Et(t[0],e.t1,e.t),Et(t[1],e.t,e.t2)]}function Ot(e,t){return Math.abs(e.cx-t.cx)*2=a||e.width<=r&&e.height<=r&&t.width<=r&&t.height<=r){i.push([e.t,t.t]);return}var o=Dt(e),s=Dt(t);kt(o[0],s[0],n+1,r,i,a),kt(o[0],s[1],n+1,r,i,a),kt(o[1],s[0],n+1,r,i,a),kt(o[1],s[1],n+1,r,i,a)}}wt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return kt(Et(this,0,1),Et(e,0,1),0,t,r,n),r},wt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new wt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},wt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new wt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function At(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function jt(e,t,n,r){var i=[e[0],e[1],1],a=[t[0],t[1],1],o=[n[0],n[1],1],s=[r[0],r[1],1],c=At(At(i,a),At(o,s));return vt(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function X(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Mt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Nt(e,t){return _t(e[0],t[0])&&_t(e[1],t[1])}function Pt(){}u([dt],Pt),Pt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=G.getProp(e,t.s,0,null,this),this.frequency=G.getProp(e,t.r,0,null,this),this.pointsType=G.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function Ft(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function It(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function Lt(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=It(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Rt(e,t,n,r,i,a,o){var s=Lt(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;Ft(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function zt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ut(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Gt(e){for(var t,n=1;n1&&(t=Wt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Kt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Vt(e,t)];if(n.length===1||_t(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Vt(r,t),Vt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Vt(r,t),Vt(o,t),Vt(i,t)]}function qt(){}u([dt],qt),qt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=G.getProp(e,t.a,0,null,this),this.miterLimit=G.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},qt.prototype.processPath=function(e,t,n,r){var i=Ue.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=wt.shapeSegmentInverted(e,o),l.push(Kt(c,t));l=Gt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function Xt(e){this.animationData=e}Xt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Zt(e){return new Xt(e)}function Qt(){}Qt.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},mn.prototype.show=function(){},mn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},mn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},mn.prototype.resume=function(){this._canPlay=!0},mn.prototype.setRate=function(e){this.audio.rate(e)},mn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},mn.prototype.getBaseElement=function(){return null},mn.prototype.destroy=function(){},mn.prototype.sourceRectAtTime=function(){},mn.prototype.initExpressions=function(){};function hn(){}hn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},hn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},hn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},hn.prototype.createAudio=function(e){return new mn(e,this.globalData,this)},hn.prototype.createFootage=function(e){return new pn(e,this.globalData,this)},hn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}vn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},vn.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},vn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var yn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=z(`filter`);return n.setAttribute(`id`,e),t!==!0&&(n.setAttribute(`filterUnits`,`objectBoundingBox`),n.setAttribute(`x`,`0%`),n.setAttribute(`y`,`0%`),n.setAttribute(`width`,`100%`),n.setAttribute(`height`,`100%`)),n}function n(){var e=z(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),bn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),xn={},Sn=`filter_result_`;function Cn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=I(),a=yn.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Rn.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function Gn(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([dn,_n,wn,kn,Tn,fn,En],Gn),Gn.prototype.initSecondaryElement=function(){},Gn.prototype.identityMatrix=new K,Gn.prototype.buildExpressionInterface=function(){},Gn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Gn.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},Gn.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},qn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},qn.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},qn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Yt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Yt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Yt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Yt.isVariationSelector(i)&&(o=!0)):Yt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},qn.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=Jt(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var F,I,L=[];for(g=0;g<_;g+=1){for(N=M[g],N.a.sc&&(e.strokeColorAnim=!0),N.a.sw&&(e.strokeWidthAnim=!0),(N.a.fc||N.a.fh||N.a.fs||N.a.fb)&&(e.fillColorAnim=!0),I=0,F=N.s.b,i=0;i0?i=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?o=1-this.xe.v/100:s=1+this.xe.v/100;var c=W.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([Be],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=G.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=G.getProp;for(e=0;e=m+be||!x?(T=(m+be-g)/h.partialLength,ie=b.point[0]+(h.point[0]-b.point[0])*T,ae=b.point[1]+(h.point[1]-b.point[1])*T,a.translate(-n[0]*f[u].an*.005,-(n[1]*A)*.01),_=!1):x&&(g+=h.partialLength,v+=1,v>=x.length&&(v=0,y+=1,S[y]?x=S[y].points:D.v.c?(v=0,y=0,x=S[y].points):(g-=h.partialLength,x=null)),x&&(b=h,h=x[v],C=h.partialLength));re=f[u].an/2-f[u].add,a.translate(-re,0,0)}else re=f[u].an/2-f[u].add,a.translate(-re,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:z(s?`g`:`text`),b<=e){if(c.setAttribute(`stroke-linecap`,`butt`),c.setAttribute(`stroke-linejoin`,`round`),c.setAttribute(`stroke-miterlimit`,`4`),this.textSpans[e].span=c,s){var S=z(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new ir(x.data,this.globalData,this);else{var w=Qn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Gn(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},rr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&lr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new dr(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=$t(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new fr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(gn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=lr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=mr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new 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=Ke.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)},ge(`canvas`,Cr),ut.registerModifier(`tm`,ft),ut.registerModifier(`pb`,pt),ut.registerModifier(`rp`,ht),ut.registerModifier(`rd`,gt),ut.registerModifier(`zz`,Pt),ut.registerModifier(`op`,qt),q}))}))(),1),Tt=0,Et=e=>`${e}-${++Tt}`,Dt=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function Ot(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:Et(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function At(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=Dt[e.length%Dt.length];return{key:Et(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var jt=e=>Ot([kt(e,0),kt(e,1)]),X=()=>{let e=At([]);return Ot([e,At([e])])};function Mt({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=wt.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,W.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function Nt({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,W.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,W.jsx)(Mt,{data:n,compact:!0}):(0,W.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,W.jsx)(A,{className:`spin`,size:15})})}async function Pt(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 Ft=e=>Number.parseInt(e.replace(`#`,``),16),It=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function Lt({gift:e,onClose:t,onPublished:n}){let{t:r}=we(),[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)(()=>jt(`model`)),[D,O]=(0,g.useState)(()=>jt(`pattern`)),[M,N]=(0,g.useState)(X);(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 Pt(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function ee(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||M.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=M.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:M.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:Ft(e.center),edge_color:Ft(e.edge),pattern_color:Ft(e.pattern),text_color:Ft(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}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,W.jsxs)(`section`,{className:`collectible-section`,children:[(0,W.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,W.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,W.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,W.jsxs)(J,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(Ot([...t,kt(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,W.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,W.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,W.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,W.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`common.name`)}),(0,W.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,W.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,W.jsxs)(`label`,{className:`collectible-file`,children:[(0,W.jsx)(`span`,{children:r(`gifts.animation`)}),(0,W.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,W.jsxs)(`em`,{children:[(0,W.jsx)(R,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,W.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,W.jsx)(Mt,{data:i.animation,compact:!0}):(0,W.jsx)(j,{size:16})}),(0,W.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(Ot(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,W.jsx)(_e,{size:14})}),i.fileError&&(0,W.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,ot.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,W.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,W.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,W.jsx)(H,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,W.jsxs)(`div`,{className:`collectible-loading`,children:[(0,W.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,W.jsxs)(`section`,{className:`collectible-active`,children:[(0,W.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(ne,{size:18}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,W.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,W.jsx)(J,{tone:`good`,children:r(`collectibles.published`)})]}),(0,W.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,W.jsxs)(`article`,{children:[(0,W.jsx)(Nt,{giftID:e.GiftID,attribute:t}),(0,W.jsxs)(`div`,{children:[(0,W.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,W.jsx)(J,{children:`crafted`})]}),(0,W.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,It(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,W.jsxs)(`article`,{children:[(0,W.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,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:e.name}),(0,W.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,It(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,W.jsxs)(`div`,{className:`collectible-empty`,children:[(0,W.jsx)(ne,{size:22}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,W.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,W.jsxs)(`section`,{className:`collectible-definition`,children:[(0,W.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,W.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,W.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,W.jsx)(`span`,{children:`TGS`}),(0,W.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,W.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,W.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`gifts.reason`)}),(0,W.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,W.jsxs)(`section`,{className:`collectible-section`,children:[(0,W.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,W.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,W.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,W.jsxs)(J,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N(Ot([...M,At(M)])),F()},children:[(0,W.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,W.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,W.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,W.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`common.name`)}),(0,W.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,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,W.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,W.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,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,W.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,W.jsxs)(`label`,{className:`collectible-color`,children:[(0,W.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,W.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,W.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,W.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length<=2,onClick:()=>{N(Ot(M.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,W.jsx)(_e,{size:14})})]},e.key))})]})]}),u&&(0,W.jsx)(Qe,{children:u}),f&&(0,W.jsxs)(`div`,{className:`gift-validation`,children:[(0,W.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,W.jsx)(k,{size:17}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,W.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,W.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:te,disabled:c,children:[c?(0,W.jsx)(A,{className:`spin`,size:15}):(0,W.jsx)(pe,{size:15}),r(`gifts.validate`)]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:re,disabled:c||!f,children:[(0,W.jsx)(ve,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function Rt(e){return e.model_count+e.pattern_count+e.backdrop_count}function zt(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Bt({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=wt.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,W.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,W.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,W.jsx)(`span`,{children:s})}),(0,W.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,W.jsx)(ue,{size:14}):(0,W.jsx)(z,{size:14})})]})}function Vt({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=wt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,W.jsx)(`div`,{className:`gift-animation-shell`,children:(0,W.jsx)(`div`,{className:`gift-animation`,ref:t})})}function Ht(){let{t:e}=we(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,te]=(0,g.useState)(`50`),[re,ie]=(0,g.useState)(`50`),[ae,oe]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[z,fe]=(0,g.useState)(null),[me,he]=(0,g.useState)(!1),[ge,_e]=(0,g.useState)(``),[ye,U]=(0,g.useState)(``);async function be(){_e(``);try{n((await x.gifts()).Gifts??[])}catch(e){_e(b(e))}}(0,g.useEffect)(()=>{be()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>U(b(e)))},[a,d,p.length]);let xe=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),Se=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),Ce=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),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 Ee(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae)})),r.set(`file`,l,l.name),r}function De(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function Oe(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),te(String(t.stars)),ie(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),fe(null)}async function ke(){he(!0),U(``),fe(null);try{fe(d===`official`?await x.importOfficialGift(De(!1)):await x.importGift(Ee(!1)))}catch(e){U(b(e))}finally{he(!1)}}async function Ae(){if(z){he(!0),U(``);try{d===`official`?await x.importOfficialGift(De(!0,z.command_id)):await x.importGift(Ee(!0,z.command_id)),fe(null),u(null),F(`0`),L(``),C(``),await be(),o(!1)}catch(e){U(b(e))}finally{he(!1)}}}function je(){F(`0`),L(``),te(`50`),ie(`50`),oe(`0`),ce(!0),ue(``),u(null),fe(null),U(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Me(e){F(e.GiftID),L(e.Title),te(String(e.Stars)),ie(String(e.ConvertStars)),oe(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),fe(null),U(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,W.jsxs)(Je,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>be(),disabled:me,children:[(0,W.jsx)(B,{size:15}),` `,e(`common.refresh`)]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,children:[(0,W.jsx)(de,{size:15}),` `,e(`gifts.add`)]})]}),children:[ge&&(0,W.jsx)(Qe,{children:ge}),(0,W.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,W.jsx)(et,{label:e(`gifts.total`),value:String(t.length)}),(0,W.jsx)(et,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,W.jsx)(et,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,W.jsx)(et,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,W.jsx)(Ye,{children:(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,W.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Te.length,total:t.length})})]})}),(0,W.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:e(`gifts.animation`)}),(0,W.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,W.jsx)(`th`,{children:e(`gifts.title`)}),(0,W.jsx)(`th`,{children:e(`gifts.price`)}),(0,W.jsx)(`th`,{children:e(`gifts.source`)}),(0,W.jsx)(`th`,{children:e(`gifts.received`)}),(0,W.jsx)(`th`,{children:e(`common.status`)}),(0,W.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,W.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,W.jsxs)(`tbody`,{children:[Te.map(t=>(0,W.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(Bt,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,W.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,W.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,W.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(J,{children:t.SourceFormat}),(0,W.jsx)(`span`,{className:`gift-source-size`,children:zt(t.AnimationSize)})]}),(0,W.jsx)(`td`,{children:t.ReceivedCount}),(0,W.jsx)(`td`,{children:(0,W.jsx)(J,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,W.jsx)(`td`,{children:Ke(t.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,W.jsx)(ne,{size:13}),e(`collectibles.manage`)]}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Me(t),children:e(`gifts.replace`)}),(0,W.jsx)(st,{compact:!0,tone:`neutral`,label:t.Enabled?e(`gifts.disable`):e(`gifts.enable`),path:`/api/actions/set-gift-enabled`,payload:()=>({gift_id:t.GiftID,enabled:!t.Enabled}),onDone:()=>void be()})]})})]},t.GiftID)),Te.length===0&&(0,W.jsx)(nt,{colSpan:9})]})]})}),a&&(0,ot.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,W.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:me,"aria-label":e(`action.close`),children:(0,W.jsx)(H,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,W.jsxs)(`div`,{className:`command-steps`,children:[(0,W.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,W.jsx)(`span`,{children:`1`}),(0,W.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${z?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`2`}),(0,W.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${z?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`3`}),(0,W.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,W.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,W.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),fe(null)},children:e(`gifts.officialSource`)}),(0,W.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),fe(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,W.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,W.jsxs)(`div`,{className:`gift-import-note`,children:[(0,W.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,W.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,W.jsx)(`span`,{children:p.length}),(0,W.jsx)(`span`,{children:`SHA-256`})]})]}),(0,W.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,W.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:Ce.length,total:p.length})})]}),(0,W.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,W.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,W.jsx)(`span`,{children:Se[t]})]},t))}),(0,W.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[Ce.map(t=>{let n=t.source_gift_id===S;return(0,W.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>Oe(t),children:[(0,W.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,W.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,W.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,W.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,W.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,W.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:Rt(t)})})]}),(0,W.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,W.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,W.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),Ce.length===0&&(0,W.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),xe&&(0,W.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,W.jsx)(Vt,{sourceGiftID:xe.source_gift_id}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:xe.title||e(`gifts.officialUnnamed`,{id:xe.source_gift_id})}),(0,W.jsx)(`span`,{className:`mono`,children:xe.source_gift_id}),(0,W.jsxs)(`small`,{children:[xe.model_count,` `,e(`collectibles.models`),` · `,xe.pattern_count,` `,e(`collectibles.patterns`),` · `,xe.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,W.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,W.jsx)(`span`,{className:xe.can_upgrade?`yes`:`no`,children:xe.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,W.jsx)(`span`,{className:xe.can_craft?`craft`:`no`,children:xe.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),xe?.can_upgrade&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`gift-switch`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),fe(null)}}),(0,W.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,W.jsx)(`span`,{})}),(0,W.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,W.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,W.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),fe(null)}})]})]})]})]}):(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`gift-import-note`,children:[(0,W.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,W.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,W.jsx)(`span`,{children:`TGS`}),(0,W.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,W.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),fe(null)}}),(0,W.jsx)(`span`,{className:`gift-file-icon`,children:(0,W.jsx)(R,{size:22})}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,W.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,W.jsx)(`small`,{children:l?zt(l.size):e(`gifts.fileHint`)})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,W.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.title`)}),(0,W.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.stars`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{te(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,W.jsx)(`input`,{type:`number`,min:`0`,value:re,onChange:e=>{ie(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,W.jsx)(`input`,{type:`number`,value:ae,onChange:e=>{oe(e.target.value),fe(null)}})]})]}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:e(`gifts.reason`)}),(0,W.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,W.jsxs)(`label`,{className:`gift-switch`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),fe(null)}}),(0,W.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,W.jsx)(`span`,{})}),(0,W.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),ye&&(0,W.jsx)(Qe,{children:ye}),z&&(0,W.jsxs)(`div`,{className:`gift-validation`,children:[(0,W.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,W.jsx)(k,{size:17}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,W.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,W.jsx)(`pre`,{children:JSON.stringify(z.details,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:me,children:e(`common.close`)}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:ke,disabled:me,children:[me?(0,W.jsx)(A,{className:`spin`,size:15}):(0,W.jsx)(pe,{size:15}),e(`gifts.validate`)]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Ae,disabled:me||!z,children:[(0,W.jsx)(ve,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,W.jsx)(Lt,{gift:s,onClose:()=>c(null),onPublished:()=>void be()})]})}function Ut({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1];return n?(0,W.jsx)(lt,{id:Number(n),navigate:t}):r?(0,W.jsx)(mt,{id:Number(r),navigate:t}):e.path===`/accounts`?(0,W.jsx)(pt,{navigate:t}):e.path===`/channels`?(0,W.jsx)(ht,{navigate:t}):e.path===`/gifts`?(0,W.jsx)(Ht,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,W.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,W.jsx)(vt,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,W.jsx)(xt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,W.jsx)(Ct,{navigate:t}):(0,W.jsx)(gt,{navigate:t})}function Wt(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>ke());(0,g.useEffect)(()=>{let e=()=>r(ke());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(ke())};return e===void 0?(0,W.jsx)(G,{}):e===null?(0,W.jsx)(at,{onLogin:t}):(0,W.jsx)(Be,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,W.jsx)(Ut,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,W.jsx)(g.StrictMode,{children:(0,W.jsx)(Fe,{children:(0,W.jsx)(Ce,{children:(0,W.jsx)(Wt,{})})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-DHdrFM5j.css b/cmd/telesrv-admin/web/dist/assets/index-DHdrFM5j.css deleted file mode 100644 index f38bf9a5..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-DHdrFM5j.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;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow: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-source-tabs{gap:8px;display:flex}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{color:#49605c;min-height:32px;font:inherit;cursor:pointer;background:#f7faf9;border:1px solid #d7e2df;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:#9fc9c0}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:0 4px 12px #176d612b}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#ffffffa6;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand)}.official-gift-list{border:1px solid var(--line);scrollbar-gutter:stable;background:#f6f9f8;border-radius:14px;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);cursor:pointer;background:#fff;border:1px solid #dce6e3;border-radius:11px;gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid;box-shadow:0 1px 2px #20363208}.official-gift-option:hover{border-color:#9fc9c0;transform:translateY(-1px);box-shadow:0 5px 14px #204c4414}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px #176d611f,0 5px 14px #204c4414}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:#667773;flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:#136b4d;background:#e9f8f0;border-color:#bde6cf}.official-gift-capabilities>span.craft{color:#6e3ca0;background:#f3ebfb;border-color:#d9c5ef}.official-gift-capabilities>span.no{color:#78837f;background:#f1f3f2;border-color:#dde2e0}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);background:var(--surface-soft);border-radius:14px;grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);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{width:100%;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}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.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}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{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{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius: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-DuOdm70q.css b/cmd/telesrv-admin/web/dist/assets/index-DuOdm70q.css new file mode 100644 index 00000000..f80a193d --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-DuOdm70q.css @@ -0,0 +1 @@ +:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#eef1f5;--bg-accent:#e7ecf1;--panel:#fff;--panel-subtle:#f5f8fb;--panel-strong:#eef2f6;--surface-soft:#f2f7f6;--overlay:#18222f6b;--topbar-bg:#ffffffdb;--line:#e5eaf0;--line-strong:#d3dce4;--heading:#253040;--text:#333f4d;--text-soft:#45525f;--muted:#6d7885;--muted-2:#9aa4b1;--brand:#1f7d6f;--brand-strong:#196155;--brand-2:#3a6cae;--brand-tint:#e8f4f0;--brand-tint-border:#c8e2db;--brand-tint-text:#235d53;--good:#1f8a57;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a86a12;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#c0392b;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#1c2530;--sidebar-soft:#26313d;--sidebar-line:#313c4a;--sidebar-row:#232d38;--sidebar-text:#dbe3ec;--sidebar-muted:#8b98a8;--sidebar-faint:#7c8a9a;--sidebar-heading:#fff;--focus:#1f7d6f29;--shadow:0 12px 34px #1827381a;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #1f7d6f38;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#37a596;--brand-strong:#45b6a6;--brand-2:#6fa8e6;--brand-tint:#14322d;--brand-tint-border:#245349;--brand-tint-text:#7fd3c4;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#37a5963d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #37a59642}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow:var(--shadow-brand)}.brand-mark{color:#fff;background:var(--brand);border-radius:var(--radius-sm);border:1px solid #fff3;place-items:center;width:34px;height:34px;font-weight:800;display:grid}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border:1px solid var(--sidebar-line);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.language-switch{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:2px;display:inline-flex}.language-switch button{min-width:42px;min-height:24px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0 9px;font-weight:800;transition:color .14s,background-color .14s}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input{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{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:190px}.device-actions-cell{width:190px;min-width:190px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(82px,1fr));gap:6px;min-width:178px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:first-child{width:74px}.gift-table td{vertical-align:middle}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid,.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/assets/index-Duge82ST.js b/cmd/telesrv-admin/web/dist/assets/index-Duge82ST.js deleted file mode 100644 index 3c2389cf..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-Duge82ST.js +++ /dev/null @@ -1,9 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.provider`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.iterator;function p(e){return typeof e!=`object`||!e?null:(e=f&&e[f]||e[`@@iterator`],typeof e==`function`?e:null)}var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,g={};function _(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}_.prototype.isReactComponent={},_.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`setState(...): takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},_.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function v(){}v.prototype=_.prototype;function y(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}var b=y.prototype=new v;b.constructor=y,h(b,_.prototype),b.isPureReactComponent=!0;var x=Array.isArray,S=Object.prototype.hasOwnProperty,C={current:null},w={key:!0,ref:!0,__self:!0,__source:!0};function T(e,n,r){var i,a={},o=null,s=null;if(n!=null)for(i in n.ref!==void 0&&(s=n.ref),n.key!==void 0&&(o=``+n.key),n)S.call(n,i)&&!w.hasOwnProperty(i)&&(a[i]=n[i]);var c=arguments.length-2;if(c===1)a.children=r;else if(1{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=typeof setTimeout==`function`?setTimeout:null,_=typeof clearTimeout==`function`?clearTimeout:null,v=typeof setImmediate<`u`?setImmediate:null;typeof navigator<`u`&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function b(e){if(h=!1,y(e),!m)if(n(c)!==null)m=!0,M(x);else{var t=n(l);t!==null&&N(b,t.startTime-e)}}function x(t,i){m=!1,h&&(h=!1,_(w),w=-1),p=!0;var a=f;try{for(y(i),d=n(c);d!==null&&(!(d.expirationTime>i)||t&&!D());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=i);i=e.unstable_now(),typeof s==`function`?d.callback=s:d===n(c)&&r(c),y(i)}else r(c);d=n(c)}if(d!==null)var u=!0;else{var g=n(l);g!==null&&N(b,g.startTime-i),u=!1}return u}finally{d=null,f=a,p=!1}}var S=!1,C=null,w=-1,T=5,E=-1;function D(){return!(e.unstable_now()-Ee||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(_(w),w=-1):h=!0,N(b,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,M(x))),r},e.unstable_shouldYield=D,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u(),n=f();function r(e){for(var t=`https://reactjs.org/docs/error-decoder.html?invariant=`+e,n=1;n`u`||window.document===void 0||window.document.createElement===void 0),l=Object.prototype.hasOwnProperty,d=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function h(e){return l.call(m,e)?!0:l.call(p,e)?!1:d.test(e)?m[e]=!0:(p[e]=!0,!1)}function g(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case`function`:case`symbol`:return!0;case`boolean`:return r?!1:n===null?(e=e.toLowerCase().slice(0,5),e!==`data-`&&e!==`aria-`):!n.acceptsBooleans;default:return!1}}function _(e,t,n,r){if(t==null||g(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return!1===t;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function v(e,t,n,r,i,a,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=o}var y={};`children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style`.split(` `).forEach(function(e){y[e]=new v(e,0,!1,e,null,!1,!1)}),[[`acceptCharset`,`accept-charset`],[`className`,`class`],[`htmlFor`,`for`],[`httpEquiv`,`http-equiv`]].forEach(function(e){var t=e[0];y[t]=new v(t,1,!1,e[1],null,!1,!1)}),[`contentEditable`,`draggable`,`spellCheck`,`value`].forEach(function(e){y[e]=new v(e,2,!1,e.toLowerCase(),null,!1,!1)}),[`autoReverse`,`externalResourcesRequired`,`focusable`,`preserveAlpha`].forEach(function(e){y[e]=new v(e,2,!1,e,null,!1,!1)}),`allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope`.split(` `).forEach(function(e){y[e]=new v(e,3,!1,e.toLowerCase(),null,!1,!1)}),[`checked`,`multiple`,`muted`,`selected`].forEach(function(e){y[e]=new v(e,3,!0,e,null,!1,!1)}),[`capture`,`download`].forEach(function(e){y[e]=new v(e,4,!1,e,null,!1,!1)}),[`cols`,`rows`,`size`,`span`].forEach(function(e){y[e]=new v(e,6,!1,e,null,!1,!1)}),[`rowSpan`,`start`].forEach(function(e){y[e]=new v(e,5,!1,e.toLowerCase(),null,!1,!1)});var b=/[\-:]([a-z])/g;function x(e){return e[1].toUpperCase()}`accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,null,!1,!1)}),`xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/1999/xlink`,!1,!1)}),[`xml:base`,`xml:lang`,`xml:space`].forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/XML/1998/namespace`,!1,!1)}),[`tabIndex`,`crossOrigin`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!1,!1)}),y.xlinkHref=new v(`xlinkHref`,1,!1,`xlink:href`,`http://www.w3.org/1999/xlink`,!0,!1),[`src`,`href`,`action`,`formAction`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!0,!0)});function S(e,t,n,r){var i=y.hasOwnProperty(t)?y[t]:null;(i===null?r||!(2s||i[o]!==a[s]){var c=` -`+i[o].replace(` at new `,` at `);return e.displayName&&c.includes(``)&&(c=c.replace(``,e.displayName)),c}while(1<=o&&0<=s);break}}}finally{re=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?ne(e):``}function ae(e){switch(e.tag){case 5:return ne(e.type);case 16:return ne(`Lazy`);case 13:return ne(`Suspense`);case 19:return ne(`SuspenseList`);case 0:case 2:case 15:return e=ie(e.type,!1),e;case 11:return e=ie(e.type.render,!1),e;case 1:return e=ie(e.type,!0),e;default:return``}}function oe(e){if(e==null)return null;if(typeof e==`function`)return e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case E:return`Fragment`;case T:return`Portal`;case O:return`Profiler`;case D:return`StrictMode`;case M:return`Suspense`;case N:return`SuspenseList`}if(typeof e==`object`)switch(e.$$typeof){case A:return(e.displayName||`Context`)+`.Consumer`;case k:return(e._context.displayName||`Context`)+`.Provider`;case j:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case P:return t=e.displayName||null,t===null?oe(e.type)||`Memo`:t;case F:t=e._payload,e=e._init;try{return oe(e(t))}catch{}}return null}function se(e){var t=e.type;switch(e.tag){case 24:return`Cache`;case 9:return(t.displayName||`Context`)+`.Consumer`;case 10:return(t._context.displayName||`Context`)+`.Provider`;case 18:return`DehydratedFragment`;case 11:return e=t.render,e=e.displayName||e.name||``,t.displayName||(e===``?`ForwardRef`:`ForwardRef(`+e+`)`);case 7:return`Fragment`;case 5:return t;case 4:return`Portal`;case 3:return`Root`;case 6:return`Text`;case 16:return oe(t);case 8:return t===D?`StrictMode`:`Mode`;case 22:return`Offscreen`;case 12:return`Profiler`;case 21:return`Scope`;case 13:return`Suspense`;case 19:return`SuspenseList`;case 25:return`TracingMarker`;case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t==`function`)return t.displayName||t.name||null;if(typeof t==`string`)return t}return null}function ce(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function le(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function ue(e){var t=le(e)?`checked`:`value`,n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=``+e[t];if(!e.hasOwnProperty(t)&&n!==void 0&&typeof n.get==`function`&&typeof n.set==`function`){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(e){r=``+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=``+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function z(e){e._valueTracker||=ue(e)}function de(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=le(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function B(e){if(e||=typeof document<`u`?document:void 0,e===void 0)return null;try{return e.activeElement||e.body}catch{return e.body}}function V(e,t){var n=t.checked;return R({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function fe(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 pe(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function me(e,t){pe(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`)?ge(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ge(e,t.type,ce(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function he(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 ge(e,t,n){(t!==`number`||B(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var _e=Array.isArray;function ve(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Ce.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(_e(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`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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`}]]),V=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`}]]),fe=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`}]]),pe=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`}]]),me=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`}]]),he=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`}]]),ge=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`}]]),_e=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`}]]),ve=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),ye=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),H=o(((e,t)=>{t.exports=ye()}))(),U=`telesrv.admin.lang`,be={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":`中文`,"language.ru":`RU`,"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.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`中文`,"language.ru":`RU`,"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.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка...`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтвержден`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.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":`中文`,"language.ru":`RU`,"login.heading":`Панель администратора`,"login.body":`Введите учетные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход...`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, премиум, верификация, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, количество участников, статус верификации.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтвержден`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звезд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество звёзд`,"account.starsAmountAria":`Указать количество начисляемых звёзд`,"account.grantStars":`Начислить звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновленные`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждено`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Указать и удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звездных подарков`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звездного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звездах`,"gifts.convertStars":`Звезд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звездные подарки еще не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звездах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. При добавлении и удалении веса permille перераспределяются до 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Разлогинить все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтвержденные`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Запустить тестовый запуск снова`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},xe=(0,g.createContext)(null);function Se({children:e}){let[t,n]=(0,g.useState)(()=>Ee());(0,g.useEffect)(()=>{try{localStorage.setItem(U,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Te(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Te(t,e,n)}),[t]);return(0,H.jsx)(xe.Provider,{value:r,children:e})}function Ce(){let e=(0,g.useContext)(xe);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function we(){let{lang:e,setLang:t,t:n}=Ce();return(0,H.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,H.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Te(e,t,n){let r=be[e][t]??be.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(U));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`:t===`ru`||t.startsWith(`ru-`)?`ru`: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,H.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Me(){let{t:e}=Ce();return(0,H.jsxs)(`div`,{className:`boot-screen`,children:[(0,H.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`loader-bar`})]})}function Ne({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=Ce(),o=t.path.startsWith(`/messages`),[s,c]=(0,g.useState)(o);(0,g.useEffect)(()=>{o&&c(!0)},[o]);async function l(){await x.logout().catch(()=>void 0),r()}return(0,H.jsxs)(`div`,{className:`shell`,children:[(0,H.jsxs)(`aside`,{className:`sidebar`,children:[(0,H.jsxs)(je,{className:`brand`,href:`/`,navigate:n,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,H.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,H.jsx)(Pe,{icon:(0,H.jsx)(oe,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(_e,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(fe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,H.jsx)(Pe,{icon:(0,H.jsx)(re,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,H.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,H.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,H.jsx)(ce,{size:16}),(0,H.jsx)(`span`,{children:a(`layout.messages`)}),(0,H.jsx)(F,{className:`nav-section-chevron`,size:15})]}),s&&(0,H.jsxs)(`div`,{className:`nav-children`,children:[(0,H.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,H.jsx)(Pe,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,H.jsxs)(`div`,{className:`sidebar-status`,children:[(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(V,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,H.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(ee,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,H.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(pe,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,H.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,H.jsxs)(`div`,{className:`workspace`,children:[(0,H.jsxs)(`header`,{className:`topbar`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:Ae(t.path,a)}),(0,H.jsx)(`h1`,{children:ke(t.path,a)})]}),(0,H.jsxs)(`div`,{className:`topbar-actions`,children:[(0,H.jsx)(we,{}),(0,H.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,H.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,H.jsx)(se,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,H.jsx)(`main`,{className:`content`,children:i})]})]})}function Pe({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,H.jsxs)(je,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,H.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,H.jsx)(`span`,{children:i})]})}function 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,H.jsxs)(`div`,{className:`page-frame`,children:[(0,H.jsxs)(`div`,{className:`page-title-row`,children:[(0,H.jsxs)(`div`,{children:[t&&(0,H.jsx)(`div`,{className:`eyebrow`,children:t}),(0,H.jsx)(`h2`,{children:e})]}),r&&(0,H.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ue({children:e}){return(0,H.jsx)(`div`,{className:`query-panel`,children:e})}function We({main:e,side:t}){return(0,H.jsxs)(`div`,{className:`split-layout`,children:[(0,H.jsx)(`div`,{className:`split-main`,children:e}),(0,H.jsx)(`aside`,{className:`split-side`,children:t})]})}function Ge({title:e,text:t,action:n}){return(0,H.jsxs)(`div`,{className:`section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`h2`,{children:e}),t&&(0,H.jsx)(`p`,{children:t})]}),n&&(0,H.jsx)(`div`,{className:`section-action`,children:n})]})}function Ke({children:e}){return(0,H.jsxs)(`div`,{className:`alert`,children:[(0,H.jsx)(O,{size:16}),` `,(0,H.jsx)(`span`,{children:e})]})}function G({children:e,tone:t=`neutral`}){return(0,H.jsx)(`span`,{className:`badge ${t}`,children:e})}function K({label:e,value:t,tone:n}){return(0,H.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{children:t})]})}function q({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,H.jsxs)(`div`,{className:`metric ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function J({label:e,value:t,mono:n=!1}){return(0,H.jsxs)(`div`,{className:`summary-item`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function qe({rows:e}){let{t}=Ce();return(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`audit.id`)}),(0,H.jsx)(`th`,{children:t(`audit.commandID`)}),(0,H.jsx)(`th`,{children:t(`audit.action`)}),(0,H.jsx)(`th`,{children:t(`audit.actor`)}),(0,H.jsx)(`th`,{children:t(`audit.status`)}),(0,H.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,H.jsx)(`th`,{children:t(`audit.reason`)}),(0,H.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[e.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,H.jsx)(`td`,{children:e.Action}),(0,H.jsx)(`td`,{children:e.Actor}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,H.jsx)(`td`,{children:ze(e.CreatedAt)})]},e.ID)),e.length===0&&(0,H.jsx)(Je,{colSpan:8})]})]})})}function Je({colSpan:e}){let{t}=Ce();return(0,H.jsx)(`tr`,{children:(0,H.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function Ye({label:e}){return(0,H.jsx)(`section`,{className:`surface`,children:(0,H.jsx)(`div`,{className:`loading-line`,children:e})})}function Xe({value:e}){return(0,H.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function Ze({onLogin:e}){let{t}=Ce(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1);async function c(t){t.preventDefault(),s(!0),a(``);try{e((await x.login(n)).actor)}catch(e){a(b(e))}finally{s(!1)}}return(0,H.jsx)(`main`,{className:`login-page`,children:(0,H.jsxs)(`section`,{className:`login-panel`,children:[(0,H.jsxs)(`div`,{className:`login-head`,children:[(0,H.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,H.jsxs)(`div`,{className:`login-head-actions`,children:[(0,H.jsx)(we,{}),(0,H.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,H.jsxs)(`div`,{className:`login-copy`,children:[(0,H.jsx)(`h1`,{children:t(`login.heading`)}),(0,H.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,H.jsx)(Ke,{children:i}),(0,H.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:t(`login.secret`)}),(0,H.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,H.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var Qe=m();function $e({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=Ce(),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1);function y(){d(``),p(null),h(``)}async function S(e){if(!u.trim()){h(s(`action.reasonRequired`));return}v(!0),h(``);try{let r={...n(),reason:u,confirm:e};p(await x.action(t,r)),e&&o?.()}catch(e){h(b(e))}finally{v(!1)}}let C=f?.dry_run&&!f.error,w=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,T=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:b(e)}}},[c,n]);return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,H.jsx)(`h2`,{children:e})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:s(`action.reason`)}),(0,H.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,H.jsxs)(`div`,{className:`command-preview`,children:[(0,H.jsxs)(`div`,{className:`preview-head`,children:[(0,H.jsx)(te,{size:14}),` `,s(`action.requestPreview`)]}),(0,H.jsx)(Xe,{value:JSON.stringify(T,null,2)})]}),m&&(0,H.jsx)(Ke,{children:m}),f&&(0,H.jsxs)(`div`,{className:`result-box`,children:[(0,H.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,H.jsx)(O,{size:16}):(0,H.jsx)(k,{size:16}),(0,H.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.commandID`)}),(0,H.jsx)(`strong`,{children:f.command_id})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.status`)}),(0,H.jsx)(`strong`,{children:f.status})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.dryRun`)}),(0,H.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,H.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,H.jsx)(Xe,{value:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(ue,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,H.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,H.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function et({rows:e,userID:t,onDone:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{a(new Set)},[t]);let o=(0,g.useMemo)(()=>e.filter(e=>!i.has(e.Hash)),[e,i]);function s(e){a(t=>e(t)),n()}return(0,H.jsxs)(`div`,{className:`authorization-block`,children:[(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:r(`auth.device`)}),(0,H.jsx)(`th`,{children:r(`auth.platform`)}),(0,H.jsx)(`th`,{children:r(`auth.ip`)}),(0,H.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,H.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[o.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,H.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,H.jsx)(`td`,{children:n.IP}),(0,H.jsx)(`td`,{children:ze(n.ActiveAt)}),(0,H.jsx)(`td`,{className:`device-actions-cell`,children:(0,H.jsxs)(`div`,{className:`device-actions`,children:[(0,H.jsx)($e,{label:r(`auth.revokeCurrent`),icon:(0,H.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,H.jsx)($e,{label:r(`auth.keepCurrent`),icon:(0,H.jsx)(fe,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>s(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),o.length===0&&(0,H.jsx)(Je,{colSpan:5})]})]})}),(0,H.jsx)(`div`,{className:`danger-zone`,children:(0,H.jsx)($e,{label:r(`auth.revokeAll`),icon:(0,H.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}=Ce(),[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,H.jsx)(Ke,{children:a});if(!r)return(0,H.jsx)(Ye,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,H.jsx)(He,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,H.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:Le(y)}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(y.Username)||n(`account.noUsername`),` · `,Fe(y.Phone)||n(`account.noPhone`)]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,H.jsx)(G,{tone:`good`,children:n(`account.premium`)}):(0,H.jsx)(G,{children:n(`account.notPremium`)}),r.Verified?(0,H.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(G,{children:n(`account.notVerified`)}),y.Frozen?(0,H.jsx)(G,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,H.jsx)(G,{children:n(`account.accountActive`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,H.jsx)(J,{label:n(`account.lastActive`),value:Be(r.LastSeenAt)||`-`}),(0,H.jsx)(J,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?Be(y.PremiumUntil):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,H.jsx)(J,{label:n(`common.updatedAt`),value:ze(y.UpdatedAt)||`-`}),(0,H.jsx)(J,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,H.jsx)(J,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,H.jsx)(J,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeSince`),value:r.Restriction.Since?ze(r.Restriction.Since):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeUntil`),value:r.Restriction.Until?ze(r.Restriction.Until):n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,H.jsx)(J,{label:n(`account.createdAt`),value:ze(y.CreatedAt)||`-`})]}),r.About&&(0,H.jsx)(`p`,{className:`about-text`,children:r.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,H.jsx)(et,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(qe,{rows:r.AuditLogs})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,H.jsx)($e,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!0,freeze_until:new Date(p).toISOString(),freeze_appeal_url:h.trim()}),onDone:v}),y.Frozen&&(0,H.jsx)($e,{label:n(`account.unfreezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)($e,{label:n(`account.setPremium`),icon:(0,H.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:W(l)}),onDone:v}),(0,H.jsx)($e,{label:n(`account.clearPremium`),icon:(0,H.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,H.jsx)($e,{label:n(`account.grantStars`),icon:(0,H.jsx)(me,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:W(d)}),onDone:v}),(0,H.jsx)($e,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]})]})})})}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}=Ce(),[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,H.jsxs)(He,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(q,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,H.jsx)(q,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,H.jsx)(q,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`account.userID`)}),(0,H.jsx)(`th`,{children:t(`account.phone`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.name`)}),(0,H.jsx)(`th`,{children:t(`common.device`)}),(0,H.jsx)(`th`,{children:t(`account.lastActive`)}),(0,H.jsx)(`th`,{children:t(`account.premium`)}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`account.frozen`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Fe(n.Phone)}),(0,H.jsx)(`td`,{children:Ie(n.Username)}),(0,H.jsx)(`td`,{children:Le(n)}),(0,H.jsx)(`td`,{children:n.DeviceCount}),(0,H.jsx)(`td`,{children:ze(n.LastActiveAt)}),(0,H.jsx)(`td`,{children:n.PremiumUntil>0?(0,H.jsxs)(G,{tone:`good`,children:[t(`account.premium`),` `,Be(n.PremiumUntil)]}):(0,H.jsx)(G,{children:t(`common.none`)})}),(0,H.jsx)(`td`,{children:n.Verified?(0,H.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(G,{children:t(`account.notVerified`)})}),(0,H.jsx)(`td`,{children:n.Frozen?(0,H.jsx)(G,{tone:`danger`,children:t(`account.frozen`)}):(0,H.jsx)(G,{children:t(`common.normal`)})}),(0,H.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Je,{colSpan:11})]})]})})]})}function ot({id:e,navigate:t}){let{t:n}=Ce(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await x.channel(e))}catch(e){o(b(e))}}if((0,g.useEffect)(()=>{s()},[e]),a)return(0,H.jsx)(Ke,{children:a});if(!r)return(0,H.jsx)(Ye,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,H.jsx)(He,{title:`${Re(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,H.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(G,{children:Re(c,n)}),c.Verified?(0,H.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(G,{children:n(`account.notVerified`)}),c.Deleted?(0,H.jsx)(G,{tone:`danger`,children:n(`common.deleted`)}):(0,H.jsx)(G,{children:n(`common.valid`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,H.jsx)(J,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,H.jsx)(J,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,H.jsx)(J,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,H.jsx)(J,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,H.jsx)(J,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,H.jsx)(J,{label:n(`account.createdAt`),value:Be(c.Date)||`-`}),(0,H.jsx)(J,{label:n(`common.updatedAt`),value:ze(c.UpdatedAt)||`-`})]}),c.About&&(0,H.jsx)(`p`,{className:`about-text`,children:c.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(qe,{rows:r.AuditLogs})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,H.jsx)(Xe,{value:r.ChannelJSON})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,H.jsx)($e,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s})]})})})}function st({navigate:e}){let{t}=Ce(),[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,H.jsxs)(He,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(q,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,H.jsx)(q,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,H.jsx)(q,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`channel.channelID`)}),(0,H.jsx)(`th`,{children:t(`channel.kind`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`channel.title`)}),(0,H.jsx)(`th`,{children:t(`common.members`)}),(0,H.jsx)(`th`,{children:t(`common.admins`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Re(n,t)}),(0,H.jsx)(`td`,{children:Ie(n.Username)}),(0,H.jsx)(`td`,{children:n.Title}),(0,H.jsx)(`td`,{children:n.ParticipantsCount}),(0,H.jsx)(`td`,{children:n.AdminsCount}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Verified?(0,H.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(G,{children:t(`account.notVerified`)})}),(0,H.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Je,{colSpan:10})]})]})})]})}function ct({navigate:e}){let{t}=Ce();return(0,H.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,H.jsxs)(`section`,{className:`overview-band`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,H.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,H.jsxs)(`div`,{className:`overview-metrics`,children:[(0,H.jsx)(K,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,H.jsx)(K,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,H.jsx)(K,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,H.jsxs)(`div`,{className:`command-grid`,children:[(0,H.jsx)(lt,{icon:(0,H.jsx)(_e,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,H.jsx)(lt,{icon:(0,H.jsx)(fe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,H.jsx)(lt,{icon:(0,H.jsx)(ce,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,H.jsxs)(`section`,{className:`work-strip`,children:[(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(k,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(ae,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(L,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(te,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function lt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,H.jsxs)(je,{className:`launcher`,href:r,navigate:i,children:[(0,H.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,H.jsxs)(`span`,{className:`launcher-copy`,children:[(0,H.jsx)(`strong`,{children:t}),(0,H.jsx)(`span`,{children:n})]}),(0,H.jsx)(I,{size:16})]})}function ut({channelID:e,msgID:t,navigate:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.groupMessage(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(Ke,{children:o});if(!i)return(0,H.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(He,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,H.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:Be(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(G,{children:r(`common.survived`)}),l.Pinned&&(0,H.jsx)(G,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,H.jsx)(G,{children:r(`messages.channelPost`)}),(0,H.jsxs)(G,{children:[`pts `,l.PTS]})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,H.jsx)(J,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,H.jsx)(J,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,H.jsx)(Xe,{value:i.MessageJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,H.jsx)(Xe,{value:i.ChannelJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.messageId`)}),(0,H.jsx)(`th`,{children:r(`common.sender`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:e.MessageID}),(0,H.jsx)(`td`,{children:e.SenderUserID}),(0,H.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Je,{colSpan:6})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.eventJson`)}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,H.jsx)(Xe,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,H.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function dt({label:e,value:t,onChange:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.accounts(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(ve,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(P,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:Le(t)}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:Ie(t.Username)||Fe(t.Phone)||`-`})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:Le(e)}),(0,H.jsx)(`span`,{children:Ie(e.Username)||Fe(e.Phone)||`-`}),e.Verified?(0,H.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(G,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function ft({label:e,value:t,onChange:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.channels(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(ve,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(P,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:t.Title||`-`}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:Ie(t.Username)||Re(t,r)})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:e.Title||`-`}),(0,H.jsx)(`span`,{children:Ie(e.Username)||Re(e,r)}),e.Verified?(0,H.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(G,{children:Re(e,r)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function pt({navigate:e}){let{t}=Ce(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`100`),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``);async function m(e=!1){if(p(``),!n){p(t(`messages.selectChannel`));return}let r=new URLSearchParams({channel_id:String(n.ID),limit:c});if(e&&u?.rows.length){let e=u.rows[u.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.ID)),a(String(e.Date)),s(String(e.ID))}else i&&r.set(`before_date`,i),o&&r.set(`before_id`,o);try{d(await x.groupMessages(r))}catch(e){p(b(e))}}function h(e){r(e),a(``),s(``),d(null)}let _=u?.rows??[];return(0,H.jsxs)(He,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,H.jsx)(Ke,{children:f}),(0,H.jsxs)(Ue,{children:[(0,H.jsx)(`div`,{className:`message-selector-grid single`,children:(0,H.jsx)(ft,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`messages.currentPage`),value:String(_.length)}),(0,H.jsx)(q,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,H.jsx)(q,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,H.jsx)(q,{label:t(`messages.channelGroup`),value:n?`${n.Title||Re(n,t)} (${n.ID})`:`-`})]}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:`From Peer`}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.views`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[_.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:Be(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,H.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.ViewsCount}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,H.jsx)(G,{tone:`warn`,children:t(`messages.pinned`)}):(0,H.jsx)(G,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,H.jsx)(Je,{colSpan:9})]})]})})]})}function mt({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=Ce(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.message(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(Ke,{children:o});if(!i)return(0,H.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(He,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,H.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,H.jsx)(We,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:Be(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(G,{children:r(`common.survived`)}),(0,H.jsxs)(G,{children:[`pts `,l.PTS]}),(0,H.jsx)(G,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(J,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,H.jsx)(J,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,H.jsx)(J,{label:r(`common.time`),value:Be(l.Date)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,H.jsx)(Xe,{value:i.MessageJSON})]}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,H.jsx)(Xe,{value:i.DialogJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,H.jsx)(Xe,{value:i.PrivateJSON})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Je,{colSpan:4})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(Ge,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`ID`}),(0,H.jsx)(`th`,{children:r(`account.userID`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.status`)}),(0,H.jsx)(`th`,{children:r(`messages.attempts`)}),(0,H.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{children:e.TargetUserID}),(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.EventType}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.Attempts}),(0,H.jsx)(`td`,{children:ze(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,H.jsx)(Je,{colSpan:7})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,H.jsx)($e,{label:r(`messages.deleteThis`),icon:(0,H.jsx)(he,{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}=Ce(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`100`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,S]=(0,g.useState)(``),[C,w]=(0,g.useState)(`1`),[T,E]=(0,g.useState)(null),[D,O]=(0,g.useState)(``);async function k(e=!1){if(O(``),!n||!i){O(t(`messages.selectPrivatePeers`));return}let r=new URLSearchParams({owner_user_id:String(n.ID),peer_id:String(i.ID),limit:u});if(e&&T?.rows.length){let e=T.rows[T.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.BoxID)),s(String(e.Date)),l(String(e.BoxID))}else o&&r.set(`before_date`,o),c&&r.set(`before_id`,c);try{E(await x.messages(r))}catch(e){O(b(e))}}function A(e){r(e),s(``),l(``),E(null)}function j(e){a(e),s(``),l(``),E(null)}return(0,H.jsxs)(He,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,H.jsx)(Ke,{children:D}),(0,H.jsxs)(Ue,{children:[(0,H.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,H.jsx)(dt,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,H.jsx)(dt,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,H.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(q,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,H.jsx)(q,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,H.jsx)(q,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,H.jsx)(q,{label:t(`messages.ownerPeer`),value:n&&i?`${Le(n)} / ${Le(i)}`:`-`})]}),(0,H.jsxs)(`div`,{className:`operation-row`,children:[(0,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(he,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,H.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsx)($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,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(ie,{size:15}),` `,t(`messages.clearHistory`)]}),(0,H.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,H.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,H.jsx)($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,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:t(`messages.direction`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,H.jsx)(`td`,{children:Be(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,H.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):(0,H.jsx)(G,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,H.jsx)(I,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,H.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`)},H.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)}},H.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`)))},H.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())},H.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},H.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},H.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`))}},H.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`)},H.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)},H.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),ye(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}(),xe=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Se=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=xe.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Ce=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return Se(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=Ce.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=be.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?U.searchAnimations(Je,qe,Ye):U.searchAnimations()}function Qe(e){re(e)}function $e(e){ue(e)}function et(e){return qe===!0&&(e.animationData=JSON.parse(Je)),U.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=U.play,J.pause=U.pause,J.setLocationHref=Xe,J.togglePause=U.togglePause,J.setSpeed=U.setSpeed,J.setDirection=U.setDirection,J.stop=U.stop,J.searchAnimations=Ze,J.registerAnimation=U.registerAnimation,J.loadAnimation=et,J.setSubframeRendering=Qe,J.resize=U.resize,J.goToAndStop=U.goToAndStop,J.destroy=U.destroy,J.setQuality=tt,J.inBrowser=nt,J.installPlugin=rt,J.freeze=U.freeze,J.unfreeze=U.unfreeze,J.setVolume=U.setVolume,J.mute=U.mute,J.unmute=U.unmute,J.getRegisteredAnimations=U.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=be.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+xe||!x?(T=(m+xe-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)},ge(`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=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function bt(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:vt(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function St(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=yt[e.length%yt.length];return{key:vt(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var Ct=e=>bt([xt(e,0),xt(e,1)]),wt=()=>{let e=St([]);return bt([e,St([e])])};function Tt({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,H.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function Et({giftID:e,attribute:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(!1);return(0,g.useEffect)(()=>{let n=!1;return a(!1),x.giftCollectibleAnimation(e,t.kind,t.id).then(e=>{n||r(e)}).catch(()=>{n||a(!0)}),()=>{n=!0}},[e,t.id,t.kind]),i?(0,H.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,H.jsx)(Tt,{data:n,compact:!0}):(0,H.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,H.jsx)(A,{className:`spin`,size:15})})}async function Dt(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 Ot=e=>Number.parseInt(e.replace(`#`,``),16),kt=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function At({gift:e,onClose:t,onPublished:n}){let{t:r}=Ce(),[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)(()=>Ct(`model`)),[D,O]=(0,g.useState)(()=>Ct(`pattern`)),[M,N]=(0,g.useState)(wt);(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 Dt(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function ee(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||M.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=M.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:M.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:Ot(e.center),edge_color:Ot(e.edge),pattern_color:Ot(e.pattern),text_color:Ot(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}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,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,H.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(G,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(bt([...t,xt(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,H.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,H.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,H.jsxs)(`label`,{className:`collectible-file`,children:[(0,H.jsx)(`span`,{children:r(`gifts.animation`)}),(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:t=>void L(e,i,t.target.files?.[0]??null)}),(0,H.jsxs)(`em`,{children:[(0,H.jsx)(R,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,H.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,H.jsx)(Tt,{data:i.animation,compact:!0}):(0,H.jsx)(j,{size:16})}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(bt(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(he,{size:14})}),i.fileError&&(0,H.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,H.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,H.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,H.jsxs)(`div`,{className:`collectible-loading`,children:[(0,H.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,H.jsxs)(`section`,{className:`collectible-active`,children:[(0,H.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(ne,{size:18}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,H.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,H.jsx)(G,{tone:`good`,children:r(`collectibles.published`)})]}),(0,H.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(Et,{giftID:e.GiftID,attribute:t}),(0,H.jsxs)(`div`,{children:[(0,H.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,H.jsx)(G,{children:`crafted`})]}),(0,H.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,kt(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, #${(e.center_color??0).toString(16).padStart(6,`0`)}, #${(e.edge_color??0).toString(16).padStart(6,`0`)})`,color:`#${(e.text_color??16777215).toString(16).padStart(6,`0`)}`},children:`Aa`}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e.name}),(0,H.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,kt(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,H.jsxs)(`div`,{className:`collectible-empty`,children:[(0,H.jsx)(ne,{size:22}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,H.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,H.jsxs)(`section`,{className:`collectible-definition`,children:[(0,H.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,H.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.reason`)}),(0,H.jsx)(`input`,{value:C,maxLength:1e3,placeholder:r(`gifts.reasonPlaceholder`),onChange:e=>w(e.target.value)})]})]}),ie(`models`,T,E),ie(`patterns`,D,O),(0,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,H.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(G,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N(bt([...M,St(M)])),F()},children:[(0,H.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,H.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:e.name,maxLength:128,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,name:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:e.rarity,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,rarity:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:e.sortOrder,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,sortOrder:t.target.value}:n)),F()}})]}),[`center`,`edge`,`pattern`,`text`].map(t=>(0,H.jsxs)(`label`,{className:`collectible-color`,children:[(0,H.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,H.jsx)(`input`,{type:`color`,value:e[t],onChange:n=>{N(M.map(r=>r.key===e.key?{...r,[t]:n.target.value}:r)),F()}})]},t)),(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length<=2,onClick:()=>{N(bt(M.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(he,{size:14})})]},e.key))})]})]}),u&&(0,H.jsx)(Ke,{children:u}),f&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,H.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:te,disabled:c,children:[c?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(fe,{size:15}),r(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:re,disabled:c||!f,children:[(0,H.jsx)(ge,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function jt(e){return e.model_count+e.pattern_count+e.backdrop_count}function Mt(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Y({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,H.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,H.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,H.jsx)(`span`,{children:s})}),(0,H.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,H.jsx)(le,{size:14}):(0,H.jsx)(ue,{size:14})})]})}function Nt({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=gt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,H.jsx)(`div`,{className:`gift-animation-shell`,children:(0,H.jsx)(`div`,{className:`gift-animation`,ref:t})})}function Pt(){let{t:e}=Ce(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,te]=(0,g.useState)(`50`),[re,ie]=(0,g.useState)(`50`),[ae,oe]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[V,pe]=(0,g.useState)(null),[me,he]=(0,g.useState)(!1),[_e,ye]=(0,g.useState)(``),[U,be]=(0,g.useState)(``);async function xe(){ye(``);try{n((await x.gifts()).Gifts??[])}catch(e){ye(b(e))}}(0,g.useEffect)(()=>{xe()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>be(b(e)))},[a,d,p.length]);let Se=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),we=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),Te=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),Ee=(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 De(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae)})),r.set(`file`,l,l.name),r}function Oe(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function ke(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),te(String(t.stars)),ie(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),pe(null)}async function Ae(){he(!0),be(``),pe(null);try{pe(d===`official`?await x.importOfficialGift(Oe(!1)):await x.importGift(De(!1)))}catch(e){be(b(e))}finally{he(!1)}}async function je(){if(V){he(!0),be(``);try{d===`official`?await x.importOfficialGift(Oe(!0,V.command_id)):await x.importGift(De(!0,V.command_id)),pe(null),u(null),F(`0`),L(``),C(``),await xe(),o(!1)}catch(e){be(b(e))}finally{he(!1)}}}function Me(){F(`0`),L(``),te(`50`),ie(`50`),oe(`0`),ce(!0),ue(``),u(null),pe(null),be(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Ne(e){F(e.GiftID),L(e.Title),te(String(e.Stars)),ie(String(e.ConvertStars)),oe(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),pe(null),be(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,H.jsxs)(He,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>xe(),disabled:me,children:[(0,H.jsx)(de,{size:15}),` `,e(`common.refresh`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Me,children:[(0,H.jsx)(z,{size:15}),` `,e(`gifts.add`)]})]}),children:[_e&&(0,H.jsx)(Ke,{children:_e}),(0,H.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,H.jsx)(q,{label:e(`gifts.total`),value:String(t.length)}),(0,H.jsx)(q,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,H.jsx)(q,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,H.jsx)(q,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,H.jsx)(Ue,{children:(0,H.jsxs)(`div`,{className:`toolbar`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,H.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Ee.length,total:t.length})})]})}),(0,H.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:e(`gifts.animation`)}),(0,H.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,H.jsx)(`th`,{children:e(`gifts.title`)}),(0,H.jsx)(`th`,{children:e(`gifts.price`)}),(0,H.jsx)(`th`,{children:e(`gifts.source`)}),(0,H.jsx)(`th`,{children:e(`gifts.received`)}),(0,H.jsx)(`th`,{children:e(`common.status`)}),(0,H.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,H.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[Ee.map(t=>(0,H.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,H.jsx)(`td`,{children:(0,H.jsx)(Y,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,H.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,H.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,H.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(G,{children:t.SourceFormat}),(0,H.jsx)(`span`,{className:`gift-source-size`,children:Mt(t.AnimationSize)})]}),(0,H.jsx)(`td`,{children:t.ReceivedCount}),(0,H.jsx)(`td`,{children:(0,H.jsx)(G,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,H.jsx)(`td`,{children:ze(t.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,H.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,H.jsx)(ne,{size:13}),e(`collectibles.manage`)]}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Ne(t),children:e(`gifts.replace`)}),(0,H.jsx)($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 xe()})]})})]},t.GiftID)),Ee.length===0&&(0,H.jsx)(Je,{colSpan:9})]})]})}),a&&(0,Qe.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,H.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:me,"aria-label":e(`action.close`),children:(0,H.jsx)(ve,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${V?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${V?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,H.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),pe(null)},children:e(`gifts.officialSource`)}),(0,H.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),pe(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,H.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:p.length}),(0,H.jsx)(`span`,{children:`SHA-256`})]})]}),(0,H.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(B,{size:15}),(0,H.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,H.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:Te.length,total:p.length})})]}),(0,H.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,H.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,H.jsx)(`span`,{children:we[t]})]},t))}),(0,H.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[Te.map(t=>{let n=t.source_gift_id===S;return(0,H.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>ke(t),children:[(0,H.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,H.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,H.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,H.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,H.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:jt(t)})})]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),Te.length===0&&(0,H.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),Se&&(0,H.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,H.jsx)(Nt,{sourceGiftID:Se.source_gift_id}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:Se.title||e(`gifts.officialUnnamed`,{id:Se.source_gift_id})}),(0,H.jsx)(`span`,{className:`mono`,children:Se.source_gift_id}),(0,H.jsxs)(`small`,{children:[Se.model_count,` `,e(`collectibles.models`),` · `,Se.pattern_count,` `,e(`collectibles.patterns`),` · `,Se.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:Se.can_upgrade?`yes`:`no`,children:Se.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:Se.can_craft?`craft`:`no`,children:Se.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),Se?.can_upgrade&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),pe(null)}})]})]})]})]}):(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-file-icon`,children:(0,H.jsx)(R,{size:22})}),(0,H.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,H.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,H.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,H.jsx)(`small`,{children:l?Mt(l.size):e(`gifts.fileHint`)})]}),(0,H.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.title`)}),(0,H.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.stars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{te(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:re,onChange:e=>{ie(e.target.value),pe(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:ae,onChange:e=>{oe(e.target.value),pe(null)}})]})]}),(0,H.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,H.jsx)(`span`,{children:e(`gifts.reason`)}),(0,H.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),pe(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),U&&(0,H.jsx)(Ke,{children:U}),V&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,H.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(V.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:me,children:e(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:Ae,disabled:me,children:[me?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(fe,{size:15}),e(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,disabled:me||!V,children:[(0,H.jsx)(ge,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,H.jsx)(At,{gift:s,onClose:()=>c(null),onPublished:()=>void xe()})]})}function Ft({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1];return n?(0,H.jsx)(tt,{id:Number(n),navigate:t}):r?(0,H.jsx)(ot,{id:Number(r),navigate:t}):e.path===`/accounts`?(0,H.jsx)(at,{navigate:t}):e.path===`/channels`?(0,H.jsx)(st,{navigate:t}):e.path===`/gifts`?(0,H.jsx)(Pt,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,H.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,H.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,H.jsx)(pt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,H.jsx)(ht,{navigate:t}):(0,H.jsx)(ct,{navigate:t})}function It(){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,H.jsx)(Me,{}):e===null?(0,H.jsx)(Ze,{onLogin:t}):(0,H.jsx)(Ne,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,H.jsx)(Ft,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,H.jsx)(g.StrictMode,{children:(0,H.jsx)(Se,{children:(0,H.jsx)(It,{})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/index.html b/cmd/telesrv-admin/web/dist/index.html index 18651f58..8da7a220 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -1,13 +1,30 @@ - - - - - - telesrv admin - - - - -
- - + + + + + + telesrv admin + + + + + +
+ + diff --git a/cmd/telesrv-admin/web/index.html b/cmd/telesrv-admin/web/index.html index 81352931..3f63af1f 100644 --- a/cmd/telesrv-admin/web/index.html +++ b/cmd/telesrv-admin/web/index.html @@ -4,6 +4,23 @@ telesrv admin +
diff --git a/cmd/telesrv-admin/web/src/components/Layout.tsx b/cmd/telesrv-admin/web/src/components/Layout.tsx index 41c81c64..d9600970 100644 --- a/cmd/telesrv-admin/web/src/components/Layout.tsx +++ b/cmd/telesrv-admin/web/src/components/Layout.tsx @@ -14,6 +14,7 @@ import { useEffect, useState, type ReactNode } from "react"; import { api } from "../api"; import { LanguageSwitch, useI18n } from "../i18n"; import { type Navigate, type RouteState, routeSubtitle, routeTitle } from "../routing"; +import { ThemeSwitch } from "../theme"; import { AppLink } from "./AppLink"; export function BootScreen() { @@ -123,6 +124,7 @@ export function Shell({

{routeTitle(route.path, t)}

+ {t("layout.actor", { actor })}
+ {t("app.localAccess")}
diff --git a/cmd/telesrv-admin/web/src/styles/01-foundation.css b/cmd/telesrv-admin/web/src/styles/01-foundation.css index c8b7f42d..f80b1285 100644 --- a/cmd/telesrv-admin/web/src/styles/01-foundation.css +++ b/cmd/telesrv-admin/web/src/styles/01-foundation.css @@ -1,24 +1,155 @@ :root { color-scheme: light; - --bg: #f3f5f7; + + /* Surfaces */ + --bg: #eef1f5; + --bg-accent: #e7ecf1; --panel: #ffffff; - --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: rgba(23, 109, 97, 0.16); - --shadow: 0 18px 52px rgba(16, 24, 40, 0.14); + --panel-subtle: #f5f8fb; + --panel-strong: #eef2f6; + --surface-soft: #f2f7f6; + --overlay: rgba(24, 34, 47, 0.42); + --topbar-bg: rgba(255, 255, 255, 0.86); + + /* Lines */ + --line: #e5eaf0; + --line-strong: #d3dce4; + + /* Text */ + --heading: #253040; + --text: #333f4d; + --text-soft: #45525f; + --muted: #6d7885; + --muted-2: #9aa4b1; + + /* Brand */ + --brand: #1f7d6f; + --brand-strong: #196155; + --brand-2: #3a6cae; + --brand-tint: #e8f4f0; + --brand-tint-border: #c8e2db; + --brand-tint-text: #235d53; + + /* Semantic */ + --good: #1f8a57; + --good-tint: #eaf6ef; + --good-border: #c1e1cf; + --warn: #a86a12; + --warn-tint: #fcf4e4; + --warn-border: #e7d09e; + --danger: #c0392b; + --danger-tint: #fcefec; + --danger-border: #eecac3; + --danger-text: #8f2f27; + + /* Accent (collectibles / craft) */ + --purple: #6a4fa3; + --purple-tint: #f4effb; + --purple-border: #dcd0f0; + --purple-text: #5a4590; + + /* Inputs & controls */ + --input-bg: #ffffff; + --btn-bg: #ffffff; + --btn-text: #29323d; + --btn-hover: #f4f7fa; + --switch-track: #c8d0d6; + + /* Code / JSON blocks */ + --code-bg: #1b2733; + --code-text: #d6e3ef; + --code-border: #2b3a49; + + /* Sidebar */ + --sidebar: #1c2530; + --sidebar-soft: #26313d; + --sidebar-line: #313c4a; + --sidebar-row: #232d38; + --sidebar-text: #dbe3ec; + --sidebar-muted: #8b98a8; + --sidebar-faint: #7c8a9a; + --sidebar-heading: #ffffff; + + /* Effects */ + --focus: rgba(31, 125, 111, 0.16); + --shadow: 0 12px 34px rgba(24, 39, 56, 0.1); + --shadow-sm: 0 2px 10px rgba(24, 39, 56, 0.05); + --shadow-brand: 0 8px 22px rgba(31, 125, 111, 0.22); + + /* Radii */ + --radius-xs: 8px; + --radius-sm: 9px; + --radius: 11px; + --radius-lg: 14px; +} + +[data-theme="dark"] { + color-scheme: dark; + + --bg: #0f141a; + --bg-accent: #131a22; + --panel: #171f28; + --panel-subtle: #1c2530; + --panel-strong: #212c38; + --surface-soft: #1a232d; + --overlay: rgba(5, 8, 12, 0.62); + --topbar-bg: rgba(21, 28, 36, 0.86); + + --line: #29333f; + --line-strong: #38434f; + + --heading: #eef3f8; + --text: #d5dde6; + --text-soft: #c2ccd6; + --muted: #98a4b1; + --muted-2: #6d7885; + + --brand: #37a596; + --brand-strong: #45b6a6; + --brand-2: #6fa8e6; + --brand-tint: #14322d; + --brand-tint-border: #245349; + --brand-tint-text: #7fd3c4; + + --good: #47c281; + --good-tint: #12301f; + --good-border: #245639; + --warn: #e0aa4d; + --warn-tint: #322810; + --warn-border: #574413; + --danger: #e6695c; + --danger-tint: #35201d; + --danger-border: #5c332d; + --danger-text: #f0a49b; + + --purple: #ac90e2; + --purple-tint: #221b31; + --purple-border: #3d3357; + --purple-text: #c9b6ef; + + --input-bg: #131a22; + --btn-bg: #1e2731; + --btn-text: #dbe2ea; + --btn-hover: #26313d; + --switch-track: #3a454f; + + --code-bg: #0c1218; + --code-text: #cdd9e5; + --code-border: #232f3b; + + --sidebar: #10151b; + --sidebar-soft: #1c242f; + --sidebar-line: #262f3a; + --sidebar-row: #161d25; + --sidebar-text: #cbd4de; + --sidebar-muted: #7c8794; + --sidebar-faint: #6f7b88; + --sidebar-heading: #f0f4f8; + + --focus: rgba(55, 165, 150, 0.24); + --shadow: 0 16px 40px rgba(0, 0, 0, 0.46); + --shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.38); + --shadow-brand: 0 8px 22px rgba(55, 165, 150, 0.26); } * { @@ -35,7 +166,10 @@ body { margin: 0; color: var(--text); background: var(--bg); - font: 13px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font: 13px/1.5 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + transition: background-color 200ms ease, color 200ms ease; } button, @@ -64,7 +198,7 @@ a { gap: 16px; overflow-y: auto; padding: 18px 12px; - color: #eef2f6; + color: var(--sidebar-text); background: var(--sidebar); border-right: 1px solid var(--sidebar-line); } @@ -82,7 +216,7 @@ a { } .brand-elevated .brand-mark { - box-shadow: 0 8px 24px rgba(23, 109, 97, 0.26); + box-shadow: var(--shadow-brand); } .brand-mark { @@ -93,7 +227,7 @@ a { color: #ffffff; background: var(--brand); border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 8px; + border-radius: var(--radius-sm); font-weight: 800; } @@ -106,16 +240,17 @@ a { .brand small { display: block; margin-top: 3px; - color: #aeb8c4; + color: var(--sidebar-muted); font-size: 11px; } .sidebar-label { padding: 0 8px; - color: #8492a6; + color: var(--sidebar-faint); font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: 0.04em; } .nav-list { @@ -136,26 +271,27 @@ a { align-items: center; gap: 9px; padding: 0 10px; - color: #8fa0b4; + color: var(--sidebar-muted); background: transparent; border: 1px solid transparent; - border-radius: 7px; + border-radius: var(--radius-sm); cursor: pointer; font-size: 12px; font-weight: 800; text-align: left; + transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease; } .nav-section-toggle:hover, .nav-section.active .nav-section-toggle { - color: #ffffff; + color: var(--sidebar-heading); background: var(--sidebar-soft); - border-color: #34404d; + border-color: var(--sidebar-line); } .nav-section-chevron { justify-self: end; - color: #8fa0b4; + color: var(--sidebar-muted); transition: transform 140ms ease; } @@ -176,24 +312,25 @@ a { align-items: center; gap: 9px; padding: 0 10px; - color: #c6d0dc; + color: var(--sidebar-text); border: 1px solid transparent; - border-radius: 7px; + border-radius: var(--radius-sm); + transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease; } .nav-dot { width: 6px; height: 6px; justify-self: center; - background: #687789; + background: var(--sidebar-faint); border-radius: 999px; } .nav-item:hover, .nav-item.active { - color: #ffffff; + color: var(--sidebar-heading); background: var(--sidebar-soft); - border-color: #34404d; + border-color: var(--sidebar-line); } .nav-item.active .nav-dot { @@ -213,14 +350,14 @@ a { align-items: center; gap: 7px; padding: 0 8px; - color: #cbd5df; - background: #171d25; - border: 1px solid #27313c; - border-radius: 7px; + color: var(--sidebar-text); + background: var(--sidebar-row); + border: 1px solid var(--sidebar-line); + border-radius: var(--radius-sm); } .runtime-row strong { - color: #ffffff; + color: var(--sidebar-heading); font-size: 11px; } @@ -238,13 +375,14 @@ a { justify-content: space-between; gap: 18px; padding: 12px 24px; - background: rgba(255, 255, 255, 0.94); + background: var(--topbar-bg); border-bottom: 1px solid var(--line); backdrop-filter: blur(12px); } .topbar h1 { margin: 2px 0 0; + color: var(--heading); font-size: 20px; line-height: 1.2; } @@ -281,6 +419,7 @@ a { border-radius: 999px; cursor: pointer; font-weight: 800; + transition: color 140ms ease, background-color 140ms ease; } .language-switch button.active { @@ -293,12 +432,36 @@ a { outline-offset: 2px; } +.theme-toggle { + display: inline-grid; + width: 34px; + height: 34px; + place-items: center; + color: var(--muted); + background: var(--panel-subtle); + border: 1px solid var(--line); + border-radius: 999px; + cursor: pointer; + transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease; +} + +.theme-toggle:hover { + color: var(--brand); + border-color: var(--brand-tint-border); + background: var(--brand-tint); +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--brand); + outline-offset: 2px; +} + .actor-pill { display: inline-flex; min-height: 30px; align-items: center; padding: 0 10px; - color: #344054; + color: var(--text-soft); background: var(--panel-subtle); border: 1px solid var(--line); border-radius: 999px; @@ -315,4 +478,5 @@ a { font-size: 11px; font-weight: 800; text-transform: uppercase; + letter-spacing: 0.04em; } diff --git a/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css b/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css index ea86437d..2c3e0be3 100644 --- a/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css +++ b/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css @@ -9,7 +9,8 @@ min-width: 0; background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); + box-shadow: var(--shadow-sm); } .overview-band { @@ -25,6 +26,7 @@ .section-head h2, .modal h2 { margin: 0; + color: var(--heading); font-size: 18px; line-height: 1.25; } @@ -47,7 +49,7 @@ padding: 10px; background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 7px; + border-radius: var(--radius-sm); } .status-item span, @@ -70,16 +72,16 @@ .status-item.good, .metric.good { - border-color: #afd8bf; + border-color: var(--good-border); } .status-item.warn, .metric.warn { - border-color: #e7c77e; + border-color: var(--warn-border); } .metric.danger { - border-color: #efb4ad; + border-color: var(--danger-border); } .command-grid { @@ -97,11 +99,15 @@ padding: 14px; background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; } .launcher:hover { - border-color: var(--brand); + border-color: var(--brand-tint-border); + box-shadow: var(--shadow); + transform: translateY(-1px); } .launcher-icon { @@ -110,9 +116,9 @@ height: 38px; place-items: center; color: var(--brand); - background: #edf7f4; - border: 1px solid #c9e2dc; - border-radius: 8px; + background: var(--brand-tint); + border: 1px solid var(--brand-tint-border); + border-radius: var(--radius-sm); } .launcher-copy { @@ -121,6 +127,7 @@ } .launcher-copy strong { + color: var(--heading); font-size: 15px; } @@ -140,10 +147,10 @@ align-items: center; gap: 8px; padding: 0 10px; - color: #344054; + color: var(--text-soft); background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-sm); } .page-frame { @@ -165,7 +172,7 @@ padding: 10px; background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-sm); } .toolbar { @@ -195,9 +202,9 @@ min-width: 0; gap: 8px; padding: 10px; - background: #ffffff; + background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-sm); } .picker-head { @@ -206,7 +213,7 @@ align-items: center; justify-content: space-between; gap: 8px; - color: #344054; + color: var(--text-soft); font-weight: 800; } @@ -217,10 +224,10 @@ align-items: center; gap: 8px; padding: 7px 9px; - color: #0f3f38; - background: #eef8f5; - border: 1px solid #b9dcd3; - border-radius: 7px; + color: var(--brand-tint-text); + background: var(--brand-tint); + border: 1px solid var(--brand-tint-border); + border-radius: var(--radius-sm); } .selected-entity strong, @@ -237,8 +244,9 @@ } .selected-entity div span { - color: #52606d; + color: var(--brand-tint-text); font-size: 11px; + opacity: 0.85; } .picker-search { @@ -250,7 +258,7 @@ padding: 0 6px 0 9px; background: var(--panel-subtle); border: 1px solid var(--line-strong); - border-radius: 7px; + border-radius: var(--radius-sm); } .picker-search input { @@ -267,7 +275,7 @@ max-height: 236px; overflow: auto; border: 1px solid var(--line); - border-radius: 7px; + border-radius: var(--radius-sm); } .picker-row { @@ -278,7 +286,7 @@ gap: 8px; padding: 6px 8px; color: var(--text); - background: #ffffff; + background: var(--panel); border: 0; border-bottom: 1px solid var(--line); cursor: pointer; @@ -291,7 +299,7 @@ .picker-row:hover, .picker-row.selected { - background: #f3f8f6; + background: var(--surface-soft); } .picker-row strong, @@ -311,18 +319,24 @@ .picker-error { color: var(--danger); - background: #fff2f0; - border: 1px solid #efb4ad; - border-radius: 7px; + background: var(--danger-tint); + border: 1px solid var(--danger-border); + border-radius: var(--radius-sm); } input, textarea { color: var(--text); - background: #ffffff; + background: var(--input-bg); border: 1px solid var(--line-strong); - border-radius: 7px; + border-radius: var(--radius-sm); outline: none; + transition: border-color 140ms ease, box-shadow 140ms ease; +} + +input::placeholder, +textarea::placeholder { + color: var(--muted-2); } input { @@ -366,9 +380,10 @@ textarea:focus { width: min(380px, 100%); height: 34px; padding: 0 10px; - background: #ffffff; + color: var(--text); + background: var(--input-bg); border: 1px solid var(--line-strong); - border-radius: 7px; + border-radius: var(--radius-sm); } .searchbox input { @@ -386,16 +401,17 @@ textarea:focus { justify-content: center; gap: 6px; padding: 0 12px; - color: #1d2939; - background: #ffffff; + color: var(--btn-text); + background: var(--btn-bg); border: 1px solid var(--line-strong); - border-radius: 7px; + border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap; + transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease, box-shadow 140ms ease; } .btn:hover:not(:disabled) { - background: #f7f9fb; + background: var(--btn-hover); } .btn:disabled { @@ -410,7 +426,8 @@ textarea:focus { } .btn.primary:hover:not(:disabled) { - background: #12594f; + background: var(--brand-strong); + border-color: var(--brand-strong); } .btn.ghost { @@ -419,22 +436,24 @@ textarea:focus { .btn.danger { color: var(--danger); - background: #fff7f5; - border-color: #efb4ad; + background: var(--danger-tint); + border-color: var(--danger-border); } .btn.danger:hover:not(:disabled) { - background: #ffeceb; + background: var(--danger-tint); + border-color: var(--danger); } .btn.warn { color: var(--warn); - background: #fff8ec; - border-color: #e7c77e; + background: var(--warn-tint); + border-color: var(--warn-border); } .btn.warn:hover:not(:disabled) { - background: #fff1d6; + background: var(--warn-tint); + border-color: var(--warn); } .btn:disabled, @@ -442,7 +461,7 @@ textarea:focus { .btn.warn:disabled, .btn.danger:disabled { color: var(--muted-2); - background: #f3f5f7; + background: var(--panel-strong); border-color: var(--line); cursor: not-allowed; } @@ -476,8 +495,9 @@ textarea:focus { .table-wrap { width: 100%; overflow-x: auto; + background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); } .data-table { @@ -500,13 +520,13 @@ textarea:focus { position: sticky; top: 0; z-index: 0; - color: #475467; + color: var(--muted); background: var(--panel-strong); font-weight: 800; } .data-table tbody tr:hover { - background: #fbfcfd; + background: var(--panel-subtle); } .data-table tr:last-child td { @@ -528,29 +548,29 @@ textarea:focus { min-height: 22px; align-items: center; padding: 1px 8px; - color: #4f5b68; - background: #f3f6f8; - border: 1px solid #d7e0e8; + color: var(--muted); + background: var(--panel-strong); + border: 1px solid var(--line-strong); border-radius: 999px; white-space: nowrap; } .badge.good { color: var(--good); - background: #eef8f2; - border-color: #b9dcc7; + background: var(--good-tint); + border-color: var(--good-border); } .badge.danger { color: var(--danger); - background: #fff2f0; - border-color: #efb4ad; + background: var(--danger-tint); + border-color: var(--danger-border); } .badge.warn { color: var(--warn); - background: #fff8e7; - border-color: #e7c77e; + background: var(--warn-tint); + border-color: var(--warn-border); } .empty-cell { diff --git a/cmd/telesrv-admin/web/src/styles/03-entities-and-actions.css b/cmd/telesrv-admin/web/src/styles/03-entities-and-actions.css index 9d1b5684..d29fe88e 100644 --- a/cmd/telesrv-admin/web/src/styles/03-entities-and-actions.css +++ b/cmd/telesrv-admin/web/src/styles/03-entities-and-actions.css @@ -18,10 +18,11 @@ padding: 14px; background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); } .entity-title { + color: var(--heading); font-size: 20px; font-weight: 800; line-height: 1.25; @@ -41,10 +42,10 @@ .about-text { margin: 0; padding: 10px; - color: #344054; - background: #fbfcfd; + color: var(--text-soft); + background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); } .section-block, @@ -54,7 +55,8 @@ padding: 12px; background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); + box-shadow: var(--shadow-sm); } .section-head { @@ -79,7 +81,7 @@ .dock-title { padding-bottom: 4px; - color: #344054; + color: var(--text-soft); font-weight: 800; border-bottom: 1px solid var(--line); } @@ -184,7 +186,7 @@ padding: 10px; background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); } .operation-title { @@ -192,6 +194,7 @@ width: 100%; align-items: center; gap: 6px; + color: var(--heading); font-weight: 800; } @@ -212,10 +215,10 @@ align-items: flex-start; gap: 8px; padding: 9px 10px; - color: #8a251d; - background: #fff2f0; - border: 1px solid #efb4ad; - border-radius: 8px; + color: var(--danger-text); + background: var(--danger-tint); + border: 1px solid var(--danger-border); + border-radius: var(--radius); } .json-block { @@ -223,10 +226,10 @@ overflow: auto; margin: 0; padding: 12px; - color: #d8e6f0; - background: #141a22; - border: 1px solid #2a3542; - border-radius: 8px; + color: var(--code-text); + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: var(--radius); font-size: 12px; } @@ -250,12 +253,12 @@ color: var(--muted); background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); } .gift-metrics .metric { min-height: 68px; padding: 12px; - background: linear-gradient(145deg, #ffffff, #f6f9f9); + background: var(--panel-subtle); } .gift-metrics .metric strong { font-size: 17px; } @@ -265,12 +268,12 @@ flex: 0 0 auto; place-items: center; color: var(--brand); - background: #eaf6f3; - border: 1px solid #c7e3dc; + background: var(--brand-tint); + border: 1px solid var(--brand-tint-border); } .gift-format-chips { display: flex; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; gap: 6px; } -.gift-format-chips span { padding: 4px 8px; color: #33645d; background: #eef8f5; border: 1px solid #cfe5df; border-radius: 999px; font-size: 10px; font-weight: 800; letter-spacing: .02em; } +.gift-format-chips span { padding: 4px 8px; color: var(--brand-tint-text); background: var(--brand-tint); border: 1px solid var(--brand-tint-border); border-radius: 999px; font-size: 10px; font-weight: 800; letter-spacing: .02em; } .gift-list-summary { margin-left: auto; color: var(--muted); font-size: 11px; font-weight: 700; } @@ -284,48 +287,48 @@ .official-gift-categories { display: flex; flex-wrap: wrap; gap: 7px; } .official-gift-categories button { display: inline-flex; align-items: center; gap: 7px; min-height: 32px; padding: 5px 10px; - color: #49605c; background: #f7faf9; border: 1px solid #d7e2df; border-radius: 999px; + color: var(--text-soft); background: var(--panel-subtle); border: 1px solid var(--line-strong); border-radius: 999px; font: inherit; font-size: 11px; font-weight: 800; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, box-shadow .15s ease; } -.official-gift-categories button:hover { color: var(--brand); border-color: #9fc9c0; } -.official-gift-categories button.active { color: #ffffff; background: var(--brand); border-color: var(--brand); box-shadow: 0 4px 12px rgba(23, 109, 97, .17); } +.official-gift-categories button:hover { color: var(--brand); border-color: var(--brand-tint-border); } +.official-gift-categories button.active { color: #ffffff; background: var(--brand); border-color: var(--brand); box-shadow: var(--shadow-brand); } .official-gift-categories button span { display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center; - color: inherit; background: rgba(255,255,255,.65); border-radius: 999px; font-size: 10px; + color: inherit; background: rgba(125, 140, 155, .22); border-radius: 999px; font-size: 10px; } -.official-gift-categories button.active span { color: var(--brand); } +.official-gift-categories button.active span { color: var(--brand); background: rgba(255, 255, 255, .85); } .official-gift-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; max-height: 314px; - min-height: 126px; padding: 8px; overflow: auto; border: 1px solid var(--line); border-radius: 14px; - background: #f6f9f8; scrollbar-gutter: stable; + min-height: 126px; padding: 8px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius-lg); + background: var(--panel-subtle); scrollbar-gutter: stable; } .official-gift-option { display: grid; min-width: 0; gap: 8px; padding: 11px 12px; text-align: left; color: var(--text); - background: #ffffff; border: 1px solid #dce6e3; border-radius: 11px; cursor: pointer; - box-shadow: 0 1px 2px rgba(32, 54, 50, .03); + background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); cursor: pointer; + box-shadow: var(--shadow-sm); transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; } -.official-gift-option:hover { border-color: #9fc9c0; box-shadow: 0 5px 14px rgba(32, 76, 68, .08); transform: translateY(-1px); } -.official-gift-option.selected { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(23, 109, 97, .12), 0 5px 14px rgba(32, 76, 68, .08); } +.official-gift-option:hover { border-color: var(--brand-tint-border); box-shadow: var(--shadow); transform: translateY(-1px); } +.official-gift-option.selected { border-color: var(--brand); box-shadow: 0 0 0 2px var(--focus), var(--shadow); } .official-gift-option-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: baseline; } .official-gift-option-head strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; } .official-gift-option-head .mono { color: var(--muted); font-size: 9px; } -.official-gift-option-meta { display: flex; flex-wrap: wrap; gap: 10px; color: #667773; font-size: 10px; font-weight: 700; } +.official-gift-option-meta { display: flex; flex-wrap: wrap; gap: 10px; color: var(--muted); font-size: 10px; font-weight: 700; } .official-gift-capabilities { display: flex; flex-wrap: wrap; gap: 5px; } .official-gift-capabilities > span { padding: 3px 7px; border: 1px solid transparent; border-radius: 999px; font-size: 9px; font-weight: 850; letter-spacing: .01em; } -.official-gift-capabilities > span.yes { color: #136b4d; background: #e9f8f0; border-color: #bde6cf; } -.official-gift-capabilities > span.craft { color: #6e3ca0; background: #f3ebfb; border-color: #d9c5ef; } -.official-gift-capabilities > span.no { color: #78837f; background: #f1f3f2; border-color: #dde2e0; } +.official-gift-capabilities > span.yes { color: var(--good); background: var(--good-tint); border-color: var(--good-border); } +.official-gift-capabilities > span.craft { color: var(--purple); background: var(--purple-tint); border-color: var(--purple-border); } +.official-gift-capabilities > span.no { color: var(--muted); background: var(--panel-strong); border-color: var(--line-strong); } .official-gift-empty { display: grid; grid-column: 1 / -1; min-height: 108px; place-items: center; padding: 20px; color: var(--muted); text-align: center; font-size: 12px; } .official-gift-selected { display: grid; grid-template-columns: 108px minmax(0, 1fr); gap: 14px; align-items: center; - padding: 12px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-soft); + padding: 12px; border: 1px solid var(--line); border-radius: var(--radius-lg); background: var(--surface-soft); } .official-gift-selected .gift-animation-shell { width: 96px; height: 96px; } .official-gift-selected > div:last-child { display: grid; gap: 5px; min-width: 0; } @@ -341,22 +344,22 @@ gap: 12px; padding: 12px 14px; color: var(--text); - background: #ffffff; - border: 1px dashed #b7ccc8; - border-radius: 10px; + background: var(--panel); + border: 1px dashed var(--line-strong); + border-radius: var(--radius); cursor: pointer; transition: border-color .16s ease, background .16s ease, box-shadow .16s ease; } .gift-file-picker:hover, -.gift-file-picker.has-file { background: #f8fcfb; border-color: var(--brand); box-shadow: 0 0 0 2px rgba(23, 109, 97, .05); } +.gift-file-picker.has-file { background: var(--brand-tint); border-color: var(--brand); box-shadow: 0 0 0 2px var(--focus); } .gift-file-picker input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } -.gift-file-icon { width: 40px; height: 40px; border-radius: 9px; } +.gift-file-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); } .gift-file-copy { display: grid; min-width: 0; gap: 2px; } .gift-field-label { color: var(--muted); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; } -.gift-file-copy strong { overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } +.gift-file-copy strong { overflow: hidden; color: var(--heading); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } .gift-file-copy small { color: var(--muted); font-size: 11px; font-weight: 500; } -.gift-file-action { padding: 7px 10px; color: var(--brand); background: #f0f8f6; border: 1px solid #c7e3dc; border-radius: 7px; font-size: 11px; font-weight: 800; } +.gift-file-action { padding: 7px 10px; color: var(--brand); background: var(--brand-tint); border: 1px solid var(--brand-tint-border); border-radius: var(--radius-sm); font-size: 11px; font-weight: 800; } .gift-fields-grid { display: grid; @@ -380,37 +383,37 @@ height: 38px; padding: 0 10px; color: var(--text); - background: #fff; + background: var(--input-bg); border: 1px solid var(--line); - border-radius: 7px; + border-radius: var(--radius-sm); } .gift-fields-grid input:focus, -.gift-reason-field input:focus { border-color: #77b6aa; box-shadow: 0 0 0 3px rgba(23, 109, 97, .08); outline: none; } +.gift-reason-field input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--focus); outline: none; } -.gift-switch { display: inline-flex; align-items: center; gap: 9px; color: #344054; font-size: 12px; font-weight: 700; cursor: pointer; } +.gift-switch { display: inline-flex; align-items: center; gap: 9px; color: var(--text-soft); font-size: 12px; font-weight: 700; cursor: pointer; } .gift-switch input { position: absolute; width: 1px; height: 1px; opacity: 0; } -.gift-switch-track { display: flex; width: 34px; height: 19px; align-items: center; padding: 2px; background: #c8d0d5; border-radius: 999px; transition: background .16s ease; } +.gift-switch-track { display: flex; width: 34px; height: 19px; align-items: center; padding: 2px; background: var(--switch-track); border-radius: 999px; transition: background .16s ease; } .gift-switch-track span { width: 15px; height: 15px; background: #ffffff; border-radius: 50%; box-shadow: 0 1px 3px rgba(16, 24, 40, .22); transition: transform .16s ease; } .gift-switch input:checked + .gift-switch-track { background: var(--brand); } .gift-switch input:checked + .gift-switch-track span { transform: translateX(15px); } -.gift-switch input:focus-visible + .gift-switch-track { outline: 3px solid rgba(23, 109, 97, .16); outline-offset: 2px; } -.gift-validation { overflow: hidden; color: #d5fff5; background: #173631; border: 1px solid #24564e; border-radius: 9px; } -.gift-validation-head { display: flex; align-items: center; gap: 9px; padding: 10px 12px; color: #e3fff9; background: rgba(255, 255, 255, .035); border-bottom: 1px solid rgba(255, 255, 255, .09); } +.gift-switch input:focus-visible + .gift-switch-track { outline: 3px solid var(--focus); outline-offset: 2px; } +.gift-validation { overflow: hidden; color: var(--code-text); background: var(--code-bg); border: 1px solid var(--code-border); border-radius: var(--radius-sm); } +.gift-validation-head { display: flex; align-items: center; gap: 9px; padding: 10px 12px; color: var(--code-text); background: rgba(255, 255, 255, .035); border-bottom: 1px solid rgba(255, 255, 255, .09); } .gift-validation-head div { display: grid; gap: 2px; } -.gift-validation-head span { color: #99cfc4; font-size: 10px; } -.gift-validation pre { max-height: 180px; overflow: auto; margin: 0; padding: 11px 12px; color: #d5fff5; font-size: 11px; } +.gift-validation-head span { color: var(--brand); font-size: 10px; } +.gift-validation pre { max-height: 180px; overflow: auto; margin: 0; padding: 11px 12px; color: var(--code-text); font-size: 11px; } -.gift-animation-shell { position: relative; display: grid; min-height: 210px; place-items: center; background: radial-gradient(circle, #f9f3ff, #eef8f5); } +.gift-animation-shell { position: relative; display: grid; min-height: 210px; place-items: center; background: var(--surface-soft); } .gift-animation { width: 200px; height: 200px; } .gift-animation canvas { width: 100% !important; height: 100% !important; } -.gift-play { position: absolute; right: 8px; bottom: 8px; display: grid; width: 30px; height: 30px; place-items: center; color: var(--text); background: rgba(255,255,255,.9); border: 1px solid var(--line); border-radius: 50%; } +.gift-play { position: absolute; right: 8px; bottom: 8px; display: grid; width: 30px; height: 30px; place-items: center; color: var(--text); background: var(--panel); border: 1px solid var(--line); border-radius: 50%; } -.gift-table-wrap { background: #ffffff; } +.gift-table-wrap { background: var(--panel); } .gift-table { min-width: 1080px; } .gift-table th:first-child { width: 74px; } .gift-table td { vertical-align: middle; } -.gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; } +.gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius-sm); } .gift-animation-shell.compact .gift-animation { width: 54px; height: 54px; } .gift-animation-shell.compact .gift-play { right: 3px; bottom: 3px; width: 20px; height: 20px; } .gift-row-disabled { opacity: .68; } @@ -422,60 +425,60 @@ .gift-sort-order, .gift-source-size, .gift-convert-price { margin-top: 3px; color: var(--muted); font-size: 10px; } -.gift-table-price { color: #755b00; } +.gift-table-price { color: var(--warn); } .gift-table-actions { display: flex; align-items: center; gap: 6px; } -.collectible-button { color: #6548a8; background: #f7f3ff; border-color: #ddd2f5; } -.collectible-button:hover { background: #efe8ff; border-color: #cbbaf0; } +.collectible-button { color: var(--purple); background: var(--purple-tint); border-color: var(--purple-border); } +.collectible-button:hover { background: var(--purple-tint); border-color: var(--purple); } .collectible-modal { width: min(1180px, 100%); max-height: min(92vh, 980px); } .collectible-modal .modal-head p { margin: 4px 0 0; color: var(--muted); font-size: 11px; } -.collectible-modal-body { gap: 16px; overflow: auto; padding: 16px 18px 22px; background: #f5f7fa; } +.collectible-modal-body { gap: 16px; overflow: auto; padding: 16px 18px 22px; background: var(--bg); } .collectible-loading { display: flex; min-height: 90px; align-items: center; justify-content: center; gap: 8px; color: var(--muted); } -.collectible-empty { display: flex; align-items: center; gap: 12px; padding: 16px; color: #66568c; background: linear-gradient(135deg, #fbf9ff, #f2f7ff); border: 1px dashed #cfc3e9; border-radius: 12px; } +.collectible-empty { display: flex; align-items: center; gap: 12px; padding: 16px; color: var(--purple-text); background: var(--purple-tint); border: 1px dashed var(--purple-border); border-radius: var(--radius); } .collectible-empty div, .collectible-definition-head > 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 { overflow: hidden; background: var(--panel); border: 1px solid var(--purple-border); border-radius: var(--radius); box-shadow: var(--shadow-sm); } +.collectible-active-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: var(--purple-tint); border-bottom: 1px solid var(--purple-border); } +.collectible-active-head > div { display: flex; align-items: center; gap: 9px; color: var(--purple-text); } .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 { display: flex; min-width: 0; align-items: center; gap: 9px; padding: 9px 11px; background: var(--panel); } .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-definition { overflow: hidden; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-sm); } +.collectible-definition-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; background: var(--panel-subtle); border-bottom: 1px solid var(--line); } +.collectible-main-fields { padding: 14px 16px; background: var(--panel-subtle); 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 { position: relative; display: grid; align-items: end; gap: 7px; padding: 9px 9px 9px 36px; background: var(--panel-subtle); border: 1px solid var(--line); border-radius: var(--radius-sm); } +.collectible-row:hover { background: var(--panel); border-color: var(--line-strong); box-shadow: var(--shadow-sm); } .collectible-row.animated { grid-template-columns: minmax(120px, 1.2fr) 90px 78px minmax(160px, 1.4fr) 48px 30px; } .collectible-row.backdrop { grid-template-columns: minmax(110px, 1.2fr) 70px 80px 70px repeat(4, 52px) 48px 30px; } -.collectible-row-index { 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-index { position: absolute; top: 0; bottom: 0; left: 0; display: grid; width: 27px; place-items: center; color: var(--purple-text); background: var(--purple-tint); border-right: 1px solid var(--purple-border); border-radius: var(--radius-xs) 0 0 var(--radius-xs); 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-row input:not([type="file"]) { width: 100%; min-width: 0; height: 32px; padding: 0 8px; color: var(--text); background: var(--input-bg); border: 1px solid var(--line-strong); border-radius: var(--radius-sm); font: inherit; font-size: 11px; } +.collectible-row input:focus { border-color: var(--purple); box-shadow: 0 0 0 3px var(--purple-tint); outline: none; } .collectible-file input { 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-file em { display: flex; min-width: 0; height: 32px; align-items: center; gap: 5px; overflow: hidden; padding: 0 8px; color: var(--purple-text); background: var(--purple-tint); border: 1px dashed var(--purple-border); border-radius: var(--radius-sm); 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: var(--purple); background: var(--purple-tint); border: 1px solid var(--purple-border); border-radius: var(--radius-sm); } .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.compact { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; background: var(--purple-tint); border: 1px solid var(--purple-border); border-radius: var(--radius-sm); } .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-animation.failed { color: var(--danger); background: var(--danger-tint); } +.collectible-animation.loading { color: var(--purple-text); } +.collectible-file-error { grid-column: 1 / -1; color: var(--danger); 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-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: var(--radius-sm); 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; } diff --git a/cmd/telesrv-admin/web/src/styles/04-modal-and-login.css b/cmd/telesrv-admin/web/src/styles/04-modal-and-login.css index 60a3cc84..1e51094c 100644 --- a/cmd/telesrv-admin/web/src/styles/04-modal-and-login.css +++ b/cmd/telesrv-admin/web/src/styles/04-modal-and-login.css @@ -5,7 +5,8 @@ display: grid; place-items: center; padding: 24px; - background: rgba(17, 24, 39, 0.52); + background: var(--overlay); + backdrop-filter: blur(2px); } .modal { @@ -13,9 +14,9 @@ max-height: min(820px, calc(100vh - 48px)); overflow: hidden; padding: 0; - background: #ffffff; + background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-lg); box-shadow: var(--shadow); } @@ -43,10 +44,17 @@ width: 30px; height: 30px; place-items: center; + color: var(--text-soft); background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 7px; + border-radius: var(--radius-sm); cursor: pointer; + transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease; +} + +.icon-btn:hover { + background: var(--btn-hover); + border-color: var(--line-strong); } .command-steps { @@ -73,7 +81,7 @@ color: var(--muted); background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-sm); } .command-step span { @@ -81,7 +89,7 @@ width: 20px; height: 20px; place-items: center; - background: #ffffff; + background: var(--panel); border: 1px solid var(--line); border-radius: 999px; font-size: 11px; @@ -90,12 +98,12 @@ .command-step.active { color: var(--brand); - border-color: #a9d8ce; + border-color: var(--brand-tint-border); } .command-step.done { color: var(--good); - border-color: #b9dcc7; + border-color: var(--good-border); } .form-field { @@ -105,7 +113,7 @@ .form-field span, .form-stack span { - color: #4b5563; + color: var(--text-soft); font-weight: 800; } @@ -123,7 +131,7 @@ display: flex; align-items: center; gap: 7px; - color: #344054; + color: var(--text-soft); font-weight: 800; } @@ -131,9 +139,9 @@ display: grid; gap: 8px; padding: 10px; - background: #fbfcfd; + background: var(--panel-subtle); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius); } .result-line { @@ -151,13 +159,13 @@ } .result-message { - color: #344054; + color: var(--text-soft); } .modal-actions { justify-content: flex-end; padding: 12px 18px; - background: #ffffff; + background: var(--panel); border-top: 1px solid var(--line); } @@ -174,9 +182,9 @@ width: min(420px, 100%); gap: 18px; padding: 22px; - background: #ffffff; + background: var(--panel); border: 1px solid var(--line); - border-radius: 8px; + border-radius: var(--radius-lg); box-shadow: var(--shadow); } @@ -201,14 +209,15 @@ align-items: center; padding: 0 8px; color: var(--brand); - background: #edf7f4; - border: 1px solid #c9e2dc; + background: var(--brand-tint); + border: 1px solid var(--brand-tint-border); border-radius: 999px; font-size: 12px; } .login-copy h1 { margin: 0; + color: var(--heading); font-size: 22px; } @@ -237,13 +246,14 @@ place-items: center; align-content: center; gap: 18px; + background: var(--bg); } .loader-bar { width: 180px; height: 4px; overflow: hidden; - background: #d7dde4; + background: var(--line-strong); border-radius: 999px; } diff --git a/cmd/telesrv-admin/web/src/theme.tsx b/cmd/telesrv-admin/web/src/theme.tsx new file mode 100644 index 00000000..bb0dddfd --- /dev/null +++ b/cmd/telesrv-admin/web/src/theme.tsx @@ -0,0 +1,106 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; +import { Moon, Sun } from "lucide-react"; +import { useI18n } from "./i18n"; + +export type Theme = "light" | "dark"; + +const storageKey = "telesrv.admin.theme"; + +type ThemeContextValue = { + theme: Theme; + setTheme: (theme: Theme) => void; + toggleTheme: () => void; +}; + +const ThemeContext = createContext(null); + +export function applyTheme(theme: Theme) { + document.documentElement.setAttribute("data-theme", theme); + document.documentElement.style.colorScheme = theme; +} + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setThemeState] = useState(() => initialTheme()); + + useEffect(() => { + applyTheme(theme); + try { + localStorage.setItem(storageKey, theme); + } catch { + // Theme persistence is best-effort. + } + }, [theme]); + + // Follow the OS preference until the user makes an explicit choice. + useEffect(() => { + if (!window.matchMedia) { + return; + } + const media = window.matchMedia("(prefers-color-scheme: dark)"); + const onChange = (event: MediaQueryListEvent) => { + let stored: string | null = null; + try { + stored = localStorage.getItem(storageKey); + } catch { + stored = null; + } + if (stored !== "light" && stored !== "dark") { + setThemeState(event.matches ? "dark" : "light"); + } + }; + media.addEventListener("change", onChange); + return () => media.removeEventListener("change", onChange); + }, []); + + const setTheme = useCallback((next: Theme) => setThemeState(next), []); + const toggleTheme = useCallback(() => setThemeState((current) => (current === "dark" ? "light" : "dark")), []); + + const value = useMemo(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme]); + + return {children}; +} + +export function useTheme(): ThemeContextValue { + const value = useContext(ThemeContext); + if (!value) { + throw new Error("useTheme must be used inside ThemeProvider"); + } + return value; +} + +export function ThemeSwitch() { + const { theme, toggleTheme } = useTheme(); + const { t } = useI18n(); + const nextIsDark = theme === "light"; + const label = t(nextIsDark ? "theme.switchToDark" : "theme.switchToLight"); + return ( + + ); +} + +function initialTheme(): Theme { + try { + const stored = localStorage.getItem(storageKey); + if (stored === "light" || stored === "dark") { + return stored; + } + } catch { + // Storage is optional. + } + try { + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "dark"; + } + } catch { + // matchMedia can be unavailable in unusual embedded contexts. + } + return "light"; +} From 9e45da69efaa5500b97757b7e00b57f14f2d8f7b Mon Sep 17 00:00:00 2001 From: epilepticseizureee Date: Thu, 23 Jul 2026 00:34:05 +0300 Subject: [PATCH 23/28] feat(admin): bot management (list, verify, create, delete) - Add a Bots admin tab: list/search bots with a dedicated read query (users.is_bot, excluded from the accounts list), showing owner and system-vs-user type - Create system bots from the admin via a new bot.create command that reuses the existing bot provisioning flow; the token is shown once - Delete user-created bots via a new bot.delete command backed by a dedicated Postgres DeleteBotAccount (revokes sessions, purges private state, releases username, drops the bots row, tombstones the user); system service bots are rejected - Verified badge toggling reuses the existing set-verified command - All write paths go through the dry-run/confirm + audit command pipeline - Rebuild dist bundle --- cmd/telesrv-admin/readstore.go | 132 ++++++++++++++ cmd/telesrv-admin/server.go | 106 +++++++++++ .../web/dist/assets/index-BB8hN3NX.js | 9 + .../web/dist/assets/index-BlWlOvtx.css | 1 + .../web/dist/assets/index-CqgHld2y.js | 9 - .../web/dist/assets/index-DuOdm70q.css | 1 - cmd/telesrv-admin/web/dist/index.html | 4 +- cmd/telesrv-admin/web/src/api.ts | 4 + .../web/src/components/Layout.tsx | 2 + cmd/telesrv-admin/web/src/i18n.tsx | 111 ++++++++++++ .../web/src/pages/BotDetailPage.tsx | 108 +++++++++++ cmd/telesrv-admin/web/src/pages/BotsPage.tsx | 167 ++++++++++++++++++ cmd/telesrv-admin/web/src/pages/Routes.tsx | 9 + cmd/telesrv-admin/web/src/routing.ts | 2 + .../web/src/styles/02-pages-and-forms.css | 37 ++++ cmd/telesrv-admin/web/src/types.ts | 28 +++ cmd/telesrv/main.go | 1 + internal/admin/service.go | 114 ++++++++++++ internal/adminapi/server.go | 22 +++ internal/adminapi/server_test.go | 8 + internal/app/bots/service.go | 36 ++++ internal/store/postgres/bot.go | 86 +++++++++ 22 files changed, 985 insertions(+), 12 deletions(-) create mode 100644 cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.js create mode 100644 cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.js delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-DuOdm70q.css create mode 100644 cmd/telesrv-admin/web/src/pages/BotDetailPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/BotsPage.tsx diff --git a/cmd/telesrv-admin/readstore.go b/cmd/telesrv-admin/readstore.go index 6ebd08e6..c16387bf 100644 --- a/cmd/telesrv-admin/readstore.go +++ b/cmd/telesrv-admin/readstore.go @@ -232,6 +232,138 @@ LIMIT $5`, id, phone, phoneRaw, username, accountSearchLimit) return out, rows.Err() } +type BotRow struct { + ID int64 + Username string + FirstName string + Verified bool + System bool + OwnerUserID int64 + CreatedAt time.Time + UpdatedAt time.Time +} + +type BotDetail struct { + Bot BotRow + About string + Description string + OwnerUsername string + AuditLogs []AuditLogRow +} + +// ListBots pages over live bot accounts (users.is_bot, not tombstoned) by +// descending id. Bots are excluded from ListAccounts, so this is the dedicated +// projection for them. +func (s *readStore) ListBots(ctx context.Context, beforeID int64, limit int) ([]BotRow, bool, error) { + if limit <= 0 { + limit = accountListDefaultLimit + } + if limit > accountListMaxLimit { + limit = accountListMaxLimit + } + rows, err := s.pool.Query(ctx, ` +SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.verified, + COALESCE(b.owner_user_id, 0), u.created_at, u.updated_at +FROM users u +LEFT JOIN bots b ON b.bot_user_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +WHERE u.is_bot AND u.deleted_at IS NULL AND ($1::bigint = 0 OR u.id < $1) +ORDER BY u.id DESC +LIMIT $2`, beforeID, limit+1) + if err != nil { + return nil, false, fmt.Errorf("list bots: %w", err) + } + defer rows.Close() + out := make([]BotRow, 0, limit+1) + for rows.Next() { + var item BotRow + if err := rows.Scan(&item.ID, &item.Username, &item.FirstName, &item.Verified, &item.OwnerUserID, &item.CreatedAt, &item.UpdatedAt); err != nil { + return nil, false, err + } + item.System = domain.IsSystemUserID(item.ID) + out = append(out, item) + } + if err := rows.Err(); err != nil { + return nil, false, err + } + hasMore := len(out) > limit + if hasMore { + out = out[:limit] + } + return out, hasMore, nil +} + +func (s *readStore) SearchBots(ctx context.Context, q string) ([]BotRow, error) { + q = strings.TrimSpace(q) + if q == "" { + return nil, nil + } + id := int64(-1) + if n, err := strconv.ParseInt(q, 10, 64); err == nil { + id = n + } + username := strings.ToLower(strings.TrimPrefix(q, "@")) + rows, err := s.pool.Query(ctx, ` +SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.verified, + COALESCE(b.owner_user_id, 0), u.created_at, u.updated_at +FROM users u +LEFT JOIN bots b ON b.bot_user_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +WHERE u.is_bot AND u.deleted_at IS NULL AND (u.id = $1 OR lower(u.username) = $2 OR p.username_lower = $2) +ORDER BY u.id DESC +LIMIT $3`, id, username, accountSearchLimit) + if err != nil { + return nil, fmt.Errorf("search bots: %w", err) + } + defer rows.Close() + out := make([]BotRow, 0) + for rows.Next() { + var item BotRow + if err := rows.Scan(&item.ID, &item.Username, &item.FirstName, &item.Verified, &item.OwnerUserID, &item.CreatedAt, &item.UpdatedAt); err != nil { + return nil, err + } + item.System = domain.IsSystemUserID(item.ID) + out = append(out, item) + } + return out, rows.Err() +} + +func (s *readStore) BotDetail(ctx context.Context, botUserID int64) (BotDetail, error) { + var out BotDetail + err := s.pool.QueryRow(ctx, ` +SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.about, u.verified, + COALESCE(b.owner_user_id, 0), COALESCE(b.description, ''), + u.created_at, u.updated_at +FROM users u +LEFT JOIN bots b ON b.bot_user_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +WHERE u.id = $1 AND u.is_bot AND u.deleted_at IS NULL`, botUserID).Scan( + &out.Bot.ID, &out.Bot.Username, &out.Bot.FirstName, &out.About, &out.Bot.Verified, + &out.Bot.OwnerUserID, &out.Description, &out.Bot.CreatedAt, &out.Bot.UpdatedAt, + ) + if err != nil { + return out, fmt.Errorf("get bot: %w", err) + } + out.Bot.System = domain.IsSystemUserID(out.Bot.ID) + if out.Bot.OwnerUserID > 0 { + var ownerUsername string + if err := s.pool.QueryRow(ctx, ` +SELECT COALESCE(NULLIF(u.username, ''), p.username_lower, '') +FROM users u +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +WHERE u.id = $1`, out.Bot.OwnerUserID).Scan(&ownerUsername); err != nil && err != pgx.ErrNoRows { + return out, fmt.Errorf("get bot owner: %w", err) + } else { + out.OwnerUsername = ownerUsername + } + } + out.AuditLogs, err = s.auditLogs(ctx, botUserID) + if err != nil { + return out, err + } + return out, nil +} + func (s *readStore) SearchChannels(ctx context.Context, q string) ([]ChannelRow, error) { q = strings.TrimSpace(q) if q == "" { diff --git a/cmd/telesrv-admin/server.go b/cmd/telesrv-admin/server.go index 8318ae2e..e1ee773f 100644 --- a/cmd/telesrv-admin/server.go +++ b/cmd/telesrv-admin/server.go @@ -53,6 +53,8 @@ func (s *server) routes() http.Handler { mux.Handle("GET /api/accounts/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleAccountDetailAPI))) mux.Handle("GET /api/channels", s.requireAuthAPI(http.HandlerFunc(s.handleChannelsAPI))) mux.Handle("GET /api/channels/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleChannelDetailAPI))) + mux.Handle("GET /api/bots", s.requireAuthAPI(http.HandlerFunc(s.handleBotsAPI))) + mux.Handle("GET /api/bots/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleBotDetailAPI))) mux.Handle("GET /api/messages", s.requireAuthAPI(http.HandlerFunc(s.handleMessagesAPI))) mux.Handle("GET /api/messages/detail", s.requireAuthAPI(http.HandlerFunc(s.handleMessageDetailAPI))) mux.Handle("GET /api/messages/groups", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessagesAPI))) @@ -67,6 +69,8 @@ func (s *server) routes() http.Handler { 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))) mux.Handle("POST /api/actions/set-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetVerifiedAPI))) + mux.Handle("POST /api/actions/create-bot", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBotAPI))) + mux.Handle("POST /api/actions/delete-bot", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteBotAPI))) mux.Handle("POST /api/actions/set-channel-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelVerifiedAPI))) 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))) @@ -346,6 +350,108 @@ func (s *server) handleAccountDetailAPI(w http.ResponseWriter, r *http.Request) writeJSON(w, http.StatusOK, detail) } +func (s *server) handleBotsAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + q := r.URL.Query().Get("q") + beforeID, _ := parseInt64(r.URL.Query().Get("before_id")) + limit, _ := parseInt(r.URL.Query().Get("limit")) + rows := []BotRow{} + hasMore := false + var err error + if strings.TrimSpace(q) != "" { + rows, err = s.read.SearchBots(r.Context(), q) + } else { + rows, hasMore, err = s.read.ListBots(r.Context(), beforeID, limit) + } + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + nextBeforeID := int64(0) + if hasMore && len(rows) > 0 { + nextBeforeID = rows[len(rows)-1].ID + } + if limit <= 0 { + limit = accountListDefaultLimit + } + if limit > accountListMaxLimit { + limit = accountListMaxLimit + } + writeJSON(w, http.StatusOK, map[string]any{ + "query": q, + "limit": limit, + "rows": rows, + "has_more": hasMore, + "next_before_id": nextBeforeID, + "listing": strings.TrimSpace(q) == "", + }) +} + +func (s *server) handleBotDetailAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + botID, err := parseInt64(r.PathValue("id")) + if err != nil || botID <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid id") + return + } + detail, err := s.read.BotDetail(r.Context(), botID) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, detail) +} + +type createBotAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + OwnerUserID int64 `json:"owner_user_id"` + Name string `json:"name"` + Username string `json:"username"` +} + +func (s *server) handleCreateBotAPI(w http.ResponseWriter, r *http.Request) { + var body createBotAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.CreateBotRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "create-bot"), + OwnerUserID: body.OwnerUserID, + Name: body.Name, + Username: body.Username, + } + result, err := s.callAdminAPI(r.Context(), "/v1/bots/create", req) + writeCommandResultAPI(w, result, err) +} + +type deleteBotAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + BotUserID int64 `json:"bot_user_id"` +} + +func (s *server) handleDeleteBotAPI(w http.ResponseWriter, r *http.Request) { + var body deleteBotAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.DeleteBotRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "delete-bot"), + BotUserID: body.BotUserID, + } + result, err := s.callAdminAPI(r.Context(), "/v1/bots/delete", req) + writeCommandResultAPI(w, result, err) +} + func (s *server) handleChannelsAPI(w http.ResponseWriter, r *http.Request) { if s.read == nil { writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") diff --git a/cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.js b/cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.js new file mode 100644 index 00000000..3278d88a --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.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 B(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 de(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`||de(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=Ce.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 W={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},Ee=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(W).forEach(function(e){Ee.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),W[t]=W[e]})});function De(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||W.hasOwnProperty(e)&&W[e]?(``+t).trim():t+`px`}function Oe(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=De(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var ke=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 Ae(e,t){if(t){if(ke[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 je(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 Me=null;function Ne(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Pe=null,Fe=null,Ie=null;function Le(e){if(e=ji(e)){if(typeof Pe!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Ni(t),Pe(e.stateNode,e.type,t))}}function Re(e){Fe?Ie?Ie.push(e):Ie=[e]:Fe=e}function ze(){if(Fe){var e=Fe,t=Ie;if(Ie=Fe=null,Le(e),t)for(e=0;e>>=0,e===0?32:31-(_t(e)/vt|0)|0}var bt=64,xt=4194304;function St(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ct(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,o=n&268435455;if(o!==0){var s=o&~i;s===0?(a&=o,a!==0&&(r=St(a))):r=St(s)}else o=n&~i,o===0?a!==0&&(r=St(a)):r=St(o);if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&a&4194240))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function kt(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-gt(t),e[t]=n}function At(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Gn),Jn=` `,Yn=!1;function Xn(e,t){switch(e){case`keyup`:return Un.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Zn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Qn=!1;function $n(e,t){switch(e){case`compositionend`:return Zn(t);case`keypress`:return t.which===32?(Yn=!0,Jn):null;case`textInput`:return e=t.data,e===Jn&&Yn?null:e;default:return null}}function er(e,t){if(Qn)return e===`compositionend`||!Wn&&Xn(e,t)?(e=mn(),pn=fn=dn=null,Qn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=xr(n)}}function Cr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Cr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wr(){for(var e=window,t=de();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=de(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!==de(r)||(r=Or,`selectionStart`in r&&Tr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ar&&br(Ar,r)||(Ar=r,r=ii(kr,`onSelect`),0Fi||(e.current=Pi[Fi],Pi[Fi]=null,Fi--)}function Ri(e,t){Fi++,Pi[Fi]=e.current,e.current=t}var zi={},Bi=Ii(zi),Vi=Ii(!1),Hi=zi;function Ui(e,t){var n=e.type.contextTypes;if(!n)return zi;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Wi(e){return e=e.childContextTypes,e!=null}function Gi(){Li(Vi),Li(Bi)}function Ki(e,t,n){if(Bi.current!==zi)throw Error(r(168));Ri(Bi,t),Ri(Vi,n)}function qi(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,se(e)||`Unknown`,a));return R({},n,i)}function Ji(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||zi,Hi=Bi.current,Ri(Bi,e),Ri(Vi,Vi.current),!0}function Yi(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=qi(e,t,Hi),i.__reactInternalMemoizedMergedChildContext=e,Li(Vi),Li(Bi),Ri(Bi,e)):Li(Vi),Ri(Vi,n)}var Xi=null,Zi=!1,Qi=!1;function $i(e){Xi===null?Xi=[e]:Xi.push(e)}function ea(e){Zi=!0,$i(e)}function ta(){if(!Qi&&Xi!==null){Qi=!0;var e=0,t=X;try{var n=Xi;for(X=1;e>=o,i-=o,la=1<<32-gt(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),_a&&da(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),_a&&da(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return _a&&da(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),_a&&da(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===F&&ja(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=ka(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=ka(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case F:return l=i._init,_(e,r,l(i._payload),o)}if(ve(i))return h(e,r,i,o);if(ee(i))return g(e,r,i,o);Aa(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Na=Ma(!0),Pa=Ma(!1),Fa=Ii(null),Ia=null,La=null,Ra=null;function za(){Ra=La=Ia=null}function Ba(e){var t=Fa.current;Li(Fa),e._currentValue=t}function Va(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Ha(e,t){Ia=e,Ra=La=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Ms=!0),e.firstContext=null)}function Ua(e){var t=e._currentValue;if(Ra!==e)if(e={context:e,memoizedValue:t,next:null},La===null){if(Ia===null)throw Error(r(308));La=e,Ia.dependencies={lanes:0,firstContext:e}}else La=La.next=e;return t}var Wa=null;function Ga(e){Wa===null?Wa=[e]:Wa.push(e)}function Ka(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Ga(t)):(n.next=i.next,i.next=n),t.interleaved=n,qa(e,r)}function qa(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Ja=!1;function Ya(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Za(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Qa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,$&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,qa(e,n)}return i=r.interleaved,i===null?(t.next=t,Ga(r)):(t.next=i.next,i.next=t),r.interleaved=t,qa(e,n)}function $a(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}function eo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function to(e,t,n,r){var i=e.updateQueue;Ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=R({},d,f);break a;case 2:Ja=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function no(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=vo.transition;vo.transition={};try{e(!1),t()}finally{X=n,vo.transition=r}}function as(){return Mo().memoizedState}function os(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},cs(e))ls(t,n);else if(n=Ka(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),us(n,t,r)}}function ss(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(cs(e))ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Z(s,o)){var c=t.interleaved;c===null?(i.next=i,Ga(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ka(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),us(n,t,r))}}function cs(e){var t=e.alternate;return e===bo||t!==null&&t===bo}function ls(e,t){wo=Co=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function us(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}var ds={readContext:Ua,useCallback:Do,useContext:Do,useEffect:Do,useImperativeHandle:Do,useInsertionEffect:Do,useLayoutEffect:Do,useMemo:Do,useReducer:Do,useRef:Do,useState:Do,useDebugValue:Do,useDeferredValue:Do,useTransition:Do,useMutableSource:Do,useSyncExternalStore:Do,useId:Do,unstable_isNewReconciler:!1},fs={readContext:Ua,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:Ua,useEffect:Jo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Ko(4194308,4,Qo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ko(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ko(4,2,e,t)},useMemo:function(e,t){var n=jo();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=jo();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=os.bind(null,bo,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:Uo,useDebugValue:es,useDeferredValue:function(e){return jo().memoizedState=e},useTransition:function(){var e=Uo(!1),t=e[0];return e=is.bind(null,e[1]),jo().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=bo,a=jo();if(_a){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));yo&30||Ro(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,Jo(Bo.bind(null,i,o,e),[e]),i.flags|=2048,Wo(9,zo.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=jo(),t=Vc.identifierPrefix;if(_a){var n=ua,r=la;n=(r&~(1<<32-gt(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=To++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[wi]=t,e[Ti]=i,nc(e,t,!1,!1),t.stateNode=e;a:{switch(c=je(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*ot()-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=ot(),t.sibling=null,n=po.current,Ri(po,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(oc(t),t.subtreeFlags&6&&(t.flags|=8192)):oc(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function cc(e,t){switch(ma(t),t.tag){case 1:return Wi(t.type)&&Gi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return lo(),Li(Vi),Li(Bi),go(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return fo(t),null;case 13:if(Li(po),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ea()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Li(po),null;case 4:return lo(),null;case 10:return Ba(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var lc=!1,uc=!1,dc=typeof WeakSet==`function`?WeakSet:Set,Q=null;function fc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function pc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var mc=!1;function hc(e,t){if(fi=rn,e=wr(),Tr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(pi={focusedElem:e,selectionRange:n},rn=!1,Q=t;Q!==null;)if(t=Q,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Q=e;else for(;Q!==null;){t=Q;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:hs(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,Q=e;break}Q=t.return}return h=mc,mc=!1,h}function gc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&pc(t,n,a)}i=i.next}while(i!==r)}}function _c(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function vc(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function yc(e){var t=e.alternate;t!==null&&(e.alternate=null,yc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[wi],delete t[Ti],delete t[Di],delete t[Oi],delete t[ki])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function bc(e){return e.tag===5||e.tag===3||e.tag===4}function xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||bc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=di));else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}function Cc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Cc(e,t,n),e=e.sibling;e!==null;)Cc(e,t,n),e=e.sibling}var wc=null,Tc=!1;function Ec(e,t,n){for(n=n.child;n!==null;)Dc(e,t,n),n=n.sibling}function Dc(e,t,n){if(mt&&typeof mt.onCommitFiberUnmount==`function`)try{mt.onCommitFiberUnmount(pt,n)}catch{}switch(n.tag){case 5:uc||fc(n,t);case 6:var r=wc,i=Tc;wc=null,Ec(e,t,n),wc=r,Tc=i,wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):wc.removeChild(n.stateNode));break;case 18:wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?bi(e.parentNode,n):e.nodeType===1&&bi(e,n),tn(e)):bi(wc,n.stateNode));break;case 4:r=wc,i=Tc,wc=n.stateNode.containerInfo,Tc=!0,Ec(e,t,n),wc=r,Tc=i;break;case 0:case 11:case 14:case 15:if(!uc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&pc(n,t,o),i=i.next}while(i!==r)}Ec(e,t,n);break;case 1:if(!uc&&(fc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Ec(e,t,n);break;case 21:Ec(e,t,n);break;case 22:n.mode&1?(uc=(r=uc)||n.memoizedState!==null,Ec(e,t,n),uc=r):Ec(e,t,n);break;default:Ec(e,t,n)}}function Oc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new dc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function kc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=ot()-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;lot()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=xt,xt<<=1,!(xt&130023424)&&(xt=4194304)):t=1);var n=fl();e=qa(e,t),e!==null&&(kt(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Vi.current)Ms=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return Ms=!1,tc(e,t,n);Ms=!!(e.flags&131072)}else Ms=!1,_a&&t.flags&1048576&&fa(t,aa,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;$s(e,t),e=t.pendingProps;var a=Ui(t,Bi.current);Ha(t,n),a=ko(null,t,i,e,a,n);var o=Ao();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Wi(i)?(o=!0,Ji(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ya(t),a.updater=_s,t.stateNode=a,a._reactInternals=t,xs(t,i,e,n),t=Vs(null,t,i,!0,o,n)):(t.tag=0,_a&&o&&pa(t),Ns(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch($s(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=hs(i,e),a){case 0:t=zs(null,t,i,e,n);break a;case 1:t=Bs(null,t,i,e,n);break a;case 11:t=Ps(null,t,i,e,n);break a;case 14:t=Fs(null,t,i,hs(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),zs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Bs(e,t,i,a,n);case 3:a:{if(Hs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Xa(e,t),to(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=Ss(Error(r(423)),t),t=Us(e,t,i,n,a);break a}else if(i!==a){a=Ss(Error(r(424)),t),t=Us(e,t,i,n,a);break a}else for(ga=xi(t.stateNode.containerInfo.firstChild),ha=t,_a=!0,va=null,n=Pa(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ea(),i===a){t=ec(e,t,n);break a}Ns(e,t,i,n)}t=t.child}return t;case 5:return uo(t),e===null&&Sa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,mi(i,a)?s=null:o!==null&&mi(i,o)&&(t.flags|=32),Rs(e,t),Ns(e,t,s,n),t.child;case 6:return e===null&&Sa(t),null;case 13:return Ks(e,t,n);case 4:return co(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Na(t,null,i,n):Ns(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Ps(e,t,i,a,n);case 7:return Ns(e,t,t.pendingProps,n),t.child;case 8:return Ns(e,t,t.pendingProps.children,n),t.child;case 12:return Ns(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Ri(Fa,i._currentValue),i._currentValue=s,o!==null)if(Z(o.value,s)){if(o.children===a.children&&!Vi.current){t=ec(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Za(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Va(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Va(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ns(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Ha(t,n),a=Ua(a),i=i(a),t.flags|=1,Ns(e,t,i,n),t.child;case 14:return i=t.type,a=hs(i,t.pendingProps),a=hs(i.type,a),Fs(e,t,i,a,n);case 15:return Is(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),$s(e,t),t.tag=1,Wi(i)?(e=!0,Ji(t)):e=!1,Ha(t,n),ys(t,i,a),xs(t,i,a,n),Vs(null,t,i,!0,e,n);case 19:return Qs(e,t,n);case 22:return Ls(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return nt(e,t)}function Gl(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(e,t,n,r){return new Gl(e,t,n,r)}function ql(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jl(e){if(typeof e==`function`)return+!!ql(e);if(e!=null){if(e=e.$$typeof,e===j)return 11;if(e===P)return 14}return 2}function Yl(e,t){var n=e.alternate;return n===null?(n=Kl(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xl(e,t,n,i,a,o){var s=2;if(i=e,typeof e==`function`)ql(e)&&(s=1);else if(typeof e==`string`)s=5;else a:switch(e){case E:return Zl(n.children,a,o,t);case D:s=8,a|=8;break;case O:return e=Kl(12,n,t,a|2),e.elementType=O,e.lanes=o,e;case M:return e=Kl(13,n,t,a),e.elementType=M,e.lanes=o,e;case N:return e=Kl(19,n,t,a),e.elementType=N,e.lanes=o,e;case I:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case k:s=10;break a;case A:s=9;break a;case j:s=11;break a;case P:s=14;break a;case F:s=16,i=null;break a}throw Error(r(130,e==null?e:typeof e,``))}return t=Kl(s,n,t,a),t.elementType=e,t.type=i,t.lanes=o,t}function Zl(e,t,n,r){return e=Kl(7,e,r,t),e.lanes=n,e}function Ql(e,t,n,r){return e=Kl(22,e,r,t),e.elementType=I,e.lanes=n,e.stateNode={isHidden:!1},e}function $l(e,t,n){return e=Kl(6,e,null,t),e.lanes=n,e}function eu(e,t,n){return t=Kl(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function tu(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ot(0),this.expirationTimes=Ot(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ot(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ya(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}};async function y(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData,r=await fetch(e,{credentials:`same-origin`,headers:n?t.headers:{"Content-Type":`application/json`,...t.headers??{}},...t}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function b(e){return e instanceof Error?e.message:String(e)}var x={session:()=>y(`/api/session`),login:e=>y(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})}),logout:()=>y(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>y(`/api/accounts?${e.toString()}`),account:e=>y(`/api/accounts/${e}`),channels:e=>y(`/api/channels?${e.toString()}`),channel:e=>y(`/api/channels/${e}`),bots:e=>y(`/api/bots?${e.toString()}`),bot:e=>y(`/api/bots/${e}`),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`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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(`Bot`,[[`path`,{d:`M12 8V4H8`,key:`hb8ula`}],[`rect`,{width:`16`,height:`12`,x:`4`,y:`8`,rx:`2`,key:`enze0r`}],[`path`,{d:`M2 14h2`,key:`vft8re`}],[`path`,{d:`M20 14h2`,key:`4cs60a`}],[`path`,{d:`M15 13v2`,key:`1xurst`}],[`path`,{d:`M9 13v2`,key:`rq6x2g`}]]),P=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`}]]),F=E(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),I=E(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),L=E(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),ee=E(`Clock3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`polyline`,{points:`12 6 12 12 16.5 12`,key:`1aq6pp`}]]),R=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`}]]),te=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`}]]),ne=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`}]]),re=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`}]]),ie=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`}]]),ae=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`}]]),oe=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`}]]),se=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`}]]),ce=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`}]]),le=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`}]]),ue=E(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),z=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`}]]),B=E(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),de=E(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),fe=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`}]]),pe=E(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),me=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`}]]),he=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`}]]),ge=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`}]]),_e=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`}]]),ve=E(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),ye=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`}]]),be=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`}]]),V=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`}]]),H=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),xe=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})),U=o(((e,t)=>{t.exports=xe()}))(),Se=`telesrv.admin.lang`,Ce={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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Switch to dark theme`,"theme.switchToLight":`Switch to light theme`,"login.heading":`Operations Admin`,"login.body":`Enter credentials to open the console.`,"login.secret":`Admin password or token`,"login.submit":`Log in`,"login.submitting":`Logging in`,"dashboard.eyebrow":`Runtime Overview`,"dashboard.title":`Console Overview`,"dashboard.readPath":`Read path`,"dashboard.readPathValue":`PG read-only`,"dashboard.writePath":`Write path`,"dashboard.executionPolicy":`Execution policy`,"dashboard.dryRunFirst":`Dry-run first`,"dashboard.accountsText":`Account status, premium, verification, sessions.`,"dashboard.channelsText":`Public entities, member counts, verification state.`,"dashboard.messagesText":`Message boxes, updates, outbox state.`,"dashboard.strip.dryRun":`All dangerous actions start with dry-run`,"dashboard.strip.token":`Browser never stores internal tokens`,"dashboard.strip.pagination":`Lists use cursor pagination`,"dashboard.strip.snapshot":`Detail pages retain raw state snapshots`,"account.pageTitle":`Accounts`,"account.queryResults":`Search results`,"account.recentActive":`Recently active accounts`,"account.currentPage":`Accounts on page`,"account.onlineDevices":`Online device records`,"account.premium":`Premium`,"account.frozen":`Frozen`,"account.searchPlaceholder":`User ID / phone / username`,"account.userID":`User ID`,"account.phone":`Phone`,"account.lastActive":`Last active`,"account.notVerified":`Not verified`,"account.notPremium":`Not premium`,"account.premiumUntil":`Premium expires`,"account.starsBalance":`Stars balance`,"account.startingGrantApplied":`initial grant applied`,"account.startingGrantPending":`initial grant pending`,"account.activeSessions":`Authorized devices`,"account.accountFlags":`Account flags`,"account.restriction":`Restriction`,"account.restricted":`Restricted`,"account.createdAt":`Created`,"account.detailTitle":`Account #{id}`,"account.profile":`Account Profile`,"account.loadingDetail":`Loading account detail`,"account.waitingData":`Waiting for data`,"account.noUsername":`No username`,"account.noPhone":`No phone`,"account.accountFrozen":`Account frozen`,"account.accountActive":`Account active`,"account.authorizationsTitle":`Authorized Devices`,"account.authorizationsCount":`{count} authorizations`,"account.recentAdminOps":`Recent Admin Actions`,"account.recent30Audit":`Last 30 audit rows`,"account.actionDock":`Account Actions`,"account.freezeAccount":`Freeze account`,"account.updateFreeze":`Update freeze`,"account.unfreezeAccount":`Unfreeze account`,"account.freezeSince":`Frozen since`,"account.freezeUntil":`Appeal deadline`,"account.freezeUntilAria":`Freeze appeal deadline`,"account.freezeAppealURL":`Appeal URL`,"account.freezeAppealURLAria":`Freeze appeal URL`,"account.premiumMonths":`Premium duration (months)`,"account.premiumMonthsAria":`Set premium duration in months`,"account.setPremium":`Set premium`,"account.clearPremium":`Clear premium`,"account.starsAmount":`Stars to grant`,"account.starsAmountAria":`Set Stars amount to grant`,"account.grantStars":`Grant Stars`,"account.setVerified":`Set verified`,"account.clearVerified":`Clear verified`,"channel.pageTitle":`Supergroups and Channels`,"channel.recentUpdated":`Recently updated`,"channel.currentPage":`Entities on page`,"channel.megagroups":`Supergroups`,"channel.broadcasts":`Channels`,"channel.verifiedCount":`Verified`,"channel.searchPlaceholder":`Channel ID / username / title`,"channel.channelID":`Channel ID`,"channel.kind":`Kind`,"channel.title":`Title`,"channel.pts":`PTS`,"channel.detailProfile":`Channel Profile`,"channel.loadingDetail":`Loading channel detail`,"channel.creator":`Creator {id}`,"channel.governance":`Moderation`,"channel.governanceValue":`Banned {banned} / Kicked {kicked}`,"channel.flags":`Channel flags`,"channel.rawRow":`Channel Raw Row`,"channel.rawRowText":`Database read-only snapshot`,"channel.actionDock":`Channel Actions`,"channel.setVerified":`Set verified`,"channel.clearVerified":`Clear verified`,"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`,"route.bots":`Bots`,"route.botsSubtitle":`Console / Bots`,"layout.bots":`Bots`,"bots.pageTitle":`Bots`,"bots.queryResults":`Search results`,"bots.recent":`Recently created bots`,"bots.currentPage":`Bots on page`,"bots.banned":`Banned`,"bots.active":`Active`,"bots.createTitle":`Create a system bot`,"bots.createHint":`Provision a bot account owned by the given user. The token is shown once after confirmation.`,"bots.ownerUserID":`Owner user ID`,"bots.name":`Display name`,"bots.namePlaceholder":`e.g. Service Bot`,"bots.username":`Username`,"bots.usernameHint":`Username must be 5-32 characters and end with 'bot'.`,"bots.create":`Create bot`,"bots.searchPlaceholder":`Bot ID / username`,"bots.botID":`Bot ID`,"bots.owner":`Owner`,"bots.status":`Status`,"bots.detailTitle":`Bot #{id}`,"bots.profile":`Bot Profile`,"bots.loadingDetail":`Loading bot detail`,"bots.unnamed":`Unnamed bot`,"bots.restriction":`Restriction`,"bots.actionDock":`Bot Actions`,"bots.banUntil":`Ban until`,"bots.ban":`Ban bot`,"bots.updateBan":`Update ban`,"bots.unban":`Unban bot`,"bots.type":`Type`,"bots.system":`System`,"bots.user":`User`,"bots.delete":`Delete bot`,"bots.deleteHint":`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`,"bots.systemHint":`System bots are built in and cannot be deleted.`,"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.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`中文`,"language.ru":`RU`,"theme.switchToDark":`切换到深色主题`,"theme.switchToLight":`切换到浅色主题`,"login.heading":`运维后台`,"login.body":`输入凭据后进入控制台。`,"login.secret":`管理员密码或 token`,"login.submit":`登录`,"login.submitting":`登录中`,"dashboard.eyebrow":`运行总览`,"dashboard.title":`控制台总览`,"dashboard.readPath":`读路径`,"dashboard.readPathValue":`PG 只读`,"dashboard.writePath":`写路径`,"dashboard.executionPolicy":`执行策略`,"dashboard.dryRunFirst":`先预演`,"dashboard.accountsText":`账号状态、会员、认证、会话。`,"dashboard.channelsText":`公开实体、成员计数、认证状态。`,"dashboard.messagesText":`消息盒、update、outbox 状态。`,"dashboard.strip.dryRun":`所有危险操作先预演`,"dashboard.strip.token":`浏览器不持有内部 token`,"dashboard.strip.pagination":`列表使用游标分页`,"dashboard.strip.snapshot":`详情页保留原始状态快照`,"account.pageTitle":`账号`,"account.queryResults":`查询结果`,"account.recentActive":`最近活跃账号`,"account.currentPage":`当前页账号`,"account.onlineDevices":`在线设备记录`,"account.premium":`会员`,"account.frozen":`冻结`,"account.searchPlaceholder":`用户 ID / 手机号 / 用户名`,"account.userID":`用户 ID`,"account.phone":`手机号`,"account.lastActive":`最近活跃`,"account.notVerified":`未认证`,"account.notPremium":`非会员`,"account.premiumUntil":`会员到期`,"account.starsBalance":`Stars 余额`,"account.startingGrantApplied":`初始赠送已发放`,"account.startingGrantPending":`初始赠送未触发`,"account.activeSessions":`授权设备`,"account.accountFlags":`账号标记`,"account.restriction":`限制状态`,"account.restricted":`已限制`,"account.createdAt":`创建时间`,"account.detailTitle":`账号 #{id}`,"account.profile":`账号档案`,"account.loadingDetail":`加载账号详情`,"account.waitingData":`等待数据`,"account.noUsername":`无用户名`,"account.noPhone":`无手机号`,"account.accountFrozen":`账号已冻结`,"account.accountActive":`账号正常`,"account.authorizationsTitle":`授权设备`,"account.authorizationsCount":`共 {count} 个授权`,"account.recentAdminOps":`最近后台操作`,"account.recent30Audit":`最近 30 条审计`,"account.actionDock":`账号操作`,"account.freezeAccount":`冻结账号`,"account.updateFreeze":`更新冻结信息`,"account.unfreezeAccount":`解冻账号`,"account.freezeSince":`冻结开始时间`,"account.freezeUntil":`申诉截止时间`,"account.freezeUntilAria":`账号冻结申诉截止时间`,"account.freezeAppealURL":`申诉链接`,"account.freezeAppealURLAria":`账号冻结申诉链接`,"account.premiumMonths":`会员时长(月)`,"account.premiumMonthsAria":`设置会员时长,单位月`,"account.setPremium":`设置会员`,"account.clearPremium":`取消会员`,"account.starsAmount":`赠送 Stars 数量`,"account.starsAmountAria":`设置要赠送的 Stars 数量`,"account.grantStars":`赠送 Stars`,"account.setVerified":`设置认证`,"account.clearVerified":`取消认证`,"channel.pageTitle":`超级群与频道`,"channel.recentUpdated":`最近更新`,"channel.currentPage":`当前页实体`,"channel.megagroups":`超级群`,"channel.broadcasts":`频道`,"channel.verifiedCount":`已认证`,"channel.searchPlaceholder":`频道 ID / 用户名 / 标题`,"channel.channelID":`频道 ID`,"channel.kind":`类型`,"channel.title":`标题`,"channel.pts":`PTS`,"channel.detailProfile":`频道档案`,"channel.loadingDetail":`加载频道详情`,"channel.creator":`创建者 {id}`,"channel.governance":`治理状态`,"channel.governanceValue":`封禁 {banned} / 踢出 {kicked}`,"channel.flags":`频道标记`,"channel.rawRow":`频道原始行`,"channel.rawRowText":`数据库只读快照`,"channel.actionDock":`频道操作`,"channel.setVerified":`设置认证`,"channel.clearVerified":`取消认证`,"channel.kind.broadcast":`频道`,"channel.kind.forum":`超级群/论坛`,"channel.kind.megagroup":`超级群`,"channel.kind.generic":`频道/群`,"route.bots":`机器人`,"route.botsSubtitle":`控制台 / 机器人`,"layout.bots":`机器人`,"bots.pageTitle":`机器人`,"bots.queryResults":`查询结果`,"bots.recent":`最近创建的机器人`,"bots.currentPage":`当前页机器人`,"bots.banned":`已封禁`,"bots.active":`正常`,"bots.createTitle":`创建系统机器人`,"bots.createHint":`为指定用户创建机器人账号。确认后 token 只显示一次。`,"bots.ownerUserID":`所属用户 ID`,"bots.name":`显示名称`,"bots.namePlaceholder":`例如:服务机器人`,"bots.username":`用户名`,"bots.usernameHint":`用户名需 5-32 个字符,且以 bot 结尾。`,"bots.create":`创建机器人`,"bots.searchPlaceholder":`机器人 ID / 用户名`,"bots.botID":`机器人 ID`,"bots.owner":`所属用户`,"bots.status":`状态`,"bots.detailTitle":`机器人 #{id}`,"bots.profile":`机器人档案`,"bots.loadingDetail":`加载机器人详情`,"bots.unnamed":`未命名机器人`,"bots.restriction":`限制状态`,"bots.actionDock":`机器人操作`,"bots.banUntil":`封禁至`,"bots.ban":`封禁机器人`,"bots.updateBan":`更新封禁`,"bots.unban":`解封机器人`,"bots.type":`类型`,"bots.system":`系统`,"bots.user":`用户`,"bots.delete":`删除机器人`,"bots.deleteHint":`永久删除该用户创建的机器人并使其 token 失效。此操作不可撤销。`,"bots.systemHint":`系统内置机器人不可删除。`,"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.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка…`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтверждён`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Тёмная тема`,"theme.switchToLight":`Светлая тема`,"login.heading":`Панель администратора`,"login.body":`Введите учётные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход…`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, Premium, подтверждение, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, число участников, статус подтверждения.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтверждён`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звёзд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество Звёзд`,"account.starsAmountAria":`Указать количество начисляемых Звёзд`,"account.grantStars":`Начислить Звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновлённые`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждённые`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"route.bots":`Боты`,"route.botsSubtitle":`Консоль / Боты`,"layout.bots":`Боты`,"bots.pageTitle":`Боты`,"bots.queryResults":`Результаты поиска`,"bots.recent":`Недавно созданные боты`,"bots.currentPage":`Боты на странице`,"bots.banned":`Забанен`,"bots.active":`Активен`,"bots.createTitle":`Создать системного бота`,"bots.createHint":`Создаёт бота, принадлежащего указанному пользователю. Токен показывается один раз после подтверждения.`,"bots.ownerUserID":`ID владельца`,"bots.name":`Отображаемое имя`,"bots.namePlaceholder":`например, Service Bot`,"bots.username":`Имя пользователя`,"bots.usernameHint":`Имя пользователя: 5–32 символа, обязательно оканчивается на «bot».`,"bots.create":`Создать бота`,"bots.searchPlaceholder":`ID бота / имя пользователя`,"bots.botID":`ID бота`,"bots.owner":`Владелец`,"bots.status":`Статус`,"bots.detailTitle":`Бот #{id}`,"bots.profile":`Профиль бота`,"bots.loadingDetail":`Загрузка данных бота`,"bots.unnamed":`Без имени`,"bots.restriction":`Ограничение`,"bots.actionDock":`Действия с ботом`,"bots.banUntil":`Забанить до`,"bots.ban":`Забанить бота`,"bots.updateBan":`Обновить бан`,"bots.unban":`Разбанить бота`,"bots.type":`Тип`,"bots.system":`Системный`,"bots.user":`Пользовательский`,"bots.delete":`Удалить бота`,"bots.deleteHint":`Безвозвратно удаляет созданного пользователем бота и аннулирует его токен. Действие необратимо.`,"bots.systemHint":`Системные боты встроены и не могут быть удалены.`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звёздных подарков`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звёздного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звёздах`,"gifts.convertStars":`Звёзд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звёздные подарки ещё не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звёздах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Отозвать все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтверждённые`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Повторить тестовый запуск`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},we=(0,g.createContext)(null);function Te({children:e}){let[t,n]=(0,g.useState)(()=>Oe());(0,g.useEffect)(()=>{try{localStorage.setItem(Se,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=De(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>De(t,e,n)}),[t]);return(0,U.jsx)(we.Provider,{value:r,children:e})}function W(){let e=(0,g.useContext)(we);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function Ee(){let{lang:e,setLang:t,t:n}=W();return(0,U.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,U.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function De(e,t,n){let r=Ce[e][t]??Ce.en[t]??t;return n?r.replace(/\{(\w+)\}/g,(e,t)=>String(n[t]??``)):r}function Oe(){try{let e=ke(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=ke(localStorage.getItem(Se));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=ke(t);if(e)return e}return`en`}function ke(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:t===`ru`||t.startsWith(`ru-`)?`ru`:null}function Ae(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function je(e,t){return e.startsWith(`/accounts`)?t(`route.accounts`):e.startsWith(`/channels`)?t(`route.channels`):e.startsWith(`/bots`)?t(`route.bots`):e.startsWith(`/messages`)?t(`route.messages`):e.startsWith(`/gifts`)?t(`route.gifts`):t(`route.dashboard`)}function Me(e,t){return e.startsWith(`/accounts`)?t(`route.accountsSubtitle`):e.startsWith(`/channels`)?t(`route.channelsSubtitle`):e.startsWith(`/bots`)?t(`route.botsSubtitle`):e.startsWith(`/messages`)?t(`route.messagesSubtitle`):e.startsWith(`/gifts`)?t(`route.giftsSubtitle`):t(`route.dashboardSubtitle`)}var Ne=`telesrv.admin.theme`,Pe=(0,g.createContext)(null);function Fe(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Ie({children:e}){let[t,n]=(0,g.useState)(()=>ze());(0,g.useEffect)(()=>{Fe(t);try{localStorage.setItem(Ne,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(Ne)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,U.jsx)(Pe.Provider,{value:a,children:e})}function Le(){let e=(0,g.useContext)(Pe);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function Re(){let{theme:e,toggleTheme:t}=Le(),{t:n}=W(),r=n(e===`light`?`theme.switchToDark`:`theme.switchToLight`);return(0,U.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":r,title:r,children:e===`dark`?(0,U.jsx)(ve,{size:16}):(0,U.jsx)(ue,{size:16})})}function ze(){try{let e=localStorage.getItem(Ne);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}function G({href:e,navigate:t,className:n,children:r}){return(0,U.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Be(){let{t:e}=W();return(0,U.jsxs)(`div`,{className:`boot-screen`,children:[(0,U.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,U.jsx)(`div`,{className:`loader-bar`})]})}function Ve({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=W(),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,U.jsxs)(`div`,{className:`shell`,children:[(0,U.jsxs)(`aside`,{className:`sidebar`,children:[(0,U.jsxs)(G,{className:`brand`,href:`/`,navigate:n,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,U.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,U.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,U.jsx)(He,{icon:(0,U.jsx)(se,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(V,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(he,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(N,{size:16}),href:`/bots`,route:t,navigate:n,children:a(`layout.bots`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(ie,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,U.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,U.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,U.jsx)(le,{size:16}),(0,U.jsx)(`span`,{children:a(`layout.messages`)}),(0,U.jsx)(I,{className:`nav-section-chevron`,size:15})]}),s&&(0,U.jsxs)(`div`,{className:`nav-children`,children:[(0,U.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,U.jsx)(He,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,U.jsxs)(`div`,{className:`sidebar-status`,children:[(0,U.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(me,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,U.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(R,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,U.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(ge,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,U.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,U.jsxs)(`div`,{className:`workspace`,children:[(0,U.jsxs)(`header`,{className:`topbar`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:Me(t.path,a)}),(0,U.jsx)(`h1`,{children:je(t.path,a)})]}),(0,U.jsxs)(`div`,{className:`topbar-actions`,children:[(0,U.jsx)(Re,{}),(0,U.jsx)(Ee,{}),(0,U.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,U.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,U.jsx)(ce,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,U.jsx)(`main`,{className:`content`,children:i})]})]})}function He({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,U.jsxs)(G,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,U.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,U.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 K(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function qe(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function q(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function Je(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 Ye({title:e,eyebrow:t,children:n,actions:r}){return(0,U.jsxs)(`div`,{className:`page-frame`,children:[(0,U.jsxs)(`div`,{className:`page-title-row`,children:[(0,U.jsxs)(`div`,{children:[t&&(0,U.jsx)(`div`,{className:`eyebrow`,children:t}),(0,U.jsx)(`h2`,{children:e})]}),r&&(0,U.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Xe({children:e}){return(0,U.jsx)(`div`,{className:`query-panel`,children:e})}function Ze({main:e,side:t}){return(0,U.jsxs)(`div`,{className:`split-layout`,children:[(0,U.jsx)(`div`,{className:`split-main`,children:e}),(0,U.jsx)(`aside`,{className:`split-side`,children:t})]})}function Qe({title:e,text:t,action:n}){return(0,U.jsxs)(`div`,{className:`section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`h2`,{children:e}),t&&(0,U.jsx)(`p`,{children:t})]}),n&&(0,U.jsx)(`div`,{className:`section-action`,children:n})]})}function $e({children:e}){return(0,U.jsxs)(`div`,{className:`alert`,children:[(0,U.jsx)(O,{size:16}),` `,(0,U.jsx)(`span`,{children:e})]})}function J({children:e,tone:t=`neutral`}){return(0,U.jsx)(`span`,{className:`badge ${t}`,children:e})}function et({label:e,value:t,tone:n}){return(0,U.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{children:t})]})}function tt({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,U.jsxs)(`div`,{className:`metric ${n}`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Y({label:e,value:t,mono:n=!1}){return(0,U.jsxs)(`div`,{className:`summary-item`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function nt({rows:e}){let{t}=W();return(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`audit.id`)}),(0,U.jsx)(`th`,{children:t(`audit.commandID`)}),(0,U.jsx)(`th`,{children:t(`audit.action`)}),(0,U.jsx)(`th`,{children:t(`audit.actor`)}),(0,U.jsx)(`th`,{children:t(`audit.status`)}),(0,U.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,U.jsx)(`th`,{children:t(`audit.reason`)}),(0,U.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[e.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,U.jsx)(`td`,{children:e.Action}),(0,U.jsx)(`td`,{children:e.Actor}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,U.jsx)(`td`,{children:K(e.CreatedAt)})]},e.ID)),e.length===0&&(0,U.jsx)(rt,{colSpan:8})]})]})})}function rt({colSpan:e}){let{t}=W();return(0,U.jsx)(`tr`,{children:(0,U.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function it({label:e}){return(0,U.jsx)(`section`,{className:`surface`,children:(0,U.jsx)(`div`,{className:`loading-line`,children:e})})}function at({value:e}){return(0,U.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function ot({onLogin:e}){let{t}=W(),[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,U.jsx)(`main`,{className:`login-page`,children:(0,U.jsxs)(`section`,{className:`login-panel`,children:[(0,U.jsxs)(`div`,{className:`login-head`,children:[(0,U.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,U.jsxs)(`div`,{className:`login-head-actions`,children:[(0,U.jsx)(Re,{}),(0,U.jsx)(Ee,{}),(0,U.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,U.jsxs)(`div`,{className:`login-copy`,children:[(0,U.jsx)(`h1`,{children:t(`login.heading`)}),(0,U.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,U.jsx)($e,{children:i}),(0,U.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:t(`login.secret`)}),(0,U.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,U.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var st=m();function ct({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=W(),[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,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,st.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,U.jsx)(`h2`,{children:e})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,U.jsx)(H,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`div`,{className:`command-steps`,children:[(0,U.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,U.jsx)(`span`,{children:`1`}),(0,U.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`2`}),(0,U.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`3`}),(0,U.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:s(`action.reason`)}),(0,U.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,U.jsxs)(`div`,{className:`command-preview`,children:[(0,U.jsxs)(`div`,{className:`preview-head`,children:[(0,U.jsx)(ne,{size:14}),` `,s(`action.requestPreview`)]}),(0,U.jsx)(at,{value:JSON.stringify(T,null,2)})]}),m&&(0,U.jsx)($e,{children:m}),f&&(0,U.jsxs)(`div`,{className:`result-box`,children:[(0,U.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,U.jsx)(O,{size:16}):(0,U.jsx)(k,{size:16}),(0,U.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.commandID`)}),(0,U.jsx)(`strong`,{children:f.command_id})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.status`)}),(0,U.jsx)(`strong`,{children:f.status})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.dryRun`)}),(0,U.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,U.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,U.jsx)(at,{value:JSON.stringify(f.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(B,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,U.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,U.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function lt({rows:e,userID:t,onDone:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`authorization-block`,children:[(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:r(`auth.device`)}),(0,U.jsx)(`th`,{children:r(`auth.platform`)}),(0,U.jsx)(`th`,{children:r(`auth.ip`)}),(0,U.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,U.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,U.jsxs)(`tbody`,{children:[o.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,U.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,U.jsx)(`td`,{children:n.IP}),(0,U.jsx)(`td`,{children:K(n.ActiveAt)}),(0,U.jsx)(`td`,{className:`device-actions-cell`,children:(0,U.jsxs)(`div`,{className:`device-actions`,children:[(0,U.jsx)(ct,{label:r(`auth.revokeCurrent`),icon:(0,U.jsx)(ce,{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,U.jsx)(ct,{label:r(`auth.keepCurrent`),icon:(0,U.jsx)(he,{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,U.jsx)(rt,{colSpan:5})]})]})}),(0,U.jsx)(`div`,{className:`danger-zone`,children:(0,U.jsx)(ct,{label:r(`auth.revokeAll`),icon:(0,U.jsx)(P,{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}=W(),[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,U.jsx)($e,{children:a});if(!r)return(0,U.jsx)(it,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,U.jsx)(Ye,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,U.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:Ge(y)}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[We(y.Username)||n(`account.noUsername`),` · `,Ue(y.Phone)||n(`account.noPhone`)]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,U.jsx)(J,{tone:`good`,children:n(`account.premium`)}):(0,U.jsx)(J,{children:n(`account.notPremium`)}),r.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)}),y.Frozen?(0,U.jsx)(J,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,U.jsx)(J,{children:n(`account.accountActive`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,U.jsx)(Y,{label:n(`account.lastActive`),value:qe(r.LastSeenAt)||`-`}),(0,U.jsx)(Y,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?qe(y.PremiumUntil):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,U.jsx)(Y,{label:n(`common.updatedAt`),value:K(y.UpdatedAt)||`-`}),(0,U.jsx)(Y,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,U.jsx)(Y,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,U.jsx)(Y,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.freezeSince`),value:r.Restriction.Since?K(r.Restriction.Since):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.freezeUntil`),value:r.Restriction.Until?K(r.Restriction.Until):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.createdAt`),value:K(y.CreatedAt)||`-`})]}),r.About&&(0,U.jsx)(`p`,{className:`about-text`,children:r.About}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,U.jsx)(lt,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(nt,{rows:r.AuditLogs})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,U.jsx)(ct,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,U.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,U.jsx)(ct,{label:n(`account.unfreezeAccount`),icon:(0,U.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(ct,{label:n(`account.setPremium`),icon:(0,U.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:q(l)}),onDone:v}),(0,U.jsx)(ct,{label:n(`account.clearPremium`),icon:(0,U.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,U.jsx)(ct,{label:n(`account.grantStars`),icon:(0,U.jsx)(_e,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:q(d)}),onDone:v}),(0,U.jsx)(ct,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,U.jsx)(D,{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}=W(),[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,U.jsxs)(Ye,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,U.jsx)(fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,U.jsx)(tt,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,U.jsx)(tt,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,U.jsx)(tt,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(pe,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`account.userID`)}),(0,U.jsx)(`th`,{children:t(`account.phone`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`common.name`)}),(0,U.jsx)(`th`,{children:t(`common.device`)}),(0,U.jsx)(`th`,{children:t(`account.lastActive`)}),(0,U.jsx)(`th`,{children:t(`account.premium`)}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`account.frozen`)}),(0,U.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:Ue(n.Phone)}),(0,U.jsx)(`td`,{children:We(n.Username)}),(0,U.jsx)(`td`,{children:Ge(n)}),(0,U.jsx)(`td`,{children:n.DeviceCount}),(0,U.jsx)(`td`,{children:K(n.LastActiveAt)}),(0,U.jsx)(`td`,{children:n.PremiumUntil>0?(0,U.jsxs)(J,{tone:`good`,children:[t(`account.premium`),` `,qe(n.PremiumUntil)]}):(0,U.jsx)(J,{children:t(`common.none`)})}),(0,U.jsx)(`td`,{children:n.Verified?(0,U.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,U.jsx)(J,{children:t(`account.notVerified`)})}),(0,U.jsx)(`td`,{children:n.Frozen?(0,U.jsx)(J,{tone:`danger`,children:t(`account.frozen`)}):(0,U.jsx)(J,{children:t(`common.normal`)})}),(0,U.jsx)(`td`,{children:K(n.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,U.jsx)(rt,{colSpan:11})]})]})})]})}function ht({id:e,navigate:t}){let{t:n}=W(),[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,U.jsx)($e,{children:a});if(!r)return(0,U.jsx)(it,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,U.jsx)(Ye,{title:`${Ke(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,U.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[We(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{children:Ke(c,n)}),c.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)}),c.Deleted?(0,U.jsx)(J,{tone:`danger`,children:n(`common.deleted`)}):(0,U.jsx)(J,{children:n(`common.valid`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,U.jsx)(Y,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,U.jsx)(Y,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,U.jsx)(Y,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,U.jsx)(Y,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,U.jsx)(Y,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,U.jsx)(Y,{label:n(`account.createdAt`),value:qe(c.Date)||`-`}),(0,U.jsx)(Y,{label:n(`common.updatedAt`),value:K(c.UpdatedAt)||`-`})]}),c.About&&(0,U.jsx)(`p`,{className:`about-text`,children:c.About}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(nt,{rows:r.AuditLogs})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,U.jsx)(at,{value:r.ChannelJSON})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,U.jsx)(ct,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,U.jsx)(D,{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}=W(),[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,U.jsxs)(Ye,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,U.jsx)(fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,U.jsx)(tt,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,U.jsx)(tt,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,U.jsx)(tt,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(pe,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`channel.channelID`)}),(0,U.jsx)(`th`,{children:t(`channel.kind`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`channel.title`)}),(0,U.jsx)(`th`,{children:t(`common.members`)}),(0,U.jsx)(`th`,{children:t(`common.admins`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:Ke(n,t)}),(0,U.jsx)(`td`,{children:We(n.Username)}),(0,U.jsx)(`td`,{children:n.Title}),(0,U.jsx)(`td`,{children:n.ParticipantsCount}),(0,U.jsx)(`td`,{children:n.AdminsCount}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.Verified?(0,U.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,U.jsx)(J,{children:t(`account.notVerified`)})}),(0,U.jsx)(`td`,{children:K(n.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,U.jsx)(rt,{colSpan:10})]})]})})]})}function _t({id:e,navigate:t}){let{t:n}=W(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1);async function l(){c(!0),o(``);try{i(await x.bot(e))}catch(e){o(b(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{l()},[e]),a)return(0,U.jsx)($e,{children:a});if(!r)return(0,U.jsx)(it,{label:n(s?`bots.loadingDetail`:`account.waitingData`)});let u=r.Bot;return(0,U.jsx)(Ye,{title:n(`bots.detailTitle`,{id:u.ID}),eyebrow:n(`bots.profile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,U.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:u.FirstName||n(`bots.unnamed`)}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:We(u.Username)||n(`account.noUsername`)})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{tone:u.System?`warn`:`neutral`,children:u.System?n(`bots.system`):n(`bots.user`)}),u.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:n(`bots.botID`),value:String(u.ID),mono:!0}),(0,U.jsx)(Y,{label:n(`bots.owner`),value:u.OwnerUserID>0?`${u.OwnerUserID} ${We(r.OwnerUsername)}`.trim():n(`common.none`)}),(0,U.jsx)(Y,{label:n(`bots.type`),value:u.System?n(`bots.system`):n(`bots.user`)}),(0,U.jsx)(Y,{label:n(`common.updatedAt`),value:K(u.UpdatedAt)||`-`}),(0,U.jsx)(Y,{label:n(`account.createdAt`),value:K(u.CreatedAt)||`-`})]}),r.About&&(0,U.jsx)(`p`,{className:`about-text`,children:r.About}),r.Description&&r.Description.trim()!==r.About.trim()&&(0,U.jsx)(`p`,{className:`about-text`,children:r.Description}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(nt,{rows:r.AuditLogs})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`bots.actionDock`)}),(0,U.jsx)(`div`,{className:`action-stack`,children:(0,U.jsx)(ct,{label:u.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,U.jsx)(D,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:u.ID,verified:!u.Verified}),onDone:l})}),u.System?(0,U.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.systemHint`)}):(0,U.jsxs)(`div`,{className:`danger-zone`,children:[(0,U.jsx)(ct,{label:n(`bots.delete`),icon:(0,U.jsx)(ye,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:u.ID}),onDone:()=>t(`/bots`)}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.deleteHint`)})]})]})})})}function vt({navigate:e}){let{t}=W(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(0),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(``),[y,S]=(0,g.useState)(``);async function C(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&t.set(`before_id`,String(c));try{let e=await x.bots(t);s(e),l(e.next_before_id)}catch(e){p(b(e))}finally{d(!1)}}(0,g.useEffect)(()=>{C(!1)},[]);let w=o?.rows??[],T=w.filter(e=>e.Verified).length,E=w.filter(e=>e.System).length;return(0,U.jsxs)(Ye,{title:t(`bots.pageTitle`),eyebrow:o?.listing===!1?t(`bots.queryResults`):t(`bots.recent`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>C(!1),disabled:u,children:[(0,U.jsx)(fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`bots.currentPage`),value:String(w.length)}),(0,U.jsx)(tt,{label:t(`common.verified`),value:String(T),tone:`good`}),(0,U.jsx)(tt,{label:t(`bots.system`),value:String(E)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(`div`,{className:`section-head`,children:(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`h2`,{children:t(`bots.createTitle`)}),(0,U.jsx)(`p`,{children:t(`bots.createHint`)})]})}),(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.ownerUserID`)}),(0,U.jsx)(`input`,{value:m,onChange:e=>h(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.name`)}),(0,U.jsx)(`input`,{value:_,onChange:e=>v(e.target.value),placeholder:t(`bots.namePlaceholder`),maxLength:64})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.username`)}),(0,U.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:`my_service_bot`})]})]}),(0,U.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,U.jsx)(`span`,{className:`bot-create-note`,children:t(`bots.usernameHint`)}),(0,U.jsx)(ct,{label:t(`bots.create`),icon:(0,U.jsx)(de,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:q(m),name:_.trim(),username:y.trim().replace(/^@/,``)}),onDone:()=>C(!1)})]})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),C(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`bots.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(pe,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>C(!0),disabled:u,children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`bots.botID`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`common.name`)}),(0,U.jsx)(`th`,{children:t(`bots.owner`)}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`bots.type`)}),(0,U.jsx)(`th`,{children:t(`account.createdAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[w.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:We(n.Username)||`-`}),(0,U.jsx)(`td`,{children:n.FirstName||`-`}),(0,U.jsx)(`td`,{className:`mono`,children:n.OwnerUserID>0?n.OwnerUserID:`-`}),(0,U.jsx)(`td`,{children:n.Verified?(0,U.jsxs)(J,{tone:`good`,children:[(0,U.jsx)(D,{size:12}),` `,t(`common.verified`)]}):(0,U.jsx)(J,{children:t(`account.notVerified`)})}),(0,U.jsx)(`td`,{children:n.System?(0,U.jsx)(J,{tone:`warn`,children:t(`bots.system`)}):(0,U.jsx)(J,{children:t(`bots.user`)})}),(0,U.jsx)(`td`,{children:K(n.CreatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${n.ID}`),children:[(0,U.jsx)(N,{size:14}),` `,t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},n.ID)),w.length===0&&(0,U.jsx)(rt,{colSpan:8})]})]})})]})}function yt({navigate:e}){let{t}=W();return(0,U.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,U.jsxs)(`section`,{className:`overview-band`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,U.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,U.jsxs)(`div`,{className:`overview-metrics`,children:[(0,U.jsx)(et,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,U.jsx)(et,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,U.jsx)(et,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,U.jsxs)(`div`,{className:`command-grid`,children:[(0,U.jsx)(bt,{icon:(0,U.jsx)(V,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,U.jsx)(bt,{icon:(0,U.jsx)(he,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,U.jsx)(bt,{icon:(0,U.jsx)(le,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,U.jsxs)(`section`,{className:`work-strip`,children:[(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(k,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(oe,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(ee,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(ne,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function bt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,U.jsxs)(G,{className:`launcher`,href:r,navigate:i,children:[(0,U.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,U.jsxs)(`span`,{className:`launcher-copy`,children:[(0,U.jsx)(`strong`,{children:t}),(0,U.jsx)(`span`,{children:n})]}),(0,U.jsx)(L,{size:16})]})}function xt({channelID:e,msgID:t,navigate:n}){let{t:r}=W(),[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,U.jsx)($e,{children:o});if(!i)return(0,U.jsx)(it,{label:r(`common.loading`)});let l=i.Message;return(0,U.jsx)(Ye,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,U.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:qe(l.Date)})})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,U.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,U.jsx)(J,{children:r(`common.survived`)}),l.Pinned&&(0,U.jsx)(J,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,U.jsx)(J,{children:r(`messages.channelPost`)}),(0,U.jsxs)(J,{children:[`pts `,l.PTS]})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,U.jsx)(Y,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,U.jsx)(Y,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,U.jsx)(Y,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,U.jsx)(at,{value:i.MessageJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,U.jsx)(at,{value:i.ChannelJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.count`)}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.messageId`)}),(0,U.jsx)(`th`,{children:r(`common.sender`)}),(0,U.jsx)(`th`,{children:r(`common.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:e.MessageID}),(0,U.jsx)(`td`,{children:e.SenderUserID}),(0,U.jsx)(`td`,{children:qe(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,U.jsx)(rt,{colSpan:6})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.eventJson`)}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,U.jsx)(at,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,U.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function St({label:e,value:t,onChange:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(F,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:Ge(t)}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:We(t.Username)||Ue(t.Phone)||`-`})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,U.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,U.jsx)(`div`,{className:`picker-error`,children:u}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:Ge(e)}),(0,U.jsx)(`span`,{children:We(e.Username)||Ue(e.Phone)||`-`}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,U.jsx)(J,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,U.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function Ct({label:e,value:t,onChange:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(F,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:t.Title||`-`}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:We(t.Username)||Ke(t,r)})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,U.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,U.jsx)(`div`,{className:`picker-error`,children:u}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:e.Title||`-`}),(0,U.jsx)(`span`,{children:We(e.Username)||Ke(e,r)}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,U.jsx)(J,{children:Ke(e,r)})]},e.ID)),o.length===0&&!c?(0,U.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function wt({navigate:e}){let{t}=W(),[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,U.jsxs)(Ye,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(Xe,{children:[(0,U.jsx)(`div`,{className:`message-selector-grid single`,children:(0,U.jsx)(Ct,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,U.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(pe,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`messages.currentPage`),value:String(_.length)}),(0,U.jsx)(tt,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,U.jsx)(tt,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,U.jsx)(tt,{label:t(`messages.channelGroup`),value:n?`${n.Title||Ke(n,t)} (${n.ID})`:`-`})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`common.messageId`)}),(0,U.jsx)(`th`,{children:t(`common.time`)}),(0,U.jsx)(`th`,{children:t(`common.sender`)}),(0,U.jsx)(`th`,{children:`From Peer`}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.views`)}),(0,U.jsx)(`th`,{children:t(`common.status`)}),(0,U.jsx)(`th`,{children:t(`messages.body`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[_.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:qe(n.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,U.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.ViewsCount}),(0,U.jsx)(`td`,{children:n.Deleted?(0,U.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,U.jsx)(J,{tone:`warn`,children:t(`messages.pinned`)}):(0,U.jsx)(J,{children:t(`common.survived`)})}),(0,U.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,U.jsx)(rt,{colSpan:9})]})]})})]})}function Tt({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=W(),[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,U.jsx)($e,{children:o});if(!i)return(0,U.jsx)(it,{label:r(`common.loading`)});let l=i.Message;return(0,U.jsx)(Ye,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,U.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:qe(l.Date)})})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,U.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,U.jsx)(J,{children:r(`common.survived`)}),(0,U.jsxs)(J,{children:[`pts `,l.PTS]}),(0,U.jsx)(J,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,U.jsx)(Y,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,U.jsx)(Y,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,U.jsx)(Y,{label:r(`common.time`),value:qe(l.Date)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,U.jsx)(at,{value:i.MessageJSON})]}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,U.jsx)(at,{value:i.DialogJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,U.jsx)(at,{value:i.PrivateJSON})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.count`)}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:qe(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,U.jsx)(rt,{colSpan:4})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:r(`account.userID`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.status`)}),(0,U.jsx)(`th`,{children:r(`messages.attempts`)}),(0,U.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{children:e.TargetUserID}),(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.EventType}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.Attempts}),(0,U.jsx)(`td`,{children:K(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,U.jsx)(rt,{colSpan:7})]})]})})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,U.jsx)(ct,{label:r(`messages.deleteThis`),icon:(0,U.jsx)(ye,{size:15}),path:`/api/actions/delete-messages`,payload:()=>({owner_user_id:l.OwnerUserID,peer_id:l.PeerID,ids:[l.BoxID],revoke:!0}),onDone:c})]})})})}function Et({navigate:e}){let{t}=W(),[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,U.jsxs)(Ye,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,U.jsx)($e,{children:D}),(0,U.jsxs)(Xe,{children:[(0,U.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,U.jsx)(St,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,U.jsx)(St,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,U.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,U.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(pe,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,U.jsx)(tt,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,U.jsx)(tt,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,U.jsx)(tt,{label:t(`messages.ownerPeer`),value:n&&i?`${Ge(n)} / ${Ge(i)}`:`-`})]}),(0,U.jsxs)(`div`,{className:`operation-row`,children:[(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(ye,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,U.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,U.jsx)(ct,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:Je(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(ae,{size:15}),` `,t(`messages.clearHistory`)]}),(0,U.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,U.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,U.jsx)(ct,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:q(y),max_batches:q(C),just_clear:_,revoke:m})})]})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`common.messageId`)}),(0,U.jsx)(`th`,{children:t(`common.time`)}),(0,U.jsx)(`th`,{children:t(`common.sender`)}),(0,U.jsx)(`th`,{children:t(`messages.direction`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.status`)}),(0,U.jsx)(`th`,{children:t(`messages.body`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,U.jsx)(`td`,{children:qe(n.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,U.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.Deleted?(0,U.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):(0,U.jsx)(J,{children:t(`common.survived`)})}),(0,U.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,U.jsx)(rt,{colSpan:8})]})]})})]})}var Dt=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 B(e){"@babel/helpers - typeof";return B=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},B(e)}var de=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 de.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}(),U=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Se=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=U.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Ce=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return Se(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=Ce.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=je(c.s),M=je(b),N=(e-y)/(v-y);Ae(r,ke(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 Ae(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 je(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 Me(){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!==Ee&&(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 Ne(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,De(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 Pe(){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]=Ve.newElement()),a[r][0]=e,a[r][1]=t},He.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)},He.prototype.reverse=function(){var e=new He;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 qe(e){"@babel/helpers - typeof";return qe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},qe(e)}var q={},Je=`__[STANDALONE]__`,Ye=`__[ANIMATIONDATA]__`,Xe=``;function Ze(e){s(e)}function Qe(){Je===!0?H.searchAnimations(Ye,Je,Xe):H.searchAnimations()}function $e(e){re(e)}function J(e){ue(e)}function et(e){return Je===!0&&(e.animationData=JSON.parse(Ye)),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 Y(){return typeof navigator<`u`}function nt(e,t){e===`expressions`&&ae(t)}function rt(e){switch(e){case`propertyFactory`:return G;case`shapePropertyFactory`:return Ke;case`matrix`:return K;default:return null}}q.play=H.play,q.pause=H.pause,q.setLocationHref=Ze,q.togglePause=H.togglePause,q.setSpeed=H.setSpeed,q.setDirection=H.setDirection,q.stop=H.stop,q.searchAnimations=Qe,q.registerAnimation=H.registerAnimation,q.loadAnimation=et,q.setSubframeRendering=$e,q.resize=H.resize,q.goToAndStop=H.goToAndStop,q.destroy=H.destroy,q.setQuality=tt,q.inBrowser=Y,q.installPlugin=nt,q.freeze=H.freeze,q.unfreeze=H.unfreeze,q.setVolume=H.setVolume,q.mute=H.mute,q.unmute=H.unmute,q.getRegisteredAnimations=H.getRegisteredAnimations,q.useWebWorker=a,q.setIDPrefix=J,q.__getFactory=rt,q.version=`5.13.0`;function it(){document.readyState===`complete`&&(clearInterval(lt),Qe())}function at(e){for(var t=ot.split(`&`),n=0;n=1?a.push({s:e-1,e:t-1}):(a.push({s:e,e:1}),a.push({s:0,e:t-1}));var o=[],s,c=a.length,l;for(s=0;sr+n)){var u=l.s*i<=r?0:(l.s*i-r)/n,d=l.e*i>=r+n?1:(l.e*i-r)/n;o.push([u,d])}return o.length||o.push([0,0]),o},ft.prototype.releasePathsData=function(e){var t,n=e.length;for(t=0;t1?1+r:this.s.v<0?0+r:this.s.v+r,n=this.e.v>1?1+r:this.e.v<0?0+r:this.e.v+r,t>n){var i=t;t=n,n=i}t=Math.round(t*1e4)*1e-4,n=Math.round(n*1e4)*1e-4,this.sValue=t,this.eValue=n}else t=this.sValue,n=this.eValue;var a,o,s=this.shapes.length,c,l,u,d,f,p=0;if(n===t)for(o=0;o=0;--o)if(h=this.shapes[o],h.shape._mdf){for(g=h.localShapeCollection,g.releaseShapes(),this.m===2&&s>1?(b=this.calculateShapeEdges(t,n,h.totalShapeLength,y,p),y+=h.totalShapeLength):b=[[_,v]],l=b.length,c=0;c=1?m.push({s:h.totalShapeLength*(_-1),e:h.totalShapeLength*(v-1)}):(m.push({s:h.totalShapeLength*_,e:h.totalShapeLength}),m.push({s:0,e:h.totalShapeLength*(v-1)}));var x=this.addShapes(h,m[0]);if(m[0].s!==m[0].e){if(m.length>1)if(h.shape.paths.shapes[h.shape.paths._length-1].c){var S=x.pop();this.addPaths(x,g),x=this.addShapes(h,m[1],S)}else this.addPaths(x,g),x=this.addShapes(h,m[1]);this.addPaths(x,g)}}h.shape.paths=g}}else if(this._mdf)for(o=0;ot.e){n.c=!1;break}else t.s<=l&&t.e>=l+u.addedLength?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[s],i[a].v[s],n,d,g),g=!1):(p=W.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=W.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=G.getProp(e,t.p.x,0,0,this),this.py=G.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=G.getProp(e,t.p.z,0,0,this))):this.p=G.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=G.getProp(e,t.rx,0,D,this),this.ry=G.getProp(e,t.ry,0,D,this),this.rz=G.getProp(e,t.rz,0,D,this),t.or.k[0].ti){var r,i=t.or.k.length;for(r=0;r0;)--n,this._elements.unshift(t[n]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},ht.prototype.resetElements=function(e){var t,n=e.length;for(t=0;t0?Math.floor(f):Math.ceil(f),h=this.pMatrix.props,g=this.rMatrix.props,_=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v=0;if(f>0){for(;vm;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),--v;p&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-p,!0),v-=p)}r=this.data.m===1?0:this._currentCopies-1,i=this.data.m===1?1:-1,a=this._currentCopies;for(var y,b;a;){if(t=this.elemsData[r].it,n=t[t.length-1].transform.mProps.v.props,b=n.length,t[t.length-1].transform.mProps._mdf=!0,t[t.length-1].transform.op._mdf=!0,t[t.length-1].transform.op.v=this._currentCopies===1?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),v!==0){for((r!==0&&i===1||r!==this._currentCopies-1&&i===-1)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(_[0],_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15]),this.matrix.transform(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8],h[9],h[10],h[11],h[12],h[13],h[14],h[15]),y=0;y0&&r<1?[t]:[]:[t-r,t+r].filter(function(e){return e>0&&e<1})},wt.prototype.split=function(e){if(e<=0)return[Ct(this.points[0]),this];if(e>=1)return[this,Ct(this.points[this.points.length-1])];var t=bt(this.points[0],this.points[1],e),n=bt(this.points[1],this.points[2],e),r=bt(this.points[2],this.points[3],e),i=bt(t,n,e),a=bt(n,r,e),o=bt(i,a,e);return[new wt(this.points[0],t,i,o,!0),new wt(o,a,r,this.points[3],!0)]};function Tt(e,t){var n=e.points[0][t],r=e.points[e.points.length-1][t];if(n>r){var i=r;r=n,n=i}for(var a=xt(3*e.a[t],2*e.b[t],e.c[t]),o=0;o0&&a[o]<1){var s=e.point(a[o])[t];sr&&(r=s)}return{min:n,max:r}}wt.prototype.bounds=function(){return{x:Tt(this,0),y:Tt(this,1)}},wt.prototype.boundingBox=function(){var e=this.bounds();return{left:e.x.min,right:e.x.max,top:e.y.min,bottom:e.y.max,width:e.x.max-e.x.min,height:e.y.max-e.y.min,cx:(e.x.max+e.x.min)/2,cy:(e.y.max+e.y.min)/2}};function Et(e,t,n){var r=e.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:e,t:(t+n)/2,t1:t,t2:n}}function Dt(e){var t=e.bez.split(.5);return[Et(t[0],e.t1,e.t),Et(t[1],e.t,e.t2)]}function Ot(e,t){return Math.abs(e.cx-t.cx)*2=a||e.width<=r&&e.height<=r&&t.width<=r&&t.height<=r){i.push([e.t,t.t]);return}var o=Dt(e),s=Dt(t);kt(o[0],s[0],n+1,r,i,a),kt(o[0],s[1],n+1,r,i,a),kt(o[1],s[0],n+1,r,i,a),kt(o[1],s[1],n+1,r,i,a)}}wt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return kt(Et(this,0,1),Et(e,0,1),0,t,r,n),r},wt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new wt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},wt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new wt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function At(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function jt(e,t,n,r){var i=[e[0],e[1],1],a=[t[0],t[1],1],o=[n[0],n[1],1],s=[r[0],r[1],1],c=At(At(i,a),At(o,s));return vt(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function X(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Mt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Nt(e,t){return _t(e[0],t[0])&&_t(e[1],t[1])}function Pt(){}u([dt],Pt),Pt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=G.getProp(e,t.s,0,null,this),this.frequency=G.getProp(e,t.r,0,null,this),this.pointsType=G.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function Ft(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function It(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function Lt(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=It(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Rt(e,t,n,r,i,a,o){var s=Lt(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;Ft(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function zt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ut(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Gt(e){for(var t,n=1;n1&&(t=Wt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Kt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Vt(e,t)];if(n.length===1||_t(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Vt(r,t),Vt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Vt(r,t),Vt(o,t),Vt(i,t)]}function qt(){}u([dt],qt),qt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=G.getProp(e,t.a,0,null,this),this.miterLimit=G.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},qt.prototype.processPath=function(e,t,n,r){var i=Ue.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=wt.shapeSegmentInverted(e,o),l.push(Kt(c,t));l=Gt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function Xt(e){this.animationData=e}Xt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Zt(e){return new Xt(e)}function Qt(){}Qt.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},mn.prototype.show=function(){},mn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},mn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},mn.prototype.resume=function(){this._canPlay=!0},mn.prototype.setRate=function(e){this.audio.rate(e)},mn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},mn.prototype.getBaseElement=function(){return null},mn.prototype.destroy=function(){},mn.prototype.sourceRectAtTime=function(){},mn.prototype.initExpressions=function(){};function hn(){}hn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},hn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},hn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},hn.prototype.createAudio=function(e){return new mn(e,this.globalData,this)},hn.prototype.createFootage=function(e){return new pn(e,this.globalData,this)},hn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}vn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},vn.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},vn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var yn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=z(`filter`);return n.setAttribute(`id`,e),t!==!0&&(n.setAttribute(`filterUnits`,`objectBoundingBox`),n.setAttribute(`x`,`0%`),n.setAttribute(`y`,`0%`),n.setAttribute(`width`,`100%`),n.setAttribute(`height`,`100%`)),n}function n(){var e=z(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),bn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),xn={},Sn=`filter_result_`;function Cn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=I(),a=yn.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Rn.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function Gn(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([dn,_n,wn,kn,Tn,fn,En],Gn),Gn.prototype.initSecondaryElement=function(){},Gn.prototype.identityMatrix=new K,Gn.prototype.buildExpressionInterface=function(){},Gn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Gn.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},Gn.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},qn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},qn.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},qn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Yt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Yt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Yt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Yt.isVariationSelector(i)&&(o=!0)):Yt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},qn.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=Jt(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var F,I,L=[];for(g=0;g<_;g+=1){for(N=M[g],N.a.sc&&(e.strokeColorAnim=!0),N.a.sw&&(e.strokeWidthAnim=!0),(N.a.fc||N.a.fh||N.a.fs||N.a.fb)&&(e.fillColorAnim=!0),I=0,F=N.s.b,i=0;i0?i=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?o=1-this.xe.v/100:s=1+this.xe.v/100;var c=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([Be],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=G.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=G.getProp;for(e=0;e=m+U||!x?(T=(m+U-g)/h.partialLength,ie=b.point[0]+(h.point[0]-b.point[0])*T,ae=b.point[1]+(h.point[1]-b.point[1])*T,a.translate(-n[0]*f[u].an*.005,-(n[1]*A)*.01),_=!1):x&&(g+=h.partialLength,v+=1,v>=x.length&&(v=0,y+=1,S[y]?x=S[y].points:D.v.c?(v=0,y=0,x=S[y].points):(g-=h.partialLength,x=null)),x&&(b=h,h=x[v],C=h.partialLength));re=f[u].an/2-f[u].add,a.translate(-re,0,0)}else re=f[u].an/2-f[u].add,a.translate(-re,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:z(s?`g`:`text`),b<=e){if(c.setAttribute(`stroke-linecap`,`butt`),c.setAttribute(`stroke-linejoin`,`round`),c.setAttribute(`stroke-miterlimit`,`4`),this.textSpans[e].span=c,s){var S=z(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new ir(x.data,this.globalData,this);else{var w=Qn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Gn(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},rr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&lr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new dr(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=$t(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new fr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(gn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=lr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=mr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new 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=Ke.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),ut.registerModifier(`tm`,ft),ut.registerModifier(`pb`,pt),ut.registerModifier(`rp`,ht),ut.registerModifier(`rd`,gt),ut.registerModifier(`zz`,Pt),ut.registerModifier(`op`,qt),q}))}))(),1),Ot=0,kt=e=>`${e}-${++Ot}`,At=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function jt(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:kt(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function Mt(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=At[e.length%At.length];return{key:kt(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var Nt=e=>jt([X(e,0),X(e,1)]),Pt=()=>{let e=Mt([]);return jt([e,Mt([e])])};function Ft({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=Dt.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,U.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function It({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,U.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,U.jsx)(Ft,{data:n,compact:!0}):(0,U.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,U.jsx)(A,{className:`spin`,size:15})})}async function Lt(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 Rt=e=>Number.parseInt(e.replace(`#`,``),16),zt=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function Bt({gift:e,onClose:t,onPublished:n}){let{t:r}=W(),[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)(()=>Nt(`model`)),[D,O]=(0,g.useState)(()=>Nt(`pattern`)),[M,N]=(0,g.useState)(Pt);(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 Lt(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function ee(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||M.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=M.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:M.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:Rt(e.center),edge_color:Rt(e.edge),pattern_color:Rt(e.pattern),text_color:Rt(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}async function R(){l(!0),d(``),p(null);try{p(await x.publishGiftCollectibles(e.GiftID,ee(!1)))}catch(e){d(b(e))}finally{l(!1)}}async function ne(){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,U.jsxs)(`section`,{className:`collectible-section`,children:[(0,U.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,U.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,U.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,U.jsxs)(J,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(jt([...t,X(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,U.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,U.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,U.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,U.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`common.name`)}),(0,U.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,U.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,U.jsxs)(`label`,{className:`collectible-file`,children:[(0,U.jsx)(`span`,{children:r(`gifts.animation`)}),(0,U.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,U.jsxs)(`em`,{children:[(0,U.jsx)(te,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,U.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,U.jsx)(Ft,{data:i.animation,compact:!0}):(0,U.jsx)(j,{size:16})}),(0,U.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(jt(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,U.jsx)(ye,{size:14})}),i.fileError&&(0,U.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,st.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,U.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,U.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,U.jsx)(H,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,U.jsxs)(`div`,{className:`collectible-loading`,children:[(0,U.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,U.jsxs)(`section`,{className:`collectible-active`,children:[(0,U.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(re,{size:18}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,U.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,U.jsx)(J,{tone:`good`,children:r(`collectibles.published`)})]}),(0,U.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,U.jsxs)(`article`,{children:[(0,U.jsx)(It,{giftID:e.GiftID,attribute:t}),(0,U.jsxs)(`div`,{children:[(0,U.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,U.jsx)(J,{children:`crafted`})]}),(0,U.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,zt(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,U.jsxs)(`article`,{children:[(0,U.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,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e.name}),(0,U.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,zt(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,U.jsxs)(`div`,{className:`collectible-empty`,children:[(0,U.jsx)(re,{size:22}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,U.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,U.jsxs)(`section`,{className:`collectible-definition`,children:[(0,U.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,U.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,U.jsx)(`span`,{children:`TGS`}),(0,U.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,U.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,U.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.reason`)}),(0,U.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,U.jsxs)(`section`,{className:`collectible-section`,children:[(0,U.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,U.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,U.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,U.jsxs)(J,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N(jt([...M,Mt(M)])),F()},children:[(0,U.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,U.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,U.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,U.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`common.name`)}),(0,U.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,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,U.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,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,U.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,U.jsxs)(`label`,{className:`collectible-color`,children:[(0,U.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,U.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,U.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,U.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length<=2,onClick:()=>{N(jt(M.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,U.jsx)(ye,{size:14})})]},e.key))})]})]}),u&&(0,U.jsx)($e,{children:u}),f&&(0,U.jsxs)(`div`,{className:`gift-validation`,children:[(0,U.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,U.jsx)(k,{size:17}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,U.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,U.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:R,disabled:c,children:[c?(0,U.jsx)(A,{className:`spin`,size:15}):(0,U.jsx)(he,{size:15}),r(`gifts.validate`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:ne,disabled:c||!f,children:[(0,U.jsx)(be,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function Vt(e){return e.model_count+e.pattern_count+e.backdrop_count}function Ht(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Ut({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=Dt.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,U.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,U.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,U.jsx)(`span`,{children:s})}),(0,U.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,U.jsx)(z,{size:14}):(0,U.jsx)(B,{size:14})})]})}function Wt({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=Dt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,U.jsx)(`div`,{className:`gift-animation-shell`,children:(0,U.jsx)(`div`,{className:`gift-animation`,ref:t})})}function Gt(){let{t:e}=W(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,R]=(0,g.useState)(`50`),[ne,ie]=(0,g.useState)(`50`),[ae,oe]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[z,B]=(0,g.useState)(null),[me,ge]=(0,g.useState)(!1),[_e,ve]=(0,g.useState)(``),[ye,V]=(0,g.useState)(``);async function xe(){ve(``);try{n((await x.gifts()).Gifts??[])}catch(e){ve(b(e))}}(0,g.useEffect)(()=>{xe()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>V(b(e)))},[a,d,p.length]);let Se=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),Ce=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),we=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),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 Ee(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:ne,enabled:se,sort_order:Number(ae)})),r.set(`file`,l,l.name),r}function De(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:ne,enabled:se,sort_order:Number(ae),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function Oe(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),R(String(t.stars)),ie(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),B(null)}async function ke(){ge(!0),V(``),B(null);try{B(d===`official`?await x.importOfficialGift(De(!1)):await x.importGift(Ee(!1)))}catch(e){V(b(e))}finally{ge(!1)}}async function Ae(){if(z){ge(!0),V(``);try{d===`official`?await x.importOfficialGift(De(!0,z.command_id)):await x.importGift(Ee(!0,z.command_id)),B(null),u(null),F(`0`),L(``),C(``),await xe(),o(!1)}catch(e){V(b(e))}finally{ge(!1)}}}function je(){F(`0`),L(``),R(`50`),ie(`50`),oe(`0`),ce(!0),ue(``),u(null),B(null),V(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Me(e){F(e.GiftID),L(e.Title),R(String(e.Stars)),ie(String(e.ConvertStars)),oe(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),B(null),V(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,U.jsxs)(Ye,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>xe(),disabled:me,children:[(0,U.jsx)(fe,{size:15}),` `,e(`common.refresh`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,children:[(0,U.jsx)(de,{size:15}),` `,e(`gifts.add`)]})]}),children:[_e&&(0,U.jsx)($e,{children:_e}),(0,U.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,U.jsx)(tt,{label:e(`gifts.total`),value:String(t.length)}),(0,U.jsx)(tt,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,U.jsx)(tt,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,U.jsx)(tt,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`div`,{className:`toolbar`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,U.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Te.length,total:t.length})})]})}),(0,U.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:e(`gifts.animation`)}),(0,U.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,U.jsx)(`th`,{children:e(`gifts.title`)}),(0,U.jsx)(`th`,{children:e(`gifts.price`)}),(0,U.jsx)(`th`,{children:e(`gifts.source`)}),(0,U.jsx)(`th`,{children:e(`gifts.received`)}),(0,U.jsx)(`th`,{children:e(`common.status`)}),(0,U.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,U.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,U.jsxs)(`tbody`,{children:[Te.map(t=>(0,U.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,U.jsx)(`td`,{children:(0,U.jsx)(Ut,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,U.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,U.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,U.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(J,{children:t.SourceFormat}),(0,U.jsx)(`span`,{className:`gift-source-size`,children:Ht(t.AnimationSize)})]}),(0,U.jsx)(`td`,{children:t.ReceivedCount}),(0,U.jsx)(`td`,{children:(0,U.jsx)(J,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,U.jsx)(`td`,{children:K(t.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,U.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,U.jsx)(re,{size:13}),e(`collectibles.manage`)]}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Me(t),children:e(`gifts.replace`)}),(0,U.jsx)(ct,{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 xe()})]})})]},t.GiftID)),Te.length===0&&(0,U.jsx)(rt,{colSpan:9})]})]})}),a&&(0,st.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,U.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:me,"aria-label":e(`action.close`),children:(0,U.jsx)(H,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,U.jsxs)(`div`,{className:`command-steps`,children:[(0,U.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,U.jsx)(`span`,{children:`1`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${z?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`2`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${z?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`3`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,U.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,U.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),B(null)},children:e(`gifts.officialSource`)}),(0,U.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),B(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,U.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,U.jsxs)(`div`,{className:`gift-import-note`,children:[(0,U.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,U.jsx)(`span`,{children:p.length}),(0,U.jsx)(`span`,{children:`SHA-256`})]})]}),(0,U.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,U.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:we.length,total:p.length})})]}),(0,U.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,U.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,U.jsx)(`span`,{children:Ce[t]})]},t))}),(0,U.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[we.map(t=>{let n=t.source_gift_id===S;return(0,U.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>Oe(t),children:[(0,U.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,U.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,U.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,U.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,U.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,U.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:Vt(t)})})]}),(0,U.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,U.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,U.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),we.length===0&&(0,U.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),Se&&(0,U.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,U.jsx)(Wt,{sourceGiftID:Se.source_gift_id}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:Se.title||e(`gifts.officialUnnamed`,{id:Se.source_gift_id})}),(0,U.jsx)(`span`,{className:`mono`,children:Se.source_gift_id}),(0,U.jsxs)(`small`,{children:[Se.model_count,` `,e(`collectibles.models`),` · `,Se.pattern_count,` `,e(`collectibles.patterns`),` · `,Se.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,U.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,U.jsx)(`span`,{className:Se.can_upgrade?`yes`:`no`,children:Se.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,U.jsx)(`span`,{className:Se.can_craft?`craft`:`no`,children:Se.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),Se?.can_upgrade&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),B(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,U.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,U.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),B(null)}})]})]})]})]}):(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`div`,{className:`gift-import-note`,children:[(0,U.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,U.jsx)(`span`,{children:`TGS`}),(0,U.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,U.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),B(null)}}),(0,U.jsx)(`span`,{className:`gift-file-icon`,children:(0,U.jsx)(te,{size:22})}),(0,U.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,U.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,U.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,U.jsx)(`small`,{children:l?Ht(l.size):e(`gifts.fileHint`)})]}),(0,U.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,U.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.title`)}),(0,U.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.stars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{R(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:ne,onChange:e=>{ie(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,U.jsx)(`input`,{type:`number`,value:ae,onChange:e=>{oe(e.target.value),B(null)}})]})]}),(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:e(`gifts.reason`)}),(0,U.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),B(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),ye&&(0,U.jsx)($e,{children:ye}),z&&(0,U.jsxs)(`div`,{className:`gift-validation`,children:[(0,U.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,U.jsx)(k,{size:17}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,U.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,U.jsx)(`pre`,{children:JSON.stringify(z.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:me,children:e(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:ke,disabled:me,children:[me?(0,U.jsx)(A,{className:`spin`,size:15}):(0,U.jsx)(he,{size:15}),e(`gifts.validate`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Ae,disabled:me||!z,children:[(0,U.jsx)(be,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,U.jsx)(Bt,{gift:s,onClose:()=>c(null),onPublished:()=>void xe()})]})}function Kt({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1],i=e.path.match(/^\/bots\/(\d+)$/)?.[1];return n?(0,U.jsx)(ut,{id:Number(n),navigate:t}):r?(0,U.jsx)(ht,{id:Number(r),navigate:t}):i?(0,U.jsx)(_t,{id:Number(i),navigate:t}):e.path===`/accounts`?(0,U.jsx)(mt,{navigate:t}):e.path===`/channels`?(0,U.jsx)(gt,{navigate:t}):e.path===`/bots`?(0,U.jsx)(vt,{navigate:t}):e.path===`/gifts`?(0,U.jsx)(Gt,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,U.jsx)(Tt,{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,U.jsx)(xt,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,U.jsx)(wt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,U.jsx)(Et,{navigate:t}):(0,U.jsx)(yt,{navigate:t})}function qt(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>Ae());(0,g.useEffect)(()=>{let e=()=>r(Ae());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(Ae())};return e===void 0?(0,U.jsx)(Be,{}):e===null?(0,U.jsx)(ot,{onLogin:t}):(0,U.jsx)(Ve,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,U.jsx)(Kt,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,U.jsx)(g.StrictMode,{children:(0,U.jsx)(Ie,{children:(0,U.jsx)(Te,{children:(0,U.jsx)(qt,{})})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css b/cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css new file mode 100644 index 00000000..a538e8da --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css @@ -0,0 +1 @@ +:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#eef1f5;--bg-accent:#e7ecf1;--panel:#fff;--panel-subtle:#f5f8fb;--panel-strong:#eef2f6;--surface-soft:#f2f7f6;--overlay:#18222f6b;--topbar-bg:#ffffffdb;--line:#e5eaf0;--line-strong:#d3dce4;--heading:#253040;--text:#333f4d;--text-soft:#45525f;--muted:#6d7885;--muted-2:#9aa4b1;--brand:#1f7d6f;--brand-strong:#196155;--brand-2:#3a6cae;--brand-tint:#e8f4f0;--brand-tint-border:#c8e2db;--brand-tint-text:#235d53;--good:#1f8a57;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a86a12;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#c0392b;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#1c2530;--sidebar-soft:#26313d;--sidebar-line:#313c4a;--sidebar-row:#232d38;--sidebar-text:#dbe3ec;--sidebar-muted:#8b98a8;--sidebar-faint:#7c8a9a;--sidebar-heading:#fff;--focus:#1f7d6f29;--shadow:0 12px 34px #1827381a;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #1f7d6f38;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#37a596;--brand-strong:#45b6a6;--brand-2:#6fa8e6;--brand-tint:#14322d;--brand-tint-border:#245349;--brand-tint-text:#7fd3c4;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#37a5963d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #37a59642}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow:var(--shadow-brand)}.brand-mark{color:#fff;background:var(--brand);border-radius:var(--radius-sm);border:1px solid #fff3;place-items:center;width:34px;height:34px;font-weight:800;display:grid}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border:1px solid var(--sidebar-line);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.language-switch{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:2px;display:inline-flex}.language-switch button{min-width:42px;min-height:24px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0 9px;font-weight:800;transition:color .14s,background-color .14s}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input{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{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.bot-create-fields{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.bot-create-fields .duration-field input{width:100%}.bot-create-actions{border-top:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;margin-top:14px;padding-top:14px;display:flex}.bot-create-note{color:var(--muted);font-size:12px;line-height:1.4}@media (width<=760px){.bot-create-fields{grid-template-columns:1fr}.bot-create-actions{flex-direction:column;align-items:stretch}}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:190px}.device-actions-cell{width:190px;min-width:190px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(82px,1fr));gap:6px;min-width:178px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:first-child{width:74px}.gift-table td{vertical-align:middle}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid,.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.js b/cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.js deleted file mode 100644 index 4d0101b7..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-CqgHld2y.js +++ /dev/null @@ -1,9 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.provider`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.iterator;function p(e){return typeof e!=`object`||!e?null:(e=f&&e[f]||e[`@@iterator`],typeof e==`function`?e:null)}var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,g={};function _(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}_.prototype.isReactComponent={},_.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`setState(...): takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},_.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function v(){}v.prototype=_.prototype;function y(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}var b=y.prototype=new v;b.constructor=y,h(b,_.prototype),b.isPureReactComponent=!0;var x=Array.isArray,S=Object.prototype.hasOwnProperty,C={current:null},w={key:!0,ref:!0,__self:!0,__source:!0};function T(e,n,r){var i,a={},o=null,s=null;if(n!=null)for(i in n.ref!==void 0&&(s=n.ref),n.key!==void 0&&(o=``+n.key),n)S.call(n,i)&&!w.hasOwnProperty(i)&&(a[i]=n[i]);var c=arguments.length-2;if(c===1)a.children=r;else if(1{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=typeof setTimeout==`function`?setTimeout:null,_=typeof clearTimeout==`function`?clearTimeout:null,v=typeof setImmediate<`u`?setImmediate:null;typeof navigator<`u`&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function b(e){if(h=!1,y(e),!m)if(n(c)!==null)m=!0,M(x);else{var t=n(l);t!==null&&N(b,t.startTime-e)}}function x(t,i){m=!1,h&&(h=!1,_(w),w=-1),p=!0;var a=f;try{for(y(i),d=n(c);d!==null&&(!(d.expirationTime>i)||t&&!D());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=i);i=e.unstable_now(),typeof s==`function`?d.callback=s:d===n(c)&&r(c),y(i)}else r(c);d=n(c)}if(d!==null)var u=!0;else{var g=n(l);g!==null&&N(b,g.startTime-i),u=!1}return u}finally{d=null,f=a,p=!1}}var S=!1,C=null,w=-1,T=5,E=-1;function D(){return!(e.unstable_now()-Ee||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(_(w),w=-1):h=!0,N(b,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,M(x))),r},e.unstable_shouldYield=D,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u(),n=f();function r(e){for(var t=`https://reactjs.org/docs/error-decoder.html?invariant=`+e,n=1;n`u`||window.document===void 0||window.document.createElement===void 0),l=Object.prototype.hasOwnProperty,d=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function h(e){return l.call(m,e)?!0:l.call(p,e)?!1:d.test(e)?m[e]=!0:(p[e]=!0,!1)}function g(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case`function`:case`symbol`:return!0;case`boolean`:return r?!1:n===null?(e=e.toLowerCase().slice(0,5),e!==`data-`&&e!==`aria-`):!n.acceptsBooleans;default:return!1}}function _(e,t,n,r){if(t==null||g(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return!1===t;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function v(e,t,n,r,i,a,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=o}var y={};`children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style`.split(` `).forEach(function(e){y[e]=new v(e,0,!1,e,null,!1,!1)}),[[`acceptCharset`,`accept-charset`],[`className`,`class`],[`htmlFor`,`for`],[`httpEquiv`,`http-equiv`]].forEach(function(e){var t=e[0];y[t]=new v(t,1,!1,e[1],null,!1,!1)}),[`contentEditable`,`draggable`,`spellCheck`,`value`].forEach(function(e){y[e]=new v(e,2,!1,e.toLowerCase(),null,!1,!1)}),[`autoReverse`,`externalResourcesRequired`,`focusable`,`preserveAlpha`].forEach(function(e){y[e]=new v(e,2,!1,e,null,!1,!1)}),`allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope`.split(` `).forEach(function(e){y[e]=new v(e,3,!1,e.toLowerCase(),null,!1,!1)}),[`checked`,`multiple`,`muted`,`selected`].forEach(function(e){y[e]=new v(e,3,!0,e,null,!1,!1)}),[`capture`,`download`].forEach(function(e){y[e]=new v(e,4,!1,e,null,!1,!1)}),[`cols`,`rows`,`size`,`span`].forEach(function(e){y[e]=new v(e,6,!1,e,null,!1,!1)}),[`rowSpan`,`start`].forEach(function(e){y[e]=new v(e,5,!1,e.toLowerCase(),null,!1,!1)});var b=/[\-:]([a-z])/g;function x(e){return e[1].toUpperCase()}`accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,null,!1,!1)}),`xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/1999/xlink`,!1,!1)}),[`xml:base`,`xml:lang`,`xml:space`].forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/XML/1998/namespace`,!1,!1)}),[`tabIndex`,`crossOrigin`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!1,!1)}),y.xlinkHref=new v(`xlinkHref`,1,!1,`xlink:href`,`http://www.w3.org/1999/xlink`,!0,!1),[`src`,`href`,`action`,`formAction`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!0,!0)});function S(e,t,n,r){var i=y.hasOwnProperty(t)?y[t]:null;(i===null?r||!(2s||i[o]!==a[s]){var c=` -`+i[o].replace(` at new `,` at `);return e.displayName&&c.includes(``)&&(c=c.replace(``,e.displayName)),c}while(1<=o&&0<=s);break}}}finally{re=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?ne(e):``}function ae(e){switch(e.tag){case 5:return ne(e.type);case 16:return ne(`Lazy`);case 13:return ne(`Suspense`);case 19:return ne(`SuspenseList`);case 0:case 2:case 15:return e=ie(e.type,!1),e;case 11:return e=ie(e.type.render,!1),e;case 1:return e=ie(e.type,!0),e;default:return``}}function oe(e){if(e==null)return null;if(typeof e==`function`)return e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case E:return`Fragment`;case T:return`Portal`;case O:return`Profiler`;case D:return`StrictMode`;case M:return`Suspense`;case N:return`SuspenseList`}if(typeof e==`object`)switch(e.$$typeof){case A:return(e.displayName||`Context`)+`.Consumer`;case k:return(e._context.displayName||`Context`)+`.Provider`;case j:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case P:return t=e.displayName||null,t===null?oe(e.type)||`Memo`:t;case F:t=e._payload,e=e._init;try{return oe(e(t))}catch{}}return null}function se(e){var t=e.type;switch(e.tag){case 24:return`Cache`;case 9:return(t.displayName||`Context`)+`.Consumer`;case 10:return(t._context.displayName||`Context`)+`.Provider`;case 18:return`DehydratedFragment`;case 11:return e=t.render,e=e.displayName||e.name||``,t.displayName||(e===``?`ForwardRef`:`ForwardRef(`+e+`)`);case 7:return`Fragment`;case 5:return t;case 4:return`Portal`;case 3:return`Root`;case 6:return`Text`;case 16:return oe(t);case 8:return t===D?`StrictMode`:`Mode`;case 22:return`Offscreen`;case 12:return`Profiler`;case 21:return`Scope`;case 13:return`Suspense`;case 19:return`SuspenseList`;case 25:return`TracingMarker`;case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t==`function`)return t.displayName||t.name||null;if(typeof t==`string`)return t}return null}function ce(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function le(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function ue(e){var t=le(e)?`checked`:`value`,n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=``+e[t];if(!e.hasOwnProperty(t)&&n!==void 0&&typeof n.get==`function`&&typeof n.set==`function`){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(e){r=``+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=``+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function z(e){e._valueTracker||=ue(e)}function de(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=le(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function B(e){if(e||=typeof document<`u`?document:void 0,e===void 0)return null;try{return e.activeElement||e.body}catch{return e.body}}function V(e,t){var n=t.checked;return R({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function fe(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 pe(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function me(e,t){pe(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`)?ge(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ge(e,t.type,ce(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function he(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 ge(e,t,n){(t!==`number`||B(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var _e=Array.isArray;function ve(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Se.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function we(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Te={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},Ee=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(Te).forEach(function(e){Ee.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Te[t]=Te[e]})});function De(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||Te.hasOwnProperty(e)&&Te[e]?(``+t).trim():t+`px`}function Oe(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=De(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var ke=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 Ae(e,t){if(t){if(ke[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 je(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 Me=null;function Ne(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Pe=null,Fe=null,Ie=null;function Le(e){if(e=ji(e)){if(typeof Pe!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Ni(t),Pe(e.stateNode,e.type,t))}}function Re(e){Fe?Ie?Ie.push(e):Ie=[e]:Fe=e}function ze(){if(Fe){var e=Fe,t=Ie;if(Ie=Fe=null,Le(e),t)for(e=0;e>>=0,e===0?32:31-(_t(e)/vt|0)|0}var bt=64,xt=4194304;function St(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ct(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,o=n&268435455;if(o!==0){var s=o&~i;s===0?(a&=o,a!==0&&(r=St(a))):r=St(s)}else o=n&~i,o===0?a!==0&&(r=St(a)):r=St(o);if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&a&4194240))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function kt(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-gt(t),e[t]=n}function At(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Gn),Jn=` `,Yn=!1;function Xn(e,t){switch(e){case`keyup`:return Un.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Zn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Qn=!1;function $n(e,t){switch(e){case`compositionend`:return Zn(t);case`keypress`:return t.which===32?(Yn=!0,Jn):null;case`textInput`:return e=t.data,e===Jn&&Yn?null:e;default:return null}}function er(e,t){if(Qn)return e===`compositionend`||!Wn&&Xn(e,t)?(e=mn(),pn=fn=dn=null,Qn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=xr(n)}}function Cr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Cr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wr(){for(var e=window,t=B();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=B(e.document)}return t}function Tr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}function Er(e){var t=wr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Cr(n.ownerDocument.documentElement,n)){if(r!==null&&Tr(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),`selectionStart`in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=Sr(n,a);var o=Sr(n,r);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus==`function`&&n.focus(),n=0;n=document.documentMode,Or=null,kr=null,Ar=null,jr=!1;function Mr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;jr||Or==null||Or!==B(r)||(r=Or,`selectionStart`in r&&Tr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ar&&br(Ar,r)||(Ar=r,r=ii(kr,`onSelect`),0Fi||(e.current=Pi[Fi],Pi[Fi]=null,Fi--)}function Ri(e,t){Fi++,Pi[Fi]=e.current,e.current=t}var zi={},Bi=Ii(zi),Vi=Ii(!1),Hi=zi;function Ui(e,t){var n=e.type.contextTypes;if(!n)return zi;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Wi(e){return e=e.childContextTypes,e!=null}function Gi(){Li(Vi),Li(Bi)}function Ki(e,t,n){if(Bi.current!==zi)throw Error(r(168));Ri(Bi,t),Ri(Vi,n)}function qi(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,se(e)||`Unknown`,a));return R({},n,i)}function Ji(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||zi,Hi=Bi.current,Ri(Bi,e),Ri(Vi,Vi.current),!0}function Yi(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=qi(e,t,Hi),i.__reactInternalMemoizedMergedChildContext=e,Li(Vi),Li(Bi),Ri(Bi,e)):Li(Vi),Ri(Vi,n)}var Xi=null,Zi=!1,Qi=!1;function $i(e){Xi===null?Xi=[e]:Xi.push(e)}function ea(e){Zi=!0,$i(e)}function ta(){if(!Qi&&Xi!==null){Qi=!0;var e=0,t=X;try{var n=Xi;for(X=1;e>=o,i-=o,la=1<<32-gt(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),_a&&da(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),_a&&da(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return _a&&da(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),_a&&da(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===F&&ja(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=ka(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=ka(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case F:return l=i._init,_(e,r,l(i._payload),o)}if(_e(i))return h(e,r,i,o);if(ee(i))return g(e,r,i,o);Aa(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Na=Ma(!0),Pa=Ma(!1),Fa=Ii(null),Ia=null,La=null,Ra=null;function za(){Ra=La=Ia=null}function Ba(e){var t=Fa.current;Li(Fa),e._currentValue=t}function Va(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Ha(e,t){Ia=e,Ra=La=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Ms=!0),e.firstContext=null)}function Ua(e){var t=e._currentValue;if(Ra!==e)if(e={context:e,memoizedValue:t,next:null},La===null){if(Ia===null)throw Error(r(308));La=e,Ia.dependencies={lanes:0,firstContext:e}}else La=La.next=e;return t}var Wa=null;function Ga(e){Wa===null?Wa=[e]:Wa.push(e)}function Ka(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Ga(t)):(n.next=i.next,i.next=n),t.interleaved=n,qa(e,r)}function qa(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Ja=!1;function Ya(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Za(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Qa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,$&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,qa(e,n)}return i=r.interleaved,i===null?(t.next=t,Ga(r)):(t.next=i.next,i.next=t),r.interleaved=t,qa(e,n)}function $a(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}function eo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function to(e,t,n,r){var i=e.updateQueue;Ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=R({},d,f);break a;case 2:Ja=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function no(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=vo.transition;vo.transition={};try{e(!1),t()}finally{X=n,vo.transition=r}}function as(){return Mo().memoizedState}function os(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},cs(e))ls(t,n);else if(n=Ka(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),us(n,t,r)}}function ss(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(cs(e))ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Z(s,o)){var c=t.interleaved;c===null?(i.next=i,Ga(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ka(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),us(n,t,r))}}function cs(e){var t=e.alternate;return e===bo||t!==null&&t===bo}function ls(e,t){wo=Co=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function us(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}var ds={readContext:Ua,useCallback:Do,useContext:Do,useEffect:Do,useImperativeHandle:Do,useInsertionEffect:Do,useLayoutEffect:Do,useMemo:Do,useReducer:Do,useRef:Do,useState:Do,useDebugValue:Do,useDeferredValue:Do,useTransition:Do,useMutableSource:Do,useSyncExternalStore:Do,useId:Do,unstable_isNewReconciler:!1},fs={readContext:Ua,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:Ua,useEffect:Jo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Ko(4194308,4,Qo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ko(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ko(4,2,e,t)},useMemo:function(e,t){var n=jo();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=jo();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=os.bind(null,bo,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:Uo,useDebugValue:es,useDeferredValue:function(e){return jo().memoizedState=e},useTransition:function(){var e=Uo(!1),t=e[0];return e=is.bind(null,e[1]),jo().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=bo,a=jo();if(_a){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));yo&30||Ro(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,Jo(Bo.bind(null,i,o,e),[e]),i.flags|=2048,Wo(9,zo.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=jo(),t=Vc.identifierPrefix;if(_a){var n=ua,r=la;n=(r&~(1<<32-gt(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=To++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[wi]=t,e[Ti]=i,nc(e,t,!1,!1),t.stateNode=e;a:{switch(c=je(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*ot()-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=ot(),t.sibling=null,n=po.current,Ri(po,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(oc(t),t.subtreeFlags&6&&(t.flags|=8192)):oc(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function cc(e,t){switch(ma(t),t.tag){case 1:return Wi(t.type)&&Gi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return lo(),Li(Vi),Li(Bi),go(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return fo(t),null;case 13:if(Li(po),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ea()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Li(po),null;case 4:return lo(),null;case 10:return Ba(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var lc=!1,uc=!1,dc=typeof WeakSet==`function`?WeakSet:Set,Q=null;function fc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function pc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var mc=!1;function hc(e,t){if(fi=rn,e=wr(),Tr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(pi={focusedElem:e,selectionRange:n},rn=!1,Q=t;Q!==null;)if(t=Q,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Q=e;else for(;Q!==null;){t=Q;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:hs(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,Q=e;break}Q=t.return}return h=mc,mc=!1,h}function gc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&pc(t,n,a)}i=i.next}while(i!==r)}}function _c(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function vc(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function yc(e){var t=e.alternate;t!==null&&(e.alternate=null,yc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[wi],delete t[Ti],delete t[Di],delete t[Oi],delete t[ki])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function bc(e){return e.tag===5||e.tag===3||e.tag===4}function xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||bc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=di));else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}function Cc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Cc(e,t,n),e=e.sibling;e!==null;)Cc(e,t,n),e=e.sibling}var wc=null,Tc=!1;function Ec(e,t,n){for(n=n.child;n!==null;)Dc(e,t,n),n=n.sibling}function Dc(e,t,n){if(mt&&typeof mt.onCommitFiberUnmount==`function`)try{mt.onCommitFiberUnmount(pt,n)}catch{}switch(n.tag){case 5:uc||fc(n,t);case 6:var r=wc,i=Tc;wc=null,Ec(e,t,n),wc=r,Tc=i,wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):wc.removeChild(n.stateNode));break;case 18:wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?bi(e.parentNode,n):e.nodeType===1&&bi(e,n),tn(e)):bi(wc,n.stateNode));break;case 4:r=wc,i=Tc,wc=n.stateNode.containerInfo,Tc=!0,Ec(e,t,n),wc=r,Tc=i;break;case 0:case 11:case 14:case 15:if(!uc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&pc(n,t,o),i=i.next}while(i!==r)}Ec(e,t,n);break;case 1:if(!uc&&(fc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Ec(e,t,n);break;case 21:Ec(e,t,n);break;case 22:n.mode&1?(uc=(r=uc)||n.memoizedState!==null,Ec(e,t,n),uc=r):Ec(e,t,n);break;default:Ec(e,t,n)}}function Oc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new dc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function kc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=ot()-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;lot()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=xt,xt<<=1,!(xt&130023424)&&(xt=4194304)):t=1);var n=fl();e=qa(e,t),e!==null&&(kt(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Vi.current)Ms=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return Ms=!1,tc(e,t,n);Ms=!!(e.flags&131072)}else Ms=!1,_a&&t.flags&1048576&&fa(t,aa,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;$s(e,t),e=t.pendingProps;var a=Ui(t,Bi.current);Ha(t,n),a=ko(null,t,i,e,a,n);var o=Ao();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Wi(i)?(o=!0,Ji(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ya(t),a.updater=_s,t.stateNode=a,a._reactInternals=t,xs(t,i,e,n),t=Vs(null,t,i,!0,o,n)):(t.tag=0,_a&&o&&pa(t),Ns(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch($s(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=hs(i,e),a){case 0:t=zs(null,t,i,e,n);break a;case 1:t=Bs(null,t,i,e,n);break a;case 11:t=Ps(null,t,i,e,n);break a;case 14:t=Fs(null,t,i,hs(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),zs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Bs(e,t,i,a,n);case 3:a:{if(Hs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Xa(e,t),to(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=Ss(Error(r(423)),t),t=Us(e,t,i,n,a);break a}else if(i!==a){a=Ss(Error(r(424)),t),t=Us(e,t,i,n,a);break a}else for(ga=xi(t.stateNode.containerInfo.firstChild),ha=t,_a=!0,va=null,n=Pa(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ea(),i===a){t=ec(e,t,n);break a}Ns(e,t,i,n)}t=t.child}return t;case 5:return uo(t),e===null&&Sa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,mi(i,a)?s=null:o!==null&&mi(i,o)&&(t.flags|=32),Rs(e,t),Ns(e,t,s,n),t.child;case 6:return e===null&&Sa(t),null;case 13:return Ks(e,t,n);case 4:return co(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Na(t,null,i,n):Ns(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Ps(e,t,i,a,n);case 7:return Ns(e,t,t.pendingProps,n),t.child;case 8:return Ns(e,t,t.pendingProps.children,n),t.child;case 12:return Ns(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Ri(Fa,i._currentValue),i._currentValue=s,o!==null)if(Z(o.value,s)){if(o.children===a.children&&!Vi.current){t=ec(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Za(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Va(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Va(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ns(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Ha(t,n),a=Ua(a),i=i(a),t.flags|=1,Ns(e,t,i,n),t.child;case 14:return i=t.type,a=hs(i,t.pendingProps),a=hs(i.type,a),Fs(e,t,i,a,n);case 15:return Is(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),$s(e,t),t.tag=1,Wi(i)?(e=!0,Ji(t)):e=!1,Ha(t,n),ys(t,i,a),xs(t,i,a,n),Vs(null,t,i,!0,e,n);case 19:return Qs(e,t,n);case 22:return Ls(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return nt(e,t)}function Gl(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(e,t,n,r){return new Gl(e,t,n,r)}function ql(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jl(e){if(typeof e==`function`)return+!!ql(e);if(e!=null){if(e=e.$$typeof,e===j)return 11;if(e===P)return 14}return 2}function Yl(e,t){var n=e.alternate;return n===null?(n=Kl(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xl(e,t,n,i,a,o){var s=2;if(i=e,typeof e==`function`)ql(e)&&(s=1);else if(typeof e==`string`)s=5;else a:switch(e){case E:return Zl(n.children,a,o,t);case D:s=8,a|=8;break;case O:return e=Kl(12,n,t,a|2),e.elementType=O,e.lanes=o,e;case M:return e=Kl(13,n,t,a),e.elementType=M,e.lanes=o,e;case N:return e=Kl(19,n,t,a),e.elementType=N,e.lanes=o,e;case I:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case k:s=10;break a;case A:s=9;break a;case j:s=11;break a;case P:s=14;break a;case F:s=16,i=null;break a}throw Error(r(130,e==null?e:typeof e,``))}return t=Kl(s,n,t,a),t.elementType=e,t.type=i,t.lanes=o,t}function Zl(e,t,n,r){return e=Kl(7,e,r,t),e.lanes=n,e}function Ql(e,t,n,r){return e=Kl(22,e,r,t),e.elementType=I,e.lanes=n,e.stateNode={isHidden:!1},e}function $l(e,t,n){return e=Kl(6,e,null,t),e.lanes=n,e}function eu(e,t,n){return t=Kl(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function tu(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ot(0),this.expirationTimes=Ot(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ot(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ya(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}};async function y(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData,r=await fetch(e,{credentials:`same-origin`,headers:n?t.headers:{"Content-Type":`application/json`,...t.headers??{}},...t}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function b(e){return e instanceof Error?e.message:String(e)}var x={session:()=>y(`/api/session`),login:e=>y(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})}),logout:()=>y(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>y(`/api/accounts?${e.toString()}`),account:e=>y(`/api/accounts/${e}`),channels:e=>y(`/api/channels?${e.toString()}`),channel:e=>y(`/api/channels/${e}`),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`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),ue=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`}]]),z=E(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),de=E(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),B=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`}]]),V=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(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),_e=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`}]]),ve=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`}]]),ye=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`}]]),H=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),U=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),W=o(((e,t)=>{t.exports=U()}))(),be=`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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Switch to dark theme`,"theme.switchToLight":`Switch to light theme`,"login.heading":`Operations Admin`,"login.body":`Enter credentials to open the console.`,"login.secret":`Admin password or token`,"login.submit":`Log in`,"login.submitting":`Logging in`,"dashboard.eyebrow":`Runtime Overview`,"dashboard.title":`Console Overview`,"dashboard.readPath":`Read path`,"dashboard.readPathValue":`PG read-only`,"dashboard.writePath":`Write path`,"dashboard.executionPolicy":`Execution policy`,"dashboard.dryRunFirst":`Dry-run first`,"dashboard.accountsText":`Account status, premium, verification, sessions.`,"dashboard.channelsText":`Public entities, member counts, verification state.`,"dashboard.messagesText":`Message boxes, updates, outbox state.`,"dashboard.strip.dryRun":`All dangerous actions start with dry-run`,"dashboard.strip.token":`Browser never stores internal tokens`,"dashboard.strip.pagination":`Lists use cursor pagination`,"dashboard.strip.snapshot":`Detail pages retain raw state snapshots`,"account.pageTitle":`Accounts`,"account.queryResults":`Search results`,"account.recentActive":`Recently active accounts`,"account.currentPage":`Accounts on page`,"account.onlineDevices":`Online device records`,"account.premium":`Premium`,"account.frozen":`Frozen`,"account.searchPlaceholder":`User ID / phone / username`,"account.userID":`User ID`,"account.phone":`Phone`,"account.lastActive":`Last active`,"account.notVerified":`Not verified`,"account.notPremium":`Not premium`,"account.premiumUntil":`Premium expires`,"account.starsBalance":`Stars balance`,"account.startingGrantApplied":`initial grant applied`,"account.startingGrantPending":`initial grant pending`,"account.activeSessions":`Authorized devices`,"account.accountFlags":`Account flags`,"account.restriction":`Restriction`,"account.restricted":`Restricted`,"account.createdAt":`Created`,"account.detailTitle":`Account #{id}`,"account.profile":`Account Profile`,"account.loadingDetail":`Loading account detail`,"account.waitingData":`Waiting for data`,"account.noUsername":`No username`,"account.noPhone":`No phone`,"account.accountFrozen":`Account frozen`,"account.accountActive":`Account active`,"account.authorizationsTitle":`Authorized Devices`,"account.authorizationsCount":`{count} authorizations`,"account.recentAdminOps":`Recent Admin Actions`,"account.recent30Audit":`Last 30 audit rows`,"account.actionDock":`Account Actions`,"account.freezeAccount":`Freeze account`,"account.updateFreeze":`Update freeze`,"account.unfreezeAccount":`Unfreeze account`,"account.freezeSince":`Frozen since`,"account.freezeUntil":`Appeal deadline`,"account.freezeUntilAria":`Freeze appeal deadline`,"account.freezeAppealURL":`Appeal URL`,"account.freezeAppealURLAria":`Freeze appeal URL`,"account.premiumMonths":`Premium duration (months)`,"account.premiumMonthsAria":`Set premium duration in months`,"account.setPremium":`Set premium`,"account.clearPremium":`Clear premium`,"account.starsAmount":`Stars to grant`,"account.starsAmountAria":`Set Stars amount to grant`,"account.grantStars":`Grant Stars`,"account.setVerified":`Set verified`,"account.clearVerified":`Clear verified`,"channel.pageTitle":`Supergroups and Channels`,"channel.recentUpdated":`Recently updated`,"channel.currentPage":`Entities on page`,"channel.megagroups":`Supergroups`,"channel.broadcasts":`Channels`,"channel.verifiedCount":`Verified`,"channel.searchPlaceholder":`Channel ID / username / title`,"channel.channelID":`Channel ID`,"channel.kind":`Kind`,"channel.title":`Title`,"channel.pts":`PTS`,"channel.detailProfile":`Channel Profile`,"channel.loadingDetail":`Loading channel detail`,"channel.creator":`Creator {id}`,"channel.governance":`Moderation`,"channel.governanceValue":`Banned {banned} / Kicked {kicked}`,"channel.flags":`Channel flags`,"channel.rawRow":`Channel Raw Row`,"channel.rawRowText":`Database read-only snapshot`,"channel.actionDock":`Channel Actions`,"channel.setVerified":`Set verified`,"channel.clearVerified":`Clear verified`,"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`,"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.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`中文`,"language.ru":`RU`,"theme.switchToDark":`切换到深色主题`,"theme.switchToLight":`切换到浅色主题`,"login.heading":`运维后台`,"login.body":`输入凭据后进入控制台。`,"login.secret":`管理员密码或 token`,"login.submit":`登录`,"login.submitting":`登录中`,"dashboard.eyebrow":`运行总览`,"dashboard.title":`控制台总览`,"dashboard.readPath":`读路径`,"dashboard.readPathValue":`PG 只读`,"dashboard.writePath":`写路径`,"dashboard.executionPolicy":`执行策略`,"dashboard.dryRunFirst":`先预演`,"dashboard.accountsText":`账号状态、会员、认证、会话。`,"dashboard.channelsText":`公开实体、成员计数、认证状态。`,"dashboard.messagesText":`消息盒、update、outbox 状态。`,"dashboard.strip.dryRun":`所有危险操作先预演`,"dashboard.strip.token":`浏览器不持有内部 token`,"dashboard.strip.pagination":`列表使用游标分页`,"dashboard.strip.snapshot":`详情页保留原始状态快照`,"account.pageTitle":`账号`,"account.queryResults":`查询结果`,"account.recentActive":`最近活跃账号`,"account.currentPage":`当前页账号`,"account.onlineDevices":`在线设备记录`,"account.premium":`会员`,"account.frozen":`冻结`,"account.searchPlaceholder":`用户 ID / 手机号 / 用户名`,"account.userID":`用户 ID`,"account.phone":`手机号`,"account.lastActive":`最近活跃`,"account.notVerified":`未认证`,"account.notPremium":`非会员`,"account.premiumUntil":`会员到期`,"account.starsBalance":`Stars 余额`,"account.startingGrantApplied":`初始赠送已发放`,"account.startingGrantPending":`初始赠送未触发`,"account.activeSessions":`授权设备`,"account.accountFlags":`账号标记`,"account.restriction":`限制状态`,"account.restricted":`已限制`,"account.createdAt":`创建时间`,"account.detailTitle":`账号 #{id}`,"account.profile":`账号档案`,"account.loadingDetail":`加载账号详情`,"account.waitingData":`等待数据`,"account.noUsername":`无用户名`,"account.noPhone":`无手机号`,"account.accountFrozen":`账号已冻结`,"account.accountActive":`账号正常`,"account.authorizationsTitle":`授权设备`,"account.authorizationsCount":`共 {count} 个授权`,"account.recentAdminOps":`最近后台操作`,"account.recent30Audit":`最近 30 条审计`,"account.actionDock":`账号操作`,"account.freezeAccount":`冻结账号`,"account.updateFreeze":`更新冻结信息`,"account.unfreezeAccount":`解冻账号`,"account.freezeSince":`冻结开始时间`,"account.freezeUntil":`申诉截止时间`,"account.freezeUntilAria":`账号冻结申诉截止时间`,"account.freezeAppealURL":`申诉链接`,"account.freezeAppealURLAria":`账号冻结申诉链接`,"account.premiumMonths":`会员时长(月)`,"account.premiumMonthsAria":`设置会员时长,单位月`,"account.setPremium":`设置会员`,"account.clearPremium":`取消会员`,"account.starsAmount":`赠送 Stars 数量`,"account.starsAmountAria":`设置要赠送的 Stars 数量`,"account.grantStars":`赠送 Stars`,"account.setVerified":`设置认证`,"account.clearVerified":`取消认证`,"channel.pageTitle":`超级群与频道`,"channel.recentUpdated":`最近更新`,"channel.currentPage":`当前页实体`,"channel.megagroups":`超级群`,"channel.broadcasts":`频道`,"channel.verifiedCount":`已认证`,"channel.searchPlaceholder":`频道 ID / 用户名 / 标题`,"channel.channelID":`频道 ID`,"channel.kind":`类型`,"channel.title":`标题`,"channel.pts":`PTS`,"channel.detailProfile":`频道档案`,"channel.loadingDetail":`加载频道详情`,"channel.creator":`创建者 {id}`,"channel.governance":`治理状态`,"channel.governanceValue":`封禁 {banned} / 踢出 {kicked}`,"channel.flags":`频道标记`,"channel.rawRow":`频道原始行`,"channel.rawRowText":`数据库只读快照`,"channel.actionDock":`频道操作`,"channel.setVerified":`设置认证`,"channel.clearVerified":`取消认证`,"channel.kind.broadcast":`频道`,"channel.kind.forum":`超级群/论坛`,"channel.kind.megagroup":`超级群`,"channel.kind.generic":`频道/群`,"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.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка…`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтверждён`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Тёмная тема`,"theme.switchToLight":`Светлая тема`,"login.heading":`Панель администратора`,"login.body":`Введите учётные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход…`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, Premium, подтверждение, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, число участников, статус подтверждения.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтверждён`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звёзд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество Звёзд`,"account.starsAmountAria":`Указать количество начисляемых Звёзд`,"account.grantStars":`Начислить Звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновлённые`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждённые`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звёздных подарков`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звёздного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звёздах`,"gifts.convertStars":`Звёзд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звёздные подарки ещё не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звёздах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Отозвать все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтверждённые`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Повторить тестовый запуск`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},Se=(0,g.createContext)(null);function Ce({children:e}){let[t,n]=(0,g.useState)(()=>De());(0,g.useEffect)(()=>{try{localStorage.setItem(be,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Ee(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Ee(t,e,n)}),[t]);return(0,W.jsx)(Se.Provider,{value:r,children:e})}function we(){let e=(0,g.useContext)(Se);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function Te(){let{lang:e,setLang:t,t:n}=we();return(0,W.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,W.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Ee(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 De(){try{let e=Oe(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=Oe(localStorage.getItem(be));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=Oe(t);if(e)return e}return`en`}function Oe(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:t===`ru`||t.startsWith(`ru-`)?`ru`:null}function ke(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Ae(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 je(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`)}var Me=`telesrv.admin.theme`,Ne=(0,g.createContext)(null);function Pe(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Fe({children:e}){let[t,n]=(0,g.useState)(()=>Re());(0,g.useEffect)(()=>{Pe(t);try{localStorage.setItem(Me,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(Me)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,W.jsx)(Ne.Provider,{value:a,children:e})}function Ie(){let e=(0,g.useContext)(Ne);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function Le(){let{theme:e,toggleTheme:t}=Ie(),{t:n}=we(),r=n(e===`light`?`theme.switchToDark`:`theme.switchToLight`);return(0,W.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":r,title:r,children:e===`dark`?(0,W.jsx)(ge,{size:16}):(0,W.jsx)(le,{size:16})})}function Re(){try{let e=localStorage.getItem(Me);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}function ze({href:e,navigate:t,className:n,children:r}){return(0,W.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function G(){let{t:e}=we();return(0,W.jsxs)(`div`,{className:`boot-screen`,children:[(0,W.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`telesrv`}),(0,W.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,W.jsx)(`div`,{className:`loader-bar`})]})}function Be({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=we(),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,W.jsxs)(`div`,{className:`shell`,children:[(0,W.jsxs)(`aside`,{className:`sidebar`,children:[(0,W.jsxs)(ze,{className:`brand`,href:`/`,navigate:n,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`telesrv`}),(0,W.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,W.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,W.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,W.jsx)(Ve,{icon:(0,W.jsx)(oe,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,W.jsx)(Ve,{icon:(0,W.jsx)(ye,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,W.jsx)(Ve,{icon:(0,W.jsx)(pe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,W.jsx)(Ve,{icon:(0,W.jsx)(re,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,W.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,W.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,W.jsx)(ce,{size:16}),(0,W.jsx)(`span`,{children:a(`layout.messages`)}),(0,W.jsx)(F,{className:`nav-section-chevron`,size:15})]}),s&&(0,W.jsxs)(`div`,{className:`nav-children`,children:[(0,W.jsx)(Ve,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:a(`layout.privateMessages`)}),(0,W.jsx)(Ve,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,W.jsxs)(`div`,{className:`sidebar-status`,children:[(0,W.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,W.jsxs)(`div`,{className:`runtime-row`,children:[(0,W.jsx)(fe,{size:14}),(0,W.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,W.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,W.jsxs)(`div`,{className:`runtime-row`,children:[(0,W.jsx)(ee,{size:14}),(0,W.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,W.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,W.jsxs)(`div`,{className:`runtime-row`,children:[(0,W.jsx)(me,{size:14}),(0,W.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,W.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,W.jsxs)(`div`,{className:`workspace`,children:[(0,W.jsxs)(`header`,{className:`topbar`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:je(t.path,a)}),(0,W.jsx)(`h1`,{children:Ae(t.path,a)})]}),(0,W.jsxs)(`div`,{className:`topbar-actions`,children:[(0,W.jsx)(Le,{}),(0,W.jsx)(Te,{}),(0,W.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,W.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,W.jsx)(se,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,W.jsx)(`main`,{className:`content`,children:i})]})]})}function Ve({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,W.jsxs)(ze,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,W.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,W.jsx)(`span`,{children:i})]})}function He(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function Ue(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function We(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function Ge(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 Ke(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function K(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function qe(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function q(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 Je({title:e,eyebrow:t,children:n,actions:r}){return(0,W.jsxs)(`div`,{className:`page-frame`,children:[(0,W.jsxs)(`div`,{className:`page-title-row`,children:[(0,W.jsxs)(`div`,{children:[t&&(0,W.jsx)(`div`,{className:`eyebrow`,children:t}),(0,W.jsx)(`h2`,{children:e})]}),r&&(0,W.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ye({children:e}){return(0,W.jsx)(`div`,{className:`query-panel`,children:e})}function Xe({main:e,side:t}){return(0,W.jsxs)(`div`,{className:`split-layout`,children:[(0,W.jsx)(`div`,{className:`split-main`,children:e}),(0,W.jsx)(`aside`,{className:`split-side`,children:t})]})}function Ze({title:e,text:t,action:n}){return(0,W.jsxs)(`div`,{className:`section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`h2`,{children:e}),t&&(0,W.jsx)(`p`,{children:t})]}),n&&(0,W.jsx)(`div`,{className:`section-action`,children:n})]})}function Qe({children:e}){return(0,W.jsxs)(`div`,{className:`alert`,children:[(0,W.jsx)(O,{size:16}),` `,(0,W.jsx)(`span`,{children:e})]})}function J({children:e,tone:t=`neutral`}){return(0,W.jsx)(`span`,{className:`badge ${t}`,children:e})}function $e({label:e,value:t,tone:n}){return(0,W.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{children:t})]})}function et({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,W.jsxs)(`div`,{className:`metric ${n}`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Y({label:e,value:t,mono:n=!1}){return(0,W.jsxs)(`div`,{className:`summary-item`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function tt({rows:e}){let{t}=we();return(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`audit.id`)}),(0,W.jsx)(`th`,{children:t(`audit.commandID`)}),(0,W.jsx)(`th`,{children:t(`audit.action`)}),(0,W.jsx)(`th`,{children:t(`audit.actor`)}),(0,W.jsx)(`th`,{children:t(`audit.status`)}),(0,W.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,W.jsx)(`th`,{children:t(`audit.reason`)}),(0,W.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,W.jsx)(`td`,{children:e.Action}),(0,W.jsx)(`td`,{children:e.Actor}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,W.jsx)(`td`,{children:Ke(e.CreatedAt)})]},e.ID)),e.length===0&&(0,W.jsx)(nt,{colSpan:8})]})]})})}function nt({colSpan:e}){let{t}=we();return(0,W.jsx)(`tr`,{children:(0,W.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function rt({label:e}){return(0,W.jsx)(`section`,{className:`surface`,children:(0,W.jsx)(`div`,{className:`loading-line`,children:e})})}function it({value:e}){return(0,W.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function at({onLogin:e}){let{t}=we(),[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,W.jsx)(`main`,{className:`login-page`,children:(0,W.jsxs)(`section`,{className:`login-panel`,children:[(0,W.jsxs)(`div`,{className:`login-head`,children:[(0,W.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`telesrv`}),(0,W.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,W.jsxs)(`div`,{className:`login-head-actions`,children:[(0,W.jsx)(Le,{}),(0,W.jsx)(Te,{}),(0,W.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,W.jsxs)(`div`,{className:`login-copy`,children:[(0,W.jsx)(`h1`,{children:t(`login.heading`)}),(0,W.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,W.jsx)(Qe,{children:i}),(0,W.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:t(`login.secret`)}),(0,W.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,W.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var ot=m();function st({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=we(),[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,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,ot.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,W.jsx)(`h2`,{children:e})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,W.jsx)(H,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`command-steps`,children:[(0,W.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,W.jsx)(`span`,{children:`1`}),(0,W.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`2`}),(0,W.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`3`}),(0,W.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:s(`action.reason`)}),(0,W.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,W.jsxs)(`div`,{className:`command-preview`,children:[(0,W.jsxs)(`div`,{className:`preview-head`,children:[(0,W.jsx)(te,{size:14}),` `,s(`action.requestPreview`)]}),(0,W.jsx)(it,{value:JSON.stringify(T,null,2)})]}),m&&(0,W.jsx)(Qe,{children:m}),f&&(0,W.jsxs)(`div`,{className:`result-box`,children:[(0,W.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,W.jsx)(O,{size:16}):(0,W.jsx)(k,{size:16}),(0,W.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:s(`action.commandID`)}),(0,W.jsx)(`strong`,{children:f.command_id})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:s(`action.status`)}),(0,W.jsx)(`strong`,{children:f.status})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:s(`action.dryRun`)}),(0,W.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,W.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,W.jsx)(it,{value:JSON.stringify(f.details,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,W.jsx)(A,{size:15,className:`spin`}):(0,W.jsx)(z,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,W.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,W.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function ct({rows:e,userID:t,onDone:n}){let{t:r}=we(),[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,W.jsxs)(`div`,{className:`authorization-block`,children:[(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:r(`auth.device`)}),(0,W.jsx)(`th`,{children:r(`auth.platform`)}),(0,W.jsx)(`th`,{children:r(`auth.ip`)}),(0,W.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,W.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,W.jsxs)(`tbody`,{children:[o.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,W.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,W.jsx)(`td`,{children:n.IP}),(0,W.jsx)(`td`,{children:Ke(n.ActiveAt)}),(0,W.jsx)(`td`,{className:`device-actions-cell`,children:(0,W.jsxs)(`div`,{className:`device-actions`,children:[(0,W.jsx)(st,{label:r(`auth.revokeCurrent`),icon:(0,W.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,W.jsx)(st,{label:r(`auth.keepCurrent`),icon:(0,W.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,W.jsx)(nt,{colSpan:5})]})]})}),(0,W.jsx)(`div`,{className:`danger-zone`,children:(0,W.jsx)(st,{label:r(`auth.revokeAll`),icon:(0,W.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 lt({id:e,navigate:t}){let{t:n}=we(),[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)(()=>ut(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(ut(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,W.jsx)(Qe,{children:a});if(!r)return(0,W.jsx)(rt,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,W.jsx)(Je,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,W.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,W.jsx)(Xe,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:We(y)}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[Ue(y.Username)||n(`account.noUsername`),` · `,He(y.Phone)||n(`account.noPhone`)]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,W.jsx)(J,{tone:`good`,children:n(`account.premium`)}):(0,W.jsx)(J,{children:n(`account.notPremium`)}),r.Verified?(0,W.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,W.jsx)(J,{children:n(`account.notVerified`)}),y.Frozen?(0,W.jsx)(J,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,W.jsx)(J,{children:n(`account.accountActive`)})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,W.jsx)(Y,{label:n(`account.lastActive`),value:K(r.LastSeenAt)||`-`}),(0,W.jsx)(Y,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?K(y.PremiumUntil):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,W.jsx)(Y,{label:n(`common.updatedAt`),value:Ke(y.UpdatedAt)||`-`}),(0,W.jsx)(Y,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,W.jsx)(Y,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,W.jsx)(Y,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.freezeSince`),value:r.Restriction.Since?Ke(r.Restriction.Since):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.freezeUntil`),value:r.Restriction.Until?Ke(r.Restriction.Until):n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,W.jsx)(Y,{label:n(`account.createdAt`),value:Ke(y.CreatedAt)||`-`})]}),r.About&&(0,W.jsx)(`p`,{className:`about-text`,children:r.About}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,W.jsx)(ct,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,W.jsx)(tt,{rows:r.AuditLogs})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,W.jsx)(st,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,W.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,W.jsx)(st,{label:n(`account.unfreezeAccount`),icon:(0,W.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(st,{label:n(`account.setPremium`),icon:(0,W.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:qe(l)}),onDone:v}),(0,W.jsx)(st,{label:n(`account.clearPremium`),icon:(0,W.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,W.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,W.jsx)(st,{label:n(`account.grantStars`),icon:(0,W.jsx)(he,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:qe(d)}),onDone:v}),(0,W.jsx)(st,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,W.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]})]})})})}function ut(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function dt(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 ft(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 pt({navigate:e}){let{t}=we(),[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=dt(o?.rows??[]);return(0,W.jsxs)(Je,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,W.jsx)(B,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,W.jsx)(Qe,{children:f}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,W.jsx)(et,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,W.jsx)(et,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,W.jsx)(et,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,W.jsx)(Ye,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:t(`common.limit`)}),(0,W.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,W.jsx)(A,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`account.userID`)}),(0,W.jsx)(`th`,{children:t(`account.phone`)}),(0,W.jsx)(`th`,{children:t(`common.username`)}),(0,W.jsx)(`th`,{children:t(`common.name`)}),(0,W.jsx)(`th`,{children:t(`common.device`)}),(0,W.jsx)(`th`,{children:t(`account.lastActive`)}),(0,W.jsx)(`th`,{children:t(`account.premium`)}),(0,W.jsx)(`th`,{children:t(`common.verified`)}),(0,W.jsx)(`th`,{children:t(`account.frozen`)}),(0,W.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.ID}),(0,W.jsx)(`td`,{children:He(n.Phone)}),(0,W.jsx)(`td`,{children:Ue(n.Username)}),(0,W.jsx)(`td`,{children:We(n)}),(0,W.jsx)(`td`,{children:n.DeviceCount}),(0,W.jsx)(`td`,{children:Ke(n.LastActiveAt)}),(0,W.jsx)(`td`,{children:n.PremiumUntil>0?(0,W.jsxs)(J,{tone:`good`,children:[t(`account.premium`),` `,K(n.PremiumUntil)]}):(0,W.jsx)(J,{children:t(`common.none`)})}),(0,W.jsx)(`td`,{children:n.Verified?(0,W.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,W.jsx)(J,{children:t(`account.notVerified`)})}),(0,W.jsx)(`td`,{children:n.Frozen?(0,W.jsx)(J,{tone:`danger`,children:t(`account.frozen`)}):(0,W.jsx)(J,{children:t(`common.normal`)})}),(0,W.jsx)(`td`,{children:Ke(n.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,W.jsx)(nt,{colSpan:11})]})]})})]})}function mt({id:e,navigate:t}){let{t:n}=we(),[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,W.jsx)(Qe,{children:a});if(!r)return(0,W.jsx)(rt,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,W.jsx)(Je,{title:`${Ge(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,W.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,W.jsx)(Xe,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[Ue(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(J,{children:Ge(c,n)}),c.Verified?(0,W.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,W.jsx)(J,{children:n(`account.notVerified`)}),c.Deleted?(0,W.jsx)(J,{tone:`danger`,children:n(`common.deleted`)}):(0,W.jsx)(J,{children:n(`common.valid`)})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,W.jsx)(Y,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,W.jsx)(Y,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,W.jsx)(Y,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,W.jsx)(Y,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,W.jsx)(Y,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,W.jsx)(Y,{label:n(`account.createdAt`),value:K(c.Date)||`-`}),(0,W.jsx)(Y,{label:n(`common.updatedAt`),value:Ke(c.UpdatedAt)||`-`})]}),c.About&&(0,W.jsx)(`p`,{className:`about-text`,children:c.About}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,W.jsx)(tt,{rows:r.AuditLogs})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,W.jsx)(it,{value:r.ChannelJSON})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,W.jsx)(st,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,W.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s})]})})})}function ht({navigate:e}){let{t}=we(),[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=ft(o?.rows??[]);return(0,W.jsxs)(Je,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,W.jsx)(B,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,W.jsx)(Qe,{children:f}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,W.jsx)(et,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,W.jsx)(et,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,W.jsx)(et,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,W.jsx)(Ye,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:t(`common.limit`)}),(0,W.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,W.jsx)(A,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`channel.channelID`)}),(0,W.jsx)(`th`,{children:t(`channel.kind`)}),(0,W.jsx)(`th`,{children:t(`common.username`)}),(0,W.jsx)(`th`,{children:t(`channel.title`)}),(0,W.jsx)(`th`,{children:t(`common.members`)}),(0,W.jsx)(`th`,{children:t(`common.admins`)}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:t(`common.verified`)}),(0,W.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.ID}),(0,W.jsx)(`td`,{children:Ge(n,t)}),(0,W.jsx)(`td`,{children:Ue(n.Username)}),(0,W.jsx)(`td`,{children:n.Title}),(0,W.jsx)(`td`,{children:n.ParticipantsCount}),(0,W.jsx)(`td`,{children:n.AdminsCount}),(0,W.jsx)(`td`,{children:n.PTS}),(0,W.jsx)(`td`,{children:n.Verified?(0,W.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,W.jsx)(J,{children:t(`account.notVerified`)})}),(0,W.jsx)(`td`,{children:Ke(n.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,W.jsx)(nt,{colSpan:10})]})]})})]})}function gt({navigate:e}){let{t}=we();return(0,W.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,W.jsxs)(`section`,{className:`overview-band`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,W.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,W.jsxs)(`div`,{className:`overview-metrics`,children:[(0,W.jsx)($e,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,W.jsx)($e,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,W.jsx)($e,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,W.jsxs)(`div`,{className:`command-grid`,children:[(0,W.jsx)(_t,{icon:(0,W.jsx)(ye,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,W.jsx)(_t,{icon:(0,W.jsx)(pe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,W.jsx)(_t,{icon:(0,W.jsx)(ce,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,W.jsxs)(`section`,{className:`work-strip`,children:[(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(k,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(ae,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(L,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,W.jsxs)(`div`,{className:`strip-item`,children:[(0,W.jsx)(te,{size:16}),(0,W.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function _t({icon:e,title:t,text:n,href:r,navigate:i}){return(0,W.jsxs)(ze,{className:`launcher`,href:r,navigate:i,children:[(0,W.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,W.jsxs)(`span`,{className:`launcher-copy`,children:[(0,W.jsx)(`strong`,{children:t}),(0,W.jsx)(`span`,{children:n})]}),(0,W.jsx)(I,{size:16})]})}function vt({channelID:e,msgID:t,navigate:n}){let{t:r}=we(),[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,W.jsx)(Qe,{children:o});if(!i)return(0,W.jsx)(rt,{label:r(`common.loading`)});let l=i.Message;return(0,W.jsx)(Je,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,W.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:K(l.Date)})})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,W.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,W.jsx)(J,{children:r(`common.survived`)}),l.Pinned&&(0,W.jsx)(J,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,W.jsx)(J,{children:r(`messages.channelPost`)}),(0,W.jsxs)(J,{children:[`pts `,l.PTS]})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,W.jsx)(Y,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,W.jsx)(Y,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,W.jsx)(Y,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,W.jsx)(it,{value:i.MessageJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,W.jsx)(it,{value:i.ChannelJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:r(`common.count`)}),(0,W.jsx)(`th`,{children:r(`common.type`)}),(0,W.jsx)(`th`,{children:r(`common.messageId`)}),(0,W.jsx)(`th`,{children:r(`common.sender`)}),(0,W.jsx)(`th`,{children:r(`common.time`)})]})}),(0,W.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:e.MessageID}),(0,W.jsx)(`td`,{children:e.SenderUserID}),(0,W.jsx)(`td`,{children:K(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,W.jsx)(nt,{colSpan:6})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.eventJson`)}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,W.jsx)(it,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,W.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function yt({label:e,value:t,onChange:n}){let{t:r}=we(),[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,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(P,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:We(t)}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:Ue(t.Username)||He(t.Phone)||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,W.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,W.jsx)(`div`,{className:`picker-error`,children:u}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:We(e)}),(0,W.jsx)(`span`,{children:Ue(e.Username)||He(e.Phone)||`-`}),e.Verified?(0,W.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,W.jsx)(J,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,W.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function bt({label:e,value:t,onChange:n}){let{t:r}=we(),[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,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(P,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:t.Title||`-`}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:Ue(t.Username)||Ge(t,r)})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,W.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,W.jsx)(`div`,{className:`picker-error`,children:u}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:e.Title||`-`}),(0,W.jsx)(`span`,{children:Ue(e.Username)||Ge(e,r)}),e.Verified?(0,W.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,W.jsx)(J,{children:Ge(e,r)})]},e.ID)),o.length===0&&!c?(0,W.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function xt({navigate:e}){let{t}=we(),[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,W.jsxs)(Je,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,W.jsx)(Qe,{children:f}),(0,W.jsxs)(Ye,{children:[(0,W.jsx)(`div`,{className:`message-selector-grid single`,children:(0,W.jsx)(bt,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,W.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(V,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`messages.currentPage`),value:String(_.length)}),(0,W.jsx)(et,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,W.jsx)(et,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,W.jsx)(et,{label:t(`messages.channelGroup`),value:n?`${n.Title||Ge(n,t)} (${n.ID})`:`-`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`common.messageId`)}),(0,W.jsx)(`th`,{children:t(`common.time`)}),(0,W.jsx)(`th`,{children:t(`common.sender`)}),(0,W.jsx)(`th`,{children:`From Peer`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:t(`common.views`)}),(0,W.jsx)(`th`,{children:t(`common.status`)}),(0,W.jsx)(`th`,{children:t(`messages.body`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[_.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.ID}),(0,W.jsx)(`td`,{children:K(n.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,W.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,W.jsx)(`td`,{children:n.PTS}),(0,W.jsx)(`td`,{children:n.ViewsCount}),(0,W.jsx)(`td`,{children:n.Deleted?(0,W.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,W.jsx)(J,{tone:`warn`,children:t(`messages.pinned`)}):(0,W.jsx)(J,{children:t(`common.survived`)})}),(0,W.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,W.jsx)(nt,{colSpan:9})]})]})})]})}function St({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=we(),[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,W.jsx)(Qe,{children:o});if(!i)return(0,W.jsx)(rt,{label:r(`common.loading`)});let l=i.Message;return(0,W.jsx)(Je,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,W.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,W.jsx)(Xe,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:K(l.Date)})})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,W.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,W.jsx)(J,{children:r(`common.survived`)}),(0,W.jsxs)(J,{children:[`pts `,l.PTS]}),(0,W.jsx)(J,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,W.jsx)(Y,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,W.jsx)(Y,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,W.jsx)(Y,{label:r(`common.time`),value:K(l.Date)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,W.jsx)(it,{value:i.MessageJSON})]}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,W.jsx)(it,{value:i.DialogJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,W.jsx)(it,{value:i.PrivateJSON})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:r(`common.count`)}),(0,W.jsx)(`th`,{children:r(`common.type`)}),(0,W.jsx)(`th`,{children:r(`common.time`)})]})}),(0,W.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:K(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,W.jsx)(nt,{colSpan:4})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(Ze,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:r(`account.userID`)}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:r(`common.type`)}),(0,W.jsx)(`th`,{children:r(`common.status`)}),(0,W.jsx)(`th`,{children:r(`messages.attempts`)}),(0,W.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,W.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{children:e.TargetUserID}),(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.EventType}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.Attempts}),(0,W.jsx)(`td`,{children:Ke(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,W.jsx)(nt,{colSpan:7})]})]})})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,W.jsx)(st,{label:r(`messages.deleteThis`),icon:(0,W.jsx)(_e,{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}=we(),[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,W.jsxs)(Je,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,W.jsx)(Qe,{children:D}),(0,W.jsxs)(Ye,{children:[(0,W.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,W.jsx)(yt,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,W.jsx)(yt,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,W.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(V,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,W.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(et,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,W.jsx)(et,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,W.jsx)(et,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,W.jsx)(et,{label:t(`messages.ownerPeer`),value:n&&i?`${We(n)} / ${We(i)}`:`-`})]}),(0,W.jsxs)(`div`,{className:`operation-row`,children:[(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(_e,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,W.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,W.jsx)(st,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:q(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(ie,{size:15}),` `,t(`messages.clearHistory`)]}),(0,W.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,W.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,W.jsx)(st,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:qe(y),max_batches:qe(C),just_clear:_,revoke:m})})]})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:t(`common.messageId`)}),(0,W.jsx)(`th`,{children:t(`common.time`)}),(0,W.jsx)(`th`,{children:t(`common.sender`)}),(0,W.jsx)(`th`,{children:t(`messages.direction`)}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:t(`common.status`)}),(0,W.jsx)(`th`,{children:t(`messages.body`)}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,W.jsx)(`td`,{children:K(n.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,W.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,W.jsx)(`td`,{children:n.PTS}),(0,W.jsx)(`td`,{children:n.Deleted?(0,W.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):(0,W.jsx)(J,{children:t(`common.survived`)})}),(0,W.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,W.jsx)(I,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,W.jsx)(nt,{colSpan:8})]})]})})]})}var wt=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`)},H.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)}},H.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`)))},H.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())},H.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},H.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},H.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`))}},H.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`)},H.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)},H.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),ye(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}(),be=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),xe=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=be.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Se=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return xe(8,e)}(),Ce=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=Se.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=je(c.s),M=je(b),N=(e-y)/(v-y);Ae(r,ke(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 Ae(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 je(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 Me(){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!==Ee&&(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 Ne(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,De(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 Pe(){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]=Ve.newElement()),a[r][0]=e,a[r][1]=t},He.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)},He.prototype.reverse=function(){var e=new He;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=W.getBezierEasing(_.o.x,_.o.y,_.i.x,_.i.y).get,y.__fnct=b),d=b((e-(_.t-this.offsetTime))/(v.t-this.offsetTime-(_.t-this.offsetTime)))}a=v.s?v.s[0]:_.e[0]}i=_.s[0]}for(l=t._length,u=i.i[0].length,n.lastIndex=r,s=0;sr&&t>r)||(this._caching.lastIndex=i0||e>-1e-6&&e<0?r(e*t)/t:e}function P(){var e=this.props,t=N(e[0]),n=N(e[1]),r=N(e[4]),i=N(e[5]),a=N(e[12]),o=N(e[13]);return`matrix(`+t+`,`+n+`,`+r+`,`+i+`,`+a+`,`+o+`)`}return function(){this.reset=i,this.rotate=a,this.rotateX=o,this.rotateY=s,this.rotateZ=c,this.skew=u,this.skewFromAxis=d,this.shear=l,this.scale=f,this.setTransform=m,this.translate=h,this.transform=g,this.multiply=_,this.applyToPoint=S,this.applyToX=C,this.applyToY=w,this.applyToZ=T,this.applyToPointArray=A,this.applyToTriplePoints=k,this.applyToPointStringified=j,this.toCSS=M,this.to2dCSS=P,this.clone=b,this.cloneFromProps=x,this.equals=y,this.inversePoints=O,this.inversePoint=D,this.getInverseMatrix=E,this._t=this.transform,this.isIdentity=v,this._identity=!0,this._identityCalculated=!1,this.props=p(`float32`,16),this.reset()}}();function qe(e){"@babel/helpers - typeof";return qe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},qe(e)}var q={},Je=`__[STANDALONE]__`,Ye=`__[ANIMATIONDATA]__`,Xe=``;function Ze(e){s(e)}function Qe(){Je===!0?U.searchAnimations(Ye,Je,Xe):U.searchAnimations()}function J(e){re(e)}function $e(e){ue(e)}function et(e){return Je===!0&&(e.animationData=JSON.parse(Ye)),U.loadAnimation(e)}function Y(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 tt(){return typeof navigator<`u`}function nt(e,t){e===`expressions`&&ae(t)}function rt(e){switch(e){case`propertyFactory`:return G;case`shapePropertyFactory`:return Ke;case`matrix`:return K;default:return null}}q.play=U.play,q.pause=U.pause,q.setLocationHref=Ze,q.togglePause=U.togglePause,q.setSpeed=U.setSpeed,q.setDirection=U.setDirection,q.stop=U.stop,q.searchAnimations=Qe,q.registerAnimation=U.registerAnimation,q.loadAnimation=et,q.setSubframeRendering=J,q.resize=U.resize,q.goToAndStop=U.goToAndStop,q.destroy=U.destroy,q.setQuality=Y,q.inBrowser=tt,q.installPlugin=nt,q.freeze=U.freeze,q.unfreeze=U.unfreeze,q.setVolume=U.setVolume,q.mute=U.mute,q.unmute=U.unmute,q.getRegisteredAnimations=U.getRegisteredAnimations,q.useWebWorker=a,q.setIDPrefix=$e,q.__getFactory=rt,q.version=`5.13.0`;function it(){document.readyState===`complete`&&(clearInterval(lt),Qe())}function at(e){for(var t=ot.split(`&`),n=0;n=1?a.push({s:e-1,e:t-1}):(a.push({s:e,e:1}),a.push({s:0,e:t-1}));var o=[],s,c=a.length,l;for(s=0;sr+n)){var u=l.s*i<=r?0:(l.s*i-r)/n,d=l.e*i>=r+n?1:(l.e*i-r)/n;o.push([u,d])}return o.length||o.push([0,0]),o},ft.prototype.releasePathsData=function(e){var t,n=e.length;for(t=0;t1?1+r:this.s.v<0?0+r:this.s.v+r,n=this.e.v>1?1+r:this.e.v<0?0+r:this.e.v+r,t>n){var i=t;t=n,n=i}t=Math.round(t*1e4)*1e-4,n=Math.round(n*1e4)*1e-4,this.sValue=t,this.eValue=n}else t=this.sValue,n=this.eValue;var a,o,s=this.shapes.length,c,l,u,d,f,p=0;if(n===t)for(o=0;o=0;--o)if(h=this.shapes[o],h.shape._mdf){for(g=h.localShapeCollection,g.releaseShapes(),this.m===2&&s>1?(b=this.calculateShapeEdges(t,n,h.totalShapeLength,y,p),y+=h.totalShapeLength):b=[[_,v]],l=b.length,c=0;c=1?m.push({s:h.totalShapeLength*(_-1),e:h.totalShapeLength*(v-1)}):(m.push({s:h.totalShapeLength*_,e:h.totalShapeLength}),m.push({s:0,e:h.totalShapeLength*(v-1)}));var x=this.addShapes(h,m[0]);if(m[0].s!==m[0].e){if(m.length>1)if(h.shape.paths.shapes[h.shape.paths._length-1].c){var S=x.pop();this.addPaths(x,g),x=this.addShapes(h,m[1],S)}else this.addPaths(x,g),x=this.addShapes(h,m[1]);this.addPaths(x,g)}}h.shape.paths=g}}else if(this._mdf)for(o=0;ot.e){n.c=!1;break}else t.s<=l&&t.e>=l+u.addedLength?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[s],i[a].v[s],n,d,g),g=!1):(p=Te.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=Te.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=G.getProp(e,t.p.x,0,0,this),this.py=G.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=G.getProp(e,t.p.z,0,0,this))):this.p=G.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=G.getProp(e,t.rx,0,D,this),this.ry=G.getProp(e,t.ry,0,D,this),this.rz=G.getProp(e,t.rz,0,D,this),t.or.k[0].ti){var r,i=t.or.k.length;for(r=0;r0;)--n,this._elements.unshift(t[n]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},ht.prototype.resetElements=function(e){var t,n=e.length;for(t=0;t0?Math.floor(f):Math.ceil(f),h=this.pMatrix.props,g=this.rMatrix.props,_=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v=0;if(f>0){for(;vm;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),--v;p&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-p,!0),v-=p)}r=this.data.m===1?0:this._currentCopies-1,i=this.data.m===1?1:-1,a=this._currentCopies;for(var y,b;a;){if(t=this.elemsData[r].it,n=t[t.length-1].transform.mProps.v.props,b=n.length,t[t.length-1].transform.mProps._mdf=!0,t[t.length-1].transform.op._mdf=!0,t[t.length-1].transform.op.v=this._currentCopies===1?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),v!==0){for((r!==0&&i===1||r!==this._currentCopies-1&&i===-1)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(_[0],_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15]),this.matrix.transform(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8],h[9],h[10],h[11],h[12],h[13],h[14],h[15]),y=0;y0&&r<1?[t]:[]:[t-r,t+r].filter(function(e){return e>0&&e<1})},wt.prototype.split=function(e){if(e<=0)return[Ct(this.points[0]),this];if(e>=1)return[this,Ct(this.points[this.points.length-1])];var t=bt(this.points[0],this.points[1],e),n=bt(this.points[1],this.points[2],e),r=bt(this.points[2],this.points[3],e),i=bt(t,n,e),a=bt(n,r,e),o=bt(i,a,e);return[new wt(this.points[0],t,i,o,!0),new wt(o,a,r,this.points[3],!0)]};function Tt(e,t){var n=e.points[0][t],r=e.points[e.points.length-1][t];if(n>r){var i=r;r=n,n=i}for(var a=xt(3*e.a[t],2*e.b[t],e.c[t]),o=0;o0&&a[o]<1){var s=e.point(a[o])[t];sr&&(r=s)}return{min:n,max:r}}wt.prototype.bounds=function(){return{x:Tt(this,0),y:Tt(this,1)}},wt.prototype.boundingBox=function(){var e=this.bounds();return{left:e.x.min,right:e.x.max,top:e.y.min,bottom:e.y.max,width:e.x.max-e.x.min,height:e.y.max-e.y.min,cx:(e.x.max+e.x.min)/2,cy:(e.y.max+e.y.min)/2}};function Et(e,t,n){var r=e.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:e,t:(t+n)/2,t1:t,t2:n}}function Dt(e){var t=e.bez.split(.5);return[Et(t[0],e.t1,e.t),Et(t[1],e.t,e.t2)]}function Ot(e,t){return Math.abs(e.cx-t.cx)*2=a||e.width<=r&&e.height<=r&&t.width<=r&&t.height<=r){i.push([e.t,t.t]);return}var o=Dt(e),s=Dt(t);kt(o[0],s[0],n+1,r,i,a),kt(o[0],s[1],n+1,r,i,a),kt(o[1],s[0],n+1,r,i,a),kt(o[1],s[1],n+1,r,i,a)}}wt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return kt(Et(this,0,1),Et(e,0,1),0,t,r,n),r},wt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new wt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},wt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new wt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function At(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function jt(e,t,n,r){var i=[e[0],e[1],1],a=[t[0],t[1],1],o=[n[0],n[1],1],s=[r[0],r[1],1],c=At(At(i,a),At(o,s));return vt(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function X(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Mt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Nt(e,t){return _t(e[0],t[0])&&_t(e[1],t[1])}function Pt(){}u([dt],Pt),Pt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=G.getProp(e,t.s,0,null,this),this.frequency=G.getProp(e,t.r,0,null,this),this.pointsType=G.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function Ft(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function It(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function Lt(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=It(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Rt(e,t,n,r,i,a,o){var s=Lt(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;Ft(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function zt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ut(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Gt(e){for(var t,n=1;n1&&(t=Wt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Kt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Vt(e,t)];if(n.length===1||_t(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Vt(r,t),Vt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Vt(r,t),Vt(o,t),Vt(i,t)]}function qt(){}u([dt],qt),qt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=G.getProp(e,t.a,0,null,this),this.miterLimit=G.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},qt.prototype.processPath=function(e,t,n,r){var i=Ue.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=wt.shapeSegmentInverted(e,o),l.push(Kt(c,t));l=Gt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function Xt(e){this.animationData=e}Xt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Zt(e){return new Xt(e)}function Qt(){}Qt.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},mn.prototype.show=function(){},mn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},mn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},mn.prototype.resume=function(){this._canPlay=!0},mn.prototype.setRate=function(e){this.audio.rate(e)},mn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},mn.prototype.getBaseElement=function(){return null},mn.prototype.destroy=function(){},mn.prototype.sourceRectAtTime=function(){},mn.prototype.initExpressions=function(){};function hn(){}hn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},hn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},hn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},hn.prototype.createAudio=function(e){return new mn(e,this.globalData,this)},hn.prototype.createFootage=function(e){return new pn(e,this.globalData,this)},hn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}vn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},vn.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},vn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var yn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=z(`filter`);return n.setAttribute(`id`,e),t!==!0&&(n.setAttribute(`filterUnits`,`objectBoundingBox`),n.setAttribute(`x`,`0%`),n.setAttribute(`y`,`0%`),n.setAttribute(`width`,`100%`),n.setAttribute(`height`,`100%`)),n}function n(){var e=z(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),bn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),xn={},Sn=`filter_result_`;function Cn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=I(),a=yn.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Rn.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function Gn(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([dn,_n,wn,kn,Tn,fn,En],Gn),Gn.prototype.initSecondaryElement=function(){},Gn.prototype.identityMatrix=new K,Gn.prototype.buildExpressionInterface=function(){},Gn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Gn.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},Gn.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},qn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},qn.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},qn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Yt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Yt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Yt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Yt.isVariationSelector(i)&&(o=!0)):Yt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},qn.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=Jt(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var F,I,L=[];for(g=0;g<_;g+=1){for(N=M[g],N.a.sc&&(e.strokeColorAnim=!0),N.a.sw&&(e.strokeWidthAnim=!0),(N.a.fc||N.a.fh||N.a.fs||N.a.fb)&&(e.fillColorAnim=!0),I=0,F=N.s.b,i=0;i0?i=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?o=1-this.xe.v/100:s=1+this.xe.v/100;var c=W.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([Be],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=G.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=G.getProp;for(e=0;e=m+be||!x?(T=(m+be-g)/h.partialLength,ie=b.point[0]+(h.point[0]-b.point[0])*T,ae=b.point[1]+(h.point[1]-b.point[1])*T,a.translate(-n[0]*f[u].an*.005,-(n[1]*A)*.01),_=!1):x&&(g+=h.partialLength,v+=1,v>=x.length&&(v=0,y+=1,S[y]?x=S[y].points:D.v.c?(v=0,y=0,x=S[y].points):(g-=h.partialLength,x=null)),x&&(b=h,h=x[v],C=h.partialLength));re=f[u].an/2-f[u].add,a.translate(-re,0,0)}else re=f[u].an/2-f[u].add,a.translate(-re,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:z(s?`g`:`text`),b<=e){if(c.setAttribute(`stroke-linecap`,`butt`),c.setAttribute(`stroke-linejoin`,`round`),c.setAttribute(`stroke-miterlimit`,`4`),this.textSpans[e].span=c,s){var S=z(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new ir(x.data,this.globalData,this);else{var w=Qn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Gn(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},rr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&lr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new dr(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=$t(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new fr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(gn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=lr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=mr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new 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=Ke.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)},ge(`canvas`,Cr),ut.registerModifier(`tm`,ft),ut.registerModifier(`pb`,pt),ut.registerModifier(`rp`,ht),ut.registerModifier(`rd`,gt),ut.registerModifier(`zz`,Pt),ut.registerModifier(`op`,qt),q}))}))(),1),Tt=0,Et=e=>`${e}-${++Tt}`,Dt=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function Ot(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:Et(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function At(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=Dt[e.length%Dt.length];return{key:Et(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var jt=e=>Ot([kt(e,0),kt(e,1)]),X=()=>{let e=At([]);return Ot([e,At([e])])};function Mt({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=wt.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,W.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function Nt({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,W.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,W.jsx)(Mt,{data:n,compact:!0}):(0,W.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,W.jsx)(A,{className:`spin`,size:15})})}async function Pt(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 Ft=e=>Number.parseInt(e.replace(`#`,``),16),It=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function Lt({gift:e,onClose:t,onPublished:n}){let{t:r}=we(),[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)(()=>jt(`model`)),[D,O]=(0,g.useState)(()=>jt(`pattern`)),[M,N]=(0,g.useState)(X);(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 Pt(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function ee(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||M.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=M.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:M.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:Ft(e.center),edge_color:Ft(e.edge),pattern_color:Ft(e.pattern),text_color:Ft(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}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,W.jsxs)(`section`,{className:`collectible-section`,children:[(0,W.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,W.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,W.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,W.jsxs)(J,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(Ot([...t,kt(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,W.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,W.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,W.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,W.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`common.name`)}),(0,W.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,W.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,W.jsxs)(`label`,{className:`collectible-file`,children:[(0,W.jsx)(`span`,{children:r(`gifts.animation`)}),(0,W.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,W.jsxs)(`em`,{children:[(0,W.jsx)(R,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,W.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,W.jsx)(Mt,{data:i.animation,compact:!0}):(0,W.jsx)(j,{size:16})}),(0,W.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(Ot(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,W.jsx)(_e,{size:14})}),i.fileError&&(0,W.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,ot.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,W.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,W.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,W.jsx)(H,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,W.jsxs)(`div`,{className:`collectible-loading`,children:[(0,W.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,W.jsxs)(`section`,{className:`collectible-active`,children:[(0,W.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(ne,{size:18}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,W.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,W.jsx)(J,{tone:`good`,children:r(`collectibles.published`)})]}),(0,W.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,W.jsxs)(`article`,{children:[(0,W.jsx)(Nt,{giftID:e.GiftID,attribute:t}),(0,W.jsxs)(`div`,{children:[(0,W.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,W.jsx)(J,{children:`crafted`})]}),(0,W.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,It(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,W.jsxs)(`article`,{children:[(0,W.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,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:e.name}),(0,W.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,It(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,W.jsxs)(`div`,{className:`collectible-empty`,children:[(0,W.jsx)(ne,{size:22}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,W.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,W.jsxs)(`section`,{className:`collectible-definition`,children:[(0,W.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,W.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,W.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,W.jsx)(`span`,{children:`TGS`}),(0,W.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,W.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,W.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`gifts.reason`)}),(0,W.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,W.jsxs)(`section`,{className:`collectible-section`,children:[(0,W.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,W.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,W.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,W.jsxs)(J,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N(Ot([...M,At(M)])),F()},children:[(0,W.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,W.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,W.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,W.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`common.name`)}),(0,W.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,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,W.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,W.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,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,W.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,W.jsxs)(`label`,{className:`collectible-color`,children:[(0,W.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,W.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,W.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,W.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length<=2,onClick:()=>{N(Ot(M.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,W.jsx)(_e,{size:14})})]},e.key))})]})]}),u&&(0,W.jsx)(Qe,{children:u}),f&&(0,W.jsxs)(`div`,{className:`gift-validation`,children:[(0,W.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,W.jsx)(k,{size:17}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,W.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,W.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:te,disabled:c,children:[c?(0,W.jsx)(A,{className:`spin`,size:15}):(0,W.jsx)(pe,{size:15}),r(`gifts.validate`)]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:re,disabled:c||!f,children:[(0,W.jsx)(ve,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function Rt(e){return e.model_count+e.pattern_count+e.backdrop_count}function zt(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Bt({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=wt.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,W.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,W.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,W.jsx)(`span`,{children:s})}),(0,W.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,W.jsx)(ue,{size:14}):(0,W.jsx)(z,{size:14})})]})}function Vt({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=wt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,W.jsx)(`div`,{className:`gift-animation-shell`,children:(0,W.jsx)(`div`,{className:`gift-animation`,ref:t})})}function Ht(){let{t:e}=we(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,te]=(0,g.useState)(`50`),[re,ie]=(0,g.useState)(`50`),[ae,oe]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[z,fe]=(0,g.useState)(null),[me,he]=(0,g.useState)(!1),[ge,_e]=(0,g.useState)(``),[ye,U]=(0,g.useState)(``);async function be(){_e(``);try{n((await x.gifts()).Gifts??[])}catch(e){_e(b(e))}}(0,g.useEffect)(()=>{be()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>U(b(e)))},[a,d,p.length]);let xe=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),Se=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),Ce=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),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 Ee(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae)})),r.set(`file`,l,l.name),r}function De(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:re,enabled:se,sort_order:Number(ae),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function Oe(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),te(String(t.stars)),ie(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),fe(null)}async function ke(){he(!0),U(``),fe(null);try{fe(d===`official`?await x.importOfficialGift(De(!1)):await x.importGift(Ee(!1)))}catch(e){U(b(e))}finally{he(!1)}}async function Ae(){if(z){he(!0),U(``);try{d===`official`?await x.importOfficialGift(De(!0,z.command_id)):await x.importGift(Ee(!0,z.command_id)),fe(null),u(null),F(`0`),L(``),C(``),await be(),o(!1)}catch(e){U(b(e))}finally{he(!1)}}}function je(){F(`0`),L(``),te(`50`),ie(`50`),oe(`0`),ce(!0),ue(``),u(null),fe(null),U(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Me(e){F(e.GiftID),L(e.Title),te(String(e.Stars)),ie(String(e.ConvertStars)),oe(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),fe(null),U(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,W.jsxs)(Je,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>be(),disabled:me,children:[(0,W.jsx)(B,{size:15}),` `,e(`common.refresh`)]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,children:[(0,W.jsx)(de,{size:15}),` `,e(`gifts.add`)]})]}),children:[ge&&(0,W.jsx)(Qe,{children:ge}),(0,W.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,W.jsx)(et,{label:e(`gifts.total`),value:String(t.length)}),(0,W.jsx)(et,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,W.jsx)(et,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,W.jsx)(et,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,W.jsx)(Ye,{children:(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,W.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Te.length,total:t.length})})]})}),(0,W.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:e(`gifts.animation`)}),(0,W.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,W.jsx)(`th`,{children:e(`gifts.title`)}),(0,W.jsx)(`th`,{children:e(`gifts.price`)}),(0,W.jsx)(`th`,{children:e(`gifts.source`)}),(0,W.jsx)(`th`,{children:e(`gifts.received`)}),(0,W.jsx)(`th`,{children:e(`common.status`)}),(0,W.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,W.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,W.jsxs)(`tbody`,{children:[Te.map(t=>(0,W.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(Bt,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,W.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,W.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,W.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(J,{children:t.SourceFormat}),(0,W.jsx)(`span`,{className:`gift-source-size`,children:zt(t.AnimationSize)})]}),(0,W.jsx)(`td`,{children:t.ReceivedCount}),(0,W.jsx)(`td`,{children:(0,W.jsx)(J,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,W.jsx)(`td`,{children:Ke(t.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,W.jsx)(ne,{size:13}),e(`collectibles.manage`)]}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Me(t),children:e(`gifts.replace`)}),(0,W.jsx)(st,{compact:!0,tone:`neutral`,label:t.Enabled?e(`gifts.disable`):e(`gifts.enable`),path:`/api/actions/set-gift-enabled`,payload:()=>({gift_id:t.GiftID,enabled:!t.Enabled}),onDone:()=>void be()})]})})]},t.GiftID)),Te.length===0&&(0,W.jsx)(nt,{colSpan:9})]})]})}),a&&(0,ot.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,W.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:me,"aria-label":e(`action.close`),children:(0,W.jsx)(H,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,W.jsxs)(`div`,{className:`command-steps`,children:[(0,W.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,W.jsx)(`span`,{children:`1`}),(0,W.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${z?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`2`}),(0,W.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,W.jsxs)(`div`,{className:`command-step ${z?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`3`}),(0,W.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,W.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,W.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),fe(null)},children:e(`gifts.officialSource`)}),(0,W.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),fe(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,W.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,W.jsxs)(`div`,{className:`gift-import-note`,children:[(0,W.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,W.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,W.jsx)(`span`,{children:p.length}),(0,W.jsx)(`span`,{children:`SHA-256`})]})]}),(0,W.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,W.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:Ce.length,total:p.length})})]}),(0,W.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,W.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,W.jsx)(`span`,{children:Se[t]})]},t))}),(0,W.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[Ce.map(t=>{let n=t.source_gift_id===S;return(0,W.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>Oe(t),children:[(0,W.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,W.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,W.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,W.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,W.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,W.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:Rt(t)})})]}),(0,W.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,W.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,W.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),Ce.length===0&&(0,W.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),xe&&(0,W.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,W.jsx)(Vt,{sourceGiftID:xe.source_gift_id}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:xe.title||e(`gifts.officialUnnamed`,{id:xe.source_gift_id})}),(0,W.jsx)(`span`,{className:`mono`,children:xe.source_gift_id}),(0,W.jsxs)(`small`,{children:[xe.model_count,` `,e(`collectibles.models`),` · `,xe.pattern_count,` `,e(`collectibles.patterns`),` · `,xe.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,W.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,W.jsx)(`span`,{className:xe.can_upgrade?`yes`:`no`,children:xe.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,W.jsx)(`span`,{className:xe.can_craft?`craft`:`no`,children:xe.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),xe?.can_upgrade&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`gift-switch`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),fe(null)}}),(0,W.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,W.jsx)(`span`,{})}),(0,W.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,W.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,W.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),fe(null)}})]})]})]})]}):(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`gift-import-note`,children:[(0,W.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,W.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,W.jsx)(`span`,{children:`TGS`}),(0,W.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,W.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),fe(null)}}),(0,W.jsx)(`span`,{className:`gift-file-icon`,children:(0,W.jsx)(R,{size:22})}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,W.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,W.jsx)(`small`,{children:l?zt(l.size):e(`gifts.fileHint`)})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,W.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.title`)}),(0,W.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.stars`)}),(0,W.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{te(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,W.jsx)(`input`,{type:`number`,min:`0`,value:re,onChange:e=>{ie(e.target.value),fe(null)}})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,W.jsx)(`input`,{type:`number`,value:ae,onChange:e=>{oe(e.target.value),fe(null)}})]})]}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:e(`gifts.reason`)}),(0,W.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,W.jsxs)(`label`,{className:`gift-switch`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),fe(null)}}),(0,W.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,W.jsx)(`span`,{})}),(0,W.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),ye&&(0,W.jsx)(Qe,{children:ye}),z&&(0,W.jsxs)(`div`,{className:`gift-validation`,children:[(0,W.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,W.jsx)(k,{size:17}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,W.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,W.jsx)(`pre`,{children:JSON.stringify(z.details,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:me,children:e(`common.close`)}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:ke,disabled:me,children:[me?(0,W.jsx)(A,{className:`spin`,size:15}):(0,W.jsx)(pe,{size:15}),e(`gifts.validate`)]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Ae,disabled:me||!z,children:[(0,W.jsx)(ve,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,W.jsx)(Lt,{gift:s,onClose:()=>c(null),onPublished:()=>void be()})]})}function Ut({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1];return n?(0,W.jsx)(lt,{id:Number(n),navigate:t}):r?(0,W.jsx)(mt,{id:Number(r),navigate:t}):e.path===`/accounts`?(0,W.jsx)(pt,{navigate:t}):e.path===`/channels`?(0,W.jsx)(ht,{navigate:t}):e.path===`/gifts`?(0,W.jsx)(Ht,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,W.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,W.jsx)(vt,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,W.jsx)(xt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,W.jsx)(Ct,{navigate:t}):(0,W.jsx)(gt,{navigate:t})}function Wt(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>ke());(0,g.useEffect)(()=>{let e=()=>r(ke());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(ke())};return e===void 0?(0,W.jsx)(G,{}):e===null?(0,W.jsx)(at,{onLogin:t}):(0,W.jsx)(Be,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,W.jsx)(Ut,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,W.jsx)(g.StrictMode,{children:(0,W.jsx)(Fe,{children:(0,W.jsx)(Ce,{children:(0,W.jsx)(Wt,{})})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-DuOdm70q.css b/cmd/telesrv-admin/web/dist/assets/index-DuOdm70q.css deleted file mode 100644 index f80a193d..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-DuOdm70q.css +++ /dev/null @@ -1 +0,0 @@ -:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#eef1f5;--bg-accent:#e7ecf1;--panel:#fff;--panel-subtle:#f5f8fb;--panel-strong:#eef2f6;--surface-soft:#f2f7f6;--overlay:#18222f6b;--topbar-bg:#ffffffdb;--line:#e5eaf0;--line-strong:#d3dce4;--heading:#253040;--text:#333f4d;--text-soft:#45525f;--muted:#6d7885;--muted-2:#9aa4b1;--brand:#1f7d6f;--brand-strong:#196155;--brand-2:#3a6cae;--brand-tint:#e8f4f0;--brand-tint-border:#c8e2db;--brand-tint-text:#235d53;--good:#1f8a57;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a86a12;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#c0392b;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#1c2530;--sidebar-soft:#26313d;--sidebar-line:#313c4a;--sidebar-row:#232d38;--sidebar-text:#dbe3ec;--sidebar-muted:#8b98a8;--sidebar-faint:#7c8a9a;--sidebar-heading:#fff;--focus:#1f7d6f29;--shadow:0 12px 34px #1827381a;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #1f7d6f38;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#37a596;--brand-strong:#45b6a6;--brand-2:#6fa8e6;--brand-tint:#14322d;--brand-tint-border:#245349;--brand-tint-text:#7fd3c4;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#37a5963d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #37a59642}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow:var(--shadow-brand)}.brand-mark{color:#fff;background:var(--brand);border-radius:var(--radius-sm);border:1px solid #fff3;place-items:center;width:34px;height:34px;font-weight:800;display:grid}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border:1px solid var(--sidebar-line);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.language-switch{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:2px;display:inline-flex}.language-switch button{min-width:42px;min-height:24px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0 9px;font-weight:800;transition:color .14s,background-color .14s}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input{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{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:190px}.device-actions-cell{width:190px;min-width:190px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(82px,1fr));gap:6px;min-width:178px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:first-child{width:74px}.gift-table td{vertical-align:middle}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid,.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/index.html b/cmd/telesrv-admin/web/dist/index.html index 8da7a220..1c5443a7 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -21,8 +21,8 @@ } })(); - - + +
diff --git a/cmd/telesrv-admin/web/src/api.ts b/cmd/telesrv-admin/web/src/api.ts index 75bc7d79..a568db36 100644 --- a/cmd/telesrv-admin/web/src/api.ts +++ b/cmd/telesrv-admin/web/src/api.ts @@ -1,6 +1,8 @@ import type { AccountDetail, AccountListResponse, + BotDetail, + BotListResponse, ChannelDetail, ChannelListResponse, CommandResult, @@ -56,6 +58,8 @@ export const api = { account: (id: number) => request(`/api/accounts/${id}`), channels: (params: URLSearchParams) => request(`/api/channels?${params.toString()}`), channel: (id: number) => request(`/api/channels/${id}`), + bots: (params: URLSearchParams) => request(`/api/bots?${params.toString()}`), + bot: (id: number) => request(`/api/bots/${id}`), messages: (params: URLSearchParams) => request(`/api/messages?${params.toString()}`), message: (ownerUserID: number, msgID: number) => { const params = new URLSearchParams({ owner_user_id: String(ownerUserID), msg_id: String(msgID) }); diff --git a/cmd/telesrv-admin/web/src/components/Layout.tsx b/cmd/telesrv-admin/web/src/components/Layout.tsx index d9600970..411fc0ab 100644 --- a/cmd/telesrv-admin/web/src/components/Layout.tsx +++ b/cmd/telesrv-admin/web/src/components/Layout.tsx @@ -1,4 +1,5 @@ import { + Bot, ChevronDown, Database, LayoutDashboard, @@ -76,6 +77,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="/bots" route={route} navigate={navigate}>{t("layout.bots")} } href="/gifts" route={route} navigate={navigate}>{t("layout.gifts")}
} + > + +
+
+
{bot.FirstName || t("bots.unnamed")}
+
{displayUsername(bot.Username) || t("account.noUsername")}
+
+
+ {bot.System ? t("bots.system") : t("bots.user")} + {bot.Verified ? {t("common.verified")} : {t("account.notVerified")}} +
+
+
+ + 0 ? `${bot.OwnerUserID} ${displayUsername(detail.OwnerUsername)}`.trim() : t("common.none")} /> + + + +
+ {detail.About &&

{detail.About}

} + {detail.Description && detail.Description.trim() !== detail.About.trim() &&

{detail.Description}

} +
+ + +
+
+ } + side={ +
+
{t("bots.actionDock")}
+
+ } + tone="neutral" + path="/api/actions/set-verified" + payload={() => ({ user_id: bot.ID, verified: !bot.Verified })} + onDone={load} + /> +
+ {bot.System ? ( +

{t("bots.systemHint")}

+ ) : ( +
+ } + tone="danger" + path="/api/actions/delete-bot" + payload={() => ({ bot_user_id: bot.ID })} + onDone={() => navigate("/bots")} + /> +

{t("bots.deleteHint")}

+
+ )} +
+ } + /> + + ); +} diff --git a/cmd/telesrv-admin/web/src/pages/BotsPage.tsx b/cmd/telesrv-admin/web/src/pages/BotsPage.tsx new file mode 100644 index 00000000..a4cde501 --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/BotsPage.tsx @@ -0,0 +1,167 @@ +import { BadgeCheck, Bot, ChevronRight, Loader2, Plus, RefreshCw, Search } from "lucide-react"; +import { useEffect, useState } from "react"; +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 { displayUsername, formatDate, toInt } from "../lib/format"; +import type { Navigate } from "../routing"; +import type { BotListResponse } from "../types"; + +export function BotsPage({ navigate }: { navigate: Navigate }) { + const { t } = useI18n(); + const [q, setQ] = useState(""); + const [limit, setLimit] = useState("50"); + const [data, setData] = useState(null); + const [cursor, setCursor] = useState(0); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + const [ownerID, setOwnerID] = useState(""); + const [botName, setBotName] = useState(""); + const [botUsername, setBotUsername] = useState(""); + + async function load(next = false) { + setBusy(true); + setError(""); + const params = new URLSearchParams({ limit }); + if (q.trim()) { + params.set("q", q.trim()); + } else if (next) { + params.set("before_id", String(cursor)); + } + try { + const result = await api.bots(params); + setData(result); + setCursor(result.next_before_id); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + useEffect(() => { + void load(false); + }, []); + + const rows = data?.rows ?? []; + const verified = rows.filter((row) => row.Verified).length; + const systemCount = rows.filter((row) => row.System).length; + + return ( + load(false)} disabled={busy}> + {t("common.refresh")} + + } + > + {error && {error}} +
+ + + +
+ +
+
+
+

{t("bots.createTitle")}

+

{t("bots.createHint")}

+
+
+
+ + + +
+
+ {t("bots.usernameHint")} + } + tone="neutral" + path="/api/actions/create-bot" + payload={() => ({ + owner_user_id: toInt(ownerID), + name: botName.trim(), + username: botUsername.trim().replace(/^@/, "") + })} + onDone={() => load(false)} + /> +
+
+ + +
{ event.preventDefault(); void load(false); }}> + + + + {data?.listing && data.has_more && ( + + )} +
+
+ +
+ + + + + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + + + + ))} + {rows.length === 0 && } + +
{t("bots.botID")}{t("common.username")}{t("common.name")}{t("bots.owner")}{t("common.verified")}{t("bots.type")}{t("account.createdAt")}
{row.ID}{displayUsername(row.Username) || "-"}{row.FirstName || "-"}{row.OwnerUserID > 0 ? row.OwnerUserID : "-"}{row.Verified ? {t("common.verified")} : {t("account.notVerified")}}{row.System ? {t("bots.system")} : {t("bots.user")}}{formatDate(row.CreatedAt)}
+
+
+ ); +} diff --git a/cmd/telesrv-admin/web/src/pages/Routes.tsx b/cmd/telesrv-admin/web/src/pages/Routes.tsx index 441a9e1d..a76dd433 100644 --- a/cmd/telesrv-admin/web/src/pages/Routes.tsx +++ b/cmd/telesrv-admin/web/src/pages/Routes.tsx @@ -3,6 +3,8 @@ import { AccountDetailPage } from "./AccountDetailPage"; import { AccountsPage } from "./AccountsPage"; import { ChannelDetailPage } from "./ChannelDetailPage"; import { ChannelsPage } from "./ChannelsPage"; +import { BotDetailPage } from "./BotDetailPage"; +import { BotsPage } from "./BotsPage"; import { Dashboard } from "./Dashboard"; import { GroupMessageDetailPage } from "./GroupMessageDetailPage"; import { GroupMessagesPage } from "./GroupMessagesPage"; @@ -13,17 +15,24 @@ import { GiftsPage } from "./GiftsPage"; export function Routes({ route, navigate }: { route: RouteState; navigate: Navigate }) { const accountID = route.path.match(/^\/accounts\/(\d+)$/)?.[1]; const channelID = route.path.match(/^\/channels\/(\d+)$/)?.[1]; + const botID = route.path.match(/^\/bots\/(\d+)$/)?.[1]; if (accountID) { return ; } if (channelID) { return ; } + if (botID) { + return ; + } if (route.path === "/accounts") { return ; } if (route.path === "/channels") { return ; + } + if (route.path === "/bots") { + return ; } if (route.path === "/gifts") { return ; diff --git a/cmd/telesrv-admin/web/src/routing.ts b/cmd/telesrv-admin/web/src/routing.ts index 43273165..a3a042e6 100644 --- a/cmd/telesrv-admin/web/src/routing.ts +++ b/cmd/telesrv-admin/web/src/routing.ts @@ -19,6 +19,7 @@ export function currentRoute(): RouteState { export function routeTitle(pathname: string, t: TFunction): string { if (pathname.startsWith("/accounts")) return t("route.accounts"); if (pathname.startsWith("/channels")) return t("route.channels"); + if (pathname.startsWith("/bots")) return t("route.bots"); if (pathname.startsWith("/messages")) return t("route.messages"); if (pathname.startsWith("/gifts")) return t("route.gifts"); return t("route.dashboard"); @@ -27,6 +28,7 @@ export function routeTitle(pathname: string, t: TFunction): string { export function routeSubtitle(pathname: string, t: TFunction): string { if (pathname.startsWith("/accounts")) return t("route.accountsSubtitle"); if (pathname.startsWith("/channels")) return t("route.channelsSubtitle"); + if (pathname.startsWith("/bots")) return t("route.botsSubtitle"); if (pathname.startsWith("/messages")) return t("route.messagesSubtitle"); if (pathname.startsWith("/gifts")) return t("route.giftsSubtitle"); return t("route.dashboardSubtitle"); diff --git a/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css b/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css index 2c3e0be3..51923628 100644 --- a/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css +++ b/cmd/telesrv-admin/web/src/styles/02-pages-and-forms.css @@ -577,3 +577,40 @@ textarea:focus { color: var(--muted); text-align: center; } + +.bot-create-fields { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.bot-create-fields .duration-field input { + width: 100%; +} + +.bot-create-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid var(--line); +} + +.bot-create-note { + color: var(--muted); + font-size: 12px; + line-height: 1.4; +} + +@media (max-width: 760px) { + .bot-create-fields { + grid-template-columns: 1fr; + } + + .bot-create-actions { + flex-direction: column; + align-items: stretch; + } +} diff --git a/cmd/telesrv-admin/web/src/types.ts b/cmd/telesrv-admin/web/src/types.ts index be00b027..d4963a65 100644 --- a/cmd/telesrv-admin/web/src/types.ts +++ b/cmd/telesrv-admin/web/src/types.ts @@ -99,6 +99,25 @@ export type ChannelDetail = { AuditLogs: AuditLogRow[]; }; +export type BotRow = { + ID: number; + Username: string; + FirstName: string; + Verified: boolean; + System: boolean; + OwnerUserID: number; + CreatedAt: string; + UpdatedAt: string; +}; + +export type BotDetail = { + Bot: BotRow; + About: string; + Description: string; + OwnerUsername: string; + AuditLogs: AuditLogRow[]; +}; + export type MessageRow = { OwnerUserID: number; BoxID: number; @@ -285,6 +304,15 @@ export type ChannelListResponse = { listing: boolean; }; +export type BotListResponse = { + query: string; + limit: number; + rows: BotRow[]; + has_more: boolean; + next_before_id: number; + listing: boolean; +}; + export type MessageListResponse = { owner_user_id: number; peer_id: number; diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index 2dd4e458..5aecfbc8 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -921,6 +921,7 @@ func run(logger *zap.Logger) error { ChannelNotifier: router, Messages: messagesService, Gifts: giftsService, + Bots: botsService, }) // bot session 撤销、在线通知与 @ChatBot 流式草稿推送经 router 实现(需 tg.* 边界), // router 创建后注入。 diff --git a/internal/admin/service.go b/internal/admin/service.go index 356c02b2..ce0e5edc 100644 --- a/internal/admin/service.go +++ b/internal/admin/service.go @@ -32,6 +32,8 @@ const ( ActionPublishGiftCollectibles = "gifts.collectibles.publish" ActionSetStarGiftEnabled = "gifts.set_enabled" ActionSetStarGiftSortOrder = "gifts.set_sort_order" + ActionCreateBot = "bot.create" + ActionDeleteBot = "bot.delete" maxCommandIDLength = 128 maxActorLength = 128 @@ -127,6 +129,14 @@ type OfficialGiftsSource interface { Bundle(ctx context.Context, giftID int64, includeCollectible bool) (officialgifts.Bundle, error) } +// BotService creates bot accounts on behalf of the admin. It mirrors the +// owner-scoped /newbot flow: a bot is a users row (is_bot=true) plus a bots row +// owned by ownerUserID, and the returned token is shown once to the operator. +type BotService interface { + CreateBot(ctx context.Context, ownerUserID int64, name, username string) (domain.User, string, error) + DeleteBot(ctx context.Context, botUserID int64) (domain.User, error) +} + type Dependencies struct { Commands CommandRepository Restrictions RestrictionStore @@ -142,6 +152,7 @@ type Dependencies struct { Messages MessagesService Gifts GiftsService OfficialGifts OfficialGiftsSource + Bots BotService Now func() time.Time } @@ -160,6 +171,7 @@ type Service struct { messages MessagesService gifts GiftsService officialGifts OfficialGiftsSource + bots BotService now func() time.Time } @@ -211,6 +223,9 @@ func (s *Service) Configure(deps Dependencies) *Service { if deps.OfficialGifts != nil { s.officialGifts = deps.OfficialGifts } + if deps.Bots != nil { + s.bots = deps.Bots + } if deps.Now != nil { s.now = deps.Now } @@ -346,6 +361,18 @@ type SetChannelVerifiedRequest struct { Verified bool `json:"verified"` } +type CreateBotRequest struct { + CommandMeta + OwnerUserID int64 `json:"owner_user_id"` + Name string `json:"name"` + Username string `json:"username"` +} + +type DeleteBotRequest struct { + CommandMeta + BotUserID int64 `json:"bot_user_id"` +} + type RevokeSessionsRequest struct { CommandMeta UserID int64 `json:"user_id"` @@ -691,6 +718,93 @@ func (s *Service) SetVerified(ctx context.Context, req SetVerifiedRequest) (Comm }) } +// CreateBot provisions a new bot account owned by ownerUserID. The dry-run stage +// only validates the display name and username; the confirm stage creates the +// users+bots rows and returns the freshly minted token in the result details so +// the operator can copy it once. +func (s *Service) CreateBot(ctx context.Context, req CreateBotRequest) (CommandResult, error) { + if s == nil || s.bots == nil { + return CommandResult{}, fmt.Errorf("admin bot dependency is not configured") + } + if req.OwnerUserID <= 0 { + return CommandResult{}, fmt.Errorf("owner_user_id is required") + } + name := strings.TrimSpace(req.Name) + if name == "" || len([]rune(name)) > domain.MaxBotNameLength { + return CommandResult{}, domain.ErrBotNameInvalid + } + username := strings.TrimSpace(strings.TrimPrefix(req.Username, "@")) + if !domain.ValidBotUsername(username) { + return CommandResult{}, domain.ErrBotUsernameInvalid + } + req.Name = name + req.Username = username + return s.runCommand(ctx, req.CommandMeta, ActionCreateBot, req.OwnerUserID, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{ + "owner_user_id": req.OwnerUserID, + "name": name, + "username": username, + } + if req.DryRun { + return CommandResult{Message: "bot creation validated", Details: details}, nil + } + bot, token, err := s.bots.CreateBot(ctx, req.OwnerUserID, name, username) + if err != nil { + return CommandResult{Details: details}, err + } + details["bot_user_id"] = bot.ID + // The token is a credential. It is surfaced once so the operator can copy + // it; it is also persisted in the audit result, so treat admin audit logs + // as sensitive. + details["token"] = token + if err := s.notifyUserChanged(ctx, bot); err != nil { + details["notify_error"] = err.Error() + } + return CommandResult{Message: "bot created", Details: details}, nil + }) +} + +// DeleteBot permanently removes a user-created bot. The dry-run stage verifies +// the target is a non-system bot; the confirm stage tombstones the account and +// invalidates its token. System bots are rejected outright. +func (s *Service) DeleteBot(ctx context.Context, req DeleteBotRequest) (CommandResult, error) { + if s == nil || s.bots == nil { + return CommandResult{}, fmt.Errorf("admin bot dependency is not configured") + } + if req.BotUserID <= 0 { + return CommandResult{}, fmt.Errorf("bot_user_id is required") + } + if domain.IsSystemUserID(req.BotUserID) { + return CommandResult{}, fmt.Errorf("system bots cannot be deleted") + } + return s.runCommand(ctx, req.CommandMeta, ActionDeleteBot, req.BotUserID, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{"bot_user_id": req.BotUserID} + if s.users != nil { + u, found, err := s.users.AdminUser(ctx, req.BotUserID) + if err != nil { + return CommandResult{}, err + } + if !found || !u.Bot { + return CommandResult{}, domain.ErrBotNotFound + } + details["username"] = u.Username + details["name"] = u.FirstName + } + if req.DryRun { + return CommandResult{Message: "bot deletion validated", Details: details}, nil + } + deleted, err := s.bots.DeleteBot(ctx, req.BotUserID) + if err != nil { + return CommandResult{Details: details}, err + } + details["deleted"] = true + if err := s.notifyUserChanged(ctx, deleted); err != nil { + details["notify_error"] = err.Error() + } + return CommandResult{Message: "bot deleted", Details: details}, nil + }) +} + func (s *Service) SetChannelVerified(ctx context.Context, req SetChannelVerifiedRequest) (CommandResult, error) { if req.ChannelID <= 0 { return CommandResult{}, fmt.Errorf("channel_id is required") diff --git a/internal/adminapi/server.go b/internal/adminapi/server.go index e7f07f14..a8428af4 100644 --- a/internal/adminapi/server.go +++ b/internal/adminapi/server.go @@ -30,6 +30,8 @@ type Service interface { GrantStars(ctx context.Context, req admin.GrantStarsRequest) (admin.CommandResult, error) SetVerified(ctx context.Context, req admin.SetVerifiedRequest) (admin.CommandResult, error) SetChannelVerified(ctx context.Context, req admin.SetChannelVerifiedRequest) (admin.CommandResult, error) + CreateBot(ctx context.Context, req admin.CreateBotRequest) (admin.CommandResult, error) + DeleteBot(ctx context.Context, req admin.DeleteBotRequest) (admin.CommandResult, error) 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) @@ -97,6 +99,8 @@ func (s *Server) routes() http.Handler { mux.HandleFunc("POST /v1/accounts/set-verified", s.authenticated(s.handleSetVerified)) mux.HandleFunc("POST /v1/accounts/revoke-sessions", s.authenticated(s.handleRevokeSessions)) mux.HandleFunc("POST /v1/channels/set-verified", s.authenticated(s.handleSetChannelVerified)) + mux.HandleFunc("POST /v1/bots/create", s.authenticated(s.handleCreateBot)) + mux.HandleFunc("POST /v1/bots/delete", s.authenticated(s.handleDeleteBot)) 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)) @@ -168,6 +172,24 @@ func (s *Server) handleSetChannelVerified(w http.ResponseWriter, r *http.Request writeCommandResult(w, result, err) } +func (s *Server) handleCreateBot(w http.ResponseWriter, r *http.Request) { + var req admin.CreateBotRequest + if !decodeJSON(w, r, &req) { + return + } + result, err := s.svc.CreateBot(r.Context(), req) + writeCommandResult(w, result, err) +} + +func (s *Server) handleDeleteBot(w http.ResponseWriter, r *http.Request) { + var req admin.DeleteBotRequest + if !decodeJSON(w, r, &req) { + return + } + result, err := s.svc.DeleteBot(r.Context(), req) + writeCommandResult(w, result, err) +} + func (s *Server) handleRevokeSessions(w http.ResponseWriter, r *http.Request) { var req admin.RevokeSessionsRequest if !decodeJSON(w, r, &req) { diff --git a/internal/adminapi/server_test.go b/internal/adminapi/server_test.go index b4591396..a2cdd49e 100644 --- a/internal/adminapi/server_test.go +++ b/internal/adminapi/server_test.go @@ -250,6 +250,14 @@ func (fakeService) SetChannelVerified(_ context.Context, req admin.SetChannelVer return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } +func (fakeService) CreateBot(_ context.Context, req admin.CreateBotRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (fakeService) DeleteBot(_ context.Context, req admin.DeleteBotRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + func (fakeService) RevokeSessions(context.Context, admin.RevokeSessionsRequest) (admin.CommandResult, error) { return admin.CommandResult{}, nil } diff --git a/internal/app/bots/service.go b/internal/app/bots/service.go index a610b8cd..717c3528 100644 --- a/internal/app/bots/service.go +++ b/internal/app/bots/service.go @@ -448,6 +448,42 @@ func (s *Service) ListOwnedBots(ctx context.Context, ownerUserID int64) ([]domai return out, nil } +// botAccountDeleter is the optional store capability used to permanently delete +// a user-created bot. Only the Postgres store implements it, so the memory store +// and other BotStore mocks are unaffected. +type botAccountDeleter interface { + DeleteBotAccount(ctx context.Context, botUserID int64) (domain.User, error) +} + +// DeleteBot permanently removes a user-created bot. System service bots are +// rejected. Live sessions are dropped and the bot's caches are invalidated so +// the deletion is visible immediately. Returns the tombstoned user. +func (s *Service) DeleteBot(ctx context.Context, botUserID int64) (domain.User, error) { + if s == nil || s.bots == nil || botUserID == 0 { + return domain.User{}, domain.ErrBotNotFound + } + if domain.IsSystemUserID(botUserID) { + return domain.User{}, domain.ErrBotNotFound + } + deleter, ok := s.bots.(botAccountDeleter) + if !ok { + return domain.User{}, fmt.Errorf("bot deletion is not supported by the configured store") + } + // Drop live sessions up front so the token stops working even if a caller + // races the tombstone; DeleteBotAccount also revokes the authorization rows. + if s.hooks != nil { + if err := s.hooks.RevokeBotSessions(ctx, botUserID); err != nil { + s.log.Warn("revoke bot sessions before delete", zap.Int64("bot_user_id", botUserID), zap.Error(err)) + } + } + u, err := deleter.DeleteBotAccount(ctx, botUserID) + if err != nil { + return domain.User{}, err + } + s.invalidateBotReadCaches(ctx, botUserID) + return u, nil +} + // ExportBotToken 返回 bot token;revoke=true 时先轮换 secret 并撤销已登录 session。 func (s *Service) ExportBotToken(ctx context.Context, ownerUserID, botUserID int64, revoke bool) (string, error) { if revoke { diff --git a/internal/store/postgres/bot.go b/internal/store/postgres/bot.go index 2810ad0e..0be2b4aa 100644 --- a/internal/store/postgres/bot.go +++ b/internal/store/postgres/bot.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/jackc/pgerrcode" "github.com/jackc/pgx/v5" @@ -87,6 +88,91 @@ func (s *BotStore) CreateBotAccount(ctx context.Context, user domain.User, profi return userFromModel(row), profile, nil } +// DeleteBotAccount permanently removes a user-created bot in one transaction: +// it revokes the bot's sessions, purges its private state, releases its +// username, drops the bots row (which invalidates the token) and tombstones the +// users row. System service bots and non-bot users are rejected. The reused +// helpers are the same vetted primitives that back account deletion, so the +// tombstone satisfies users_deletion_state_check. Returns the tombstoned user +// for change notifications. +func (s *BotStore) DeleteBotAccount(ctx context.Context, botUserID int64) (domain.User, error) { + if botUserID == 0 || domain.IsSystemUserID(botUserID) { + return domain.User{}, domain.ErrBotNotFound + } + beginner, ok := s.db.(txBeginner) + if !ok { + return domain.User{}, fmt.Errorf("delete bot account: db does not support transactions") + } + tx, err := beginner.Begin(ctx) + if err != nil { + return domain.User{}, fmt.Errorf("delete bot account: begin: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + + if err := lockUsersForUpdate(ctx, tx, botUserID); err != nil { + return domain.User{}, fmt.Errorf("delete bot account: lock: %w", err) + } + u, found, err := NewUserStore(tx).ByID(ctx, botUserID) + if err != nil { + return domain.User{}, err + } + if !found || !u.Bot || u.Deleted { + return domain.User{}, domain.ErrBotNotFound + } + // Only bots backed by a bots row (created via /newbot or the admin) are + // deletable here; system service bots are already excluded above. + var hasBotRow bool + if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM bots WHERE bot_user_id = $1)`, botUserID).Scan(&hasBotRow); err != nil { + return domain.User{}, fmt.Errorf("delete bot account: probe bots row: %w", err) + } + if !hasBotRow { + return domain.User{}, domain.ErrBotNotFound + } + + now := time.Now().UTC() + if err := enqueueAccountDeletionNotifications(ctx, tx, botUserID); err != nil { + return domain.User{}, err + } + if _, err := revokeByUserExceptTx(ctx, tx, botUserID, 0); err != nil { + return domain.User{}, fmt.Errorf("delete bot account: revoke sessions: %w", err) + } + if err := purgeDeletedAccountPrivateState(ctx, tx, botUserID, now); err != nil { + return domain.User{}, err + } + if err := replacePeerUsernameTx(ctx, tx, peerUsernameTypeUser, botUserID, ""); err != nil { + return domain.User{}, fmt.Errorf("delete bot account: release username: %w", err) + } + // Drop the bots row so the token can no longer authenticate a login. + if _, err := tx.Exec(ctx, `DELETE FROM bots WHERE bot_user_id = $1`, botUserID); err != nil { + return domain.User{}, fmt.Errorf("delete bot account: delete bots row: %w", err) + } + if _, err := tx.Exec(ctx, ` +UPDATE users SET + phone = '', first_name = '', last_name = '', username = '', country_code = '', about = '', + verified = false, support = false, last_seen_at = 0, + premium_expires_at = NULL, emoji_status_document_id = 0, emoji_status_until = 0, + emoji_status_collectible_id = NULL, emoji_status_collectible = '{}'::jsonb, + color_set = false, color = 0, color_background_emoji_id = 0, + profile_color_set = false, profile_color = 0, profile_color_background_emoji_id = 0, + birthday_day = 0, birthday_month = 0, birthday_year = 0, personal_channel_id = 0, + deleted_at = $2, deletion_source = 'manual', deletion_reason = 'admin bot deletion', + account_delete_at = NULL, updated_at = $2 +WHERE id = $1 AND deleted_at IS NULL`, botUserID, now); err != nil { + return domain.User{}, fmt.Errorf("delete bot account: tombstone: %w", err) + } + u, found, err = NewUserStore(tx).ByID(ctx, botUserID) + if err != nil || !found { + if err == nil { + err = domain.ErrUserNotFound + } + return domain.User{}, err + } + if err := tx.Commit(ctx); err != nil { + return domain.User{}, fmt.Errorf("delete bot account: commit: %w", err) + } + return u, nil +} + func (s *BotStore) GetBot(ctx context.Context, botUserID int64) (domain.BotProfile, bool, error) { if botUserID == 0 { return domain.BotProfile{}, false, nil From 313624eab20132e3bd8740006db5ad1a49e94fdd Mon Sep 17 00:00:00 2001 From: epilepticseizureee Date: Thu, 23 Jul 2026 04:00:29 +0300 Subject: [PATCH 24/28] admin: gift granting, collectible attribute/number control, and Layer 228 moderation tools Admin console additions (Layer 228): - Give Gifts: dedicated tab with sorted Lottie/TGS gift picker + inline form; grant any catalog gift to a user/channel from 777000 (no charge) - Upgraded/collectible delivery: mint a unique gift with admin-selected model/pattern/backdrop and custom number, or random/auto (DB FK + UNIQUE(gift_id,num) enforce invariants) - SCAM/FAKE flags for users/channels (migration 0136) with configurable profile warning (TELESRV_SCAM_WARNING/TELESRV_FAKE_WARNING) - Support toggle, force channel settings incl. gigagroup (migration 0137), username management, cosmetic color/emoji-status - Emoji admin tab (custom emoji list + document IDs + Lottie/TGS preview) - Bot management; soft UI / dark theme Wired through Router -> admin.Service -> adminapi -> BFF -> React panel (en/zh/ru). --- .env.example | 8 + cmd/telesrv-admin/readstore.go | 146 ++++- cmd/telesrv-admin/server.go | 368 +++++++++++ .../web/dist/assets/index-BB8hN3NX.js | 9 - .../web/dist/assets/index-BlWlOvtx.css | 1 - .../web/dist/assets/index-Bx7A77x9.js | 9 + .../web/dist/assets/index-XV_IEG5m.css | 1 + cmd/telesrv-admin/web/dist/index.html | 4 +- cmd/telesrv-admin/web/src/api.ts | 3 + .../web/src/components/Layout.tsx | 6 +- .../web/src/components/attributes.tsx | 168 +++++ .../web/src/components/flags.tsx | 59 ++ cmd/telesrv-admin/web/src/i18n.tsx | 219 +++++++ .../web/src/pages/AccountDetailPage.tsx | 9 + .../web/src/pages/AccountsPage.tsx | 3 +- .../web/src/pages/BotDetailPage.tsx | 8 + cmd/telesrv-admin/web/src/pages/BotsPage.tsx | 3 +- .../web/src/pages/ChannelDetailPage.tsx | 10 + .../web/src/pages/ChannelsPage.tsx | 3 +- cmd/telesrv-admin/web/src/pages/EmojiPage.tsx | 158 +++++ cmd/telesrv-admin/web/src/pages/GiftsPage.tsx | 2 +- .../web/src/pages/GiveGiftForm.tsx | 229 +++++++ .../web/src/pages/GiveGiftsPage.tsx | 83 +++ cmd/telesrv-admin/web/src/pages/Routes.tsx | 8 + cmd/telesrv-admin/web/src/routing.ts | 4 + .../src/styles/03-entities-and-actions.css | 167 +++++ cmd/telesrv-admin/web/src/types.ts | 32 + cmd/telesrv/main.go | 3 + .../migrations/0136_scam_fake_flags.down.sql | 7 + deploy/migrations/0136_scam_fake_flags.up.sql | 9 + .../0137_channel_gigagroup.down.sql | 2 + .../migrations/0137_channel_gigagroup.up.sql | 3 + docs/configuration.en.md | 2 + internal/admin/service.go | 613 ++++++++++++++++++ internal/admin/service_test.go | 120 ++++ internal/adminapi/server.go | 144 ++++ internal/adminapi/server_test.go | 48 ++ internal/app/channels/service.go | 40 ++ internal/app/files/emoji_animation.go | 79 +++ internal/app/users/service.go | 47 ++ internal/config/config.go | 8 + internal/domain/channel.go | 22 + internal/domain/star_gift.go | 28 + internal/domain/user.go | 2 + internal/rpc/convert_channels_core.go | 15 +- internal/rpc/convert_flags.go | 81 +++ internal/rpc/convert_users.go | 2 + internal/rpc/payments_star_gifts.go | 22 +- internal/rpc/payments_star_gifts_admin.go | 99 +++ internal/rpc/users.go | 5 + internal/store/channel.go | 5 + internal/store/memory/channel_settings.go | 104 +++ internal/store/memory/users.go | 27 + internal/store/postgres/channel_core.go | 2 +- internal/store/postgres/channel_settings.go | 171 +++++ internal/store/postgres/channel_store.go | 2 +- internal/store/postgres/queries/user.sql | 15 + internal/store/postgres/sqlcgen/bot.sql.go | 4 +- internal/store/postgres/sqlcgen/models.go | 2 + internal/store/postgres/sqlcgen/user.sql.go | 188 +++++- internal/store/postgres/star_gift_upgrade.go | 43 +- internal/store/postgres/user.go | 33 + internal/store/user.go | 4 + 63 files changed, 3650 insertions(+), 71 deletions(-) delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.js delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css create mode 100644 cmd/telesrv-admin/web/dist/assets/index-Bx7A77x9.js create mode 100644 cmd/telesrv-admin/web/dist/assets/index-XV_IEG5m.css create mode 100644 cmd/telesrv-admin/web/src/components/attributes.tsx create mode 100644 cmd/telesrv-admin/web/src/components/flags.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/EmojiPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/GiveGiftForm.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/GiveGiftsPage.tsx create mode 100644 deploy/migrations/0136_scam_fake_flags.down.sql create mode 100644 deploy/migrations/0136_scam_fake_flags.up.sql create mode 100644 deploy/migrations/0137_channel_gigagroup.down.sql create mode 100644 deploy/migrations/0137_channel_gigagroup.up.sql create mode 100644 internal/app/files/emoji_animation.go create mode 100644 internal/rpc/convert_flags.go create mode 100644 internal/rpc/payments_star_gifts_admin.go diff --git a/.env.example b/.env.example index d4211299..6aeed89e 100644 --- a/.env.example +++ b/.env.example @@ -91,6 +91,14 @@ TELESRV_PUBLIC_APP_LINK_BASE= TELESRV_PUBLIC_WEB_BASE_URL=https://web.telesrv.net TELESRV_PUBLIC_APP_NAME=telesrv +# Profile warning text injected into getFullUser/getFullChannel About for peers +# flagged SCAM/FAKE from the admin panel. Empty keeps built-in English defaults. +# Clients cannot localize server text, so set your audience language here. The +# stored bio/description is never overwritten; the warning is re-applied from the +# flag on every read and survives the owner editing their description. +TELESRV_SCAM_WARNING= +TELESRV_FAKE_WARNING= + # Admin API / Admin UI 配置 # # TELESRV_ADMIN_API_TOKEN 是主服务 (cmd/telesrv) 暴露 Admin REST API 的鉴权 token, diff --git a/cmd/telesrv-admin/readstore.go b/cmd/telesrv-admin/readstore.go index c16387bf..0d169fa9 100644 --- a/cmd/telesrv-admin/readstore.go +++ b/cmd/telesrv-admin/readstore.go @@ -43,6 +43,8 @@ type AccountRow struct { Frozen bool Reason string Verified bool + Scam bool + Fake bool PremiumUntil int64 LastActiveAt time.Time DeviceCount int @@ -53,6 +55,8 @@ type AccountDetail struct { About string LastSeenAt int64 Verified bool + Scam bool + Fake bool Support bool Bot bool StarsBalance int64 @@ -112,10 +116,19 @@ type ChannelRow struct { Broadcast bool Megagroup bool Forum bool - Monoforum bool - Verified bool - Deleted bool - ParticipantsCount int + Monoforum bool + Verified bool + Scam bool + Fake bool + Gigagroup bool + Deleted bool + AntiSpam bool + ParticipantsHidden bool + NoForwards bool + JoinToSend bool + JoinRequest bool + SlowmodeSeconds int + ParticipantsCount int AdminsCount int KickedCount int BannedCount int @@ -206,7 +219,7 @@ WITH auth AS ( GROUP BY user_id ) SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at, - COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified, + COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified, u.scam, u.fake, COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint, COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), COALESCE(a.device_count, 0)::int, COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username @@ -224,7 +237,7 @@ LIMIT $5`, id, phone, phoneRaw, username, accountSearchLimit) out := make([]AccountRow, 0) for rows.Next() { var item AccountRow - if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username); err != nil { + if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.Scam, &item.Fake, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username); err != nil { return nil, err } out = append(out, item) @@ -237,6 +250,8 @@ type BotRow struct { Username string FirstName string Verified bool + Scam bool + Fake bool System bool OwnerUserID int64 CreatedAt time.Time @@ -262,7 +277,7 @@ func (s *readStore) ListBots(ctx context.Context, beforeID int64, limit int) ([] limit = accountListMaxLimit } rows, err := s.pool.Query(ctx, ` -SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.verified, +SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.verified, u.scam, u.fake, COALESCE(b.owner_user_id, 0), u.created_at, u.updated_at FROM users u LEFT JOIN bots b ON b.bot_user_id = u.id @@ -277,7 +292,7 @@ LIMIT $2`, beforeID, limit+1) out := make([]BotRow, 0, limit+1) for rows.Next() { var item BotRow - if err := rows.Scan(&item.ID, &item.Username, &item.FirstName, &item.Verified, &item.OwnerUserID, &item.CreatedAt, &item.UpdatedAt); err != nil { + if err := rows.Scan(&item.ID, &item.Username, &item.FirstName, &item.Verified, &item.Scam, &item.Fake, &item.OwnerUserID, &item.CreatedAt, &item.UpdatedAt); err != nil { return nil, false, err } item.System = domain.IsSystemUserID(item.ID) @@ -304,7 +319,7 @@ func (s *readStore) SearchBots(ctx context.Context, q string) ([]BotRow, error) } username := strings.ToLower(strings.TrimPrefix(q, "@")) rows, err := s.pool.Query(ctx, ` -SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.verified, +SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.verified, u.scam, u.fake, COALESCE(b.owner_user_id, 0), u.created_at, u.updated_at FROM users u LEFT JOIN bots b ON b.bot_user_id = u.id @@ -319,7 +334,7 @@ LIMIT $3`, id, username, accountSearchLimit) out := make([]BotRow, 0) for rows.Next() { var item BotRow - if err := rows.Scan(&item.ID, &item.Username, &item.FirstName, &item.Verified, &item.OwnerUserID, &item.CreatedAt, &item.UpdatedAt); err != nil { + if err := rows.Scan(&item.ID, &item.Username, &item.FirstName, &item.Verified, &item.Scam, &item.Fake, &item.OwnerUserID, &item.CreatedAt, &item.UpdatedAt); err != nil { return nil, err } item.System = domain.IsSystemUserID(item.ID) @@ -331,14 +346,14 @@ LIMIT $3`, id, username, accountSearchLimit) func (s *readStore) BotDetail(ctx context.Context, botUserID int64) (BotDetail, error) { var out BotDetail err := s.pool.QueryRow(ctx, ` -SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.about, u.verified, +SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name, u.about, u.verified, u.scam, u.fake, COALESCE(b.owner_user_id, 0), COALESCE(b.description, ''), u.created_at, u.updated_at FROM users u LEFT JOIN bots b ON b.bot_user_id = u.id LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id WHERE u.id = $1 AND u.is_bot AND u.deleted_at IS NULL`, botUserID).Scan( - &out.Bot.ID, &out.Bot.Username, &out.Bot.FirstName, &out.About, &out.Bot.Verified, + &out.Bot.ID, &out.Bot.Username, &out.Bot.FirstName, &out.About, &out.Bot.Verified, &out.Bot.Scam, &out.Bot.Fake, &out.Bot.OwnerUserID, &out.Description, &out.Bot.CreatedAt, &out.Bot.UpdatedAt, ) if err != nil { @@ -377,7 +392,8 @@ func (s *readStore) SearchChannels(ctx context.Context, q string) ([]ChannelRow, rows, err := s.pool.Query(ctx, ` SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about, COALESCE(NULLIF(c.username, ''), p.username_lower, '') AS display_username, - c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.deleted, + c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.scam, c.fake, c.gigagroup, c.deleted, + c.antispam, c.participants_hidden, c.noforwards, c.join_to_send, c.join_request, c.slowmode_seconds, c.participants_count, c.admins_count, c.kicked_count, c.banned_count, c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at FROM channels c @@ -410,7 +426,8 @@ func (s *readStore) ListChannels(ctx context.Context, beforeUpdatedUS, beforeID rows, err := s.pool.Query(ctx, ` SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about, COALESCE(NULLIF(c.username, ''), p.username_lower, '') AS display_username, - c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.deleted, + c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.scam, c.fake, c.gigagroup, c.deleted, + c.antispam, c.participants_hidden, c.noforwards, c.join_to_send, c.join_request, c.slowmode_seconds, c.participants_count, c.admins_count, c.kicked_count, c.banned_count, c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at FROM channels c @@ -442,7 +459,8 @@ func (s *readStore) ChannelDetail(ctx context.Context, channelID int64) (Channel err := s.pool.QueryRow(ctx, ` SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about, COALESCE(NULLIF(c.username, ''), p.username_lower, '') AS display_username, - c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.deleted, + c.broadcast, c.megagroup, c.forum, c.monoforum, c.verified, c.scam, c.fake, c.gigagroup, c.deleted, + c.antispam, c.participants_hidden, c.noforwards, c.join_to_send, c.join_request, c.slowmode_seconds, c.participants_count, c.admins_count, c.kicked_count, c.banned_count, c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at, row_to_json(c)::jsonb @@ -486,7 +504,8 @@ func scanChannelRow(row channelScanner, item *ChannelRow) error { func channelScanDest(item *ChannelRow) []any { return []any{ &item.ID, &item.AccessHash, &item.CreatorUserID, &item.Title, &item.About, &item.Username, - &item.Broadcast, &item.Megagroup, &item.Forum, &item.Monoforum, &item.Verified, &item.Deleted, + &item.Broadcast, &item.Megagroup, &item.Forum, &item.Monoforum, &item.Verified, &item.Scam, &item.Fake, &item.Gigagroup, &item.Deleted, + &item.AntiSpam, &item.ParticipantsHidden, &item.NoForwards, &item.JoinToSend, &item.JoinRequest, &item.SlowmodeSeconds, &item.ParticipantsCount, &item.AdminsCount, &item.KickedCount, &item.BannedCount, &item.TopMessageID, &item.PinnedMessageID, &item.PTS, &item.Date, &item.CreatedAt, &item.UpdatedAt, } @@ -511,7 +530,7 @@ WITH auth AS ( GROUP BY user_id ) SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at, - COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified, + COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified, u.scam, u.fake, COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint, auth.last_active_at, auth.device_count, COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username @@ -530,7 +549,7 @@ LIMIT $3`, beforeActiveUS, beforeID, limit+1) out := make([]AccountRow, 0, limit+1) for rows.Next() { var item AccountRow - if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username); err != nil { + if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.Scam, &item.Fake, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username); err != nil { return nil, false, err } out = append(out, item) @@ -549,7 +568,7 @@ func (s *readStore) AccountDetail(ctx context.Context, userID int64) (AccountDet var out AccountDetail err := s.pool.QueryRow(ctx, ` SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at, - u.about, u.last_seen_at, u.verified, u.support, u.is_bot, + u.about, u.last_seen_at, u.verified, u.scam, u.fake, u.support, u.is_bot, COALESCE(r.frozen, false), COALESCE(r.reason, ''), COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint, COALESCE(sb.balance, 0)::bigint, COALESCE(sb.granted, false), @@ -560,7 +579,7 @@ LEFT JOIN stars_balances sb ON sb.user_id = u.id LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id WHERE u.id = $1`, userID).Scan( &out.Account.ID, &out.Account.Phone, &out.Account.Username, &out.Account.FirstName, &out.Account.LastName, - &out.Account.CreatedAt, &out.Account.UpdatedAt, &out.About, &out.LastSeenAt, &out.Verified, &out.Support, &out.Bot, + &out.Account.CreatedAt, &out.Account.UpdatedAt, &out.About, &out.LastSeenAt, &out.Verified, &out.Scam, &out.Fake, &out.Support, &out.Bot, &out.Account.Frozen, &out.Account.Reason, &out.Account.PremiumUntil, &out.StarsBalance, &out.StarsGranted, &out.Account.Username, ) if err != nil { @@ -962,3 +981,90 @@ func prettyJSON(raw []byte) string { } return string(out) } + +// EmojiRow is a custom-emoji document projection for the admin emoji browser. +type EmojiRow struct { + DocumentID int64 `json:"DocumentID,string"` + Alt string + MimeType string + Size int64 + SetTitle string + CreatedAt time.Time +} + +const emojiListDefaultLimit = 60 +const emojiListMaxLimit = 200 + +func scanEmojiRows(rows pgx.Rows) ([]EmojiRow, error) { + out := make([]EmojiRow, 0) + for rows.Next() { + var item EmojiRow + if err := rows.Scan(&item.DocumentID, &item.Alt, &item.MimeType, &item.Size, &item.CreatedAt, &item.SetTitle); err != nil { + return nil, err + } + out = append(out, item) + } + return out, rows.Err() +} + +const emojiSelectColumns = `d.id, + COALESCE((SELECT a->>'alt' FROM jsonb_array_elements(d.attributes) a WHERE a->>'kind' = 'custom_emoji' LIMIT 1), ''), + d.mime_type, d.size, d.created_at, + COALESCE((SELECT s.title FROM sticker_sets s WHERE s.emojis AND NOT s.deleted AND s.document_ids @> to_jsonb(d.id) LIMIT 1), '')` + +// ListEmoji pages over custom-emoji documents by descending id. +func (s *readStore) ListEmoji(ctx context.Context, beforeID int64, limit int) ([]EmojiRow, bool, error) { + if limit <= 0 { + limit = emojiListDefaultLimit + } + if limit > emojiListMaxLimit { + limit = emojiListMaxLimit + } + rows, err := s.pool.Query(ctx, ` +SELECT `+emojiSelectColumns+` +FROM documents d +WHERE d.attributes @> '[{"kind":"custom_emoji"}]'::jsonb + AND ($1::bigint = 0 OR d.id < $1) +ORDER BY d.id DESC +LIMIT $2`, beforeID, limit+1) + if err != nil { + return nil, false, fmt.Errorf("list emoji: %w", err) + } + defer rows.Close() + out, err := scanEmojiRows(rows) + if err != nil { + return nil, false, err + } + hasMore := len(out) > limit + if hasMore { + out = out[:limit] + } + return out, hasMore, nil +} + +// SearchEmoji finds custom-emoji documents by document id or emoticon substring. +func (s *readStore) SearchEmoji(ctx context.Context, q string) ([]EmojiRow, error) { + q = strings.TrimSpace(q) + if q == "" { + return nil, nil + } + id := int64(-1) + if n, err := strconv.ParseInt(q, 10, 64); err == nil { + id = n + } + rows, err := s.pool.Query(ctx, ` +SELECT `+emojiSelectColumns+` +FROM documents d +WHERE d.attributes @> '[{"kind":"custom_emoji"}]'::jsonb + AND (d.id = $1 OR EXISTS ( + SELECT 1 FROM jsonb_array_elements(d.attributes) a + WHERE a->>'kind' = 'custom_emoji' AND a->>'alt' ILIKE '%' || $2 || '%' + )) +ORDER BY d.id DESC +LIMIT $3`, id, q, emojiListMaxLimit) + if err != nil { + return nil, fmt.Errorf("search emoji: %w", err) + } + defer rows.Close() + return scanEmojiRows(rows) +} diff --git a/cmd/telesrv-admin/server.go b/cmd/telesrv-admin/server.go index e1ee773f..08a8bcc0 100644 --- a/cmd/telesrv-admin/server.go +++ b/cmd/telesrv-admin/server.go @@ -55,6 +55,8 @@ func (s *server) routes() http.Handler { mux.Handle("GET /api/channels/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleChannelDetailAPI))) mux.Handle("GET /api/bots", s.requireAuthAPI(http.HandlerFunc(s.handleBotsAPI))) mux.Handle("GET /api/bots/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleBotDetailAPI))) + mux.Handle("GET /api/emoji", s.requireAuthAPI(http.HandlerFunc(s.handleEmojiAPI))) + mux.Handle("GET /api/emoji/{id}/animation", s.requireAuthAPI(http.HandlerFunc(s.handleEmojiAnimationAPI))) mux.Handle("GET /api/messages", s.requireAuthAPI(http.HandlerFunc(s.handleMessagesAPI))) mux.Handle("GET /api/messages/detail", s.requireAuthAPI(http.HandlerFunc(s.handleMessageDetailAPI))) mux.Handle("GET /api/messages/groups", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessagesAPI))) @@ -69,6 +71,16 @@ func (s *server) routes() http.Handler { 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))) mux.Handle("POST /api/actions/set-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetVerifiedAPI))) + mux.Handle("POST /api/actions/set-account-flags", s.requireAuthAPI(http.HandlerFunc(s.handleSetUserFlagsAPI))) + mux.Handle("POST /api/actions/set-channel-flags", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelFlagsAPI))) + mux.Handle("POST /api/actions/set-support", s.requireAuthAPI(http.HandlerFunc(s.handleSetSupportAPI))) + mux.Handle("POST /api/actions/set-account-username", s.requireAuthAPI(http.HandlerFunc(s.handleSetUsernameAPI))) + mux.Handle("POST /api/actions/set-account-color", s.requireAuthAPI(http.HandlerFunc(s.handleSetUserColorAPI))) + mux.Handle("POST /api/actions/set-account-emoji-status", s.requireAuthAPI(http.HandlerFunc(s.handleSetUserEmojiStatusAPI))) + mux.Handle("POST /api/actions/set-channel-settings", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelSettingsAPI))) + mux.Handle("POST /api/actions/set-channel-username", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelUsernameAPI))) + mux.Handle("POST /api/actions/set-channel-color", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelColorAPI))) + mux.Handle("POST /api/actions/set-channel-emoji-status", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelEmojiStatusAPI))) mux.Handle("POST /api/actions/create-bot", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBotAPI))) mux.Handle("POST /api/actions/delete-bot", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteBotAPI))) mux.Handle("POST /api/actions/set-channel-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelVerifiedAPI))) @@ -80,6 +92,7 @@ func (s *server) routes() http.Handler { 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.Handle("POST /api/actions/give-gift", s.requireAuthAPI(http.HandlerFunc(s.handleGiveGiftAPI))) mux.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { writeAPIError(w, http.StatusNotFound, "api route not found") }) @@ -192,6 +205,73 @@ func (s *server) handleStarGiftsAPI(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"Gifts": rows}) } +func (s *server) handleEmojiAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + q := r.URL.Query().Get("q") + beforeID, _ := parseInt64(r.URL.Query().Get("before_id")) + limit, _ := parseInt(r.URL.Query().Get("limit")) + rows := []EmojiRow{} + hasMore := false + var err error + if strings.TrimSpace(q) != "" { + rows, err = s.read.SearchEmoji(r.Context(), q) + } else { + rows, hasMore, err = s.read.ListEmoji(r.Context(), beforeID, limit) + } + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + nextBeforeID := int64(0) + if hasMore && len(rows) > 0 { + nextBeforeID = rows[len(rows)-1].DocumentID + } + writeJSON(w, http.StatusOK, map[string]any{ + "query": q, + "rows": rows, + "has_more": hasMore, + "next_before_id": nextBeforeID, + "listing": strings.TrimSpace(q) == "", + }) +} + +func (s *server) handleEmojiAnimationAPI(w http.ResponseWriter, r *http.Request) { + documentID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || documentID <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid document id") + return + } + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, + fmt.Sprintf("%s/v1/emoji/%d/animation", s.cfg.AdminAPIURL, documentID), 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) handleStarGiftAnimationAPI(w http.ResponseWriter, r *http.Request) { giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil || giftID <= 0 { @@ -706,6 +786,254 @@ func (s *server) handleSetVerifiedAPI(w http.ResponseWriter, r *http.Request) { writeCommandResultAPI(w, result, err) } +type setUserFlagsAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID int64 `json:"user_id"` + Scam bool `json:"scam"` + Fake bool `json:"fake"` +} + +func (s *server) handleSetUserFlagsAPI(w http.ResponseWriter, r *http.Request) { + var body setUserFlagsAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetUserFlagsRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-account-flags"), + UserID: body.UserID, + Scam: body.Scam, + Fake: body.Fake, + } + result, err := s.callAdminAPI(r.Context(), "/v1/accounts/set-flags", req) + writeCommandResultAPI(w, result, err) +} + +type setChannelFlagsAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + ChannelID int64 `json:"channel_id"` + Scam bool `json:"scam"` + Fake bool `json:"fake"` +} + +func (s *server) handleSetChannelFlagsAPI(w http.ResponseWriter, r *http.Request) { + var body setChannelFlagsAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetChannelFlagsRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-channel-flags"), + ChannelID: body.ChannelID, + Scam: body.Scam, + Fake: body.Fake, + } + result, err := s.callAdminAPI(r.Context(), "/v1/channels/set-flags", req) + writeCommandResultAPI(w, result, err) +} + +type setSupportAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID int64 `json:"user_id"` + Support bool `json:"support"` +} + +func (s *server) handleSetSupportAPI(w http.ResponseWriter, r *http.Request) { + var body setSupportAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetSupportRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-support"), + UserID: body.UserID, + Support: body.Support, + } + result, err := s.callAdminAPI(r.Context(), "/v1/accounts/set-support", req) + writeCommandResultAPI(w, result, err) +} + +type setUsernameAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID int64 `json:"user_id"` + Username string `json:"username"` +} + +func (s *server) handleSetUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body setUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-username"), + UserID: body.UserID, + Username: body.Username, + } + result, err := s.callAdminAPI(r.Context(), "/v1/accounts/set-username", req) + writeCommandResultAPI(w, result, err) +} + +type setUserColorAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID int64 `json:"user_id"` + ForProfile bool `json:"for_profile"` + HasColor bool `json:"has_color"` + Color int `json:"color"` + BackgroundEmojiID int64 `json:"background_emoji_id,string"` +} + +func (s *server) handleSetUserColorAPI(w http.ResponseWriter, r *http.Request) { + var body setUserColorAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetUserColorRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-account-color"), + UserID: body.UserID, + PeerColorInput: admin.PeerColorInput{ + ForProfile: body.ForProfile, HasColor: body.HasColor, Color: body.Color, BackgroundEmojiID: body.BackgroundEmojiID, + }, + } + result, err := s.callAdminAPI(r.Context(), "/v1/accounts/set-color", req) + writeCommandResultAPI(w, result, err) +} + +type setUserEmojiStatusAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID int64 `json:"user_id"` + DocumentID int64 `json:"document_id,string"` + Until int `json:"until"` +} + +func (s *server) handleSetUserEmojiStatusAPI(w http.ResponseWriter, r *http.Request) { + var body setUserEmojiStatusAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetUserEmojiStatusRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-account-emoji-status"), + UserID: body.UserID, + EmojiStatusInput: admin.EmojiStatusInput{DocumentID: body.DocumentID, Until: body.Until}, + } + result, err := s.callAdminAPI(r.Context(), "/v1/accounts/set-emoji-status", req) + writeCommandResultAPI(w, result, err) +} + +type setChannelSettingsAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + ChannelID int64 `json:"channel_id"` + Gigagroup *bool `json:"gigagroup,omitempty"` + AntiSpam *bool `json:"antispam,omitempty"` + ParticipantsHidden *bool `json:"participants_hidden,omitempty"` + NoForwards *bool `json:"noforwards,omitempty"` + JoinToSend *bool `json:"join_to_send,omitempty"` + JoinRequest *bool `json:"join_request,omitempty"` + SlowmodeSeconds *int `json:"slowmode_seconds,omitempty"` +} + +func (s *server) handleSetChannelSettingsAPI(w http.ResponseWriter, r *http.Request) { + var body setChannelSettingsAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetChannelSettingsRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-channel-settings"), + ChannelID: body.ChannelID, + Gigagroup: body.Gigagroup, + AntiSpam: body.AntiSpam, + ParticipantsHidden: body.ParticipantsHidden, + NoForwards: body.NoForwards, + JoinToSend: body.JoinToSend, + JoinRequest: body.JoinRequest, + SlowmodeSeconds: body.SlowmodeSeconds, + } + result, err := s.callAdminAPI(r.Context(), "/v1/channels/set-settings", req) + writeCommandResultAPI(w, result, err) +} + +type setChannelUsernameAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + ChannelID int64 `json:"channel_id"` + Username string `json:"username"` +} + +func (s *server) handleSetChannelUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body setChannelUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetChannelUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-channel-username"), + ChannelID: body.ChannelID, + Username: body.Username, + } + result, err := s.callAdminAPI(r.Context(), "/v1/channels/set-username", req) + writeCommandResultAPI(w, result, err) +} + +type setChannelColorAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + ChannelID int64 `json:"channel_id"` + ForProfile bool `json:"for_profile"` + HasColor bool `json:"has_color"` + Color int `json:"color"` + BackgroundEmojiID int64 `json:"background_emoji_id,string"` +} + +func (s *server) handleSetChannelColorAPI(w http.ResponseWriter, r *http.Request) { + var body setChannelColorAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetChannelColorRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-channel-color"), + ChannelID: body.ChannelID, + PeerColorInput: admin.PeerColorInput{ + ForProfile: body.ForProfile, HasColor: body.HasColor, Color: body.Color, BackgroundEmojiID: body.BackgroundEmojiID, + }, + } + result, err := s.callAdminAPI(r.Context(), "/v1/channels/set-color", req) + writeCommandResultAPI(w, result, err) +} + +type setChannelEmojiStatusAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + ChannelID int64 `json:"channel_id"` + DocumentID int64 `json:"document_id,string"` + Until int `json:"until"` +} + +func (s *server) handleSetChannelEmojiStatusAPI(w http.ResponseWriter, r *http.Request) { + var body setChannelEmojiStatusAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetChannelEmojiStatusRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-channel-emoji-status"), + ChannelID: body.ChannelID, + EmojiStatusInput: admin.EmojiStatusInput{DocumentID: body.DocumentID, Until: body.Until}, + } + result, err := s.callAdminAPI(r.Context(), "/v1/channels/set-emoji-status", req) + writeCommandResultAPI(w, result, err) +} + type setChannelVerifiedAPIRequest struct { CommandID string `json:"command_id"` Reason string `json:"reason"` @@ -1029,6 +1357,46 @@ func (s *server) handleSetStarGiftSortOrderAPI(w http.ResponseWriter, r *http.Re writeCommandResultAPI(w, result, err) } +type giveGiftAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + SenderUserID int64 `json:"sender_user_id"` + UserID int64 `json:"user_id"` + ChannelID int64 `json:"channel_id"` + GiftID int64 `json:"gift_id,string"` + HideName bool `json:"hide_name"` + Message string `json:"message"` + Upgrade bool `json:"upgrade"` + ModelAttributeID int64 `json:"model_attribute_id,string"` + PatternAttributeID int64 `json:"pattern_attribute_id,string"` + BackdropAttributeID int64 `json:"backdrop_attribute_id,string"` + Num int `json:"num"` +} + +func (s *server) handleGiveGiftAPI(w http.ResponseWriter, r *http.Request) { + var body giveGiftAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.GiveGiftRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "give-gift"), + SenderUserID: body.SenderUserID, + UserID: body.UserID, + ChannelID: body.ChannelID, + GiftID: body.GiftID, + HideName: body.HideName, + Message: body.Message, + Upgrade: body.Upgrade, + ModelAttributeID: body.ModelAttributeID, + PatternAttributeID: body.PatternAttributeID, + BackdropAttributeID: body.BackdropAttributeID, + Num: body.Num, + } + result, err := s.callAdminAPI(r.Context(), "/v1/gifts/give", 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-") { diff --git a/cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.js b/cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.js deleted file mode 100644 index 3278d88a..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-BB8hN3NX.js +++ /dev/null @@ -1,9 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.provider`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.iterator;function p(e){return typeof e!=`object`||!e?null:(e=f&&e[f]||e[`@@iterator`],typeof e==`function`?e:null)}var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,g={};function _(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}_.prototype.isReactComponent={},_.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`setState(...): takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},_.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function v(){}v.prototype=_.prototype;function y(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}var b=y.prototype=new v;b.constructor=y,h(b,_.prototype),b.isPureReactComponent=!0;var x=Array.isArray,S=Object.prototype.hasOwnProperty,C={current:null},w={key:!0,ref:!0,__self:!0,__source:!0};function T(e,n,r){var i,a={},o=null,s=null;if(n!=null)for(i in n.ref!==void 0&&(s=n.ref),n.key!==void 0&&(o=``+n.key),n)S.call(n,i)&&!w.hasOwnProperty(i)&&(a[i]=n[i]);var c=arguments.length-2;if(c===1)a.children=r;else if(1{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=typeof setTimeout==`function`?setTimeout:null,_=typeof clearTimeout==`function`?clearTimeout:null,v=typeof setImmediate<`u`?setImmediate:null;typeof navigator<`u`&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function b(e){if(h=!1,y(e),!m)if(n(c)!==null)m=!0,M(x);else{var t=n(l);t!==null&&N(b,t.startTime-e)}}function x(t,i){m=!1,h&&(h=!1,_(w),w=-1),p=!0;var a=f;try{for(y(i),d=n(c);d!==null&&(!(d.expirationTime>i)||t&&!D());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=i);i=e.unstable_now(),typeof s==`function`?d.callback=s:d===n(c)&&r(c),y(i)}else r(c);d=n(c)}if(d!==null)var u=!0;else{var g=n(l);g!==null&&N(b,g.startTime-i),u=!1}return u}finally{d=null,f=a,p=!1}}var S=!1,C=null,w=-1,T=5,E=-1;function D(){return!(e.unstable_now()-Ee||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(_(w),w=-1):h=!0,N(b,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,M(x))),r},e.unstable_shouldYield=D,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u(),n=f();function r(e){for(var t=`https://reactjs.org/docs/error-decoder.html?invariant=`+e,n=1;n`u`||window.document===void 0||window.document.createElement===void 0),l=Object.prototype.hasOwnProperty,d=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function h(e){return l.call(m,e)?!0:l.call(p,e)?!1:d.test(e)?m[e]=!0:(p[e]=!0,!1)}function g(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case`function`:case`symbol`:return!0;case`boolean`:return r?!1:n===null?(e=e.toLowerCase().slice(0,5),e!==`data-`&&e!==`aria-`):!n.acceptsBooleans;default:return!1}}function _(e,t,n,r){if(t==null||g(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return!1===t;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function v(e,t,n,r,i,a,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=o}var y={};`children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style`.split(` `).forEach(function(e){y[e]=new v(e,0,!1,e,null,!1,!1)}),[[`acceptCharset`,`accept-charset`],[`className`,`class`],[`htmlFor`,`for`],[`httpEquiv`,`http-equiv`]].forEach(function(e){var t=e[0];y[t]=new v(t,1,!1,e[1],null,!1,!1)}),[`contentEditable`,`draggable`,`spellCheck`,`value`].forEach(function(e){y[e]=new v(e,2,!1,e.toLowerCase(),null,!1,!1)}),[`autoReverse`,`externalResourcesRequired`,`focusable`,`preserveAlpha`].forEach(function(e){y[e]=new v(e,2,!1,e,null,!1,!1)}),`allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope`.split(` `).forEach(function(e){y[e]=new v(e,3,!1,e.toLowerCase(),null,!1,!1)}),[`checked`,`multiple`,`muted`,`selected`].forEach(function(e){y[e]=new v(e,3,!0,e,null,!1,!1)}),[`capture`,`download`].forEach(function(e){y[e]=new v(e,4,!1,e,null,!1,!1)}),[`cols`,`rows`,`size`,`span`].forEach(function(e){y[e]=new v(e,6,!1,e,null,!1,!1)}),[`rowSpan`,`start`].forEach(function(e){y[e]=new v(e,5,!1,e.toLowerCase(),null,!1,!1)});var b=/[\-:]([a-z])/g;function x(e){return e[1].toUpperCase()}`accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,null,!1,!1)}),`xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/1999/xlink`,!1,!1)}),[`xml:base`,`xml:lang`,`xml:space`].forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/XML/1998/namespace`,!1,!1)}),[`tabIndex`,`crossOrigin`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!1,!1)}),y.xlinkHref=new v(`xlinkHref`,1,!1,`xlink:href`,`http://www.w3.org/1999/xlink`,!0,!1),[`src`,`href`,`action`,`formAction`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!0,!0)});function S(e,t,n,r){var i=y.hasOwnProperty(t)?y[t]:null;(i===null?r||!(2s||i[o]!==a[s]){var c=` -`+i[o].replace(` at new `,` at `);return e.displayName&&c.includes(``)&&(c=c.replace(``,e.displayName)),c}while(1<=o&&0<=s);break}}}finally{re=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?ne(e):``}function ae(e){switch(e.tag){case 5:return ne(e.type);case 16:return ne(`Lazy`);case 13:return ne(`Suspense`);case 19:return ne(`SuspenseList`);case 0:case 2:case 15:return e=ie(e.type,!1),e;case 11:return e=ie(e.type.render,!1),e;case 1:return e=ie(e.type,!0),e;default:return``}}function oe(e){if(e==null)return null;if(typeof e==`function`)return e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case E:return`Fragment`;case T:return`Portal`;case O:return`Profiler`;case D:return`StrictMode`;case M:return`Suspense`;case N:return`SuspenseList`}if(typeof e==`object`)switch(e.$$typeof){case A:return(e.displayName||`Context`)+`.Consumer`;case k:return(e._context.displayName||`Context`)+`.Provider`;case j:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case P:return t=e.displayName||null,t===null?oe(e.type)||`Memo`:t;case F:t=e._payload,e=e._init;try{return oe(e(t))}catch{}}return null}function se(e){var t=e.type;switch(e.tag){case 24:return`Cache`;case 9:return(t.displayName||`Context`)+`.Consumer`;case 10:return(t._context.displayName||`Context`)+`.Provider`;case 18:return`DehydratedFragment`;case 11:return e=t.render,e=e.displayName||e.name||``,t.displayName||(e===``?`ForwardRef`:`ForwardRef(`+e+`)`);case 7:return`Fragment`;case 5:return t;case 4:return`Portal`;case 3:return`Root`;case 6:return`Text`;case 16:return oe(t);case 8:return t===D?`StrictMode`:`Mode`;case 22:return`Offscreen`;case 12:return`Profiler`;case 21:return`Scope`;case 13:return`Suspense`;case 19:return`SuspenseList`;case 25:return`TracingMarker`;case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t==`function`)return t.displayName||t.name||null;if(typeof t==`string`)return t}return null}function ce(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function le(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function ue(e){var t=le(e)?`checked`:`value`,n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=``+e[t];if(!e.hasOwnProperty(t)&&n!==void 0&&typeof n.get==`function`&&typeof n.set==`function`){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(e){r=``+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=``+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function z(e){e._valueTracker||=ue(e)}function B(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 de(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`||de(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=Ce.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 W={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},Ee=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(W).forEach(function(e){Ee.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),W[t]=W[e]})});function De(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||W.hasOwnProperty(e)&&W[e]?(``+t).trim():t+`px`}function Oe(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=De(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var ke=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 Ae(e,t){if(t){if(ke[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 je(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 Me=null;function Ne(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Pe=null,Fe=null,Ie=null;function Le(e){if(e=ji(e)){if(typeof Pe!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Ni(t),Pe(e.stateNode,e.type,t))}}function Re(e){Fe?Ie?Ie.push(e):Ie=[e]:Fe=e}function ze(){if(Fe){var e=Fe,t=Ie;if(Ie=Fe=null,Le(e),t)for(e=0;e>>=0,e===0?32:31-(_t(e)/vt|0)|0}var bt=64,xt=4194304;function St(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ct(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,o=n&268435455;if(o!==0){var s=o&~i;s===0?(a&=o,a!==0&&(r=St(a))):r=St(s)}else o=n&~i,o===0?a!==0&&(r=St(a)):r=St(o);if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&a&4194240))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function kt(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-gt(t),e[t]=n}function At(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Gn),Jn=` `,Yn=!1;function Xn(e,t){switch(e){case`keyup`:return Un.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Zn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Qn=!1;function $n(e,t){switch(e){case`compositionend`:return Zn(t);case`keypress`:return t.which===32?(Yn=!0,Jn):null;case`textInput`:return e=t.data,e===Jn&&Yn?null:e;default:return null}}function er(e,t){if(Qn)return e===`compositionend`||!Wn&&Xn(e,t)?(e=mn(),pn=fn=dn=null,Qn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=xr(n)}}function Cr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Cr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wr(){for(var e=window,t=de();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=de(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!==de(r)||(r=Or,`selectionStart`in r&&Tr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Ar&&br(Ar,r)||(Ar=r,r=ii(kr,`onSelect`),0Fi||(e.current=Pi[Fi],Pi[Fi]=null,Fi--)}function Ri(e,t){Fi++,Pi[Fi]=e.current,e.current=t}var zi={},Bi=Ii(zi),Vi=Ii(!1),Hi=zi;function Ui(e,t){var n=e.type.contextTypes;if(!n)return zi;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Wi(e){return e=e.childContextTypes,e!=null}function Gi(){Li(Vi),Li(Bi)}function Ki(e,t,n){if(Bi.current!==zi)throw Error(r(168));Ri(Bi,t),Ri(Vi,n)}function qi(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,se(e)||`Unknown`,a));return R({},n,i)}function Ji(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||zi,Hi=Bi.current,Ri(Bi,e),Ri(Vi,Vi.current),!0}function Yi(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=qi(e,t,Hi),i.__reactInternalMemoizedMergedChildContext=e,Li(Vi),Li(Bi),Ri(Bi,e)):Li(Vi),Ri(Vi,n)}var Xi=null,Zi=!1,Qi=!1;function $i(e){Xi===null?Xi=[e]:Xi.push(e)}function ea(e){Zi=!0,$i(e)}function ta(){if(!Qi&&Xi!==null){Qi=!0;var e=0,t=X;try{var n=Xi;for(X=1;e>=o,i-=o,la=1<<32-gt(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),_a&&da(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),_a&&da(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return _a&&da(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),_a&&da(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===F&&ja(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=ka(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=ka(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case F:return l=i._init,_(e,r,l(i._payload),o)}if(ve(i))return h(e,r,i,o);if(ee(i))return g(e,r,i,o);Aa(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Na=Ma(!0),Pa=Ma(!1),Fa=Ii(null),Ia=null,La=null,Ra=null;function za(){Ra=La=Ia=null}function Ba(e){var t=Fa.current;Li(Fa),e._currentValue=t}function Va(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Ha(e,t){Ia=e,Ra=La=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Ms=!0),e.firstContext=null)}function Ua(e){var t=e._currentValue;if(Ra!==e)if(e={context:e,memoizedValue:t,next:null},La===null){if(Ia===null)throw Error(r(308));La=e,Ia.dependencies={lanes:0,firstContext:e}}else La=La.next=e;return t}var Wa=null;function Ga(e){Wa===null?Wa=[e]:Wa.push(e)}function Ka(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Ga(t)):(n.next=i.next,i.next=n),t.interleaved=n,qa(e,r)}function qa(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Ja=!1;function Ya(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Za(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Qa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,$&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,qa(e,n)}return i=r.interleaved,i===null?(t.next=t,Ga(r)):(t.next=i.next,i.next=t),r.interleaved=t,qa(e,n)}function $a(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}function eo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function to(e,t,n,r){var i=e.updateQueue;Ja=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=R({},d,f);break a;case 2:Ja=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function no(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=vo.transition;vo.transition={};try{e(!1),t()}finally{X=n,vo.transition=r}}function as(){return Mo().memoizedState}function os(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},cs(e))ls(t,n);else if(n=Ka(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),us(n,t,r)}}function ss(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(cs(e))ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Z(s,o)){var c=t.interleaved;c===null?(i.next=i,Ga(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ka(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),us(n,t,r))}}function cs(e){var t=e.alternate;return e===bo||t!==null&&t===bo}function ls(e,t){wo=Co=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function us(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}var ds={readContext:Ua,useCallback:Do,useContext:Do,useEffect:Do,useImperativeHandle:Do,useInsertionEffect:Do,useLayoutEffect:Do,useMemo:Do,useReducer:Do,useRef:Do,useState:Do,useDebugValue:Do,useDeferredValue:Do,useTransition:Do,useMutableSource:Do,useSyncExternalStore:Do,useId:Do,unstable_isNewReconciler:!1},fs={readContext:Ua,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:Ua,useEffect:Jo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Ko(4194308,4,Qo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ko(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ko(4,2,e,t)},useMemo:function(e,t){var n=jo();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=jo();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=os.bind(null,bo,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:Uo,useDebugValue:es,useDeferredValue:function(e){return jo().memoizedState=e},useTransition:function(){var e=Uo(!1),t=e[0];return e=is.bind(null,e[1]),jo().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=bo,a=jo();if(_a){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));yo&30||Ro(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,Jo(Bo.bind(null,i,o,e),[e]),i.flags|=2048,Wo(9,zo.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=jo(),t=Vc.identifierPrefix;if(_a){var n=ua,r=la;n=(r&~(1<<32-gt(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=To++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[wi]=t,e[Ti]=i,nc(e,t,!1,!1),t.stateNode=e;a:{switch(c=je(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*ot()-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=ot(),t.sibling=null,n=po.current,Ri(po,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(oc(t),t.subtreeFlags&6&&(t.flags|=8192)):oc(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function cc(e,t){switch(ma(t),t.tag){case 1:return Wi(t.type)&&Gi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return lo(),Li(Vi),Li(Bi),go(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return fo(t),null;case 13:if(Li(po),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ea()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Li(po),null;case 4:return lo(),null;case 10:return Ba(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var lc=!1,uc=!1,dc=typeof WeakSet==`function`?WeakSet:Set,Q=null;function fc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function pc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var mc=!1;function hc(e,t){if(fi=rn,e=wr(),Tr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(pi={focusedElem:e,selectionRange:n},rn=!1,Q=t;Q!==null;)if(t=Q,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Q=e;else for(;Q!==null;){t=Q;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:hs(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,Q=e;break}Q=t.return}return h=mc,mc=!1,h}function gc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&pc(t,n,a)}i=i.next}while(i!==r)}}function _c(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function vc(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function yc(e){var t=e.alternate;t!==null&&(e.alternate=null,yc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[wi],delete t[Ti],delete t[Di],delete t[Oi],delete t[ki])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function bc(e){return e.tag===5||e.tag===3||e.tag===4}function xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||bc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=di));else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}function Cc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Cc(e,t,n),e=e.sibling;e!==null;)Cc(e,t,n),e=e.sibling}var wc=null,Tc=!1;function Ec(e,t,n){for(n=n.child;n!==null;)Dc(e,t,n),n=n.sibling}function Dc(e,t,n){if(mt&&typeof mt.onCommitFiberUnmount==`function`)try{mt.onCommitFiberUnmount(pt,n)}catch{}switch(n.tag){case 5:uc||fc(n,t);case 6:var r=wc,i=Tc;wc=null,Ec(e,t,n),wc=r,Tc=i,wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):wc.removeChild(n.stateNode));break;case 18:wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?bi(e.parentNode,n):e.nodeType===1&&bi(e,n),tn(e)):bi(wc,n.stateNode));break;case 4:r=wc,i=Tc,wc=n.stateNode.containerInfo,Tc=!0,Ec(e,t,n),wc=r,Tc=i;break;case 0:case 11:case 14:case 15:if(!uc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&pc(n,t,o),i=i.next}while(i!==r)}Ec(e,t,n);break;case 1:if(!uc&&(fc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Ec(e,t,n);break;case 21:Ec(e,t,n);break;case 22:n.mode&1?(uc=(r=uc)||n.memoizedState!==null,Ec(e,t,n),uc=r):Ec(e,t,n);break;default:Ec(e,t,n)}}function Oc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new dc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function kc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=ot()-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;lot()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=xt,xt<<=1,!(xt&130023424)&&(xt=4194304)):t=1);var n=fl();e=qa(e,t),e!==null&&(kt(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Vi.current)Ms=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return Ms=!1,tc(e,t,n);Ms=!!(e.flags&131072)}else Ms=!1,_a&&t.flags&1048576&&fa(t,aa,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;$s(e,t),e=t.pendingProps;var a=Ui(t,Bi.current);Ha(t,n),a=ko(null,t,i,e,a,n);var o=Ao();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Wi(i)?(o=!0,Ji(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ya(t),a.updater=_s,t.stateNode=a,a._reactInternals=t,xs(t,i,e,n),t=Vs(null,t,i,!0,o,n)):(t.tag=0,_a&&o&&pa(t),Ns(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch($s(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=hs(i,e),a){case 0:t=zs(null,t,i,e,n);break a;case 1:t=Bs(null,t,i,e,n);break a;case 11:t=Ps(null,t,i,e,n);break a;case 14:t=Fs(null,t,i,hs(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),zs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Bs(e,t,i,a,n);case 3:a:{if(Hs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Xa(e,t),to(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=Ss(Error(r(423)),t),t=Us(e,t,i,n,a);break a}else if(i!==a){a=Ss(Error(r(424)),t),t=Us(e,t,i,n,a);break a}else for(ga=xi(t.stateNode.containerInfo.firstChild),ha=t,_a=!0,va=null,n=Pa(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ea(),i===a){t=ec(e,t,n);break a}Ns(e,t,i,n)}t=t.child}return t;case 5:return uo(t),e===null&&Sa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,mi(i,a)?s=null:o!==null&&mi(i,o)&&(t.flags|=32),Rs(e,t),Ns(e,t,s,n),t.child;case 6:return e===null&&Sa(t),null;case 13:return Ks(e,t,n);case 4:return co(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Na(t,null,i,n):Ns(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Ps(e,t,i,a,n);case 7:return Ns(e,t,t.pendingProps,n),t.child;case 8:return Ns(e,t,t.pendingProps.children,n),t.child;case 12:return Ns(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Ri(Fa,i._currentValue),i._currentValue=s,o!==null)if(Z(o.value,s)){if(o.children===a.children&&!Vi.current){t=ec(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Za(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Va(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Va(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ns(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Ha(t,n),a=Ua(a),i=i(a),t.flags|=1,Ns(e,t,i,n),t.child;case 14:return i=t.type,a=hs(i,t.pendingProps),a=hs(i.type,a),Fs(e,t,i,a,n);case 15:return Is(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),$s(e,t),t.tag=1,Wi(i)?(e=!0,Ji(t)):e=!1,Ha(t,n),ys(t,i,a),xs(t,i,a,n),Vs(null,t,i,!0,e,n);case 19:return Qs(e,t,n);case 22:return Ls(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return nt(e,t)}function Gl(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(e,t,n,r){return new Gl(e,t,n,r)}function ql(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jl(e){if(typeof e==`function`)return+!!ql(e);if(e!=null){if(e=e.$$typeof,e===j)return 11;if(e===P)return 14}return 2}function Yl(e,t){var n=e.alternate;return n===null?(n=Kl(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xl(e,t,n,i,a,o){var s=2;if(i=e,typeof e==`function`)ql(e)&&(s=1);else if(typeof e==`string`)s=5;else a:switch(e){case E:return Zl(n.children,a,o,t);case D:s=8,a|=8;break;case O:return e=Kl(12,n,t,a|2),e.elementType=O,e.lanes=o,e;case M:return e=Kl(13,n,t,a),e.elementType=M,e.lanes=o,e;case N:return e=Kl(19,n,t,a),e.elementType=N,e.lanes=o,e;case I:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case k:s=10;break a;case A:s=9;break a;case j:s=11;break a;case P:s=14;break a;case F:s=16,i=null;break a}throw Error(r(130,e==null?e:typeof e,``))}return t=Kl(s,n,t,a),t.elementType=e,t.type=i,t.lanes=o,t}function Zl(e,t,n,r){return e=Kl(7,e,r,t),e.lanes=n,e}function Ql(e,t,n,r){return e=Kl(22,e,r,t),e.elementType=I,e.lanes=n,e.stateNode={isHidden:!1},e}function $l(e,t,n){return e=Kl(6,e,null,t),e.lanes=n,e}function eu(e,t,n){return t=Kl(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function tu(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ot(0),this.expirationTimes=Ot(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ot(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ya(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}};async function y(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData,r=await fetch(e,{credentials:`same-origin`,headers:n?t.headers:{"Content-Type":`application/json`,...t.headers??{}},...t}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function b(e){return e instanceof Error?e.message:String(e)}var x={session:()=>y(`/api/session`),login:e=>y(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})}),logout:()=>y(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>y(`/api/accounts?${e.toString()}`),account:e=>y(`/api/accounts/${e}`),channels:e=>y(`/api/channels?${e.toString()}`),channel:e=>y(`/api/channels/${e}`),bots:e=>y(`/api/bots?${e.toString()}`),bot:e=>y(`/api/bots/${e}`),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`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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(`Bot`,[[`path`,{d:`M12 8V4H8`,key:`hb8ula`}],[`rect`,{width:`16`,height:`12`,x:`4`,y:`8`,rx:`2`,key:`enze0r`}],[`path`,{d:`M2 14h2`,key:`vft8re`}],[`path`,{d:`M20 14h2`,key:`4cs60a`}],[`path`,{d:`M15 13v2`,key:`1xurst`}],[`path`,{d:`M9 13v2`,key:`rq6x2g`}]]),P=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`}]]),F=E(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),I=E(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),L=E(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),ee=E(`Clock3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`polyline`,{points:`12 6 12 12 16.5 12`,key:`1aq6pp`}]]),R=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`}]]),te=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`}]]),ne=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`}]]),re=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`}]]),ie=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`}]]),ae=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`}]]),oe=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`}]]),se=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`}]]),ce=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`}]]),le=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`}]]),ue=E(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),z=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`}]]),B=E(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),de=E(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),fe=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`}]]),pe=E(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),me=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`}]]),he=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`}]]),ge=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`}]]),_e=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`}]]),ve=E(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),ye=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`}]]),be=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`}]]),V=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`}]]),H=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),xe=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})),U=o(((e,t)=>{t.exports=xe()}))(),Se=`telesrv.admin.lang`,Ce={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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Switch to dark theme`,"theme.switchToLight":`Switch to light theme`,"login.heading":`Operations Admin`,"login.body":`Enter credentials to open the console.`,"login.secret":`Admin password or token`,"login.submit":`Log in`,"login.submitting":`Logging in`,"dashboard.eyebrow":`Runtime Overview`,"dashboard.title":`Console Overview`,"dashboard.readPath":`Read path`,"dashboard.readPathValue":`PG read-only`,"dashboard.writePath":`Write path`,"dashboard.executionPolicy":`Execution policy`,"dashboard.dryRunFirst":`Dry-run first`,"dashboard.accountsText":`Account status, premium, verification, sessions.`,"dashboard.channelsText":`Public entities, member counts, verification state.`,"dashboard.messagesText":`Message boxes, updates, outbox state.`,"dashboard.strip.dryRun":`All dangerous actions start with dry-run`,"dashboard.strip.token":`Browser never stores internal tokens`,"dashboard.strip.pagination":`Lists use cursor pagination`,"dashboard.strip.snapshot":`Detail pages retain raw state snapshots`,"account.pageTitle":`Accounts`,"account.queryResults":`Search results`,"account.recentActive":`Recently active accounts`,"account.currentPage":`Accounts on page`,"account.onlineDevices":`Online device records`,"account.premium":`Premium`,"account.frozen":`Frozen`,"account.searchPlaceholder":`User ID / phone / username`,"account.userID":`User ID`,"account.phone":`Phone`,"account.lastActive":`Last active`,"account.notVerified":`Not verified`,"account.notPremium":`Not premium`,"account.premiumUntil":`Premium expires`,"account.starsBalance":`Stars balance`,"account.startingGrantApplied":`initial grant applied`,"account.startingGrantPending":`initial grant pending`,"account.activeSessions":`Authorized devices`,"account.accountFlags":`Account flags`,"account.restriction":`Restriction`,"account.restricted":`Restricted`,"account.createdAt":`Created`,"account.detailTitle":`Account #{id}`,"account.profile":`Account Profile`,"account.loadingDetail":`Loading account detail`,"account.waitingData":`Waiting for data`,"account.noUsername":`No username`,"account.noPhone":`No phone`,"account.accountFrozen":`Account frozen`,"account.accountActive":`Account active`,"account.authorizationsTitle":`Authorized Devices`,"account.authorizationsCount":`{count} authorizations`,"account.recentAdminOps":`Recent Admin Actions`,"account.recent30Audit":`Last 30 audit rows`,"account.actionDock":`Account Actions`,"account.freezeAccount":`Freeze account`,"account.updateFreeze":`Update freeze`,"account.unfreezeAccount":`Unfreeze account`,"account.freezeSince":`Frozen since`,"account.freezeUntil":`Appeal deadline`,"account.freezeUntilAria":`Freeze appeal deadline`,"account.freezeAppealURL":`Appeal URL`,"account.freezeAppealURLAria":`Freeze appeal URL`,"account.premiumMonths":`Premium duration (months)`,"account.premiumMonthsAria":`Set premium duration in months`,"account.setPremium":`Set premium`,"account.clearPremium":`Clear premium`,"account.starsAmount":`Stars to grant`,"account.starsAmountAria":`Set Stars amount to grant`,"account.grantStars":`Grant Stars`,"account.setVerified":`Set verified`,"account.clearVerified":`Clear verified`,"channel.pageTitle":`Supergroups and Channels`,"channel.recentUpdated":`Recently updated`,"channel.currentPage":`Entities on page`,"channel.megagroups":`Supergroups`,"channel.broadcasts":`Channels`,"channel.verifiedCount":`Verified`,"channel.searchPlaceholder":`Channel ID / username / title`,"channel.channelID":`Channel ID`,"channel.kind":`Kind`,"channel.title":`Title`,"channel.pts":`PTS`,"channel.detailProfile":`Channel Profile`,"channel.loadingDetail":`Loading channel detail`,"channel.creator":`Creator {id}`,"channel.governance":`Moderation`,"channel.governanceValue":`Banned {banned} / Kicked {kicked}`,"channel.flags":`Channel flags`,"channel.rawRow":`Channel Raw Row`,"channel.rawRowText":`Database read-only snapshot`,"channel.actionDock":`Channel Actions`,"channel.setVerified":`Set verified`,"channel.clearVerified":`Clear verified`,"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`,"route.bots":`Bots`,"route.botsSubtitle":`Console / Bots`,"layout.bots":`Bots`,"bots.pageTitle":`Bots`,"bots.queryResults":`Search results`,"bots.recent":`Recently created bots`,"bots.currentPage":`Bots on page`,"bots.banned":`Banned`,"bots.active":`Active`,"bots.createTitle":`Create a system bot`,"bots.createHint":`Provision a bot account owned by the given user. The token is shown once after confirmation.`,"bots.ownerUserID":`Owner user ID`,"bots.name":`Display name`,"bots.namePlaceholder":`e.g. Service Bot`,"bots.username":`Username`,"bots.usernameHint":`Username must be 5-32 characters and end with 'bot'.`,"bots.create":`Create bot`,"bots.searchPlaceholder":`Bot ID / username`,"bots.botID":`Bot ID`,"bots.owner":`Owner`,"bots.status":`Status`,"bots.detailTitle":`Bot #{id}`,"bots.profile":`Bot Profile`,"bots.loadingDetail":`Loading bot detail`,"bots.unnamed":`Unnamed bot`,"bots.restriction":`Restriction`,"bots.actionDock":`Bot Actions`,"bots.banUntil":`Ban until`,"bots.ban":`Ban bot`,"bots.updateBan":`Update ban`,"bots.unban":`Unban bot`,"bots.type":`Type`,"bots.system":`System`,"bots.user":`User`,"bots.delete":`Delete bot`,"bots.deleteHint":`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`,"bots.systemHint":`System bots are built in and cannot be deleted.`,"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.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`中文`,"language.ru":`RU`,"theme.switchToDark":`切换到深色主题`,"theme.switchToLight":`切换到浅色主题`,"login.heading":`运维后台`,"login.body":`输入凭据后进入控制台。`,"login.secret":`管理员密码或 token`,"login.submit":`登录`,"login.submitting":`登录中`,"dashboard.eyebrow":`运行总览`,"dashboard.title":`控制台总览`,"dashboard.readPath":`读路径`,"dashboard.readPathValue":`PG 只读`,"dashboard.writePath":`写路径`,"dashboard.executionPolicy":`执行策略`,"dashboard.dryRunFirst":`先预演`,"dashboard.accountsText":`账号状态、会员、认证、会话。`,"dashboard.channelsText":`公开实体、成员计数、认证状态。`,"dashboard.messagesText":`消息盒、update、outbox 状态。`,"dashboard.strip.dryRun":`所有危险操作先预演`,"dashboard.strip.token":`浏览器不持有内部 token`,"dashboard.strip.pagination":`列表使用游标分页`,"dashboard.strip.snapshot":`详情页保留原始状态快照`,"account.pageTitle":`账号`,"account.queryResults":`查询结果`,"account.recentActive":`最近活跃账号`,"account.currentPage":`当前页账号`,"account.onlineDevices":`在线设备记录`,"account.premium":`会员`,"account.frozen":`冻结`,"account.searchPlaceholder":`用户 ID / 手机号 / 用户名`,"account.userID":`用户 ID`,"account.phone":`手机号`,"account.lastActive":`最近活跃`,"account.notVerified":`未认证`,"account.notPremium":`非会员`,"account.premiumUntil":`会员到期`,"account.starsBalance":`Stars 余额`,"account.startingGrantApplied":`初始赠送已发放`,"account.startingGrantPending":`初始赠送未触发`,"account.activeSessions":`授权设备`,"account.accountFlags":`账号标记`,"account.restriction":`限制状态`,"account.restricted":`已限制`,"account.createdAt":`创建时间`,"account.detailTitle":`账号 #{id}`,"account.profile":`账号档案`,"account.loadingDetail":`加载账号详情`,"account.waitingData":`等待数据`,"account.noUsername":`无用户名`,"account.noPhone":`无手机号`,"account.accountFrozen":`账号已冻结`,"account.accountActive":`账号正常`,"account.authorizationsTitle":`授权设备`,"account.authorizationsCount":`共 {count} 个授权`,"account.recentAdminOps":`最近后台操作`,"account.recent30Audit":`最近 30 条审计`,"account.actionDock":`账号操作`,"account.freezeAccount":`冻结账号`,"account.updateFreeze":`更新冻结信息`,"account.unfreezeAccount":`解冻账号`,"account.freezeSince":`冻结开始时间`,"account.freezeUntil":`申诉截止时间`,"account.freezeUntilAria":`账号冻结申诉截止时间`,"account.freezeAppealURL":`申诉链接`,"account.freezeAppealURLAria":`账号冻结申诉链接`,"account.premiumMonths":`会员时长(月)`,"account.premiumMonthsAria":`设置会员时长,单位月`,"account.setPremium":`设置会员`,"account.clearPremium":`取消会员`,"account.starsAmount":`赠送 Stars 数量`,"account.starsAmountAria":`设置要赠送的 Stars 数量`,"account.grantStars":`赠送 Stars`,"account.setVerified":`设置认证`,"account.clearVerified":`取消认证`,"channel.pageTitle":`超级群与频道`,"channel.recentUpdated":`最近更新`,"channel.currentPage":`当前页实体`,"channel.megagroups":`超级群`,"channel.broadcasts":`频道`,"channel.verifiedCount":`已认证`,"channel.searchPlaceholder":`频道 ID / 用户名 / 标题`,"channel.channelID":`频道 ID`,"channel.kind":`类型`,"channel.title":`标题`,"channel.pts":`PTS`,"channel.detailProfile":`频道档案`,"channel.loadingDetail":`加载频道详情`,"channel.creator":`创建者 {id}`,"channel.governance":`治理状态`,"channel.governanceValue":`封禁 {banned} / 踢出 {kicked}`,"channel.flags":`频道标记`,"channel.rawRow":`频道原始行`,"channel.rawRowText":`数据库只读快照`,"channel.actionDock":`频道操作`,"channel.setVerified":`设置认证`,"channel.clearVerified":`取消认证`,"channel.kind.broadcast":`频道`,"channel.kind.forum":`超级群/论坛`,"channel.kind.megagroup":`超级群`,"channel.kind.generic":`频道/群`,"route.bots":`机器人`,"route.botsSubtitle":`控制台 / 机器人`,"layout.bots":`机器人`,"bots.pageTitle":`机器人`,"bots.queryResults":`查询结果`,"bots.recent":`最近创建的机器人`,"bots.currentPage":`当前页机器人`,"bots.banned":`已封禁`,"bots.active":`正常`,"bots.createTitle":`创建系统机器人`,"bots.createHint":`为指定用户创建机器人账号。确认后 token 只显示一次。`,"bots.ownerUserID":`所属用户 ID`,"bots.name":`显示名称`,"bots.namePlaceholder":`例如:服务机器人`,"bots.username":`用户名`,"bots.usernameHint":`用户名需 5-32 个字符,且以 bot 结尾。`,"bots.create":`创建机器人`,"bots.searchPlaceholder":`机器人 ID / 用户名`,"bots.botID":`机器人 ID`,"bots.owner":`所属用户`,"bots.status":`状态`,"bots.detailTitle":`机器人 #{id}`,"bots.profile":`机器人档案`,"bots.loadingDetail":`加载机器人详情`,"bots.unnamed":`未命名机器人`,"bots.restriction":`限制状态`,"bots.actionDock":`机器人操作`,"bots.banUntil":`封禁至`,"bots.ban":`封禁机器人`,"bots.updateBan":`更新封禁`,"bots.unban":`解封机器人`,"bots.type":`类型`,"bots.system":`系统`,"bots.user":`用户`,"bots.delete":`删除机器人`,"bots.deleteHint":`永久删除该用户创建的机器人并使其 token 失效。此操作不可撤销。`,"bots.systemHint":`系统内置机器人不可删除。`,"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.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка…`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтверждён`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.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":`中文`,"language.ru":`RU`,"theme.switchToDark":`Тёмная тема`,"theme.switchToLight":`Светлая тема`,"login.heading":`Панель администратора`,"login.body":`Введите учётные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход…`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, Premium, подтверждение, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, число участников, статус подтверждения.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтверждён`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звёзд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество Звёзд`,"account.starsAmountAria":`Указать количество начисляемых Звёзд`,"account.grantStars":`Начислить Звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновлённые`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждённые`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"route.bots":`Боты`,"route.botsSubtitle":`Консоль / Боты`,"layout.bots":`Боты`,"bots.pageTitle":`Боты`,"bots.queryResults":`Результаты поиска`,"bots.recent":`Недавно созданные боты`,"bots.currentPage":`Боты на странице`,"bots.banned":`Забанен`,"bots.active":`Активен`,"bots.createTitle":`Создать системного бота`,"bots.createHint":`Создаёт бота, принадлежащего указанному пользователю. Токен показывается один раз после подтверждения.`,"bots.ownerUserID":`ID владельца`,"bots.name":`Отображаемое имя`,"bots.namePlaceholder":`например, Service Bot`,"bots.username":`Имя пользователя`,"bots.usernameHint":`Имя пользователя: 5–32 символа, обязательно оканчивается на «bot».`,"bots.create":`Создать бота`,"bots.searchPlaceholder":`ID бота / имя пользователя`,"bots.botID":`ID бота`,"bots.owner":`Владелец`,"bots.status":`Статус`,"bots.detailTitle":`Бот #{id}`,"bots.profile":`Профиль бота`,"bots.loadingDetail":`Загрузка данных бота`,"bots.unnamed":`Без имени`,"bots.restriction":`Ограничение`,"bots.actionDock":`Действия с ботом`,"bots.banUntil":`Забанить до`,"bots.ban":`Забанить бота`,"bots.updateBan":`Обновить бан`,"bots.unban":`Разбанить бота`,"bots.type":`Тип`,"bots.system":`Системный`,"bots.user":`Пользовательский`,"bots.delete":`Удалить бота`,"bots.deleteHint":`Безвозвратно удаляет созданного пользователем бота и аннулирует его токен. Действие необратимо.`,"bots.systemHint":`Системные боты встроены и не могут быть удалены.`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звёздных подарков`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звёздного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звёздах`,"gifts.convertStars":`Звёзд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звёздные подарки ещё не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звёздах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Отозвать все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтверждённые`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Повторить тестовый запуск`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},we=(0,g.createContext)(null);function Te({children:e}){let[t,n]=(0,g.useState)(()=>Oe());(0,g.useEffect)(()=>{try{localStorage.setItem(Se,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=De(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>De(t,e,n)}),[t]);return(0,U.jsx)(we.Provider,{value:r,children:e})}function W(){let e=(0,g.useContext)(we);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function Ee(){let{lang:e,setLang:t,t:n}=W();return(0,U.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,U.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function De(e,t,n){let r=Ce[e][t]??Ce.en[t]??t;return n?r.replace(/\{(\w+)\}/g,(e,t)=>String(n[t]??``)):r}function Oe(){try{let e=ke(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=ke(localStorage.getItem(Se));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=ke(t);if(e)return e}return`en`}function ke(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:t===`ru`||t.startsWith(`ru-`)?`ru`:null}function Ae(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function je(e,t){return e.startsWith(`/accounts`)?t(`route.accounts`):e.startsWith(`/channels`)?t(`route.channels`):e.startsWith(`/bots`)?t(`route.bots`):e.startsWith(`/messages`)?t(`route.messages`):e.startsWith(`/gifts`)?t(`route.gifts`):t(`route.dashboard`)}function Me(e,t){return e.startsWith(`/accounts`)?t(`route.accountsSubtitle`):e.startsWith(`/channels`)?t(`route.channelsSubtitle`):e.startsWith(`/bots`)?t(`route.botsSubtitle`):e.startsWith(`/messages`)?t(`route.messagesSubtitle`):e.startsWith(`/gifts`)?t(`route.giftsSubtitle`):t(`route.dashboardSubtitle`)}var Ne=`telesrv.admin.theme`,Pe=(0,g.createContext)(null);function Fe(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Ie({children:e}){let[t,n]=(0,g.useState)(()=>ze());(0,g.useEffect)(()=>{Fe(t);try{localStorage.setItem(Ne,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(Ne)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,U.jsx)(Pe.Provider,{value:a,children:e})}function Le(){let e=(0,g.useContext)(Pe);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function Re(){let{theme:e,toggleTheme:t}=Le(),{t:n}=W(),r=n(e===`light`?`theme.switchToDark`:`theme.switchToLight`);return(0,U.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":r,title:r,children:e===`dark`?(0,U.jsx)(ve,{size:16}):(0,U.jsx)(ue,{size:16})})}function ze(){try{let e=localStorage.getItem(Ne);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}function G({href:e,navigate:t,className:n,children:r}){return(0,U.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Be(){let{t:e}=W();return(0,U.jsxs)(`div`,{className:`boot-screen`,children:[(0,U.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,U.jsx)(`div`,{className:`loader-bar`})]})}function Ve({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=W(),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,U.jsxs)(`div`,{className:`shell`,children:[(0,U.jsxs)(`aside`,{className:`sidebar`,children:[(0,U.jsxs)(G,{className:`brand`,href:`/`,navigate:n,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,U.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,U.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,U.jsx)(He,{icon:(0,U.jsx)(se,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(V,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(he,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(N,{size:16}),href:`/bots`,route:t,navigate:n,children:a(`layout.bots`)}),(0,U.jsx)(He,{icon:(0,U.jsx)(ie,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,U.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,U.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,U.jsx)(le,{size:16}),(0,U.jsx)(`span`,{children:a(`layout.messages`)}),(0,U.jsx)(I,{className:`nav-section-chevron`,size:15})]}),s&&(0,U.jsxs)(`div`,{className:`nav-children`,children:[(0,U.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,U.jsx)(He,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,U.jsxs)(`div`,{className:`sidebar-status`,children:[(0,U.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(me,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,U.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(R,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,U.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(ge,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,U.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,U.jsxs)(`div`,{className:`workspace`,children:[(0,U.jsxs)(`header`,{className:`topbar`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:Me(t.path,a)}),(0,U.jsx)(`h1`,{children:je(t.path,a)})]}),(0,U.jsxs)(`div`,{className:`topbar-actions`,children:[(0,U.jsx)(Re,{}),(0,U.jsx)(Ee,{}),(0,U.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,U.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,U.jsx)(ce,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,U.jsx)(`main`,{className:`content`,children:i})]})]})}function He({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,U.jsxs)(G,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,U.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,U.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 K(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function qe(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function q(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function Je(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 Ye({title:e,eyebrow:t,children:n,actions:r}){return(0,U.jsxs)(`div`,{className:`page-frame`,children:[(0,U.jsxs)(`div`,{className:`page-title-row`,children:[(0,U.jsxs)(`div`,{children:[t&&(0,U.jsx)(`div`,{className:`eyebrow`,children:t}),(0,U.jsx)(`h2`,{children:e})]}),r&&(0,U.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Xe({children:e}){return(0,U.jsx)(`div`,{className:`query-panel`,children:e})}function Ze({main:e,side:t}){return(0,U.jsxs)(`div`,{className:`split-layout`,children:[(0,U.jsx)(`div`,{className:`split-main`,children:e}),(0,U.jsx)(`aside`,{className:`split-side`,children:t})]})}function Qe({title:e,text:t,action:n}){return(0,U.jsxs)(`div`,{className:`section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`h2`,{children:e}),t&&(0,U.jsx)(`p`,{children:t})]}),n&&(0,U.jsx)(`div`,{className:`section-action`,children:n})]})}function $e({children:e}){return(0,U.jsxs)(`div`,{className:`alert`,children:[(0,U.jsx)(O,{size:16}),` `,(0,U.jsx)(`span`,{children:e})]})}function J({children:e,tone:t=`neutral`}){return(0,U.jsx)(`span`,{className:`badge ${t}`,children:e})}function et({label:e,value:t,tone:n}){return(0,U.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{children:t})]})}function tt({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,U.jsxs)(`div`,{className:`metric ${n}`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Y({label:e,value:t,mono:n=!1}){return(0,U.jsxs)(`div`,{className:`summary-item`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function nt({rows:e}){let{t}=W();return(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`audit.id`)}),(0,U.jsx)(`th`,{children:t(`audit.commandID`)}),(0,U.jsx)(`th`,{children:t(`audit.action`)}),(0,U.jsx)(`th`,{children:t(`audit.actor`)}),(0,U.jsx)(`th`,{children:t(`audit.status`)}),(0,U.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,U.jsx)(`th`,{children:t(`audit.reason`)}),(0,U.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[e.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,U.jsx)(`td`,{children:e.Action}),(0,U.jsx)(`td`,{children:e.Actor}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,U.jsx)(`td`,{children:K(e.CreatedAt)})]},e.ID)),e.length===0&&(0,U.jsx)(rt,{colSpan:8})]})]})})}function rt({colSpan:e}){let{t}=W();return(0,U.jsx)(`tr`,{children:(0,U.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function it({label:e}){return(0,U.jsx)(`section`,{className:`surface`,children:(0,U.jsx)(`div`,{className:`loading-line`,children:e})})}function at({value:e}){return(0,U.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function ot({onLogin:e}){let{t}=W(),[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,U.jsx)(`main`,{className:`login-page`,children:(0,U.jsxs)(`section`,{className:`login-panel`,children:[(0,U.jsxs)(`div`,{className:`login-head`,children:[(0,U.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,U.jsxs)(`div`,{className:`login-head-actions`,children:[(0,U.jsx)(Re,{}),(0,U.jsx)(Ee,{}),(0,U.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,U.jsxs)(`div`,{className:`login-copy`,children:[(0,U.jsx)(`h1`,{children:t(`login.heading`)}),(0,U.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,U.jsx)($e,{children:i}),(0,U.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:t(`login.secret`)}),(0,U.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,U.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var st=m();function ct({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=W(),[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,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,st.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,U.jsx)(`h2`,{children:e})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,U.jsx)(H,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`div`,{className:`command-steps`,children:[(0,U.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,U.jsx)(`span`,{children:`1`}),(0,U.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`2`}),(0,U.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`3`}),(0,U.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:s(`action.reason`)}),(0,U.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,U.jsxs)(`div`,{className:`command-preview`,children:[(0,U.jsxs)(`div`,{className:`preview-head`,children:[(0,U.jsx)(ne,{size:14}),` `,s(`action.requestPreview`)]}),(0,U.jsx)(at,{value:JSON.stringify(T,null,2)})]}),m&&(0,U.jsx)($e,{children:m}),f&&(0,U.jsxs)(`div`,{className:`result-box`,children:[(0,U.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,U.jsx)(O,{size:16}):(0,U.jsx)(k,{size:16}),(0,U.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.commandID`)}),(0,U.jsx)(`strong`,{children:f.command_id})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.status`)}),(0,U.jsx)(`strong`,{children:f.status})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.dryRun`)}),(0,U.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,U.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,U.jsx)(at,{value:JSON.stringify(f.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(B,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,U.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,U.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function lt({rows:e,userID:t,onDone:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`authorization-block`,children:[(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:r(`auth.device`)}),(0,U.jsx)(`th`,{children:r(`auth.platform`)}),(0,U.jsx)(`th`,{children:r(`auth.ip`)}),(0,U.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,U.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,U.jsxs)(`tbody`,{children:[o.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,U.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,U.jsx)(`td`,{children:n.IP}),(0,U.jsx)(`td`,{children:K(n.ActiveAt)}),(0,U.jsx)(`td`,{className:`device-actions-cell`,children:(0,U.jsxs)(`div`,{className:`device-actions`,children:[(0,U.jsx)(ct,{label:r(`auth.revokeCurrent`),icon:(0,U.jsx)(ce,{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,U.jsx)(ct,{label:r(`auth.keepCurrent`),icon:(0,U.jsx)(he,{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,U.jsx)(rt,{colSpan:5})]})]})}),(0,U.jsx)(`div`,{className:`danger-zone`,children:(0,U.jsx)(ct,{label:r(`auth.revokeAll`),icon:(0,U.jsx)(P,{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}=W(),[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,U.jsx)($e,{children:a});if(!r)return(0,U.jsx)(it,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,U.jsx)(Ye,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,U.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:Ge(y)}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[We(y.Username)||n(`account.noUsername`),` · `,Ue(y.Phone)||n(`account.noPhone`)]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,U.jsx)(J,{tone:`good`,children:n(`account.premium`)}):(0,U.jsx)(J,{children:n(`account.notPremium`)}),r.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)}),y.Frozen?(0,U.jsx)(J,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,U.jsx)(J,{children:n(`account.accountActive`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,U.jsx)(Y,{label:n(`account.lastActive`),value:qe(r.LastSeenAt)||`-`}),(0,U.jsx)(Y,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?qe(y.PremiumUntil):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,U.jsx)(Y,{label:n(`common.updatedAt`),value:K(y.UpdatedAt)||`-`}),(0,U.jsx)(Y,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,U.jsx)(Y,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,U.jsx)(Y,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.freezeSince`),value:r.Restriction.Since?K(r.Restriction.Since):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.freezeUntil`),value:r.Restriction.Until?K(r.Restriction.Until):n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,U.jsx)(Y,{label:n(`account.createdAt`),value:K(y.CreatedAt)||`-`})]}),r.About&&(0,U.jsx)(`p`,{className:`about-text`,children:r.About}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,U.jsx)(lt,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(nt,{rows:r.AuditLogs})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,U.jsx)(ct,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,U.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,U.jsx)(ct,{label:n(`account.unfreezeAccount`),icon:(0,U.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(ct,{label:n(`account.setPremium`),icon:(0,U.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:q(l)}),onDone:v}),(0,U.jsx)(ct,{label:n(`account.clearPremium`),icon:(0,U.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,U.jsx)(ct,{label:n(`account.grantStars`),icon:(0,U.jsx)(_e,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:q(d)}),onDone:v}),(0,U.jsx)(ct,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,U.jsx)(D,{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}=W(),[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,U.jsxs)(Ye,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,U.jsx)(fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,U.jsx)(tt,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,U.jsx)(tt,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,U.jsx)(tt,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(pe,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`account.userID`)}),(0,U.jsx)(`th`,{children:t(`account.phone`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`common.name`)}),(0,U.jsx)(`th`,{children:t(`common.device`)}),(0,U.jsx)(`th`,{children:t(`account.lastActive`)}),(0,U.jsx)(`th`,{children:t(`account.premium`)}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`account.frozen`)}),(0,U.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:Ue(n.Phone)}),(0,U.jsx)(`td`,{children:We(n.Username)}),(0,U.jsx)(`td`,{children:Ge(n)}),(0,U.jsx)(`td`,{children:n.DeviceCount}),(0,U.jsx)(`td`,{children:K(n.LastActiveAt)}),(0,U.jsx)(`td`,{children:n.PremiumUntil>0?(0,U.jsxs)(J,{tone:`good`,children:[t(`account.premium`),` `,qe(n.PremiumUntil)]}):(0,U.jsx)(J,{children:t(`common.none`)})}),(0,U.jsx)(`td`,{children:n.Verified?(0,U.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,U.jsx)(J,{children:t(`account.notVerified`)})}),(0,U.jsx)(`td`,{children:n.Frozen?(0,U.jsx)(J,{tone:`danger`,children:t(`account.frozen`)}):(0,U.jsx)(J,{children:t(`common.normal`)})}),(0,U.jsx)(`td`,{children:K(n.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,U.jsx)(rt,{colSpan:11})]})]})})]})}function ht({id:e,navigate:t}){let{t:n}=W(),[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,U.jsx)($e,{children:a});if(!r)return(0,U.jsx)(it,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,U.jsx)(Ye,{title:`${Ke(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,U.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[We(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{children:Ke(c,n)}),c.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)}),c.Deleted?(0,U.jsx)(J,{tone:`danger`,children:n(`common.deleted`)}):(0,U.jsx)(J,{children:n(`common.valid`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,U.jsx)(Y,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,U.jsx)(Y,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,U.jsx)(Y,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,U.jsx)(Y,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,U.jsx)(Y,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,U.jsx)(Y,{label:n(`account.createdAt`),value:qe(c.Date)||`-`}),(0,U.jsx)(Y,{label:n(`common.updatedAt`),value:K(c.UpdatedAt)||`-`})]}),c.About&&(0,U.jsx)(`p`,{className:`about-text`,children:c.About}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(nt,{rows:r.AuditLogs})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,U.jsx)(at,{value:r.ChannelJSON})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,U.jsx)(ct,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,U.jsx)(D,{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}=W(),[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,U.jsxs)(Ye,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,U.jsx)(fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,U.jsx)(tt,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,U.jsx)(tt,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,U.jsx)(tt,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(pe,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`channel.channelID`)}),(0,U.jsx)(`th`,{children:t(`channel.kind`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`channel.title`)}),(0,U.jsx)(`th`,{children:t(`common.members`)}),(0,U.jsx)(`th`,{children:t(`common.admins`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:Ke(n,t)}),(0,U.jsx)(`td`,{children:We(n.Username)}),(0,U.jsx)(`td`,{children:n.Title}),(0,U.jsx)(`td`,{children:n.ParticipantsCount}),(0,U.jsx)(`td`,{children:n.AdminsCount}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.Verified?(0,U.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,U.jsx)(J,{children:t(`account.notVerified`)})}),(0,U.jsx)(`td`,{children:K(n.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,U.jsx)(rt,{colSpan:10})]})]})})]})}function _t({id:e,navigate:t}){let{t:n}=W(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1);async function l(){c(!0),o(``);try{i(await x.bot(e))}catch(e){o(b(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{l()},[e]),a)return(0,U.jsx)($e,{children:a});if(!r)return(0,U.jsx)(it,{label:n(s?`bots.loadingDetail`:`account.waitingData`)});let u=r.Bot;return(0,U.jsx)(Ye,{title:n(`bots.detailTitle`,{id:u.ID}),eyebrow:n(`bots.profile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,U.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:u.FirstName||n(`bots.unnamed`)}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:We(u.Username)||n(`account.noUsername`)})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{tone:u.System?`warn`:`neutral`,children:u.System?n(`bots.system`):n(`bots.user`)}),u.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:n(`bots.botID`),value:String(u.ID),mono:!0}),(0,U.jsx)(Y,{label:n(`bots.owner`),value:u.OwnerUserID>0?`${u.OwnerUserID} ${We(r.OwnerUsername)}`.trim():n(`common.none`)}),(0,U.jsx)(Y,{label:n(`bots.type`),value:u.System?n(`bots.system`):n(`bots.user`)}),(0,U.jsx)(Y,{label:n(`common.updatedAt`),value:K(u.UpdatedAt)||`-`}),(0,U.jsx)(Y,{label:n(`account.createdAt`),value:K(u.CreatedAt)||`-`})]}),r.About&&(0,U.jsx)(`p`,{className:`about-text`,children:r.About}),r.Description&&r.Description.trim()!==r.About.trim()&&(0,U.jsx)(`p`,{className:`about-text`,children:r.Description}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(nt,{rows:r.AuditLogs})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`bots.actionDock`)}),(0,U.jsx)(`div`,{className:`action-stack`,children:(0,U.jsx)(ct,{label:u.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,U.jsx)(D,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:u.ID,verified:!u.Verified}),onDone:l})}),u.System?(0,U.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.systemHint`)}):(0,U.jsxs)(`div`,{className:`danger-zone`,children:[(0,U.jsx)(ct,{label:n(`bots.delete`),icon:(0,U.jsx)(ye,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:u.ID}),onDone:()=>t(`/bots`)}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.deleteHint`)})]})]})})})}function vt({navigate:e}){let{t}=W(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(0),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(``),[y,S]=(0,g.useState)(``);async function C(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&t.set(`before_id`,String(c));try{let e=await x.bots(t);s(e),l(e.next_before_id)}catch(e){p(b(e))}finally{d(!1)}}(0,g.useEffect)(()=>{C(!1)},[]);let w=o?.rows??[],T=w.filter(e=>e.Verified).length,E=w.filter(e=>e.System).length;return(0,U.jsxs)(Ye,{title:t(`bots.pageTitle`),eyebrow:o?.listing===!1?t(`bots.queryResults`):t(`bots.recent`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>C(!1),disabled:u,children:[(0,U.jsx)(fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`bots.currentPage`),value:String(w.length)}),(0,U.jsx)(tt,{label:t(`common.verified`),value:String(T),tone:`good`}),(0,U.jsx)(tt,{label:t(`bots.system`),value:String(E)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(`div`,{className:`section-head`,children:(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`h2`,{children:t(`bots.createTitle`)}),(0,U.jsx)(`p`,{children:t(`bots.createHint`)})]})}),(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.ownerUserID`)}),(0,U.jsx)(`input`,{value:m,onChange:e=>h(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.name`)}),(0,U.jsx)(`input`,{value:_,onChange:e=>v(e.target.value),placeholder:t(`bots.namePlaceholder`),maxLength:64})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.username`)}),(0,U.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:`my_service_bot`})]})]}),(0,U.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,U.jsx)(`span`,{className:`bot-create-note`,children:t(`bots.usernameHint`)}),(0,U.jsx)(ct,{label:t(`bots.create`),icon:(0,U.jsx)(de,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:q(m),name:_.trim(),username:y.trim().replace(/^@/,``)}),onDone:()=>C(!1)})]})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),C(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`bots.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(pe,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>C(!0),disabled:u,children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`bots.botID`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`common.name`)}),(0,U.jsx)(`th`,{children:t(`bots.owner`)}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`bots.type`)}),(0,U.jsx)(`th`,{children:t(`account.createdAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[w.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:We(n.Username)||`-`}),(0,U.jsx)(`td`,{children:n.FirstName||`-`}),(0,U.jsx)(`td`,{className:`mono`,children:n.OwnerUserID>0?n.OwnerUserID:`-`}),(0,U.jsx)(`td`,{children:n.Verified?(0,U.jsxs)(J,{tone:`good`,children:[(0,U.jsx)(D,{size:12}),` `,t(`common.verified`)]}):(0,U.jsx)(J,{children:t(`account.notVerified`)})}),(0,U.jsx)(`td`,{children:n.System?(0,U.jsx)(J,{tone:`warn`,children:t(`bots.system`)}):(0,U.jsx)(J,{children:t(`bots.user`)})}),(0,U.jsx)(`td`,{children:K(n.CreatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${n.ID}`),children:[(0,U.jsx)(N,{size:14}),` `,t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},n.ID)),w.length===0&&(0,U.jsx)(rt,{colSpan:8})]})]})})]})}function yt({navigate:e}){let{t}=W();return(0,U.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,U.jsxs)(`section`,{className:`overview-band`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,U.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,U.jsxs)(`div`,{className:`overview-metrics`,children:[(0,U.jsx)(et,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,U.jsx)(et,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,U.jsx)(et,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,U.jsxs)(`div`,{className:`command-grid`,children:[(0,U.jsx)(bt,{icon:(0,U.jsx)(V,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,U.jsx)(bt,{icon:(0,U.jsx)(he,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,U.jsx)(bt,{icon:(0,U.jsx)(le,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,U.jsxs)(`section`,{className:`work-strip`,children:[(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(k,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(oe,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(ee,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(ne,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function bt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,U.jsxs)(G,{className:`launcher`,href:r,navigate:i,children:[(0,U.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,U.jsxs)(`span`,{className:`launcher-copy`,children:[(0,U.jsx)(`strong`,{children:t}),(0,U.jsx)(`span`,{children:n})]}),(0,U.jsx)(L,{size:16})]})}function xt({channelID:e,msgID:t,navigate:n}){let{t:r}=W(),[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,U.jsx)($e,{children:o});if(!i)return(0,U.jsx)(it,{label:r(`common.loading`)});let l=i.Message;return(0,U.jsx)(Ye,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,U.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:qe(l.Date)})})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,U.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,U.jsx)(J,{children:r(`common.survived`)}),l.Pinned&&(0,U.jsx)(J,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,U.jsx)(J,{children:r(`messages.channelPost`)}),(0,U.jsxs)(J,{children:[`pts `,l.PTS]})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,U.jsx)(Y,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,U.jsx)(Y,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,U.jsx)(Y,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,U.jsx)(at,{value:i.MessageJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,U.jsx)(at,{value:i.ChannelJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.count`)}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.messageId`)}),(0,U.jsx)(`th`,{children:r(`common.sender`)}),(0,U.jsx)(`th`,{children:r(`common.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:e.MessageID}),(0,U.jsx)(`td`,{children:e.SenderUserID}),(0,U.jsx)(`td`,{children:qe(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,U.jsx)(rt,{colSpan:6})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.eventJson`)}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,U.jsx)(at,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,U.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function St({label:e,value:t,onChange:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(F,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:Ge(t)}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:We(t.Username)||Ue(t.Phone)||`-`})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,U.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,U.jsx)(`div`,{className:`picker-error`,children:u}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:Ge(e)}),(0,U.jsx)(`span`,{children:We(e.Username)||Ue(e.Phone)||`-`}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,U.jsx)(J,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,U.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function Ct({label:e,value:t,onChange:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(H,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(F,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:t.Title||`-`}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:We(t.Username)||Ke(t,r)})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,U.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,U.jsx)(`div`,{className:`picker-error`,children:u}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:e.Title||`-`}),(0,U.jsx)(`span`,{children:We(e.Username)||Ke(e,r)}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,U.jsx)(J,{children:Ke(e,r)})]},e.ID)),o.length===0&&!c?(0,U.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function wt({navigate:e}){let{t}=W(),[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,U.jsxs)(Ye,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,U.jsx)($e,{children:f}),(0,U.jsxs)(Xe,{children:[(0,U.jsx)(`div`,{className:`message-selector-grid single`,children:(0,U.jsx)(Ct,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,U.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(pe,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`messages.currentPage`),value:String(_.length)}),(0,U.jsx)(tt,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,U.jsx)(tt,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,U.jsx)(tt,{label:t(`messages.channelGroup`),value:n?`${n.Title||Ke(n,t)} (${n.ID})`:`-`})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`common.messageId`)}),(0,U.jsx)(`th`,{children:t(`common.time`)}),(0,U.jsx)(`th`,{children:t(`common.sender`)}),(0,U.jsx)(`th`,{children:`From Peer`}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.views`)}),(0,U.jsx)(`th`,{children:t(`common.status`)}),(0,U.jsx)(`th`,{children:t(`messages.body`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[_.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:qe(n.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,U.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.ViewsCount}),(0,U.jsx)(`td`,{children:n.Deleted?(0,U.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,U.jsx)(J,{tone:`warn`,children:t(`messages.pinned`)}):(0,U.jsx)(J,{children:t(`common.survived`)})}),(0,U.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,U.jsx)(rt,{colSpan:9})]})]})})]})}function Tt({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=W(),[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,U.jsx)($e,{children:o});if(!i)return(0,U.jsx)(it,{label:r(`common.loading`)});let l=i.Message;return(0,U.jsx)(Ye,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,U.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,U.jsx)(Ze,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:qe(l.Date)})})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,U.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,U.jsx)(J,{children:r(`common.survived`)}),(0,U.jsxs)(J,{children:[`pts `,l.PTS]}),(0,U.jsx)(J,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(Y,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,U.jsx)(Y,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,U.jsx)(Y,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,U.jsx)(Y,{label:r(`common.time`),value:qe(l.Date)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,U.jsx)(at,{value:i.MessageJSON})]}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,U.jsx)(at,{value:i.DialogJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,U.jsx)(at,{value:i.PrivateJSON})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.count`)}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:qe(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,U.jsx)(rt,{colSpan:4})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(Qe,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:r(`account.userID`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.status`)}),(0,U.jsx)(`th`,{children:r(`messages.attempts`)}),(0,U.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{children:e.TargetUserID}),(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.EventType}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.Attempts}),(0,U.jsx)(`td`,{children:K(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,U.jsx)(rt,{colSpan:7})]})]})})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,U.jsx)(ct,{label:r(`messages.deleteThis`),icon:(0,U.jsx)(ye,{size:15}),path:`/api/actions/delete-messages`,payload:()=>({owner_user_id:l.OwnerUserID,peer_id:l.PeerID,ids:[l.BoxID],revoke:!0}),onDone:c})]})})})}function Et({navigate:e}){let{t}=W(),[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,U.jsxs)(Ye,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,U.jsx)($e,{children:D}),(0,U.jsxs)(Xe,{children:[(0,U.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,U.jsx)(St,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,U.jsx)(St,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,U.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,U.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(pe,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,U.jsx)(L,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(tt,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,U.jsx)(tt,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,U.jsx)(tt,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,U.jsx)(tt,{label:t(`messages.ownerPeer`),value:n&&i?`${Ge(n)} / ${Ge(i)}`:`-`})]}),(0,U.jsxs)(`div`,{className:`operation-row`,children:[(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(ye,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,U.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,U.jsx)(ct,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:Je(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(ae,{size:15}),` `,t(`messages.clearHistory`)]}),(0,U.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,U.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,U.jsx)(ct,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:q(y),max_batches:q(C),just_clear:_,revoke:m})})]})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`common.messageId`)}),(0,U.jsx)(`th`,{children:t(`common.time`)}),(0,U.jsx)(`th`,{children:t(`common.sender`)}),(0,U.jsx)(`th`,{children:t(`messages.direction`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.status`)}),(0,U.jsx)(`th`,{children:t(`messages.body`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,U.jsx)(`td`,{children:qe(n.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,U.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.Deleted?(0,U.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):(0,U.jsx)(J,{children:t(`common.survived`)})}),(0,U.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,U.jsx)(L,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,U.jsx)(rt,{colSpan:8})]})]})})]})}var Dt=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 B(e){"@babel/helpers - typeof";return B=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},B(e)}var de=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 de.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}(),U=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Se=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=U.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Ce=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return Se(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=Ce.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=je(c.s),M=je(b),N=(e-y)/(v-y);Ae(r,ke(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 Ae(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 je(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 Me(){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!==Ee&&(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 Ne(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,De(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 Pe(){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]=Ve.newElement()),a[r][0]=e,a[r][1]=t},He.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)},He.prototype.reverse=function(){var e=new He;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 qe(e){"@babel/helpers - typeof";return qe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},qe(e)}var q={},Je=`__[STANDALONE]__`,Ye=`__[ANIMATIONDATA]__`,Xe=``;function Ze(e){s(e)}function Qe(){Je===!0?H.searchAnimations(Ye,Je,Xe):H.searchAnimations()}function $e(e){re(e)}function J(e){ue(e)}function et(e){return Je===!0&&(e.animationData=JSON.parse(Ye)),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 Y(){return typeof navigator<`u`}function nt(e,t){e===`expressions`&&ae(t)}function rt(e){switch(e){case`propertyFactory`:return G;case`shapePropertyFactory`:return Ke;case`matrix`:return K;default:return null}}q.play=H.play,q.pause=H.pause,q.setLocationHref=Ze,q.togglePause=H.togglePause,q.setSpeed=H.setSpeed,q.setDirection=H.setDirection,q.stop=H.stop,q.searchAnimations=Qe,q.registerAnimation=H.registerAnimation,q.loadAnimation=et,q.setSubframeRendering=$e,q.resize=H.resize,q.goToAndStop=H.goToAndStop,q.destroy=H.destroy,q.setQuality=tt,q.inBrowser=Y,q.installPlugin=nt,q.freeze=H.freeze,q.unfreeze=H.unfreeze,q.setVolume=H.setVolume,q.mute=H.mute,q.unmute=H.unmute,q.getRegisteredAnimations=H.getRegisteredAnimations,q.useWebWorker=a,q.setIDPrefix=J,q.__getFactory=rt,q.version=`5.13.0`;function it(){document.readyState===`complete`&&(clearInterval(lt),Qe())}function at(e){for(var t=ot.split(`&`),n=0;n=1?a.push({s:e-1,e:t-1}):(a.push({s:e,e:1}),a.push({s:0,e:t-1}));var o=[],s,c=a.length,l;for(s=0;sr+n)){var u=l.s*i<=r?0:(l.s*i-r)/n,d=l.e*i>=r+n?1:(l.e*i-r)/n;o.push([u,d])}return o.length||o.push([0,0]),o},ft.prototype.releasePathsData=function(e){var t,n=e.length;for(t=0;t1?1+r:this.s.v<0?0+r:this.s.v+r,n=this.e.v>1?1+r:this.e.v<0?0+r:this.e.v+r,t>n){var i=t;t=n,n=i}t=Math.round(t*1e4)*1e-4,n=Math.round(n*1e4)*1e-4,this.sValue=t,this.eValue=n}else t=this.sValue,n=this.eValue;var a,o,s=this.shapes.length,c,l,u,d,f,p=0;if(n===t)for(o=0;o=0;--o)if(h=this.shapes[o],h.shape._mdf){for(g=h.localShapeCollection,g.releaseShapes(),this.m===2&&s>1?(b=this.calculateShapeEdges(t,n,h.totalShapeLength,y,p),y+=h.totalShapeLength):b=[[_,v]],l=b.length,c=0;c=1?m.push({s:h.totalShapeLength*(_-1),e:h.totalShapeLength*(v-1)}):(m.push({s:h.totalShapeLength*_,e:h.totalShapeLength}),m.push({s:0,e:h.totalShapeLength*(v-1)}));var x=this.addShapes(h,m[0]);if(m[0].s!==m[0].e){if(m.length>1)if(h.shape.paths.shapes[h.shape.paths._length-1].c){var S=x.pop();this.addPaths(x,g),x=this.addShapes(h,m[1],S)}else this.addPaths(x,g),x=this.addShapes(h,m[1]);this.addPaths(x,g)}}h.shape.paths=g}}else if(this._mdf)for(o=0;ot.e){n.c=!1;break}else t.s<=l&&t.e>=l+u.addedLength?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[s],i[a].v[s],n,d,g),g=!1):(p=W.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=W.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=G.getProp(e,t.p.x,0,0,this),this.py=G.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=G.getProp(e,t.p.z,0,0,this))):this.p=G.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=G.getProp(e,t.rx,0,D,this),this.ry=G.getProp(e,t.ry,0,D,this),this.rz=G.getProp(e,t.rz,0,D,this),t.or.k[0].ti){var r,i=t.or.k.length;for(r=0;r0;)--n,this._elements.unshift(t[n]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},ht.prototype.resetElements=function(e){var t,n=e.length;for(t=0;t0?Math.floor(f):Math.ceil(f),h=this.pMatrix.props,g=this.rMatrix.props,_=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v=0;if(f>0){for(;vm;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),--v;p&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-p,!0),v-=p)}r=this.data.m===1?0:this._currentCopies-1,i=this.data.m===1?1:-1,a=this._currentCopies;for(var y,b;a;){if(t=this.elemsData[r].it,n=t[t.length-1].transform.mProps.v.props,b=n.length,t[t.length-1].transform.mProps._mdf=!0,t[t.length-1].transform.op._mdf=!0,t[t.length-1].transform.op.v=this._currentCopies===1?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),v!==0){for((r!==0&&i===1||r!==this._currentCopies-1&&i===-1)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(_[0],_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15]),this.matrix.transform(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8],h[9],h[10],h[11],h[12],h[13],h[14],h[15]),y=0;y0&&r<1?[t]:[]:[t-r,t+r].filter(function(e){return e>0&&e<1})},wt.prototype.split=function(e){if(e<=0)return[Ct(this.points[0]),this];if(e>=1)return[this,Ct(this.points[this.points.length-1])];var t=bt(this.points[0],this.points[1],e),n=bt(this.points[1],this.points[2],e),r=bt(this.points[2],this.points[3],e),i=bt(t,n,e),a=bt(n,r,e),o=bt(i,a,e);return[new wt(this.points[0],t,i,o,!0),new wt(o,a,r,this.points[3],!0)]};function Tt(e,t){var n=e.points[0][t],r=e.points[e.points.length-1][t];if(n>r){var i=r;r=n,n=i}for(var a=xt(3*e.a[t],2*e.b[t],e.c[t]),o=0;o0&&a[o]<1){var s=e.point(a[o])[t];sr&&(r=s)}return{min:n,max:r}}wt.prototype.bounds=function(){return{x:Tt(this,0),y:Tt(this,1)}},wt.prototype.boundingBox=function(){var e=this.bounds();return{left:e.x.min,right:e.x.max,top:e.y.min,bottom:e.y.max,width:e.x.max-e.x.min,height:e.y.max-e.y.min,cx:(e.x.max+e.x.min)/2,cy:(e.y.max+e.y.min)/2}};function Et(e,t,n){var r=e.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:e,t:(t+n)/2,t1:t,t2:n}}function Dt(e){var t=e.bez.split(.5);return[Et(t[0],e.t1,e.t),Et(t[1],e.t,e.t2)]}function Ot(e,t){return Math.abs(e.cx-t.cx)*2=a||e.width<=r&&e.height<=r&&t.width<=r&&t.height<=r){i.push([e.t,t.t]);return}var o=Dt(e),s=Dt(t);kt(o[0],s[0],n+1,r,i,a),kt(o[0],s[1],n+1,r,i,a),kt(o[1],s[0],n+1,r,i,a),kt(o[1],s[1],n+1,r,i,a)}}wt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return kt(Et(this,0,1),Et(e,0,1),0,t,r,n),r},wt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new wt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},wt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new wt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function At(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function jt(e,t,n,r){var i=[e[0],e[1],1],a=[t[0],t[1],1],o=[n[0],n[1],1],s=[r[0],r[1],1],c=At(At(i,a),At(o,s));return vt(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function X(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Mt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Nt(e,t){return _t(e[0],t[0])&&_t(e[1],t[1])}function Pt(){}u([dt],Pt),Pt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=G.getProp(e,t.s,0,null,this),this.frequency=G.getProp(e,t.r,0,null,this),this.pointsType=G.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function Ft(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function It(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function Lt(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=It(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Rt(e,t,n,r,i,a,o){var s=Lt(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;Ft(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function zt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ut(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Gt(e){for(var t,n=1;n1&&(t=Wt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Kt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Vt(e,t)];if(n.length===1||_t(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Vt(r,t),Vt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Vt(r,t),Vt(o,t),Vt(i,t)]}function qt(){}u([dt],qt),qt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=G.getProp(e,t.a,0,null,this),this.miterLimit=G.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},qt.prototype.processPath=function(e,t,n,r){var i=Ue.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=wt.shapeSegmentInverted(e,o),l.push(Kt(c,t));l=Gt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function Xt(e){this.animationData=e}Xt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Zt(e){return new Xt(e)}function Qt(){}Qt.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},mn.prototype.show=function(){},mn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},mn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},mn.prototype.resume=function(){this._canPlay=!0},mn.prototype.setRate=function(e){this.audio.rate(e)},mn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},mn.prototype.getBaseElement=function(){return null},mn.prototype.destroy=function(){},mn.prototype.sourceRectAtTime=function(){},mn.prototype.initExpressions=function(){};function hn(){}hn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},hn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},hn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},hn.prototype.createAudio=function(e){return new mn(e,this.globalData,this)},hn.prototype.createFootage=function(e){return new pn(e,this.globalData,this)},hn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}vn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},vn.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},vn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var yn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=z(`filter`);return n.setAttribute(`id`,e),t!==!0&&(n.setAttribute(`filterUnits`,`objectBoundingBox`),n.setAttribute(`x`,`0%`),n.setAttribute(`y`,`0%`),n.setAttribute(`width`,`100%`),n.setAttribute(`height`,`100%`)),n}function n(){var e=z(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),bn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),xn={},Sn=`filter_result_`;function Cn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=I(),a=yn.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Rn.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function Gn(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([dn,_n,wn,kn,Tn,fn,En],Gn),Gn.prototype.initSecondaryElement=function(){},Gn.prototype.identityMatrix=new K,Gn.prototype.buildExpressionInterface=function(){},Gn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Gn.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},Gn.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},qn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},qn.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},qn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Yt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Yt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Yt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Yt.isVariationSelector(i)&&(o=!0)):Yt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},qn.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=Jt(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var F,I,L=[];for(g=0;g<_;g+=1){for(N=M[g],N.a.sc&&(e.strokeColorAnim=!0),N.a.sw&&(e.strokeWidthAnim=!0),(N.a.fc||N.a.fh||N.a.fs||N.a.fb)&&(e.fillColorAnim=!0),I=0,F=N.s.b,i=0;i0?i=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?o=1-this.xe.v/100:s=1+this.xe.v/100;var c=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([Be],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=G.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=G.getProp;for(e=0;e=m+U||!x?(T=(m+U-g)/h.partialLength,ie=b.point[0]+(h.point[0]-b.point[0])*T,ae=b.point[1]+(h.point[1]-b.point[1])*T,a.translate(-n[0]*f[u].an*.005,-(n[1]*A)*.01),_=!1):x&&(g+=h.partialLength,v+=1,v>=x.length&&(v=0,y+=1,S[y]?x=S[y].points:D.v.c?(v=0,y=0,x=S[y].points):(g-=h.partialLength,x=null)),x&&(b=h,h=x[v],C=h.partialLength));re=f[u].an/2-f[u].add,a.translate(-re,0,0)}else re=f[u].an/2-f[u].add,a.translate(-re,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:z(s?`g`:`text`),b<=e){if(c.setAttribute(`stroke-linecap`,`butt`),c.setAttribute(`stroke-linejoin`,`round`),c.setAttribute(`stroke-miterlimit`,`4`),this.textSpans[e].span=c,s){var S=z(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new ir(x.data,this.globalData,this);else{var w=Qn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Gn(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},rr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&lr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new dr(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=$t(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new fr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(gn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=lr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=mr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new 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=Ke.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),ut.registerModifier(`tm`,ft),ut.registerModifier(`pb`,pt),ut.registerModifier(`rp`,ht),ut.registerModifier(`rd`,gt),ut.registerModifier(`zz`,Pt),ut.registerModifier(`op`,qt),q}))}))(),1),Ot=0,kt=e=>`${e}-${++Ot}`,At=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function jt(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:kt(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function Mt(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=At[e.length%At.length];return{key:kt(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var Nt=e=>jt([X(e,0),X(e,1)]),Pt=()=>{let e=Mt([]);return jt([e,Mt([e])])};function Ft({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=Dt.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,U.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function It({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,U.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,U.jsx)(Ft,{data:n,compact:!0}):(0,U.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,U.jsx)(A,{className:`spin`,size:15})})}async function Lt(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 Rt=e=>Number.parseInt(e.replace(`#`,``),16),zt=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function Bt({gift:e,onClose:t,onPublished:n}){let{t:r}=W(),[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)(()=>Nt(`model`)),[D,O]=(0,g.useState)(()=>Nt(`pattern`)),[M,N]=(0,g.useState)(Pt);(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 Lt(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function ee(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||M.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=M.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:M.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:Rt(e.center),edge_color:Rt(e.edge),pattern_color:Rt(e.pattern),text_color:Rt(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}async function R(){l(!0),d(``),p(null);try{p(await x.publishGiftCollectibles(e.GiftID,ee(!1)))}catch(e){d(b(e))}finally{l(!1)}}async function ne(){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,U.jsxs)(`section`,{className:`collectible-section`,children:[(0,U.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,U.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,U.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,U.jsxs)(J,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(jt([...t,X(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,U.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,U.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,U.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,U.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`common.name`)}),(0,U.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,U.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,U.jsxs)(`label`,{className:`collectible-file`,children:[(0,U.jsx)(`span`,{children:r(`gifts.animation`)}),(0,U.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,U.jsxs)(`em`,{children:[(0,U.jsx)(te,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,U.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,U.jsx)(Ft,{data:i.animation,compact:!0}):(0,U.jsx)(j,{size:16})}),(0,U.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(jt(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,U.jsx)(ye,{size:14})}),i.fileError&&(0,U.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,st.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,U.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,U.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,U.jsx)(H,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,U.jsxs)(`div`,{className:`collectible-loading`,children:[(0,U.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,U.jsxs)(`section`,{className:`collectible-active`,children:[(0,U.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(re,{size:18}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,U.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,U.jsx)(J,{tone:`good`,children:r(`collectibles.published`)})]}),(0,U.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,U.jsxs)(`article`,{children:[(0,U.jsx)(It,{giftID:e.GiftID,attribute:t}),(0,U.jsxs)(`div`,{children:[(0,U.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,U.jsx)(J,{children:`crafted`})]}),(0,U.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,zt(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,U.jsxs)(`article`,{children:[(0,U.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,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e.name}),(0,U.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,zt(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,U.jsxs)(`div`,{className:`collectible-empty`,children:[(0,U.jsx)(re,{size:22}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,U.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,U.jsxs)(`section`,{className:`collectible-definition`,children:[(0,U.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,U.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,U.jsx)(`span`,{children:`TGS`}),(0,U.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,U.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,U.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.reason`)}),(0,U.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,U.jsxs)(`section`,{className:`collectible-section`,children:[(0,U.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,U.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,U.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,U.jsxs)(J,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N(jt([...M,Mt(M)])),F()},children:[(0,U.jsx)(de,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,U.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,U.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,U.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`common.name`)}),(0,U.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,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,U.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,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,U.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,U.jsxs)(`label`,{className:`collectible-color`,children:[(0,U.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,U.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,U.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,U.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length<=2,onClick:()=>{N(jt(M.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,U.jsx)(ye,{size:14})})]},e.key))})]})]}),u&&(0,U.jsx)($e,{children:u}),f&&(0,U.jsxs)(`div`,{className:`gift-validation`,children:[(0,U.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,U.jsx)(k,{size:17}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,U.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,U.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:R,disabled:c,children:[c?(0,U.jsx)(A,{className:`spin`,size:15}):(0,U.jsx)(he,{size:15}),r(`gifts.validate`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:ne,disabled:c||!f,children:[(0,U.jsx)(be,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function Vt(e){return e.model_count+e.pattern_count+e.backdrop_count}function Ht(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function Ut({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=Dt.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,U.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,U.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,U.jsx)(`span`,{children:s})}),(0,U.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,U.jsx)(z,{size:14}):(0,U.jsx)(B,{size:14})})]})}function Wt({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=Dt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,U.jsx)(`div`,{className:`gift-animation-shell`,children:(0,U.jsx)(`div`,{className:`gift-animation`,ref:t})})}function Gt(){let{t:e}=W(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,R]=(0,g.useState)(`50`),[ne,ie]=(0,g.useState)(`50`),[ae,oe]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[z,B]=(0,g.useState)(null),[me,ge]=(0,g.useState)(!1),[_e,ve]=(0,g.useState)(``),[ye,V]=(0,g.useState)(``);async function xe(){ve(``);try{n((await x.gifts()).Gifts??[])}catch(e){ve(b(e))}}(0,g.useEffect)(()=>{xe()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>V(b(e)))},[a,d,p.length]);let Se=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),Ce=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),we=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),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 Ee(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:ne,enabled:se,sort_order:Number(ae)})),r.set(`file`,l,l.name),r}function De(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:ne,enabled:se,sort_order:Number(ae),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function Oe(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),R(String(t.stars)),ie(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),B(null)}async function ke(){ge(!0),V(``),B(null);try{B(d===`official`?await x.importOfficialGift(De(!1)):await x.importGift(Ee(!1)))}catch(e){V(b(e))}finally{ge(!1)}}async function Ae(){if(z){ge(!0),V(``);try{d===`official`?await x.importOfficialGift(De(!0,z.command_id)):await x.importGift(Ee(!0,z.command_id)),B(null),u(null),F(`0`),L(``),C(``),await xe(),o(!1)}catch(e){V(b(e))}finally{ge(!1)}}}function je(){F(`0`),L(``),R(`50`),ie(`50`),oe(`0`),ce(!0),ue(``),u(null),B(null),V(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Me(e){F(e.GiftID),L(e.Title),R(String(e.Stars)),ie(String(e.ConvertStars)),oe(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),B(null),V(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,U.jsxs)(Ye,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>xe(),disabled:me,children:[(0,U.jsx)(fe,{size:15}),` `,e(`common.refresh`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,children:[(0,U.jsx)(de,{size:15}),` `,e(`gifts.add`)]})]}),children:[_e&&(0,U.jsx)($e,{children:_e}),(0,U.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,U.jsx)(tt,{label:e(`gifts.total`),value:String(t.length)}),(0,U.jsx)(tt,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,U.jsx)(tt,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,U.jsx)(tt,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,U.jsx)(Xe,{children:(0,U.jsxs)(`div`,{className:`toolbar`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,U.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Te.length,total:t.length})})]})}),(0,U.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:e(`gifts.animation`)}),(0,U.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,U.jsx)(`th`,{children:e(`gifts.title`)}),(0,U.jsx)(`th`,{children:e(`gifts.price`)}),(0,U.jsx)(`th`,{children:e(`gifts.source`)}),(0,U.jsx)(`th`,{children:e(`gifts.received`)}),(0,U.jsx)(`th`,{children:e(`common.status`)}),(0,U.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,U.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,U.jsxs)(`tbody`,{children:[Te.map(t=>(0,U.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,U.jsx)(`td`,{children:(0,U.jsx)(Ut,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,U.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,U.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,U.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(J,{children:t.SourceFormat}),(0,U.jsx)(`span`,{className:`gift-source-size`,children:Ht(t.AnimationSize)})]}),(0,U.jsx)(`td`,{children:t.ReceivedCount}),(0,U.jsx)(`td`,{children:(0,U.jsx)(J,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,U.jsx)(`td`,{children:K(t.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,U.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,U.jsx)(re,{size:13}),e(`collectibles.manage`)]}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Me(t),children:e(`gifts.replace`)}),(0,U.jsx)(ct,{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 xe()})]})})]},t.GiftID)),Te.length===0&&(0,U.jsx)(rt,{colSpan:9})]})]})}),a&&(0,st.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,U.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:me,"aria-label":e(`action.close`),children:(0,U.jsx)(H,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,U.jsxs)(`div`,{className:`command-steps`,children:[(0,U.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,U.jsx)(`span`,{children:`1`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${z?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`2`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${z?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`3`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,U.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,U.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),B(null)},children:e(`gifts.officialSource`)}),(0,U.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),B(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,U.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,U.jsxs)(`div`,{className:`gift-import-note`,children:[(0,U.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,U.jsx)(`span`,{children:p.length}),(0,U.jsx)(`span`,{children:`SHA-256`})]})]}),(0,U.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(pe,{size:15}),(0,U.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,U.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:we.length,total:p.length})})]}),(0,U.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,U.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,U.jsx)(`span`,{children:Ce[t]})]},t))}),(0,U.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[we.map(t=>{let n=t.source_gift_id===S;return(0,U.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>Oe(t),children:[(0,U.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,U.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,U.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,U.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,U.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,U.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:Vt(t)})})]}),(0,U.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,U.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,U.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),we.length===0&&(0,U.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),Se&&(0,U.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,U.jsx)(Wt,{sourceGiftID:Se.source_gift_id}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:Se.title||e(`gifts.officialUnnamed`,{id:Se.source_gift_id})}),(0,U.jsx)(`span`,{className:`mono`,children:Se.source_gift_id}),(0,U.jsxs)(`small`,{children:[Se.model_count,` `,e(`collectibles.models`),` · `,Se.pattern_count,` `,e(`collectibles.patterns`),` · `,Se.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,U.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,U.jsx)(`span`,{className:Se.can_upgrade?`yes`:`no`,children:Se.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,U.jsx)(`span`,{className:Se.can_craft?`craft`:`no`,children:Se.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),Se?.can_upgrade&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),B(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,U.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,U.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),B(null)}})]})]})]})]}):(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`div`,{className:`gift-import-note`,children:[(0,U.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,U.jsx)(`span`,{children:`TGS`}),(0,U.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,U.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),B(null)}}),(0,U.jsx)(`span`,{className:`gift-file-icon`,children:(0,U.jsx)(te,{size:22})}),(0,U.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,U.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,U.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,U.jsx)(`small`,{children:l?Ht(l.size):e(`gifts.fileHint`)})]}),(0,U.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,U.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.title`)}),(0,U.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.stars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{R(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:ne,onChange:e=>{ie(e.target.value),B(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,U.jsx)(`input`,{type:`number`,value:ae,onChange:e=>{oe(e.target.value),B(null)}})]})]}),(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:e(`gifts.reason`)}),(0,U.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),B(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),ye&&(0,U.jsx)($e,{children:ye}),z&&(0,U.jsxs)(`div`,{className:`gift-validation`,children:[(0,U.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,U.jsx)(k,{size:17}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,U.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,U.jsx)(`pre`,{children:JSON.stringify(z.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:me,children:e(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:ke,disabled:me,children:[me?(0,U.jsx)(A,{className:`spin`,size:15}):(0,U.jsx)(he,{size:15}),e(`gifts.validate`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Ae,disabled:me||!z,children:[(0,U.jsx)(be,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,U.jsx)(Bt,{gift:s,onClose:()=>c(null),onPublished:()=>void xe()})]})}function Kt({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1],i=e.path.match(/^\/bots\/(\d+)$/)?.[1];return n?(0,U.jsx)(ut,{id:Number(n),navigate:t}):r?(0,U.jsx)(ht,{id:Number(r),navigate:t}):i?(0,U.jsx)(_t,{id:Number(i),navigate:t}):e.path===`/accounts`?(0,U.jsx)(mt,{navigate:t}):e.path===`/channels`?(0,U.jsx)(gt,{navigate:t}):e.path===`/bots`?(0,U.jsx)(vt,{navigate:t}):e.path===`/gifts`?(0,U.jsx)(Gt,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,U.jsx)(Tt,{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,U.jsx)(xt,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,U.jsx)(wt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,U.jsx)(Et,{navigate:t}):(0,U.jsx)(yt,{navigate:t})}function qt(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>Ae());(0,g.useEffect)(()=>{let e=()=>r(Ae());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(Ae())};return e===void 0?(0,U.jsx)(Be,{}):e===null?(0,U.jsx)(ot,{onLogin:t}):(0,U.jsx)(Ve,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,U.jsx)(Kt,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,U.jsx)(g.StrictMode,{children:(0,U.jsx)(Ie,{children:(0,U.jsx)(Te,{children:(0,U.jsx)(qt,{})})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css b/cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css deleted file mode 100644 index a538e8da..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-BlWlOvtx.css +++ /dev/null @@ -1 +0,0 @@ -:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#eef1f5;--bg-accent:#e7ecf1;--panel:#fff;--panel-subtle:#f5f8fb;--panel-strong:#eef2f6;--surface-soft:#f2f7f6;--overlay:#18222f6b;--topbar-bg:#ffffffdb;--line:#e5eaf0;--line-strong:#d3dce4;--heading:#253040;--text:#333f4d;--text-soft:#45525f;--muted:#6d7885;--muted-2:#9aa4b1;--brand:#1f7d6f;--brand-strong:#196155;--brand-2:#3a6cae;--brand-tint:#e8f4f0;--brand-tint-border:#c8e2db;--brand-tint-text:#235d53;--good:#1f8a57;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a86a12;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#c0392b;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#1c2530;--sidebar-soft:#26313d;--sidebar-line:#313c4a;--sidebar-row:#232d38;--sidebar-text:#dbe3ec;--sidebar-muted:#8b98a8;--sidebar-faint:#7c8a9a;--sidebar-heading:#fff;--focus:#1f7d6f29;--shadow:0 12px 34px #1827381a;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #1f7d6f38;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#37a596;--brand-strong:#45b6a6;--brand-2:#6fa8e6;--brand-tint:#14322d;--brand-tint-border:#245349;--brand-tint-text:#7fd3c4;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#37a5963d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #37a59642}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow:var(--shadow-brand)}.brand-mark{color:#fff;background:var(--brand);border-radius:var(--radius-sm);border:1px solid #fff3;place-items:center;width:34px;height:34px;font-weight:800;display:grid}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border:1px solid var(--sidebar-line);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.language-switch{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:2px;display:inline-flex}.language-switch button{min-width:42px;min-height:24px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0 9px;font-weight:800;transition:color .14s,background-color .14s}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input{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{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.bot-create-fields{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.bot-create-fields .duration-field input{width:100%}.bot-create-actions{border-top:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;margin-top:14px;padding-top:14px;display:flex}.bot-create-note{color:var(--muted);font-size:12px;line-height:1.4}@media (width<=760px){.bot-create-fields{grid-template-columns:1fr}.bot-create-actions{flex-direction:column;align-items:stretch}}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:190px}.device-actions-cell{width:190px;min-width:190px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(82px,1fr));gap:6px;min-width:178px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:first-child{width:74px}.gift-table td{vertical-align:middle}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid,.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/assets/index-Bx7A77x9.js b/cmd/telesrv-admin/web/dist/assets/index-Bx7A77x9.js new file mode 100644 index 00000000..e36e2012 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-Bx7A77x9.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{ne=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?te(e):``}function ie(e){switch(e.tag){case 5:return te(e.type);case 16:return te(`Lazy`);case 13:return te(`Suspense`);case 19:return te(`SuspenseList`);case 0:case 2:case 15:return e=re(e.type,!1),e;case 11:return e=re(e.type.render,!1),e;case 1:return e=re(e.type,!0),e;default:return``}}function ae(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?ae(e.type)||`Memo`:t;case F:t=e._payload,e=e._init;try{return ae(e(t))}catch{}}return null}function oe(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 ae(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 se(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function ce(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function le(e){var t=ce(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 B(e){e._valueTracker||=le(e)}function ue(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=ce(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function V(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 de(e,t){var n=t.checked;return z({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function fe(e,t){var n=t.defaultValue==null?``:t.defaultValue,r=t.checked==null?t.defaultChecked:t.checked;n=se(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 pe(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function me(e,t){pe(e,t);var n=se(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`)?ge(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ge(e,t.type,se(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function he(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 ge(e,t,n){(t!==`number`||V(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var _e=Array.isArray;function ve(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=we.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Ee(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var De={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},Oe=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(De).forEach(function(e){Oe.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),De[t]=De[e]})});function ke(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||De.hasOwnProperty(e)&&De[e]?(``+t).trim():t+`px`}function Ae(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=ke(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var je=z({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 U(e,t){if(t){if(je[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,W=null,Ie=null;function Le(e){if(e=Ai(e)){if(typeof Fe!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Mi(t),Fe(e.stateNode,e.type,t))}}function Re(e){W?Ie?Ie.push(e):Ie=[e]:W=e}function ze(){if(W){var e=W,t=Ie;if(Ie=W=null,Le(e),t)for(e=0;e>>=0,e===0?32:31-(gt(e)/_t|0)|0}var yt=64,bt=4194304;function xt(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 St(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=xt(a))):r=xt(s)}else o=n&~i,o===0?a!==0&&(r=xt(a)):r=xt(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 Ot(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-ht(t),e[t]=n}function kt(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Wn),qn=` `,Jn=!1;function Yn(e,t){switch(e){case`keyup`:return Hn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Xn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Zn=!1;function Qn(e,t){switch(e){case`compositionend`:return Xn(t);case`keypress`:return t.which===32?(Jn=!0,qn):null;case`textInput`:return e=t.data,e===qn&&Jn?null:e;default:return null}}function $n(e,t){if(Zn)return e===`compositionend`||!Un&&Yn(e,t)?(e=pn(),fn=dn=un=null,Zn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=br(n)}}function Sr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Sr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Cr(){for(var e=window,t=V();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=V(e.document)}return t}function wr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}function Tr(e){var t=Cr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Sr(n.ownerDocument.documentElement,n)){if(r!==null&&wr(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),`selectionStart`in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=xr(n,a);var o=xr(n,r);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus==`function`&&n.focus(),n=0;n=document.documentMode,Dr=null,Or=null,kr=null,Ar=!1;function jr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ar||Dr==null||Dr!==V(r)||(r=Dr,`selectionStart`in r&&wr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),kr&&yr(kr,r)||(kr=r,r=ri(Or,`onSelect`),0Pi||(e.current=Ni[Pi],Ni[Pi]=null,Pi--)}function Li(e,t){Pi++,Ni[Pi]=e.current,e.current=t}var Ri={},zi=Fi(Ri),Bi=Fi(!1),Vi=Ri;function Hi(e,t){var n=e.type.contextTypes;if(!n)return Ri;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Ui(e){return e=e.childContextTypes,e!=null}function Wi(){Ii(Bi),Ii(zi)}function Gi(e,t,n){if(zi.current!==Ri)throw Error(r(168));Li(zi,t),Li(Bi,n)}function Ki(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,oe(e)||`Unknown`,a));return z({},n,i)}function qi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ri,Vi=zi.current,Li(zi,e),Li(Bi,Bi.current),!0}function Ji(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=Ki(e,t,Vi),i.__reactInternalMemoizedMergedChildContext=e,Ii(Bi),Ii(zi),Li(zi,e)):Ii(Bi),Li(Bi,n)}var Yi=null,Xi=!1,Zi=!1;function Qi(e){Yi===null?Yi=[e]:Yi.push(e)}function $i(e){Xi=!0,Qi(e)}function ea(){if(!Zi&&Yi!==null){Zi=!0;var e=0,t=Z;try{var n=Yi;for(Z=1;e>=o,i-=o,ca=1<<32-ht(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),ga&&ua(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),ga&&ua(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return ga&&ua(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),ga&&ua(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===F&&Aa(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=Oa(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=Oa(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case F:return l=i._init,_(e,r,l(i._payload),o)}if(_e(i))return h(e,r,i,o);if(R(i))return g(e,r,i,o);ka(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Ma=ja(!0),Na=ja(!1),Pa=Fi(null),Fa=null,Ia=null,La=null;function Ra(){La=Ia=Fa=null}function za(e){var t=Pa.current;Ii(Pa),e._currentValue=t}function Ba(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Va(e,t){Fa=e,La=Ia=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(js=!0),e.firstContext=null)}function Ha(e){var t=e._currentValue;if(La!==e)if(e={context:e,memoizedValue:t,next:null},Ia===null){if(Fa===null)throw Error(r(308));Ia=e,Fa.dependencies={lanes:0,firstContext:e}}else Ia=Ia.next=e;return t}var Ua=null;function Wa(e){Ua===null?Ua=[e]:Ua.push(e)}function Ga(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Wa(t)):(n.next=i.next,i.next=n),t.interleaved=n,Ka(e,r)}function Ka(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var qa=!1;function Ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ya(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xa(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Za(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Bc&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,Ka(e,n)}return i=r.interleaved,i===null?(t.next=t,Wa(r)):(t.next=i.next,i.next=t),r.interleaved=t,Ka(e,n)}function Qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,At(e,n)}}function $a(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function eo(e,t,n,r){var i=e.updateQueue;qa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=z({},d,f);break a;case 2:qa=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function to(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=_o.transition;_o.transition={};try{e(!1),t()}finally{Z=n,_o.transition=r}}function is(){return jo().memoizedState}function as(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ss(e))cs(t,n);else if(n=Ga(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),ls(n,t,r)}}function os(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ss(e))cs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Q(s,o)){var c=t.interleaved;c===null?(i.next=i,Wa(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ga(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),ls(n,t,r))}}function ss(e){var t=e.alternate;return e===yo||t!==null&&t===yo}function cs(e,t){Co=So=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ls(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,At(e,n)}}var us={readContext:Ha,useCallback:Eo,useContext:Eo,useEffect:Eo,useImperativeHandle:Eo,useInsertionEffect:Eo,useLayoutEffect:Eo,useMemo:Eo,useReducer:Eo,useRef:Eo,useState:Eo,useDebugValue:Eo,useDeferredValue:Eo,useTransition:Eo,useMutableSource:Eo,useSyncExternalStore:Eo,useId:Eo,unstable_isNewReconciler:!1},ds={readContext:Ha,useCallback:function(e,t){return Ao().memoizedState=[e,t===void 0?null:t],e},useContext:Ha,useEffect:qo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Go(4194308,4,Zo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Go(4194308,4,e,t)},useInsertionEffect:function(e,t){return Go(4,2,e,t)},useMemo:function(e,t){var n=Ao();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ao();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=as.bind(null,yo,e),[r.memoizedState,e]},useRef:function(e){var t=Ao();return e={current:e},t.memoizedState=e},useState:Ho,useDebugValue:$o,useDeferredValue:function(e){return Ao().memoizedState=e},useTransition:function(){var e=Ho(!1),t=e[0];return e=rs.bind(null,e[1]),Ao().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=yo,a=Ao();if(ga){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));vo&30||Lo(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,qo(zo.bind(null,i,o,e),[e]),i.flags|=2048,Uo(9,Ro.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=Ao(),t=Vc.identifierPrefix;if(ga){var n=la,r=ca;n=(r&~(1<<32-ht(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=wo++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[Ci]=t,e[wi]=i,tc(e,t,!1,!1),t.stateNode=e;a:{switch(c=Me(n,i),n){case`dialog`:Xr(`cancel`,e),Xr(`close`,e),o=i;break;case`iframe`:case`object`:case`embed`:Xr(`load`,e),o=i;break;case`video`:case`audio`:for(o=0;oel&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304)}else{if(!i)if(e=po(c),e!==null){if(t.flags|=128,i=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ic(s,!0),s.tail===null&&s.tailMode===`hidden`&&!c.alternate&&!ga)return ac(t),null}else 2*q()-s.renderingStartTime>el&&n!==1073741824&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304);s.isBackwards?(c.sibling=t.child,t.child=c):(n=s.last,n===null?t.child=c:n.sibling=c,s.last=c)}return s.tail===null?(ac(t),null):(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=q(),t.sibling=null,n=fo.current,Li(fo,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(ac(t),t.subtreeFlags&6&&(t.flags|=8192)):ac(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function sc(e,t){switch(pa(t),t.tag){case 1:return Ui(t.type)&&Wi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return co(),Ii(Bi),Ii(zi),ho(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return uo(t),null;case 13:if(Ii(fo),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ta()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Ii(fo),null;case 4:return co(),null;case 10:return za(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var cc=!1,lc=!1,uc=typeof WeakSet==`function`?WeakSet:Set,$=null;function dc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function fc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var pc=!1;function mc(e,t){if(di=nn,e=Cr(),wr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(fi={focusedElem:e,selectionRange:n},nn=!1,$=t;$!==null;)if(t=$,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$=e;else for(;$!==null;){t=$;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:ms(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,$=e;break}$=t.return}return h=pc,pc=!1,h}function hc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&fc(t,n,a)}i=i.next}while(i!==r)}}function gc(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function _c(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function vc(e){var t=e.alternate;t!==null&&(e.alternate=null,vc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ci],delete t[wi],delete t[Ei],delete t[Di],delete t[Oi])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yc(e){return e.tag===5||e.tag===3||e.tag===4}function bc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||yc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function xc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ui));else if(r!==4&&(e=e.child,e!==null))for(xc(e,t,n),e=e.sibling;e!==null;)xc(e,t,n),e=e.sibling}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}var Cc=null,wc=!1;function Tc(e,t,n){for(n=n.child;n!==null;)Ec(e,t,n),n=n.sibling}function Ec(e,t,n){if(pt&&typeof pt.onCommitFiberUnmount==`function`)try{pt.onCommitFiberUnmount(ft,n)}catch{}switch(n.tag){case 5:lc||dc(n,t);case 6:var r=Cc,i=wc;Cc=null,Tc(e,t,n),Cc=r,wc=i,Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Cc.removeChild(n.stateNode));break;case 18:Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?yi(e.parentNode,n):e.nodeType===1&&yi(e,n),en(e)):yi(Cc,n.stateNode));break;case 4:r=Cc,i=wc,Cc=n.stateNode.containerInfo,wc=!0,Tc(e,t,n),Cc=r,wc=i;break;case 0:case 11:case 14:case 15:if(!lc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&fc(n,t,o),i=i.next}while(i!==r)}Tc(e,t,n);break;case 1:if(!lc&&(dc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Tc(e,t,n);break;case 21:Tc(e,t,n);break;case 22:n.mode&1?(lc=(r=lc)||n.memoizedState!==null,Tc(e,t,n),lc=r):Tc(e,t,n);break;default:Tc(e,t,n)}}function Dc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new uc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function Oc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=q()-i,i=(120>i?120:480>i?480:1080>i?1080:1920>i?1920:3e3>i?3e3:4320>i?4320:1960*Ic(i/1960))-i,10e?16:e,ol===null)var i=!1;else{if(e=ol,ol=null,sl=0,Bc&6)throw Error(r(331));var a=Bc;for(Bc|=4,$=e.current;$!==null;){var o=$,s=o.child;if($.flags&16){var c=o.deletions;if(c!==null){for(var l=0;lq()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=bt,bt<<=1,!(bt&130023424)&&(bt=4194304)):t=1);var n=fl();e=Ka(e,t),e!==null&&(Ot(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Bi.current)js=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return js=!1,ec(e,t,n);js=!!(e.flags&131072)}else js=!1,ga&&t.flags&1048576&&da(t,ia,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;Qs(e,t),e=t.pendingProps;var a=Hi(t,zi.current);Va(t,n),a=Oo(null,t,i,e,a,n);var o=ko();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ui(i)?(o=!0,qi(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ja(t),a.updater=gs,t.stateNode=a,a._reactInternals=t,bs(t,i,e,n),t=Bs(null,t,i,!0,o,n)):(t.tag=0,ga&&o&&fa(t),Ms(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch(Qs(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=ms(i,e),a){case 0:t=Rs(null,t,i,e,n);break a;case 1:t=zs(null,t,i,e,n);break a;case 11:t=Ns(null,t,i,e,n);break a;case 14:t=Ps(null,t,i,ms(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Rs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),zs(e,t,i,a,n);case 3:a:{if(Vs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Ya(e,t),eo(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=xs(Error(r(423)),t),t=Hs(e,t,i,n,a);break a}else if(i!==a){a=xs(Error(r(424)),t),t=Hs(e,t,i,n,a);break a}else for(ha=bi(t.stateNode.containerInfo.firstChild),ma=t,ga=!0,_a=null,n=Na(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ta(),i===a){t=$s(e,t,n);break a}Ms(e,t,i,n)}t=t.child}return t;case 5:return lo(t),e===null&&xa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,pi(i,a)?s=null:o!==null&&pi(i,o)&&(t.flags|=32),Ls(e,t),Ms(e,t,s,n),t.child;case 6:return e===null&&xa(t),null;case 13:return Gs(e,t,n);case 4:return so(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Ma(t,null,i,n):Ms(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Ns(e,t,i,a,n);case 7:return Ms(e,t,t.pendingProps,n),t.child;case 8:return Ms(e,t,t.pendingProps.children,n),t.child;case 12:return Ms(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Li(Pa,i._currentValue),i._currentValue=s,o!==null)if(Q(o.value,s)){if(o.children===a.children&&!Bi.current){t=$s(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Xa(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Ba(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Ba(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ms(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Va(t,n),a=Ha(a),i=i(a),t.flags|=1,Ms(e,t,i,n),t.child;case 14:return i=t.type,a=ms(i,t.pendingProps),a=ms(i.type,a),Ps(e,t,i,a,n);case 15:return Fs(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Qs(e,t),t.tag=1,Ui(i)?(e=!0,qi(t)):e=!1,Va(t,n),vs(t,i,a),bs(t,i,a,n),Bs(null,t,i,!0,e,n);case 19:return Zs(e,t,n);case 22:return Is(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return at(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=Dt(0),this.expirationTimes=Dt(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Dt(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ja(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}};async function y(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData,r=await fetch(e,{credentials:`same-origin`,headers:n?t.headers:{"Content-Type":`application/json`,...t.headers??{}},...t}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function b(e){return e instanceof Error?e.message:String(e)}var x={session:()=>y(`/api/session`),login:e=>y(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})}),logout:()=>y(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>y(`/api/accounts?${e.toString()}`),account:e=>y(`/api/accounts/${e}`),channels:e=>y(`/api/channels?${e.toString()}`),channel:e=>y(`/api/channels/${e}`),bots:e=>y(`/api/bots?${e.toString()}`),bot:e=>y(`/api/bots/${e}`),emoji:e=>y(`/api/emoji?${e.toString()}`),emojiAnimation:e=>y(`/api/emoji/${encodeURIComponent(e)}/animation`),messages:e=>y(`/api/messages?${e.toString()}`),message:(e,t)=>y(`/api/messages/detail?${new URLSearchParams({owner_user_id:String(e),msg_id:String(t)}).toString()}`),groupMessages:e=>y(`/api/messages/groups?${e.toString()}`),groupMessage:(e,t)=>y(`/api/messages/groups/detail?${new URLSearchParams({channel_id:String(e),msg_id:String(t)}).toString()}`),gifts:()=>y(`/api/gifts`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},T=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),O=E(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`ShieldX`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m14.5 9.5-5 5`,key:`17q4r4`}],[`path`,{d:`m9.5 9.5 5 5`,key:`18nt4w`}]]),M=E(`Sparkles`,[[`path`,{d:`M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z`,key:`4pj2yx`}],[`path`,{d:`M20 3v4`,key:`1olli1`}],[`path`,{d:`M22 5h-4`,key:`1gvqau`}],[`path`,{d:`M4 17v2`,key:`vumght`}],[`path`,{d:`M5 18H3`,key:`zchphs`}]]),N=E(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),P=E(`AtSign`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8`,key:`7n84p3`}]]),F=E(`Bot`,[[`path`,{d:`M12 8V4H8`,key:`hb8ula`}],[`rect`,{width:`16`,height:`12`,x:`4`,y:`8`,rx:`2`,key:`enze0r`}],[`path`,{d:`M2 14h2`,key:`vft8re`}],[`path`,{d:`M20 14h2`,key:`4cs60a`}],[`path`,{d:`M15 13v2`,key:`1xurst`}],[`path`,{d:`M9 13v2`,key:`rq6x2g`}]]),I=E(`Cable`,[[`path`,{d:`M17 21v-2a1 1 0 0 1-1-1v-1a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1`,key:`10bnsj`}],[`path`,{d:`M19 15V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V9`,key:`1eqmu1`}],[`path`,{d:`M21 21v-2h-4`,key:`14zm7j`}],[`path`,{d:`M3 5h4V3`,key:`z442eg`}],[`path`,{d:`M7 5a1 1 0 0 1 1 1v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1V3`,key:`ebdjd7`}]]),L=E(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),R=E(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),z=E(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),ee=E(`Clock3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`polyline`,{points:`12 6 12 12 16.5 12`,key:`1aq6pp`}]]),te=E(`Copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]),ne=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`}]]),re=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`}]]),ie=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`}]]),ae=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`}]]),oe=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`}]]),se=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`}]]),ce=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`}]]),le=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`}]]),B=E(`LifeBuoy`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.93 4.93 4.24 4.24`,key:`1ymg45`}],[`path`,{d:`m14.83 9.17 4.24-4.24`,key:`1cb5xl`}],[`path`,{d:`m14.83 14.83 4.24 4.24`,key:`q42g0n`}],[`path`,{d:`m9.17 14.83-4.24 4.24`,key:`bqpfvv`}],[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}]]),ue=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`}]]),V=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`}]]),de=E(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),fe=E(`Palette`,[[`circle`,{cx:`13.5`,cy:`6.5`,r:`.5`,fill:`currentColor`,key:`1okk4w`}],[`circle`,{cx:`17.5`,cy:`10.5`,r:`.5`,fill:`currentColor`,key:`f64h9f`}],[`circle`,{cx:`8.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`fotxhn`}],[`circle`,{cx:`6.5`,cy:`12.5`,r:`.5`,fill:`currentColor`,key:`qy21gx`}],[`path`,{d:`M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z`,key:`12rzf8`}]]),pe=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`}]]),me=E(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),he=E(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),ge=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`}]]),_e=E(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),ve=E(`Send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),ye=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`}]]),H=E(`Settings2`,[[`path`,{d:`M20 7h-9`,key:`3s1dr2`}],[`path`,{d:`M14 17H5`,key:`gfn3mx`}],[`circle`,{cx:`17`,cy:`17`,r:`3`,key:`18b49y`}],[`circle`,{cx:`7`,cy:`7`,r:`3`,key:`dfmy0x`}]]),be=E(`ShieldAlert`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`M12 8v4`,key:`1got3b`}],[`path`,{d:`M12 16h.01`,key:`1drbdi`}]]),xe=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`}]]),Se=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`}]]),Ce=E(`Smile`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M8 14s1.5 2 4 2 4-2 4-2`,key:`1y1vjs`}],[`line`,{x1:`9`,x2:`9.01`,y1:`9`,y2:`9`,key:`yxxnd0`}],[`line`,{x1:`15`,x2:`15.01`,y1:`9`,y2:`9`,key:`1p4y9e`}]]),we=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`}]]),Te=E(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),Ee=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`}]]),De=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`}]]),Oe=E(`User`,[[`path`,{d:`M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2`,key:`975kel`}],[`circle`,{cx:`12`,cy:`7`,r:`4`,key:`17ys0d`}]]),ke=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`}]]),Ae=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),je=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})),U=o(((e,t)=>{t.exports=je()}))(),Me=`telesrv.admin.lang`,Ne={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`,"route.giveGifts":`Give Gifts`,"route.giveGiftsSubtitle":`Console / Give Gifts`,"layout.navigation":`Navigation`,"layout.primaryNav":`Primary navigation`,"layout.dashboard":`Overview`,"layout.accounts":`Accounts`,"layout.channels":`Supergroups / Channels`,"layout.messages":`Messages`,"layout.gifts":`Star Gifts`,"layout.giveGifts":`Give Gifts`,"layout.privateMessages":`Private`,"layout.groupMessages":`Groups`,"layout.runtime":`Runtime`,"layout.adminBackend":`Admin backend`,"layout.ready":`Ready`,"layout.pgRead":`PG read`,"layout.readOnly":`Read-only`,"layout.writeOps":`Write operations`,"layout.dryRun":`Dry-run`,"layout.actor":`Actor: {actor}`,"layout.logout":`Log out`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`Switch to dark theme`,"theme.switchToLight":`Switch to light theme`,"login.heading":`Operations Admin`,"login.body":`Enter credentials to open the console.`,"login.secret":`Admin password or token`,"login.submit":`Log in`,"login.submitting":`Logging in`,"dashboard.eyebrow":`Runtime Overview`,"dashboard.title":`Console Overview`,"dashboard.readPath":`Read path`,"dashboard.readPathValue":`PG read-only`,"dashboard.writePath":`Write path`,"dashboard.executionPolicy":`Execution policy`,"dashboard.dryRunFirst":`Dry-run first`,"dashboard.accountsText":`Account status, premium, verification, sessions.`,"dashboard.channelsText":`Public entities, member counts, verification state.`,"dashboard.messagesText":`Message boxes, updates, outbox state.`,"dashboard.strip.dryRun":`All dangerous actions start with dry-run`,"dashboard.strip.token":`Browser never stores internal tokens`,"dashboard.strip.pagination":`Lists use cursor pagination`,"dashboard.strip.snapshot":`Detail pages retain raw state snapshots`,"account.pageTitle":`Accounts`,"account.queryResults":`Search results`,"account.recentActive":`Recently active accounts`,"account.currentPage":`Accounts on page`,"account.onlineDevices":`Online device records`,"account.premium":`Premium`,"account.frozen":`Frozen`,"account.searchPlaceholder":`User ID / phone / username`,"account.userID":`User ID`,"account.phone":`Phone`,"account.lastActive":`Last active`,"account.notVerified":`Not verified`,"account.notPremium":`Not premium`,"account.premiumUntil":`Premium expires`,"account.starsBalance":`Stars balance`,"account.startingGrantApplied":`initial grant applied`,"account.startingGrantPending":`initial grant pending`,"account.activeSessions":`Authorized devices`,"account.accountFlags":`Account flags`,"account.restriction":`Restriction`,"account.restricted":`Restricted`,"account.createdAt":`Created`,"account.detailTitle":`Account #{id}`,"account.profile":`Account Profile`,"account.loadingDetail":`Loading account detail`,"account.waitingData":`Waiting for data`,"account.noUsername":`No username`,"account.noPhone":`No phone`,"account.accountFrozen":`Account frozen`,"account.accountActive":`Account active`,"account.authorizationsTitle":`Authorized Devices`,"account.authorizationsCount":`{count} authorizations`,"account.recentAdminOps":`Recent Admin Actions`,"account.recent30Audit":`Last 30 audit rows`,"account.actionDock":`Account Actions`,"account.freezeAccount":`Freeze account`,"account.updateFreeze":`Update freeze`,"account.unfreezeAccount":`Unfreeze account`,"account.freezeSince":`Frozen since`,"account.freezeUntil":`Appeal deadline`,"account.freezeUntilAria":`Freeze appeal deadline`,"account.freezeAppealURL":`Appeal URL`,"account.freezeAppealURLAria":`Freeze appeal URL`,"account.premiumMonths":`Premium duration (months)`,"account.premiumMonthsAria":`Set premium duration in months`,"account.setPremium":`Set premium`,"account.clearPremium":`Clear premium`,"account.starsAmount":`Stars to grant`,"account.starsAmountAria":`Set Stars amount to grant`,"account.grantStars":`Grant Stars`,"account.setVerified":`Set verified`,"account.clearVerified":`Clear verified`,"channel.pageTitle":`Supergroups and Channels`,"channel.recentUpdated":`Recently updated`,"channel.currentPage":`Entities on page`,"channel.megagroups":`Supergroups`,"channel.broadcasts":`Channels`,"channel.verifiedCount":`Verified`,"channel.searchPlaceholder":`Channel ID / username / title`,"channel.channelID":`Channel ID`,"channel.kind":`Kind`,"channel.title":`Title`,"channel.pts":`PTS`,"channel.detailProfile":`Channel Profile`,"channel.loadingDetail":`Loading channel detail`,"channel.creator":`Creator {id}`,"channel.governance":`Moderation`,"channel.governanceValue":`Banned {banned} / Kicked {kicked}`,"channel.flags":`Channel flags`,"channel.rawRow":`Channel Raw Row`,"channel.rawRowText":`Database read-only snapshot`,"channel.actionDock":`Channel Actions`,"channel.setVerified":`Set verified`,"channel.clearVerified":`Clear verified`,"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`,"route.bots":`Bots`,"route.botsSubtitle":`Console / Bots`,"layout.bots":`Bots`,"bots.pageTitle":`Bots`,"bots.queryResults":`Search results`,"bots.recent":`Recently created bots`,"bots.currentPage":`Bots on page`,"bots.banned":`Banned`,"bots.active":`Active`,"bots.createTitle":`Create a system bot`,"bots.createHint":`Provision a bot account owned by the given user. The token is shown once after confirmation.`,"bots.ownerUserID":`Owner user ID`,"bots.name":`Display name`,"bots.namePlaceholder":`e.g. Service Bot`,"bots.username":`Username`,"bots.usernameHint":`Username must be 5-32 characters and end with 'bot'.`,"bots.create":`Create bot`,"bots.searchPlaceholder":`Bot ID / username`,"bots.botID":`Bot ID`,"bots.owner":`Owner`,"bots.status":`Status`,"bots.detailTitle":`Bot #{id}`,"bots.profile":`Bot Profile`,"bots.loadingDetail":`Loading bot detail`,"bots.unnamed":`Unnamed bot`,"bots.restriction":`Restriction`,"bots.actionDock":`Bot Actions`,"bots.banUntil":`Ban until`,"bots.ban":`Ban bot`,"bots.updateBan":`Update ban`,"bots.unban":`Unban bot`,"bots.type":`Type`,"bots.system":`System`,"bots.user":`User`,"bots.delete":`Delete bot`,"bots.deleteHint":`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`,"bots.systemHint":`System bots are built in and cannot be deleted.`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`Mark as SCAM`,"flags.clearScam":`Clear SCAM`,"flags.setFake":`Mark as FAKE`,"flags.clearFake":`Clear FAKE`,"attr.attributes":`Attributes`,"attr.settings":`Settings`,"attr.username":`Username`,"attr.setUsername":`Set username`,"attr.setSupport":`Mark as support`,"attr.clearSupport":`Clear support`,"attr.forProfile":`Profile color`,"attr.hasColor":`Enable color`,"attr.colorIndex":`Color index`,"attr.bgEmojiID":`Background emoji ID`,"attr.setColor":`Set color`,"attr.emojiDocID":`Emoji document ID`,"attr.emojiUntil":`Until (unix, 0 = permanent)`,"attr.setEmojiStatus":`Set emoji status`,"attr.gigagroup":`Gigagroup`,"attr.antispam":`Aggressive anti-spam`,"attr.participantsHidden":`Hide members`,"attr.noforwards":`Restrict forwarding`,"attr.joinToSend":`Join to send messages`,"attr.joinRequest":`Join by request`,"attr.slowmode":`Slowmode (seconds)`,"attr.applySettings":`Apply settings`,"route.emoji":`Emoji`,"route.emojiSubtitle":`Console / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`Custom Emoji`,"emoji.queryResults":`Search results`,"emoji.recent":`Custom emoji catalog`,"emoji.currentPage":`Emoji on page`,"emoji.searchPlaceholder":`Document ID or emoji`,"emoji.copyID":`Copy document ID`,"emoji.noSet":`No set`,"emoji.hint":`Document IDs here can be pasted into the Emoji status field on account, bot and channel profiles.`,"messages.privateTitle":`Private Messages`,"messages.privateEyebrow":`Private message boxes`,"messages.groupTitle":`Group Messages`,"messages.groupEyebrow":`Supergroup / channel messages`,"messages.selectPrivatePeers":`Search and select the owner user and peer user first`,"messages.selectChannel":`Search and select a supergroup or channel first`,"messages.ownerUser":`Owner user`,"messages.peerUser":`Peer user`,"messages.beforeDatePlaceholder":`before_date cursor`,"messages.beforeIDPlaceholder":`before_msg_id cursor`,"messages.limitPlaceholder":`limit <= 100`,"messages.searchMessages":`Search messages`,"messages.nextPage":`Next page`,"messages.currentPage":`Messages on page`,"messages.deleted":`Deleted`,"messages.outgoing":`Outgoing`,"messages.incoming":`Incoming`,"messages.ownerPeer":`Owner / Peer`,"messages.deleteSelected":`Delete selected messages`,"messages.idsPlaceholder":`Message IDs, comma separated`,"messages.revoke":`Revoke for both sides`,"messages.previewDelete":`Dry-run delete`,"messages.clearHistory":`Clear private history`,"messages.maxIDPlaceholder":`max_id cutoff`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Clear only this side`,"messages.previewClearHistory":`Dry-run clear history`,"messages.direction":`Direction`,"messages.body":`Body`,"messages.privateDetailTitle":`Message #{id}`,"messages.detailEyebrow":`Message Detail`,"messages.backPrivate":`Back to private messages`,"messages.backGroup":`Back to group messages`,"messages.ownerPeerTitle":`Owner {owner} · Peer {peer}`,"messages.senderSubtitle":`Sender {sender} · {date}`,"messages.boxID":`Message box ID`,"messages.privateMessageID":`Private message ID`,"messages.messageSender":`Message sender`,"messages.messageBox":`Message Box`,"messages.dialogRow":`Dialog Row`,"messages.privateRow":`Private Message Row`,"messages.channelMessageRow":`Channel Message Row`,"messages.channelRow":`Channel Row`,"messages.userUpdateEvents":`Update Events`,"messages.channelUpdateEvents":`Channel Update Events`,"messages.eventJson":`Event JSON`,"messages.dispatchOutbox":`Dispatch Queue`,"messages.messageBoxesSnapshot":`message_boxes read-only snapshot`,"messages.dialogSnapshot":`dialogs read-only snapshot`,"messages.privateSnapshot":`private_messages read-only snapshot`,"messages.channelMessagesSnapshot":`channel_messages read-only snapshot`,"messages.channelSnapshot":`channels read-only snapshot`,"messages.userEventsSource":`durable user_update_events`,"messages.channelEventsSource":`durable channel_update_events`,"messages.outboxSource":`online/offline dispatch_outbox`,"messages.attempts":`Attempts`,"messages.deleteThis":`Delete this message`,"messages.groupDetailTitle":`Group Message #{id}`,"messages.channelGroupTitle":`Channel / Group {id}`,"messages.mediaCount":`With media`,"messages.channelPosts":`Channel posts`,"messages.channelGroup":`Channel / Group`,"messages.pinned":`Pinned`,"messages.channelPost":`Channel post`,"gifts.pageTitle":`Star Gift Catalog`,"giveGift.action":`Give`,"giveGift.eyebrow":`Grant a gift · no charge`,"giveGift.title":`Give gift`,"giveGift.recipientKind":`Recipient type`,"giveGift.recipientUser":`User`,"giveGift.recipientChannel":`Channel`,"giveGift.pickUser":`Recipient user`,"giveGift.pickChannel":`Recipient channel`,"giveGift.recipientRequired":`Select a recipient first`,"giveGift.sender":`Sender account ID`,"giveGift.senderHint":`Defaults to the system account 777000 (Telesrv).`,"giveGift.message":`Attached message (optional)`,"giveGift.messagePlaceholder":`Shown with the gift`,"giveGift.hideName":`Hide sender name from recipient`,"giveGift.upgrade":`Deliver as upgraded collectible`,"giveGift.upgradeNote":`The gift is minted as a unique collectible. Pick specific attributes and a number below, or leave them on Random to draw from the published pool. Requires a published collectible upgrade with remaining supply.`,"giveGift.model":`Model`,"giveGift.pattern":`Pattern`,"giveGift.backdrop":`Backdrop`,"giveGift.number":`Number`,"giveGift.numberAuto":`Auto`,"giveGift.random":`Random`,"giveGift.confirm":`Give gift`,"giveGifts.pageTitle":`Give Gifts`,"giveGifts.eyebrow":`Grant catalog gifts to any user or channel`,"giveGifts.available":`Available gifts`,"giveGifts.sender":`Default sender`,"giveGifts.searchPlaceholder":`Search by title or gift ID`,"giveGifts.hint":`Pick a gift to grant. Delivery is free of charge and sent from the system account 777000 (Telesrv) by default.`,"giveGifts.pickGift":`Select a gift`,"giveGifts.selectPrompt":`Select a gift from the list to start.`,"gifts.eyebrow":`Catalog, immutable revisions and animation assets`,"gifts.total":`Catalog entries`,"gifts.enabled":`Enabled`,"gifts.received":`Received gifts`,"gifts.formats":`Accepted formats`,"gifts.add":`Add gift`,"gifts.searchPlaceholder":`Search gift ID, title or format`,"gifts.listSummary":`Showing {shown} of {total}`,"gifts.idRevision":`ID / Revision`,"gifts.price":`Price / Conversion`,"gifts.importTitle":`Import a Star Gift`,"gifts.importEyebrow":`Gift catalog operation`,"gifts.newRevision":`Create revision for gift #{id}`,"gifts.importHint":`Upload TGS or plain Lottie JSON. Lottie is normalized and compressed to TGS.`,"gifts.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"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":`控制台 / 星星礼物`,"route.giveGifts":`赠送礼物`,"route.giveGiftsSubtitle":`控制台 / 赠送礼物`,"layout.navigation":`导航`,"layout.primaryNav":`主导航`,"layout.dashboard":`总览`,"layout.accounts":`账号`,"layout.channels":`超级群/频道`,"layout.messages":`消息`,"layout.gifts":`礼物目录`,"layout.giveGifts":`赠送礼物`,"layout.privateMessages":`私聊`,"layout.groupMessages":`群聊`,"layout.runtime":`运行状态`,"layout.adminBackend":`管理后台`,"layout.ready":`就绪`,"layout.pgRead":`PG 读取`,"layout.readOnly":`只读`,"layout.writeOps":`写操作`,"layout.dryRun":`预演`,"layout.actor":`操作者:{actor}`,"layout.logout":`退出`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`切换到深色主题`,"theme.switchToLight":`切换到浅色主题`,"login.heading":`运维后台`,"login.body":`输入凭据后进入控制台。`,"login.secret":`管理员密码或 token`,"login.submit":`登录`,"login.submitting":`登录中`,"dashboard.eyebrow":`运行总览`,"dashboard.title":`控制台总览`,"dashboard.readPath":`读路径`,"dashboard.readPathValue":`PG 只读`,"dashboard.writePath":`写路径`,"dashboard.executionPolicy":`执行策略`,"dashboard.dryRunFirst":`先预演`,"dashboard.accountsText":`账号状态、会员、认证、会话。`,"dashboard.channelsText":`公开实体、成员计数、认证状态。`,"dashboard.messagesText":`消息盒、update、outbox 状态。`,"dashboard.strip.dryRun":`所有危险操作先预演`,"dashboard.strip.token":`浏览器不持有内部 token`,"dashboard.strip.pagination":`列表使用游标分页`,"dashboard.strip.snapshot":`详情页保留原始状态快照`,"account.pageTitle":`账号`,"account.queryResults":`查询结果`,"account.recentActive":`最近活跃账号`,"account.currentPage":`当前页账号`,"account.onlineDevices":`在线设备记录`,"account.premium":`会员`,"account.frozen":`冻结`,"account.searchPlaceholder":`用户 ID / 手机号 / 用户名`,"account.userID":`用户 ID`,"account.phone":`手机号`,"account.lastActive":`最近活跃`,"account.notVerified":`未认证`,"account.notPremium":`非会员`,"account.premiumUntil":`会员到期`,"account.starsBalance":`Stars 余额`,"account.startingGrantApplied":`初始赠送已发放`,"account.startingGrantPending":`初始赠送未触发`,"account.activeSessions":`授权设备`,"account.accountFlags":`账号标记`,"account.restriction":`限制状态`,"account.restricted":`已限制`,"account.createdAt":`创建时间`,"account.detailTitle":`账号 #{id}`,"account.profile":`账号档案`,"account.loadingDetail":`加载账号详情`,"account.waitingData":`等待数据`,"account.noUsername":`无用户名`,"account.noPhone":`无手机号`,"account.accountFrozen":`账号已冻结`,"account.accountActive":`账号正常`,"account.authorizationsTitle":`授权设备`,"account.authorizationsCount":`共 {count} 个授权`,"account.recentAdminOps":`最近后台操作`,"account.recent30Audit":`最近 30 条审计`,"account.actionDock":`账号操作`,"account.freezeAccount":`冻结账号`,"account.updateFreeze":`更新冻结信息`,"account.unfreezeAccount":`解冻账号`,"account.freezeSince":`冻结开始时间`,"account.freezeUntil":`申诉截止时间`,"account.freezeUntilAria":`账号冻结申诉截止时间`,"account.freezeAppealURL":`申诉链接`,"account.freezeAppealURLAria":`账号冻结申诉链接`,"account.premiumMonths":`会员时长(月)`,"account.premiumMonthsAria":`设置会员时长,单位月`,"account.setPremium":`设置会员`,"account.clearPremium":`取消会员`,"account.starsAmount":`赠送 Stars 数量`,"account.starsAmountAria":`设置要赠送的 Stars 数量`,"account.grantStars":`赠送 Stars`,"account.setVerified":`设置认证`,"account.clearVerified":`取消认证`,"channel.pageTitle":`超级群与频道`,"channel.recentUpdated":`最近更新`,"channel.currentPage":`当前页实体`,"channel.megagroups":`超级群`,"channel.broadcasts":`频道`,"channel.verifiedCount":`已认证`,"channel.searchPlaceholder":`频道 ID / 用户名 / 标题`,"channel.channelID":`频道 ID`,"channel.kind":`类型`,"channel.title":`标题`,"channel.pts":`PTS`,"channel.detailProfile":`频道档案`,"channel.loadingDetail":`加载频道详情`,"channel.creator":`创建者 {id}`,"channel.governance":`治理状态`,"channel.governanceValue":`封禁 {banned} / 踢出 {kicked}`,"channel.flags":`频道标记`,"channel.rawRow":`频道原始行`,"channel.rawRowText":`数据库只读快照`,"channel.actionDock":`频道操作`,"channel.setVerified":`设置认证`,"channel.clearVerified":`取消认证`,"channel.kind.broadcast":`频道`,"channel.kind.forum":`超级群/论坛`,"channel.kind.megagroup":`超级群`,"channel.kind.generic":`频道/群`,"route.bots":`机器人`,"route.botsSubtitle":`控制台 / 机器人`,"layout.bots":`机器人`,"bots.pageTitle":`机器人`,"bots.queryResults":`查询结果`,"bots.recent":`最近创建的机器人`,"bots.currentPage":`当前页机器人`,"bots.banned":`已封禁`,"bots.active":`正常`,"bots.createTitle":`创建系统机器人`,"bots.createHint":`为指定用户创建机器人账号。确认后 token 只显示一次。`,"bots.ownerUserID":`所属用户 ID`,"bots.name":`显示名称`,"bots.namePlaceholder":`例如:服务机器人`,"bots.username":`用户名`,"bots.usernameHint":`用户名需 5-32 个字符,且以 bot 结尾。`,"bots.create":`创建机器人`,"bots.searchPlaceholder":`机器人 ID / 用户名`,"bots.botID":`机器人 ID`,"bots.owner":`所属用户`,"bots.status":`状态`,"bots.detailTitle":`机器人 #{id}`,"bots.profile":`机器人档案`,"bots.loadingDetail":`加载机器人详情`,"bots.unnamed":`未命名机器人`,"bots.restriction":`限制状态`,"bots.actionDock":`机器人操作`,"bots.banUntil":`封禁至`,"bots.ban":`封禁机器人`,"bots.updateBan":`更新封禁`,"bots.unban":`解封机器人`,"bots.type":`类型`,"bots.system":`系统`,"bots.user":`用户`,"bots.delete":`删除机器人`,"bots.deleteHint":`永久删除该用户创建的机器人并使其 token 失效。此操作不可撤销。`,"bots.systemHint":`系统内置机器人不可删除。`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`标记为 SCAM`,"flags.clearScam":`移除 SCAM`,"flags.setFake":`标记为 FAKE`,"flags.clearFake":`移除 FAKE`,"attr.attributes":`属性`,"attr.settings":`设置`,"attr.username":`用户名`,"attr.setUsername":`设置用户名`,"attr.setSupport":`标记为客服`,"attr.clearSupport":`取消客服`,"attr.forProfile":`资料颜色`,"attr.hasColor":`启用颜色`,"attr.colorIndex":`颜色编号`,"attr.bgEmojiID":`背景 emoji ID`,"attr.setColor":`设置颜色`,"attr.emojiDocID":`Emoji 文档 ID`,"attr.emojiUntil":`有效期 (unix, 0 = 永久)`,"attr.setEmojiStatus":`设置 emoji 状态`,"attr.gigagroup":`广播群 (gigagroup)`,"attr.antispam":`激进反垃圾`,"attr.participantsHidden":`隐藏成员`,"attr.noforwards":`禁止转发`,"attr.joinToSend":`先加入才能发言`,"attr.joinRequest":`加入需审批`,"attr.slowmode":`慢速模式 (秒)`,"attr.applySettings":`应用设置`,"route.emoji":`Emoji`,"route.emojiSubtitle":`控制台 / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`自定义 Emoji`,"emoji.queryResults":`查询结果`,"emoji.recent":`自定义 Emoji 目录`,"emoji.currentPage":`当前页 Emoji`,"emoji.searchPlaceholder":`文档 ID 或表情`,"emoji.copyID":`复制文档 ID`,"emoji.noSet":`无所属集合`,"emoji.hint":`这里的文档 ID 可直接填入账号、机器人和频道资料的 Emoji 状态字段。`,"messages.privateTitle":`私聊消息`,"messages.privateEyebrow":`私聊消息盒`,"messages.groupTitle":`群聊消息`,"messages.groupEyebrow":`超级群 / 频道消息`,"messages.selectPrivatePeers":`请先搜索并选择所属用户和对端用户`,"messages.selectChannel":`请先搜索并选择超级群或频道`,"messages.ownerUser":`所属用户`,"messages.peerUser":`对端用户`,"messages.beforeDatePlaceholder":`before_date 游标`,"messages.beforeIDPlaceholder":`before_msg_id 游标`,"messages.limitPlaceholder":`条数 <= 100`,"messages.searchMessages":`查询消息`,"messages.nextPage":`下一页`,"messages.currentPage":`当前页消息`,"messages.deleted":`已删除`,"messages.outgoing":`发出消息`,"messages.incoming":`收到`,"messages.ownerPeer":`所属 / 对端`,"messages.deleteSelected":`删除指定消息`,"messages.idsPlaceholder":`消息 ID,逗号分隔`,"messages.revoke":`同步撤回`,"messages.previewDelete":`预演删除`,"messages.clearHistory":`清空私聊历史`,"messages.maxIDPlaceholder":`max_id 截止消息`,"messages.maxBatchesPlaceholder":`max_batches 批次数`,"messages.justClear":`仅清本侧`,"messages.previewClearHistory":`预演清历史`,"messages.direction":`方向`,"messages.body":`正文`,"messages.privateDetailTitle":`消息 #{id}`,"messages.detailEyebrow":`消息详情`,"messages.backPrivate":`返回私聊消息`,"messages.backGroup":`返回群聊消息`,"messages.ownerPeerTitle":`所属 {owner} · 对端 {peer}`,"messages.senderSubtitle":`发送方 {sender} · {date}`,"messages.boxID":`消息盒 ID`,"messages.privateMessageID":`私聊消息 ID`,"messages.messageSender":`发送方`,"messages.messageBox":`消息盒`,"messages.dialogRow":`会话行`,"messages.privateRow":`私聊消息行`,"messages.channelMessageRow":`消息行`,"messages.channelRow":`频道行`,"messages.userUpdateEvents":`更新事件`,"messages.channelUpdateEvents":`频道更新事件`,"messages.eventJson":`事件 JSON`,"messages.dispatchOutbox":`分发队列`,"messages.messageBoxesSnapshot":`message_boxes 只读快照`,"messages.dialogSnapshot":`dialogs 只读快照`,"messages.privateSnapshot":`private_messages 只读快照`,"messages.channelMessagesSnapshot":`channel_messages 只读快照`,"messages.channelSnapshot":`channels 只读快照`,"messages.userEventsSource":`durable user_update_events`,"messages.channelEventsSource":`durable channel_update_events`,"messages.outboxSource":`在线/离线 dispatch_outbox`,"messages.attempts":`尝试`,"messages.deleteThis":`删除此消息`,"messages.groupDetailTitle":`群聊消息 #{id}`,"messages.channelGroupTitle":`频道/群 {id}`,"messages.mediaCount":`有媒体`,"messages.channelPosts":`频道帖子`,"messages.channelGroup":`频道 / 群`,"messages.pinned":`置顶`,"messages.channelPost":`频道帖子`,"gifts.pageTitle":`星星礼物目录`,"giveGift.action":`赠送`,"giveGift.eyebrow":`发放礼物 · 免费`,"giveGift.title":`赠送礼物`,"giveGift.recipientKind":`接收方类型`,"giveGift.recipientUser":`用户`,"giveGift.recipientChannel":`频道`,"giveGift.pickUser":`接收用户`,"giveGift.pickChannel":`接收频道`,"giveGift.recipientRequired":`请先选择接收方`,"giveGift.sender":`发送方账号 ID`,"giveGift.senderHint":`默认使用系统账号 777000(Telesrv)。`,"giveGift.message":`附加留言(可选)`,"giveGift.messagePlaceholder":`随礼物一起显示`,"giveGift.hideName":`对接收方隐藏发送方名称`,"giveGift.upgrade":`作为升级收藏品发放`,"giveGift.upgradeNote":`礼物将铸造为唯一收藏品。可在下方指定具体属性和编号,或保持“随机”从已发布的属性池中抽取。需要存在有剩余供应量的已发布收藏品升级。`,"giveGift.model":`模型`,"giveGift.pattern":`图案`,"giveGift.backdrop":`背景`,"giveGift.number":`编号`,"giveGift.numberAuto":`自动`,"giveGift.random":`随机`,"giveGift.confirm":`赠送礼物`,"giveGifts.pageTitle":`赠送礼物`,"giveGifts.eyebrow":`向任意用户或频道发放目录礼物`,"giveGifts.available":`可用礼物`,"giveGifts.sender":`默认发送方`,"giveGifts.searchPlaceholder":`按标题或礼物 ID 搜索`,"giveGifts.hint":`选择要发放的礼物。发放免费,默认由系统账号 777000(Telesrv)发送。`,"giveGifts.pickGift":`选择礼物`,"giveGifts.selectPrompt":`从列表中选择一个礼物开始。`,"gifts.eyebrow":`目录、不可变版本与动画资源`,"gifts.total":`目录条目`,"gifts.enabled":`已启用`,"gifts.received":`已领取礼物`,"gifts.formats":`支持格式`,"gifts.add":`添加礼物`,"gifts.searchPlaceholder":`搜索礼物 ID、标题或格式`,"gifts.listSummary":`显示 {shown} / {total} 项`,"gifts.idRevision":`ID / 版本`,"gifts.price":`售价 / 兑换`,"gifts.importTitle":`导入星星礼物`,"gifts.importEyebrow":`礼物目录操作`,"gifts.newRevision":`为礼物 #{id} 创建新版本`,"gifts.importHint":`支持 TGS 或纯 Lottie JSON;Lottie 会规范化并压缩成 TGS。`,"gifts.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка…`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтверждён`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.dashboard":`Панель управления`,"route.dashboardSubtitle":`Консоль / Обзор`,"route.messages":`Аудит сообщений`,"route.messagesSubtitle":`Консоль / Сообщения`,"route.gifts":`Звёздные подарки`,"route.giftsSubtitle":`Консоль / Звёздные подарки`,"route.giveGifts":`Выдача подарков`,"route.giveGiftsSubtitle":`Консоль / Выдача подарков`,"layout.navigation":`Навигация`,"layout.primaryNav":`Основное меню`,"layout.dashboard":`Обзор`,"layout.accounts":`Аккаунты`,"layout.channels":`Супергруппы / Каналы`,"layout.messages":`Сообщения`,"layout.gifts":`Звёздные подарки`,"layout.giveGifts":`Выдача подарков`,"layout.privateMessages":`Личные`,"layout.groupMessages":`Группы`,"layout.runtime":`Среда выполнения`,"layout.adminBackend":`Админ-бэкенд`,"layout.ready":`Готов`,"layout.pgRead":`Чтение из PG`,"layout.readOnly":`Только чтение`,"layout.writeOps":`Операции записи`,"layout.dryRun":`Тестовый запуск`,"layout.actor":`Вы вошли как: {actor}`,"layout.logout":`Выйти`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`Тёмная тема`,"theme.switchToLight":`Светлая тема`,"login.heading":`Панель администратора`,"login.body":`Введите учётные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход…`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, Premium, подтверждение, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, число участников, статус подтверждения.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтверждён`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звёзд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество Звёзд`,"account.starsAmountAria":`Указать количество начисляемых Звёзд`,"account.grantStars":`Начислить Звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновлённые`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждённые`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"route.bots":`Боты`,"route.botsSubtitle":`Консоль / Боты`,"layout.bots":`Боты`,"bots.pageTitle":`Боты`,"bots.queryResults":`Результаты поиска`,"bots.recent":`Недавно созданные боты`,"bots.currentPage":`Боты на странице`,"bots.banned":`Забанен`,"bots.active":`Активен`,"bots.createTitle":`Создать системного бота`,"bots.createHint":`Создаёт бота, принадлежащего указанному пользователю. Токен показывается один раз после подтверждения.`,"bots.ownerUserID":`ID владельца`,"bots.name":`Отображаемое имя`,"bots.namePlaceholder":`например, Service Bot`,"bots.username":`Имя пользователя`,"bots.usernameHint":`Имя пользователя: 5–32 символа, обязательно оканчивается на «bot».`,"bots.create":`Создать бота`,"bots.searchPlaceholder":`ID бота / имя пользователя`,"bots.botID":`ID бота`,"bots.owner":`Владелец`,"bots.status":`Статус`,"bots.detailTitle":`Бот #{id}`,"bots.profile":`Профиль бота`,"bots.loadingDetail":`Загрузка данных бота`,"bots.unnamed":`Без имени`,"bots.restriction":`Ограничение`,"bots.actionDock":`Действия с ботом`,"bots.banUntil":`Забанить до`,"bots.ban":`Забанить бота`,"bots.updateBan":`Обновить бан`,"bots.unban":`Разбанить бота`,"bots.type":`Тип`,"bots.system":`Системный`,"bots.user":`Пользовательский`,"bots.delete":`Удалить бота`,"bots.deleteHint":`Безвозвратно удаляет созданного пользователем бота и аннулирует его токен. Действие необратимо.`,"bots.systemHint":`Системные боты встроены и не могут быть удалены.`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`Пометить как SCAM`,"flags.clearScam":`Снять метку SCAM`,"flags.setFake":`Пометить как FAKE`,"flags.clearFake":`Снять метку FAKE`,"attr.attributes":`Атрибуты`,"attr.settings":`Настройки`,"attr.username":`Имя пользователя`,"attr.setUsername":`Задать имя пользователя`,"attr.setSupport":`Пометить как support`,"attr.clearSupport":`Снять support`,"attr.forProfile":`Цвет профиля`,"attr.hasColor":`Включить цвет`,"attr.colorIndex":`Индекс цвета`,"attr.bgEmojiID":`ID фонового эмодзи`,"attr.setColor":`Задать цвет`,"attr.emojiDocID":`ID документа эмодзи`,"attr.emojiUntil":`До (unix, 0 = бессрочно)`,"attr.setEmojiStatus":`Задать emoji-статус`,"attr.gigagroup":`Гигагруппа`,"attr.antispam":`Агрессивный антиспам`,"attr.participantsHidden":`Скрыть участников`,"attr.noforwards":`Запретить пересылку`,"attr.joinToSend":`Вступление для отправки`,"attr.joinRequest":`Вступление по заявке`,"attr.slowmode":`Медленный режим (сек)`,"attr.applySettings":`Применить настройки`,"route.emoji":`Emoji`,"route.emojiSubtitle":`Консоль / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`Кастом-эмодзи`,"emoji.queryResults":`Результаты поиска`,"emoji.recent":`Каталог кастом-эмодзи`,"emoji.currentPage":`Эмодзи на странице`,"emoji.searchPlaceholder":`ID документа или эмодзи`,"emoji.copyID":`Скопировать ID документа`,"emoji.noSet":`Без набора`,"emoji.hint":`ID документов отсюда можно вставлять в поле Emoji-статуса в профилях аккаунтов, ботов и каналов.`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звёздных подарков`,"giveGift.action":`Выдать`,"giveGift.eyebrow":`Выдача подарка · без списания`,"giveGift.title":`Выдать подарок`,"giveGift.recipientKind":`Тип получателя`,"giveGift.recipientUser":`Пользователь`,"giveGift.recipientChannel":`Канал`,"giveGift.pickUser":`Получатель (пользователь)`,"giveGift.pickChannel":`Получатель (канал)`,"giveGift.recipientRequired":`Сначала выберите получателя`,"giveGift.sender":`ID аккаунта-отправителя`,"giveGift.senderHint":`По умолчанию системный аккаунт 777000 (Telesrv).`,"giveGift.message":`Сообщение к подарку (необязательно)`,"giveGift.messagePlaceholder":`Показывается вместе с подарком`,"giveGift.hideName":`Скрыть имя отправителя от получателя`,"giveGift.upgrade":`Выдать как улучшенный коллекционный`,"giveGift.upgradeNote":`Подарок будет отчеканен как уникальный коллекционный. Ниже можно выбрать конкретные атрибуты и номер или оставить «Случайно» для выбора из опубликованного пула. Требуется опубликованное коллекционное улучшение с остатком тиража.`,"giveGift.model":`Модель`,"giveGift.pattern":`Узор`,"giveGift.backdrop":`Фон`,"giveGift.number":`Номер`,"giveGift.numberAuto":`Авто`,"giveGift.random":`Случайно`,"giveGift.confirm":`Выдать подарок`,"giveGifts.pageTitle":`Выдача подарков`,"giveGifts.eyebrow":`Выдача каталожных подарков любому пользователю или каналу`,"giveGifts.available":`Доступно подарков`,"giveGifts.sender":`Отправитель по умолчанию`,"giveGifts.searchPlaceholder":`Поиск по названию или ID подарка`,"giveGifts.hint":`Выберите подарок для выдачи. Выдача бесплатна и по умолчанию отправляется от системного аккаунта 777000 (Telesrv).`,"giveGifts.pickGift":`Выберите подарок`,"giveGifts.selectPrompt":`Выберите подарок из списка, чтобы начать.`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звёздного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звёздах`,"gifts.convertStars":`Звёзд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звёздные подарки ещё не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звёздах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Отозвать все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтверждённые`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Повторить тестовый запуск`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},Pe=(0,g.createContext)(null);function Fe({children:e}){let[t,n]=(0,g.useState)(()=>Re());(0,g.useEffect)(()=>{try{localStorage.setItem(Me,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Le(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Le(t,e,n)}),[t]);return(0,U.jsx)(Pe.Provider,{value:r,children:e})}function W(){let e=(0,g.useContext)(Pe);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function Ie(){let{lang:e,setLang:t,t:n}=W();return(0,U.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,U.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Le(e,t,n){let r=Ne[e][t]??Ne.en[t]??t;return n?r.replace(/\{(\w+)\}/g,(e,t)=>String(n[t]??``)):r}function Re(){try{let e=ze(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=ze(localStorage.getItem(Me));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=ze(t);if(e)return e}return`en`}function ze(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:t===`ru`||t.startsWith(`ru-`)?`ru`:null}function G(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Be(e,t){return e.startsWith(`/accounts`)?t(`route.accounts`):e.startsWith(`/channels`)?t(`route.channels`):e.startsWith(`/bots`)?t(`route.bots`):e.startsWith(`/emoji`)?t(`route.emoji`):e.startsWith(`/messages`)?t(`route.messages`):e.startsWith(`/give-gifts`)?t(`route.giveGifts`):e.startsWith(`/gifts`)?t(`route.gifts`):t(`route.dashboard`)}function Ve(e,t){return e.startsWith(`/accounts`)?t(`route.accountsSubtitle`):e.startsWith(`/channels`)?t(`route.channelsSubtitle`):e.startsWith(`/bots`)?t(`route.botsSubtitle`):e.startsWith(`/emoji`)?t(`route.emojiSubtitle`):e.startsWith(`/messages`)?t(`route.messagesSubtitle`):e.startsWith(`/give-gifts`)?t(`route.giveGiftsSubtitle`):e.startsWith(`/gifts`)?t(`route.giftsSubtitle`):t(`route.dashboardSubtitle`)}var He=`telesrv.admin.theme`,Ue=(0,g.createContext)(null);function We(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Ge({children:e}){let[t,n]=(0,g.useState)(()=>Je());(0,g.useEffect)(()=>{We(t);try{localStorage.setItem(He,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(He)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,U.jsx)(Ue.Provider,{value:a,children:e})}function Ke(){let e=(0,g.useContext)(Ue);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function qe(){let{theme:e,toggleTheme:t}=Ke(),{t:n}=W(),r=n(e===`light`?`theme.switchToDark`:`theme.switchToLight`);return(0,U.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":r,title:r,children:e===`dark`?(0,U.jsx)(Te,{size:16}):(0,U.jsx)(de,{size:16})})}function Je(){try{let e=localStorage.getItem(He);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}function K({href:e,navigate:t,className:n,children:r}){return(0,U.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Ye(){let{t:e}=W();return(0,U.jsxs)(`div`,{className:`boot-screen`,children:[(0,U.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,U.jsx)(`div`,{className:`loader-bar`})]})}function Xe({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=W(),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,U.jsxs)(`div`,{className:`shell`,children:[(0,U.jsxs)(`aside`,{className:`sidebar`,children:[(0,U.jsxs)(K,{className:`brand`,href:`/`,navigate:n,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,U.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,U.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,U.jsx)(Ze,{icon:(0,U.jsx)(le,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,U.jsx)(Ze,{icon:(0,U.jsx)(ke,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,U.jsx)(Ze,{icon:(0,U.jsx)(xe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,U.jsx)(Ze,{icon:(0,U.jsx)(F,{size:16}),href:`/bots`,route:t,navigate:n,children:a(`layout.bots`)}),(0,U.jsx)(Ze,{icon:(0,U.jsx)(oe,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,U.jsx)(Ze,{icon:(0,U.jsx)(ve,{size:16}),href:`/give-gifts`,route:t,navigate:n,children:a(`layout.giveGifts`)}),(0,U.jsx)(Ze,{icon:(0,U.jsx)(Ce,{size:16}),href:`/emoji`,route:t,navigate:n,children:a(`layout.emoji`)}),(0,U.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,U.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,U.jsx)(V,{size:16}),(0,U.jsx)(`span`,{children:a(`layout.messages`)}),(0,U.jsx)(R,{className:`nav-section-chevron`,size:15})]}),s&&(0,U.jsxs)(`div`,{className:`nav-children`,children:[(0,U.jsx)(Ze,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:a(`layout.privateMessages`)}),(0,U.jsx)(Ze,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,U.jsxs)(`div`,{className:`sidebar-status`,children:[(0,U.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(ye,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,U.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(ne,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,U.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,U.jsxs)(`div`,{className:`runtime-row`,children:[(0,U.jsx)(Se,{size:14}),(0,U.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,U.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,U.jsxs)(`div`,{className:`workspace`,children:[(0,U.jsxs)(`header`,{className:`topbar`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:Ve(t.path,a)}),(0,U.jsx)(`h1`,{children:Be(t.path,a)})]}),(0,U.jsxs)(`div`,{className:`topbar-actions`,children:[(0,U.jsx)(qe,{}),(0,U.jsx)(Ie,{}),(0,U.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,U.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,U.jsx)(ue,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,U.jsx)(`main`,{className:`content`,children:i})]})]})}function Ze({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,U.jsxs)(K,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,U.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,U.jsx)(`span`,{children:i})]})}function Qe(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function $e(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function et(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function tt(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 nt(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function rt(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function it(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function at(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 ot({title:e,eyebrow:t,children:n,actions:r}){return(0,U.jsxs)(`div`,{className:`page-frame`,children:[(0,U.jsxs)(`div`,{className:`page-title-row`,children:[(0,U.jsxs)(`div`,{children:[t&&(0,U.jsx)(`div`,{className:`eyebrow`,children:t}),(0,U.jsx)(`h2`,{children:e})]}),r&&(0,U.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function st({children:e}){return(0,U.jsx)(`div`,{className:`query-panel`,children:e})}function ct({main:e,side:t}){return(0,U.jsxs)(`div`,{className:`split-layout`,children:[(0,U.jsx)(`div`,{className:`split-main`,children:e}),(0,U.jsx)(`aside`,{className:`split-side`,children:t})]})}function q({title:e,text:t,action:n}){return(0,U.jsxs)(`div`,{className:`section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`h2`,{children:e}),t&&(0,U.jsx)(`p`,{children:t})]}),n&&(0,U.jsx)(`div`,{className:`section-action`,children:n})]})}function lt({children:e}){return(0,U.jsxs)(`div`,{className:`alert`,children:[(0,U.jsx)(O,{size:16}),` `,(0,U.jsx)(`span`,{children:e})]})}function J({children:e,tone:t=`neutral`}){return(0,U.jsx)(`span`,{className:`badge ${t}`,children:e})}function ut({label:e,value:t,tone:n}){return(0,U.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{children:t})]})}function Y({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,U.jsxs)(`div`,{className:`metric ${n}`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function X({label:e,value:t,mono:n=!1}){return(0,U.jsxs)(`div`,{className:`summary-item`,children:[(0,U.jsx)(`span`,{children:e}),(0,U.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function dt({rows:e}){let{t}=W();return(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`audit.id`)}),(0,U.jsx)(`th`,{children:t(`audit.commandID`)}),(0,U.jsx)(`th`,{children:t(`audit.action`)}),(0,U.jsx)(`th`,{children:t(`audit.actor`)}),(0,U.jsx)(`th`,{children:t(`audit.status`)}),(0,U.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,U.jsx)(`th`,{children:t(`audit.reason`)}),(0,U.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[e.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,U.jsx)(`td`,{children:e.Action}),(0,U.jsx)(`td`,{children:e.Actor}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,U.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,U.jsx)(`td`,{children:nt(e.CreatedAt)})]},e.ID)),e.length===0&&(0,U.jsx)(ft,{colSpan:8})]})]})})}function ft({colSpan:e}){let{t}=W();return(0,U.jsx)(`tr`,{children:(0,U.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function pt({label:e}){return(0,U.jsx)(`section`,{className:`surface`,children:(0,U.jsx)(`div`,{className:`loading-line`,children:e})})}function mt({value:e}){return(0,U.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function ht({onLogin:e}){let{t}=W(),[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,U.jsx)(`main`,{className:`login-page`,children:(0,U.jsxs)(`section`,{className:`login-panel`,children:[(0,U.jsxs)(`div`,{className:`login-head`,children:[(0,U.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,U.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,U.jsxs)(`span`,{children:[(0,U.jsx)(`strong`,{children:`telesrv`}),(0,U.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,U.jsxs)(`div`,{className:`login-head-actions`,children:[(0,U.jsx)(qe,{}),(0,U.jsx)(Ie,{}),(0,U.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,U.jsxs)(`div`,{className:`login-copy`,children:[(0,U.jsx)(`h1`,{children:t(`login.heading`)}),(0,U.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,U.jsx)(lt,{children:i}),(0,U.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:t(`login.secret`)}),(0,U.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,U.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var gt=m();function _t({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=W(),[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,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,gt.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,U.jsx)(`h2`,{children:e})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,U.jsx)(Ae,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body`,children:[(0,U.jsxs)(`div`,{className:`command-steps`,children:[(0,U.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,U.jsx)(`span`,{children:`1`}),(0,U.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`2`}),(0,U.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`3`}),(0,U.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:s(`action.reason`)}),(0,U.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,U.jsxs)(`div`,{className:`command-preview`,children:[(0,U.jsxs)(`div`,{className:`preview-head`,children:[(0,U.jsx)(ie,{size:14}),` `,s(`action.requestPreview`)]}),(0,U.jsx)(mt,{value:JSON.stringify(T,null,2)})]}),m&&(0,U.jsx)(lt,{children:m}),f&&(0,U.jsxs)(`div`,{className:`result-box`,children:[(0,U.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,U.jsx)(O,{size:16}):(0,U.jsx)(k,{size:16}),(0,U.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.commandID`)}),(0,U.jsx)(`strong`,{children:f.command_id})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.status`)}),(0,U.jsx)(`strong`,{children:f.status})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:s(`action.dryRun`)}),(0,U.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,U.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,U.jsx)(mt,{value:JSON.stringify(f.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(me,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,U.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,U.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function vt({rows:e,userID:t,onDone:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`authorization-block`,children:[(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:r(`auth.device`)}),(0,U.jsx)(`th`,{children:r(`auth.platform`)}),(0,U.jsx)(`th`,{children:r(`auth.ip`)}),(0,U.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,U.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,U.jsxs)(`tbody`,{children:[o.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,U.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,U.jsx)(`td`,{children:n.IP}),(0,U.jsx)(`td`,{children:nt(n.ActiveAt)}),(0,U.jsx)(`td`,{className:`device-actions-cell`,children:(0,U.jsxs)(`div`,{className:`device-actions`,children:[(0,U.jsx)(_t,{label:r(`auth.revokeCurrent`),icon:(0,U.jsx)(ue,{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,U.jsx)(_t,{label:r(`auth.keepCurrent`),icon:(0,U.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,U.jsx)(ft,{colSpan:5})]})]})}),(0,U.jsx)(`div`,{className:`danger-zone`,children:(0,U.jsx)(_t,{label:r(`auth.revokeAll`),icon:(0,U.jsx)(I,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>s(()=>new Set(e.map(e=>e.Hash)))})})]})}function yt({scam:e,fake:t}){let{t:n}=W();return!e&&!t?null:(0,U.jsxs)(U.Fragment,{children:[e&&(0,U.jsx)(J,{tone:`danger`,children:n(`flags.scam`)}),t&&(0,U.jsx)(J,{tone:`danger`,children:n(`flags.fake`)})]})}function bt({idKey:e,id:t,path:n,scam:r,fake:i,onDone:a}){let{t:o}=W();return(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(_t,{label:o(r?`flags.clearScam`:`flags.setScam`),icon:(0,U.jsx)(be,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,scam:!r,fake:i}),onDone:a}),(0,U.jsx)(_t,{label:o(i?`flags.clearFake`:`flags.setFake`),icon:(0,U.jsx)(j,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,scam:r,fake:!i}),onDone:a})]})}function xt({id:e,support:t,onDone:n}){let{t:r}=W();return(0,U.jsx)(_t,{label:r(t?`attr.clearSupport`:`attr.setSupport`),icon:(0,U.jsx)(B,{size:15}),tone:`neutral`,path:`/api/actions/set-support`,payload:()=>({user_id:e,support:!t}),onDone:n})}function St({idKey:e,id:t,path:n,current:r,onDone:i}){let{t:a}=W(),[o,s]=(0,g.useState)(r.replace(/^@/,``));return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:a(`attr.username`)}),(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`username`})]}),(0,U.jsx)(_t,{label:a(`attr.setUsername`),icon:(0,U.jsx)(P,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,username:o.trim().replace(/^@/,``)}),onDone:i})]})}function Ct({idKey:e,id:t,path:n,onDone:r}){let{t:i}=W(),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(!0),[l,u]=(0,g.useState)(`0`),[d,f]=(0,g.useState)(``);return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),` `,i(`attr.forProfile`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>c(e.target.checked)}),` `,i(`attr.hasColor`)]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:i(`attr.colorIndex`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,max:`20`,value:l,onChange:e=>u(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:i(`attr.bgEmojiID`)}),(0,U.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`0`})]}),(0,U.jsx)(_t,{label:i(`attr.setColor`),icon:(0,U.jsx)(fe,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,for_profile:a,has_color:s,color:it(l),background_emoji_id:d.trim()||`0`}),onDone:r})]})}function wt({idKey:e,id:t,path:n,onDone:r}){let{t:i}=W(),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(`0`);return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:i(`attr.emojiDocID`)}),(0,U.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`0 = clear`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:i(`attr.emojiUntil`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:s,onChange:e=>c(e.target.value)})]}),(0,U.jsx)(_t,{label:i(`attr.setEmojiStatus`),icon:(0,U.jsx)(Ce,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,document_id:a.trim()||`0`,until:it(s)}),onDone:r})]})}function Tt({channel:e,onDone:t}){let{t:n}=W(),[r,i]=(0,g.useState)(e.Gigagroup),[a,o]=(0,g.useState)(e.AntiSpam),[s,c]=(0,g.useState)(e.ParticipantsHidden),[l,u]=(0,g.useState)(e.NoForwards),[d,f]=(0,g.useState)(e.JoinToSend),[p,m]=(0,g.useState)(e.JoinRequest),[h,_]=(0,g.useState)(String(e.SlowmodeSeconds));return(0,U.jsxs)(`div`,{className:`attr-block`,children:[(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:r,onChange:e=>i(e.target.checked)}),` `,n(`attr.gigagroup`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),` `,n(`attr.antispam`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>c(e.target.checked)}),` `,n(`attr.participantsHidden`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:l,onChange:e=>u(e.target.checked)}),` `,n(`attr.noforwards`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:d,onChange:e=>f(e.target.checked)}),` `,n(`attr.joinToSend`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,n(`attr.joinRequest`)]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`attr.slowmode`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,max:`86400`,value:h,onChange:e=>_(e.target.value)})]}),(0,U.jsx)(_t,{label:n(`attr.applySettings`),icon:(0,U.jsx)(H,{size:15}),tone:`warn`,path:`/api/actions/set-channel-settings`,payload:()=>({channel_id:e.ID,gigagroup:r,antispam:a,participants_hidden:s,noforwards:l,join_to_send:d,join_request:p,slowmode_seconds:it(h)}),onDone:t})]})}function Et({id:e,navigate:t}){let{t:n}=W(),[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,U.jsx)(lt,{children:a});if(!r)return(0,U.jsx)(pt,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,U.jsx)(ot,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,U.jsx)(N,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(ct,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:et(y)}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[$e(y.Username)||n(`account.noUsername`),` · `,Qe(y.Phone)||n(`account.noPhone`)]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,U.jsx)(J,{tone:`good`,children:n(`account.premium`)}):(0,U.jsx)(J,{children:n(`account.notPremium`)}),r.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)}),(0,U.jsx)(yt,{scam:r.Scam,fake:r.Fake}),y.Frozen?(0,U.jsx)(J,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,U.jsx)(J,{children:n(`account.accountActive`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,U.jsx)(X,{label:n(`account.lastActive`),value:rt(r.LastSeenAt)||`-`}),(0,U.jsx)(X,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?rt(y.PremiumUntil):n(`common.none`)}),(0,U.jsx)(X,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,U.jsx)(X,{label:n(`common.updatedAt`),value:nt(y.UpdatedAt)||`-`}),(0,U.jsx)(X,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,U.jsx)(X,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,U.jsx)(X,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,U.jsx)(X,{label:n(`account.freezeSince`),value:r.Restriction.Since?nt(r.Restriction.Since):n(`common.none`)}),(0,U.jsx)(X,{label:n(`account.freezeUntil`),value:r.Restriction.Until?nt(r.Restriction.Until):n(`common.none`)}),(0,U.jsx)(X,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,U.jsx)(X,{label:n(`account.createdAt`),value:nt(y.CreatedAt)||`-`})]}),r.About&&(0,U.jsx)(`p`,{className:`about-text`,children:r.About}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,U.jsx)(vt,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(dt,{rows:r.AuditLogs})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,U.jsx)(_t,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,U.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,U.jsx)(_t,{label:n(`account.unfreezeAccount`),icon:(0,U.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,U.jsxs)(`div`,{className:`action-stack`,children:[(0,U.jsx)(_t,{label:n(`account.setPremium`),icon:(0,U.jsx)(M,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:it(l)}),onDone:v}),(0,U.jsx)(_t,{label:n(`account.clearPremium`),icon:(0,U.jsx)(M,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,U.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,U.jsx)(_t,{label:n(`account.grantStars`),icon:(0,U.jsx)(we,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:it(d)}),onDone:v}),(0,U.jsx)(_t,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,U.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]}),(0,U.jsx)(bt,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-flags`,scam:r.Scam,fake:r.Fake,onDone:v}),(0,U.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,U.jsx)(xt,{id:y.ID,support:r.Support,onDone:v}),(0,U.jsx)(St,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-username`,current:y.Username,onDone:v}),(0,U.jsx)(Ct,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-color`,onDone:v}),(0,U.jsx)(wt,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-emoji-status`,onDone:v})]})})})}function Dt(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function Ot(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 kt(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}=W(),[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=Ot(o?.rows??[]);return(0,U.jsxs)(ot,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,U.jsx)(ge,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)(lt,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,U.jsx)(Y,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,U.jsx)(Y,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,U.jsx)(Y,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,U.jsx)(st,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(_e,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,U.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`account.userID`)}),(0,U.jsx)(`th`,{children:t(`account.phone`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`common.name`)}),(0,U.jsx)(`th`,{children:t(`common.device`)}),(0,U.jsx)(`th`,{children:t(`account.lastActive`)}),(0,U.jsx)(`th`,{children:t(`account.premium`)}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`account.frozen`)}),(0,U.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:Qe(n.Phone)}),(0,U.jsx)(`td`,{children:$e(n.Username)}),(0,U.jsx)(`td`,{children:et(n)}),(0,U.jsx)(`td`,{children:n.DeviceCount}),(0,U.jsx)(`td`,{children:nt(n.LastActiveAt)}),(0,U.jsx)(`td`,{children:n.PremiumUntil>0?(0,U.jsxs)(J,{tone:`good`,children:[t(`account.premium`),` `,rt(n.PremiumUntil)]}):(0,U.jsx)(J,{children:t(`common.none`)})}),(0,U.jsxs)(`td`,{children:[n.Verified?(0,U.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,U.jsx)(J,{children:t(`account.notVerified`)}),` `,(0,U.jsx)(yt,{scam:n.Scam,fake:n.Fake})]}),(0,U.jsx)(`td`,{children:n.Frozen?(0,U.jsx)(J,{tone:`danger`,children:t(`account.frozen`)}):(0,U.jsx)(J,{children:t(`common.normal`)})}),(0,U.jsx)(`td`,{children:nt(n.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(z,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,U.jsx)(ft,{colSpan:11})]})]})})]})}function Z({id:e,navigate:t}){let{t:n}=W(),[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,U.jsx)(lt,{children:a});if(!r)return(0,U.jsx)(pt,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,U.jsx)(ot,{title:`${tt(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,U.jsx)(N,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(ct,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,U.jsxs)(`div`,{className:`entity-subtitle`,children:[$e(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{children:tt(c,n)}),c.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)}),(0,U.jsx)(yt,{scam:c.Scam,fake:c.Fake}),c.Deleted?(0,U.jsx)(J,{tone:`danger`,children:n(`common.deleted`)}):(0,U.jsx)(J,{children:n(`common.valid`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,U.jsx)(X,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,U.jsx)(X,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,U.jsx)(X,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,U.jsx)(X,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,U.jsx)(X,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,U.jsx)(X,{label:n(`account.createdAt`),value:rt(c.Date)||`-`}),(0,U.jsx)(X,{label:n(`common.updatedAt`),value:nt(c.UpdatedAt)||`-`})]}),c.About&&(0,U.jsx)(`p`,{className:`about-text`,children:c.About}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(dt,{rows:r.AuditLogs})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,U.jsx)(mt,{value:r.ChannelJSON})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,U.jsx)(_t,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,U.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s}),(0,U.jsx)(bt,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-flags`,scam:c.Scam,fake:c.Fake,onDone:s}),(0,U.jsx)(`div`,{className:`dock-title`,children:n(`attr.settings`)}),(0,U.jsx)(Tt,{channel:c,onDone:s}),(0,U.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,U.jsx)(St,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-username`,current:c.Username,onDone:s}),(0,U.jsx)(Ct,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-color`,onDone:s}),(0,U.jsx)(wt,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-emoji-status`,onDone:s})]})})})}function jt({navigate:e}){let{t}=W(),[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=kt(o?.rows??[]);return(0,U.jsxs)(ot,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,U.jsx)(ge,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)(lt,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,U.jsx)(Y,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,U.jsx)(Y,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,U.jsx)(Y,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,U.jsx)(st,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(_e,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,U.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`channel.channelID`)}),(0,U.jsx)(`th`,{children:t(`channel.kind`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`channel.title`)}),(0,U.jsx)(`th`,{children:t(`common.members`)}),(0,U.jsx)(`th`,{children:t(`common.admins`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:tt(n,t)}),(0,U.jsx)(`td`,{children:$e(n.Username)}),(0,U.jsx)(`td`,{children:n.Title}),(0,U.jsx)(`td`,{children:n.ParticipantsCount}),(0,U.jsx)(`td`,{children:n.AdminsCount}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsxs)(`td`,{children:[n.Verified?(0,U.jsx)(J,{tone:`good`,children:t(`common.verified`)}):(0,U.jsx)(J,{children:t(`account.notVerified`)}),` `,(0,U.jsx)(yt,{scam:n.Scam,fake:n.Fake})]}),(0,U.jsx)(`td`,{children:nt(n.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(z,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,U.jsx)(ft,{colSpan:10})]})]})})]})}function Mt({id:e,navigate:t}){let{t:n}=W(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1);async function l(){c(!0),o(``);try{i(await x.bot(e))}catch(e){o(b(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{l()},[e]),a)return(0,U.jsx)(lt,{children:a});if(!r)return(0,U.jsx)(pt,{label:n(s?`bots.loadingDetail`:`account.waitingData`)});let u=r.Bot;return(0,U.jsx)(ot,{title:n(`bots.detailTitle`,{id:u.ID}),eyebrow:n(`bots.profile`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,U.jsx)(N,{size:15}),` `,n(`common.backToList`)]}),children:(0,U.jsx)(ct,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:u.FirstName||n(`bots.unnamed`)}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:$e(u.Username)||n(`account.noUsername`)})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[(0,U.jsx)(J,{tone:u.System?`warn`:`neutral`,children:u.System?n(`bots.system`):n(`bots.user`)}),u.Verified?(0,U.jsx)(J,{tone:`good`,children:n(`common.verified`)}):(0,U.jsx)(J,{children:n(`account.notVerified`)}),(0,U.jsx)(yt,{scam:u.Scam,fake:u.Fake})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:n(`bots.botID`),value:String(u.ID),mono:!0}),(0,U.jsx)(X,{label:n(`bots.owner`),value:u.OwnerUserID>0?`${u.OwnerUserID} ${$e(r.OwnerUsername)}`.trim():n(`common.none`)}),(0,U.jsx)(X,{label:n(`bots.type`),value:u.System?n(`bots.system`):n(`bots.user`)}),(0,U.jsx)(X,{label:n(`common.updatedAt`),value:nt(u.UpdatedAt)||`-`}),(0,U.jsx)(X,{label:n(`account.createdAt`),value:nt(u.CreatedAt)||`-`})]}),r.About&&(0,U.jsx)(`p`,{className:`about-text`,children:r.About}),r.Description&&r.Description.trim()!==r.About.trim()&&(0,U.jsx)(`p`,{className:`about-text`,children:r.Description}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,U.jsx)(dt,{rows:r.AuditLogs})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:n(`bots.actionDock`)}),(0,U.jsx)(`div`,{className:`action-stack`,children:(0,U.jsx)(_t,{label:u.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,U.jsx)(D,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:u.ID,verified:!u.Verified}),onDone:l})}),(0,U.jsx)(bt,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-flags`,scam:u.Scam,fake:u.Fake,onDone:l}),(0,U.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,U.jsx)(St,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-username`,current:u.Username,onDone:l}),(0,U.jsx)(Ct,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-color`,onDone:l}),(0,U.jsx)(wt,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-emoji-status`,onDone:l}),u.System?(0,U.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.systemHint`)}):(0,U.jsxs)(`div`,{className:`danger-zone`,children:[(0,U.jsx)(_t,{label:n(`bots.delete`),icon:(0,U.jsx)(Ee,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:u.ID}),onDone:()=>t(`/bots`)}),(0,U.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.deleteHint`)})]})]})})})}function Nt({navigate:e}){let{t}=W(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(0),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(``),[y,S]=(0,g.useState)(``);async function C(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&t.set(`before_id`,String(c));try{let e=await x.bots(t);s(e),l(e.next_before_id)}catch(e){p(b(e))}finally{d(!1)}}(0,g.useEffect)(()=>{C(!1)},[]);let w=o?.rows??[],T=w.filter(e=>e.Verified).length,E=w.filter(e=>e.System).length;return(0,U.jsxs)(ot,{title:t(`bots.pageTitle`),eyebrow:o?.listing===!1?t(`bots.queryResults`):t(`bots.recent`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>C(!1),disabled:u,children:[(0,U.jsx)(ge,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,U.jsx)(lt,{children:f}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:t(`bots.currentPage`),value:String(w.length)}),(0,U.jsx)(Y,{label:t(`common.verified`),value:String(T),tone:`good`}),(0,U.jsx)(Y,{label:t(`bots.system`),value:String(E)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(`div`,{className:`section-head`,children:(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`h2`,{children:t(`bots.createTitle`)}),(0,U.jsx)(`p`,{children:t(`bots.createHint`)})]})}),(0,U.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.ownerUserID`)}),(0,U.jsx)(`input`,{value:m,onChange:e=>h(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.name`)}),(0,U.jsx)(`input`,{value:_,onChange:e=>v(e.target.value),placeholder:t(`bots.namePlaceholder`),maxLength:64})]}),(0,U.jsxs)(`label`,{className:`duration-field`,children:[(0,U.jsx)(`span`,{children:t(`bots.username`)}),(0,U.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:`my_service_bot`})]})]}),(0,U.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,U.jsx)(`span`,{className:`bot-create-note`,children:t(`bots.usernameHint`)}),(0,U.jsx)(_t,{label:t(`bots.create`),icon:(0,U.jsx)(he,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:it(m),name:_.trim(),username:y.trim().replace(/^@/,``)}),onDone:()=>C(!1)})]})]}),(0,U.jsx)(st,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),C(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`bots.searchPlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`field-inline`,children:[(0,U.jsx)(`span`,{children:t(`common.limit`)}),(0,U.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(_e,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>C(!0),disabled:u,children:[(0,U.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`bots.botID`)}),(0,U.jsx)(`th`,{children:t(`common.username`)}),(0,U.jsx)(`th`,{children:t(`common.name`)}),(0,U.jsx)(`th`,{children:t(`bots.owner`)}),(0,U.jsx)(`th`,{children:t(`common.verified`)}),(0,U.jsx)(`th`,{children:t(`bots.type`)}),(0,U.jsx)(`th`,{children:t(`account.createdAt`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[w.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:$e(n.Username)||`-`}),(0,U.jsx)(`td`,{children:n.FirstName||`-`}),(0,U.jsx)(`td`,{className:`mono`,children:n.OwnerUserID>0?n.OwnerUserID:`-`}),(0,U.jsxs)(`td`,{children:[n.Verified?(0,U.jsxs)(J,{tone:`good`,children:[(0,U.jsx)(D,{size:12}),` `,t(`common.verified`)]}):(0,U.jsx)(J,{children:t(`account.notVerified`)}),` `,(0,U.jsx)(yt,{scam:n.Scam,fake:n.Fake})]}),(0,U.jsx)(`td`,{children:n.System?(0,U.jsx)(J,{tone:`warn`,children:t(`bots.system`)}):(0,U.jsx)(J,{children:t(`bots.user`)})}),(0,U.jsx)(`td`,{children:nt(n.CreatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${n.ID}`),children:[(0,U.jsx)(F,{size:14}),` `,t(`common.detail`),` `,(0,U.jsx)(z,{size:14})]})})]},n.ID)),w.length===0&&(0,U.jsx)(ft,{colSpan:8})]})]})})]})}var Pt=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 ee(e,t){var n=R(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 te(e,t){var n=R(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 ne=function(e){g=!!e},re=function(){return g},ie=function(e){_=e},ae=function(){return _},oe=function(){return v},se=function(e){E=e},ce=function(){return E},le=function(e){y=e};function B(e){return document.createElementNS(`http://www.w3.org/2000/svg`,e)}function ue(e){"@babel/helpers - typeof";return ue=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},ue(e)}var V=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=B(`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 V.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`)},H.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)}},H.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`)))},H.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())},H.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},H.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},H.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`))}},H.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`)},H.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)},H.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),ye(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}}(),we=function(){function e(){return{addedLength:0,percents:p(`float32`,ce()),lengths:p(`float32`,ce())}}return Ce(8,e)}(),Te=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=ce(),a,o,s,c,l,u=0,d,f=[],p=[],m=we.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);U(r,je(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 U(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!==Oe&&(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,ke(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]=Ve.newElement()),a[r][0]=e,a[r][1]=t},He.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)},He.prototype.reverse=function(){var e=new He;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 Je(e){"@babel/helpers - typeof";return Je=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},Je(e)}var K={},Ye=`__[STANDALONE]__`,Xe=`__[ANIMATIONDATA]__`,Ze=``;function Qe(e){s(e)}function $e(){Ye===!0?be.searchAnimations(Xe,Ye,Ze):be.searchAnimations()}function et(e){ne(e)}function tt(e){le(e)}function nt(e){return Ye===!0&&(e.animationData=JSON.parse(Xe)),be.loadAnimation(e)}function rt(e){if(typeof e==`string`)switch(e){case`high`:se(200);break;default:case`medium`:se(50);break;case`low`:se(10);break}else!isNaN(e)&&e>1&&se(e)}function it(){return typeof navigator<`u`}function at(e,t){e===`expressions`&&ie(t)}function ot(e){switch(e){case`propertyFactory`:return G;case`shapePropertyFactory`:return Ke;case`matrix`:return qe;default:return null}}K.play=be.play,K.pause=be.pause,K.setLocationHref=Qe,K.togglePause=be.togglePause,K.setSpeed=be.setSpeed,K.setDirection=be.setDirection,K.stop=be.stop,K.searchAnimations=$e,K.registerAnimation=be.registerAnimation,K.loadAnimation=nt,K.setSubframeRendering=et,K.resize=be.resize,K.goToAndStop=be.goToAndStop,K.destroy=be.destroy,K.setQuality=rt,K.inBrowser=it,K.installPlugin=at,K.freeze=be.freeze,K.unfreeze=be.unfreeze,K.setVolume=be.setVolume,K.mute=be.mute,K.unmute=be.unmute,K.getRegisteredAnimations=be.getRegisteredAnimations,K.useWebWorker=a,K.setIDPrefix=tt,K.__getFactory=ot,K.version=`5.13.0`;function st(){document.readyState===`complete`&&(clearInterval(ut),$e())}function ct(e){for(var t=q.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},dt.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=De.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=De.getNewSegment(i[a].v[s-1],i[a].v[0],i[a].o[s-1],i[a].i[0],(t.s-l)/_,(t.e-l)/_,f[s-1]),this.addSegmentFromArray(p,n,d,g),g=!1,n.c=!1)}else n.c=!1;l+=u.addedLength,d+=1}if(n._length&&(n.setXYAt(n.v[h][0],n.v[h][1],`i`,h),n.setXYAt(n.v[n._length-1][0],n.v[n._length-1][1],`o`,n._length-1)),l>t.e)break;a=this.p.keyframes[this.p.keyframes.length-1].t?(r=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/n,0),i=this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/n,0)):(r=this.p.pv,i=this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/n,this.p.offsetTime));else if(this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime){r=[],i=[];var a=this.px,o=this.py;a._caching.lastFrame+a.offsetTime<=a.keyframes[0].t?(r[0]=a.getValueAtTime((a.keyframes[0].t+.01)/n,0),r[1]=o.getValueAtTime((o.keyframes[0].t+.01)/n,0),i[0]=a.getValueAtTime(a.keyframes[0].t/n,0),i[1]=o.getValueAtTime(o.keyframes[0].t/n,0)):a._caching.lastFrame+a.offsetTime>=a.keyframes[a.keyframes.length-1].t?(r[0]=a.getValueAtTime(a.keyframes[a.keyframes.length-1].t/n,0),r[1]=o.getValueAtTime(o.keyframes[o.keyframes.length-1].t/n,0),i[0]=a.getValueAtTime((a.keyframes[a.keyframes.length-1].t-.01)/n,0),i[1]=o.getValueAtTime((o.keyframes[o.keyframes.length-1].t-.01)/n,0)):(r=[a.pv,o.pv],i[0]=a.getValueAtTime((a._caching.lastFrame+a.offsetTime-.01)/n,a.offsetTime),i[1]=o.getValueAtTime((o._caching.lastFrame+o.offsetTime-.01)/n,o.offsetTime))}else i=e,r=i;this.v.rotate(-Math.atan2(r[1]-i[1],r[0]-i[0]))}this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])}this.frameId=this.elem.globalData.frameId}}function r(){if(this.appliedTransformations=0,this.pre.reset(),!this.a.effectsSequence.length)this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1;else return;if(!this.s.effectsSequence.length)this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2;else return;if(this.sk)if(!this.sk.effectsSequence.length&&!this.sa.effectsSequence.length)this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3;else return;this.r?this.r.effectsSequence.length||(this.pre.rotate(-this.r.v),this.appliedTransformations=4):!this.rz.effectsSequence.length&&!this.ry.effectsSequence.length&&!this.rx.effectsSequence.length&&!this.or.effectsSequence.length&&(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}function i(){}function a(e){this._addDynamicProperty(e),this.elem.addDynamicProperty(e),this._isDirty=!0}function o(e,t,n){if(this.elem=e,this.frameId=-1,this.propType=`transform`,this.data=t,this.v=new qe,this.pre=new qe,this.appliedTransformations=0,this.initDynamicPropertyContainer(n||e),t.p&&t.p.s?(this.px=G.getProp(e,t.p.x,0,0,this),this.py=G.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=G.getProp(e,t.p.z,0,0,this))):this.p=G.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=G.getProp(e,t.rx,0,D,this),this.ry=G.getProp(e,t.ry,0,D,this),this.rz=G.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)},mt.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})},Ct.prototype.split=function(e){if(e<=0)return[St(this.points[0]),this];if(e>=1)return[this,St(this.points[this.points.length-1])];var t=yt(this.points[0],this.points[1],e),n=yt(this.points[1],this.points[2],e),r=yt(this.points[2],this.points[3],e),i=yt(t,n,e),a=yt(n,r,e),o=yt(i,a,e);return[new Ct(this.points[0],t,i,o,!0),new Ct(o,a,r,this.points[3],!0)]};function wt(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=bt(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}}Ct.prototype.bounds=function(){return{x:wt(this,0),y:wt(this,1)}},Ct.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 Tt(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 Et(e){var t=e.bez.split(.5);return[Tt(t[0],e.t1,e.t),Tt(t[1],e.t,e.t2)]}function Dt(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=Et(e),s=Et(t);Ot(o[0],s[0],n+1,r,i,a),Ot(o[0],s[1],n+1,r,i,a),Ot(o[1],s[0],n+1,r,i,a),Ot(o[1],s[1],n+1,r,i,a)}}Ct.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return Ot(Tt(this,0,1),Tt(e,0,1),0,t,r,n),r},Ct.shapeSegment=function(e,t){var n=(t+1)%e.length();return new Ct(e.v[t],e.o[t],e.i[n],e.v[n],!0)},Ct.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new Ct(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function kt(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 At(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=kt(kt(i,a),kt(o,s));return _t(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function Z(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function jt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Mt(e,t){return gt(e[0],t[0])&>(e[1],t[1])}function Nt(){}u([X],Nt),Nt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=G.getProp(e,t.s,0,null,this),this.frequency=G.getProp(e,t.r,0,null,this),this.pointsType=G.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function Pt(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function Ft(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function It(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=Ft(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Lt(e,t,n,r,i,a,o){var s=It(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;Pt(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function Rt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ht(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Wt(e){for(var t,n=1;n1&&(t=Ut(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Gt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Bt(e,t)];if(n.length===1||gt(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Bt(r,t),Bt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Bt(r,t),Bt(o,t),Bt(i,t)]}function Kt(){}u([X],Kt),Kt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=G.getProp(e,t.a,0,null,this),this.miterLimit=G.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},Kt.prototype.processPath=function(e,t,n,r){var i=Ue.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=Ct.shapeSegmentInverted(e,o),l.push(Gt(c,t));l=Wt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function Yt(e){this.animationData=e}Yt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Xt(e){return new Yt(e)}function Zt(){}Zt.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},pn.prototype.show=function(){},pn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},pn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},pn.prototype.resume=function(){this._canPlay=!0},pn.prototype.setRate=function(e){this.audio.rate(e)},pn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},pn.prototype.getBaseElement=function(){return null},pn.prototype.destroy=function(){},pn.prototype.sourceRectAtTime=function(){},pn.prototype.initExpressions=function(){};function mn(){}mn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},mn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},mn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},mn.prototype.createAudio=function(e){return new pn(e,this.globalData,this)},mn.prototype.createFootage=function(e){return new fn(e,this.globalData,this)},mn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}_n.prototype.getMaskProperty=function(e){return this.viewData[e].prop},_n.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},_n.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var vn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=B(`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=B(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),yn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),bn={},xn=`filter_result_`;function Sn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=I(),a=vn.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Ln.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function Wn(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([un,gn,Cn,On,wn,dn,Tn],Wn),Wn.prototype.initSecondaryElement=function(){},Wn.prototype.identityMatrix=new qe,Wn.prototype.buildExpressionInterface=function(){},Wn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Wn.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},Wn.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},Kn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},Kn.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},Kn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Jt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Jt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Jt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Jt.isVariationSelector(i)&&(o=!0)):Jt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},Kn.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=qt(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var F,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([Be],r);function i(e,t,n){return new r(e,t,n)}return{getTextSelectorProp:i}}();function Jn(e,t,n){var r={propType:!1},i=G.getProp,a=t.a;this.a={r:a.r?i(e,a.r,0,D,n):r,rx:a.rx?i(e,a.rx,0,D,n):r,ry:a.ry?i(e,a.ry,0,D,n):r,sk:a.sk?i(e,a.sk,0,D,n):r,sa:a.sa?i(e,a.sa,0,D,n):r,s:a.s?i(e,a.s,1,.01,n):r,a:a.a?i(e,a.a,1,0,n):r,o:a.o?i(e,a.o,0,.01,n):r,p:a.p?i(e,a.p,1,0,n):r,sw:a.sw?i(e,a.sw,0,0,n):r,sc:a.sc?i(e,a.sc,1,0,n):r,fc:a.fc?i(e,a.fc,1,0,n):r,fh:a.fh?i(e,a.fh,0,0,n):r,fs:a.fs?i(e,a.fs,0,.01,n):r,fb:a.fb?i(e,a.fb,0,.01,n):r,t:a.t?i(e,a.t,0,0,n):r},this.s=qn.getTextSelectorProp(e,t.s,n),this.s.t=t.s.t}function Yn(e,t,n){this._isFirstFrame=!0,this._hasMaskedPath=!1,this._frameId=-1,this._textData=e,this._renderType=t,this._elem=n,this._animatorsData=m(this._textData.a.length),this._pathData={},this._moreOptions={alignment:{}},this.renderedLetters=[],this.lettersChangedFlag=!1,this.initDynamicPropertyContainer(n)}Yn.prototype.searchProperties=function(){var e,t=this._textData.a.length,n,r=G.getProp;for(e=0;e=m+Se||!x?(T=(m+Se-g)/h.partialLength,re=b.point[0]+(h.point[0]-b.point[0])*T,ie=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));ne=f[u].an/2-f[u].add,a.translate(-ne,0,0)}else ne=f[u].an/2-f[u].add,a.translate(-ne,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:B(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=B(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new rr(x.data,this.globalData,this);else{var w=Zn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Wn(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},nr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=cr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=cr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&cr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new ur(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=Qt(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new dr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(hn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=cr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=pr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new qe},fr.prototype.hide=fr.prototype.hideElement,fr.prototype.show=fr.prototype.showElement;function mr(e,t,n,r){this.styledShapes=[],this.tr=[0,0,0,0,0,0];var i=4;t.ty===`rc`?i=5:t.ty===`el`?i=6:t.ty===`sr`&&(i=7),this.sh=Ke.getShapeProp(e,t,i,e);var a,o=n.length,s;for(a=0;a=0;--a){if(d=this.searchProcessedElement(e[a]),d?t[a]=n[d-1]:e[a]._shouldRender=r,e[a].ty===`fl`||e[a].ty===`st`||e[a].ty===`gf`||e[a].ty===`gs`)d?t[a].style.closed=!1:t[a]=this.createStyleElement(e[a],m),l.push(t[a].style);else if(e[a].ty===`gr`){if(!d)t[a]=this.createGroupElement(e[a]);else for(c=t[a].it.length,s=0;s=0;--i)t[i].ty===`tr`?(o=n[i].transform,this.renderShapeTransform(e,o)):t[i].ty===`sh`||t[i].ty===`el`||t[i].ty===`rc`||t[i].ty===`sr`?this.renderPath(t[i],n[i]):t[i].ty===`fl`?this.renderFill(t[i],n[i],o):t[i].ty===`st`?this.renderStroke(t[i],n[i],o):t[i].ty===`gf`||t[i].ty===`gs`?this.renderGradientFill(t[i],n[i],o):t[i].ty===`gr`?this.renderShape(o,t[i].it,n[i].it):t[i].ty;r&&this.drawLayer()},hr.prototype.renderStyledShape=function(e,t){if(this._isFirstFrame||t._mdf||e.transforms._mdf){var n=e.trNodes,r=t.paths,i,a,o,s=r._length;n.length=0;var c=e.transforms.finalTransform;for(o=0;o=1?u=.99:u<=-1&&(u=-.99);var d=c*u,f=Math.cos(l+t.a.v)*d+o[0],p=Math.sin(l+t.a.v)*d+o[1];i=a.createRadialGradient(f,p,0,o[0],o[1],c)}var m,h=e.g.p,g=t.g.c,_=1;for(m=0;ma&&c===`xMidYMid slice`||ii&&s===`meet`||ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))/2*this.renderConfig.dpr:l===`xMax`&&(ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))*this.renderConfig.dpr:this.transformCanvas.tx=0,u===`YMid`&&(a>i&&s===`meet`||ai&&s===`meet`||a=0;--e)this.elements[e]&&this.elements[e].destroy&&this.elements[e].destroy();this.elements.length=0,this.globalData.canvasContext=null,this.animationItem.container=null,this.destroyed=!0},Q.prototype.renderFrame=function(e,t){if(!(this.renderedFrame===e&&this.renderConfig.clearCanvas===!0&&!t||this.destroyed||e===-1)){this.renderedFrame=e,this.globalData.frameNum=e-this.animationItem._isFirstFrame,this.globalData.frameId+=1,this.globalData._mdf=!this.renderConfig.clearCanvas||t,this.globalData.projectInterface.currentFrame=e;var n,r=this.layers.length;for(this.completeLayers||this.checkLayers(e),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].prepareFrame(e-this.layers[n].st);if(this.globalData._mdf){for(this.renderConfig.clearCanvas===!0?this.canvasContext.clearRect(0,0,this.transformCanvas.w,this.transformCanvas.h):this.save(),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].renderFrame();this.renderConfig.clearCanvas!==!0&&this.restore()}}},Q.prototype.buildItem=function(e){var t=this.elements;if(!(t[e]||this.layers[e].ty===99)){var n=this.createItem(this.layers[e],this,this.globalData);t[e]=n,n.initExpressions()}},Q.prototype.checkPendingElements=function(){for(;this.pendingElements.length;)this.pendingElements.pop().checkParenting()},Q.prototype.hide=function(){this.animationItem.container.style.display=`none`},Q.prototype.show=function(){this.animationItem.container.style.display=`block`};function yr(){this.opacity=-1,this.transform=p(`float32`,16),this.fillStyle=``,this.strokeStyle=``,this.lineWidth=``,this.lineCap=``,this.lineJoin=``,this.miterLimit=``,this.id=Math.random()}function br(){this.stack=[],this.cArrPos=0,this.cTr=new qe;var e,t=15;for(e=0;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].renderFrame()},xr.prototype.destroy=function(){var e;for(e=this.layers.length-1;e>=0;--e)this.elements[e]&&this.elements[e].destroy();this.layers=null,this.elements=null},xr.prototype.createComp=function(e){return new xr(e,this.globalData,this)};function Sr(e,t){this.animationItem=e,this.renderConfig={clearCanvas:t&&t.clearCanvas!==void 0?t.clearCanvas:!0,context:t&&t.context||null,progressiveLoad:t&&t.progressiveLoad||!1,preserveAspectRatio:t&&t.preserveAspectRatio||`xMidYMid meet`,imagePreserveAspectRatio:t&&t.imagePreserveAspectRatio||`xMidYMid slice`,contentVisibility:t&&t.contentVisibility||`visible`,className:t&&t.className||``,id:t&&t.id||``,runExpressions:!t||t.runExpressions===void 0||t.runExpressions},this.renderConfig.dpr=t&&t.dpr||1,this.animationItem.wrapper&&(this.renderConfig.dpr=t&&t.dpr||window.devicePixelRatio||1),this.renderedFrame=-1,this.globalData={frameNum:-1,_mdf:!1,renderConfig:this.renderConfig,currentGlobalAlpha:-1},this.contextData=new br,this.elements=[],this.pendingElements=[],this.transformMat=new qe,this.completeLayers=!1,this.rendererType=`canvas`,this.renderConfig.clearCanvas&&(this.ctxTransform=this.contextData.transform.bind(this.contextData),this.ctxOpacity=this.contextData.opacity.bind(this.contextData),this.ctxFillStyle=this.contextData.fillStyle.bind(this.contextData),this.ctxStrokeStyle=this.contextData.strokeStyle.bind(this.contextData),this.ctxLineWidth=this.contextData.lineWidth.bind(this.contextData),this.ctxLineCap=this.contextData.lineCap.bind(this.contextData),this.ctxLineJoin=this.contextData.lineJoin.bind(this.contextData),this.ctxMiterLimit=this.contextData.miterLimit.bind(this.contextData),this.ctxFill=this.contextData.fill.bind(this.contextData),this.ctxFillRect=this.contextData.fillRect.bind(this.contextData),this.ctxStroke=this.contextData.stroke.bind(this.contextData),this.save=this.contextData.save.bind(this.contextData))}return u([Q],Sr),Sr.prototype.createComp=function(e){return new xr(e,this.globalData,this)},ge(`canvas`,Sr),Y.registerModifier(`tm`,dt),Y.registerModifier(`pb`,ft),Y.registerModifier(`rp`,mt),Y.registerModifier(`rd`,ht),Y.registerModifier(`zz`,Nt),Y.registerModifier(`op`,Kt),K}))}))(),1);function Ft(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function It(e){let t=e.toLowerCase();return t.includes(`tgsticker`)||t.includes(`lottie`)||t.includes(`json`)}function Lt({row:e}){let t=(0,g.useRef)(null),n=(0,g.useRef)(null),[r,i]=(0,g.useState)(!It(e.MimeType));return(0,g.useEffect)(()=>{if(!It(e.MimeType)){i(!0);return}let r=!1;return x.emojiAnimation(e.DocumentID).then(e=>{r||!t.current||(n.current?.destroy(),n.current=Pt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>i(!0)),()=>{r=!0,n.current?.destroy(),n.current=null}},[e.DocumentID,e.MimeType]),r?(0,U.jsx)(`div`,{className:`emoji-glyph`,children:e.Alt||`🙂`}):(0,U.jsx)(`div`,{className:`emoji-anim`,ref:t})}function Rt({row:e}){let{t}=W(),[n,r]=(0,g.useState)(!1);async function i(){try{await navigator.clipboard.writeText(e.DocumentID),r(!0),setTimeout(()=>r(!1),1200)}catch{}}return(0,U.jsxs)(`div`,{className:`emoji-card`,children:[(0,U.jsx)(`div`,{className:`emoji-preview`,children:(0,U.jsx)(Lt,{row:e})}),(0,U.jsxs)(`div`,{className:`emoji-meta`,children:[(0,U.jsx)(`span`,{className:`emoji-alt`,children:e.Alt||`—`}),(0,U.jsxs)(`button`,{className:`emoji-id`,type:`button`,onClick:i,title:t(`emoji.copyID`),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.DocumentID}),n?(0,U.jsx)(L,{size:12}):(0,U.jsx)(te,{size:12})]}),(0,U.jsxs)(`span`,{className:`emoji-sub`,children:[e.SetTitle||t(`emoji.noSet`),` · `,Ft(e.Size)]})]})]})}function zt(){let{t:e}=W(),[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(0),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(e=!1){c(!0),u(``);let n=new URLSearchParams;t.trim()?n.set(`q`,t.trim()):e&&n.set(`before_id`,String(a));try{let e=await x.emoji(n);i(e),o(e.next_before_id)}catch(e){u(b(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d(!1)},[]);let f=r?.rows??[];return(0,U.jsxs)(ot,{title:e(`emoji.pageTitle`),eyebrow:r?.listing===!1?e(`emoji.queryResults`):e(`emoji.recent`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),disabled:s,children:[(0,U.jsx)(ge,{size:15}),` `,e(`common.refresh`)]}),children:[l&&(0,U.jsx)(lt,{children:l}),(0,U.jsx)(`div`,{className:`metric-row`,children:(0,U.jsx)(Y,{label:e(`emoji.currentPage`),value:String(f.length)})}),(0,U.jsx)(st,{children:(0,U.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),d(!1)},children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:e(`emoji.searchPlaceholder`)})]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:s,children:[s?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(_e,{size:15}),` `,e(`common.search`)]}),r?.listing&&r.has_more&&(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>d(!0),disabled:s,children:[(0,U.jsx)(z,{size:15}),` `,e(`messages.nextPage`)]})]})}),(0,U.jsx)(`p`,{className:`about-text`,children:e(`emoji.hint`)}),f.length===0?(0,U.jsx)(`div`,{className:`empty-panel`,children:e(`common.noResults`)}):(0,U.jsx)(`div`,{className:`emoji-grid`,children:f.map(e=>(0,U.jsx)(Rt,{row:e},e.DocumentID))})]})}function Bt({navigate:e}){let{t}=W();return(0,U.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,U.jsxs)(`section`,{className:`overview-band`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,U.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,U.jsxs)(`div`,{className:`overview-metrics`,children:[(0,U.jsx)(ut,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,U.jsx)(ut,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,U.jsx)(ut,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,U.jsxs)(`div`,{className:`command-grid`,children:[(0,U.jsx)(Vt,{icon:(0,U.jsx)(ke,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,U.jsx)(Vt,{icon:(0,U.jsx)(xe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,U.jsx)(Vt,{icon:(0,U.jsx)(V,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,U.jsxs)(`section`,{className:`work-strip`,children:[(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(k,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(ce,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(ee,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,U.jsxs)(`div`,{className:`strip-item`,children:[(0,U.jsx)(ie,{size:16}),(0,U.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function Vt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,U.jsxs)(K,{className:`launcher`,href:r,navigate:i,children:[(0,U.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,U.jsxs)(`span`,{className:`launcher-copy`,children:[(0,U.jsx)(`strong`,{children:t}),(0,U.jsx)(`span`,{children:n})]}),(0,U.jsx)(z,{size:16})]})}function Ht({channelID:e,msgID:t,navigate:n}){let{t:r}=W(),[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,U.jsx)(lt,{children:o});if(!i)return(0,U.jsx)(pt,{label:r(`common.loading`)});let l=i.Message;return(0,U.jsx)(ot,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,U.jsx)(N,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:rt(l.Date)})})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,U.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,U.jsx)(J,{children:r(`common.survived`)}),l.Pinned&&(0,U.jsx)(J,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,U.jsx)(J,{children:r(`messages.channelPost`)}),(0,U.jsxs)(J,{children:[`pts `,l.PTS]})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,U.jsx)(X,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,U.jsx)(X,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,U.jsx)(X,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,U.jsx)(mt,{value:i.MessageJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,U.jsx)(mt,{value:i.ChannelJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.count`)}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.messageId`)}),(0,U.jsx)(`th`,{children:r(`common.sender`)}),(0,U.jsx)(`th`,{children:r(`common.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:e.MessageID}),(0,U.jsx)(`td`,{children:e.SenderUserID}),(0,U.jsx)(`td`,{children:rt(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,U.jsx)(ft,{colSpan:6})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.eventJson`)}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,U.jsx)(mt,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,U.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function Ut({label:e,value:t,onChange:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(Ae,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(L,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:et(t)}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:$e(t.Username)||Qe(t.Phone)||`-`})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,U.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,U.jsx)(`div`,{className:`picker-error`,children:u}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:et(e)}),(0,U.jsx)(`span`,{children:$e(e.Username)||Qe(e.Phone)||`-`}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,U.jsx)(J,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,U.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function Wt({label:e,value:t,onChange:n}){let{t:r}=W(),[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,U.jsxs)(`div`,{className:`entity-picker`,children:[(0,U.jsxs)(`div`,{className:`picker-head`,children:[(0,U.jsx)(`span`,{children:e}),t?(0,U.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,U.jsx)(Ae,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,U.jsxs)(`div`,{className:`selected-entity`,children:[(0,U.jsx)(L,{size:15}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:t.Title||`-`}),(0,U.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,U.jsx)(`span`,{children:$e(t.Username)||tt(t,r)})]}):null,(0,U.jsxs)(`div`,{className:`picker-search`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,U.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,U.jsx)(`div`,{className:`picker-error`,children:u}),(0,U.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,U.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,U.jsx)(`span`,{className:`mono`,children:e.ID}),(0,U.jsx)(`strong`,{children:e.Title||`-`}),(0,U.jsx)(`span`,{children:$e(e.Username)||tt(e,r)}),e.Verified?(0,U.jsx)(J,{tone:`good`,children:r(`picker.verified`)}):(0,U.jsx)(J,{children:tt(e,r)})]},e.ID)),o.length===0&&!c?(0,U.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function Gt({navigate:e}){let{t}=W(),[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,U.jsxs)(ot,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,U.jsx)(lt,{children:f}),(0,U.jsxs)(st,{children:[(0,U.jsx)(`div`,{className:`message-selector-grid single`,children:(0,U.jsx)(Wt,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,U.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,U.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(_e,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,U.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:t(`messages.currentPage`),value:String(_.length)}),(0,U.jsx)(Y,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,U.jsx)(Y,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,U.jsx)(Y,{label:t(`messages.channelGroup`),value:n?`${n.Title||tt(n,t)} (${n.ID})`:`-`})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`common.messageId`)}),(0,U.jsx)(`th`,{children:t(`common.time`)}),(0,U.jsx)(`th`,{children:t(`common.sender`)}),(0,U.jsx)(`th`,{children:`From Peer`}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.views`)}),(0,U.jsx)(`th`,{children:t(`common.status`)}),(0,U.jsx)(`th`,{children:t(`messages.body`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[_.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.ID}),(0,U.jsx)(`td`,{children:rt(n.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,U.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.ViewsCount}),(0,U.jsx)(`td`,{children:n.Deleted?(0,U.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,U.jsx)(J,{tone:`warn`,children:t(`messages.pinned`)}):(0,U.jsx)(J,{children:t(`common.survived`)})}),(0,U.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,U.jsx)(z,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,U.jsx)(ft,{colSpan:9})]})]})})]})}function Kt({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=W(),[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,U.jsx)(lt,{children:o});if(!i)return(0,U.jsx)(pt,{label:r(`common.loading`)});let l=i.Message;return(0,U.jsx)(ot,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,U.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,U.jsx)(N,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,U.jsx)(ct,{main:(0,U.jsxs)(`div`,{className:`stacked-sections`,children:[(0,U.jsxs)(`section`,{className:`entity-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,U.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:rt(l.Date)})})]}),(0,U.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,U.jsx)(J,{tone:`danger`,children:r(`common.deleted`)}):(0,U.jsx)(J,{children:r(`common.survived`)}),(0,U.jsxs)(J,{children:[`pts `,l.PTS]}),(0,U.jsx)(J,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,U.jsxs)(`div`,{className:`summary-grid`,children:[(0,U.jsx)(X,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,U.jsx)(X,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,U.jsx)(X,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,U.jsx)(X,{label:r(`common.time`),value:rt(l.Date)})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,U.jsx)(mt,{value:i.MessageJSON})]}),(0,U.jsxs)(`div`,{className:`raw-grid`,children:[(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,U.jsx)(mt,{value:i.DialogJSON})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,U.jsx)(mt,{value:i.PrivateJSON})]})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.count`)}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.time`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.PTSCount}),(0,U.jsx)(`td`,{children:e.Type}),(0,U.jsx)(`td`,{children:rt(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,U.jsx)(ft,{colSpan:4})]})]})})]}),(0,U.jsxs)(`section`,{className:`section-block`,children:[(0,U.jsx)(q,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:`ID`}),(0,U.jsx)(`th`,{children:r(`account.userID`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:r(`common.type`)}),(0,U.jsx)(`th`,{children:r(`common.status`)}),(0,U.jsx)(`th`,{children:r(`messages.attempts`)}),(0,U.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,U.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{children:e.ID}),(0,U.jsx)(`td`,{children:e.TargetUserID}),(0,U.jsx)(`td`,{children:e.PTS}),(0,U.jsx)(`td`,{children:e.EventType}),(0,U.jsx)(`td`,{children:e.Status}),(0,U.jsx)(`td`,{children:e.Attempts}),(0,U.jsx)(`td`,{children:nt(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,U.jsx)(ft,{colSpan:7})]})]})})]})]}),side:(0,U.jsxs)(`section`,{className:`action-dock`,children:[(0,U.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,U.jsx)(_t,{label:r(`messages.deleteThis`),icon:(0,U.jsx)(Ee,{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 qt({navigate:e}){let{t}=W(),[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,U.jsxs)(ot,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,U.jsx)(lt,{children:D}),(0,U.jsxs)(st,{children:[(0,U.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,U.jsx)(Ut,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,U.jsx)(Ut,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,U.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,U.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,U.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,U.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,U.jsx)(_e,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,U.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,U.jsxs)(`div`,{className:`metric-row`,children:[(0,U.jsx)(Y,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,U.jsx)(Y,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,U.jsx)(Y,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,U.jsx)(Y,{label:t(`messages.ownerPeer`),value:n&&i?`${et(n)} / ${et(i)}`:`-`})]}),(0,U.jsxs)(`div`,{className:`operation-row`,children:[(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(Ee,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,U.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,U.jsx)(_t,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:at(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,U.jsxs)(`div`,{className:`operation-box`,children:[(0,U.jsxs)(`div`,{className:`operation-title`,children:[(0,U.jsx)(se,{size:15}),` `,t(`messages.clearHistory`)]}),(0,U.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,U.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,U.jsxs)(`label`,{className:`checkline`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,U.jsx)(_t,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:it(y),max_batches:it(C),just_clear:_,revoke:m})})]})]}),(0,U.jsx)(`div`,{className:`table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:t(`common.messageId`)}),(0,U.jsx)(`th`,{children:t(`common.time`)}),(0,U.jsx)(`th`,{children:t(`common.sender`)}),(0,U.jsx)(`th`,{children:t(`messages.direction`)}),(0,U.jsx)(`th`,{children:`PTS`}),(0,U.jsx)(`th`,{children:t(`common.status`)}),(0,U.jsx)(`th`,{children:t(`messages.body`)}),(0,U.jsx)(`th`,{})]})}),(0,U.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,U.jsx)(`td`,{children:rt(n.Date)}),(0,U.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,U.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,U.jsx)(`td`,{children:n.PTS}),(0,U.jsx)(`td`,{children:n.Deleted?(0,U.jsx)(J,{tone:`danger`,children:t(`common.deleted`)}):(0,U.jsx)(J,{children:t(`common.survived`)})}),(0,U.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,U.jsx)(z,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,U.jsx)(ft,{colSpan:8})]})]})})]})}var Jt=0,Yt=e=>`${e}-${++Jt}`,Xt=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function Zt(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:Yt(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function $t(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=Xt[e.length%Xt.length];return{key:Yt(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var en=e=>Zt([Qt(e,0),Qt(e,1)]),tn=()=>{let e=$t([]);return Zt([e,$t([e])])};function nn({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=Pt.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,U.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function rn({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,U.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,U.jsx)(nn,{data:n,compact:!0}):(0,U.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,U.jsx)(A,{className:`spin`,size:15})})}async function an(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 on=e=>Number.parseInt(e.replace(`#`,``),16),sn=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function cn({gift:e,onClose:t,onPublished:n}){let{t:r}=W(),[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)(()=>en(`model`)),[D,O]=(0,g.useState)(()=>en(`pattern`)),[j,N]=(0,g.useState)(tn);(0,g.useEffect)(()=>{let t=!1;return x.giftCollectibles(e.GiftID).then(n=>{t||(a(n),n.found&&(h(String(n.upgrade_stars??100)),v(String(n.supply_total??1e3)),S(n.slug_prefix??`gift-${e.GiftID}`)))}).catch(e=>d(b(e))).finally(()=>{t||s(!1)}),()=>{t=!0}},[e.GiftID]);let P=(0,g.useMemo)(()=>({models:T.reduce((e,t)=>e+Number(t.rarity||0),0),patterns:D.reduce((e,t)=>e+Number(t.rarity||0),0),backdrops:j.reduce((e,t)=>e+Number(t.rarity||0),0)}),[T,D,j]),F=()=>p(null),I=(e,t,n)=>{(e===`models`?E:O)(e=>e.map(e=>e.key===t?{...e,...n}:e)),F()};async function L(e,t,n){if(I(e,t.key,{file:n,animation:null,fileError:``}),n)try{let r=await an(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function R(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||j.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=j.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:j.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:on(e.center),edge_color:on(e.edge),pattern_color:on(e.pattern),text_color:on(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}async function z(){l(!0),d(``),p(null);try{p(await x.publishGiftCollectibles(e.GiftID,R(!1)))}catch(e){d(b(e))}finally{l(!1)}}async function ee(){if(f){l(!0),d(``);try{await x.publishGiftCollectibles(e.GiftID,R(!0,f.command_id)),n(),t()}catch(e){d(b(e))}finally{l(!1)}}}let te=(e,t,n)=>(0,U.jsxs)(`section`,{className:`collectible-section`,children:[(0,U.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,U.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,U.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,U.jsxs)(J,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(Zt([...t,Qt(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,U.jsx)(he,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,U.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,U.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,U.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`common.name`)}),(0,U.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,U.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,U.jsxs)(`label`,{className:`collectible-file`,children:[(0,U.jsx)(`span`,{children:r(`gifts.animation`)}),(0,U.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,U.jsxs)(`em`,{children:[(0,U.jsx)(re,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,U.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,U.jsx)(nn,{data:i.animation,compact:!0}):(0,U.jsx)(M,{size:16})}),(0,U.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(Zt(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,U.jsx)(Ee,{size:14})}),i.fileError&&(0,U.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,gt.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,U.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,U.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,U.jsx)(Ae,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,U.jsxs)(`div`,{className:`collectible-loading`,children:[(0,U.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,U.jsxs)(`section`,{className:`collectible-active`,children:[(0,U.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(ae,{size:18}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,U.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,U.jsx)(J,{tone:`good`,children:r(`collectibles.published`)})]}),(0,U.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,U.jsxs)(`article`,{children:[(0,U.jsx)(rn,{giftID:e.GiftID,attribute:t}),(0,U.jsxs)(`div`,{children:[(0,U.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,U.jsx)(J,{children:`crafted`})]}),(0,U.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,sn(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,U.jsxs)(`article`,{children:[(0,U.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,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e.name}),(0,U.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,sn(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,U.jsxs)(`div`,{className:`collectible-empty`,children:[(0,U.jsx)(ae,{size:22}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,U.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,U.jsxs)(`section`,{className:`collectible-definition`,children:[(0,U.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,U.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,U.jsx)(`span`,{children:`TGS`}),(0,U.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,U.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,U.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.reason`)}),(0,U.jsx)(`input`,{value:C,maxLength:1e3,placeholder:r(`gifts.reasonPlaceholder`),onChange:e=>w(e.target.value)})]})]}),te(`models`,T,E),te(`patterns`,D,O),(0,U.jsxs)(`section`,{className:`collectible-section`,children:[(0,U.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,U.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,U.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,U.jsxs)(J,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,U.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N(Zt([...j,$t(j)])),F()},children:[(0,U.jsx)(he,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,U.jsx)(`div`,{className:`collectible-rows`,children:j.map((e,t)=>(0,U.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,U.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`common.name`)}),(0,U.jsx)(`input`,{value:e.name,maxLength:128,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,name:t.target.value}:n)),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:e.rarity,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,rarity:t.target.value}:n)),F()}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,U.jsx)(`input`,{type:`number`,value:e.sortOrder,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,sortOrder:t.target.value}:n)),F()}})]}),[`center`,`edge`,`pattern`,`text`].map(t=>(0,U.jsxs)(`label`,{className:`collectible-color`,children:[(0,U.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,U.jsx)(`input`,{type:`color`,value:e[t],onChange:n=>{N(j.map(r=>r.key===e.key?{...r,[t]:n.target.value}:r)),F()}})]},t)),(0,U.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,U.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:j.length<=2,onClick:()=>{N(Zt(j.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,U.jsx)(Ee,{size:14})})]},e.key))})]})]}),u&&(0,U.jsx)(lt,{children:u}),f&&(0,U.jsxs)(`div`,{className:`gift-validation`,children:[(0,U.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,U.jsx)(k,{size:17}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,U.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,U.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:z,disabled:c,children:[c?(0,U.jsx)(A,{className:`spin`,size:15}):(0,U.jsx)(xe,{size:15}),r(`gifts.validate`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:ee,disabled:c||!f,children:[(0,U.jsx)(De,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function ln(e){return e.model_count+e.pattern_count+e.backdrop_count}function un(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function dn({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=Pt.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,U.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,U.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,U.jsx)(`span`,{children:s})}),(0,U.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,U.jsx)(pe,{size:14}):(0,U.jsx)(me,{size:14})})]})}function fn({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=Pt.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,U.jsx)(`div`,{className:`gift-animation-shell`,children:(0,U.jsx)(`div`,{className:`gift-animation`,ref:t})})}function pn(){let{t:e}=W(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[R,z]=(0,g.useState)(`50`),[ee,te]=(0,g.useState)(`50`),[ne,ie]=(0,g.useState)(`0`),[oe,se]=(0,g.useState)(!0),[ce,le]=(0,g.useState)(``),[B,ue]=(0,g.useState)(null),[V,de]=(0,g.useState)(!1),[fe,pe]=(0,g.useState)(``),[me,ve]=(0,g.useState)(``);async function ye(){pe(``);try{n((await x.gifts()).Gifts??[])}catch(e){pe(b(e))}}(0,g.useEffect)(()=>{ye()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>ve(b(e)))},[a,d,p.length]);let H=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),be=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),Se=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),Ce=(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 we(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!ce.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:ce.trim(),confirm:t,gift_id:P,title:I.trim(),stars:R,convert_stars:ee,enabled:oe,sort_order:Number(ne)})),r.set(`file`,l,l.name),r}function Te(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!ce.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:ce.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:R,convert_stars:ee,enabled:oe,sort_order:Number(ne),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function Ee(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),z(String(t.stars)),te(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),ue(null)}async function Oe(){de(!0),ve(``),ue(null);try{ue(d===`official`?await x.importOfficialGift(Te(!1)):await x.importGift(we(!1)))}catch(e){ve(b(e))}finally{de(!1)}}async function ke(){if(B){de(!0),ve(``);try{d===`official`?await x.importOfficialGift(Te(!0,B.command_id)):await x.importGift(we(!0,B.command_id)),ue(null),u(null),F(`0`),L(``),C(``),await ye(),o(!1)}catch(e){ve(b(e))}finally{de(!1)}}}function je(){F(`0`),L(``),z(`50`),te(`50`),ie(`0`),se(!0),le(``),u(null),ue(null),ve(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Me(e){F(e.GiftID),L(e.Title),z(String(e.Stars)),te(String(e.ConvertStars)),ie(String(e.SortOrder)),se(e.Enabled),le(``),u(null),ue(null),ve(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,U.jsxs)(ot,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>ye(),disabled:V,children:[(0,U.jsx)(ge,{size:15}),` `,e(`common.refresh`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:je,children:[(0,U.jsx)(he,{size:15}),` `,e(`gifts.add`)]})]}),children:[fe&&(0,U.jsx)(lt,{children:fe}),(0,U.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,U.jsx)(Y,{label:e(`gifts.total`),value:String(t.length)}),(0,U.jsx)(Y,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,U.jsx)(Y,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,U.jsx)(Y,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,U.jsx)(st,{children:(0,U.jsxs)(`div`,{className:`toolbar`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,U.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:Ce.length,total:t.length})})]})}),(0,U.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,U.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,U.jsx)(`thead`,{children:(0,U.jsxs)(`tr`,{children:[(0,U.jsx)(`th`,{children:e(`gifts.animation`)}),(0,U.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,U.jsx)(`th`,{children:e(`gifts.title`)}),(0,U.jsx)(`th`,{children:e(`gifts.price`)}),(0,U.jsx)(`th`,{children:e(`gifts.source`)}),(0,U.jsx)(`th`,{children:e(`gifts.received`)}),(0,U.jsx)(`th`,{children:e(`common.status`)}),(0,U.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,U.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,U.jsxs)(`tbody`,{children:[Ce.map(t=>(0,U.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,U.jsx)(`td`,{children:(0,U.jsx)(dn,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,U.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,U.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,U.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,U.jsxs)(`td`,{children:[(0,U.jsx)(J,{children:t.SourceFormat}),(0,U.jsx)(`span`,{className:`gift-source-size`,children:un(t.AnimationSize)})]}),(0,U.jsx)(`td`,{children:t.ReceivedCount}),(0,U.jsx)(`td`,{children:(0,U.jsx)(J,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,U.jsx)(`td`,{children:nt(t.UpdatedAt)}),(0,U.jsx)(`td`,{children:(0,U.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,U.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,U.jsx)(ae,{size:13}),e(`collectibles.manage`)]}),(0,U.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Me(t),children:e(`gifts.replace`)}),(0,U.jsx)(_t,{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 ye()})]})})]},t.GiftID)),Ce.length===0&&(0,U.jsx)(ft,{colSpan:9})]})]})}),a&&(0,gt.createPortal)((0,U.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,U.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,U.jsxs)(`div`,{className:`modal-head`,children:[(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,U.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,U.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:V,"aria-label":e(`action.close`),children:(0,U.jsx)(Ae,{size:15})})]}),(0,U.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,U.jsxs)(`div`,{className:`command-steps`,children:[(0,U.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,U.jsx)(`span`,{children:`1`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${B?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`2`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,U.jsxs)(`div`,{className:`command-step ${B?`active`:``}`,children:[(0,U.jsx)(`span`,{children:`3`}),(0,U.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,U.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,U.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),ue(null)},children:e(`gifts.officialSource`)}),(0,U.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),ue(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,U.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,U.jsxs)(`div`,{className:`gift-import-note`,children:[(0,U.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,U.jsx)(`span`,{children:p.length}),(0,U.jsx)(`span`,{children:`SHA-256`})]})]}),(0,U.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,U.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:Se.length,total:p.length})})]}),(0,U.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,U.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,U.jsx)(`span`,{children:be[t]})]},t))}),(0,U.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[Se.map(t=>{let n=t.source_gift_id===S;return(0,U.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>Ee(t),children:[(0,U.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,U.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,U.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,U.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,U.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,U.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:ln(t)})})]}),(0,U.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,U.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,U.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),Se.length===0&&(0,U.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),H&&(0,U.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,U.jsx)(fn,{sourceGiftID:H.source_gift_id}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:H.title||e(`gifts.officialUnnamed`,{id:H.source_gift_id})}),(0,U.jsx)(`span`,{className:`mono`,children:H.source_gift_id}),(0,U.jsxs)(`small`,{children:[H.model_count,` `,e(`collectibles.models`),` · `,H.pattern_count,` `,e(`collectibles.patterns`),` · `,H.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,U.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,U.jsx)(`span`,{className:H.can_upgrade?`yes`:`no`,children:H.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,U.jsx)(`span`,{className:H.can_craft?`craft`:`no`,children:H.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),H?.can_upgrade&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),ue(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,U.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),ue(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),ue(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,U.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),ue(null)}})]})]})]})]}):(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`div`,{className:`gift-import-note`,children:[(0,U.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,U.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,U.jsx)(`span`,{children:`TGS`}),(0,U.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,U.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,U.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),ue(null)}}),(0,U.jsx)(`span`,{className:`gift-file-icon`,children:(0,U.jsx)(re,{size:22})}),(0,U.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,U.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,U.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,U.jsx)(`small`,{children:l?un(l.size):e(`gifts.fileHint`)})]}),(0,U.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,U.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.title`)}),(0,U.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),ue(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.stars`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,value:R,onChange:e=>{z(e.target.value),ue(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,U.jsx)(`input`,{type:`number`,min:`0`,value:ee,onChange:e=>{te(e.target.value),ue(null)}})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,U.jsx)(`input`,{type:`number`,value:ne,onChange:e=>{ie(e.target.value),ue(null)}})]})]}),(0,U.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,U.jsx)(`span`,{children:e(`gifts.reason`)}),(0,U.jsx)(`input`,{value:ce,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>le(e.target.value)})]}),(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:oe,onChange:e=>{se(e.target.checked),ue(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),me&&(0,U.jsx)(lt,{children:me}),B&&(0,U.jsxs)(`div`,{className:`gift-validation`,children:[(0,U.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,U.jsx)(k,{size:17}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,U.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,U.jsx)(`pre`,{children:JSON.stringify(B.details,null,2)})]})]}),(0,U.jsxs)(`div`,{className:`modal-actions`,children:[(0,U.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:V,children:e(`common.close`)}),(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:Oe,disabled:V,children:[V?(0,U.jsx)(A,{className:`spin`,size:15}):(0,U.jsx)(xe,{size:15}),e(`gifts.validate`)]}),(0,U.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:ke,disabled:V||!B,children:[(0,U.jsx)(De,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,U.jsx)(cn,{gift:s,onClose:()=>c(null),onPublished:()=>void ye()})]})}var mn=`777000`;function hn(e){let t=e.rarity_permille>0?` · ${(e.rarity_permille/10).toFixed(1)}%`:``;return`${e.name||`#${e.id}`}${t}`}function gn({gift:e,onDone:t}){let{t:n}=W(),[r,i]=(0,g.useState)(`user`),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(mn),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(!1),[v,y]=(0,g.useState)(null),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(`0`),[E,D]=(0,g.useState)(`0`),[j,M]=(0,g.useState)(`0`),[N,P]=(0,g.useState)(``),[F,I]=(0,g.useState)(``),[L,R]=(0,g.useState)(null),[z,ee]=(0,g.useState)(``),[te,ne]=(0,g.useState)(!1),re=r===`user`?a?.ID??0:s?.ID??0,ie=r===`user`&&h;(0,g.useEffect)(()=>{_(!1),y(null),C(``),T(`0`),D(`0`),M(`0`),P(``),R(null),ee(``)},[e.GiftID]),(0,g.useEffect)(()=>{if(!ie||v)return;let t=!1;return C(``),x.giftCollectibles(e.GiftID).then(e=>{t||y(e)}).catch(e=>{t||C(b(e))}),()=>{t=!0}},[ie,v,e.GiftID]);function ae(t){let n=Number.parseInt(l.trim()||mn,10),i=Number.parseInt(N.trim(),10);return{gift_id:e.GiftID,sender_user_id:Number.isFinite(n)?n:0,user_id:r===`user`?re:0,channel_id:r===`channel`?re:0,hide_name:p,message:d.trim(),upgrade:ie,model_attribute_id:ie?w:`0`,pattern_attribute_id:ie?E:`0`,backdrop_attribute_id:ie?j:`0`,num:ie&&Number.isFinite(i)&&i>0?i:0,reason:F.trim(),confirm:t}}let se=(0,g.useMemo)(()=>ae(!1),[e.GiftID,r,re,l,d,p,h,w,E,j,N,F]),ce=L?.dry_run&&!L.error;async function le(e){if(re<=0){ee(n(`giveGift.recipientRequired`));return}if(!F.trim()){ee(n(`action.reasonRequired`));return}ne(!0),ee(``);try{let n=await x.action(`/api/actions/give-gift`,ae(e));R(n),e&&!n.error&&t?.()}catch(e){ee(b(e))}finally{ne(!1)}}return(0,U.jsxs)(`div`,{className:`give-gift-form`,children:[(0,U.jsxs)(`div`,{className:`give-gift-summary`,children:[(0,U.jsx)(oe,{size:16}),(0,U.jsxs)(`div`,{children:[(0,U.jsx)(`strong`,{children:e.Title||`Gift #${e.GiftID}`}),(0,U.jsxs)(`span`,{className:`mono`,children:[`#`,e.GiftID,` · ⭐ `,e.Stars]})]})]}),(0,U.jsxs)(`div`,{className:`give-gift-tabs`,role:`group`,"aria-label":n(`giveGift.recipientKind`),children:[(0,U.jsxs)(`button`,{type:`button`,className:`btn ${r===`user`?`primary`:``}`,onClick:()=>{i(`user`),R(null)},children:[(0,U.jsx)(Oe,{size:15}),` `,n(`giveGift.recipientUser`)]}),(0,U.jsxs)(`button`,{type:`button`,className:`btn ${r===`channel`?`primary`:``}`,onClick:()=>{i(`channel`),_(!1),R(null)},children:[(0,U.jsx)(ke,{size:15}),` `,n(`giveGift.recipientChannel`)]})]}),r===`user`?(0,U.jsx)(Ut,{label:n(`giveGift.pickUser`),value:a,onChange:e=>{o(e),R(null)}}):(0,U.jsx)(Wt,{label:n(`giveGift.pickChannel`),value:s,onChange:e=>{c(e),R(null)}}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:n(`giveGift.sender`)}),(0,U.jsx)(`input`,{value:l,inputMode:`numeric`,onChange:e=>{u(e.target.value.replace(/[^0-9]/g,``)),R(null)},placeholder:mn}),(0,U.jsx)(`small`,{className:`field-hint`,children:n(`giveGift.senderHint`)})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:n(`giveGift.message`)}),(0,U.jsx)(`textarea`,{value:d,rows:2,maxLength:255,onChange:e=>{f(e.target.value),R(null)},placeholder:n(`giveGift.messagePlaceholder`)})]}),(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>{m(e.target.checked),R(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:n(`giveGift.hideName`)})]}),r===`user`&&(0,U.jsxs)(U.Fragment,{children:[(0,U.jsxs)(`label`,{className:`gift-switch`,children:[(0,U.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>{_(e.target.checked),e.target.checked||(T(`0`),D(`0`),M(`0`),P(``)),R(null)}}),(0,U.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,U.jsx)(`span`,{})}),(0,U.jsx)(`span`,{children:n(`giveGift.upgrade`)})]}),h&&(0,U.jsx)(`p`,{className:`give-gift-upgrade-note`,children:n(`giveGift.upgradeNote`)}),h&&S&&(0,U.jsx)(lt,{children:S}),h&&v&&(0,U.jsxs)(`div`,{className:`gift-fields-grid give-gift-attrs`,children:[(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:n(`giveGift.model`)}),(0,U.jsxs)(`select`,{value:w,onChange:e=>{T(e.target.value),R(null)},children:[(0,U.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(v.models??[]).map(e=>(0,U.jsx)(`option`,{value:e.id,children:hn(e)},e.id))]})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:n(`giveGift.pattern`)}),(0,U.jsxs)(`select`,{value:E,onChange:e=>{D(e.target.value),R(null)},children:[(0,U.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(v.patterns??[]).map(e=>(0,U.jsx)(`option`,{value:e.id,children:hn(e)},e.id))]})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:n(`giveGift.backdrop`)}),(0,U.jsxs)(`select`,{value:j,onChange:e=>{M(e.target.value),R(null)},children:[(0,U.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(v.backdrops??[]).map(e=>(0,U.jsx)(`option`,{value:e.id,children:hn(e)},e.id))]})]}),(0,U.jsxs)(`label`,{children:[(0,U.jsx)(`span`,{children:n(`giveGift.number`)}),(0,U.jsx)(`input`,{type:`number`,min:`1`,max:v.supply_total??void 0,value:N,placeholder:n(`giveGift.numberAuto`),onChange:e=>{P(e.target.value.replace(/[^0-9]/g,``)),R(null)}})]})]})]}),(0,U.jsxs)(`label`,{className:`form-field`,children:[(0,U.jsx)(`span`,{children:n(`action.reason`)}),(0,U.jsx)(`textarea`,{value:F,rows:2,onChange:e=>I(e.target.value),placeholder:n(`action.reasonPlaceholder`)})]}),(0,U.jsxs)(`div`,{className:`command-preview`,children:[(0,U.jsx)(`div`,{className:`preview-head`,children:n(`action.requestPreview`)}),(0,U.jsx)(mt,{value:JSON.stringify(se,null,2)})]}),z&&(0,U.jsx)(lt,{children:z}),L&&(0,U.jsxs)(`div`,{className:`result-box`,children:[(0,U.jsxs)(`div`,{className:`result-title`,children:[L.error?(0,U.jsx)(O,{size:16}):(0,U.jsx)(k,{size:16}),(0,U.jsx)(`strong`,{children:L.message||L.error||n(`action.result`)})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:n(`action.commandID`)}),(0,U.jsx)(`strong`,{children:L.command_id})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:n(`action.status`)}),(0,U.jsx)(`strong`,{children:L.status})]}),(0,U.jsxs)(`div`,{className:`result-line`,children:[(0,U.jsx)(`span`,{children:n(`action.dryRun`)}),(0,U.jsx)(`strong`,{children:L.dry_run?n(`common.yes`):n(`common.no`)})]}),L.details&&(0,U.jsx)(mt,{value:JSON.stringify(L.details,null,2)})]}),(0,U.jsxs)(`div`,{className:`give-gift-form-actions`,children:[(0,U.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>le(!1),disabled:te,children:[te?(0,U.jsx)(A,{size:15,className:`spin`}):(0,U.jsx)(me,{size:15}),n(L?`action.runAgain`:`action.runDry`)]}),(0,U.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>le(!0),disabled:te||!ce,children:[(0,U.jsx)(oe,{size:15}),n(`giveGift.confirm`)]})]})]})}function _n(){let{t:e}=W(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(!1);async function d(){u(!0),c(``);try{let e=(await x.gifts()).Gifts??[];n(e),o(t=>t??e[0]??null)}catch(e){c(b(e))}finally{u(!1)}}(0,g.useEffect)(()=>{d()},[]);let f=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.GiftID).includes(e)||t.Title.toLowerCase().includes(e)):t},[t,r]);return(0,U.jsxs)(ot,{title:e(`giveGifts.pageTitle`),eyebrow:e(`giveGifts.eyebrow`),actions:(0,U.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(),disabled:l,children:[(0,U.jsx)(ge,{size:15}),` `,e(`common.refresh`)]}),children:[s&&(0,U.jsx)(lt,{children:s}),(0,U.jsx)(`p`,{className:`give-gift-upgrade-note`,children:e(`giveGifts.hint`)}),(0,U.jsxs)(`div`,{className:`give-gift-layout`,children:[(0,U.jsxs)(`section`,{className:`give-gift-picker`,children:[(0,U.jsxs)(`div`,{className:`give-gift-picker-head`,children:[(0,U.jsxs)(`label`,{className:`searchbox`,children:[(0,U.jsx)(_e,{size:15}),(0,U.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`giveGifts.searchPlaceholder`)})]}),(0,U.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:f.length,total:t.length})})]}),(0,U.jsxs)(`div`,{className:`give-gift-picker-list`,role:`listbox`,"aria-label":e(`giveGifts.pickGift`),children:[f.map(t=>{let n=a?.GiftID===t.GiftID;return(0,U.jsxs)(`button`,{type:`button`,role:`option`,"aria-selected":n,className:`give-gift-option ${n?`selected`:``} ${t.Enabled?``:`gift-row-disabled`}`,onClick:()=>o(t),children:[(0,U.jsx)(dn,{giftID:t.GiftID,revision:t.Revision,compact:!0}),(0,U.jsxs)(`span`,{className:`give-gift-option-info`,children:[(0,U.jsx)(`strong`,{children:t.Title||`Gift #${t.GiftID}`}),(0,U.jsxs)(`span`,{className:`mono`,children:[`#`,t.GiftID]}),(0,U.jsx)(`span`,{className:`give-gift-option-meta`,children:t.Enabled?(0,U.jsxs)(J,{children:[`⭐ `,t.Stars]}):(0,U.jsx)(J,{tone:`neutral`,children:e(`common.disabled`)})})]})]},t.GiftID)}),f.length===0&&!l&&(0,U.jsx)(`div`,{className:`official-gift-empty`,children:e(`common.noResults`)})]})]}),(0,U.jsx)(`section`,{className:`give-gift-panel`,children:a?(0,U.jsx)(gn,{gift:a,onDone:()=>void d()},a.GiftID):(0,U.jsxs)(`div`,{className:`give-gift-empty-panel`,children:[(0,U.jsx)(oe,{size:26}),(0,U.jsx)(`p`,{children:e(`giveGifts.selectPrompt`)})]})})]})]})}function vn({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1],i=e.path.match(/^\/bots\/(\d+)$/)?.[1];return n?(0,U.jsx)(Et,{id:Number(n),navigate:t}):r?(0,U.jsx)(Z,{id:Number(r),navigate:t}):i?(0,U.jsx)(Mt,{id:Number(i),navigate:t}):e.path===`/accounts`?(0,U.jsx)(At,{navigate:t}):e.path===`/channels`?(0,U.jsx)(jt,{navigate:t}):e.path===`/bots`?(0,U.jsx)(Nt,{navigate:t}):e.path===`/emoji`?(0,U.jsx)(zt,{}):e.path===`/gifts`?(0,U.jsx)(pn,{}):e.path===`/give-gifts`?(0,U.jsx)(_n,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,U.jsx)(Kt,{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,U.jsx)(Ht,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,U.jsx)(Gt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,U.jsx)(qt,{navigate:t}):(0,U.jsx)(Bt,{navigate:t})}function yn(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>G());(0,g.useEffect)(()=>{let e=()=>r(G());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(G())};return e===void 0?(0,U.jsx)(Ye,{}):e===null?(0,U.jsx)(ht,{onLogin:t}):(0,U.jsx)(Xe,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,U.jsx)(vn,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,U.jsx)(g.StrictMode,{children:(0,U.jsx)(Ge,{children:(0,U.jsx)(Fe,{children:(0,U.jsx)(yn,{})})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-XV_IEG5m.css b/cmd/telesrv-admin/web/dist/assets/index-XV_IEG5m.css new file mode 100644 index 00000000..3e37a247 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-XV_IEG5m.css @@ -0,0 +1 @@ +:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#eef1f5;--bg-accent:#e7ecf1;--panel:#fff;--panel-subtle:#f5f8fb;--panel-strong:#eef2f6;--surface-soft:#f2f7f6;--overlay:#18222f6b;--topbar-bg:#ffffffdb;--line:#e5eaf0;--line-strong:#d3dce4;--heading:#253040;--text:#333f4d;--text-soft:#45525f;--muted:#6d7885;--muted-2:#9aa4b1;--brand:#1f7d6f;--brand-strong:#196155;--brand-2:#3a6cae;--brand-tint:#e8f4f0;--brand-tint-border:#c8e2db;--brand-tint-text:#235d53;--good:#1f8a57;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a86a12;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#c0392b;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#1c2530;--sidebar-soft:#26313d;--sidebar-line:#313c4a;--sidebar-row:#232d38;--sidebar-text:#dbe3ec;--sidebar-muted:#8b98a8;--sidebar-faint:#7c8a9a;--sidebar-heading:#fff;--focus:#1f7d6f29;--shadow:0 12px 34px #1827381a;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #1f7d6f38;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#37a596;--brand-strong:#45b6a6;--brand-2:#6fa8e6;--brand-tint:#14322d;--brand-tint-border:#245349;--brand-tint-text:#7fd3c4;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#37a5963d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #37a59642}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow:var(--shadow-brand)}.brand-mark{color:#fff;background:var(--brand);border-radius:var(--radius-sm);border:1px solid #fff3;place-items:center;width:34px;height:34px;font-weight:800;display:grid}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border:1px solid var(--sidebar-line);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.language-switch{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:2px;display:inline-flex}.language-switch button{min-width:42px;min-height:24px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0 9px;font-weight:800;transition:color .14s,background-color .14s}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input{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{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.bot-create-fields{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.bot-create-fields .duration-field input{width:100%}.bot-create-actions{border-top:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;margin-top:14px;padding-top:14px;display:flex}.bot-create-note{color:var(--muted);font-size:12px;line-height:1.4}@media (width<=760px){.bot-create-fields{grid-template-columns:1fr}.bot-create-actions{flex-direction:column;align-items:stretch}}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:190px}.device-actions-cell{width:190px;min-width:190px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(82px,1fr));gap:6px;min-width:178px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.give-gift-summary{background:var(--panel-subtle);border:1px solid var(--line-strong);color:var(--text-soft);border-radius:12px;align-items:center;gap:11px;padding:11px 13px;display:flex}.give-gift-summary>svg{color:var(--brand);flex:none}.give-gift-summary strong{color:var(--text);font-size:13px;display:block}.give-gift-summary .mono{color:var(--muted);font-size:11px}.give-gift-tabs{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:12px;gap:4px;width:100%;padding:4px;display:flex}.give-gift-tabs .btn{min-height:36px;box-shadow:none;color:var(--text-soft);background:0 0;border:1px solid #0000;border-radius:9px;flex:1 1 0;justify-content:center;transition:color .15s,background .15s,border-color .15s,box-shadow .15s}.give-gift-tabs .btn:not(.primary):hover{color:var(--brand);background:var(--brand-tint)}.give-gift-tabs .btn.primary{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.give-gift-upgrade-note{background:var(--brand-tint);border:1px solid var(--brand-tint-border);color:var(--text-soft);border-radius:10px;margin:0;padding:9px 12px;font-size:11px;font-weight:650;line-height:1.45}.give-gift-attrs{grid-template-columns:repeat(4,minmax(0,1fr));align-items:end}.give-gift-attrs select,.give-gift-attrs input{width:100%;min-width:0;height:38px;color:var(--text);background-color:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;appearance:none;cursor:pointer;padding:0 32px 0 10px;font-size:12px;font-weight:600}.give-gift-attrs input{cursor:text;text-overflow:ellipsis;padding-right:10px}.give-gift-attrs select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 11px center;background-repeat:no-repeat}.give-gift-attrs select:focus,.give-gift-attrs input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.give-gift-layout{grid-template-columns:minmax(300px,380px) minmax(0,1fr);align-items:start;gap:16px;display:grid}.give-gift-picker{align-content:start;gap:10px;display:grid}.give-gift-picker-head{align-items:center;gap:12px;display:flex}.give-gift-picker-head .searchbox{flex:auto}.give-gift-picker-list{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-lg);scrollbar-gutter:stable;gap:8px;max-height:640px;padding:8px;display:grid;overflow:auto}.give-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);grid-template-columns:56px minmax(0,1fr);align-items:center;gap:11px;padding:9px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.give-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.give-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.give-gift-option .gift-animation-shell.compact{pointer-events:none}.give-gift-option-info{gap:3px;min-width:0;display:grid}.give-gift-option-info strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.give-gift-option-info .mono{color:var(--muted);font-size:10px}.give-gift-option-meta{flex-wrap:wrap;gap:6px;margin-top:2px;display:flex}.give-gift-panel{background:var(--panel);border:1px solid var(--line-strong);border-radius:var(--radius-lg);gap:12px;min-width:0;padding:16px;display:grid}.give-gift-form{gap:12px;min-width:0;display:grid}.give-gift-form-actions{flex-wrap:wrap;justify-content:flex-end;gap:10px;padding-top:4px;display:flex}.give-gift-empty-panel{color:var(--muted);text-align:center;place-items:center;gap:10px;padding:48px 20px;display:grid}.give-gift-empty-panel svg{color:var(--brand);opacity:.8}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:first-child{width:74px}.gift-table td{vertical-align:middle}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.give-gift-layout{grid-template-columns:1fr}.give-gift-picker-list{max-height:320px}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.attr-block{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;padding:10px;display:grid}.attr-block .duration-field input{width:100%}.attr-block .btn{justify-content:center;width:100%}.emoji-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;display:grid}.emoji-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);gap:8px;padding:12px;display:grid}.emoji-preview{background:var(--surface-soft);border:1px solid var(--line);border-radius:var(--radius-sm);place-items:center;height:88px;display:grid}.emoji-anim{width:80px;height:80px}.emoji-anim canvas{width:100%!important;height:100%!important}.emoji-glyph{font-size:46px;line-height:1}.emoji-meta{gap:4px;min-width:0;display:grid}.emoji-alt{font-size:18px;line-height:1.2}.emoji-id{color:var(--text);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;justify-content:space-between;align-items:center;gap:6px;padding:4px 8px;font-size:11px;display:inline-flex}.emoji-id .mono{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.emoji-id:hover{border-color:var(--brand-tint-border);color:var(--brand)}.emoji-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/index.html b/cmd/telesrv-admin/web/dist/index.html index 1c5443a7..cec1bc84 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -21,8 +21,8 @@ } })(); - - + +
diff --git a/cmd/telesrv-admin/web/src/api.ts b/cmd/telesrv-admin/web/src/api.ts index a568db36..6cc2afdd 100644 --- a/cmd/telesrv-admin/web/src/api.ts +++ b/cmd/telesrv-admin/web/src/api.ts @@ -4,6 +4,7 @@ import type { BotDetail, BotListResponse, ChannelDetail, + EmojiListResponse, ChannelListResponse, CommandResult, GroupMessageDetail, @@ -60,6 +61,8 @@ export const api = { channel: (id: number) => request(`/api/channels/${id}`), bots: (params: URLSearchParams) => request(`/api/bots?${params.toString()}`), bot: (id: number) => request(`/api/bots/${id}`), + emoji: (params: URLSearchParams) => request(`/api/emoji?${params.toString()}`), + emojiAnimation: (documentID: string) => request>(`/api/emoji/${encodeURIComponent(documentID)}/animation`), messages: (params: URLSearchParams) => request(`/api/messages?${params.toString()}`), message: (ownerUserID: number, msgID: number) => { const params = new URLSearchParams({ owner_user_id: String(ownerUserID), msg_id: String(msgID) }); diff --git a/cmd/telesrv-admin/web/src/components/Layout.tsx b/cmd/telesrv-admin/web/src/components/Layout.tsx index 411fc0ab..06084cf5 100644 --- a/cmd/telesrv-admin/web/src/components/Layout.tsx +++ b/cmd/telesrv-admin/web/src/components/Layout.tsx @@ -8,8 +8,10 @@ import { Server, Shield, ShieldCheck, + Smile, Users, - Gift + Gift, + Send } from "lucide-react"; import { useEffect, useState, type ReactNode } from "react"; import { api } from "../api"; @@ -79,6 +81,8 @@ export function Shell({ } href="/channels" route={route} navigate={navigate}>{t("layout.channels")} } href="/bots" route={route} navigate={navigate}>{t("layout.bots")} } href="/gifts" route={route} navigate={navigate}>{t("layout.gifts")} + } href="/give-gifts" route={route} navigate={navigate}>{t("layout.giveGifts")} + } href="/emoji" route={route} navigate={navigate}>{t("layout.emoji")}
+ +
{t("attr.attributes")}
+ + + + } /> diff --git a/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx b/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx index cb6bf302..08d0d201 100644 --- a/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx @@ -2,6 +2,7 @@ import { ChevronRight, Loader2, RefreshCw, Search } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { ScamFakeBadges } from "../components/flags"; import { useI18n } from "../i18n"; import { displayName, displayPhone, displayUsername, formatDate, formatUnix } from "../lib/format"; import { accountMetrics } from "../lib/metrics"; @@ -111,7 +112,7 @@ export function AccountsPage({ navigate }: { navigate: Navigate }) { {row.DeviceCount} {formatDate(row.LastActiveAt)} {row.PremiumUntil > 0 ? {t("account.premium")} {formatUnix(row.PremiumUntil)} : {t("common.none")}} - {row.Verified ? {t("common.verified")} : {t("account.notVerified")}} + {row.Verified ? {t("common.verified")} : {t("account.notVerified")}} {row.Frozen ? {t("account.frozen")} : {t("common.normal")}} {formatDate(row.UpdatedAt)} diff --git a/cmd/telesrv-admin/web/src/pages/BotDetailPage.tsx b/cmd/telesrv-admin/web/src/pages/BotDetailPage.tsx index f6bb5804..d91ce85c 100644 --- a/cmd/telesrv-admin/web/src/pages/BotDetailPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/BotDetailPage.tsx @@ -3,6 +3,8 @@ import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { ActionButton } from "../components/ActionButton"; import { Alert, AuditTable, Badge, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui"; +import { ScamFakeActions, ScamFakeBadges } from "../components/flags"; +import { ColorAction, EmojiStatusAction, UsernameAction } from "../components/attributes"; import { useI18n } from "../i18n"; import { displayUsername, formatDate } from "../lib/format"; import type { Navigate } from "../routing"; @@ -55,6 +57,7 @@ export function BotDetailPage({ id, navigate }: { id: number; navigate: Navigate
{bot.System ? t("bots.system") : t("bots.user")} {bot.Verified ? {t("common.verified")} : {t("account.notVerified")}} +
@@ -85,6 +88,11 @@ export function BotDetailPage({ id, navigate }: { id: number; navigate: Navigate onDone={load} />
+ +
{t("attr.attributes")}
+ + + {bot.System ? (

{t("bots.systemHint")}

) : ( diff --git a/cmd/telesrv-admin/web/src/pages/BotsPage.tsx b/cmd/telesrv-admin/web/src/pages/BotsPage.tsx index a4cde501..d8890cba 100644 --- a/cmd/telesrv-admin/web/src/pages/BotsPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/BotsPage.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { ActionButton } from "../components/ActionButton"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { ScamFakeBadges } from "../components/flags"; import { useI18n } from "../i18n"; import { displayUsername, formatDate, toInt } from "../lib/format"; import type { Navigate } from "../routing"; @@ -152,7 +153,7 @@ export function BotsPage({ navigate }: { navigate: Navigate }) { {displayUsername(row.Username) || "-"} {row.FirstName || "-"} {row.OwnerUserID > 0 ? row.OwnerUserID : "-"} - {row.Verified ? {t("common.verified")} : {t("account.notVerified")}} + {row.Verified ? {t("common.verified")} : {t("account.notVerified")}} {row.System ? {t("bots.system")} : {t("bots.user")}} {formatDate(row.CreatedAt)} diff --git a/cmd/telesrv-admin/web/src/pages/ChannelDetailPage.tsx b/cmd/telesrv-admin/web/src/pages/ChannelDetailPage.tsx index f46c5a02..351b55dd 100644 --- a/cmd/telesrv-admin/web/src/pages/ChannelDetailPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/ChannelDetailPage.tsx @@ -4,6 +4,8 @@ import { api, errorMessage } from "../api"; import { ActionButton } from "../components/ActionButton"; import { Alert, AuditTable, Badge, JsonBlock, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui"; import { useI18n } from "../i18n"; +import { ScamFakeActions, ScamFakeBadges } from "../components/flags"; +import { ChannelSettingsAction, ColorAction, EmojiStatusAction, UsernameAction } from "../components/attributes"; import { channelKind, displayUsername, formatDate, formatUnix } from "../lib/format"; import type { Navigate } from "../routing"; import type { ChannelDetail } from "../types"; @@ -51,6 +53,7 @@ export function ChannelDetailPage({ id, navigate }: { id: number; navigate: Navi
{channelKind(ch, t)} {ch.Verified ? {t("common.verified")} : {t("account.notVerified")}} + {ch.Deleted ? {t("common.deleted")} : {t("common.valid")}}
@@ -86,6 +89,13 @@ export function ChannelDetailPage({ id, navigate }: { id: number; navigate: Navi payload={() => ({ channel_id: ch.ID, verified: !ch.Verified })} onDone={load} /> + +
{t("attr.settings")}
+ +
{t("attr.attributes")}
+ + + } /> diff --git a/cmd/telesrv-admin/web/src/pages/ChannelsPage.tsx b/cmd/telesrv-admin/web/src/pages/ChannelsPage.tsx index 9f45b878..f7db9567 100644 --- a/cmd/telesrv-admin/web/src/pages/ChannelsPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/ChannelsPage.tsx @@ -2,6 +2,7 @@ import { ChevronRight, Loader2, RefreshCw, Search } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { ScamFakeBadges } from "../components/flags"; import { useI18n } from "../i18n"; import { channelKind, displayUsername, formatDate } from "../lib/format"; import { channelMetrics } from "../lib/metrics"; @@ -110,7 +111,7 @@ export function ChannelsPage({ navigate }: { navigate: Navigate }) { {row.ParticipantsCount} {row.AdminsCount} {row.PTS} - {row.Verified ? {t("common.verified")} : {t("account.notVerified")}} + {row.Verified ? {t("common.verified")} : {t("account.notVerified")}} {formatDate(row.UpdatedAt)} diff --git a/cmd/telesrv-admin/web/src/pages/EmojiPage.tsx b/cmd/telesrv-admin/web/src/pages/EmojiPage.tsx new file mode 100644 index 00000000..ee139725 --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/EmojiPage.tsx @@ -0,0 +1,158 @@ +import lottie from "lottie-web/build/player/lottie_light_canvas"; +import { Check, ChevronRight, Copy, Loader2, RefreshCw, Search } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { api, errorMessage } from "../api"; +import { Alert, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { useI18n } from "../i18n"; +import type { EmojiListResponse, EmojiRow } from "../types"; + +function formatBytes(value: number): string { + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${(value / (1024 * 1024)).toFixed(1)} MB`; +} + +function isAnimated(mime: string): boolean { + const m = mime.toLowerCase(); + return m.includes("tgsticker") || m.includes("lottie") || m.includes("json"); +} + +function EmojiPreview({ row }: { row: EmojiRow }) { + const host = useRef(null); + const animation = useRef | null>(null); + const [failed, setFailed] = useState(!isAnimated(row.MimeType)); + + useEffect(() => { + if (!isAnimated(row.MimeType)) { + setFailed(true); + return; + } + let cancelled = false; + api.emojiAnimation(row.DocumentID).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(() => setFailed(true)); + return () => { + cancelled = true; + animation.current?.destroy(); + animation.current = null; + }; + }, [row.DocumentID, row.MimeType]); + + if (failed) { + return
{row.Alt || "🙂"}
; + } + return
; +} + +function EmojiCard({ row }: { row: EmojiRow }) { + const { t } = useI18n(); + const [copied, setCopied] = useState(false); + + async function copy() { + try { + await navigator.clipboard.writeText(row.DocumentID); + setCopied(true); + setTimeout(() => setCopied(false), 1200); + } catch { + // Clipboard is best-effort. + } + } + + return ( +
+
+
+ {row.Alt || "—"} + + {row.SetTitle || t("emoji.noSet")} · {formatBytes(row.Size)} +
+
+ ); +} + +export function EmojiPage() { + const { t } = useI18n(); + const [q, setQ] = useState(""); + const [data, setData] = useState(null); + const [cursor, setCursor] = useState(0); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + async function load(next = false) { + setBusy(true); + setError(""); + const params = new URLSearchParams(); + if (q.trim()) { + params.set("q", q.trim()); + } else if (next) { + params.set("before_id", String(cursor)); + } + try { + const result = await api.emoji(params); + setData(result); + setCursor(result.next_before_id); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + useEffect(() => { + void load(false); + }, []); + + const rows = data?.rows ?? []; + + return ( + load(false)} disabled={busy}> + {t("common.refresh")} + + } + > + {error && {error}} +
+ +
+ +
{ event.preventDefault(); void load(false); }}> + + + {data?.listing && data.has_more && ( + + )} +
+
+

{t("emoji.hint")}

+ {rows.length === 0 ? ( +
{t("common.noResults")}
+ ) : ( +
+ {rows.map((row) => )} +
+ )} +
+ ); +} diff --git a/cmd/telesrv-admin/web/src/pages/GiftsPage.tsx b/cmd/telesrv-admin/web/src/pages/GiftsPage.tsx index c6441b18..5301c765 100644 --- a/cmd/telesrv-admin/web/src/pages/GiftsPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/GiftsPage.tsx @@ -23,7 +23,7 @@ function formatBytes(value: number | string) { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } -function LottiePreview({ giftID, revision, compact = false }: { giftID: string; revision: number; compact?: boolean }) { +export function LottiePreview({ giftID, revision, compact = false }: { giftID: string; revision: number; compact?: boolean }) { const host = useRef(null); const animation = useRef | null>(null); const [playing, setPlaying] = useState(true); diff --git a/cmd/telesrv-admin/web/src/pages/GiveGiftForm.tsx b/cmd/telesrv-admin/web/src/pages/GiveGiftForm.tsx new file mode 100644 index 00000000..829a648a --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/GiveGiftForm.tsx @@ -0,0 +1,229 @@ +import { CheckCircle2, CircleAlert, Gift, Loader2, Play, User, Users } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { api, errorMessage } from "../api"; +import { ChannelPicker, UserPicker } from "../components/EntityPicker"; +import { Alert, JsonBlock } from "../components/ui"; +import { useI18n } from "../i18n"; +import type { AccountRow, ChannelRow, CommandResult, StarGiftCollectibleAttributeRow, StarGiftCollectiblePreview, StarGiftRow } from "../types"; + +const SYSTEM_SENDER = "777000"; + +type RecipientKind = "user" | "channel"; + +function attrLabel(attr: StarGiftCollectibleAttributeRow): string { + const rarity = attr.rarity_permille > 0 ? ` · ${(attr.rarity_permille / 10).toFixed(1)}%` : ""; + return `${attr.name || `#${attr.id}`}${rarity}`; +} + +export function GiveGiftForm({ gift, onDone }: { gift: StarGiftRow; onDone?: () => void }) { + const { t } = useI18n(); + const [kind, setKind] = useState("user"); + const [user, setUser] = useState(null); + const [channel, setChannel] = useState(null); + const [sender, setSender] = useState(SYSTEM_SENDER); + const [message, setMessage] = useState(""); + const [hideName, setHideName] = useState(false); + const [upgrade, setUpgrade] = useState(false); + const [preview, setPreview] = useState(null); + const [previewError, setPreviewError] = useState(""); + const [modelID, setModelID] = useState("0"); + const [patternID, setPatternID] = useState("0"); + const [backdropID, setBackdropID] = useState("0"); + const [num, setNum] = useState(""); + const [reason, setReason] = useState(""); + const [result, setResult] = useState(null); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + + const recipientID = kind === "user" ? user?.ID ?? 0 : channel?.ID ?? 0; + const upgradable = kind === "user" && upgrade; + + // Reset the collectible selection whenever the chosen gift changes; the + // recipient/sender/message are intentionally preserved for fast re-issuing. + useEffect(() => { + setUpgrade(false); + setPreview(null); + setPreviewError(""); + setModelID("0"); + setPatternID("0"); + setBackdropID("0"); + setNum(""); + setResult(null); + setError(""); + }, [gift.GiftID]); + + useEffect(() => { + if (!upgradable || preview) return; + let cancelled = false; + setPreviewError(""); + api.giftCollectibles(gift.GiftID) + .then((data) => { if (!cancelled) setPreview(data); }) + .catch((err) => { if (!cancelled) setPreviewError(errorMessage(err)); }); + return () => { cancelled = true; }; + }, [upgradable, preview, gift.GiftID]); + + function buildPayload(confirm: boolean): Record { + const senderID = Number.parseInt(sender.trim() || SYSTEM_SENDER, 10); + const parsedNum = Number.parseInt(num.trim(), 10); + return { + gift_id: gift.GiftID, + sender_user_id: Number.isFinite(senderID) ? senderID : 0, + user_id: kind === "user" ? recipientID : 0, + channel_id: kind === "channel" ? recipientID : 0, + hide_name: hideName, + message: message.trim(), + upgrade: upgradable, + model_attribute_id: upgradable ? modelID : "0", + pattern_attribute_id: upgradable ? patternID : "0", + backdrop_attribute_id: upgradable ? backdropID : "0", + num: upgradable && Number.isFinite(parsedNum) && parsedNum > 0 ? parsedNum : 0, + reason: reason.trim(), + confirm + }; + } + + const previewPayload = useMemo(() => buildPayload(false), [gift.GiftID, kind, recipientID, sender, message, hideName, upgrade, modelID, patternID, backdropID, num, reason]); + const canConfirm = result?.dry_run && !result.error; + + async function run(confirm: boolean) { + if (recipientID <= 0) { + setError(t("giveGift.recipientRequired")); + return; + } + if (!reason.trim()) { + setError(t("action.reasonRequired")); + return; + } + setBusy(true); + setError(""); + try { + const commandResult = await api.action("/api/actions/give-gift", buildPayload(confirm)); + setResult(commandResult); + if (confirm && !commandResult.error) { + onDone?.(); + } + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + return ( +
+
+ +
+ {gift.Title || `Gift #${gift.GiftID}`} + #{gift.GiftID} · ⭐ {gift.Stars} +
+
+ +
+ + +
+ + {kind === "user" + ? { setUser(row); setResult(null); }} /> + : { setChannel(row); setResult(null); }} />} + + + +