@jant/core 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/telegram/register-webhooks.js +93 -0
- package/dist/{app-BtNdUAqz.js → app-BIkkbVQk.js} +2252 -383
- package/dist/app-Bcr5_wZI.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-Bo7sKkAQ.js +274 -0
- package/dist/client/_assets/client-QHRvzZwk.css +2 -0
- package/dist/client/_assets/{client-auth-DJ_5wx9N.js → client-auth-D1jDQgbH.js} +81 -81
- package/dist/{env-CgaH9Mut.js → env-C7e2Nlnt.js} +30 -1
- package/dist/{export-CR9Megtb.js → export-Bbn86HmS.js} +1 -1
- package/dist/{github-sync-DYZq9rQp.js → github-sync-CBQPRZ8H.js} +1 -1
- package/dist/{github-sync-8Vv06aCr.js → github-sync-dXsiZa_e.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/node.js +61 -5
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +15 -2
- package/src/app.tsx +3 -0
- package/src/client/thread-context.ts +146 -2
- package/src/client/tiptap/__tests__/link-toolbar.test.ts +1 -1
- package/src/client/tiptap/bubble-menu.ts +1 -16
- package/src/client/tiptap/extensions.ts +2 -6
- package/src/client/tiptap/link-toolbar.ts +0 -21
- package/src/client/tiptap/toolbar-mode.ts +0 -43
- package/src/db/migrations/0022_old_gressill.sql +24 -0
- package/src/db/migrations/0023_broad_terror.sql +20 -0
- package/src/db/migrations/0024_red_the_twelve.sql +3 -0
- package/src/db/migrations/0025_exotic_wendell_rand.sql +1 -0
- package/src/db/migrations/meta/0022_snapshot.json +2267 -0
- package/src/db/migrations/meta/0023_snapshot.json +2396 -0
- package/src/db/migrations/meta/0024_snapshot.json +2417 -0
- package/src/db/migrations/meta/0025_snapshot.json +2424 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0020_bizarre_smasher.sql +24 -0
- package/src/db/migrations/pg/0021_sharp_puppet_master.sql +20 -0
- package/src/db/migrations/pg/0022_blushing_blue_shield.sql +3 -0
- package/src/db/migrations/pg/0023_organic_zemo.sql +1 -0
- package/src/db/migrations/pg/meta/0020_snapshot.json +2904 -0
- package/src/db/migrations/pg/meta/0021_snapshot.json +3060 -0
- package/src/db/migrations/pg/meta/0022_snapshot.json +3078 -0
- package/src/db/migrations/pg/meta/0023_snapshot.json +3084 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +82 -0
- package/src/db/schema.ts +90 -0
- package/src/i18n/coverage.generated.ts +2 -2
- package/src/i18n/locales/public/en.po +8 -0
- package/src/i18n/locales/public/zh-Hans.po +8 -0
- package/src/i18n/locales/public/zh-Hant.po +8 -0
- package/src/i18n/locales/settings/en.po +135 -0
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +136 -1
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +136 -1
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/image-dimensions.test.ts +314 -0
- package/src/lib/__tests__/telegram-entities.test.ts +180 -0
- package/src/lib/__tests__/telegram-pool-webhooks.test.ts +127 -0
- package/src/lib/env.ts +45 -0
- package/src/lib/ids.ts +3 -0
- package/src/lib/image-dimensions.ts +258 -0
- package/src/lib/telegram-entities.ts +240 -0
- package/src/lib/telegram-pool-webhooks.ts +86 -0
- package/src/lib/telegram-settings-status.tsx +109 -0
- package/src/lib/telegram.ts +363 -0
- package/src/node/runtime.ts +6 -0
- package/src/routes/api/__tests__/telegram.test.ts +612 -0
- package/src/routes/api/telegram.ts +782 -0
- package/src/routes/api/upload-multipart.ts +34 -12
- package/src/routes/api/upload.ts +23 -2
- package/src/routes/dash/settings.tsx +131 -1
- package/src/routes/pages/__tests__/post-page-title.test.ts +70 -0
- package/src/routes/pages/page.tsx +3 -2
- package/src/runtime/cloudflare.ts +20 -9
- package/src/runtime/node.ts +20 -9
- package/src/runtime/site.ts +2 -1
- package/src/services/__tests__/telegram.test.ts +148 -0
- package/src/services/index.ts +9 -0
- package/src/services/telegram.ts +613 -0
- package/src/services/upload-session.ts +39 -12
- package/src/styles/tokens.css +1 -0
- package/src/styles/ui.css +116 -38
- package/src/types/app-context.ts +6 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +40 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +48 -17
- package/src/ui/dash/settings/TelegramContent.tsx +549 -0
- package/src/ui/feed/ThreadPreview.tsx +91 -38
- package/src/ui/feed/__tests__/thread-preview.test.ts +67 -5
- package/src/ui/pages/PostPage.tsx +78 -15
- package/dist/app-DLINgGBd.js +0 -6
- package/dist/client/_assets/client-BErXNT6k.css +0 -2
- package/dist/client/_assets/client-CtAgWT8i.js +0 -274
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as getSitePathPrefix, c as normalizePath, d as sanitizeUrl, f as slugify, g as toPublicPath, h as toPublicHref, i as getSiteOrigin, m as toAbsoluteSiteUrl, n as extractDisplayDomain, o as isFullUrl, p as stripSitePathPrefix, r as extractDomain, s as isSafeInternalRedirect, t as buildSiteUrl, u as normalizeSiteUrl, v as __exportAll } from "./url-umUptr5z.js";
|
|
2
|
-
import { A as JANT_POSITIVE_LOGO_PNG_FILENAME, B as getJantLogoHref, C as formatYearMonth, D as HOME_BRANDING_LINK_LABEL, E as toISOString, F as getJantBundledAsset, G as base64ToUint8Array, H as JANT_LOGO_PATH_DATA, I as getJantIconFilename, L as getJantIconHref, M as getDefaultJantAppleTouchIconBytes, N as getDefaultJantFaviconIcoBytes, O as HOME_BRANDING_PREFIX, P as getJantBrandPackHref, R as getJantLogoFilename, S as formatTime, U as JANT_LOGO_VIEW_BOX, V as getJantPositiveLogoPngHref, W as arrayBufferToBase64, _ as getMediaUrl, b as formatRelativeAge, d as extractSummaryHtml, f as renderTiptapDocument, g as getImageUrl, h as escapeHtml, i as tiptapJsonToMarkdown, j as JANT_REPO_URL, k as JANT_BRAND_PACK_FILENAME, l as extractBodyText, m as trimTiptapBody, o as render, p as renderTiptapJson, s as toPlainText, t as createExportService, u as extractSummary, v as getPublicUrlForProvider, w as now, x as formatRelativeTime, y as formatDate, z as getJantLogoFills } from "./export-
|
|
3
|
-
import {
|
|
4
|
-
import { l as markdownToTiptapJson, o as createGitHubSyncService } from "./github-sync-
|
|
2
|
+
import { A as JANT_POSITIVE_LOGO_PNG_FILENAME, B as getJantLogoHref, C as formatYearMonth, D as HOME_BRANDING_LINK_LABEL, E as toISOString, F as getJantBundledAsset, G as base64ToUint8Array, H as JANT_LOGO_PATH_DATA, I as getJantIconFilename, L as getJantIconHref, M as getDefaultJantAppleTouchIconBytes, N as getDefaultJantFaviconIcoBytes, O as HOME_BRANDING_PREFIX, P as getJantBrandPackHref, R as getJantLogoFilename, S as formatTime, U as JANT_LOGO_VIEW_BOX, V as getJantPositiveLogoPngHref, W as arrayBufferToBase64, _ as getMediaUrl, b as formatRelativeAge, d as extractSummaryHtml, f as renderTiptapDocument, g as getImageUrl, h as escapeHtml, i as tiptapJsonToMarkdown, j as JANT_REPO_URL, k as JANT_BRAND_PACK_FILENAME, l as extractBodyText, m as trimTiptapBody, o as render, p as renderTiptapJson, s as toPlainText, t as createExportService, u as extractSummary, v as getPublicUrlForProvider, w as now, x as formatRelativeTime, y as formatDate, z as getJantLogoFills } from "./export-Bbn86HmS.js";
|
|
3
|
+
import { S as getTelegramWebhookSecret, T as coalesceDisplayText, _ as getInternalAdminToken, a as getConfiguredSingleSiteUrl, b as getSiteResolutionMode, c as getDevApiToken, d as getHostedControlPlaneBaseUrl, f as getHostedControlPlaneDomainCheckSecret, g as getHostedControlPlaneSsoSecret, h as getHostedControlPlaneProviderLabel$1, i as getConfiguredSingleSitePathPrefix, l as getEnvString, m as getHostedControlPlaneInternalToken, n as getAuthSecret, o as getConfiguredStorageDriver, p as getHostedControlPlaneInternalBaseUrl, r as getConfiguredSingleSiteOrigin, s as getCorsOrigins, u as getGitHubAppConfig, v as getLocalStoragePath, w as shouldUseSecureCookies, x as getTelegramBotPool } from "./env-C7e2Nlnt.js";
|
|
4
|
+
import { l as markdownToTiptapJson, o as createGitHubSyncService } from "./github-sync-CBQPRZ8H.js";
|
|
5
5
|
import { a as listInstallationReposPage, n as getInstallation, o as searchInstallationRepos, t as buildInstallUrl } from "./github-app-D0GvNnqp.js";
|
|
6
6
|
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-Bh0PH3zr.js";
|
|
7
7
|
import { I18n } from "@lingui/core";
|
|
@@ -15,6 +15,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
|
15
15
|
import { drizzle as drizzle$1 } from "drizzle-orm/d1";
|
|
16
16
|
import { check, foreignKey, index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
17
17
|
import { boolean, check as check$1, customType, foreignKey as foreignKey$1, index as index$1, integer as integer$1, pgTable, primaryKey as primaryKey$1, text as text$1, timestamp, unique, uniqueIndex as uniqueIndex$1 } from "drizzle-orm/pg-core";
|
|
18
|
+
import { renderSVG } from "uqr";
|
|
18
19
|
import { APIError, betterAuth } from "better-auth";
|
|
19
20
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
20
21
|
import { verifyPassword } from "better-auth/crypto";
|
|
@@ -1915,13 +1916,13 @@ var Hono = class extends Hono$1 {
|
|
|
1915
1916
|
var messages$3 = JSON.parse("{\"+4u2g6\":[\"A ready-made 1:1 PNG for decks, mockups, directories, and other square placements.\"],\"+DPYOZ\":[\"Add a link to your main RSS feed. Change what /feed returns in General.\"],\"+G8qqW\":[\"Collection saved.\"],\"+IJm1Z\":[\"Muted\"],\"+Irvp3\":[\"Everything on this page is ready to use for articles, launch posts, directories, and product coverage.\"],\"+Qaboy\":[\"Favicon\"],\"+fWu2O\":[\"A calmer, warmer accent makes the default theme feel quieter and more intentional.\"],\"+nHhRH\":[\"Use \",[\"brandColorName\"]],\"+siMqD\":[\"Journal\"],\"/DFKdU\":[\"Type the quote...\"],\"/PfPLc\":[\"Label (optional)\"],\"/PoNoq\":[\"Edit link\"],\"/Ui2OV\":[\"Use the reverse logo on dark backgrounds.\"],\"/Ybds4\":[\"Primary Jant logo for websites, docs, press coverage, and editorial layouts.\"],\"/rTz0M\":[\"Audio\"],\"0EcUWz\":[\"Discard changes?\"],\"0Lj7or\":[\"Save text attachment?\"],\"0XDp7X\":[\"Links should read clearly without glowing against the page.\"],\"0ieXE7\":[\"Highest rated\"],\"11h9eK\":[\"Includes\"],\"15++NM\":[\"Inline emphasis\"],\"1DBGsz\":[\"Notes\"],\"1NeeWI\":[\"Square assets for avatars, apps, browsers, and shared links\"],\"1THMr2\":[\"Brand pack\"],\"1njn7W\":[\"Light\"],\"2B7HLH\":[\"New post\"],\"2C7mSG\":[\"Collection link\"],\"2ETv7R\":[\"Tune color in a real reading context\"],\"2HbvFp\":[\"Real post components\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2koDOQ\":[\"Thread accents\"],\"2lKpcz\":[\"Write your first post to get started.\"],\"2q/Q7x\":[\"Visibility\"],\"2sCqzD\":[\"Use this for websites, docs, articles, and other light or neutral surfaces.\"],\"33DClx\":[\"Link to your latest posts. If it comes before Featured, the homepage opens here.\"],\"3Cw1AI\":[\"Add Collection\"],\"3lJk5u\":[\"Keep the artwork unchanged.\"],\"3mdteM\":[\"before deciding whether the accent is carrying too much product energy.\"],\"3neqtf\":[\"Thread accent\"],\"3qkggm\":[\"Fullscreen\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"3xi01/\":[\"Look at the footer metadata last, to make sure the accent is not fighting the typography.\"],\"47iMgt\":[\"Editorial interfaces worth borrowing from\"],\"4D09NB\":[\"Link to your collections page\"],\"4HLTdq\":[\"without media\"],\"4J/OYU\":[\"Collection created.\"],\"4eiXo+\":[\"Leave blank to generate one automatically.\"],\"4pV0kE\":[\"Avatar-ready\"],\"51EYZX\":[\"without title\"],\"5dcjwM\":[\"Choose today or an earlier date, or leave it blank to publish now.\"],\"5pAjd8\":[\"Accent should feel present, not loud.\"],\"5sEkBi\":[\"Open raw asset\"],\"6UTABI\":[\"Collection order updated.\"],\"6WAK+2\":[\"Use current date\"],\"6Y4BBO\":[\"An abstract editorial layout in warm paper colors\"],\"6cjUDB\":[\"Brand assets\"],\"6lGV3K\":[\"Show less\"],\"6p0JeQ\":[\"to make sure both still feel like they belong to the same product.\"],\"6sVyMq\":[\"Add a URL before posting this link.\"],\"6yCv8j\":[\"Save these changes to the text attachment, discard them, or keep editing.\"],\"74kJNs\":[\"View earlier notes in this thread\"],\"7DvUqV\":[\"Read the page from top to bottom without looking at the swatches.\"],\"7aris6\":[\"March 15\"],\"7d1a0d\":[\"Public\"],\"7hYXO0\":[\"Use this on dark backgrounds, image-backed surfaces, and any placement where the green logo would lose contrast.\"],\"7kMW54\":[\"Open raw SVG\"],\"7nGhhM\":[\"What's on your mind?\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8Btgys\":[\"Draft deleted.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8ZsakT\":[\"Password\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8eC78s\":[\"A calmer accent makes\"],\"8tM8+a\":[\"Save as draft\"],\"90IRF2\":[\"This article is here to answer a specific question: does the default accent still feel calm once it has to carry a full reading experience?\"],\"9SHZas\":[\"Shows 'Settings' when logged in, 'Sign in' when logged out\"],\"9aloPG\":[\"References\"],\"9dr9Nh\":[\"Open external link\"],\"9qWoxS\":[\"Feed\"],\"A1D8Yt\":[\"What the accent should do\"],\"A1taO8\":[\"Search\"],\"A2Vg/u\":[\"Navigation and reading states\"],\"AjHkcv\":[\"Default preview image for social shares and link unfurls.\"],\"AyHO4m\":[\"What's this collection about?\"],\"B1FFMj\":[\"Download Brand Pack\"],\"B495Gs\":[\"Archive\"],\"BdjLtf\":[\"thread\"],\"Bmaby2\":[\"All formats\"],\"C+9df9\":[\"Quoted or highlighted passages should feel like annotations, not warnings.\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"CAh1km\":[\"collections\"],\"CH3bgf\":[\"RSS feed for this view\"],\"CT7H2e\":[\"Link to your featured posts. If it comes before Latest, the homepage opens here.\"],\"CmBCXY\":[\"Link updated.\"],\"D4em/+\":[\"Logos\"],\"DHhJ7s\":[\"Previous\"],\"DJLY+/\":[\" and try again.\"],\"DOx286\":[\"Draft restored.\"],\"DPfwMq\":[\"Done\"],\"DSJXZM\":[\"Enter a valid date.\"],\"DYlMYF\":[\"Built-in background\"],\"DoJzLz\":[\"Collections\"],\"Du2B9f\":[\"The default accent should support reading first. Start by comparing it against the\"],\"DxwUcG\":[\"Read the palette as content first\"],\"E3NcGH\":[\"Square logo PNG\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EHWwm1\":[\"The default accent should feel written, not branded.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"EQNPYo\":[\"Featured on \",[\"date\"],\" at \",[\"time\"]],\"EQtz4D\":[\"Open a few links and check whether they still feel native to the page.\"],\"EU3tBD\":[\"Link removed.\"],\"EetoJL\":[\"Guide the eye without taking over the layout.\"],\"Eiv3bO\":[\"Buttons can stay steady, but links, thread markers, and subtle emphasis should feel closer to ink on paper than dashboard chrome.\"],\"ElTnWL\":[\"Published on\"],\"EmQw8O\":[\"If this article still feels like a page you want to keep reading, the palette is probably close.\"],\"EsJdRp\":[\"Save theme\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FEr96N\":[\"Theme\"],\"FGySZL\":[\"The default accent works best when it reads like a fountain-pen underline. Compare it against the\"],\"FM+KeU\":[\"No drafts yet. Save a draft to find it here.\"],\"Fdv5k7\":[\"What to look for while tuning it\"],\"FkMol5\":[\"Featured\"],\"Fxf4jq\":[\"Description (optional)\"],\"G2u/aQ\":[\"Download official Jant logos, icons, and preview assets.\"],\"GBJzTZ\":[\"Archive\"],\"GX2VMa\":[\"Create your admin account.\"],\"GY/1J4\":[\"Jant fallback canary string\"],\"GbIOhd\":[\"Reply quietly\"],\"GiRWtR\":[\"Why the default accent should feel written, not branded\"],\"GkpIs2\":[\"Remove this link from Collections? The destination won't change.\"],\"GorKul\":[\"Welcome to Jant\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"H4lgRd\":[\"Authentication isn't set up. Check your server config.\"],\"HFPGej\":[\"No threads match these filters. Try adjusting your selection or clear all filters.\"],\"HG79RB\":[\"Post as Private\"],\"HNEHJP\":[\"Demo credentials are pre-filled — hit Sign In to continue.\"],\"HbAIQc\":[\"A reference link for checking whether the accent feels editorial instead of promotional.\"],\"Ht1V3q\":[\"For the same reason, inline code should stay neutral. Something like theme.siteAccent = soften(green, 12%) should not suddenly become the loudest thing on the page.\"],\"I22eN0\":[\"Shared links\"],\"I6zLrz\":[\"Use these when you need a transparent square logo, a shaped tile with a built-in background, a browser icon, or a default preview image.\"],\"ICsA6P\":[\"You have unsaved changes\"],\"IUX7p+\":[\"White logo on the Jant green rounded tile for app icon mockups, touch icons, directory listings, and other square placements that should feel softer.\"],\"IagCbF\":[\"URL\"],\"IjnQHI\":[\"with title\"],\"ImOQa9\":[\"Reply\"],\"IsI3kE\":[\"Nothing here yet. Add posts to one of these collections to fill this view.\"],\"J+2Rls\":[\"Leave blank to publish now. Use an earlier date when importing older posts.\"],\"J4tAHl\":[\"Headings should keep their hierarchy even when the accent gets softer.\"],\"JYj5R2\":[\"Browse files\"],\"JcD7qf\":[\"More actions\"],\"JqJ5Xv\":[\"Latest\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"JwLPQ/\":[\"This sign-in link has expired. Return to \"],\"KOqvXP\":[\"Do not recolor, stretch, rotate, outline, or add effects to the logo.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"Khu3PV\":[\"Publish settings\"],\"KiJn9B\":[\"Note\"],\"KlZ+t+\":[\"%name% + %count% more\"],\"KsvRin\":[\"Hide from Latest\"],\"KzmC5L\":[\"Controls\"],\"L7svJg\":[\"Reading\"],\"Lbkbwy\":[\"A quote card for judging accent color against softer, citation-heavy content.\"],\"LcvzvX\":[\"Tap to retry\"],\"LkA8jz\":[\"Add alt text\"],\"LxRg6f\":[\"live theme controls\"],\"M4tzVU\":[\"Latest posts\"],\"M8kJqa\":[\"Drafts\"],\"MHrjPM\":[\"Title\"],\"MILa7n\":[\"Square tile\"],\"MSc/Yq\":[\"Do you want to publish your changes or discard them?\"],\"Mc7+6G\":[\"Enter a valid URL starting with http://, https://, or mailto:.\"],\"MdMyne\":[\"Source link (optional)\"],\"MiMY3Q\":[\"Apple touch icon\"],\"MiyoI7\":[\"default note sample\"],\"MqghUt\":[\"Search posts...\"],\"Myqkib\":[\"Create a collection to get started.\"],\"N8UzTV\":[\"Replies\"],\"NAFbuE\":[\"Search snippet\"],\"NH9Z1R\":[\"Start here\"],\"NqsRbb\":[\"Jant logo\"],\"NvXuWk\":[\"Won't move the thread to the top of latest.\"],\"O1367B\":[\"All collections\"],\"O3oNi5\":[\"Email\"],\"OEdMhi\":[\"The best default color is the one you notice only after reading for a while.\"],\"OEt/to\":[\"Guidelines\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OaoJcz\":[\"Social preview\"],\"OmfDbR\":[\"Site accent\"],\"Ovks1h\":[\"A softer blue feels more like ink than product chrome.\"],\"P/sHNL\":[\"Use this page to judge buttons, links, cards, forms, thread accents, and quiet surfaces before changing a theme globally.\"],\"Q/uoSA\":[\"Quiet here for now.\"],\"Q2mGA7\":[\"Clear filter\"],\"QBqVyM\":[\"Home screen icon for iPhone and iPad shortcuts.\"],\"QebAts\":[\"Link added.\"],\"Qgbxdw\":[\"Designing a calmer default accent for Jant\"],\"Qn9Ao8\":[\"Circle tile\"],\"QyDt3L\":[\"File uploaded.\"],\"R5CMuK\":[\"Jant looks best when the accent feels editorial. Buttons can stay sturdy, but inline emphasis should feel like a pen mark, not a dashboard highlight.\"],\"R8AthW\":[\"Divider\"],\"R9Khdg\":[\"Auto\"],\"RAv3u7\":[\"Compare it against the theme controls\"],\"ROa4Ti\":[\"Interfaces for reading should guide the eye, not keep asking for attention.\"],\"RZOWDv\":[\"Add a custom shortcut to any page or site.\"],\"RdmNnl\":[\"Browser tab\"],\"RfGczC\":[\"Square logo\"],\"Rj01Fz\":[\"Links\"],\"S37om9\":[\"Included assets\"],\"S8NCfs\":[\"Save to drafts to edit and post at a later time.\"],\"SJGVAw\":[\"Feel editorial and slightly quieter.\"],\"SJmfuf\":[\"Site Name\"],\"SaNhJE\":[\"feel deliberate instead of washed out.\"],\"SpTWH3\":[\"Download SVG\"],\"SvRuJt\":[\"Field Notes on Interface Tone\"],\"T/R+Qz\":[\"Primary\"],\"TNZKpI\":[\"Danger\"],\"TvaTxw\":[\"Doesn't appear in Latest. Still appears in collections you add it to.\"],\"UIMXHD\":[\"Remove Divider\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uc5y7o\":[\"Choose the standard logo for websites, docs, directories, and editorial layouts.\"],\"V18SVO\":[\"Use the logo on light backgrounds.\"],\"V4WsyL\":[\"Add Link\"],\"VCA6B2\":[\"These are actual feed components with real footers, summaries, and inline links. Use this section to judge whether the theme still feels calm once it is applied to realistic content.\"],\"VNqFYa\":[\"Loading post...\"],\"WCOanD\":[\"This reference is useful because it treats links and citations as part of the reading rhythm. Keep that in mind while tuning the\"],\"WbIbzR\":[\"Checking link...\"],\"WcWS//\":[\"Download file\"],\"WhsN3P\":[\"A good default accent in Jant should feel like editorial structure, not product branding. That means links, emphasis, and thread cues can be visible without turning the page into UI chrome.\"],\"Wn+/rH\":[\"Transparent square\"],\"XU7b+L\":[\"Primary logo files\"],\"XV1mAn\":[\"Only visible when signed in.\"],\"XrnWzN\":[\"Published!\"],\"YIix5Y\":[\"Search...\"],\"YUglt2\":[\"Generating a link...\"],\"YXiA6e\":[\"Primary button\"],\"Ygx3Yl\":[\"Small browser icon used in tabs and bookmarks.\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZGs2so\":[\"Delete this collection permanently? Posts inside won't be removed.\"],\"ZV5ykW\":[\"Download PNG\"],\"ZhhOwV\":[\"Quote\"],\"ZmSeP+\":[\"Save to drafts?\"],\"ZxFuun\":[[\"count\",\"plural\",{\"one\":[\"Found \",\"#\",\" result\"],\"other\":[\"Found \",\"#\",\" results\"]}]],\"a5j82I\":[\"No collections match that search. Try a different name.\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aMEyv0\":[\"Stay sturdy and readable.\"],\"aN6wx0\":[\"Nothing in Featured yet. Mark a post as featured to show it here.\"],\"aYpXKS\":[\"and checking whether the accent is guiding attention or pulling too hard.\"],\"aaGV/9\":[\"New Link\"],\"af+9p6\":[\"Quiet metadata\"],\"an5hVd\":[\"Images\"],\"ao77hr\":[[\"count\",\"plural\",{\"one\":[\"#\",\" hidden post\"],\"other\":[\"#\",\" hidden posts\"]}]],\"auFlOr\":[\"Icons and previews\"],\"avuFKG\":[\"threads\"],\"bFpC86\":[\"Everything in one download\"],\"bGtMpA\":[\"Add a label and URL.\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bbdNeX\":[\"Sign in\"],\"bfCbdi\":[\"Current post\"],\"bkBJmZ\":[\"This is useful as a color check because it puts the accent next to quotation styling, metadata, and a quieter explanatory paragraph. Compare it back to the\"],\"bzSI52\":[\"Discard\"],\"c2JRUS\":[\"Generate automatically\"],\"cIoW7X\":[\"Inline link\"],\"cTUByn\":[\"Newest first\"],\"cb7FR8\":[\"White logo on the Jant green square tile for platforms and layouts that expect a true edge-to-edge square.\"],\"cgmi4V\":[\"Delete Draft\"],\"cnGeoo\":[\"Delete\"],\"d+F4pf\":[\"The image should sit quietly inside the article instead of feeling like a card preview.\"],\"d/o/BH\":[\"Couldn't publish. Saved as draft.\"],\"dD7NPy\":[\"Outline\"],\"dEgA5A\":[\"Cancel\"],\"dUsGbd\":[\"The right accent should disappear into the writing until you need it.\"],\"dXoieq\":[\"Summary\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dbUuAj\":[\"Appears in Latest.\"],\"df4a/r\":[\"Couldn't load this post. Try again.\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"f4MAoA\":[\"Some uploads failed. Saved as draft.\"],\"f5s9EI\":[\"Press N to write\"],\"f6Hub0\":[\"Sort\"],\"f8fH8W\":[\"Design\"],\"fD+f7T\":[\"RSS feed\"],\"fKrDxS\":[\"Brand tile\"],\"fMPkxb\":[\"Show more\"],\"fqDzSu\":[\"Rate\"],\"fttd2R\":[\"My Collection\"],\"gCcxP/\":[\"Threads can include up to \",[\"count\"],\" posts.\"],\"gFdWl+\":[\"A long-form article sample for checking the default palette in a true reading context.\"],\"gNKz6Z\":[\"Collection deleted.\"],\"gXH9r/\":[\"Open raw PNG\"],\"gj52YE\":[\"This collection is empty. Add posts from the editor.\"],\"gpaPhA\":[\"Helps screen readers describe the image\"],\"h5RcXU\":[\"Post hidden\"],\"hLlWo5\":[\"A few simple rules.\"],\"hWpUeY\":[\"Auto link\"],\"hXzOVo\":[\"Next\"],\"heSQoS\":[\"Paste a URL...\"],\"hrkGms\":[\"Search\"],\"i0vDGK\":[\"Sort Order\"],\"i5+Y7d\":[\"Download the official Jant logo, icons, and preview files.\"],\"i6kro6\":[\"Edit custom link\"],\"i6nDCI\":[\"Choose a new password.\"],\"iG7KNr\":[\"Logo\"],\"iH8pgl\":[\"Back\"],\"ilSmIt\":[\"Hard edge\"],\"iu7tUI\":[\"Breadcrumb\"],\"jAXE5p\":[\"Reverse logo\"],\"jAqB/k\":[\"Post privately\"],\"jQflRT\":[\"This uses the real single-post detail rendering with a longer article, inline image, tables, lists, quotes, and code. The content column stays at the same width as the live site.\"],\"jd+8Mm\":[\"Social preview image\"],\"jdJOV1\":[\"Settings\"],\"ji7oVU\":[\"Edit post\"],\"jpctdh\":[\"View\"],\"jrsUoG\":[\"Type / for commands\"],\"jvyYZG\":[\"What's on your mind...\"],\"k3Iw35\":[\"Switch to the white logo when the standard green version would lose contrast.\"],\"kPMIr+\":[\"Give it a title...\"],\"kj6ppi\":[\"entry\"],\"kr39oD\":[\"No collections yet. Start one to organize posts by topic.\"],\"kzvWob\":[\"Link to the post archive\"],\"laT1IJ\":[\"iOS home screen\"],\"lb+Xwx\":[\"Custom link\"],\"m16xKo\":[\"Add\"],\"mKT7g0\":[\"Text attachment\"],\"mc/vLq\":[\"This link is already in use. Choose something else.\"],\"muKqfV\":[\"Featured\"],\"n1ekoW\":[\"Sign In\"],\"n3ReIn\":[\"Collections\"],\"n6QD94\":[\"Oldest first\"],\"nFukaP\":[\"Wrong email or password. Check your credentials and try again.\"],\"nV6twc\":[\"Organize\"],\"nd8Puv\":[\"White logo on the Jant green circle for profile images, badges, and other round placements where you want a ready-made asset.\"],\"ndrEYW\":[\"When the accent is slightly warmer and less literal, the whole page feels more like a writing space and less like product UI.\"],\"o21Y+P\":[\"entries\"],\"oO0hKx\":[[\"count\",\"plural\",{\"one\":[\"#\",\" more post\"],\"other\":[\"#\",\" more posts\"]}]],\"oTu7Wt\":[\"Combined Collections\"],\"ode0+L\":[\"Theme sample\"],\"ogssnn\":[\"with media\"],\"ovBPCi\":[\"Default\"],\"p1Z67P\":[\"When primary is too rigid, the whole page starts reading like product UI instead of writing space.\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pBHx39\":[\"Dark backgrounds\"],\"pVrU5x\":[\"If this page feels too branded, the first place to soften is the default theme’s site accent, not the border or body text.\"],\"pvnfJD\":[\"Dark\"],\"q+hNag\":[\"Collection\"],\"q5YRzz\":[\"Color check\"],\"q8RviX\":[\"Titled\"],\"qcawwg\":[\"Publish now\"],\"qiN9NB\":[\"Surface\"],\"qt89I8\":[\"Draft saved.\"],\"quvfGs\":[\"instead of judging it as an isolated swatch.\"],\"r7kcaA\":[\"Drag collections, links, and dividers into the order you want.\"],\"rA2TFI\":[\"Switch the palette and mode without opening settings or changing the active site theme.\"],\"rV8ZnP\":[\"Edit publish date\"],\"rdUucN\":[\"Preview\"],\"s8G5Or\":[\"This upload would exceed your shared hosted media limit. Remove files or upgrade storage to continue.\"],\"s9gHf5\":[\"your-post-link\"],\"sER+bs\":[\"Files\"],\"sQpDn6\":[\"Exit fullscreen\"],\"sgr2wQ\":[\"collection\"],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"syiAKf\":[\"note treatment\"],\"t42hIC\":[\"Everything most people need is in one ZIP.\"],\"tCctex\":[\"The brand pack includes SVG logos, a transparent square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and the default social preview image.\"],\"tKlWWY\":[\"Emoji\"],\"tSWVu5\":[\"Published on \",[\"date\"],\" at \",[\"time\"]],\"tfDRzk\":[\"Save\"],\"tg5MRw\":[\"Sign in to start writing.\"],\"tgSBSE\":[\"Remove Link\"],\"uowbPn\":[\"Remove attachment\"],\"v3E8iS\":[\"A practical checklist\"],\"vSJd18\":[\"Video\"],\"vSYKYI\":[\"Main feed\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vcpc5o\":[\"Close menu\"],\"vdFnYM\":[\"Reset link\"],\"vdvpU5\":[\"/archive?format=quote or https://example.com\"],\"vgpfCi\":[\"Save draft\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzU4k9\":[\"New Collection\"],\"w0Emel\":[\"Suggested link\"],\"w6mlns\":[\"Article detail page\"],\"wJ+GRy\":[\"All visibility\"],\"wL3cK8\":[\"Latest\"],\"wja8aL\":[\"Untitled\"],\"wlnK1t\":[\"A single ZIP with the main logo, reverse logo, square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and social preview image.\"],\"wm3Zlr\":[\"All years\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xVrkxi\":[\"quiet design\"],\"xVvw1i\":[\"This reset link is no longer valid. Request a new one to continue.\"],\"xYilR2\":[\"Media\"],\"xeiujy\":[\"Text\"],\"xhTx3y\":[\"Choose the standard logo for most placements and the reverse logo when you need more contrast.\"],\"y28hnO\":[\"Post\"],\"y2o/Y0\":[\"This Link Has Expired\"],\"yGZVl1\":[\"More\"],\"yQ2kGp\":[\"Load more\"],\"yUtAh2\":[\"New Thread\"],\"ycM1Xg\":[\"No results. Try different keywords.\"],\"ynMAhG\":[\"Default logo\"],\"yzF66j\":[\"Link\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zJDAbh\":[\"Don't save\"],\"zcDmsG\":[\"Featured posts\"],\"zoK+eO\":[\"Add a title before posting this link.\"],\"zucql+\":[\"Menu\"],\"zwBp5t\":[\"Private\"]}");
|
|
1916
1917
|
//#endregion
|
|
1917
1918
|
//#region src/i18n/locales/settings/en.ts
|
|
1918
|
-
var messages$2 = JSON.parse("{\"+4Z6iP\":[\"Create the repository on GitHub first — it can be empty.\"],\"+9JI/F\":[\"Connecting will sync your site onto \",[\"repo\"],\"'s default branch on top of its existing history. Existing files outside Jant's managed paths are kept. This can't be undone.\"],\"+AXdXp\":[\"Label and URL are required\"],\"+K0AvT\":[\"Disconnect\"],\"+zy2Nq\":[\"Type\"],\"/3H2/s\":[\"This hosted site signs in through \",[\"providerLabel\"],\". Manage password and hosted access there.\"],\"/JnyjR\":[\"Toggle built-in navigation items. Their order controls what shows in the header and which feed the homepage opens first.\"],\"/ODeyS\":[\"Sets the content language announced to readers (HTML lang, RSS) and the dashboard language. Any BCP 47 tag is accepted; tags without a dashboard translation fall back to English.\"],\"0OGSSc\":[\"Avatar display updated.\"],\"0UzCUX\":[\"Update the password you use to sign in\"],\"10UtuM\":[\"CJK Font\"],\"14BEca\":[\"Read why\"],\"1F6Mzc\":[\"No navigation items yet. Add links or enable system items below.\"],\"1njn7W\":[\"Light\"],\"2B7t+s\":[\"Sessions and password\"],\"2DoBvq\":[\"Feeds\"],\"2FYpfJ\":[\"More\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2PTjMB\":[\"I want to delete \",[\"siteName\"]],\"2cFU6q\":[\"Site Footer\"],\"2oWZo7\":[\"Last commit\"],\"2uuy4H\":[\"Connected via Personal Access Token\"],\"35x8eZ\":[\"Showing \",[\"shown\"],\" of \",[\"total\"]],\"3Cw1AI\":[\"Add Collection\"],\"3VrybB\":[\"Redirect\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"3n0zbB\":[\"Session management is off in demo mode. Use the shared demo session instead.\"],\"3sYJi5\":[\"Download a Hugo-compatible archive — host it statically or move to another Jant.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"49Bsal\":[\"Feed settings updated.\"],\"4Jge8E\":[\"Active Sessions\"],\"4KIa+q\":[\"Export downloaded.\"],\"4cEClj\":[\"Sessions\"],\"4zGJ5E\":[\"Delete Account Permanently\"],\"5QlUIt\":[\"Empty repository. Ready to connect.\"],\"5VQnR3\":[\"Use these when you want a feed URL that never changes.\"],\"5dpcN1\":[\"type to search all\"],\"69OXZB\":[\"Delete Hosted Site\"],\"6ArdBh\":[\"Uses featured posts for /feed.\"],\"6DjeBT\":[\"Demo sites always stay hidden from search engines.\"],\"6FFB7q\":[\"Uses the latest public posts for /feed.\"],\"6K1Vef\":[\"Delete this blog permanently? This cannot be undone.\"],\"6NpNLc\":[\"This repository has existing content.\"],\"6V3Ea3\":[\"Copied\"],\"746NHh\":[\"this blog\"],\"7811AW\":[\"This repository is already backing up this site.\"],\"7FaY4u\":[\"Usage\"],\"7G9YLi\":[\"Allow search engines to index my site\"],\"7MZxzw\":[\"Password changed.\"],\"7vhWI8\":[\"New Password\"],\"7z05Pf\":[\"Open the hosted site controls in \",[\"providerLabel\"],\" to cancel billing or permanently delete this site.\"],\"81nFIS\":[\"Passwords don't match. Make sure both fields are identical.\"],\"87a/t/\":[\"Label\"],\"89Upyo\":[\"That theme isn't available. Pick another one.\"],\"8BfEpW\":[\"Hosted account\"],\"8N/Mcp\":[\"Archive filter parameters (e.g. format=note&view=list)\"],\"8U2Z7f\":[\"New Custom URL\"],\"8ZsakT\":[\"Password\"],\"9+vGLh\":[\"Custom CSS\"],\"9As8Nu\":[\"Create one on GitHub\"],\"9Lsvt5\":[\"Signed in \",[\"date\"]],\"9T7Cwm\":[\"Redirects, vanity paths, and URL control\"],\"9aUyym\":[\"See where you're signed in and revoke old sessions\"],\"A1taO8\":[\"Search\"],\"AeXO77\":[\"Account\"],\"AnY+O9\":[\"Show \\\"Build with Jant\\\" at the bottom of the home page\"],\"ApZDMk\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square PNG with a solid background at least 512x512 pixels.\"],\"B495Gs\":[\"Archive\"],\"B4ESok\":[\"API reference\"],\"CTAEes\":[\"Select a repository\"],\"CjZZgz\":[\"This repository already has commits\"],\"DCKkhU\":[\"Current Password\"],\"DKKKeF\":[\"Manage password and hosted access in \",[\"providerLabel\"]],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"Enslfm\":[\"Destination\"],\"F7FKwe\":[\"Anything you paste here has full access to your visitors' browsers. Only use code from sources you trust.\"],\"FkMol5\":[\"Featured\"],\"G/1oP+\":[\"Remove the webhook and stop syncing. Your repository content will not be deleted.\"],\"G0qJsQ\":[\"Security token missing. Refresh the page and try again.\"],\"G39wnK\":[\"Back up and sync content with a GitHub repository\"],\"GMMWcy\":[\"Name, metadata, language, and search defaults\"],\"GXsAby\":[\"Revoke\"],\"GxkJXS\":[\"Uploading...\"],\"GzKzUa\":[\"Demo limits\"],\"HKH+W+\":[\"Data\"],\"Hp1l6f\":[\"Current\"],\"HxlY7t\":[\"Changing this updates what subscribers get from /feed.\"],\"HxuOlm\":[\"Site Header\"],\"I6gXOa\":[\"Path\"],\"ID38tA\":[\"Account deletion is off in demo mode. The shared demo resets separately.\"],\"IF9tPu\":[\"When to use site export, database backups, and recovery drills.\"],\"IW5PBo\":[\"Copy Token\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"Repository\"],\"J6bLeg\":[\"Add a custom link to any URL\"],\"JL7LF5\":[\"available CSS variables, data attributes, and examples.\"],\"JTviaO\":[\"Manage sign-in security, exports, and irreversible actions.\"],\"JcD7qf\":[\"More actions\"],\"JjX0OO\":[\"Copy your token now — it won't be shown again.\"],\"JrFTcr\":[\"Connecting…\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"KDw4GX\":[\"Try again\"],\"KSgo21\":[\"Pick a repository\"],\"KVVYBh\":[\"Add collection to navigation\"],\"KiJn9B\":[\"Note\"],\"L3DEwT\":[\"Remove this avatar? Your favicon and header icon will go back to the default.\"],\"L4t4/q\":[\"March 14\"],\"LdyooL\":[\"link\"],\"M/D8PK\":[\"+ Install on another account\"],\"M/haSd\":[\"Always show the light version of the theme.\"],\"M2kIWU\":[\"Font theme\"],\"M6CbAU\":[\"Toggle edit panel\"],\"Me5t5H\":[\"Connect a GitHub repository to automatically back up your posts as Markdown files. Edits on GitHub sync back to your site.\"],\"MtENL9\":[\"Tune how your site looks, reads, and runs.\"],\"N/8NPV\":[\"Before deleting, download a site export. You won't be able to recover this account after deletion.\"],\"N7UNHY\":[\"Featured feed\"],\"NHnUHF\":[\"Favicon and the profile mark in your header\"],\"NU2Fqi\":[\"Save CSS\"],\"Nldjdr\":[\"No custom URLs yet. Create one to add redirects or custom paths for posts.\"],\"O7rgs6\":[\"Header RSS points to your \",[\"feed\"],\" feed (/feed). Change what /feed returns in General.\"],\"OSJXFg\":[\"Applies to your entire site, including admin pages. Pick a palette, then choose whether it follows the system or stays fixed.\"],\"Ox3+3h\":[\"No matches.\"],\"PEUV5I\":[\"Code injection updated.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pwqkdw\":[\"Loading…\"],\"PxJ9W6\":[\"Generate Token\"],\"Q/6Y+2\":[\"Needs Contents (read/write) and Webhooks (read/write) on the target repository.\"],\"Q30z/l\":[\"Remove this collection from navigation? The collection itself won't be deleted.\"],\"Q99OtV\":[\"Pin a collection to your navigation bar. An asterisk (*) appears next to collections updated in the last 48 hours.\"],\"QZmz0H\":[\"Built-in links\"],\"Qnrzvb\":[\"Active Tokens\"],\"R6Z4LE\":[\"Download failed. Please try again.\"],\"R9Khdg\":[\"Auto\"],\"RxsRD6\":[\"Time Zone\"],\"SJmfuf\":[\"Site Name\"],\"SKZhW9\":[\"Token name\"],\"SVQQPe\":[\"Couldn't connect. Check the error and try again.\"],\"TpF3v+\":[\"Injected before </head>. Use for analytics, custom meta tags, and styles that must load early.\"],\"Tz0i8g\":[\"Settings\"],\"UFK415\":[\"Site-wide HTML for analytics and widgets\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UsODUn\":[\"Select an account\"],\"UxKoFf\":[\"Navigation\"],\"V+bhUy\":[\"Install GitHub App\"],\"V4WsyL\":[\"Add Link\"],\"V5pZwT\":[\"Search settings updated.\"],\"VXUPla\":[\"Connect with GitHub App\"],\"VhMDMg\":[\"Change Password\"],\"Vn3jYy\":[\"Navigation items\"],\"VoZYGU\":[\"This will permanently delete all your data — posts, media, collections, settings, and your account. Your blog will be reset to its initial setup state. This cannot be undone.\"],\"Weq9zb\":[\"General\"],\"Wi9i06\":[\"Follow each visitor's system preference.\"],\"Wx1M8N\":[\"Install the GitHub App to grant access without managing personal tokens. Permissions are scoped per repository and revocable from GitHub.\"],\"X+8FMk\":[\"Current password doesn't match. Try again.\"],\"X1G9eY\":[\"Navigation Preview\"],\"X9Hujr\":[\"Manual Push\"],\"XtBJV8\":[\"Checking repository…\"],\"Xtc16w\":[\"Refresh repository list\"],\"Y/F35r\":[\"Create a post with curl:\"],\"YF6zHf\":[\"Site settings updated.\"],\"YdG2RF\":[\"Export Site\"],\"YwhjRx\":[\"Manage Account\"],\"ZDY7Fy\":[\"Syncing…\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZS/CBL\":[\"Delete this navigation link? Visitors won't see it in your site header anymore.\"],\"ZhhOwV\":[\"Quote\"],\"ZiooJI\":[\"API Tokens\"],\"Zm7Qb0\":[\"Backup & Restore Guide\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"a14mj8\":[\"Unknown device\"],\"a3LDKx\":[\"Security\"],\"aAIQg2\":[\"Appearance\"],\"aFkzVF\":[\"The slug of the target post or collection\"],\"alKG0+\":[\"Font Theme\"],\"anibOb\":[\"About this blog\"],\"any7NR\":[\"Theming guide\"],\"b+/jO6\":[\"301 (Permanent)\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bHYIks\":[\"Sign Out\"],\"bmrL08\":[\"Demo mode hides sessions, password changes, and account deletion. Export still works.\"],\"c3MN2z\":[\"all available endpoints and request formats.\"],\"cSDy01\":[\"Custom CSS updated.\"],\"clzoNp\":[\"Always show the dark version of the theme.\"],\"cnGeoo\":[\"Delete\"],\"d3FRkY\":[\"Could not copy. Try again.\"],\"d5oGUo\":[\"Create a new repository on GitHub\"],\"dEgA5A\":[\"Cancel\"],\"dTXUY+\":[\"Confirm account deletion\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"Permanently delete all data and reset the blog\"],\"drodVV\":[\"No collections yet. Create one first, then add it to your navigation.\"],\"dsWkIw\":[\"Disconnect from GitHub? The webhook will be removed. Your repository content will not be deleted.\"],\"e/tSI5\":[\"Navigation order updated.\"],\"ePK91l\":[\"Edit\"],\"ebQKK7\":[\"Site\"],\"egK+Yy\":[\"Bearer tokens for scripts and automation\"],\"ehj/zN\":[\"Redirect Type\"],\"eneWvv\":[\"Draft\"],\"erTMh7\":[\"Last synced\"],\"f+m8jj\":[\"Feed URL copied.\"],\"f8fH8W\":[\"Design\"],\"fWYqkz\":[\"Code Injection\"],\"gOWiTY\":[\"Load a serif font optimized for Chinese, Japanese, or Korean content.\"],\"gZ5owP\":[\"Search repositories\"],\"gbqbh6\":[\"Safe to leave this page — syncing continues in the background.\"],\"gkFvVN\":[\"Injected before </body>. Use for chat widgets and scripts that should not block page load.\"],\"gtQsRO\":[\"Create Custom URL\"],\"hBO/y4\":[\"Security token expired. Refresh the page and try again.\"],\"hGmyDl\":[\"Tokens let you access the API from scripts, shortcuts, and other tools without signing in.\"],\"hIHkRy\":[\"Connected via GitHub App\"],\"hdSi1b\":[\"Type \",[\"repo\"],\" to confirm\"],\"he3ygx\":[\"Copy\"],\"i0qMbr\":[\"Home\"],\"iEUzMn\":[\"system\"],\"iSLIjg\":[\"Connect\"],\"iVOMRi\":[\"Home settings updated.\"],\"icB4Cv\":[\"Drag links here to show them under the More menu\"],\"ihn4zD\":[\"Search…\"],\"iiDXZc\":[\"Displayed at the bottom of all posts and pages.\"],\"j4VrG6\":[\"Download Export ZIP\"],\"j5nQL2\":[\"e.g. iOS Shortcuts\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jgBjXJ\":[\"Revoke this token? Any scripts using it will stop working.\"],\"jpctdh\":[\"View\"],\"k1ifdL\":[\"Processing...\"],\"kMXclu\":[\"Download a site export\"],\"kNiQp6\":[\"Pinned\"],\"kRhzWq\":[\"GitHub Sync\"],\"kVQs7s\":[\"Fine-grained styling overrides\"],\"ke1gWS\":[\"Custom URLs\"],\"kfcRb0\":[\"Avatar\"],\"kxDZ2i\":[\"This code runs on every page of your site.\"],\"l2Op2p\":[\"Query Parameters\"],\"lLW3vJ\":[\"Target Slug\"],\"lYHJih\":[\"Revoke this session? That device will need to sign in again.\"],\"mLOk1i\":[\"Push all posts to GitHub right now instead of waiting for the next automatic sync.\"],\"mSNmrX\":[\"List posts:\"],\"nK07ni\":[\"Choose a typographic direction for your site. Each theme changes both the font pairing and the reading rhythm.\"],\"o/vNDE\":[\"lets you override any theme variable.\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"We'll prefill the name \",[\"name\"],\". The list refreshes on return.\"],\"oKOOsY\":[\"Color Theme\"],\"oL535e\":[\"Not synced yet\"],\"oNA4If\":[\"All collections are already in your navigation.\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"pgTIrt\":[\"Choose the GitHub account and repository to sync with this site.\"],\"psoxDF\":[\"That font theme isn't available. Pick another one.\"],\"pvnfJD\":[\"Dark\"],\"q+hNag\":[\"Collection\"],\"qdcESc\":[\"Create a new repository\"],\"r5EW6f\":[\"This repository is already backing up another Jant site (\",[\"host\"],\"). Pick a different repository.\"],\"rEspiY\":[\"Navigation placement updated.\"],\"rFmBG3\":[\"Color theme\"],\"rlonmB\":[\"Couldn't delete. Try again in a moment.\"],\"satWc6\":[\"Main RSS feed\"],\"sgr2wQ\":[\"collection\"],\"sqxcaY\":[\"Created \",[\"date\"]],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"t3hvHq\":[\"Sync Now\"],\"tfDRzk\":[\"Save\"],\"tvgAq5\":[\"No accounts authorized yet\"],\"u1VTd3\":[\"Palette, surface tone, and overall mood\"],\"u3wRF+\":[\"Published\"],\"u6KOjV\":[\"Want more control?\"],\"udPwLB\":[\"Header\"],\"ui6aMF\":[\"These devices are currently signed in to your account. Revoke any session you don't recognize.\"],\"vBEKwo\":[\"Manage this site's active sessions here. Password and hosted access are managed through \",[\"providerLabel\"],\".\"],\"vRldcl\":[\"Typography choices and reading texture\"],\"vSYKYI\":[\"Main feed\"],\"vTuib7\":[\"This controls what /feed returns.\"],\"vXIe7J\":[\"Language\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzX5FB\":[\"Delete Account\"],\"w8Rv8T\":[\"Label is required\"],\"wL3cK8\":[\"Latest\"],\"wPmHHc\":[\"Quiet surfaces let writing lead.\"],\"wW6NCp\":[\"Last error\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wuLtXn\":[\"No active sessions right now. Signed-in devices show up here.\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xHt036\":[\"Personal Access Token\"],\"xbN8dp\":[\"Soft color should still carry a clear reading rhythm.\"],\"y28hnO\":[\"Post\"],\"y8Md/V\":[\"Language and time updated.\"],\"yNCqOt\":[\"Latest feed\"],\"yQ3kNF\":[\"Type the following phrase to confirm:\"],\"ydq1k2\":[\"Pick an account first\"],\"yjjCV8\":[\"Fixed feed URLs\"],\"yjkELF\":[\"Confirm New Password\"],\"yzF66j\":[\"Link\"],\"z6wakA\":[\"A short intro shown on your home page.\"],\"zEizrk\":[\"Last used \",[\"date\"]],\"zSURJW\":[\"No repositories match.\"],\"zXH2jX\":[\"Language & Time\"],\"zlcDd2\":[\"Delete this custom URL? Visitors using it won't be redirected anymore.\"],\"zwBp5t\":[\"Private\"],\"zxRN6H\":[\"Header links, home feed, and overflow menu\"]}");
|
|
1919
|
+
var messages$2 = JSON.parse("{\"+4Z6iP\":[\"Create the repository on GitHub first — it can be empty.\"],\"+9JI/F\":[\"Connecting will sync your site onto \",[\"repo\"],\"'s default branch on top of its existing history. Existing files outside Jant's managed paths are kept. This can't be undone.\"],\"+AXdXp\":[\"Label and URL are required\"],\"+K0AvT\":[\"Disconnect\"],\"+zy2Nq\":[\"Type\"],\"/3H2/s\":[\"This hosted site signs in through \",[\"providerLabel\"],\". Manage password and hosted access there.\"],\"/JnyjR\":[\"Toggle built-in navigation items. Their order controls what shows in the header and which feed the homepage opens first.\"],\"/ODeyS\":[\"Sets the content language announced to readers (HTML lang, RSS) and the dashboard language. Any BCP 47 tag is accepted; tags without a dashboard translation fall back to English.\"],\"/zOUxl\":[\"QR code linking to the Telegram bot\"],\"0OGSSc\":[\"Avatar display updated.\"],\"0UzCUX\":[\"Update the password you use to sign in\"],\"0bdA9b\":[\"Open Telegram to connect\"],\"10UtuM\":[\"CJK Font\"],\"14BEca\":[\"Read why\"],\"1F6Mzc\":[\"No navigation items yet. Add links or enable system items below.\"],\"1mbBbL\":[\"Connect manually\"],\"1njn7W\":[\"Light\"],\"2B7t+s\":[\"Sessions and password\"],\"2DoBvq\":[\"Feeds\"],\"2FYpfJ\":[\"More\"],\"2Ithfh\":[\"Message the bot any text and it's published as a note.\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2PTjMB\":[\"I want to delete \",[\"siteName\"]],\"2cFU6q\":[\"Site Footer\"],\"2oWZo7\":[\"Last commit\"],\"2uuy4H\":[\"Connected via Personal Access Token\"],\"35x8eZ\":[\"Showing \",[\"shown\"],\" of \",[\"total\"]],\"39QGku\":[\"Open the bot and send the binding code, then anything you message it becomes a note.\"],\"3Cw1AI\":[\"Add Collection\"],\"3VrybB\":[\"Redirect\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"3n0zbB\":[\"Session management is off in demo mode. Use the shared demo session instead.\"],\"3sYJi5\":[\"Download a Hugo-compatible archive — host it statically or move to another Jant.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"49Bsal\":[\"Feed settings updated.\"],\"4Jge8E\":[\"Active Sessions\"],\"4KIa+q\":[\"Export downloaded.\"],\"4cEClj\":[\"Sessions\"],\"4zGJ5E\":[\"Delete Account Permanently\"],\"5QlUIt\":[\"Empty repository. Ready to connect.\"],\"5VQnR3\":[\"Use these when you want a feed URL that never changes.\"],\"5dpcN1\":[\"type to search all\"],\"5f1Wo9\":[\"Connected as \",[\"account\"]],\"69OXZB\":[\"Delete Hosted Site\"],\"6ArdBh\":[\"Uses featured posts for /feed.\"],\"6DjeBT\":[\"Demo sites always stay hidden from search engines.\"],\"6FFB7q\":[\"Uses the latest public posts for /feed.\"],\"6K1Vef\":[\"Delete this blog permanently? This cannot be undone.\"],\"6NpNLc\":[\"This repository has existing content.\"],\"6V3Ea3\":[\"Copied\"],\"71WIgc\":[\"Get a new code\"],\"746NHh\":[\"this blog\"],\"7811AW\":[\"This repository is already backing up this site.\"],\"7FaY4u\":[\"Usage\"],\"7G9YLi\":[\"Allow search engines to index my site\"],\"7GISOt\":[\"Save bot token\"],\"7MZxzw\":[\"Password changed.\"],\"7vhWI8\":[\"New Password\"],\"7z05Pf\":[\"Open the hosted site controls in \",[\"providerLabel\"],\" to cancel billing or permanently delete this site.\"],\"81nFIS\":[\"Passwords don't match. Make sure both fields are identical.\"],\"87a/t/\":[\"Label\"],\"89Upyo\":[\"That theme isn't available. Pick another one.\"],\"8BfEpW\":[\"Hosted account\"],\"8N/Mcp\":[\"Archive filter parameters (e.g. format=note&view=list)\"],\"8T46pB\":[\"Bot token\"],\"8U2Z7f\":[\"New Custom URL\"],\"8ZsakT\":[\"Password\"],\"9+vGLh\":[\"Custom CSS\"],\"9As8Nu\":[\"Create one on GitHub\"],\"9Lsvt5\":[\"Signed in \",[\"date\"]],\"9T7Cwm\":[\"Redirects, vanity paths, and URL control\"],\"9aUyym\":[\"See where you're signed in and revoke old sessions\"],\"A1taO8\":[\"Search\"],\"AeXO77\":[\"Account\"],\"AnY+O9\":[\"Show \\\"Build with Jant\\\" at the bottom of the home page\"],\"ApZDMk\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square PNG with a solid background at least 512x512 pixels.\"],\"B495Gs\":[\"Archive\"],\"B4ESok\":[\"API reference\"],\"BzEFor\":[\"or\"],\"CDAdlf\":[\"Remove bot\"],\"CTAEes\":[\"Select a repository\"],\"CjZZgz\":[\"This repository already has commits\"],\"D8k2s6\":[\"Connect Telegram\"],\"DCKkhU\":[\"Current Password\"],\"DKKKeF\":[\"Manage password and hosted access in \",[\"providerLabel\"]],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"Enslfm\":[\"Destination\"],\"F7FKwe\":[\"Anything you paste here has full access to your visitors' browsers. Only use code from sources you trust.\"],\"FkMol5\":[\"Featured\"],\"G/1oP+\":[\"Remove the webhook and stop syncing. Your repository content will not be deleted.\"],\"G0qJsQ\":[\"Security token missing. Refresh the page and try again.\"],\"G39wnK\":[\"Back up and sync content with a GitHub repository\"],\"GMMWcy\":[\"Name, metadata, language, and search defaults\"],\"GXsAby\":[\"Revoke\"],\"GxkJXS\":[\"Uploading...\"],\"GzKzUa\":[\"Demo limits\"],\"HKH+W+\":[\"Data\"],\"Hp1l6f\":[\"Current\"],\"HxlY7t\":[\"Changing this updates what subscribers get from /feed.\"],\"HxuOlm\":[\"Site Header\"],\"I6gXOa\":[\"Path\"],\"ID38tA\":[\"Account deletion is off in demo mode. The shared demo resets separately.\"],\"IF9tPu\":[\"When to use site export, database backups, and recovery drills.\"],\"IW5PBo\":[\"Copy Token\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"Repository\"],\"J6bLeg\":[\"Add a custom link to any URL\"],\"JL7LF5\":[\"available CSS variables, data attributes, and examples.\"],\"JTviaO\":[\"Manage sign-in security, exports, and irreversible actions.\"],\"JcD7qf\":[\"More actions\"],\"JjX0OO\":[\"Copy your token now — it won't be shown again.\"],\"JrFTcr\":[\"Connecting…\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"KDw4GX\":[\"Try again\"],\"KSgo21\":[\"Pick a repository\"],\"KVVYBh\":[\"Add collection to navigation\"],\"KiJn9B\":[\"Note\"],\"L3DEwT\":[\"Remove this avatar? Your favicon and header icon will go back to the default.\"],\"L4t4/q\":[\"March 14\"],\"LdyooL\":[\"link\"],\"M/D8PK\":[\"+ Install on another account\"],\"M/haSd\":[\"Always show the light version of the theme.\"],\"M2kIWU\":[\"Font theme\"],\"M6CbAU\":[\"Toggle edit panel\"],\"MaYYE6\":[\"Post notes by messaging a Telegram bot\"],\"Me5t5H\":[\"Connect a GitHub repository to automatically back up your posts as Markdown files. Edits on GitHub sync back to your site.\"],\"Mr4QPw\":[\"Disconnect Telegram? You can reconnect any time with a new binding code.\"],\"MtENL9\":[\"Tune how your site looks, reads, and runs.\"],\"N/8NPV\":[\"Before deleting, download a site export. You won't be able to recover this account after deletion.\"],\"N7UNHY\":[\"Featured feed\"],\"NHnUHF\":[\"Favicon and the profile mark in your header\"],\"NU2Fqi\":[\"Save CSS\"],\"Nldjdr\":[\"No custom URLs yet. Create one to add redirects or custom paths for posts.\"],\"O7rgs6\":[\"Header RSS points to your \",[\"feed\"],\" feed (/feed). Change what /feed returns in General.\"],\"OSJXFg\":[\"Applies to your entire site, including admin pages. Pick a palette, then choose whether it follows the system or stays fixed.\"],\"Ox3+3h\":[\"No matches.\"],\"PEUV5I\":[\"Code injection updated.\"],\"PXj9lw\":[\"Stop accepting posts from Telegram. Your existing notes stay published.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pwqkdw\":[\"Loading…\"],\"PxJ9W6\":[\"Generate Token\"],\"Q/6Y+2\":[\"Needs Contents (read/write) and Webhooks (read/write) on the target repository.\"],\"Q30z/l\":[\"Remove this collection from navigation? The collection itself won't be deleted.\"],\"Q99OtV\":[\"Pin a collection to your navigation bar. An asterisk (*) appears next to collections updated in the last 48 hours.\"],\"QZmz0H\":[\"Built-in links\"],\"Qnrzvb\":[\"Active Tokens\"],\"R6Z4LE\":[\"Download failed. Please try again.\"],\"R9Khdg\":[\"Auto\"],\"RcdDOS\":[\"Create a bot by messaging @BotFather on Telegram, then paste the token it gives you.\"],\"RxsRD6\":[\"Time Zone\"],\"SJmfuf\":[\"Site Name\"],\"SKZhW9\":[\"Token name\"],\"SVQQPe\":[\"Couldn't connect. Check the error and try again.\"],\"SchpMp\":[\"Telegram\"],\"TpF3v+\":[\"Injected before </head>. Use for analytics, custom meta tags, and styles that must load early.\"],\"Tz0i8g\":[\"Settings\"],\"UFK415\":[\"Site-wide HTML for analytics and widgets\"],\"UTvFQq\":[\"Open \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" and send:\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UsODUn\":[\"Select an account\"],\"UxKoFf\":[\"Navigation\"],\"V+bhUy\":[\"Install GitHub App\"],\"V4WsyL\":[\"Add Link\"],\"V5pZwT\":[\"Search settings updated.\"],\"VXUPla\":[\"Connect with GitHub App\"],\"VhMDMg\":[\"Change Password\"],\"Vn3jYy\":[\"Navigation items\"],\"VoZYGU\":[\"This will permanently delete all your data — posts, media, collections, settings, and your account. Your blog will be reset to its initial setup state. This cannot be undone.\"],\"Weq9zb\":[\"General\"],\"Wi9i06\":[\"Follow each visitor's system preference.\"],\"Wx1M8N\":[\"Install the GitHub App to grant access without managing personal tokens. Permissions are scoped per repository and revocable from GitHub.\"],\"X+8FMk\":[\"Current password doesn't match. Try again.\"],\"X1G9eY\":[\"Navigation Preview\"],\"X9Hujr\":[\"Manual Push\"],\"XtBJV8\":[\"Checking repository…\"],\"Xtc16w\":[\"Refresh repository list\"],\"Y/F35r\":[\"Create a post with curl:\"],\"YF6zHf\":[\"Site settings updated.\"],\"YdG2RF\":[\"Export Site\"],\"YkgZi7\":[\"Connect a Telegram bot, then anything you message it gets published as a note.\"],\"YwhjRx\":[\"Manage Account\"],\"ZDY7Fy\":[\"Syncing…\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZS/CBL\":[\"Delete this navigation link? Visitors won't see it in your site header anymore.\"],\"ZhhOwV\":[\"Quote\"],\"ZiooJI\":[\"API Tokens\"],\"Zm7Qb0\":[\"Backup & Restore Guide\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"a14mj8\":[\"Unknown device\"],\"a3LDKx\":[\"Security\"],\"aAIQg2\":[\"Appearance\"],\"aFkzVF\":[\"The slug of the target post or collection\"],\"alKG0+\":[\"Font Theme\"],\"anibOb\":[\"About this blog\"],\"any7NR\":[\"Theming guide\"],\"b+/jO6\":[\"301 (Permanent)\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bHYIks\":[\"Sign Out\"],\"bmrL08\":[\"Demo mode hides sessions, password changes, and account deletion. Export still works.\"],\"c3MN2z\":[\"all available endpoints and request formats.\"],\"cS7/bk\":[\"Remove the saved bot token? Its webhook is deleted and any connected account is disconnected.\"],\"cSDy01\":[\"Custom CSS updated.\"],\"clzoNp\":[\"Always show the dark version of the theme.\"],\"cnGeoo\":[\"Delete\"],\"d3FRkY\":[\"Could not copy. Try again.\"],\"d5oGUo\":[\"Create a new repository on GitHub\"],\"dEgA5A\":[\"Cancel\"],\"dTXUY+\":[\"Confirm account deletion\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"Permanently delete all data and reset the blog\"],\"drodVV\":[\"No collections yet. Create one first, then add it to your navigation.\"],\"dsWkIw\":[\"Disconnect from GitHub? The webhook will be removed. Your repository content will not be deleted.\"],\"e/tSI5\":[\"Navigation order updated.\"],\"ePK91l\":[\"Edit\"],\"ebQKK7\":[\"Site\"],\"egK+Yy\":[\"Bearer tokens for scripts and automation\"],\"ehj/zN\":[\"Redirect Type\"],\"eneWvv\":[\"Draft\"],\"erTMh7\":[\"Last synced\"],\"f+m8jj\":[\"Feed URL copied.\"],\"f8fH8W\":[\"Design\"],\"fWYqkz\":[\"Code Injection\"],\"gOWiTY\":[\"Load a serif font optimized for Chinese, Japanese, or Korean content.\"],\"gZ5owP\":[\"Search repositories\"],\"gbqbh6\":[\"Safe to leave this page — syncing continues in the background.\"],\"gkFvVN\":[\"Injected before </body>. Use for chat widgets and scripts that should not block page load.\"],\"gtQsRO\":[\"Create Custom URL\"],\"hBO/y4\":[\"Security token expired. Refresh the page and try again.\"],\"hGmyDl\":[\"Tokens let you access the API from scripts, shortcuts, and other tools without signing in.\"],\"hIHkRy\":[\"Connected via GitHub App\"],\"hdSi1b\":[\"Type \",[\"repo\"],\" to confirm\"],\"he3ygx\":[\"Copy\"],\"i0qMbr\":[\"Home\"],\"iEUzMn\":[\"system\"],\"iSLIjg\":[\"Connect\"],\"iVOMRi\":[\"Home settings updated.\"],\"icB4Cv\":[\"Drag links here to show them under the More menu\"],\"id3vuh\":[\"Telegram is set up, but the bot couldn't be reached. Check the bot token and try again.\"],\"ihn4zD\":[\"Search…\"],\"iiDXZc\":[\"Displayed at the bottom of all posts and pages.\"],\"j4VrG6\":[\"Download Export ZIP\"],\"j5nQL2\":[\"e.g. iOS Shortcuts\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jgBjXJ\":[\"Revoke this token? Any scripts using it will stop working.\"],\"jpctdh\":[\"View\"],\"k1ifdL\":[\"Processing...\"],\"kMXclu\":[\"Download a site export\"],\"kNiQp6\":[\"Pinned\"],\"kRhzWq\":[\"GitHub Sync\"],\"kVQs7s\":[\"Fine-grained styling overrides\"],\"ke1gWS\":[\"Custom URLs\"],\"kfcRb0\":[\"Avatar\"],\"kxDZ2i\":[\"This code runs on every page of your site.\"],\"l2Op2p\":[\"Query Parameters\"],\"lLW3vJ\":[\"Target Slug\"],\"lYHJih\":[\"Revoke this session? That device will need to sign in again.\"],\"mLOk1i\":[\"Push all posts to GitHub right now instead of waiting for the next automatic sync.\"],\"mSNmrX\":[\"List posts:\"],\"nK07ni\":[\"Choose a typographic direction for your site. Each theme changes both the font pairing and the reading rhythm.\"],\"nbfdhU\":[\"Integrations\"],\"o/vNDE\":[\"lets you override any theme variable.\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"We'll prefill the name \",[\"name\"],\". The list refreshes on return.\"],\"oKOOsY\":[\"Color Theme\"],\"oL535e\":[\"Not synced yet\"],\"oNA4If\":[\"All collections are already in your navigation.\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"pgTIrt\":[\"Choose the GitHub account and repository to sync with this site.\"],\"psoxDF\":[\"That font theme isn't available. Pick another one.\"],\"pvnfJD\":[\"Dark\"],\"q+hNag\":[\"Collection\"],\"qdcESc\":[\"Create a new repository\"],\"r5EW6f\":[\"This repository is already backing up another Jant site (\",[\"host\"],\"). Pick a different repository.\"],\"rEspiY\":[\"Navigation placement updated.\"],\"rFmBG3\":[\"Color theme\"],\"rlonmB\":[\"Couldn't delete. Try again in a moment.\"],\"satWc6\":[\"Main RSS feed\"],\"sgr2wQ\":[\"collection\"],\"sqxcaY\":[\"Created \",[\"date\"]],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"t3hvHq\":[\"Sync Now\"],\"tJ4H0O\":[\"your Telegram account\"],\"tfDRzk\":[\"Save\"],\"tvgAq5\":[\"No accounts authorized yet\"],\"u1VTd3\":[\"Palette, surface tone, and overall mood\"],\"u3wRF+\":[\"Published\"],\"u6KOjV\":[\"Want more control?\"],\"udPwLB\":[\"Header\"],\"ui6aMF\":[\"These devices are currently signed in to your account. Revoke any session you don't recognize.\"],\"vBEKwo\":[\"Manage this site's active sessions here. Password and hosted access are managed through \",[\"providerLabel\"],\".\"],\"vRldcl\":[\"Typography choices and reading texture\"],\"vSYKYI\":[\"Main feed\"],\"vTuib7\":[\"This controls what /feed returns.\"],\"vXIe7J\":[\"Language\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzX5FB\":[\"Delete Account\"],\"w8Rv8T\":[\"Label is required\"],\"wL3cK8\":[\"Latest\"],\"wPmHHc\":[\"Quiet surfaces let writing lead.\"],\"wW6NCp\":[\"Last error\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wuLtXn\":[\"No active sessions right now. Signed-in devices show up here.\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xHt036\":[\"Personal Access Token\"],\"xbN8dp\":[\"Soft color should still carry a clear reading rhythm.\"],\"y28hnO\":[\"Post\"],\"y8Md/V\":[\"Language and time updated.\"],\"yNCqOt\":[\"Latest feed\"],\"yQ3kNF\":[\"Type the following phrase to confirm:\"],\"ydq1k2\":[\"Pick an account first\"],\"yjjCV8\":[\"Fixed feed URLs\"],\"yjkELF\":[\"Confirm New Password\"],\"yzF66j\":[\"Link\"],\"z6wakA\":[\"A short intro shown on your home page.\"],\"zEizrk\":[\"Last used \",[\"date\"]],\"zSURJW\":[\"No repositories match.\"],\"zXH2jX\":[\"Language & Time\"],\"zlcDd2\":[\"Delete this custom URL? Visitors using it won't be redirected anymore.\"],\"zwBp5t\":[\"Private\"],\"zxRN6H\":[\"Header links, home feed, and overflow menu\"]}");
|
|
1919
1920
|
//#endregion
|
|
1920
1921
|
//#region src/i18n/locales/settings/zh-Hans.ts
|
|
1921
|
-
var messages$1 = JSON.parse("{\"+4Z6iP\":[\"请先在 GitHub 上创建仓库 — 可以为空。\"],\"+9JI/F\":[\"连接后会将你的网站同步到 \",[\"repo\"],\" 的默认分支,并追加到其现有历史之上。Jant 管理路径之外的现有文件会被保留。此操作无法撤销。\"],\"+AXdXp\":[\"标签和 URL 为必填项\"],\"+K0AvT\":[\"断开连接\"],\"+zy2Nq\":[\"类型\"],\"/3H2/s\":[\"此托管站点通过 \",[\"providerLabel\"],\" 登录。请在那里管理密码和托管访问权限。\"],\"/JnyjR\":[\"切换内置导航项。它们的顺序决定页眉显示的内容以及首页显示哪个视图。\"],\"/ODeyS\":[\"声明给读者的内容语言(HTML lang、RSS),同时驱动后台界面的语言。可填任意 BCP 47 标签;没有对应翻译时后台会回退为英文。\"],\"0OGSSc\":[\"头像显示已更新.\"],\"0UzCUX\":[\"更新您用于登录的密码\"],\"10UtuM\":[\"CJK 字体\"],\"14BEca\":[\"了解原因\"],\"1F6Mzc\":[\"当前还没有导航项目。添加链接或在下方启用系统项目。\"],\"1njn7W\":[\"浅色\"],\"2B7t+s\":[\"会话与密码\"],\"2DoBvq\":[\"订阅源\"],\"2FYpfJ\":[\"更多\"],\"2MXb5X\":[\"关于静谧设计的笔记\"],\"2PTjMB\":[\"我想删除 \",[\"siteName\"]],\"2cFU6q\":[\"网站页脚\"],\"2oWZo7\":[\"最近一次提交\"],\"2uuy4H\":[\"通过个人访问令牌连接\"],\"35x8eZ\":[\"显示 \",[\"shown\"],\" 共 \",[\"total\"]],\"3Cw1AI\":[\"添加合集\"],\"3VrybB\":[\"重定向\"],\"3Yvsaz\":[\"302 (临时)\"],\"3n0zbB\":[\"会话管理在演示模式下已关闭。请使用共享的演示会话。\"],\"3sYJi5\":[\"下载一个与 Hugo 兼容的归档 — 将其静态托管或迁移到另一个 Jant。\"],\"3wKq0C\":[\"保存失败。请稍后再试。\"],\"49Bsal\":[\"订阅源设置已更新。\"],\"4Jge8E\":[\"活动会话\"],\"4KIa+q\":[\"导出文件已下载.\"],\"4cEClj\":[\"会话\"],\"4zGJ5E\":[\"永久删除账号\"],\"5QlUIt\":[\"仓库为空。准备连接。\"],\"5VQnR3\":[\"当你想要一个永远不变的订阅源 URL 时使用它们。\"],\"5dpcN1\":[\"输入以搜索全部\"],\"69OXZB\":[\"删除托管站点\"],\"6ArdBh\":[\"将精选文章用于 /feed。\"],\"6DjeBT\":[\"演示站点始终对搜索引擎隐藏。\"],\"6FFB7q\":[\"为 /feed 使用最新的公开帖子。\"],\"6K1Vef\":[\"永久删除此博客?此操作不可撤销。\"],\"6NpNLc\":[\"此仓库已有内容。\"],\"6V3Ea3\":[\"已复制\"],\"746NHh\":[\"此博客\"],\"7811AW\":[\"此仓库已在备份此站点。\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允许搜索引擎索引我的网站\"],\"7MZxzw\":[\"密码已更改.\"],\"7vhWI8\":[\"新密码\"],\"7z05Pf\":[\"在 \",[\"providerLabel\"],\" 打开托管站点控制以取消计费或永久删除此站点。\"],\"81nFIS\":[\"密码不匹配。请确保两个字段相同。\"],\"87a/t/\":[\"标签\"],\"89Upyo\":[\"该主题不可用。请选择其他主题。\"],\"8BfEpW\":[\"托管账户\"],\"8N/Mcp\":[\"归档 筛选参数 (例如 format=笔记&view=list)\"],\"8U2Z7f\":[\"新建自定义 URL\"],\"8ZsakT\":[\"密码\"],\"9+vGLh\":[\"自定义 CSS\"],\"9As8Nu\":[\"在 GitHub 上创建一个\"],\"9Lsvt5\":[\"于 \",[\"date\"],\" 登录\"],\"9T7Cwm\":[\"重定向、个性化路径和 URL 控制\"],\"9aUyym\":[\"查看你的登录位置并撤销旧会话\"],\"A1taO8\":[\"搜索\"],\"AeXO77\":[\"账户\"],\"AnY+O9\":[\"在主页底部显示 \\\"Build with Jant\\\"\"],\"ApZDMk\":[\"此图用于您的 favicon 和 apple-touch-icon。为获得最佳效果,请上传至少 512×512 像素、纯色背景的方形 PNG。\"],\"B495Gs\":[\"归档\"],\"B4ESok\":[\"API 参考\"],\"CTAEes\":[\"选择仓库\"],\"CjZZgz\":[\"该仓库已有提交\"],\"DCKkhU\":[\"当前密码\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密码和托管访问\"],\"EO3I6h\":[\"上传未成功。请稍后再试。\"],\"Enslfm\":[\"目标地址\"],\"F7FKwe\":[\"你在此处粘贴的任何内容都将完全访问访客的浏览器。仅使用来自你信任的来源的代码。\"],\"FkMol5\":[\"精选\"],\"G/1oP+\":[\"移除 webhook 并停止同步。您的仓库内容不会被删除。\"],\"G0qJsQ\":[\"缺少安全令牌。刷新页面后重试。\"],\"G39wnK\":[\"将内容备份并与 GitHub 仓库同步\"],\"GMMWcy\":[\"名称、元数据、语言和搜索默认设置\"],\"GXsAby\":[\"撤销\"],\"GxkJXS\":[\"正在上传...\"],\"GzKzUa\":[\"演示限制\"],\"HKH+W+\":[\"数据\"],\"Hp1l6f\":[\"当前\"],\"HxlY7t\":[\"更改此项会更新订阅者从 /feed 获取的内容。\"],\"HxuOlm\":[\"网站头部\"],\"I6gXOa\":[\"路径\"],\"ID38tA\":[\"演示模式下已禁用账号删除。共享演示会单独重置。\"],\"IF9tPu\":[\"何时使用站点导出、数据库备份和恢复演练。\"],\"IW5PBo\":[\"复制令牌\"],\"IagCbF\":[\"网址\"],\"IreQBq\":[\"仓库\"],\"J6bLeg\":[\"向任意 URL 添加自定义链接\"],\"JL7LF5\":[\"可用的 CSS 变量, 数据属性, 和 示例.\"],\"JTviaO\":[\"管理登录安全、导出和不可逆操作.\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"现在复制您的令牌 — 它不会再次显示。\"],\"JrFTcr\":[\"连接中…\"],\"JuN5GC\":[\"未选择文件。请选择要上传的文件。\"],\"KDw4GX\":[\"重试\"],\"KSgo21\":[\"选择仓库\"],\"KVVYBh\":[\"向导航添加合集\"],\"KiJn9B\":[\"笔记\"],\"L3DEwT\":[\"移除此头像?您的网站图标和页眉图标将恢复为默认设置。\"],\"L4t4/q\":[\"3月14日\"],\"LdyooL\":[\"链接\"],\"M/D8PK\":[\"+ 在其他帐户上安装\"],\"M/haSd\":[\"始终显示浅色主题。\"],\"M2kIWU\":[\"字体主题\"],\"M6CbAU\":[\"切换编辑面板\"],\"Me5t5H\":[\"将 GitHub 仓库连接以自动将您的文章备份为 Markdown 文件。您在 GitHub 上的编辑会同步回您的网站。\"],\"MtENL9\":[\"调整你的网站的外观、阅读体验和运行方式。\"],\"N/8NPV\":[\"在删除之前,请下载站点导出。删除后将无法恢复此账户。\"],\"N7UNHY\":[\"精选订阅源\"],\"NHnUHF\":[\"Favicon 和页眉中的个人标识\"],\"NU2Fqi\":[\"保存 CSS\"],\"Nldjdr\":[\"尚无自定义 URL。创建一个以便为文章添加重定向或自定义路径。\"],\"O7rgs6\":[\"页眉 RSS 指向你的 \",[\"feed\"],\" 源 (/feed)。在 常规 中更改 /feed 返回的内容。\"],\"OSJXFg\":[\"应用于整个站点,包括管理页面。选择一个调色板,然后选择它是随系统变化还是保持固定。\"],\"Ox3+3h\":[\"无匹配结果。\"],\"PEUV5I\":[\"代码注入已更新。\"],\"PZ7HJ8\":[\"博客头像\"],\"Pwqkdw\":[\"正在加载…\"],\"PxJ9W6\":[\"生成令牌\"],\"Q/6Y+2\":[\"需要对目标仓库的 Contents(读/写)和 Webhooks(读/写)。\"],\"Q30z/l\":[\"要从导航中移除此合集吗?合集本身不会被删除。\"],\"Q99OtV\":[\"将合集固定到导航栏。在过去 48 小时内更新的合集旁会出现一个 * 号。\"],\"QZmz0H\":[\"内置链接\"],\"Qnrzvb\":[\"活动令牌\"],\"R6Z4LE\":[\"下载失败。请重试。\"],\"R9Khdg\":[\"自动\"],\"RxsRD6\":[\"时区\"],\"SJmfuf\":[\"站点名称\"],\"SKZhW9\":[\"令牌名称\"],\"SVQQPe\":[\"无法连接。检查错误并重试。\"],\"TpF3v+\":[\"注入到 </head> 之前。用于分析、自定义元标签以及必须尽早加载的样式。\"],\"Tz0i8g\":[\"设置\"],\"UFK415\":[\"用于分析和小部件的全站 HTML\"],\"Uj/btJ\":[\"在我的站点页眉显示头像\"],\"UsODUn\":[\"选择账户\"],\"UxKoFf\":[\"导航\"],\"V+bhUy\":[\"安装 GitHub App\"],\"V4WsyL\":[\"添加链接\"],\"V5pZwT\":[\"搜索设置已更新。\"],\"VXUPla\":[\"使用 GitHub 应用连接\"],\"VhMDMg\":[\"更改密码\"],\"Vn3jYy\":[\"导航项\"],\"VoZYGU\":[\"这将永久删除您所有的数据 — 帖子、媒体、合集、设置和您的账户。您的博客将重置为初始设置状态。此操作不可撤销。\"],\"Weq9zb\":[\"常规\"],\"Wi9i06\":[\"遵循每位访客的系统偏好。\"],\"Wx1M8N\":[\"安装 GitHub App 以在无需管理个人令牌的情况下授予访问权限。权限按仓库范围授予,可在 GitHub 上撤销。\"],\"X+8FMk\":[\"当前密码不正确。请重试。\"],\"X1G9eY\":[\"导航预览\"],\"X9Hujr\":[\"手动推送\"],\"XtBJV8\":[\"正在检查仓库…\"],\"Xtc16w\":[\"刷新仓库列表\"],\"Y/F35r\":[\"使用 curl 创建帖子:\"],\"YF6zHf\":[\"站点设置已更新.\"],\"YdG2RF\":[\"导出站点\"],\"YwhjRx\":[\"管理账户\"],\"ZDY7Fy\":[\"正在同步…\"],\"ZQKLI1\":[\"危险操作\"],\"ZS/CBL\":[\"删除此导航链接? 访客将不再在您网站的页眉中看到它。\"],\"ZhhOwV\":[\"引用 (Jant 的帖子格式之一。)\"],\"ZiooJI\":[\"API 令牌\"],\"Zm7Qb0\":[\"备份与恢复指南\"],\"ZmUkwN\":[\"向导航添加自定义链接\"],\"a14mj8\":[\"未知设备\"],\"a3LDKx\":[\"安全\"],\"aAIQg2\":[\"外观\"],\"aFkzVF\":[\"目标文章或合集的 slug\"],\"alKG0+\":[\"字体主题\"],\"anibOb\":[\"关于本博客\"],\"any7NR\":[\"主题指南\"],\"b+/jO6\":[\"301 (永久)\"],\"bHOiy1\":[\"演示模式下已禁用密码更改。请使用共享的演示凭证登录。\"],\"bHYIks\":[\"退出登录\"],\"bmrL08\":[\"演示模式会隐藏会话、密码更改和账号删除。导出仍然可用.\"],\"c3MN2z\":[\"所有可用的端点和请求格式。\"],\"cSDy01\":[\"自定义 CSS 已更新.\"],\"clzoNp\":[\"始终显示暗色主题.\"],\"cnGeoo\":[\"删除\"],\"d3FRkY\":[\"无法复制。请再试一次。\"],\"d5oGUo\":[\"在 GitHub 上创建新仓库\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"确认删除账户\"],\"dYKrp3\":[\"从最新中隐藏 (不出现在 Latest 中,但仍可通过固定链接和集合访问的状态。)\"],\"dk7TCH\":[\"永久删除所有数据并重置博客\"],\"drodVV\":[\"还没有合集。请先创建一个,然后将其添加到你的导航中。\"],\"dsWkIw\":[\"要与 GitHub 断开连接吗?该 webhook 将被移除。您的仓库内容不会被删除。\"],\"e/tSI5\":[\"导航顺序已更新。\"],\"ePK91l\":[\"编辑\"],\"ebQKK7\":[\"站点\"],\"egK+Yy\":[\"用于脚本和自动化的 Bearer 令牌\"],\"ehj/zN\":[\"重定向类型\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"订阅源 URL 已复制。\"],\"f8fH8W\":[\"设计\"],\"fWYqkz\":[\"代码注入\"],\"gOWiTY\":[\"加载针对中文、日文或韩文内容优化的衬线字体。\"],\"gZ5owP\":[\"搜索仓库\"],\"gbqbh6\":[\"放心离开此页面 — 同步会在后台继续进行。\"],\"gkFvVN\":[\"注入到 </body> 之前。用于聊天小部件和不应阻塞页面加载的脚本。\"],\"gtQsRO\":[\"创建自定义 URL\"],\"hBO/y4\":[\"安全令牌已过期。刷新页面后重试。\"],\"hGmyDl\":[\"令牌让您无需登录即可从脚本、快捷方式和其他工具访问 API。\"],\"hIHkRy\":[\"已通过 GitHub App 连接\"],\"hdSi1b\":[\"输入 \",[\"repo\"],\" 以确认\"],\"he3ygx\":[\"复制\"],\"i0qMbr\":[\"首页\"],\"iEUzMn\":[\"系统\"],\"iSLIjg\":[\"连接\"],\"iVOMRi\":[\"主页设置已更新.\"],\"icB4Cv\":[\"将链接拖到此处以在更多菜单下显示它们\"],\"ihn4zD\":[\"搜索…\"],\"iiDXZc\":[\"显示在所有文章和页面的底部。\"],\"j4VrG6\":[\"下载导出 ZIP\"],\"j5nQL2\":[\"例如 iOS Shortcuts\"],\"jUV7CU\":[\"上传头像\"],\"jVUmOK\":[\"支持 Markdown\"],\"jgBjXJ\":[\"撤销此令牌?任何使用它的脚本将停止工作。\"],\"jpctdh\":[\"查看\"],\"k1ifdL\":[\"处理中...\"],\"kMXclu\":[\"下载网站导出\"],\"kNiQp6\":[\"已置顶\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"细粒度样式覆盖\"],\"ke1gWS\":[\"自定义 URL\"],\"kfcRb0\":[\"头像\"],\"kxDZ2i\":[\"此代码将在您网站的每个页面上运行。\"],\"l2Op2p\":[\"查询参数\"],\"lLW3vJ\":[\"目标 slug\"],\"lYHJih\":[\"撤销此会话? 该设备需要重新登录.\"],\"mLOk1i\":[\"立即将所有文章推送到 GitHub,而不是等待下一次自动同步。\"],\"mSNmrX\":[\"列出帖子:\"],\"nK07ni\":[\"为您的网站选择一种排版方向。每个主题都会同时改变字体搭配和阅读节奏。\"],\"o/vNDE\":[\"允许您覆盖任何主题变量.\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"我们会预先填充名称 \",[\"name\"],\"。返回时列表会刷新。\"],\"oKOOsY\":[\"颜色主题\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有合集已在您的导航中。\"],\"pZq3aX\":[\"上传失败。请重试。\"],\"pgTIrt\":[\"选择要与此站点同步的 GitHub 账户和仓库。\"],\"psoxDF\":[\"该字体主题不可用。请选择另一个。\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"合集\"],\"qdcESc\":[\"创建新仓库\"],\"r5EW6f\":[\"此仓库已在为另一个 Jant 站点 (\",[\"host\"],\") 进行备份。请选择其他仓库。\"],\"rEspiY\":[\"导航位置已更新.\"],\"rFmBG3\":[\"配色主题\"],\"rlonmB\":[\"删除失败。请稍后再试。\"],\"satWc6\":[\"主 RSS 源\"],\"sgr2wQ\":[\"合集\"],\"sqxcaY\":[\"创建于 \",[\"date\"]],\"sxkWRg\":[\"高级\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tfDRzk\":[\"保存\"],\"tvgAq5\":[\"尚未授权任何账户\"],\"u1VTd3\":[\"调色板、表面色调与整体氛围\"],\"u3wRF+\":[\"已发布\"],\"u6KOjV\":[\"想要更多控制?\"],\"udPwLB\":[\"页眉\"],\"ui6aMF\":[\"以下设备当前已登录到您的帐户。撤销任何您不认识的会话。\"],\"vBEKwo\":[\"在此管理本站点的活动会话。密码和托管访问通过 \",[\"providerLabel\"],\" 管理。\"],\"vRldcl\":[\"排版选项与阅读质感\"],\"vSYKYI\":[\"主订阅源\"],\"vTuib7\":[\"这将控制 /feed 返回的内容。\"],\"vXIe7J\":[\"语言\"],\"vmQmHx\":[\"添加自定义 CSS 以覆盖任何样式. 使用数据属性如 [data-page], [data-post], [data-format] 来定位特定元素.\"],\"vzX5FB\":[\"删除账号\"],\"w8Rv8T\":[\"标签为必填项\"],\"wL3cK8\":[\"最新\"],\"wPmHHc\":[\"低调的界面让写作成为焦点.\"],\"wW6NCp\":[\"上次错误\"],\"wc+17X\":[\"/* 在此填写您的自定义 CSS */\"],\"wuLtXn\":[\"当前没有活动会话。 已登录的设备会显示在此处。\"],\"xCWek4\":[\"文件存储尚未设置。请检查服务器配置。\"],\"xHt036\":[\"个人访问令牌\"],\"xbN8dp\":[\"柔和的颜色仍应保持清晰的阅读节奏。\"],\"y28hnO\":[\"文章\"],\"y8Md/V\":[\"语言和时间已更新。\"],\"yNCqOt\":[\"最新订阅源\"],\"yQ3kNF\":[\"输入以下短语以确认:\"],\"ydq1k2\":[\"请先选择一个账号\"],\"yjjCV8\":[\"固定订阅源 URL\"],\"yjkELF\":[\"确认新密码\"],\"yzF66j\":[\"链接\"],\"z6wakA\":[\"在您的主页上显示的简短介绍.\"],\"zEizrk\":[\"上次使用 \",[\"date\"]],\"zSURJW\":[\"没有匹配的仓库。\"],\"zXH2jX\":[\"语言与时间\"],\"zlcDd2\":[\"要删除此自定义 URL 吗?使用它的访问者将不再被重定向。\"],\"zwBp5t\":[\"私有\"],\"zxRN6H\":[\"页眉链接、首页信息流和溢出菜单\"]}");
|
|
1922
|
+
var messages$1 = JSON.parse("{\"+4Z6iP\":[\"请先在 GitHub 上创建仓库 — 可以为空。\"],\"+9JI/F\":[\"连接后会将你的网站同步到 \",[\"repo\"],\" 的默认分支,并追加到其现有历史之上。Jant 管理路径之外的现有文件会被保留。此操作无法撤销。\"],\"+AXdXp\":[\"标签和 URL 为必填项\"],\"+K0AvT\":[\"断开连接\"],\"+zy2Nq\":[\"类型\"],\"/3H2/s\":[\"此托管站点通过 \",[\"providerLabel\"],\" 登录。请在那里管理密码和托管访问权限。\"],\"/JnyjR\":[\"切换内置导航项。它们的顺序决定页眉显示的内容以及首页显示哪个视图。\"],\"/ODeyS\":[\"声明给读者的内容语言(HTML lang、RSS),同时驱动后台界面的语言。可填任意 BCP 47 标签;没有对应翻译时后台会回退为英文。\"],\"/zOUxl\":[\"QR code linking to the Telegram bot\"],\"0OGSSc\":[\"头像显示已更新.\"],\"0UzCUX\":[\"更新您用于登录的密码\"],\"0bdA9b\":[\"Open Telegram to connect\"],\"10UtuM\":[\"CJK 字体\"],\"14BEca\":[\"了解原因\"],\"1F6Mzc\":[\"当前还没有导航项目。添加链接或在下方启用系统项目。\"],\"1mbBbL\":[\"Connect manually\"],\"1njn7W\":[\"浅色\"],\"2B7t+s\":[\"会话与密码\"],\"2DoBvq\":[\"订阅源\"],\"2FYpfJ\":[\"更多\"],\"2Ithfh\":[\"Message the bot any text and it's published as a note.\"],\"2MXb5X\":[\"关于静谧设计的笔记\"],\"2PTjMB\":[\"我想删除 \",[\"siteName\"]],\"2cFU6q\":[\"网站页脚\"],\"2oWZo7\":[\"最近一次提交\"],\"2uuy4H\":[\"通过个人访问令牌连接\"],\"35x8eZ\":[\"显示 \",[\"shown\"],\" 共 \",[\"total\"]],\"39QGku\":[\"Open the bot and send the binding code, then anything you message it becomes a note.\"],\"3Cw1AI\":[\"添加合集\"],\"3VrybB\":[\"重定向\"],\"3Yvsaz\":[\"302 (临时)\"],\"3n0zbB\":[\"会话管理在演示模式下已关闭。请使用共享的演示会话。\"],\"3sYJi5\":[\"下载一个与 Hugo 兼容的归档 — 将其静态托管或迁移到另一个 Jant。\"],\"3wKq0C\":[\"保存失败。请稍后再试。\"],\"49Bsal\":[\"订阅源设置已更新。\"],\"4Jge8E\":[\"活动会话\"],\"4KIa+q\":[\"导出文件已下载.\"],\"4cEClj\":[\"会话\"],\"4zGJ5E\":[\"永久删除账号\"],\"5QlUIt\":[\"仓库为空。准备连接。\"],\"5VQnR3\":[\"当你想要一个永远不变的订阅源 URL 时使用它们。\"],\"5dpcN1\":[\"输入以搜索全部\"],\"5f1Wo9\":[\"Connected as \",[\"account\"]],\"69OXZB\":[\"删除托管站点\"],\"6ArdBh\":[\"将精选文章用于 /feed。\"],\"6DjeBT\":[\"演示站点始终对搜索引擎隐藏。\"],\"6FFB7q\":[\"为 /feed 使用最新的公开帖子。\"],\"6K1Vef\":[\"永久删除此博客?此操作不可撤销。\"],\"6NpNLc\":[\"此仓库已有内容。\"],\"6V3Ea3\":[\"已复制\"],\"71WIgc\":[\"Get a new code\"],\"746NHh\":[\"此博客\"],\"7811AW\":[\"此仓库已在备份此站点。\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允许搜索引擎索引我的网站\"],\"7GISOt\":[\"Save bot token\"],\"7MZxzw\":[\"密码已更改.\"],\"7vhWI8\":[\"新密码\"],\"7z05Pf\":[\"在 \",[\"providerLabel\"],\" 打开托管站点控制以取消计费或永久删除此站点。\"],\"81nFIS\":[\"密码不匹配。请确保两个字段相同。\"],\"87a/t/\":[\"标签\"],\"89Upyo\":[\"该主题不可用。请选择其他主题。\"],\"8BfEpW\":[\"托管账户\"],\"8N/Mcp\":[\"归档 筛选参数 (例如 format=笔记&view=list)\"],\"8T46pB\":[\"Bot token\"],\"8U2Z7f\":[\"新建自定义 URL\"],\"8ZsakT\":[\"密码\"],\"9+vGLh\":[\"自定义 CSS\"],\"9As8Nu\":[\"在 GitHub 上创建一个\"],\"9Lsvt5\":[\"于 \",[\"date\"],\" 登录\"],\"9T7Cwm\":[\"重定向、个性化路径和 URL 控制\"],\"9aUyym\":[\"查看你的登录位置并撤销旧会话\"],\"A1taO8\":[\"搜索\"],\"AeXO77\":[\"账户\"],\"AnY+O9\":[\"在主页底部显示 \\\"Build with Jant\\\"\"],\"ApZDMk\":[\"此图用于您的 favicon 和 apple-touch-icon。为获得最佳效果,请上传至少 512×512 像素、纯色背景的方形 PNG。\"],\"B495Gs\":[\"归档\"],\"B4ESok\":[\"API 参考\"],\"BzEFor\":[\"or\"],\"CDAdlf\":[\"Remove bot\"],\"CTAEes\":[\"选择仓库\"],\"CjZZgz\":[\"该仓库已有提交\"],\"D8k2s6\":[\"Connect Telegram\"],\"DCKkhU\":[\"当前密码\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密码和托管访问\"],\"EO3I6h\":[\"上传未成功。请稍后再试。\"],\"Enslfm\":[\"目标地址\"],\"F7FKwe\":[\"你在此处粘贴的任何内容都将完全访问访客的浏览器。仅使用来自你信任的来源的代码。\"],\"FkMol5\":[\"精选\"],\"G/1oP+\":[\"移除 webhook 并停止同步。您的仓库内容不会被删除。\"],\"G0qJsQ\":[\"缺少安全令牌。刷新页面后重试。\"],\"G39wnK\":[\"Back up and sync content with a GitHub repository\"],\"GMMWcy\":[\"名称、元数据、语言和搜索默认设置\"],\"GXsAby\":[\"撤销\"],\"GxkJXS\":[\"正在上传...\"],\"GzKzUa\":[\"演示限制\"],\"HKH+W+\":[\"数据\"],\"Hp1l6f\":[\"当前\"],\"HxlY7t\":[\"更改此项会更新订阅者从 /feed 获取的内容。\"],\"HxuOlm\":[\"网站头部\"],\"I6gXOa\":[\"路径\"],\"ID38tA\":[\"演示模式下已禁用账号删除。共享演示会单独重置。\"],\"IF9tPu\":[\"何时使用站点导出、数据库备份和恢复演练。\"],\"IW5PBo\":[\"复制令牌\"],\"IagCbF\":[\"网址\"],\"IreQBq\":[\"仓库\"],\"J6bLeg\":[\"向任意 URL 添加自定义链接\"],\"JL7LF5\":[\"可用的 CSS 变量, 数据属性, 和 示例.\"],\"JTviaO\":[\"管理登录安全、导出和不可逆操作.\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"现在复制您的令牌 — 它不会再次显示。\"],\"JrFTcr\":[\"连接中…\"],\"JuN5GC\":[\"未选择文件。请选择要上传的文件。\"],\"KDw4GX\":[\"重试\"],\"KSgo21\":[\"选择仓库\"],\"KVVYBh\":[\"向导航添加合集\"],\"KiJn9B\":[\"笔记\"],\"L3DEwT\":[\"移除此头像?您的网站图标和页眉图标将恢复为默认设置。\"],\"L4t4/q\":[\"3月14日\"],\"LdyooL\":[\"链接\"],\"M/D8PK\":[\"+ 在其他帐户上安装\"],\"M/haSd\":[\"始终显示浅色主题。\"],\"M2kIWU\":[\"字体主题\"],\"M6CbAU\":[\"切换编辑面板\"],\"MaYYE6\":[\"Post notes by messaging a Telegram bot\"],\"Me5t5H\":[\"将 GitHub 仓库连接以自动将您的文章备份为 Markdown 文件。您在 GitHub 上的编辑会同步回您的网站。\"],\"Mr4QPw\":[\"Disconnect Telegram? You can reconnect any time with a new binding code.\"],\"MtENL9\":[\"调整你的网站的外观、阅读体验和运行方式。\"],\"N/8NPV\":[\"在删除之前,请下载站点导出。删除后将无法恢复此账户。\"],\"N7UNHY\":[\"精选订阅源\"],\"NHnUHF\":[\"Favicon 和页眉中的个人标识\"],\"NU2Fqi\":[\"保存 CSS\"],\"Nldjdr\":[\"尚无自定义 URL。创建一个以便为文章添加重定向或自定义路径。\"],\"O7rgs6\":[\"页眉 RSS 指向你的 \",[\"feed\"],\" 源 (/feed)。在 常规 中更改 /feed 返回的内容。\"],\"OSJXFg\":[\"应用于整个站点,包括管理页面。选择一个调色板,然后选择它是随系统变化还是保持固定。\"],\"Ox3+3h\":[\"无匹配结果。\"],\"PEUV5I\":[\"代码注入已更新。\"],\"PXj9lw\":[\"Stop accepting posts from Telegram. Your existing notes stay published.\"],\"PZ7HJ8\":[\"博客头像\"],\"Pwqkdw\":[\"正在加载…\"],\"PxJ9W6\":[\"生成令牌\"],\"Q/6Y+2\":[\"需要对目标仓库的 Contents(读/写)和 Webhooks(读/写)。\"],\"Q30z/l\":[\"要从导航中移除此合集吗?合集本身不会被删除。\"],\"Q99OtV\":[\"将合集固定到导航栏。在过去 48 小时内更新的合集旁会出现一个 * 号。\"],\"QZmz0H\":[\"内置链接\"],\"Qnrzvb\":[\"活动令牌\"],\"R6Z4LE\":[\"下载失败。请重试。\"],\"R9Khdg\":[\"自动\"],\"RcdDOS\":[\"Create a bot by messaging @BotFather on Telegram, then paste the token it gives you.\"],\"RxsRD6\":[\"时区\"],\"SJmfuf\":[\"站点名称\"],\"SKZhW9\":[\"令牌名称\"],\"SVQQPe\":[\"无法连接。检查错误并重试。\"],\"SchpMp\":[\"Telegram\"],\"TpF3v+\":[\"注入到 </head> 之前。用于分析、自定义元标签以及必须尽早加载的样式。\"],\"Tz0i8g\":[\"设置\"],\"UFK415\":[\"用于分析和小部件的全站 HTML\"],\"UTvFQq\":[\"Open \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" and send:\"],\"Uj/btJ\":[\"在我的站点页眉显示头像\"],\"UsODUn\":[\"选择账户\"],\"UxKoFf\":[\"导航\"],\"V+bhUy\":[\"安装 GitHub App\"],\"V4WsyL\":[\"添加链接\"],\"V5pZwT\":[\"搜索设置已更新。\"],\"VXUPla\":[\"使用 GitHub 应用连接\"],\"VhMDMg\":[\"更改密码\"],\"Vn3jYy\":[\"导航项\"],\"VoZYGU\":[\"这将永久删除您所有的数据 — 帖子、媒体、合集、设置和您的账户。您的博客将重置为初始设置状态。此操作不可撤销。\"],\"Weq9zb\":[\"常规\"],\"Wi9i06\":[\"遵循每位访客的系统偏好。\"],\"Wx1M8N\":[\"安装 GitHub App 以在无需管理个人令牌的情况下授予访问权限。权限按仓库范围授予,可在 GitHub 上撤销。\"],\"X+8FMk\":[\"当前密码不正确。请重试。\"],\"X1G9eY\":[\"导航预览\"],\"X9Hujr\":[\"手动推送\"],\"XtBJV8\":[\"正在检查仓库…\"],\"Xtc16w\":[\"刷新仓库列表\"],\"Y/F35r\":[\"使用 curl 创建帖子:\"],\"YF6zHf\":[\"站点设置已更新.\"],\"YdG2RF\":[\"导出站点\"],\"YkgZi7\":[\"Connect a Telegram bot, then anything you message it gets published as a note.\"],\"YwhjRx\":[\"管理账户\"],\"ZDY7Fy\":[\"正在同步…\"],\"ZQKLI1\":[\"危险操作\"],\"ZS/CBL\":[\"删除此导航链接? 访客将不再在您网站的页眉中看到它。\"],\"ZhhOwV\":[\"引用 (Jant 的帖子格式之一。)\"],\"ZiooJI\":[\"API 令牌\"],\"Zm7Qb0\":[\"备份与恢复指南\"],\"ZmUkwN\":[\"向导航添加自定义链接\"],\"a14mj8\":[\"未知设备\"],\"a3LDKx\":[\"安全\"],\"aAIQg2\":[\"外观\"],\"aFkzVF\":[\"目标文章或合集的 slug\"],\"alKG0+\":[\"字体主题\"],\"anibOb\":[\"关于本博客\"],\"any7NR\":[\"主题指南\"],\"b+/jO6\":[\"301 (永久)\"],\"bHOiy1\":[\"演示模式下已禁用密码更改。请使用共享的演示凭证登录。\"],\"bHYIks\":[\"退出登录\"],\"bmrL08\":[\"演示模式会隐藏会话、密码更改和账号删除。导出仍然可用.\"],\"c3MN2z\":[\"所有可用的端点和请求格式。\"],\"cS7/bk\":[\"Remove the saved bot token? Its webhook is deleted and any connected account is disconnected.\"],\"cSDy01\":[\"自定义 CSS 已更新.\"],\"clzoNp\":[\"始终显示暗色主题.\"],\"cnGeoo\":[\"删除\"],\"d3FRkY\":[\"无法复制。请再试一次。\"],\"d5oGUo\":[\"在 GitHub 上创建新仓库\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"确认删除账户\"],\"dYKrp3\":[\"从最新中隐藏 (不出现在 Latest 中,但仍可通过固定链接和集合访问的状态。)\"],\"dk7TCH\":[\"永久删除所有数据并重置博客\"],\"drodVV\":[\"还没有合集。请先创建一个,然后将其添加到你的导航中。\"],\"dsWkIw\":[\"要与 GitHub 断开连接吗?该 webhook 将被移除。您的仓库内容不会被删除。\"],\"e/tSI5\":[\"导航顺序已更新。\"],\"ePK91l\":[\"编辑\"],\"ebQKK7\":[\"站点\"],\"egK+Yy\":[\"用于脚本和自动化的 Bearer 令牌\"],\"ehj/zN\":[\"重定向类型\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"订阅源 URL 已复制。\"],\"f8fH8W\":[\"设计\"],\"fWYqkz\":[\"代码注入\"],\"gOWiTY\":[\"加载针对中文、日文或韩文内容优化的衬线字体。\"],\"gZ5owP\":[\"搜索仓库\"],\"gbqbh6\":[\"放心离开此页面 — 同步会在后台继续进行。\"],\"gkFvVN\":[\"注入到 </body> 之前。用于聊天小部件和不应阻塞页面加载的脚本。\"],\"gtQsRO\":[\"创建自定义 URL\"],\"hBO/y4\":[\"安全令牌已过期。刷新页面后重试。\"],\"hGmyDl\":[\"令牌让您无需登录即可从脚本、快捷方式和其他工具访问 API。\"],\"hIHkRy\":[\"已通过 GitHub App 连接\"],\"hdSi1b\":[\"输入 \",[\"repo\"],\" 以确认\"],\"he3ygx\":[\"复制\"],\"i0qMbr\":[\"首页\"],\"iEUzMn\":[\"系统\"],\"iSLIjg\":[\"连接\"],\"iVOMRi\":[\"主页设置已更新.\"],\"icB4Cv\":[\"将链接拖到此处以在更多菜单下显示它们\"],\"id3vuh\":[\"Telegram is set up, but the bot couldn't be reached. Check the bot token and try again.\"],\"ihn4zD\":[\"搜索…\"],\"iiDXZc\":[\"显示在所有文章和页面的底部。\"],\"j4VrG6\":[\"下载导出 ZIP\"],\"j5nQL2\":[\"例如 iOS Shortcuts\"],\"jUV7CU\":[\"上传头像\"],\"jVUmOK\":[\"支持 Markdown\"],\"jgBjXJ\":[\"撤销此令牌?任何使用它的脚本将停止工作。\"],\"jpctdh\":[\"查看\"],\"k1ifdL\":[\"处理中...\"],\"kMXclu\":[\"下载网站导出\"],\"kNiQp6\":[\"已置顶\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"细粒度样式覆盖\"],\"ke1gWS\":[\"自定义 URL\"],\"kfcRb0\":[\"头像\"],\"kxDZ2i\":[\"此代码将在您网站的每个页面上运行。\"],\"l2Op2p\":[\"查询参数\"],\"lLW3vJ\":[\"目标 slug\"],\"lYHJih\":[\"撤销此会话? 该设备需要重新登录.\"],\"mLOk1i\":[\"立即将所有文章推送到 GitHub,而不是等待下一次自动同步。\"],\"mSNmrX\":[\"列出帖子:\"],\"nK07ni\":[\"为您的网站选择一种排版方向。每个主题都会同时改变字体搭配和阅读节奏。\"],\"nbfdhU\":[\"Integrations\"],\"o/vNDE\":[\"允许您覆盖任何主题变量.\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"我们会预先填充名称 \",[\"name\"],\"。返回时列表会刷新。\"],\"oKOOsY\":[\"颜色主题\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有合集已在您的导航中。\"],\"pZq3aX\":[\"上传失败。请重试。\"],\"pgTIrt\":[\"选择要与此站点同步的 GitHub 账户和仓库。\"],\"psoxDF\":[\"该字体主题不可用。请选择另一个。\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"合集\"],\"qdcESc\":[\"创建新仓库\"],\"r5EW6f\":[\"此仓库已在为另一个 Jant 站点 (\",[\"host\"],\") 进行备份。请选择其他仓库。\"],\"rEspiY\":[\"导航位置已更新.\"],\"rFmBG3\":[\"配色主题\"],\"rlonmB\":[\"删除失败。请稍后再试。\"],\"satWc6\":[\"主 RSS 源\"],\"sgr2wQ\":[\"合集\"],\"sqxcaY\":[\"创建于 \",[\"date\"]],\"sxkWRg\":[\"高级\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tJ4H0O\":[\"your Telegram account\"],\"tfDRzk\":[\"保存\"],\"tvgAq5\":[\"尚未授权任何账户\"],\"u1VTd3\":[\"调色板、表面色调与整体氛围\"],\"u3wRF+\":[\"已发布\"],\"u6KOjV\":[\"想要更多控制?\"],\"udPwLB\":[\"页眉\"],\"ui6aMF\":[\"以下设备当前已登录到您的帐户。撤销任何您不认识的会话。\"],\"vBEKwo\":[\"在此管理本站点的活动会话。密码和托管访问通过 \",[\"providerLabel\"],\" 管理。\"],\"vRldcl\":[\"排版选项与阅读质感\"],\"vSYKYI\":[\"主订阅源\"],\"vTuib7\":[\"这将控制 /feed 返回的内容。\"],\"vXIe7J\":[\"语言\"],\"vmQmHx\":[\"添加自定义 CSS 以覆盖任何样式. 使用数据属性如 [data-page], [data-post], [data-format] 来定位特定元素.\"],\"vzX5FB\":[\"删除账号\"],\"w8Rv8T\":[\"标签为必填项\"],\"wL3cK8\":[\"最新\"],\"wPmHHc\":[\"低调的界面让写作成为焦点.\"],\"wW6NCp\":[\"上次错误\"],\"wc+17X\":[\"/* 在此填写您的自定义 CSS */\"],\"wuLtXn\":[\"当前没有活动会话。 已登录的设备会显示在此处。\"],\"xCWek4\":[\"文件存储尚未设置。请检查服务器配置。\"],\"xHt036\":[\"个人访问令牌\"],\"xbN8dp\":[\"柔和的颜色仍应保持清晰的阅读节奏。\"],\"y28hnO\":[\"文章\"],\"y8Md/V\":[\"语言和时间已更新。\"],\"yNCqOt\":[\"最新订阅源\"],\"yQ3kNF\":[\"输入以下短语以确认:\"],\"ydq1k2\":[\"请先选择一个账号\"],\"yjjCV8\":[\"固定订阅源 URL\"],\"yjkELF\":[\"确认新密码\"],\"yzF66j\":[\"链接\"],\"z6wakA\":[\"在您的主页上显示的简短介绍.\"],\"zEizrk\":[\"上次使用 \",[\"date\"]],\"zSURJW\":[\"没有匹配的仓库。\"],\"zXH2jX\":[\"语言与时间\"],\"zlcDd2\":[\"要删除此自定义 URL 吗?使用它的访问者将不再被重定向。\"],\"zwBp5t\":[\"私有\"],\"zxRN6H\":[\"页眉链接、首页信息流和溢出菜单\"]}");
|
|
1922
1923
|
//#endregion
|
|
1923
1924
|
//#region src/i18n/locales/settings/zh-Hant.ts
|
|
1924
|
-
var messages = JSON.parse("{\"+4Z6iP\":[\"先在 GitHub 上建立儲存庫 — 可以是空的。\"],\"+9JI/F\":[\"連線後會將您的網站同步到 \",[\"repo\"],\" 的預設分支,並疊加在其現有歷史之上。Jant 管理路徑外的既有檔案會保留。此操作無法復原。\"],\"+AXdXp\":[\"標籤與 URL 為必填\"],\"+K0AvT\":[\"解除連線\"],\"+zy2Nq\":[\"類型\"],\"/3H2/s\":[\"此託管網站透過 \",[\"providerLabel\"],\" 登入。請在該處管理密碼與託管存取權限。\"],\"/JnyjR\":[\"切換內建導覽項目。它們的順序決定頁首顯示哪些項目,以及首頁會先開啟哪一個 feed。\"],\"/ODeyS\":[\"聲明給讀者的內容語言(HTML lang、RSS),同時驅動後台介面的語言。可填任意 BCP 47 標籤;沒有對應翻譯時後台會回退為英文。\"],\"0OGSSc\":[\"頭像顯示已更新.\"],\"0UzCUX\":[\"更新您用來登入的密碼\"],\"10UtuM\":[\"CJK 字體\"],\"14BEca\":[\"瞭解原因\"],\"1F6Mzc\":[\"目前尚無導覽項目。請新增連結或在下方啟用系統項目。\"],\"1njn7W\":[\"淺色\"],\"2B7t+s\":[\"工作階段與密碼\"],\"2DoBvq\":[\"訂閱來源\"],\"2FYpfJ\":[\"更多\"],\"2MXb5X\":[\"關於靜謐設計的實地筆記\"],\"2PTjMB\":[\"我想刪除 \",[\"siteName\"]],\"2cFU6q\":[\"網站頁尾\"],\"2oWZo7\":[\"最近一次提交\"],\"2uuy4H\":[\"已透過個人存取權杖連線\"],\"35x8eZ\":[\"顯示 \",[\"shown\"],\" 共 \",[\"total\"]],\"3Cw1AI\":[\"新增選集\"],\"3VrybB\":[\"重新導向\"],\"3Yvsaz\":[\"302 (暫時)\"],\"3n0zbB\":[\"在示範模式中已停用工作階段管理。請改用共用示範工作階段。\"],\"3sYJi5\":[\"下載與 Hugo 相容的封存 — 以靜態方式託管或移轉到另一個 Jant\"],\"3wKq0C\":[\"無法儲存。請稍後再試。\"],\"49Bsal\":[\"Feed 設定已更新。\"],\"4Jge8E\":[\"目前的工作階段\"],\"4KIa+q\":[\"已下載匯出檔案。\"],\"4cEClj\":[\"工作階段\"],\"4zGJ5E\":[\"永久刪除帳戶\"],\"5QlUIt\":[\"倉庫為空。準備連線。\"],\"5VQnR3\":[\"當你想要一個永遠不會改變的 feed URL 時,請使用這些。\"],\"5dpcN1\":[\"輸入以搜尋全部\"],\"69OXZB\":[\"刪除託管網站\"],\"6ArdBh\":[\"將精選文章用於 /feed.\"],\"6DjeBT\":[\"示範網站會始終對搜尋引擎保持隱藏。\"],\"6FFB7q\":[\"使用最新的公開貼文作為 /feed。\"],\"6K1Vef\":[\"確定要永久刪除此部落格嗎?此動作無法復原。\"],\"6NpNLc\":[\"此儲存庫已有內容。\"],\"6V3Ea3\":[\"已複製\"],\"746NHh\":[\"此部落格\"],\"7811AW\":[\"此儲存庫已在備份本網站。\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允許搜尋引擎收錄我的網站\"],\"7MZxzw\":[\"密碼已變更.\"],\"7vhWI8\":[\"新密碼\"],\"7z05Pf\":[\"在 \",[\"providerLabel\"],\" 開啟託管站點控制項以取消計費或永久刪除此站點。\"],\"81nFIS\":[\"密碼不符。請確認兩個欄位相同。\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"該主題目前不可用。請選擇其他主題。\"],\"8BfEpW\":[\"託管帳號\"],\"8N/Mcp\":[\"封存篩選參數 (例如 format=note&view=list)\"],\"8U2Z7f\":[\"新增自訂 URL\"],\"8ZsakT\":[\"密碼\"],\"9+vGLh\":[\"自訂 CSS\"],\"9As8Nu\":[\"在 GitHub 上建立一個\"],\"9Lsvt5\":[\"已於 \",[\"date\"],\" 登入\"],\"9T7Cwm\":[\"重新導向、自訂路徑與網址控制\"],\"9aUyym\":[\"查看您在哪裡已登入,並撤銷舊的工作階段\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AnY+O9\":[\"在首頁底部顯示「Build with Jant」\"],\"ApZDMk\":[\"此圖會用於您的 favicon 和 apple-touch-icon。為達最佳效果,請上傳至少 512x512 像素、背景為純色的正方形 PNG。\"],\"B495Gs\":[\"封存\"],\"B4ESok\":[\"API 參考\"],\"CTAEes\":[\"選擇儲存庫\"],\"CjZZgz\":[\"此儲存庫已有提交紀錄\"],\"DCKkhU\":[\"目前密碼\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密碼與託管存取\"],\"EO3I6h\":[\"上傳未成功. 請稍後再試.\"],\"Enslfm\":[\"目標網址\"],\"F7FKwe\":[\"你在此處貼上的任何內容都能完全存取訪客的瀏覽器。僅使用來自你信任來源的程式碼。\"],\"FkMol5\":[\"精選\"],\"G/1oP+\":[\"移除 webhook 並停止同步。您的儲存庫內容不會被刪除。\"],\"G0qJsQ\":[\"找不到安全權杖。請重新整理頁面後再試一次。\"],\"G39wnK\":[\"將內容備份並與 GitHub 儲存庫同步\"],\"GMMWcy\":[\"名稱, 中繼資料, 語言, 和 搜尋預設值\"],\"GXsAby\":[\"撤銷\"],\"GxkJXS\":[\"上傳中...\"],\"GzKzUa\":[\"試用限制\"],\"HKH+W+\":[\"資料\"],\"Hp1l6f\":[\"目前\"],\"HxlY7t\":[\"變更此設定會更新訂閱者從 /feed 取得的內容。\"],\"HxuOlm\":[\"網站頁首\"],\"I6gXOa\":[\"路徑\"],\"ID38tA\":[\"示範模式下帳號刪除已停用。共用示範會另行重置。\"],\"IF9tPu\":[\"何時使用網站匯出、資料庫備份與復原演練。\"],\"IW5PBo\":[\"複製權杖\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"儲存庫\"],\"J6bLeg\":[\"新增自訂連結到任何 URL\"],\"JL7LF5\":[\"可用的 CSS 變數、data 屬性與範例.\"],\"JTviaO\":[\"管理登入安全、匯出,以及不可逆的操作。\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"請立即複製您的權杖 — 它不會再顯示。\"],\"JrFTcr\":[\"連線中…\"],\"JuN5GC\":[\"未選取檔案。請選擇要上傳的檔案。\"],\"KDw4GX\":[\"重試\"],\"KSgo21\":[\"選擇一個儲存庫\"],\"KVVYBh\":[\"新增選集到導覽\"],\"KiJn9B\":[\"筆記\"],\"L3DEwT\":[\"移除這個頭像?您的 favicon 與頁首圖示會回復為預設。\"],\"L4t4/q\":[\"3月14日\"],\"LdyooL\":[\"連結\"],\"M/D8PK\":[\"+ 安裝到其他帳戶\"],\"M/haSd\":[\"永遠顯示主題的淺色版本。\"],\"M2kIWU\":[\"字型主題\"],\"M6CbAU\":[\"切換編輯面板\"],\"Me5t5H\":[\"連接 GitHub 倉庫,自動將你的文章備份為 Markdown 檔案。GitHub 上的編輯會同步回你的網站。\"],\"MtENL9\":[\"調整您的網站外觀、可讀性與執行效能。\"],\"N/8NPV\":[\"在刪除前,請先下載網站匯出檔案。刪除後無法恢復此帳號。\"],\"N7UNHY\":[\"精選 RSS 來源\"],\"NHnUHF\":[\"頁首上的網站圖示與個人標記\"],\"NU2Fqi\":[\"儲存 CSS\"],\"Nldjdr\":[\"尚未有自訂 URL。建立一個自訂 URL 以為文章新增重新導向或自訂路徑。\"],\"O7rgs6\":[\"頁首 RSS 指向你的 \",[\"feed\"],\" 訂閱 (/feed)。在「一般」中變更 /feed 回傳的內容。\"],\"OSJXFg\":[\"套用於整個網站, 包括管理頁面. 選擇一個調色盤, 然後決定它是跟隨系統還是維持固定.\"],\"Ox3+3h\":[\"無相符結果。\"],\"PEUV5I\":[\"程式碼注入已更新。\"],\"PZ7HJ8\":[\"部落格大頭貼\"],\"Pwqkdw\":[\"載入中…\"],\"PxJ9W6\":[\"產生權杖\"],\"Q/6Y+2\":[\"需要在目標儲存庫上擁有 Contents(讀/寫)和 Webhooks(讀/寫)權限。\"],\"Q30z/l\":[\"要從導覽移除這個選集嗎?選集本身不會被刪除。\"],\"Q99OtV\":[\"將選集釘選到導覽列。最近 48 小時內更新的選集旁會顯示一個 * 號。\"],\"QZmz0H\":[\"內建連結\"],\"Qnrzvb\":[\"已啟用的權杖\"],\"R6Z4LE\":[\"下載失敗。請再試一次。\"],\"R9Khdg\":[\"自動\"],\"RxsRD6\":[\"時區\"],\"SJmfuf\":[\"網站名稱\"],\"SKZhW9\":[\"權杖名稱 (API 權杖名稱欄位。)\"],\"SVQQPe\":[\"無法連線。請檢查錯誤並再試一次。\"],\"TpF3v+\":[\"在 </head> 之前注入。用於分析、自訂 meta 標籤,以及必須提前載入的樣式。\"],\"Tz0i8g\":[\"設定\"],\"UFK415\":[\"用於分析與小工具的網站全域 HTML\"],\"Uj/btJ\":[\"在我的網站頁首顯示大頭貼\"],\"UsODUn\":[\"選擇一個帳戶\"],\"UxKoFf\":[\"導覽\"],\"V+bhUy\":[\"安裝 GitHub App\"],\"V4WsyL\":[\"新增連結\"],\"V5pZwT\":[\"搜尋設定已更新。\"],\"VXUPla\":[\"使用 GitHub App 連線\"],\"VhMDMg\":[\"變更密碼\"],\"Vn3jYy\":[\"導覽項目\"],\"VoZYGU\":[\"這會永久刪除您所有的資料 — 文章、媒體、選集、設定,以及您的帳戶。您的部落格將被重設為初始設定狀態。此操作無法復原。\"],\"Weq9zb\":[\"一般\"],\"Wi9i06\":[\"依照每位訪客的系統偏好。\"],\"Wx1M8N\":[\"安裝 GitHub App,以授予存取權而無需管理個人權杖。權限以每個儲存庫為範圍,並可在 GitHub 上撤銷。\"],\"X+8FMk\":[\"目前的密碼不符。請再試一次。\"],\"X1G9eY\":[\"導覽預覽\"],\"X9Hujr\":[\"手動推送\"],\"XtBJV8\":[\"正在檢查儲存庫…\"],\"Xtc16w\":[\"重新整理儲存庫清單\"],\"Y/F35r\":[\"使用 curl 建立貼文:\"],\"YF6zHf\":[\"網站設定已更新.\"],\"YdG2RF\":[\"匯出網站\"],\"YwhjRx\":[\"管理帳戶\"],\"ZDY7Fy\":[\"同步中…\"],\"ZQKLI1\":[\"危險區域\"],\"ZS/CBL\":[\"刪除此導覽連結?訪客將不再在您的網站頁首看到它。\"],\"ZhhOwV\":[\"引用 (Jant 的貼文格式之一。)\"],\"ZiooJI\":[\"API 權杖\"],\"Zm7Qb0\":[\"備份與還原指南\"],\"ZmUkwN\":[\"新增自訂連結到導覽\"],\"a14mj8\":[\"未知裝置\"],\"a3LDKx\":[\"安全性\"],\"aAIQg2\":[\"外觀\"],\"aFkzVF\":[\"目標文章或選集的 slug\"],\"alKG0+\":[\"字型主題\"],\"anibOb\":[\"關於本部落格\"],\"any7NR\":[\"主題指南\"],\"b+/jO6\":[\"301 (永久)\"],\"bHOiy1\":[\"示範模式已停用變更密碼功能。請使用共用示範帳號登入。\"],\"bHYIks\":[\"登出\"],\"bmrL08\":[\"示範模式會隱藏會話、密碼更改與帳號刪除。匯出功能仍可使用。\"],\"c3MN2z\":[\"所有可用的端點與請求格式。\"],\"cSDy01\":[\"自訂 CSS 已更新.\"],\"clzoNp\":[\"始終顯示深色主題。\"],\"cnGeoo\":[\"刪除\"],\"d3FRkY\":[\"無法複製. 請再試一次.\"],\"d5oGUo\":[\"在 GitHub 建立新儲存庫\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"確認刪除帳號\"],\"dYKrp3\":[\"從最新中隱藏 (不出現在 Latest 中,但仍可透過固定連結和集合存取的狀態。)\"],\"dk7TCH\":[\"永久刪除所有資料並重設部落格\"],\"drodVV\":[\"目前還沒有選集。請先建立一個,然後將它加入您的導覽。\"],\"dsWkIw\":[\"要與 GitHub 斷開連線嗎? webhook 將會被移除。您的儲存庫內容不會被刪除。\"],\"e/tSI5\":[\"導覽順序已更新。\"],\"ePK91l\":[\"編輯\"],\"ebQKK7\":[\"網站\"],\"egK+Yy\":[\"供腳本與自動化使用的 Bearer 權杖\"],\"ehj/zN\":[\"重新導向類型\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"已複製訂閱網址。\"],\"f8fH8W\":[\"設計\"],\"fWYqkz\":[\"程式碼注入\"],\"gOWiTY\":[\"載入為中文、日文或韓文內容最佳化的襯線字型。\"],\"gZ5owP\":[\"搜尋儲存庫\"],\"gbqbh6\":[\"可以放心離開此頁面 — 同步會在背景繼續進行。\"],\"gkFvVN\":[\"在 </body> 之前注入。用於聊天小工具和不應阻塞頁面載入的腳本。\"],\"gtQsRO\":[\"建立自訂網址\"],\"hBO/y4\":[\"安全權杖已過期。請重新整理頁面並再試一次。\"],\"hGmyDl\":[\"權杖讓您從腳本, 捷徑和其他工具存取 API 無需登入\"],\"hIHkRy\":[\"已透過 GitHub 應用程式連線\"],\"hdSi1b\":[\"輸入 \",[\"repo\"],\" 以確認\"],\"he3ygx\":[\"複製\"],\"i0qMbr\":[\"首頁\"],\"iEUzMn\":[\"系統\"],\"iSLIjg\":[\"連接\"],\"iVOMRi\":[\"首頁設定已更新。\"],\"icB4Cv\":[\"將連結拖到此處以顯示於「更多」選單下方\"],\"ihn4zD\":[\"搜尋…\"],\"iiDXZc\":[\"顯示於所有文章與頁面的底部。\"],\"j4VrG6\":[\"下載匯出 ZIP\"],\"j5nQL2\":[\"例如 iOS 捷徑\"],\"jUV7CU\":[\"上傳大頭貼\"],\"jVUmOK\":[\"支援 Markdown\"],\"jgBjXJ\":[\"要撤銷這個權杖嗎?任何使用它的腳本都會停止運作。\"],\"jpctdh\":[\"檢視\"],\"k1ifdL\":[\"處理中...\"],\"kMXclu\":[\"下載網站匯出檔案\"],\"kNiQp6\":[\"已釘選\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"細緻的樣式覆寫\"],\"ke1gWS\":[\"自訂 URL\"],\"kfcRb0\":[\"頭像\"],\"kxDZ2i\":[\"此程式碼會在您網站的每個頁面上執行。\"],\"l2Op2p\":[\"查詢參數\"],\"lLW3vJ\":[\"目標 slug\"],\"lYHJih\":[\"撤銷此工作階段?該裝置將需要重新登入。\"],\"mLOk1i\":[\"立即將所有文章推送到 GitHub,而不是等待下一次自動同步。\"],\"mSNmrX\":[\"列出貼文:\"],\"nK07ni\":[\"為您的網站選擇一種排版風格。每個主題會同時改變字體配對與閱讀節奏。\"],\"o/vNDE\":[\"讓您覆寫任何主題變數。\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"我們會預先填入名稱 \",[\"name\"],\"。返回時清單會重新整理。\"],\"oKOOsY\":[\"色彩主題\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有選集已經在您的導覽中。\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pgTIrt\":[\"選擇要與此網站同步的 GitHub 帳號和儲存庫.\"],\"psoxDF\":[\"該字型主題無法使用. 請選擇其他主題.\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"選集\"],\"qdcESc\":[\"建立新儲存庫\"],\"r5EW6f\":[\"此儲存庫已在備份另一個 Jant 網站 (\",[\"host\"],\"). 請選擇其他儲存庫.\"],\"rEspiY\":[\"導覽位置已更新。\"],\"rFmBG3\":[\"色彩主題\"],\"rlonmB\":[\"無法刪除。請稍後再試。\"],\"satWc6\":[\"主要 RSS 訂閱\"],\"sgr2wQ\":[\"選集\"],\"sqxcaY\":[\"建立於 \",[\"date\"]],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tfDRzk\":[\"儲存\"],\"tvgAq5\":[\"尚未授權任何帳戶\"],\"u1VTd3\":[\"調色盤、表面色調與整體氛圍\"],\"u3wRF+\":[\"已發佈\"],\"u6KOjV\":[\"想要更細緻的控制?\"],\"udPwLB\":[\"頁首\"],\"ui6aMF\":[\"這些裝置目前已登入您的帳號。撤銷任何您不認識的工作階段。\"],\"vBEKwo\":[\"在此管理本網站的活動工作階段。密碼與託管存取由 \",[\"providerLabel\"],\" 管理。\"],\"vRldcl\":[\"字體選擇與閱讀質感\"],\"vSYKYI\":[\"主要訂閱來源\"],\"vTuib7\":[\"這會控制 /feed 回傳的內容。\"],\"vXIe7J\":[\"語言\"],\"vmQmHx\":[\"新增自訂 CSS 以覆寫任何樣式。使用像 [data-page]、[data-post]、[data-format] 這類資料屬性來選取特定元素。\"],\"vzX5FB\":[\"刪除帳號\"],\"w8Rv8T\":[\"標籤為必填\"],\"wL3cK8\":[\"最新\"],\"wPmHHc\":[\"低調的介面讓文字成為主角。\"],\"wW6NCp\":[\"上次錯誤\"],\"wc+17X\":[\"/* 在此放入您的自訂 CSS */\"],\"wuLtXn\":[\"目前沒有任何活動中的工作階段。已登入的裝置會顯示在此處。\"],\"xCWek4\":[\"檔案儲存尚未設定。請檢查您的伺服器設定。\"],\"xHt036\":[\"個人存取權杖\"],\"xbN8dp\":[\"柔和的色彩仍應保有清晰的閱讀節奏。\"],\"y28hnO\":[\"文章\"],\"y8Md/V\":[\"語言與時間已更新。\"],\"yNCqOt\":[\"最新 RSS 來源\"],\"yQ3kNF\":[\"請輸入以下短語以確認:\"],\"ydq1k2\":[\"請先選擇一個帳戶\"],\"yjjCV8\":[\"固定的 RSS 檔案網址\"],\"yjkELF\":[\"確認新密碼\"],\"yzF66j\":[\"連結 (Jant 的貼文格式之一。)\"],\"z6wakA\":[\"顯示在您的主頁上的簡短介紹。\"],\"zEizrk\":[\"最後使用於 \",[\"date\"]],\"zSURJW\":[\"沒有符合的儲存庫.\"],\"zXH2jX\":[\"語言與時區\"],\"zlcDd2\":[\"刪除此自訂 URL?使用該 URL 的訪客將不會再被重新導向。\"],\"zwBp5t\":[\"私有\"],\"zxRN6H\":[\"頁首連結、首頁動態與更多選單\"]}");
|
|
1925
|
+
var messages = JSON.parse("{\"+4Z6iP\":[\"先在 GitHub 上建立儲存庫 — 可以是空的。\"],\"+9JI/F\":[\"連線後會將您的網站同步到 \",[\"repo\"],\" 的預設分支,並疊加在其現有歷史之上。Jant 管理路徑外的既有檔案會保留。此操作無法復原。\"],\"+AXdXp\":[\"標籤與 URL 為必填\"],\"+K0AvT\":[\"解除連線\"],\"+zy2Nq\":[\"類型\"],\"/3H2/s\":[\"此託管網站透過 \",[\"providerLabel\"],\" 登入。請在該處管理密碼與託管存取權限。\"],\"/JnyjR\":[\"切換內建導覽項目。它們的順序決定頁首顯示哪些項目,以及首頁會先開啟哪一個 feed。\"],\"/ODeyS\":[\"聲明給讀者的內容語言(HTML lang、RSS),同時驅動後台介面的語言。可填任意 BCP 47 標籤;沒有對應翻譯時後台會回退為英文。\"],\"/zOUxl\":[\"QR code linking to the Telegram bot\"],\"0OGSSc\":[\"頭像顯示已更新.\"],\"0UzCUX\":[\"更新您用來登入的密碼\"],\"0bdA9b\":[\"Open Telegram to connect\"],\"10UtuM\":[\"CJK 字體\"],\"14BEca\":[\"瞭解原因\"],\"1F6Mzc\":[\"目前尚無導覽項目。請新增連結或在下方啟用系統項目。\"],\"1mbBbL\":[\"Connect manually\"],\"1njn7W\":[\"淺色\"],\"2B7t+s\":[\"工作階段與密碼\"],\"2DoBvq\":[\"訂閱來源\"],\"2FYpfJ\":[\"更多\"],\"2Ithfh\":[\"Message the bot any text and it's published as a note.\"],\"2MXb5X\":[\"關於靜謐設計的實地筆記\"],\"2PTjMB\":[\"我想刪除 \",[\"siteName\"]],\"2cFU6q\":[\"網站頁尾\"],\"2oWZo7\":[\"最近一次提交\"],\"2uuy4H\":[\"已透過個人存取權杖連線\"],\"35x8eZ\":[\"顯示 \",[\"shown\"],\" 共 \",[\"total\"]],\"39QGku\":[\"Open the bot and send the binding code, then anything you message it becomes a note.\"],\"3Cw1AI\":[\"新增選集\"],\"3VrybB\":[\"重新導向\"],\"3Yvsaz\":[\"302 (暫時)\"],\"3n0zbB\":[\"在示範模式中已停用工作階段管理。請改用共用示範工作階段。\"],\"3sYJi5\":[\"下載與 Hugo 相容的封存 — 以靜態方式託管或移轉到另一個 Jant\"],\"3wKq0C\":[\"無法儲存。請稍後再試。\"],\"49Bsal\":[\"Feed 設定已更新。\"],\"4Jge8E\":[\"目前的工作階段\"],\"4KIa+q\":[\"已下載匯出檔案。\"],\"4cEClj\":[\"工作階段\"],\"4zGJ5E\":[\"永久刪除帳戶\"],\"5QlUIt\":[\"倉庫為空。準備連線。\"],\"5VQnR3\":[\"當你想要一個永遠不會改變的 feed URL 時,請使用這些。\"],\"5dpcN1\":[\"輸入以搜尋全部\"],\"5f1Wo9\":[\"Connected as \",[\"account\"]],\"69OXZB\":[\"刪除託管網站\"],\"6ArdBh\":[\"將精選文章用於 /feed.\"],\"6DjeBT\":[\"示範網站會始終對搜尋引擎保持隱藏。\"],\"6FFB7q\":[\"使用最新的公開貼文作為 /feed。\"],\"6K1Vef\":[\"確定要永久刪除此部落格嗎?此動作無法復原。\"],\"6NpNLc\":[\"此儲存庫已有內容。\"],\"6V3Ea3\":[\"已複製\"],\"71WIgc\":[\"Get a new code\"],\"746NHh\":[\"此部落格\"],\"7811AW\":[\"此儲存庫已在備份本網站。\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允許搜尋引擎收錄我的網站\"],\"7GISOt\":[\"Save bot token\"],\"7MZxzw\":[\"密碼已變更.\"],\"7vhWI8\":[\"新密碼\"],\"7z05Pf\":[\"在 \",[\"providerLabel\"],\" 開啟託管站點控制項以取消計費或永久刪除此站點。\"],\"81nFIS\":[\"密碼不符。請確認兩個欄位相同。\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"該主題目前不可用。請選擇其他主題。\"],\"8BfEpW\":[\"託管帳號\"],\"8N/Mcp\":[\"封存篩選參數 (例如 format=note&view=list)\"],\"8T46pB\":[\"Bot token\"],\"8U2Z7f\":[\"新增自訂 URL\"],\"8ZsakT\":[\"密碼\"],\"9+vGLh\":[\"自訂 CSS\"],\"9As8Nu\":[\"在 GitHub 上建立一個\"],\"9Lsvt5\":[\"已於 \",[\"date\"],\" 登入\"],\"9T7Cwm\":[\"重新導向、自訂路徑與網址控制\"],\"9aUyym\":[\"查看您在哪裡已登入,並撤銷舊的工作階段\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AnY+O9\":[\"在首頁底部顯示「Build with Jant」\"],\"ApZDMk\":[\"此圖會用於您的 favicon 和 apple-touch-icon。為達最佳效果,請上傳至少 512x512 像素、背景為純色的正方形 PNG。\"],\"B495Gs\":[\"封存\"],\"B4ESok\":[\"API 參考\"],\"BzEFor\":[\"or\"],\"CDAdlf\":[\"Remove bot\"],\"CTAEes\":[\"選擇儲存庫\"],\"CjZZgz\":[\"此儲存庫已有提交紀錄\"],\"D8k2s6\":[\"Connect Telegram\"],\"DCKkhU\":[\"目前密碼\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密碼與託管存取\"],\"EO3I6h\":[\"上傳未成功. 請稍後再試.\"],\"Enslfm\":[\"目標網址\"],\"F7FKwe\":[\"你在此處貼上的任何內容都能完全存取訪客的瀏覽器。僅使用來自你信任來源的程式碼。\"],\"FkMol5\":[\"精選\"],\"G/1oP+\":[\"移除 webhook 並停止同步。您的儲存庫內容不會被刪除。\"],\"G0qJsQ\":[\"找不到安全權杖。請重新整理頁面後再試一次。\"],\"G39wnK\":[\"Back up and sync content with a GitHub repository\"],\"GMMWcy\":[\"名稱, 中繼資料, 語言, 和 搜尋預設值\"],\"GXsAby\":[\"撤銷\"],\"GxkJXS\":[\"上傳中...\"],\"GzKzUa\":[\"試用限制\"],\"HKH+W+\":[\"資料\"],\"Hp1l6f\":[\"目前\"],\"HxlY7t\":[\"變更此設定會更新訂閱者從 /feed 取得的內容。\"],\"HxuOlm\":[\"網站頁首\"],\"I6gXOa\":[\"路徑\"],\"ID38tA\":[\"示範模式下帳號刪除已停用。共用示範會另行重置。\"],\"IF9tPu\":[\"何時使用網站匯出、資料庫備份與復原演練。\"],\"IW5PBo\":[\"複製權杖\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"儲存庫\"],\"J6bLeg\":[\"新增自訂連結到任何 URL\"],\"JL7LF5\":[\"可用的 CSS 變數、data 屬性與範例.\"],\"JTviaO\":[\"管理登入安全、匯出,以及不可逆的操作。\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"請立即複製您的權杖 — 它不會再顯示。\"],\"JrFTcr\":[\"連線中…\"],\"JuN5GC\":[\"未選取檔案。請選擇要上傳的檔案。\"],\"KDw4GX\":[\"重試\"],\"KSgo21\":[\"選擇一個儲存庫\"],\"KVVYBh\":[\"新增選集到導覽\"],\"KiJn9B\":[\"筆記\"],\"L3DEwT\":[\"移除這個頭像?您的 favicon 與頁首圖示會回復為預設。\"],\"L4t4/q\":[\"3月14日\"],\"LdyooL\":[\"連結\"],\"M/D8PK\":[\"+ 安裝到其他帳戶\"],\"M/haSd\":[\"永遠顯示主題的淺色版本。\"],\"M2kIWU\":[\"字型主題\"],\"M6CbAU\":[\"切換編輯面板\"],\"MaYYE6\":[\"Post notes by messaging a Telegram bot\"],\"Me5t5H\":[\"連接 GitHub 倉庫,自動將你的文章備份為 Markdown 檔案。GitHub 上的編輯會同步回你的網站。\"],\"Mr4QPw\":[\"Disconnect Telegram? You can reconnect any time with a new binding code.\"],\"MtENL9\":[\"調整您的網站外觀、可讀性與執行效能。\"],\"N/8NPV\":[\"在刪除前,請先下載網站匯出檔案。刪除後無法恢復此帳號。\"],\"N7UNHY\":[\"精選 RSS 來源\"],\"NHnUHF\":[\"頁首上的網站圖示與個人標記\"],\"NU2Fqi\":[\"儲存 CSS\"],\"Nldjdr\":[\"尚未有自訂 URL。建立一個自訂 URL 以為文章新增重新導向或自訂路徑。\"],\"O7rgs6\":[\"頁首 RSS 指向你的 \",[\"feed\"],\" 訂閱 (/feed)。在「一般」中變更 /feed 回傳的內容。\"],\"OSJXFg\":[\"套用於整個網站, 包括管理頁面. 選擇一個調色盤, 然後決定它是跟隨系統還是維持固定.\"],\"Ox3+3h\":[\"無相符結果。\"],\"PEUV5I\":[\"程式碼注入已更新。\"],\"PXj9lw\":[\"Stop accepting posts from Telegram. Your existing notes stay published.\"],\"PZ7HJ8\":[\"部落格大頭貼\"],\"Pwqkdw\":[\"載入中…\"],\"PxJ9W6\":[\"產生權杖\"],\"Q/6Y+2\":[\"需要在目標儲存庫上擁有 Contents(讀/寫)和 Webhooks(讀/寫)權限。\"],\"Q30z/l\":[\"要從導覽移除這個選集嗎?選集本身不會被刪除。\"],\"Q99OtV\":[\"將選集釘選到導覽列。最近 48 小時內更新的選集旁會顯示一個 * 號。\"],\"QZmz0H\":[\"內建連結\"],\"Qnrzvb\":[\"已啟用的權杖\"],\"R6Z4LE\":[\"下載失敗。請再試一次。\"],\"R9Khdg\":[\"自動\"],\"RcdDOS\":[\"Create a bot by messaging @BotFather on Telegram, then paste the token it gives you.\"],\"RxsRD6\":[\"時區\"],\"SJmfuf\":[\"網站名稱\"],\"SKZhW9\":[\"權杖名稱 (API 權杖名稱欄位。)\"],\"SVQQPe\":[\"無法連線。請檢查錯誤並再試一次。\"],\"SchpMp\":[\"Telegram\"],\"TpF3v+\":[\"在 </head> 之前注入。用於分析、自訂 meta 標籤,以及必須提前載入的樣式。\"],\"Tz0i8g\":[\"設定\"],\"UFK415\":[\"用於分析與小工具的網站全域 HTML\"],\"UTvFQq\":[\"Open \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" and send:\"],\"Uj/btJ\":[\"在我的網站頁首顯示大頭貼\"],\"UsODUn\":[\"選擇一個帳戶\"],\"UxKoFf\":[\"導覽\"],\"V+bhUy\":[\"安裝 GitHub App\"],\"V4WsyL\":[\"新增連結\"],\"V5pZwT\":[\"搜尋設定已更新。\"],\"VXUPla\":[\"使用 GitHub App 連線\"],\"VhMDMg\":[\"變更密碼\"],\"Vn3jYy\":[\"導覽項目\"],\"VoZYGU\":[\"這會永久刪除您所有的資料 — 文章、媒體、選集、設定,以及您的帳戶。您的部落格將被重設為初始設定狀態。此操作無法復原。\"],\"Weq9zb\":[\"一般\"],\"Wi9i06\":[\"依照每位訪客的系統偏好。\"],\"Wx1M8N\":[\"安裝 GitHub App,以授予存取權而無需管理個人權杖。權限以每個儲存庫為範圍,並可在 GitHub 上撤銷。\"],\"X+8FMk\":[\"目前的密碼不符。請再試一次。\"],\"X1G9eY\":[\"導覽預覽\"],\"X9Hujr\":[\"手動推送\"],\"XtBJV8\":[\"正在檢查儲存庫…\"],\"Xtc16w\":[\"重新整理儲存庫清單\"],\"Y/F35r\":[\"使用 curl 建立貼文:\"],\"YF6zHf\":[\"網站設定已更新.\"],\"YdG2RF\":[\"匯出網站\"],\"YkgZi7\":[\"Connect a Telegram bot, then anything you message it gets published as a note.\"],\"YwhjRx\":[\"管理帳戶\"],\"ZDY7Fy\":[\"同步中…\"],\"ZQKLI1\":[\"危險區域\"],\"ZS/CBL\":[\"刪除此導覽連結?訪客將不再在您的網站頁首看到它。\"],\"ZhhOwV\":[\"引用 (Jant 的貼文格式之一。)\"],\"ZiooJI\":[\"API 權杖\"],\"Zm7Qb0\":[\"備份與還原指南\"],\"ZmUkwN\":[\"新增自訂連結到導覽\"],\"a14mj8\":[\"未知裝置\"],\"a3LDKx\":[\"安全性\"],\"aAIQg2\":[\"外觀\"],\"aFkzVF\":[\"目標文章或選集的 slug\"],\"alKG0+\":[\"字型主題\"],\"anibOb\":[\"關於本部落格\"],\"any7NR\":[\"主題指南\"],\"b+/jO6\":[\"301 (永久)\"],\"bHOiy1\":[\"示範模式已停用變更密碼功能。請使用共用示範帳號登入。\"],\"bHYIks\":[\"登出\"],\"bmrL08\":[\"示範模式會隱藏會話、密碼更改與帳號刪除。匯出功能仍可使用。\"],\"c3MN2z\":[\"所有可用的端點與請求格式。\"],\"cS7/bk\":[\"Remove the saved bot token? Its webhook is deleted and any connected account is disconnected.\"],\"cSDy01\":[\"自訂 CSS 已更新.\"],\"clzoNp\":[\"始終顯示深色主題。\"],\"cnGeoo\":[\"刪除\"],\"d3FRkY\":[\"無法複製. 請再試一次.\"],\"d5oGUo\":[\"在 GitHub 建立新儲存庫\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"確認刪除帳號\"],\"dYKrp3\":[\"從最新中隱藏 (不出現在 Latest 中,但仍可透過固定連結和集合存取的狀態。)\"],\"dk7TCH\":[\"永久刪除所有資料並重設部落格\"],\"drodVV\":[\"目前還沒有選集。請先建立一個,然後將它加入您的導覽。\"],\"dsWkIw\":[\"要與 GitHub 斷開連線嗎? webhook 將會被移除。您的儲存庫內容不會被刪除。\"],\"e/tSI5\":[\"導覽順序已更新。\"],\"ePK91l\":[\"編輯\"],\"ebQKK7\":[\"網站\"],\"egK+Yy\":[\"供腳本與自動化使用的 Bearer 權杖\"],\"ehj/zN\":[\"重新導向類型\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"已複製訂閱網址。\"],\"f8fH8W\":[\"設計\"],\"fWYqkz\":[\"程式碼注入\"],\"gOWiTY\":[\"載入為中文、日文或韓文內容最佳化的襯線字型。\"],\"gZ5owP\":[\"搜尋儲存庫\"],\"gbqbh6\":[\"可以放心離開此頁面 — 同步會在背景繼續進行。\"],\"gkFvVN\":[\"在 </body> 之前注入。用於聊天小工具和不應阻塞頁面載入的腳本。\"],\"gtQsRO\":[\"建立自訂網址\"],\"hBO/y4\":[\"安全權杖已過期。請重新整理頁面並再試一次。\"],\"hGmyDl\":[\"權杖讓您從腳本, 捷徑和其他工具存取 API 無需登入\"],\"hIHkRy\":[\"已透過 GitHub 應用程式連線\"],\"hdSi1b\":[\"輸入 \",[\"repo\"],\" 以確認\"],\"he3ygx\":[\"複製\"],\"i0qMbr\":[\"首頁\"],\"iEUzMn\":[\"系統\"],\"iSLIjg\":[\"連接\"],\"iVOMRi\":[\"首頁設定已更新。\"],\"icB4Cv\":[\"將連結拖到此處以顯示於「更多」選單下方\"],\"id3vuh\":[\"Telegram is set up, but the bot couldn't be reached. Check the bot token and try again.\"],\"ihn4zD\":[\"搜尋…\"],\"iiDXZc\":[\"顯示於所有文章與頁面的底部。\"],\"j4VrG6\":[\"下載匯出 ZIP\"],\"j5nQL2\":[\"例如 iOS 捷徑\"],\"jUV7CU\":[\"上傳大頭貼\"],\"jVUmOK\":[\"支援 Markdown\"],\"jgBjXJ\":[\"要撤銷這個權杖嗎?任何使用它的腳本都會停止運作。\"],\"jpctdh\":[\"檢視\"],\"k1ifdL\":[\"處理中...\"],\"kMXclu\":[\"下載網站匯出檔案\"],\"kNiQp6\":[\"已釘選\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"細緻的樣式覆寫\"],\"ke1gWS\":[\"自訂 URL\"],\"kfcRb0\":[\"頭像\"],\"kxDZ2i\":[\"此程式碼會在您網站的每個頁面上執行。\"],\"l2Op2p\":[\"查詢參數\"],\"lLW3vJ\":[\"目標 slug\"],\"lYHJih\":[\"撤銷此工作階段?該裝置將需要重新登入。\"],\"mLOk1i\":[\"立即將所有文章推送到 GitHub,而不是等待下一次自動同步。\"],\"mSNmrX\":[\"列出貼文:\"],\"nK07ni\":[\"為您的網站選擇一種排版風格。每個主題會同時改變字體配對與閱讀節奏。\"],\"nbfdhU\":[\"Integrations\"],\"o/vNDE\":[\"讓您覆寫任何主題變數。\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"我們會預先填入名稱 \",[\"name\"],\"。返回時清單會重新整理。\"],\"oKOOsY\":[\"色彩主題\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有選集已經在您的導覽中。\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pgTIrt\":[\"選擇要與此網站同步的 GitHub 帳號和儲存庫.\"],\"psoxDF\":[\"該字型主題無法使用. 請選擇其他主題.\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"選集\"],\"qdcESc\":[\"建立新儲存庫\"],\"r5EW6f\":[\"此儲存庫已在備份另一個 Jant 網站 (\",[\"host\"],\"). 請選擇其他儲存庫.\"],\"rEspiY\":[\"導覽位置已更新。\"],\"rFmBG3\":[\"色彩主題\"],\"rlonmB\":[\"無法刪除。請稍後再試。\"],\"satWc6\":[\"主要 RSS 訂閱\"],\"sgr2wQ\":[\"選集\"],\"sqxcaY\":[\"建立於 \",[\"date\"]],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tJ4H0O\":[\"your Telegram account\"],\"tfDRzk\":[\"儲存\"],\"tvgAq5\":[\"尚未授權任何帳戶\"],\"u1VTd3\":[\"調色盤、表面色調與整體氛圍\"],\"u3wRF+\":[\"已發佈\"],\"u6KOjV\":[\"想要更細緻的控制?\"],\"udPwLB\":[\"頁首\"],\"ui6aMF\":[\"這些裝置目前已登入您的帳號。撤銷任何您不認識的工作階段。\"],\"vBEKwo\":[\"在此管理本網站的活動工作階段。密碼與託管存取由 \",[\"providerLabel\"],\" 管理。\"],\"vRldcl\":[\"字體選擇與閱讀質感\"],\"vSYKYI\":[\"主要訂閱來源\"],\"vTuib7\":[\"這會控制 /feed 回傳的內容。\"],\"vXIe7J\":[\"語言\"],\"vmQmHx\":[\"新增自訂 CSS 以覆寫任何樣式。使用像 [data-page]、[data-post]、[data-format] 這類資料屬性來選取特定元素。\"],\"vzX5FB\":[\"刪除帳號\"],\"w8Rv8T\":[\"標籤為必填\"],\"wL3cK8\":[\"最新\"],\"wPmHHc\":[\"低調的介面讓文字成為主角。\"],\"wW6NCp\":[\"上次錯誤\"],\"wc+17X\":[\"/* 在此放入您的自訂 CSS */\"],\"wuLtXn\":[\"目前沒有任何活動中的工作階段。已登入的裝置會顯示在此處。\"],\"xCWek4\":[\"檔案儲存尚未設定。請檢查您的伺服器設定。\"],\"xHt036\":[\"個人存取權杖\"],\"xbN8dp\":[\"柔和的色彩仍應保有清晰的閱讀節奏。\"],\"y28hnO\":[\"文章\"],\"y8Md/V\":[\"語言與時間已更新。\"],\"yNCqOt\":[\"最新 RSS 來源\"],\"yQ3kNF\":[\"請輸入以下短語以確認:\"],\"ydq1k2\":[\"請先選擇一個帳戶\"],\"yjjCV8\":[\"固定的 RSS 檔案網址\"],\"yjkELF\":[\"確認新密碼\"],\"yzF66j\":[\"連結 (Jant 的貼文格式之一。)\"],\"z6wakA\":[\"顯示在您的主頁上的簡短介紹。\"],\"zEizrk\":[\"最後使用於 \",[\"date\"]],\"zSURJW\":[\"沒有符合的儲存庫.\"],\"zXH2jX\":[\"語言與時區\"],\"zlcDd2\":[\"刪除此自訂 URL?使用該 URL 的訪客將不會再被重新導向。\"],\"zwBp5t\":[\"私有\"],\"zxRN6H\":[\"頁首連結、首頁動態與更多選單\"]}");
|
|
1925
1926
|
//#endregion
|
|
1926
1927
|
//#region src/i18n/i18n.ts
|
|
1927
1928
|
/**
|
|
@@ -3418,10 +3419,10 @@ function normalizeThemeColorForMeta(color) {
|
|
|
3418
3419
|
* internal paths (e.g. `/_assets/client-HASH.js`) embedded by the Worker build
|
|
3419
3420
|
* from the Vite client manifest. Used only in production (IS_VITE_DEV=false).
|
|
3420
3421
|
*/ var IS_VITE_DEV = typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
3421
|
-
var CORE_VERSION = "0.
|
|
3422
|
-
var CLIENT_JS_FILE = "/_assets/client-
|
|
3423
|
-
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-
|
|
3424
|
-
var CLIENT_CSS_FILE = "/_assets/client-
|
|
3422
|
+
var CORE_VERSION = "0.6.0-f0eaa828eead8408";
|
|
3423
|
+
var CLIENT_JS_FILE = "/_assets/client-Bo7sKkAQ.js";
|
|
3424
|
+
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-D1jDQgbH.js";
|
|
3425
|
+
var CLIENT_CSS_FILE = "/_assets/client-QHRvzZwk.css";
|
|
3425
3426
|
var CLIENT_CJK_CSS_FILE = "/_assets/client-cjk-B7Z0snDu.css";
|
|
3426
3427
|
var CLIENT_CJK_TC_CSS_FILE = "/_assets/client-cjk-tc-BesJYrb2.css";
|
|
3427
3428
|
var CLIENT_CJK_JP_CSS_FILE = "/_assets/client-cjk-jp-DZwrTzQC.css";
|
|
@@ -3739,7 +3740,7 @@ var IconSprite = () => {
|
|
|
3739
3740
|
const cjkSerifFont = appConfig?.cjkSerifFont ?? "off";
|
|
3740
3741
|
const cjkStylesheetPath = cjkSerifFont === "zh-Hans" ? IS_VITE_DEV ? assetPath("/src/style-cjk.css") : toPublicAssetPath(CLIENT_CJK_CSS_FILE, assetBasePath) : cjkSerifFont === "zh-Hant" ? IS_VITE_DEV ? assetPath("/src/style-cjk-tc.css") : toPublicAssetPath(CLIENT_CJK_TC_CSS_FILE, assetBasePath) : cjkSerifFont === "ja" ? IS_VITE_DEV ? assetPath("/src/style-cjk-jp.css") : toPublicAssetPath(CLIENT_CJK_JP_CSS_FILE, assetBasePath) : cjkSerifFont === "ko" ? IS_VITE_DEV ? assetPath("/src/style-cjk-kr.css") : toPublicAssetPath(CLIENT_CJK_KR_CSS_FILE, assetBasePath) : null;
|
|
3741
3742
|
const clientScriptPath = IS_VITE_DEV ? resolvedClientBundle === "full" ? assetPath("/src/client-auth.ts") : assetPath("/src/client.ts") : toPublicAssetPath(resolvedClientBundle === "full" ? CLIENT_AUTH_JS_FILE : CLIENT_JS_FILE, assetBasePath);
|
|
3742
|
-
const faviconAssetVersion = resolvedFaviconVersion || "0.
|
|
3743
|
+
const faviconAssetVersion = resolvedFaviconVersion || "0.6.0-f0eaa828eead8408";
|
|
3743
3744
|
const resolvedFaviconHref = faviconHref ?? (faviconAssetVersion ? toPublicPath(`/favicon.ico?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/favicon.ico", sitePathPrefix));
|
|
3744
3745
|
const resolvedAppleTouchHref = appleTouchHref ?? (faviconAssetVersion ? toPublicPath(`/apple-touch-icon.png?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/apple-touch-icon.png", sitePathPrefix));
|
|
3745
3746
|
const socialImageHref = resolvedSocialImagePath && (isFullUrl(resolvedSocialImagePath) || resolvedSocialImagePath.startsWith("//") ? resolvedSocialImagePath : toAbsoluteSiteUrl(resolvedSocialImagePath, appConfig?.siteUrl || "", sitePathPrefix));
|
|
@@ -4529,6 +4530,36 @@ var STORAGE_DRIVERS = [
|
|
|
4529
4530
|
defaultValue: "",
|
|
4530
4531
|
envOnly: true,
|
|
4531
4532
|
envKeys: ["GITHUB_APP_WEBHOOK_SECRET"]
|
|
4533
|
+
},
|
|
4534
|
+
TELEGRAM_BOT_TOKENS: {
|
|
4535
|
+
defaultValue: "",
|
|
4536
|
+
envOnly: true,
|
|
4537
|
+
envKeys: ["TELEGRAM_BOT_TOKENS"]
|
|
4538
|
+
},
|
|
4539
|
+
TELEGRAM_WEBHOOK_SECRET: {
|
|
4540
|
+
defaultValue: "",
|
|
4541
|
+
envOnly: true,
|
|
4542
|
+
envKeys: ["TELEGRAM_WEBHOOK_SECRET"]
|
|
4543
|
+
},
|
|
4544
|
+
TELEGRAM_BOT_TOKEN: {
|
|
4545
|
+
defaultValue: "",
|
|
4546
|
+
envOnly: false,
|
|
4547
|
+
internal: true
|
|
4548
|
+
},
|
|
4549
|
+
TELEGRAM_BOT_ID: {
|
|
4550
|
+
defaultValue: "",
|
|
4551
|
+
envOnly: false,
|
|
4552
|
+
internal: true
|
|
4553
|
+
},
|
|
4554
|
+
TELEGRAM_BOT_USERNAME: {
|
|
4555
|
+
defaultValue: "",
|
|
4556
|
+
envOnly: false,
|
|
4557
|
+
internal: true
|
|
4558
|
+
},
|
|
4559
|
+
TELEGRAM_BOT_WEBHOOK_SECRET: {
|
|
4560
|
+
defaultValue: "",
|
|
4561
|
+
envOnly: false,
|
|
4562
|
+
internal: true
|
|
4532
4563
|
}
|
|
4533
4564
|
};
|
|
4534
4565
|
var THEME_MODES = [
|
|
@@ -4552,7 +4583,10 @@ var ID_PREFIX = {
|
|
|
4552
4583
|
user: "usr",
|
|
4553
4584
|
session: "ses",
|
|
4554
4585
|
account: "acc",
|
|
4555
|
-
verification: "vrf"
|
|
4586
|
+
verification: "vrf",
|
|
4587
|
+
telegramBinding: "tgb",
|
|
4588
|
+
telegramBindingCode: "tgc",
|
|
4589
|
+
telegramMediaGroupItem: "tmg"
|
|
4556
4590
|
};
|
|
4557
4591
|
var AUTH_ID_PREFIX = {
|
|
4558
4592
|
user: ID_PREFIX.user,
|
|
@@ -9930,7 +9964,7 @@ function getThreadPreviewState({ secondReply, penultimateReply, latestReply, tot
|
|
|
9930
9964
|
/**
|
|
9931
9965
|
* Thread Preview
|
|
9932
9966
|
*
|
|
9933
|
-
* Shows latest reply as the hero post with ancestor context above.
|
|
9967
|
+
* Shows latest reply as the hero post with collapsible faded ancestor context above.
|
|
9934
9968
|
* Thread line connects all posts via `.thread-group` / `.thread-item`.
|
|
9935
9969
|
*/ var ROOT_CONTEXT_DISPLAY = { footer: { hideReply: true } };
|
|
9936
9970
|
var CONTEXT_DISPLAY = {
|
|
@@ -9947,43 +9981,84 @@ var ThreadPreview = ({ rootPost, secondReply, penultimateReply, latestReply, tot
|
|
|
9947
9981
|
totalReplyCount
|
|
9948
9982
|
});
|
|
9949
9983
|
const hiddenPostsLabel = i18n._({ id: "oO0hKx" }, { count: hiddenCount });
|
|
9984
|
+
const showMoreLabel = i18n._({ id: "fMPkxb" });
|
|
9985
|
+
const showLessLabel = i18n._({ id: "6lGV3K" });
|
|
9950
9986
|
const renderedSecondReply = secondReply && secondReply.id !== latestReply.id ? secondReply : void 0;
|
|
9951
9987
|
const renderedPenultimateReply = penultimateReply && penultimateReply.id !== latestReply.id && penultimateReply.id !== secondReply?.id ? penultimateReply : void 0;
|
|
9952
9988
|
const gapHref = renderedSecondReply?.permalink ?? latestReply.permalink;
|
|
9989
|
+
const rootItem = /* @__PURE__ */ jsxDEV$1("div", {
|
|
9990
|
+
class: "thread-item thread-item-context",
|
|
9991
|
+
children: /* @__PURE__ */ jsxDEV$1(TimelineItemFromPost, {
|
|
9992
|
+
post: rootPost,
|
|
9993
|
+
mode: "feed",
|
|
9994
|
+
display: ROOT_CONTEXT_DISPLAY
|
|
9995
|
+
})
|
|
9996
|
+
});
|
|
9997
|
+
const secondReplyItem = renderedSecondReply ? /* @__PURE__ */ jsxDEV$1("div", {
|
|
9998
|
+
class: "thread-item thread-item-context",
|
|
9999
|
+
children: /* @__PURE__ */ jsxDEV$1(TimelineItemFromPost, {
|
|
10000
|
+
post: renderedSecondReply,
|
|
10001
|
+
mode: "feed",
|
|
10002
|
+
display: CONTEXT_DISPLAY
|
|
10003
|
+
})
|
|
10004
|
+
}) : null;
|
|
10005
|
+
const gapItem = hiddenCount > 0 ? /* @__PURE__ */ jsxDEV$1("div", {
|
|
10006
|
+
class: "thread-item thread-item-gap",
|
|
10007
|
+
children: /* @__PURE__ */ jsxDEV$1("a", {
|
|
10008
|
+
href: gapHref,
|
|
10009
|
+
class: "thread-gap-link",
|
|
10010
|
+
children: hiddenPostsLabel
|
|
10011
|
+
})
|
|
10012
|
+
}) : null;
|
|
10013
|
+
const penultimateItem = renderedPenultimateReply ? /* @__PURE__ */ jsxDEV$1("div", {
|
|
10014
|
+
class: "thread-item thread-item-context",
|
|
10015
|
+
children: /* @__PURE__ */ jsxDEV$1(TimelineItemFromPost, {
|
|
10016
|
+
post: renderedPenultimateReply,
|
|
10017
|
+
mode: "feed",
|
|
10018
|
+
display: CONTEXT_DISPLAY
|
|
10019
|
+
})
|
|
10020
|
+
}) : null;
|
|
9953
10021
|
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
9954
10022
|
class: "thread-group thread-group-preview",
|
|
9955
10023
|
children: [
|
|
9956
10024
|
/* @__PURE__ */ jsxDEV$1("div", {
|
|
9957
|
-
class: "thread-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
})
|
|
9971
|
-
}),
|
|
9972
|
-
hiddenCount > 0 && /* @__PURE__ */ jsxDEV$1("div", {
|
|
9973
|
-
class: "thread-item thread-item-gap",
|
|
9974
|
-
children: /* @__PURE__ */ jsxDEV$1("a", {
|
|
9975
|
-
href: gapHref,
|
|
9976
|
-
class: "thread-gap-link",
|
|
9977
|
-
children: hiddenPostsLabel
|
|
9978
|
-
})
|
|
10025
|
+
class: "thread-context-shell",
|
|
10026
|
+
"data-thread-context": true,
|
|
10027
|
+
"data-collapsed": "",
|
|
10028
|
+
children: [
|
|
10029
|
+
rootItem,
|
|
10030
|
+
secondReplyItem,
|
|
10031
|
+
gapItem,
|
|
10032
|
+
penultimateItem,
|
|
10033
|
+
/* @__PURE__ */ jsxDEV$1("div", {
|
|
10034
|
+
class: "thread-context-fade",
|
|
10035
|
+
"aria-hidden": "true"
|
|
10036
|
+
})
|
|
10037
|
+
]
|
|
9979
10038
|
}),
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
10039
|
+
/* @__PURE__ */ jsxDEV$1("button", {
|
|
10040
|
+
type: "button",
|
|
10041
|
+
class: "thread-context-toggle",
|
|
10042
|
+
"data-thread-context-toggle": true,
|
|
10043
|
+
"data-label-more": showMoreLabel,
|
|
10044
|
+
"data-label-less": showLessLabel,
|
|
10045
|
+
"aria-expanded": "false",
|
|
10046
|
+
children: [/* @__PURE__ */ jsxDEV$1("span", {
|
|
10047
|
+
class: "thread-context-toggle-label",
|
|
10048
|
+
children: showMoreLabel
|
|
10049
|
+
}), /* @__PURE__ */ jsxDEV$1("svg", {
|
|
10050
|
+
class: "thread-context-toggle-chevron",
|
|
10051
|
+
viewBox: "0 0 16 16",
|
|
10052
|
+
"aria-hidden": "true",
|
|
10053
|
+
children: /* @__PURE__ */ jsxDEV$1("path", {
|
|
10054
|
+
d: "M4 6l4 4 4-4",
|
|
10055
|
+
fill: "none",
|
|
10056
|
+
stroke: "currentColor",
|
|
10057
|
+
"stroke-width": "1.5",
|
|
10058
|
+
"stroke-linecap": "round",
|
|
10059
|
+
"stroke-linejoin": "round"
|
|
10060
|
+
})
|
|
10061
|
+
})]
|
|
9987
10062
|
}),
|
|
9988
10063
|
/* @__PURE__ */ jsxDEV$1("div", {
|
|
9989
10064
|
class: "thread-item thread-item-hero",
|
|
@@ -10321,24 +10396,65 @@ homeRoutes.get("/", async (c) => {
|
|
|
10321
10396
|
*
|
|
10322
10397
|
* Single post view — clean, no card border, with divider footer.
|
|
10323
10398
|
* When `threadPosts` is provided, renders the full thread with the current
|
|
10324
|
-
* post highlighted and scroll-targeted.
|
|
10325
|
-
|
|
10399
|
+
* post highlighted and scroll-targeted. Ancestors above the current post are
|
|
10400
|
+
* wrapped in the same collapsible shell used on the home feed
|
|
10401
|
+
* (`.thread-context-shell`), driven by the shared `thread-context.ts`
|
|
10402
|
+
* client logic.
|
|
10403
|
+
*/ var renderThreadItem = (tp, currentId) => {
|
|
10404
|
+
const isCurrent = tp.id === currentId;
|
|
10405
|
+
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
10406
|
+
id: `post-${tp.id}`,
|
|
10407
|
+
class: `thread-item thread-detail-item${isCurrent ? " thread-item-current" : ""}`,
|
|
10408
|
+
...isCurrent ? { "data-post-current": "" } : {},
|
|
10409
|
+
children: /* @__PURE__ */ jsxDEV$1(TimelineItemFromPost, {
|
|
10410
|
+
post: tp,
|
|
10411
|
+
mode: "detail",
|
|
10412
|
+
display: { footer: { hideTimestamp: false } }
|
|
10413
|
+
})
|
|
10414
|
+
}, tp.id);
|
|
10415
|
+
};
|
|
10416
|
+
var ThreadDetail = ({ post, threadPosts }) => {
|
|
10417
|
+
const { i18n } = useLingui();
|
|
10418
|
+
const showMoreLabel = i18n._({ id: "fMPkxb" });
|
|
10419
|
+
const showLessLabel = i18n._({ id: "6lGV3K" });
|
|
10420
|
+
const currentIndex = threadPosts.findIndex((tp) => tp.id === post.id);
|
|
10421
|
+
const ancestors = currentIndex > 0 ? threadPosts.slice(0, currentIndex) : [];
|
|
10422
|
+
const currentAndAfter = currentIndex >= 0 ? threadPosts.slice(currentIndex) : threadPosts;
|
|
10326
10423
|
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
10327
10424
|
class: "thread-group thread-group-detail",
|
|
10328
10425
|
"data-page": "post",
|
|
10329
|
-
children:
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10426
|
+
children: [ancestors.length > 0 && /* @__PURE__ */ jsxDEV$1(Fragment$1, { children: [/* @__PURE__ */ jsxDEV$1("div", {
|
|
10427
|
+
class: "thread-context-shell",
|
|
10428
|
+
"data-thread-context": true,
|
|
10429
|
+
"data-collapsed": "",
|
|
10430
|
+
children: [ancestors.map((tp) => renderThreadItem(tp, post.id)), /* @__PURE__ */ jsxDEV$1("div", {
|
|
10431
|
+
class: "thread-context-fade",
|
|
10432
|
+
"aria-hidden": "true"
|
|
10433
|
+
})]
|
|
10434
|
+
}), /* @__PURE__ */ jsxDEV$1("button", {
|
|
10435
|
+
type: "button",
|
|
10436
|
+
class: "thread-context-toggle",
|
|
10437
|
+
"data-thread-context-toggle": true,
|
|
10438
|
+
"data-label-more": showMoreLabel,
|
|
10439
|
+
"data-label-less": showLessLabel,
|
|
10440
|
+
"aria-expanded": "false",
|
|
10441
|
+
children: [/* @__PURE__ */ jsxDEV$1("span", {
|
|
10442
|
+
class: "thread-context-toggle-label",
|
|
10443
|
+
children: showMoreLabel
|
|
10444
|
+
}), /* @__PURE__ */ jsxDEV$1("svg", {
|
|
10445
|
+
class: "thread-context-toggle-chevron",
|
|
10446
|
+
viewBox: "0 0 16 16",
|
|
10447
|
+
"aria-hidden": "true",
|
|
10448
|
+
children: /* @__PURE__ */ jsxDEV$1("path", {
|
|
10449
|
+
d: "M4 6l4 4 4-4",
|
|
10450
|
+
fill: "none",
|
|
10451
|
+
stroke: "currentColor",
|
|
10452
|
+
"stroke-width": "1.5",
|
|
10453
|
+
"stroke-linecap": "round",
|
|
10454
|
+
"stroke-linejoin": "round"
|
|
10339
10455
|
})
|
|
10340
|
-
}
|
|
10341
|
-
})
|
|
10456
|
+
})]
|
|
10457
|
+
})] }), currentAndAfter.map((tp) => renderThreadItem(tp, post.id))]
|
|
10342
10458
|
});
|
|
10343
10459
|
};
|
|
10344
10460
|
var PostPage = ({ post, threadPosts }) => {
|
|
@@ -10511,6 +10627,9 @@ function buildPostMeta(post, siteName) {
|
|
|
10511
10627
|
siteDomains: () => siteDomains$1,
|
|
10512
10628
|
siteMembers: () => siteMembers$1,
|
|
10513
10629
|
sites: () => sites$1,
|
|
10630
|
+
telegramBindings: () => telegramBindings$1,
|
|
10631
|
+
telegramMediaGroupItems: () => telegramMediaGroupItems$1,
|
|
10632
|
+
telegramPendingBindings: () => telegramPendingBindings$1,
|
|
10514
10633
|
uploadSessions: () => uploadSessions$1,
|
|
10515
10634
|
user: () => user$1,
|
|
10516
10635
|
verification: () => verification$1
|
|
@@ -10934,6 +11053,50 @@ var githubAppInstallation$1 = sqliteTable("github_app_installation", {
|
|
|
10934
11053
|
index("github_app_installation_by_site").on(table.siteId),
|
|
10935
11054
|
check("chk_github_app_installation_account_type", sql`${table.accountType} IN (${sqlTextEnum$1(GITHUB_APP_ACCOUNT_TYPES$1)})`)
|
|
10936
11055
|
]);
|
|
11056
|
+
var telegramPendingBindings$1 = sqliteTable("telegram_pending_binding", {
|
|
11057
|
+
id: text("id").primaryKey(),
|
|
11058
|
+
siteId: text("site_id").notNull().references(() => sites$1.id, { onDelete: "cascade" }),
|
|
11059
|
+
code: text("code").notNull(),
|
|
11060
|
+
createdAt: integer("created_at").notNull(),
|
|
11061
|
+
expiresAt: integer("expires_at").notNull()
|
|
11062
|
+
}, (table) => [uniqueIndex("uq_telegram_pending_binding_site_id").on(table.siteId), uniqueIndex("uq_telegram_pending_binding_code").on(table.code)]);
|
|
11063
|
+
var telegramBindings$1 = sqliteTable("telegram_binding", {
|
|
11064
|
+
id: text("id").primaryKey(),
|
|
11065
|
+
siteId: text("site_id").notNull().references(() => sites$1.id, { onDelete: "cascade" }),
|
|
11066
|
+
botId: text("bot_id").notNull(),
|
|
11067
|
+
telegramUserId: text("telegram_user_id").notNull(),
|
|
11068
|
+
telegramUsername: text("telegram_username"),
|
|
11069
|
+
lastUpdateId: integer("last_update_id"),
|
|
11070
|
+
boundAt: integer("bound_at").notNull()
|
|
11071
|
+
}, (table) => [uniqueIndex("uq_telegram_binding_site_id").on(table.siteId), uniqueIndex("uq_telegram_binding_bot_user").on(table.botId, table.telegramUserId)]);
|
|
11072
|
+
/**
|
|
11073
|
+
* Short-lived buffer for Telegram album (media_group_id) messages.
|
|
11074
|
+
*
|
|
11075
|
+
* Telegram splits an album into one webhook update per item, sharing a
|
|
11076
|
+
* `media_group_id`. The webhook handler inserts one row per item, sleeps a
|
|
11077
|
+
* short window for the rest of the group to arrive, then atomically claims
|
|
11078
|
+
* the whole group with `DELETE ... RETURNING` to publish a single post with
|
|
11079
|
+
* all attachments. Rows are deleted on flush, so the table stays small.
|
|
11080
|
+
*/ var telegramMediaGroupItems$1 = sqliteTable("telegram_media_group_item", {
|
|
11081
|
+
id: text("id").primaryKey(),
|
|
11082
|
+
siteId: text("site_id").notNull().references(() => sites$1.id, { onDelete: "cascade" }),
|
|
11083
|
+
botId: text("bot_id").notNull(),
|
|
11084
|
+
telegramUserId: text("telegram_user_id").notNull(),
|
|
11085
|
+
mediaGroupId: text("media_group_id").notNull(),
|
|
11086
|
+
chatId: integer("chat_id").notNull(),
|
|
11087
|
+
messageId: integer("message_id").notNull(),
|
|
11088
|
+
updateId: integer("update_id").notNull(),
|
|
11089
|
+
fileId: text("file_id").notNull(),
|
|
11090
|
+
mediaKind: text("media_kind").notNull(),
|
|
11091
|
+
mimeType: text("mime_type"),
|
|
11092
|
+
originalName: text("original_name"),
|
|
11093
|
+
captionMarkdown: text("caption_markdown"),
|
|
11094
|
+
width: integer("width"),
|
|
11095
|
+
height: integer("height"),
|
|
11096
|
+
durationSeconds: integer("duration_seconds"),
|
|
11097
|
+
posterFileId: text("poster_file_id"),
|
|
11098
|
+
createdAt: integer("created_at").notNull()
|
|
11099
|
+
}, (table) => [index("idx_telegram_media_group_item_group").on(table.botId, table.mediaGroupId), uniqueIndex("uq_telegram_media_group_item_message").on(table.botId, table.mediaGroupId, table.messageId)]);
|
|
10937
11100
|
/**
|
|
10938
11101
|
* Per-key sliding-window rate-limit counters. Used by the Cloudflare Workers
|
|
10939
11102
|
* runtime; Node deployments keep the state in memory instead.
|
|
@@ -11057,6 +11220,9 @@ function isNodeSqliteDatabase(db) {
|
|
|
11057
11220
|
siteDomains: () => siteDomains,
|
|
11058
11221
|
siteMembers: () => siteMembers,
|
|
11059
11222
|
sites: () => sites,
|
|
11223
|
+
telegramBindings: () => telegramBindings,
|
|
11224
|
+
telegramMediaGroupItems: () => telegramMediaGroupItems,
|
|
11225
|
+
telegramPendingBindings: () => telegramPendingBindings,
|
|
11060
11226
|
uploadSessions: () => uploadSessions,
|
|
11061
11227
|
user: () => user,
|
|
11062
11228
|
verification: () => verification
|
|
@@ -11526,6 +11692,42 @@ var githubAppInstallation = pgTable("github_app_installation", {
|
|
|
11526
11692
|
index$1("github_app_installation_by_site").on(table.siteId),
|
|
11527
11693
|
check$1("chk_github_app_installation_account_type", sql`${table.accountType} IN (${sqlTextEnum(GITHUB_APP_ACCOUNT_TYPES)})`)
|
|
11528
11694
|
]);
|
|
11695
|
+
var telegramPendingBindings = pgTable("telegram_pending_binding", {
|
|
11696
|
+
id: text$1("id").primaryKey(),
|
|
11697
|
+
siteId: text$1("site_id").notNull().references(() => sites.id, { onDelete: "cascade" }),
|
|
11698
|
+
code: text$1("code").notNull(),
|
|
11699
|
+
createdAt: integer$1("created_at").notNull(),
|
|
11700
|
+
expiresAt: integer$1("expires_at").notNull()
|
|
11701
|
+
}, (table) => [uniqueIndex$1("uq_telegram_pending_binding_site_id").on(table.siteId), uniqueIndex$1("uq_telegram_pending_binding_code").on(table.code)]);
|
|
11702
|
+
var telegramBindings = pgTable("telegram_binding", {
|
|
11703
|
+
id: text$1("id").primaryKey(),
|
|
11704
|
+
siteId: text$1("site_id").notNull().references(() => sites.id, { onDelete: "cascade" }),
|
|
11705
|
+
botId: text$1("bot_id").notNull(),
|
|
11706
|
+
telegramUserId: text$1("telegram_user_id").notNull(),
|
|
11707
|
+
telegramUsername: text$1("telegram_username"),
|
|
11708
|
+
lastUpdateId: integer$1("last_update_id"),
|
|
11709
|
+
boundAt: integer$1("bound_at").notNull()
|
|
11710
|
+
}, (table) => [uniqueIndex$1("uq_telegram_binding_site_id").on(table.siteId), uniqueIndex$1("uq_telegram_binding_bot_user").on(table.botId, table.telegramUserId)]);
|
|
11711
|
+
/** Mirror of `telegram_media_group_item` in the SQLite schema. */ var telegramMediaGroupItems = pgTable("telegram_media_group_item", {
|
|
11712
|
+
id: text$1("id").primaryKey(),
|
|
11713
|
+
siteId: text$1("site_id").notNull().references(() => sites.id, { onDelete: "cascade" }),
|
|
11714
|
+
botId: text$1("bot_id").notNull(),
|
|
11715
|
+
telegramUserId: text$1("telegram_user_id").notNull(),
|
|
11716
|
+
mediaGroupId: text$1("media_group_id").notNull(),
|
|
11717
|
+
chatId: integer$1("chat_id").notNull(),
|
|
11718
|
+
messageId: integer$1("message_id").notNull(),
|
|
11719
|
+
updateId: integer$1("update_id").notNull(),
|
|
11720
|
+
fileId: text$1("file_id").notNull(),
|
|
11721
|
+
mediaKind: text$1("media_kind").notNull(),
|
|
11722
|
+
mimeType: text$1("mime_type"),
|
|
11723
|
+
originalName: text$1("original_name"),
|
|
11724
|
+
captionMarkdown: text$1("caption_markdown"),
|
|
11725
|
+
width: integer$1("width"),
|
|
11726
|
+
height: integer$1("height"),
|
|
11727
|
+
durationSeconds: integer$1("duration_seconds"),
|
|
11728
|
+
posterFileId: text$1("poster_file_id"),
|
|
11729
|
+
createdAt: integer$1("created_at").notNull()
|
|
11730
|
+
}, (table) => [index$1("idx_telegram_media_group_item_group").on(table.botId, table.mediaGroupId), uniqueIndex$1("uq_telegram_media_group_item_message").on(table.botId, table.mediaGroupId, table.messageId)]);
|
|
11529
11731
|
/**
|
|
11530
11732
|
* Per-key sliding-window rate-limit counters. Mirrors the SQLite `rate_limit`
|
|
11531
11733
|
* table — kept in lockstep because both dialects are production targets.
|
|
@@ -14037,7 +14239,7 @@ async function renderPostWithTextPreview(c, post, autoOpen) {
|
|
|
14037
14239
|
mediaId: autoOpen.mediaId
|
|
14038
14240
|
}).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
14039
14241
|
return renderPublicPage(c, {
|
|
14040
|
-
title: pageTitle,
|
|
14242
|
+
title: buildPageTitle(pageTitle, navData.siteName),
|
|
14041
14243
|
description: meta.description,
|
|
14042
14244
|
canonicalHref,
|
|
14043
14245
|
navData,
|
|
@@ -14090,7 +14292,7 @@ async function renderPost(c, post) {
|
|
|
14090
14292
|
const meta = buildPostMeta(post, navData.siteName);
|
|
14091
14293
|
const canonicalHref = buildPostCanonicalHref(display.postView, display.threadPostViews, c.var.appConfig.siteUrl);
|
|
14092
14294
|
return renderPublicPage(c, {
|
|
14093
|
-
title: meta.title,
|
|
14295
|
+
title: buildPageTitle(meta.title, navData.siteName),
|
|
14094
14296
|
description: meta.description,
|
|
14095
14297
|
canonicalHref,
|
|
14096
14298
|
navData,
|
|
@@ -15176,7 +15378,7 @@ function renderParagraphWithLink(before, href, label, after = "") {
|
|
|
15176
15378
|
function renderList(tag, items) {
|
|
15177
15379
|
return `<${tag}>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</${tag}>`;
|
|
15178
15380
|
}
|
|
15179
|
-
function renderBlockquote(text) {
|
|
15381
|
+
function renderBlockquote$1(text) {
|
|
15180
15382
|
return `<blockquote><p>${escapeHtml(text)}</p></blockquote>`;
|
|
15181
15383
|
}
|
|
15182
15384
|
function renderCodeBlock(code) {
|
|
@@ -15364,7 +15566,7 @@ function ThemeSamplePage({ themes, selectedTheme, currentMode, sitePathPrefix =
|
|
|
15364
15566
|
i18n._({ id: "J4tAHl" }),
|
|
15365
15567
|
i18n._({ id: "C+9df9" })
|
|
15366
15568
|
]),
|
|
15367
|
-
renderBlockquote(i18n._({ id: "OEdMhi" })),
|
|
15569
|
+
renderBlockquote$1(i18n._({ id: "OEdMhi" })),
|
|
15368
15570
|
renderHeading(3, i18n._({ id: "v3E8iS" })),
|
|
15369
15571
|
renderList("ol", [
|
|
15370
15572
|
i18n._({ id: "7DvUqV" }),
|
|
@@ -17926,7 +18128,8 @@ function SettingsDirectoryLink({ href, icon, tone = "default", name, description
|
|
|
17926
18128
|
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
|
17927
18129
|
key: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/></svg>`,
|
|
17928
18130
|
shield: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>`,
|
|
17929
|
-
gitBranch: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg
|
|
18131
|
+
gitBranch: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`,
|
|
18132
|
+
send: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/><path d="m21.854 2.147-10.94 10.939"/></svg>`
|
|
17930
18133
|
};
|
|
17931
18134
|
function SettingsRootContent({ sitePathPrefix = "", demoMode = false }) {
|
|
17932
18135
|
const { i18n } = useLingui();
|
|
@@ -17946,29 +18149,19 @@ function SettingsRootContent({ sitePathPrefix = "", demoMode = false }) {
|
|
|
17946
18149
|
}),
|
|
17947
18150
|
/* @__PURE__ */ jsxDEV$1(SettingsDirectorySection, {
|
|
17948
18151
|
title: i18n._({ id: "ebQKK7" }),
|
|
17949
|
-
children: [
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
description: i18n._({ id: "9T7Cwm" })
|
|
17963
|
-
}),
|
|
17964
|
-
/* @__PURE__ */ jsxDEV$1(SettingsDirectoryLink, {
|
|
17965
|
-
href: toPublicPath("/settings/github-sync", sitePathPrefix),
|
|
17966
|
-
icon: ICONS$1.gitBranch,
|
|
17967
|
-
tone: "subtle",
|
|
17968
|
-
name: i18n._({ id: "kRhzWq" }),
|
|
17969
|
-
description: i18n._({ id: "G39wnK" })
|
|
17970
|
-
})
|
|
17971
|
-
]
|
|
18152
|
+
children: [/* @__PURE__ */ jsxDEV$1(SettingsDirectoryLink, {
|
|
18153
|
+
href: toPublicPath("/settings/general", sitePathPrefix),
|
|
18154
|
+
icon: ICONS$1.settings,
|
|
18155
|
+
tone: "subtle",
|
|
18156
|
+
name: i18n._({ id: "Weq9zb" }),
|
|
18157
|
+
description: i18n._({ id: "GMMWcy" })
|
|
18158
|
+
}), /* @__PURE__ */ jsxDEV$1(SettingsDirectoryLink, {
|
|
18159
|
+
href: toPublicPath("/settings/custom-urls", sitePathPrefix),
|
|
18160
|
+
icon: ICONS$1.arrowRightLeft,
|
|
18161
|
+
tone: "subtle",
|
|
18162
|
+
name: i18n._({ id: "ke1gWS" }),
|
|
18163
|
+
description: i18n._({ id: "9T7Cwm" })
|
|
18164
|
+
})]
|
|
17972
18165
|
}),
|
|
17973
18166
|
/* @__PURE__ */ jsxDEV$1(SettingsDirectorySection, {
|
|
17974
18167
|
title: i18n._({ id: "aAIQg2" }),
|
|
@@ -18007,6 +18200,22 @@ function SettingsRootContent({ sitePathPrefix = "", demoMode = false }) {
|
|
|
18007
18200
|
})
|
|
18008
18201
|
]
|
|
18009
18202
|
}),
|
|
18203
|
+
/* @__PURE__ */ jsxDEV$1(SettingsDirectorySection, {
|
|
18204
|
+
title: i18n._({ id: "nbfdhU" }),
|
|
18205
|
+
children: [/* @__PURE__ */ jsxDEV$1(SettingsDirectoryLink, {
|
|
18206
|
+
href: toPublicPath("/settings/github-sync", sitePathPrefix),
|
|
18207
|
+
icon: ICONS$1.gitBranch,
|
|
18208
|
+
tone: "subtle",
|
|
18209
|
+
name: i18n._({ id: "kRhzWq" }),
|
|
18210
|
+
description: i18n._({ id: "G39wnK" })
|
|
18211
|
+
}), /* @__PURE__ */ jsxDEV$1(SettingsDirectoryLink, {
|
|
18212
|
+
href: toPublicPath("/settings/telegram", sitePathPrefix),
|
|
18213
|
+
icon: ICONS$1.send,
|
|
18214
|
+
tone: "subtle",
|
|
18215
|
+
name: i18n._({ id: "SchpMp" }),
|
|
18216
|
+
description: i18n._({ id: "MaYYE6" })
|
|
18217
|
+
})]
|
|
18218
|
+
}),
|
|
18010
18219
|
/* @__PURE__ */ jsxDEV$1(SettingsDirectorySection, {
|
|
18011
18220
|
title: i18n._({ id: "sxkWRg" }),
|
|
18012
18221
|
children: [/* @__PURE__ */ jsxDEV$1(SettingsDirectoryLink, {
|
|
@@ -19744,15 +19953,15 @@ function GitHubSyncSetupForm({ settingsBase, appConfigured }) {
|
|
|
19744
19953
|
type: "submit",
|
|
19745
19954
|
class: "btn",
|
|
19746
19955
|
"data-attr:disabled": "$_connecting",
|
|
19747
|
-
children: [/* @__PURE__ */ jsxDEV$1(Spinner, { signal: "_connecting" }), i18n._({ id: "iSLIjg" })]
|
|
19956
|
+
children: [/* @__PURE__ */ jsxDEV$1(Spinner$1, { signal: "_connecting" }), i18n._({ id: "iSLIjg" })]
|
|
19748
19957
|
})
|
|
19749
19958
|
})
|
|
19750
19959
|
]
|
|
19751
19960
|
})]
|
|
19752
19961
|
});
|
|
19753
19962
|
}
|
|
19754
|
-
/** Green circle SVG for connected status */ var STATUS_DOT = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="5" cy="5" r="5" fill="currentColor"/></svg>`;
|
|
19755
|
-
function Spinner({ signal }) {
|
|
19963
|
+
/** Green circle SVG for connected status */ var STATUS_DOT$1 = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="5" cy="5" r="5" fill="currentColor"/></svg>`;
|
|
19964
|
+
function Spinner$1({ signal }) {
|
|
19756
19965
|
return /* @__PURE__ */ jsxDEV$1("svg", {
|
|
19757
19966
|
"data-show": `$${signal}`,
|
|
19758
19967
|
style: "display:none",
|
|
@@ -19793,7 +20002,7 @@ function Spinner({ signal }) {
|
|
|
19793
20002
|
class: "flex items-center gap-2 text-sm font-medium",
|
|
19794
20003
|
children: [/* @__PURE__ */ jsxDEV$1("span", {
|
|
19795
20004
|
class: "text-green-600 dark:text-green-500",
|
|
19796
|
-
dangerouslySetInnerHTML: { __html: STATUS_DOT }
|
|
20005
|
+
dangerouslySetInnerHTML: { __html: STATUS_DOT$1 }
|
|
19797
20006
|
}), status.authMode === "app" ? i18n._({ id: "hIHkRy" }) : i18n._({ id: "2uuy4H" })]
|
|
19798
20007
|
}), /* @__PURE__ */ jsxDEV$1("div", {
|
|
19799
20008
|
class: "flex flex-col gap-1.5 text-sm",
|
|
@@ -19893,7 +20102,7 @@ function GitHubSyncConnected({ status, settingsBase, streamUrl }) {
|
|
|
19893
20102
|
"data-on:click__prevent": `@post('${settingsBase}/push')`,
|
|
19894
20103
|
"data-indicator": "_pushing",
|
|
19895
20104
|
"data-attr:disabled": "$_pushing",
|
|
19896
|
-
children: [/* @__PURE__ */ jsxDEV$1(Spinner, { signal: "_pushing" }), i18n._({ id: "t3hvHq" })]
|
|
20105
|
+
children: [/* @__PURE__ */ jsxDEV$1(Spinner$1, { signal: "_pushing" }), i18n._({ id: "t3hvHq" })]
|
|
19897
20106
|
})
|
|
19898
20107
|
})
|
|
19899
20108
|
]
|
|
@@ -19922,7 +20131,7 @@ function GitHubSyncConnected({ status, settingsBase, streamUrl }) {
|
|
|
19922
20131
|
cancelLabel,
|
|
19923
20132
|
tone: "danger"
|
|
19924
20133
|
}),
|
|
19925
|
-
children: [/* @__PURE__ */ jsxDEV$1(Spinner, { signal: "_disconnecting" }), disconnectLabel]
|
|
20134
|
+
children: [/* @__PURE__ */ jsxDEV$1(Spinner$1, { signal: "_disconnecting" }), disconnectLabel]
|
|
19926
20135
|
})
|
|
19927
20136
|
})
|
|
19928
20137
|
]
|
|
@@ -19931,137 +20140,643 @@ function GitHubSyncConnected({ status, settingsBase, streamUrl }) {
|
|
|
19931
20140
|
});
|
|
19932
20141
|
}
|
|
19933
20142
|
//#endregion
|
|
19934
|
-
//#region src/lib/
|
|
19935
|
-
|
|
19936
|
-
|
|
19937
|
-
|
|
19938
|
-
|
|
19939
|
-
|
|
20143
|
+
//#region src/lib/telegram.ts
|
|
20144
|
+
/**
|
|
20145
|
+
* Telegram Bot API client.
|
|
20146
|
+
*
|
|
20147
|
+
* Thin `fetch` wrappers around the handful of Bot API methods the Telegram
|
|
20148
|
+
* integration needs, plus helpers for working with bot tokens and deep links.
|
|
20149
|
+
* All network calls go to `https://api.telegram.org/bot<token>/<method>`.
|
|
20150
|
+
*/ var TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
20151
|
+
var TelegramApiError = class extends Error {
|
|
20152
|
+
method;
|
|
20153
|
+
description;
|
|
20154
|
+
constructor(method, description) {
|
|
20155
|
+
super(`Telegram ${method} failed: ${description}`), this.method = method, this.description = description;
|
|
20156
|
+
this.name = "TelegramApiError";
|
|
20157
|
+
}
|
|
20158
|
+
};
|
|
20159
|
+
async function callTelegram(token, method, body) {
|
|
20160
|
+
const payload = await (await fetch(`${TELEGRAM_API_BASE}/bot${token}/${method}`, {
|
|
20161
|
+
method: "POST",
|
|
20162
|
+
headers: { "content-type": "application/json" },
|
|
20163
|
+
body: JSON.stringify(body ?? {})
|
|
20164
|
+
})).json();
|
|
20165
|
+
if (!payload.ok) throw new TelegramApiError(method, payload.description ?? "unknown error");
|
|
20166
|
+
return payload.result;
|
|
20167
|
+
}
|
|
20168
|
+
/**
|
|
20169
|
+
* Extracts the numeric bot id from a bot token.
|
|
20170
|
+
*
|
|
20171
|
+
* A Telegram token is `<bot_id>:<secret>`, so the bot id is intrinsic and
|
|
20172
|
+
* stable — no API call required.
|
|
20173
|
+
*
|
|
20174
|
+
* @param token - Full bot token
|
|
20175
|
+
* @returns The bot id, or an empty string when the token is malformed
|
|
20176
|
+
* @example
|
|
20177
|
+
* parseBotId("123456:ABC-DEF"); // "123456"
|
|
20178
|
+
*/ function parseBotId(token) {
|
|
20179
|
+
const botId = token.split(":")[0]?.trim() ?? "";
|
|
20180
|
+
return /^\d+$/.test(botId) ? botId : "";
|
|
20181
|
+
}
|
|
20182
|
+
/**
|
|
20183
|
+
* Builds a `t.me` deep link that pre-fills `/start <code>` for a bot.
|
|
20184
|
+
*
|
|
20185
|
+
* @param botUsername - Bot username without the leading `@`
|
|
20186
|
+
* @param code - Binding code to pass as the `start` parameter
|
|
20187
|
+
* @returns The deep link URL
|
|
20188
|
+
* @example
|
|
20189
|
+
* buildDeepLink("JantBot", "abc123"); // "https://t.me/JantBot?start=abc123"
|
|
20190
|
+
*/ function buildDeepLink(botUsername, code) {
|
|
20191
|
+
return `https://t.me/${botUsername}?start=${encodeURIComponent(code)}`;
|
|
20192
|
+
}
|
|
20193
|
+
/**
|
|
20194
|
+
* Validates a bot token and returns the bot's identity.
|
|
20195
|
+
*
|
|
20196
|
+
* @param token - Bot token to validate
|
|
20197
|
+
* @returns The bot's numeric id and username
|
|
20198
|
+
* @throws {TelegramApiError} When the token is invalid
|
|
20199
|
+
*/ async function getMe(token) {
|
|
20200
|
+
const result = await callTelegram(token, "getMe");
|
|
19940
20201
|
return {
|
|
19941
|
-
|
|
19942
|
-
|
|
19943
|
-
method: "POST",
|
|
19944
|
-
headers: {
|
|
19945
|
-
Authorization: `Bearer ${token}`,
|
|
19946
|
-
"Content-Type": "application/json"
|
|
19947
|
-
},
|
|
19948
|
-
body: JSON.stringify({ additionalBytes: input.additionalBytes })
|
|
19949
|
-
});
|
|
19950
|
-
if (response.ok) return await response.json();
|
|
19951
|
-
const message = await response.text();
|
|
19952
|
-
throw new Error(`Hosted control plane media quota check failed (${response.status}): ${message || "Unknown error."}`);
|
|
19953
|
-
},
|
|
19954
|
-
async syncSiteMetadata(input) {
|
|
19955
|
-
const response = await fetchImpl(new URL(`/api/internal/core-sites/${encodeURIComponent(input.coreSiteId)}/metadata`, baseUrl).toString(), {
|
|
19956
|
-
method: "POST",
|
|
19957
|
-
headers: {
|
|
19958
|
-
Authorization: `Bearer ${token}`,
|
|
19959
|
-
"Content-Type": "application/json"
|
|
19960
|
-
},
|
|
19961
|
-
body: JSON.stringify(input)
|
|
19962
|
-
});
|
|
19963
|
-
if (response.ok) return;
|
|
19964
|
-
const message = await response.text();
|
|
19965
|
-
throw new Error(`Hosted control plane metadata sync failed (${response.status}): ${message || "Unknown error."}`);
|
|
19966
|
-
}
|
|
20202
|
+
id: result.id,
|
|
20203
|
+
username: result.username ?? ""
|
|
19967
20204
|
};
|
|
19968
20205
|
}
|
|
19969
|
-
//#endregion
|
|
19970
|
-
//#region src/lib/resolve-config.ts
|
|
19971
20206
|
/**
|
|
19972
|
-
*
|
|
20207
|
+
* Registers a webhook URL for a bot.
|
|
19973
20208
|
*
|
|
19974
|
-
*
|
|
19975
|
-
*
|
|
19976
|
-
*
|
|
20209
|
+
* @param token - Bot token
|
|
20210
|
+
* @param url - Public webhook URL Telegram should POST updates to
|
|
20211
|
+
* @param secretToken - Value Telegram echoes back in the
|
|
20212
|
+
* `X-Telegram-Bot-Api-Secret-Token` header so the handler can verify the call
|
|
20213
|
+
*/ async function setWebhook(token, url, secretToken) {
|
|
20214
|
+
await callTelegram(token, "setWebhook", {
|
|
20215
|
+
url,
|
|
20216
|
+
secret_token: secretToken,
|
|
20217
|
+
allowed_updates: ["message", "callback_query"]
|
|
20218
|
+
});
|
|
20219
|
+
}
|
|
20220
|
+
/**
|
|
20221
|
+
* Removes a bot's webhook.
|
|
19977
20222
|
*
|
|
19978
|
-
*
|
|
19979
|
-
|
|
19980
|
-
|
|
20223
|
+
* @param token - Bot token
|
|
20224
|
+
*/ async function deleteWebhook(token) {
|
|
20225
|
+
await callTelegram(token, "deleteWebhook");
|
|
20226
|
+
}
|
|
19981
20227
|
/**
|
|
19982
|
-
*
|
|
20228
|
+
* Canonical bot command list. Telegram shows these in the `/` autocomplete
|
|
20229
|
+
* popup and on the bot's profile. The list is persisted per-bot on Telegram's
|
|
20230
|
+
* servers via `setMyCommands` — we only register commands the bot actually
|
|
20231
|
+
* responds to. Anything else the user sends is treated as note content.
|
|
20232
|
+
*/ var JANT_BOT_COMMANDS = [{
|
|
20233
|
+
command: "start",
|
|
20234
|
+
description: "Connect this chat to a Jant site"
|
|
20235
|
+
}];
|
|
20236
|
+
/**
|
|
20237
|
+
* Registers the bot's command list with Telegram so typing `/` in the chat
|
|
20238
|
+
* shows autocomplete suggestions. Idempotent — safe to call on every boot.
|
|
19983
20239
|
*
|
|
19984
|
-
* @param
|
|
19985
|
-
* @param
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
const envKeys = "envKeys" in field ? field.envKeys : void 0;
|
|
19992
|
-
if (!field.envOnly) {
|
|
19993
|
-
const dbValue = allSettings[key];
|
|
19994
|
-
if (dbValue) return dbValue;
|
|
19995
|
-
}
|
|
19996
|
-
const envValue = getEnvString(env, ...envKeys ?? []);
|
|
19997
|
-
if (envValue) return envValue;
|
|
19998
|
-
return field.defaultValue;
|
|
20240
|
+
* @param token - Bot token
|
|
20241
|
+
* @param commands - Commands to register. Defaults to `JANT_BOT_COMMANDS`.
|
|
20242
|
+
*/ async function setMyCommands(token, commands = JANT_BOT_COMMANDS) {
|
|
20243
|
+
await callTelegram(token, "setMyCommands", { commands: commands.map((c) => ({
|
|
20244
|
+
command: c.command,
|
|
20245
|
+
description: c.description
|
|
20246
|
+
})) });
|
|
19999
20247
|
}
|
|
20000
20248
|
/**
|
|
20001
|
-
*
|
|
20002
|
-
* Used for placeholder values in forms.
|
|
20249
|
+
* Returns the bot's currently registered webhook URL (empty when none).
|
|
20003
20250
|
*
|
|
20004
|
-
*
|
|
20005
|
-
*
|
|
20006
|
-
*
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
if (envValue) return envValue;
|
|
20012
|
-
return field.defaultValue;
|
|
20251
|
+
* Lets callers skip a redundant `setWebhook` write when the webhook is
|
|
20252
|
+
* already pointed at the right place.
|
|
20253
|
+
*
|
|
20254
|
+
* @param token - Bot token
|
|
20255
|
+
* @returns The current webhook URL, or `""` when no webhook is set
|
|
20256
|
+
*/ async function getWebhookUrl(token) {
|
|
20257
|
+
return (await callTelegram(token, "getWebhookInfo")).url ?? "";
|
|
20013
20258
|
}
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20259
|
+
/**
|
|
20260
|
+
* Sends a text message, optionally with an inline keyboard.
|
|
20261
|
+
*
|
|
20262
|
+
* @param token - Bot token
|
|
20263
|
+
* @param chatId - Target chat id
|
|
20264
|
+
* @param text - Message body
|
|
20265
|
+
* @param replyMarkup - Optional inline keyboard
|
|
20266
|
+
*/ async function sendMessage(token, chatId, text, replyMarkup) {
|
|
20267
|
+
await callTelegram(token, "sendMessage", {
|
|
20268
|
+
chat_id: chatId,
|
|
20269
|
+
text,
|
|
20270
|
+
...replyMarkup ? { reply_markup: replyMarkup } : {}
|
|
20271
|
+
});
|
|
20017
20272
|
}
|
|
20018
20273
|
/**
|
|
20019
|
-
*
|
|
20274
|
+
* Resolves a `file_id` to its downloadable path and size.
|
|
20020
20275
|
*
|
|
20021
|
-
*
|
|
20276
|
+
* Telegram's Bot API supports downloads up to 20 MB; the caller is expected to
|
|
20277
|
+
* enforce its own limit using `file_size` before calling `downloadFile`.
|
|
20022
20278
|
*
|
|
20023
|
-
* @param
|
|
20024
|
-
* @param
|
|
20025
|
-
|
|
20279
|
+
* @param token - Bot token
|
|
20280
|
+
* @param fileId - Identifier from a photo/video/document field
|
|
20281
|
+
*/ async function getFile(token, fileId) {
|
|
20282
|
+
return callTelegram(token, "getFile", { file_id: fileId });
|
|
20283
|
+
}
|
|
20284
|
+
/**
|
|
20285
|
+
* Downloads the raw bytes for a `file_path` returned by `getFile`.
|
|
20026
20286
|
*
|
|
20027
|
-
*
|
|
20028
|
-
*
|
|
20029
|
-
*
|
|
20030
|
-
*
|
|
20031
|
-
*
|
|
20032
|
-
*/ function
|
|
20033
|
-
const
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20037
|
-
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
|
|
20053
|
-
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
|
|
20063
|
-
|
|
20064
|
-
|
|
20287
|
+
* Returns the underlying `Response` so callers can stream large bodies into
|
|
20288
|
+
* storage without first materializing them in memory.
|
|
20289
|
+
*
|
|
20290
|
+
* @param token - Bot token
|
|
20291
|
+
* @param filePath - The `file_path` from `getFile`
|
|
20292
|
+
*/ async function downloadFile(token, filePath) {
|
|
20293
|
+
const response = await fetch(`${TELEGRAM_API_BASE}/file/bot${token}/${filePath}`);
|
|
20294
|
+
if (!response.ok) throw new TelegramApiError("downloadFile", `HTTP ${response.status} fetching ${filePath}`);
|
|
20295
|
+
return response;
|
|
20296
|
+
}
|
|
20297
|
+
/**
|
|
20298
|
+
* Acknowledges a callback query so Telegram stops showing a loading state on
|
|
20299
|
+
* the tapped inline button.
|
|
20300
|
+
*
|
|
20301
|
+
* @param token - Bot token
|
|
20302
|
+
* @param callbackQueryId - The callback query id to acknowledge
|
|
20303
|
+
*/ async function answerCallbackQuery(token, callbackQueryId) {
|
|
20304
|
+
await callTelegram(token, "answerCallbackQuery", { callback_query_id: callbackQueryId });
|
|
20305
|
+
}
|
|
20306
|
+
//#endregion
|
|
20307
|
+
//#region src/ui/dash/settings/TelegramContent.tsx
|
|
20308
|
+
/**
|
|
20309
|
+
* Telegram settings page
|
|
20310
|
+
*
|
|
20311
|
+
* States:
|
|
20312
|
+
* 1. Bring-your-own bot, no token saved — token input form
|
|
20313
|
+
* 2. Not connected, a bot is available — deep link + QR + binding code
|
|
20314
|
+
* 3. Connected — connected account, posting hint, disconnect
|
|
20315
|
+
*
|
|
20316
|
+
* In env-managed-pool deployments the token field is never shown: the bot
|
|
20317
|
+
* pool is platform-owned, so users only ever see the binding code flow.
|
|
20318
|
+
*/ function Spinner({ signal, size = "size-4" }) {
|
|
20319
|
+
return /* @__PURE__ */ jsxDEV$1("svg", {
|
|
20320
|
+
"data-show": `$${signal}`,
|
|
20321
|
+
style: "display:none",
|
|
20322
|
+
class: `animate-spin ${size}`,
|
|
20323
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
20324
|
+
viewBox: "0 0 24 24",
|
|
20325
|
+
fill: "none",
|
|
20326
|
+
stroke: "currentColor",
|
|
20327
|
+
"stroke-width": "2",
|
|
20328
|
+
"stroke-linecap": "round",
|
|
20329
|
+
"stroke-linejoin": "round",
|
|
20330
|
+
role: "status",
|
|
20331
|
+
children: /* @__PURE__ */ jsxDEV$1("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
|
|
20332
|
+
});
|
|
20333
|
+
}
|
|
20334
|
+
var STATUS_DOT = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="5" cy="5" r="5" fill="currentColor"/></svg>`;
|
|
20335
|
+
function TelegramContent({ view, sitePathPrefix = "", streamUrl }) {
|
|
20336
|
+
const settingsBase = toPublicPath("/settings/telegram", sitePathPrefix);
|
|
20337
|
+
let inner;
|
|
20338
|
+
if (view.binding) inner = /* @__PURE__ */ jsxDEV$1(TelegramConnected, {
|
|
20339
|
+
view,
|
|
20340
|
+
settingsBase
|
|
20341
|
+
});
|
|
20342
|
+
else if (!view.managed && !view.userBotConfigured) inner = /* @__PURE__ */ jsxDEV$1(TelegramSetupForm, { settingsBase });
|
|
20343
|
+
else inner = /* @__PURE__ */ jsxDEV$1(TelegramConnect, {
|
|
20344
|
+
view,
|
|
20345
|
+
settingsBase
|
|
20346
|
+
});
|
|
20347
|
+
const subscribe = !view.binding && view.connect && streamUrl;
|
|
20348
|
+
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
20349
|
+
id: "telegram-status",
|
|
20350
|
+
"data-init": subscribe ? `@get('${streamUrl}')` : void 0,
|
|
20351
|
+
children: inner
|
|
20352
|
+
});
|
|
20353
|
+
}
|
|
20354
|
+
function TelegramSetupForm({ settingsBase }) {
|
|
20355
|
+
const { i18n } = useLingui();
|
|
20356
|
+
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
20357
|
+
class: "flex flex-col gap-6 max-w-form",
|
|
20358
|
+
children: [/* @__PURE__ */ jsxDEV$1("div", { children: [/* @__PURE__ */ jsxDEV$1("h2", {
|
|
20359
|
+
class: "text-lg font-medium mb-1",
|
|
20360
|
+
children: i18n._({ id: "SchpMp" })
|
|
20361
|
+
}), /* @__PURE__ */ jsxDEV$1("p", {
|
|
20362
|
+
class: "text-sm text-muted-foreground",
|
|
20363
|
+
children: i18n._({ id: "YkgZi7" })
|
|
20364
|
+
})] }), /* @__PURE__ */ jsxDEV$1("form", {
|
|
20365
|
+
class: "flex flex-col gap-4",
|
|
20366
|
+
"data-on:submit__prevent": `@post('${settingsBase}/connect')`,
|
|
20367
|
+
"data-indicator": "_connecting",
|
|
20368
|
+
children: [/* @__PURE__ */ jsxDEV$1("div", {
|
|
20369
|
+
class: "field",
|
|
20370
|
+
children: [
|
|
20371
|
+
/* @__PURE__ */ jsxDEV$1("label", {
|
|
20372
|
+
class: "label",
|
|
20373
|
+
for: "telegram-token",
|
|
20374
|
+
children: i18n._({ id: "8T46pB" })
|
|
20375
|
+
}),
|
|
20376
|
+
/* @__PURE__ */ jsxDEV$1("input", {
|
|
20377
|
+
id: "telegram-token",
|
|
20378
|
+
"data-bind": "token",
|
|
20379
|
+
type: "password",
|
|
20380
|
+
class: "input",
|
|
20381
|
+
placeholder: "123456789:ABC...",
|
|
20382
|
+
required: true,
|
|
20383
|
+
autocomplete: "off"
|
|
20384
|
+
}),
|
|
20385
|
+
/* @__PURE__ */ jsxDEV$1("p", {
|
|
20386
|
+
class: "text-sm text-muted-foreground mt-1",
|
|
20387
|
+
children: i18n._({ id: "RcdDOS" })
|
|
20388
|
+
})
|
|
20389
|
+
]
|
|
20390
|
+
}), /* @__PURE__ */ jsxDEV$1("div", {
|
|
20391
|
+
class: "flex mt-2",
|
|
20392
|
+
children: /* @__PURE__ */ jsxDEV$1("button", {
|
|
20393
|
+
type: "submit",
|
|
20394
|
+
class: "btn",
|
|
20395
|
+
"data-attr:disabled": "$_connecting",
|
|
20396
|
+
children: [/* @__PURE__ */ jsxDEV$1(Spinner, { signal: "_connecting" }), i18n._({ id: "7GISOt" })]
|
|
20397
|
+
})
|
|
20398
|
+
})]
|
|
20399
|
+
})]
|
|
20400
|
+
});
|
|
20401
|
+
}
|
|
20402
|
+
function TelegramConnect({ view, settingsBase }) {
|
|
20403
|
+
const { i18n } = useLingui();
|
|
20404
|
+
const connect = view.connect;
|
|
20405
|
+
if (!connect) return /* @__PURE__ */ jsxDEV$1("div", {
|
|
20406
|
+
class: "flex flex-col gap-4 max-w-form",
|
|
20407
|
+
children: /* @__PURE__ */ jsxDEV$1("div", {
|
|
20408
|
+
class: "alert",
|
|
20409
|
+
children: /* @__PURE__ */ jsxDEV$1("span", { children: i18n._({ id: "id3vuh" }) })
|
|
20410
|
+
})
|
|
20411
|
+
});
|
|
20412
|
+
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
20413
|
+
class: "flex flex-col gap-6 max-w-form",
|
|
20414
|
+
children: [
|
|
20415
|
+
/* @__PURE__ */ jsxDEV$1("div", { children: [/* @__PURE__ */ jsxDEV$1("h2", {
|
|
20416
|
+
class: "text-lg font-medium mb-1",
|
|
20417
|
+
children: i18n._({ id: "D8k2s6" })
|
|
20418
|
+
}), /* @__PURE__ */ jsxDEV$1("p", {
|
|
20419
|
+
class: "text-sm text-muted-foreground",
|
|
20420
|
+
children: i18n._({ id: "39QGku" })
|
|
20421
|
+
})] }),
|
|
20422
|
+
/* @__PURE__ */ jsxDEV$1("div", {
|
|
20423
|
+
class: "flex flex-col items-center gap-4 py-2",
|
|
20424
|
+
"data-signals": "{_codeCopied: false, _manualOpen: false}",
|
|
20425
|
+
children: [
|
|
20426
|
+
/* @__PURE__ */ jsxDEV$1("div", {
|
|
20427
|
+
class: "hidden sm:block bg-white p-3 rounded-lg shadow-sm",
|
|
20428
|
+
style: "width:180px;height:180px",
|
|
20429
|
+
"aria-label": i18n._({ id: "/zOUxl" }),
|
|
20430
|
+
dangerouslySetInnerHTML: { __html: connect.qrSvg }
|
|
20431
|
+
}),
|
|
20432
|
+
/* @__PURE__ */ jsxDEV$1("span", {
|
|
20433
|
+
class: "hidden sm:block text-xs text-muted-foreground",
|
|
20434
|
+
"aria-hidden": "true",
|
|
20435
|
+
children: i18n._({ id: "BzEFor" })
|
|
20436
|
+
}),
|
|
20437
|
+
/* @__PURE__ */ jsxDEV$1("a", {
|
|
20438
|
+
href: connect.deepLink,
|
|
20439
|
+
target: "_blank",
|
|
20440
|
+
rel: "noopener noreferrer",
|
|
20441
|
+
class: "btn",
|
|
20442
|
+
children: i18n._({ id: "0bdA9b" })
|
|
20443
|
+
}),
|
|
20444
|
+
/* @__PURE__ */ jsxDEV$1("div", {
|
|
20445
|
+
class: "flex items-center gap-3 text-xs text-muted-foreground",
|
|
20446
|
+
children: [
|
|
20447
|
+
/* @__PURE__ */ jsxDEV$1("button", {
|
|
20448
|
+
type: "button",
|
|
20449
|
+
class: "cursor-pointer hover:text-foreground underline-offset-4 hover:underline inline-flex items-center gap-1",
|
|
20450
|
+
"aria-controls": "telegram-manual-fallback",
|
|
20451
|
+
"data-attr:aria-expanded": "$_manualOpen",
|
|
20452
|
+
"data-on:click": "$_manualOpen = !$_manualOpen",
|
|
20453
|
+
children: [i18n._({ id: "1mbBbL" }), /* @__PURE__ */ jsxDEV$1("span", {
|
|
20454
|
+
"data-text": "$_manualOpen ? '↑' : '↓'",
|
|
20455
|
+
children: "↓"
|
|
20456
|
+
})]
|
|
20457
|
+
}),
|
|
20458
|
+
/* @__PURE__ */ jsxDEV$1("span", {
|
|
20459
|
+
"aria-hidden": "true",
|
|
20460
|
+
children: "·"
|
|
20461
|
+
}),
|
|
20462
|
+
/* @__PURE__ */ jsxDEV$1("button", {
|
|
20463
|
+
type: "button",
|
|
20464
|
+
class: "cursor-pointer hover:text-foreground underline underline-offset-4 decoration-dotted inline-flex items-center gap-1.5",
|
|
20465
|
+
"data-on:click__prevent": `@post('${settingsBase}/regenerate-code')`,
|
|
20466
|
+
"data-indicator": "_regenerating",
|
|
20467
|
+
"data-attr:disabled": "$_regenerating",
|
|
20468
|
+
children: [/* @__PURE__ */ jsxDEV$1(Spinner, {
|
|
20469
|
+
signal: "_regenerating",
|
|
20470
|
+
size: "size-3"
|
|
20471
|
+
}), i18n._({ id: "71WIgc" })]
|
|
20472
|
+
})
|
|
20473
|
+
]
|
|
20474
|
+
}),
|
|
20475
|
+
/* @__PURE__ */ jsxDEV$1("div", {
|
|
20476
|
+
id: "telegram-manual-fallback",
|
|
20477
|
+
"data-show": "$_manualOpen",
|
|
20478
|
+
style: "display:none",
|
|
20479
|
+
class: "flex flex-col items-center gap-3 w-full pt-1",
|
|
20480
|
+
children: [/* @__PURE__ */ jsxDEV$1("p", {
|
|
20481
|
+
class: "text-sm text-muted-foreground text-center",
|
|
20482
|
+
dangerouslySetInnerHTML: { __html: i18n._({ id: "UTvFQq" }, {
|
|
20483
|
+
botUsername: escapeHtml(connect.botUsername),
|
|
20484
|
+
linkOpen: `<a href="${escapeHtml(`https://t.me/${connect.botUsername}`)}" target="_blank" rel="noopener noreferrer" class="underline underline-offset-2 hover:text-foreground">`,
|
|
20485
|
+
linkClose: "</a>"
|
|
20486
|
+
}) }
|
|
20487
|
+
}), /* @__PURE__ */ jsxDEV$1("div", {
|
|
20488
|
+
class: "flex items-center gap-2",
|
|
20489
|
+
children: [/* @__PURE__ */ jsxDEV$1("code", {
|
|
20490
|
+
class: "text-sm bg-muted px-3 py-1.5 rounded font-mono select-all",
|
|
20491
|
+
children: ["/start ", connect.code]
|
|
20492
|
+
}), /* @__PURE__ */ jsxDEV$1("button", {
|
|
20493
|
+
type: "button",
|
|
20494
|
+
class: "btn-sm-outline shrink-0",
|
|
20495
|
+
"aria-label": i18n._({ id: "he3ygx" }),
|
|
20496
|
+
"data-on:click": `navigator.clipboard.writeText('/start ${connect.code}'); $_codeCopied = true`,
|
|
20497
|
+
"data-text": `$_codeCopied ? '${i18n._({ id: "6V3Ea3" })}' : '${i18n._({ id: "he3ygx" })}'`,
|
|
20498
|
+
children: i18n._({ id: "he3ygx" })
|
|
20499
|
+
})]
|
|
20500
|
+
})]
|
|
20501
|
+
})
|
|
20502
|
+
]
|
|
20503
|
+
}),
|
|
20504
|
+
!view.managed ? /* @__PURE__ */ jsxDEV$1("div", {
|
|
20505
|
+
class: "flex justify-center pt-2",
|
|
20506
|
+
children: /* @__PURE__ */ jsxDEV$1(RemoveBotLink, { settingsBase })
|
|
20507
|
+
}) : null
|
|
20508
|
+
]
|
|
20509
|
+
});
|
|
20510
|
+
}
|
|
20511
|
+
function TelegramConnected({ view, settingsBase }) {
|
|
20512
|
+
const { i18n } = useLingui();
|
|
20513
|
+
const account = view.binding?.telegramUsername ? `@${view.binding.telegramUsername}` : i18n._({ id: "tJ4H0O" });
|
|
20514
|
+
const disconnectLabel = i18n._({ id: "+K0AvT" });
|
|
20515
|
+
const cancelLabel = i18n._({ id: "dEgA5A" });
|
|
20516
|
+
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
20517
|
+
class: "flex flex-col gap-8 max-w-form",
|
|
20518
|
+
children: [/* @__PURE__ */ jsxDEV$1("div", { children: [
|
|
20519
|
+
/* @__PURE__ */ jsxDEV$1("h2", {
|
|
20520
|
+
class: "text-lg font-medium mb-1",
|
|
20521
|
+
children: i18n._({ id: "SchpMp" })
|
|
20522
|
+
}),
|
|
20523
|
+
/* @__PURE__ */ jsxDEV$1("div", {
|
|
20524
|
+
class: "flex items-center gap-2 text-sm",
|
|
20525
|
+
children: [/* @__PURE__ */ jsxDEV$1("span", {
|
|
20526
|
+
class: "text-green-600 dark:text-green-500",
|
|
20527
|
+
dangerouslySetInnerHTML: { __html: STATUS_DOT }
|
|
20528
|
+
}), /* @__PURE__ */ jsxDEV$1("span", { children: i18n._({ id: "5f1Wo9" }, { account }) })]
|
|
20529
|
+
}),
|
|
20530
|
+
/* @__PURE__ */ jsxDEV$1("p", {
|
|
20531
|
+
class: "text-sm text-muted-foreground mt-2",
|
|
20532
|
+
children: i18n._({ id: "2Ithfh" })
|
|
20533
|
+
})
|
|
20534
|
+
] }), /* @__PURE__ */ jsxDEV$1("section", {
|
|
20535
|
+
class: "flex flex-col gap-3 border-t pt-8",
|
|
20536
|
+
children: [/* @__PURE__ */ jsxDEV$1("p", {
|
|
20537
|
+
class: "text-sm text-muted-foreground",
|
|
20538
|
+
children: i18n._({ id: "PXj9lw" })
|
|
20539
|
+
}), /* @__PURE__ */ jsxDEV$1("div", {
|
|
20540
|
+
class: "flex flex-wrap items-center gap-4 mt-1",
|
|
20541
|
+
children: [/* @__PURE__ */ jsxDEV$1("button", {
|
|
20542
|
+
type: "button",
|
|
20543
|
+
class: "cursor-pointer text-sm text-destructive/80 hover:text-destructive hover:underline underline-offset-4 transition-colors inline-flex items-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed",
|
|
20544
|
+
"data-indicator": "_disconnecting",
|
|
20545
|
+
"data-attr:disabled": "$_disconnecting",
|
|
20546
|
+
"data-on:click__prevent": buildConfirmActionExpression(`@post('${settingsBase}/disconnect')`, {
|
|
20547
|
+
message: i18n._({ id: "Mr4QPw" }),
|
|
20548
|
+
confirmLabel: disconnectLabel,
|
|
20549
|
+
cancelLabel,
|
|
20550
|
+
tone: "danger"
|
|
20551
|
+
}),
|
|
20552
|
+
children: [/* @__PURE__ */ jsxDEV$1(Spinner, { signal: "_disconnecting" }), disconnectLabel]
|
|
20553
|
+
}), !view.managed ? /* @__PURE__ */ jsxDEV$1(RemoveBotLink, { settingsBase }) : null]
|
|
20554
|
+
})]
|
|
20555
|
+
})]
|
|
20556
|
+
});
|
|
20557
|
+
}
|
|
20558
|
+
function RemoveBotLink({ settingsBase }) {
|
|
20559
|
+
const { i18n } = useLingui();
|
|
20560
|
+
const removeLabel = i18n._({ id: "CDAdlf" });
|
|
20561
|
+
const cancelLabel = i18n._({ id: "dEgA5A" });
|
|
20562
|
+
return /* @__PURE__ */ jsxDEV$1("button", {
|
|
20563
|
+
type: "button",
|
|
20564
|
+
class: "cursor-pointer text-xs text-muted-foreground hover:text-destructive underline underline-offset-4 decoration-dotted inline-flex items-center gap-1.5",
|
|
20565
|
+
"data-indicator": "_removing",
|
|
20566
|
+
"data-attr:disabled": "$_removing",
|
|
20567
|
+
"data-on:click__prevent": buildConfirmActionExpression(`@post('${settingsBase}/remove-bot')`, {
|
|
20568
|
+
message: i18n._({ id: "cS7/bk" }),
|
|
20569
|
+
confirmLabel: removeLabel,
|
|
20570
|
+
cancelLabel,
|
|
20571
|
+
tone: "danger"
|
|
20572
|
+
}),
|
|
20573
|
+
children: [/* @__PURE__ */ jsxDEV$1(Spinner, {
|
|
20574
|
+
signal: "_removing",
|
|
20575
|
+
size: "size-3"
|
|
20576
|
+
}), removeLabel]
|
|
20577
|
+
});
|
|
20578
|
+
}
|
|
20579
|
+
//#endregion
|
|
20580
|
+
//#region src/lib/telegram-settings-status.tsx
|
|
20581
|
+
/**
|
|
20582
|
+
* Shared helpers for reading Telegram settings status and rendering the
|
|
20583
|
+
* settings panel.
|
|
20584
|
+
*
|
|
20585
|
+
* Consumed by:
|
|
20586
|
+
* - GET /settings/telegram — initial page render
|
|
20587
|
+
* - GET /settings/telegram/status/stream — live status polling loop that
|
|
20588
|
+
* swaps the connect view for the connected view the moment a binding lands
|
|
20589
|
+
*
|
|
20590
|
+
* Both call sites render the same `<TelegramContent>` through
|
|
20591
|
+
* `renderTelegramContentHtml`, so any markup change flows to both
|
|
20592
|
+
* automatically.
|
|
20593
|
+
*/
|
|
20594
|
+
/**
|
|
20595
|
+
* Build the `TelegramSettingsView` for the current site. Mirrors what the
|
|
20596
|
+
* `/settings/telegram` GET route does, factored out so the SSE stream can
|
|
20597
|
+
* re-render exactly the same view.
|
|
20598
|
+
*/ async function readTelegramSettingsView(c) {
|
|
20599
|
+
const pool = getTelegramBotPool(c.env);
|
|
20600
|
+
const managed = pool.length > 0;
|
|
20601
|
+
const status = await c.var.services.telegram.getStatus();
|
|
20602
|
+
let botUsername = "";
|
|
20603
|
+
const firstBot = pool[0];
|
|
20604
|
+
if (firstBot) try {
|
|
20605
|
+
botUsername = (await getMe(firstBot.token)).username;
|
|
20606
|
+
} catch {
|
|
20607
|
+
botUsername = "";
|
|
20608
|
+
}
|
|
20609
|
+
else if (status.userBot) botUsername = status.userBot.username;
|
|
20610
|
+
let connect = null;
|
|
20611
|
+
if (!status.binding && botUsername) {
|
|
20612
|
+
const code = await c.var.services.telegram.getOrCreateCode();
|
|
20613
|
+
const deepLink = buildDeepLink(botUsername, code);
|
|
20614
|
+
connect = {
|
|
20615
|
+
code,
|
|
20616
|
+
deepLink,
|
|
20617
|
+
qrSvg: renderSVG(deepLink),
|
|
20618
|
+
botUsername
|
|
20619
|
+
};
|
|
20620
|
+
}
|
|
20621
|
+
return {
|
|
20622
|
+
managed,
|
|
20623
|
+
binding: status.binding ? {
|
|
20624
|
+
telegramUsername: status.binding.telegramUsername,
|
|
20625
|
+
boundAt: status.binding.boundAt
|
|
20626
|
+
} : null,
|
|
20627
|
+
userBotConfigured: status.userBot !== null,
|
|
20628
|
+
connect
|
|
20629
|
+
};
|
|
20630
|
+
}
|
|
20631
|
+
/**
|
|
20632
|
+
* Render the Telegram settings panel to an HTML string. The `streamUrl`,
|
|
20633
|
+
* when provided, mounts a Datastar SSE subscription on the connect view so
|
|
20634
|
+
* the page auto-swaps to the connected view the moment a binding lands.
|
|
20635
|
+
*/ function renderTelegramContentHtml(c, view, streamUrl) {
|
|
20636
|
+
return String(/* @__PURE__ */ jsxDEV$1(I18nProvider, {
|
|
20637
|
+
c,
|
|
20638
|
+
children: /* @__PURE__ */ jsxDEV$1(TelegramContent, {
|
|
20639
|
+
view,
|
|
20640
|
+
sitePathPrefix: c.var.appConfig.sitePathPrefix,
|
|
20641
|
+
streamUrl
|
|
20642
|
+
})
|
|
20643
|
+
}));
|
|
20644
|
+
}
|
|
20645
|
+
/** URL of the SSE endpoint that streams settings-panel patches. */ function getTelegramStatusStreamUrl(c) {
|
|
20646
|
+
return toPublicPath("/settings/telegram/status/stream", c.var.appConfig.sitePathPrefix);
|
|
20647
|
+
}
|
|
20648
|
+
//#endregion
|
|
20649
|
+
//#region src/lib/hosted-control-plane.ts
|
|
20650
|
+
function createHostedControlPlaneClient(env, fetchImpl = fetch) {
|
|
20651
|
+
if (getSiteResolutionMode(env) !== "host-based") return null;
|
|
20652
|
+
const baseUrl = getHostedControlPlaneInternalBaseUrl(env);
|
|
20653
|
+
const token = getHostedControlPlaneInternalToken(env);
|
|
20654
|
+
if (!baseUrl || !token) return null;
|
|
20655
|
+
return {
|
|
20656
|
+
async checkMediaWriteQuota(input) {
|
|
20657
|
+
const response = await fetchImpl(new URL(`/api/internal/core-sites/${encodeURIComponent(input.coreSiteId)}/media/quota/check`, baseUrl).toString(), {
|
|
20658
|
+
method: "POST",
|
|
20659
|
+
headers: {
|
|
20660
|
+
Authorization: `Bearer ${token}`,
|
|
20661
|
+
"Content-Type": "application/json"
|
|
20662
|
+
},
|
|
20663
|
+
body: JSON.stringify({ additionalBytes: input.additionalBytes })
|
|
20664
|
+
});
|
|
20665
|
+
if (response.ok) return await response.json();
|
|
20666
|
+
const message = await response.text();
|
|
20667
|
+
throw new Error(`Hosted control plane media quota check failed (${response.status}): ${message || "Unknown error."}`);
|
|
20668
|
+
},
|
|
20669
|
+
async syncSiteMetadata(input) {
|
|
20670
|
+
const response = await fetchImpl(new URL(`/api/internal/core-sites/${encodeURIComponent(input.coreSiteId)}/metadata`, baseUrl).toString(), {
|
|
20671
|
+
method: "POST",
|
|
20672
|
+
headers: {
|
|
20673
|
+
Authorization: `Bearer ${token}`,
|
|
20674
|
+
"Content-Type": "application/json"
|
|
20675
|
+
},
|
|
20676
|
+
body: JSON.stringify(input)
|
|
20677
|
+
});
|
|
20678
|
+
if (response.ok) return;
|
|
20679
|
+
const message = await response.text();
|
|
20680
|
+
throw new Error(`Hosted control plane metadata sync failed (${response.status}): ${message || "Unknown error."}`);
|
|
20681
|
+
}
|
|
20682
|
+
};
|
|
20683
|
+
}
|
|
20684
|
+
//#endregion
|
|
20685
|
+
//#region src/lib/resolve-config.ts
|
|
20686
|
+
/**
|
|
20687
|
+
* Unified App Configuration
|
|
20688
|
+
*
|
|
20689
|
+
* Resolves all configuration from environment + DB settings into a single
|
|
20690
|
+
* immutable object. Created once per request in middleware, then accessed
|
|
20691
|
+
* via `c.var.appConfig` everywhere else.
|
|
20692
|
+
*
|
|
20693
|
+
* Priority: DB > ENV > Default (for user-configurable fields)
|
|
20694
|
+
* ENV > Default (for envOnly fields)
|
|
20695
|
+
*/
|
|
20696
|
+
/**
|
|
20697
|
+
* Resolve a single config value following priority rules.
|
|
20698
|
+
*
|
|
20699
|
+
* @param key - CONFIG_FIELDS key
|
|
20700
|
+
* @param allSettings - DB settings map
|
|
20701
|
+
* @param env - Worker bindings
|
|
20702
|
+
* @returns Resolved string value
|
|
20703
|
+
*/ function resolve(key, allSettings, env) {
|
|
20704
|
+
const field = CONFIG_FIELDS[key];
|
|
20705
|
+
if (!field) return "";
|
|
20706
|
+
const envKeys = "envKeys" in field ? field.envKeys : void 0;
|
|
20707
|
+
if (!field.envOnly) {
|
|
20708
|
+
const dbValue = allSettings[key];
|
|
20709
|
+
if (dbValue) return dbValue;
|
|
20710
|
+
}
|
|
20711
|
+
const envValue = getEnvString(env, ...envKeys ?? []);
|
|
20712
|
+
if (envValue) return envValue;
|
|
20713
|
+
return field.defaultValue;
|
|
20714
|
+
}
|
|
20715
|
+
/**
|
|
20716
|
+
* Resolve a fallback value (ENV > Default), skipping the database.
|
|
20717
|
+
* Used for placeholder values in forms.
|
|
20718
|
+
*
|
|
20719
|
+
* @param key - CONFIG_FIELDS key
|
|
20720
|
+
* @param env - Worker bindings
|
|
20721
|
+
* @returns Fallback value
|
|
20722
|
+
*/ function resolveFallback(key, env) {
|
|
20723
|
+
const field = CONFIG_FIELDS[key];
|
|
20724
|
+
if (!field) return "";
|
|
20725
|
+
const envValue = getEnvString(env, ...("envKeys" in field ? field.envKeys : void 0) ?? []);
|
|
20726
|
+
if (envValue) return envValue;
|
|
20727
|
+
return field.defaultValue;
|
|
20728
|
+
}
|
|
20729
|
+
function parseConfigInt(value, fallback) {
|
|
20730
|
+
const parsed = Number.parseInt(value, 10);
|
|
20731
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
20732
|
+
}
|
|
20733
|
+
/**
|
|
20734
|
+
* Build a complete AppConfig from environment bindings and DB settings.
|
|
20735
|
+
*
|
|
20736
|
+
* Pure function — no side effects, no DB access.
|
|
20737
|
+
*
|
|
20738
|
+
* @param env - Cloudflare Worker bindings
|
|
20739
|
+
* @param allSettings - All DB settings (from `services.settings.getAll()`)
|
|
20740
|
+
* @returns Fully resolved AppConfig
|
|
20741
|
+
*
|
|
20742
|
+
* @example
|
|
20743
|
+
* ```ts
|
|
20744
|
+
* const allSettings = await services.settings.getAll();
|
|
20745
|
+
* const appConfig = resolveConfig(c.env, allSettings);
|
|
20746
|
+
* ```
|
|
20747
|
+
*/ function resolveConfig(env, allSettings, options) {
|
|
20748
|
+
const pageSize = parseConfigInt(resolve("PAGE_SIZE", allSettings, env), 50);
|
|
20749
|
+
const searchPageSize = parseConfigInt(resolve("SEARCH_PAGE_SIZE", allSettings, env), pageSize);
|
|
20750
|
+
const archivePageSize = parseConfigInt(resolve("ARCHIVE_PAGE_SIZE", allSettings, env), pageSize);
|
|
20751
|
+
const siteUrl = normalizeSiteUrl(options?.siteUrl ?? getConfiguredSingleSiteUrl(env));
|
|
20752
|
+
const siteOrigin = getSiteOrigin(siteUrl);
|
|
20753
|
+
const sitePathPrefix = getSitePathPrefix(siteUrl);
|
|
20754
|
+
const storageDriver = getConfiguredStorageDriver(env);
|
|
20755
|
+
const r2PublicUrl = getEnvString(env, "R2_PUBLIC_URL") || "";
|
|
20756
|
+
const s3PublicUrl = getEnvString(env, "S3_PUBLIC_URL") || "";
|
|
20757
|
+
const localPublicUrl = getEnvString(env, "LOCAL_PUBLIC_URL") || "";
|
|
20758
|
+
const imageTransformUrl = getEnvString(env, "IMAGE_TRANSFORM_URL") || "";
|
|
20759
|
+
const demoMode = getEnvString(env, "DEMO_MODE") === "true";
|
|
20760
|
+
const siteAvatar = allSettings["SITE_AVATAR"] ?? "";
|
|
20761
|
+
let siteAvatarUrl = "";
|
|
20762
|
+
if (siteAvatar) siteAvatarUrl = getMediaUrl(siteAvatar, getPublicUrlForProvider(storageDriver, r2PublicUrl, s3PublicUrl, localPublicUrl), sitePathPrefix);
|
|
20763
|
+
const dbDescription = allSettings["SITE_DESCRIPTION"];
|
|
20764
|
+
const envDescription = getEnvString(env, "SITE_DESCRIPTION");
|
|
20765
|
+
const siteDescriptionExplicit = !!(dbDescription || envDescription);
|
|
20766
|
+
return {
|
|
20767
|
+
siteName: resolve("SITE_NAME", allSettings, env),
|
|
20768
|
+
siteDescription: resolve("SITE_DESCRIPTION", allSettings, env),
|
|
20769
|
+
siteDescriptionExplicit,
|
|
20770
|
+
siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
|
|
20771
|
+
cjkSerifFont: resolve("CJK_SERIF_FONT", allSettings, env),
|
|
20772
|
+
homeDefaultView: resolve("HOME_DEFAULT_VIEW", allSettings, env),
|
|
20773
|
+
mainRssFeed: resolve("MAIN_RSS_FEED", allSettings, env),
|
|
20774
|
+
timeZone: normalizeTimeZone(resolve("TIME_ZONE", allSettings, env)),
|
|
20775
|
+
siteFooter: resolve("SITE_FOOTER", allSettings, env),
|
|
20776
|
+
showJantBrandingOnHome: resolve("SHOW_JANT_BRANDING_ON_HOME", allSettings, env) === "true",
|
|
20777
|
+
noindex: demoMode || resolve("NOINDEX", allSettings, env) === "true",
|
|
20778
|
+
siteUrl,
|
|
20779
|
+
siteOrigin,
|
|
20065
20780
|
sitePathPrefix,
|
|
20066
20781
|
assetBasePath: (() => {
|
|
20067
20782
|
const assetBaseUrl = (getEnvString(env, "ASSET_BASE_URL") ?? "").trim().replace(/\/+$/, "");
|
|
@@ -20238,8 +20953,8 @@ async function syncHostedControlPlaneSiteAvatar(input) {
|
|
|
20238
20953
|
return;
|
|
20239
20954
|
}
|
|
20240
20955
|
await markSyncPending(settings);
|
|
20241
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20242
|
-
const { getGitHubAppConfig } = await import("./env-
|
|
20956
|
+
const { createGitHubSyncService } = await import("./github-sync-dXsiZa_e.js");
|
|
20957
|
+
const { getGitHubAppConfig } = await import("./env-C7e2Nlnt.js").then((n) => n.t);
|
|
20243
20958
|
const run = runBackgroundSync(settings, createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
20244
20959
|
storage: c.var.storage,
|
|
20245
20960
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -20427,6 +21142,7 @@ function publicPath(c, path) {
|
|
|
20427
21142
|
case "deleteAccount": return i18n._({ id: "vzX5FB" });
|
|
20428
21143
|
case "apiTokens": return i18n._({ id: "ZiooJI" });
|
|
20429
21144
|
case "githubSync": return i18n._({ id: "kRhzWq" });
|
|
21145
|
+
case "telegram": return i18n._({ id: "SchpMp" });
|
|
20430
21146
|
}
|
|
20431
21147
|
}
|
|
20432
21148
|
function getDemoRestrictionMessage(c, restriction) {
|
|
@@ -21025,7 +21741,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
21025
21741
|
await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "pat");
|
|
21026
21742
|
await c.var.services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", "");
|
|
21027
21743
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
21028
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
21744
|
+
const { createGitHubSyncService } = await import("./github-sync-dXsiZa_e.js");
|
|
21029
21745
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
21030
21746
|
storage: c.var.storage,
|
|
21031
21747
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -21044,7 +21760,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
21044
21760
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
21045
21761
|
});
|
|
21046
21762
|
settingsRoutes.post("/github-sync/push", async (c) => {
|
|
21047
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
21763
|
+
const { createGitHubSyncService } = await import("./github-sync-dXsiZa_e.js");
|
|
21048
21764
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
21049
21765
|
storage: c.var.storage,
|
|
21050
21766
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -21064,7 +21780,7 @@ settingsRoutes.post("/github-sync/push", async (c) => {
|
|
|
21064
21780
|
});
|
|
21065
21781
|
});
|
|
21066
21782
|
settingsRoutes.post("/github-sync/disconnect", async (c) => {
|
|
21067
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
21783
|
+
const { createGitHubSyncService } = await import("./github-sync-dXsiZa_e.js");
|
|
21068
21784
|
await createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), { githubApp: getGitHubAppConfig(c.env) }).teardownWebhook();
|
|
21069
21785
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
21070
21786
|
});
|
|
@@ -21255,7 +21971,7 @@ function buildRepoPickerLabels(c) {
|
|
|
21255
21971
|
const { parseRepoSlug, createGitHubClient } = await import("./github-api-Bh0PH3zr.js").then((n) => n.n);
|
|
21256
21972
|
const parsed = parseRepoSlug(repo);
|
|
21257
21973
|
if (!parsed) return wantsJson ? c.json({ error: "Invalid repository format." }, 400) : c.text("Invalid repository format.", 400);
|
|
21258
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
21974
|
+
const { classifyRepoForSync } = await import("./github-sync-dXsiZa_e.js");
|
|
21259
21975
|
const ghClient = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
21260
21976
|
let classification;
|
|
21261
21977
|
try {
|
|
@@ -21285,7 +22001,7 @@ function buildRepoPickerLabels(c) {
|
|
|
21285
22001
|
await c.var.services.settings.set("GITHUB_SYNC_REPO", repo);
|
|
21286
22002
|
await c.var.services.settings.set("GITHUB_SYNC_TOKEN", "");
|
|
21287
22003
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
21288
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
22004
|
+
const { createGitHubSyncService } = await import("./github-sync-dXsiZa_e.js");
|
|
21289
22005
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
21290
22006
|
storage: c.var.storage,
|
|
21291
22007
|
githubApp: app
|
|
@@ -21401,7 +22117,7 @@ function requireGitHubApp(c) {
|
|
|
21401
22117
|
const { parseRepoSlug, createGitHubClient } = await import("./github-api-Bh0PH3zr.js").then((n) => n.n);
|
|
21402
22118
|
const parsed = parseRepoSlug(repo);
|
|
21403
22119
|
if (!parsed) return c.json({ error: "Invalid repository format." }, 400);
|
|
21404
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
22120
|
+
const { classifyRepoForSync } = await import("./github-sync-dXsiZa_e.js");
|
|
21405
22121
|
const client = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
21406
22122
|
try {
|
|
21407
22123
|
const classification = await classifyRepoForSync(client, parsed.owner, parsed.repo, c.var.currentSite.id);
|
|
@@ -21447,6 +22163,84 @@ settingsRoutes.get("/github-sync", async (c) => {
|
|
|
21447
22163
|
})] })
|
|
21448
22164
|
});
|
|
21449
22165
|
});
|
|
22166
|
+
settingsRoutes.get("/telegram", async (c) => {
|
|
22167
|
+
const view = await readTelegramSettingsView(c);
|
|
22168
|
+
const streamUrl = getTelegramStatusStreamUrl(c);
|
|
22169
|
+
const navData = await getNavigationData(c);
|
|
22170
|
+
return renderPublicPage(c, {
|
|
22171
|
+
title: buildPageTitle("Telegram", navData.siteName),
|
|
22172
|
+
navData,
|
|
22173
|
+
content: /* @__PURE__ */ jsxDEV$1(Fragment$1, { children: [/* @__PURE__ */ jsxDEV$1(AdminBreadcrumb, {
|
|
22174
|
+
parent: breadcrumbLabel(c, "settings"),
|
|
22175
|
+
parentHref: publicPath(c, "/settings"),
|
|
22176
|
+
current: breadcrumbLabel(c, "telegram")
|
|
22177
|
+
}), /* @__PURE__ */ jsxDEV$1(TelegramContent, {
|
|
22178
|
+
view,
|
|
22179
|
+
sitePathPrefix: c.var.appConfig.sitePathPrefix,
|
|
22180
|
+
streamUrl
|
|
22181
|
+
})] })
|
|
22182
|
+
});
|
|
22183
|
+
});
|
|
22184
|
+
/**
|
|
22185
|
+
* Live status stream — swaps the connect view for the connected view the
|
|
22186
|
+
* moment a binding lands, so the user doesn't have to refresh after sending
|
|
22187
|
+
* the binding code to the bot. Same pattern as GitHub Sync's status stream:
|
|
22188
|
+
* subscribed via Datastar `data-init="@get(...)"`, each frame is a
|
|
22189
|
+
* `patchElements` with `mode: outer` on the stable `#telegram-status` id.
|
|
22190
|
+
*
|
|
22191
|
+
* The connected view ships without `data-init`, so the stream closes as
|
|
22192
|
+
* soon as we send the first "binding present" frame. A 5-minute cap bounds
|
|
22193
|
+
* an abandoned subscription.
|
|
22194
|
+
*/ settingsRoutes.get("/telegram/status/stream", async (c) => {
|
|
22195
|
+
const streamUrl = getTelegramStatusStreamUrl(c);
|
|
22196
|
+
const MAX_DURATION_MS = 300 * 1e3;
|
|
22197
|
+
const POLL_INTERVAL_MS = 1500;
|
|
22198
|
+
return sse(c, async (stream) => {
|
|
22199
|
+
const startedAt = Date.now();
|
|
22200
|
+
let lastHtml = null;
|
|
22201
|
+
while (true) {
|
|
22202
|
+
const view = await readTelegramSettingsView(c);
|
|
22203
|
+
const html = renderTelegramContentHtml(c, view, streamUrl);
|
|
22204
|
+
if (html !== lastHtml) {
|
|
22205
|
+
stream.patchElements(html, {
|
|
22206
|
+
mode: "outer",
|
|
22207
|
+
selector: "#telegram-status"
|
|
22208
|
+
});
|
|
22209
|
+
lastHtml = html;
|
|
22210
|
+
}
|
|
22211
|
+
if (view.binding) {
|
|
22212
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
22213
|
+
break;
|
|
22214
|
+
}
|
|
22215
|
+
if (Date.now() - startedAt >= MAX_DURATION_MS) break;
|
|
22216
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
22217
|
+
}
|
|
22218
|
+
});
|
|
22219
|
+
});
|
|
22220
|
+
settingsRoutes.post("/telegram/connect", async (c) => {
|
|
22221
|
+
if (getTelegramBotPool(c.env).length > 0) return dsToast("This deployment uses a managed Telegram bot. Connect with the binding code instead.", "error");
|
|
22222
|
+
const token = (await c.req.json()).token?.trim();
|
|
22223
|
+
if (!token) return dsToast("Paste your bot token to continue.", "error");
|
|
22224
|
+
const siteUrl = c.var.appConfig.siteUrl.replace(/\/+$/, "");
|
|
22225
|
+
try {
|
|
22226
|
+
await c.var.services.telegram.connectUserBot(token, siteUrl);
|
|
22227
|
+
} catch (err) {
|
|
22228
|
+
return dsToast(`Could not set up the bot: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
22229
|
+
}
|
|
22230
|
+
return dsRedirect(publicPath(c, "/settings/telegram"));
|
|
22231
|
+
});
|
|
22232
|
+
settingsRoutes.post("/telegram/remove-bot", async (c) => {
|
|
22233
|
+
await c.var.services.telegram.removeUserBot();
|
|
22234
|
+
return dsRedirect(publicPath(c, "/settings/telegram"));
|
|
22235
|
+
});
|
|
22236
|
+
settingsRoutes.post("/telegram/regenerate-code", async (c) => {
|
|
22237
|
+
await c.var.services.telegram.generateCode();
|
|
22238
|
+
return dsRedirect(publicPath(c, "/settings/telegram"));
|
|
22239
|
+
});
|
|
22240
|
+
settingsRoutes.post("/telegram/disconnect", async (c) => {
|
|
22241
|
+
await c.var.services.telegram.disconnect();
|
|
22242
|
+
return dsRedirect(publicPath(c, "/settings/telegram"));
|
|
22243
|
+
});
|
|
21450
22244
|
//#endregion
|
|
21451
22245
|
//#region src/ui/shared/EmptyState.tsx
|
|
21452
22246
|
/**
|
|
@@ -22480,6 +23274,194 @@ settingsApiRoutes.delete("/avatar", requireAuthApi(), async (c) => {
|
|
|
22480
23274
|
return c.json({ success: true });
|
|
22481
23275
|
});
|
|
22482
23276
|
//#endregion
|
|
23277
|
+
//#region src/lib/image-dimensions.ts
|
|
23278
|
+
/**
|
|
23279
|
+
* Parse intrinsic pixel dimensions from a raw image header buffer.
|
|
23280
|
+
*
|
|
23281
|
+
* Used as a server-side fallback when an upload client does not provide
|
|
23282
|
+
* `width` / `height`. Only the file header is needed — typically the first
|
|
23283
|
+
* few hundred bytes for PNG/JPEG/GIF/WebP, and up to ~64 KB for AVIF where
|
|
23284
|
+
* the `ispe` property can be nested inside a larger `meta` box.
|
|
23285
|
+
*
|
|
23286
|
+
* Returns `null` when the bytes are too short, the format is unsupported,
|
|
23287
|
+
* or the header is malformed.
|
|
23288
|
+
*
|
|
23289
|
+
* @param mimeType MIME type the upload was declared as (e.g. `image/png`).
|
|
23290
|
+
* @param bytes Bytes from the start of the file. Pass at least
|
|
23291
|
+
* {@link IMAGE_DIMENSION_PEEK_BYTES} for AVIF reliability; smaller is fine
|
|
23292
|
+
* for other formats.
|
|
23293
|
+
*/ function parseImageDimensions(mimeType, bytes) {
|
|
23294
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
23295
|
+
switch (mimeType) {
|
|
23296
|
+
case "image/png": return parsePng(view);
|
|
23297
|
+
case "image/jpeg":
|
|
23298
|
+
case "image/jpg": return parseJpeg(view);
|
|
23299
|
+
case "image/gif": return parseGif(view);
|
|
23300
|
+
case "image/webp": return parseWebp(view);
|
|
23301
|
+
case "image/avif": return parseIsoBmff(view, new Set(["avif", "avis"]));
|
|
23302
|
+
default: return null;
|
|
23303
|
+
}
|
|
23304
|
+
}
|
|
23305
|
+
/**
|
|
23306
|
+
* Recommended number of header bytes to feed into {@link parseImageDimensions}.
|
|
23307
|
+
*
|
|
23308
|
+
* Covers AVIF files with EXIF/ICC properties placed before the `ispe` box.
|
|
23309
|
+
* Smaller formats only inspect the first ~30 bytes.
|
|
23310
|
+
*/ var IMAGE_DIMENSION_PEEK_BYTES = 64 * 1024;
|
|
23311
|
+
function readChars(view, offset, length) {
|
|
23312
|
+
let out = "";
|
|
23313
|
+
for (let i = 0; i < length; i += 1) out += String.fromCharCode(view.getUint8(offset + i));
|
|
23314
|
+
return out;
|
|
23315
|
+
}
|
|
23316
|
+
function parsePng(view) {
|
|
23317
|
+
if (view.byteLength < 24) return null;
|
|
23318
|
+
const signature = [
|
|
23319
|
+
137,
|
|
23320
|
+
80,
|
|
23321
|
+
78,
|
|
23322
|
+
71,
|
|
23323
|
+
13,
|
|
23324
|
+
10,
|
|
23325
|
+
26,
|
|
23326
|
+
10
|
|
23327
|
+
];
|
|
23328
|
+
for (let i = 0; i < 8; i += 1) if (view.getUint8(i) !== signature[i]) return null;
|
|
23329
|
+
const width = view.getUint32(16, false);
|
|
23330
|
+
const height = view.getUint32(20, false);
|
|
23331
|
+
if (width === 0 || height === 0) return null;
|
|
23332
|
+
return {
|
|
23333
|
+
width,
|
|
23334
|
+
height
|
|
23335
|
+
};
|
|
23336
|
+
}
|
|
23337
|
+
function parseGif(view) {
|
|
23338
|
+
if (view.byteLength < 10) return null;
|
|
23339
|
+
if (view.getUint8(0) !== 71 || view.getUint8(1) !== 73 || view.getUint8(2) !== 70) return null;
|
|
23340
|
+
const width = view.getUint16(6, true);
|
|
23341
|
+
const height = view.getUint16(8, true);
|
|
23342
|
+
if (width === 0 || height === 0) return null;
|
|
23343
|
+
return {
|
|
23344
|
+
width,
|
|
23345
|
+
height
|
|
23346
|
+
};
|
|
23347
|
+
}
|
|
23348
|
+
function parseWebp(view) {
|
|
23349
|
+
if (view.byteLength < 16) return null;
|
|
23350
|
+
if (readChars(view, 0, 4) !== "RIFF") return null;
|
|
23351
|
+
if (readChars(view, 8, 4) !== "WEBP") return null;
|
|
23352
|
+
const chunk = readChars(view, 12, 4);
|
|
23353
|
+
if (chunk === "VP8 ") {
|
|
23354
|
+
if (view.byteLength < 30) return null;
|
|
23355
|
+
if (view.getUint8(23) !== 157 || view.getUint8(24) !== 1 || view.getUint8(25) !== 42) return null;
|
|
23356
|
+
const width = view.getUint16(26, true) & 16383;
|
|
23357
|
+
const height = view.getUint16(28, true) & 16383;
|
|
23358
|
+
if (width === 0 || height === 0) return null;
|
|
23359
|
+
return {
|
|
23360
|
+
width,
|
|
23361
|
+
height
|
|
23362
|
+
};
|
|
23363
|
+
}
|
|
23364
|
+
if (chunk === "VP8L") {
|
|
23365
|
+
if (view.byteLength < 25) return null;
|
|
23366
|
+
if (view.getUint8(20) !== 47) return null;
|
|
23367
|
+
const b0 = view.getUint8(21);
|
|
23368
|
+
const b1 = view.getUint8(22);
|
|
23369
|
+
const b2 = view.getUint8(23);
|
|
23370
|
+
const b3 = view.getUint8(24);
|
|
23371
|
+
return {
|
|
23372
|
+
width: 1 + ((b1 & 63) << 8 | b0),
|
|
23373
|
+
height: 1 + ((b3 & 15) << 10 | b2 << 2 | (b1 & 192) >> 6)
|
|
23374
|
+
};
|
|
23375
|
+
}
|
|
23376
|
+
if (chunk === "VP8X") {
|
|
23377
|
+
if (view.byteLength < 30) return null;
|
|
23378
|
+
return {
|
|
23379
|
+
width: 1 + (view.getUint8(24) | view.getUint8(25) << 8 | view.getUint8(26) << 16),
|
|
23380
|
+
height: 1 + (view.getUint8(27) | view.getUint8(28) << 8 | view.getUint8(29) << 16)
|
|
23381
|
+
};
|
|
23382
|
+
}
|
|
23383
|
+
return null;
|
|
23384
|
+
}
|
|
23385
|
+
function parseJpeg(view) {
|
|
23386
|
+
const length = view.byteLength;
|
|
23387
|
+
if (length < 4) return null;
|
|
23388
|
+
if (view.getUint8(0) !== 255 || view.getUint8(1) !== 216) return null;
|
|
23389
|
+
let i = 2;
|
|
23390
|
+
while (i < length) {
|
|
23391
|
+
while (i < length && view.getUint8(i) !== 255) i += 1;
|
|
23392
|
+
while (i < length && view.getUint8(i) === 255) i += 1;
|
|
23393
|
+
if (i >= length) return null;
|
|
23394
|
+
const marker = view.getUint8(i);
|
|
23395
|
+
i += 1;
|
|
23396
|
+
if (marker === 0 || marker === 1) continue;
|
|
23397
|
+
if (marker >= 208 && marker <= 217) continue;
|
|
23398
|
+
if (i + 2 > length) return null;
|
|
23399
|
+
const segLen = view.getUint16(i, false);
|
|
23400
|
+
if (segLen < 2) return null;
|
|
23401
|
+
if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) {
|
|
23402
|
+
if (i + 7 > length) return null;
|
|
23403
|
+
const height = view.getUint16(i + 3, false);
|
|
23404
|
+
const width = view.getUint16(i + 5, false);
|
|
23405
|
+
if (width === 0 || height === 0) return null;
|
|
23406
|
+
return {
|
|
23407
|
+
width,
|
|
23408
|
+
height
|
|
23409
|
+
};
|
|
23410
|
+
}
|
|
23411
|
+
i += segLen;
|
|
23412
|
+
}
|
|
23413
|
+
return null;
|
|
23414
|
+
}
|
|
23415
|
+
function readBox(view, pos) {
|
|
23416
|
+
if (pos + 8 > view.byteLength) return null;
|
|
23417
|
+
let size = view.getUint32(pos, false);
|
|
23418
|
+
const type = readChars(view, pos + 4, 4);
|
|
23419
|
+
if (size === 1) return null;
|
|
23420
|
+
if (size === 0) size = view.byteLength - pos;
|
|
23421
|
+
if (size < 8) return null;
|
|
23422
|
+
return {
|
|
23423
|
+
size,
|
|
23424
|
+
type,
|
|
23425
|
+
payloadOffset: pos + 8,
|
|
23426
|
+
end: pos + size
|
|
23427
|
+
};
|
|
23428
|
+
}
|
|
23429
|
+
function findChild(view, start, end, type) {
|
|
23430
|
+
let pos = start;
|
|
23431
|
+
while (pos < end) {
|
|
23432
|
+
const box = readBox(view, pos);
|
|
23433
|
+
if (!box) return null;
|
|
23434
|
+
if (box.type === type) return box;
|
|
23435
|
+
pos = box.end;
|
|
23436
|
+
}
|
|
23437
|
+
return null;
|
|
23438
|
+
}
|
|
23439
|
+
function parseIsoBmff(view, acceptedBrands) {
|
|
23440
|
+
const ftyp = readBox(view, 0);
|
|
23441
|
+
if (!ftyp || ftyp.type !== "ftyp") return null;
|
|
23442
|
+
let isAccepted = false;
|
|
23443
|
+
if (ftyp.payloadOffset + 4 <= ftyp.end) isAccepted = acceptedBrands.has(readChars(view, ftyp.payloadOffset, 4));
|
|
23444
|
+
for (let q = ftyp.payloadOffset + 8; !isAccepted && q + 4 <= ftyp.end; q += 4) if (acceptedBrands.has(readChars(view, q, 4))) isAccepted = true;
|
|
23445
|
+
if (!isAccepted) return null;
|
|
23446
|
+
const meta = findChild(view, ftyp.end, view.byteLength, "meta");
|
|
23447
|
+
if (!meta) return null;
|
|
23448
|
+
const iprp = findChild(view, meta.payloadOffset + 4, meta.end, "iprp");
|
|
23449
|
+
if (!iprp) return null;
|
|
23450
|
+
const ipco = findChild(view, iprp.payloadOffset, iprp.end, "ipco");
|
|
23451
|
+
if (!ipco) return null;
|
|
23452
|
+
const ispe = findChild(view, ipco.payloadOffset, ipco.end, "ispe");
|
|
23453
|
+
if (!ispe) return null;
|
|
23454
|
+
const widthOffset = ispe.payloadOffset + 4;
|
|
23455
|
+
if (widthOffset + 8 > view.byteLength) return null;
|
|
23456
|
+
const width = view.getUint32(widthOffset, false);
|
|
23457
|
+
const height = view.getUint32(widthOffset + 4, false);
|
|
23458
|
+
if (width === 0 || height === 0) return null;
|
|
23459
|
+
return {
|
|
23460
|
+
width,
|
|
23461
|
+
height
|
|
23462
|
+
};
|
|
23463
|
+
}
|
|
23464
|
+
//#endregion
|
|
22483
23465
|
//#region src/lib/api-media.ts
|
|
22484
23466
|
function toApiMedia(media, appConfig) {
|
|
22485
23467
|
const { posterKey: _posterKey, storageKey: _storageKey, ...rest } = media;
|
|
@@ -22643,6 +23625,16 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
22643
23625
|
const altRaw = formData.get("alt") || void 0;
|
|
22644
23626
|
const blurhashRaw = formData.get("blurhash") || void 0;
|
|
22645
23627
|
const waveformRaw = formData.get("waveform") || void 0;
|
|
23628
|
+
let width = widthRaw && widthRaw > 0 ? widthRaw : void 0;
|
|
23629
|
+
let height = heightRaw && heightRaw > 0 ? heightRaw : void 0;
|
|
23630
|
+
if ((!width || !height) && file.type.startsWith("image/")) try {
|
|
23631
|
+
const headerBytes = new Uint8Array(await file.slice(0, IMAGE_DIMENSION_PEEK_BYTES).arrayBuffer());
|
|
23632
|
+
const dimensions = parseImageDimensions(file.type, headerBytes);
|
|
23633
|
+
if (dimensions) {
|
|
23634
|
+
width ??= dimensions.width;
|
|
23635
|
+
height ??= dimensions.height;
|
|
23636
|
+
}
|
|
23637
|
+
} catch {}
|
|
22646
23638
|
let posterKey;
|
|
22647
23639
|
const posterFile = formData.get("poster");
|
|
22648
23640
|
if (posterFile && file.type.startsWith("video/")) {
|
|
@@ -22662,8 +23654,8 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
22662
23654
|
size: file.size,
|
|
22663
23655
|
storageKey,
|
|
22664
23656
|
provider: c.var.appConfig.storageDriver,
|
|
22665
|
-
width
|
|
22666
|
-
height
|
|
23657
|
+
width,
|
|
23658
|
+
height,
|
|
22667
23659
|
durationSeconds: durationSecondsRaw && durationSecondsRaw > 0 ? durationSecondsRaw : void 0,
|
|
22668
23660
|
alt: altRaw?.trim() || void 0,
|
|
22669
23661
|
blurhash: blurhashRaw && blurhashRaw.length < 200 ? blurhashRaw : void 0,
|
|
@@ -23527,7 +24519,11 @@ multipartUploadApiRoutes.post("/:id/complete", async (c) => {
|
|
|
23527
24519
|
throw error;
|
|
23528
24520
|
}
|
|
23529
24521
|
await storage.completeMultipartUpload(data.storageKey, data.uploadId, data.parts);
|
|
23530
|
-
const
|
|
24522
|
+
const signaturePeekLength = getStoredUploadSignaturePeekLength(data.contentType);
|
|
24523
|
+
let width = data.width && data.width > 0 ? data.width : void 0;
|
|
24524
|
+
let height = data.height && data.height > 0 ? data.height : void 0;
|
|
24525
|
+
const needsDimensionSniff = (!width || !height) && data.contentType.startsWith("image/");
|
|
24526
|
+
const peekLength = needsDimensionSniff ? Math.max(signaturePeekLength, IMAGE_DIMENSION_PEEK_BYTES) : signaturePeekLength;
|
|
23531
24527
|
if (peekLength > 0) {
|
|
23532
24528
|
const object = await storage.get(data.storageKey, { range: {
|
|
23533
24529
|
offset: 0,
|
|
@@ -23535,11 +24531,20 @@ multipartUploadApiRoutes.post("/:id/complete", async (c) => {
|
|
|
23535
24531
|
} });
|
|
23536
24532
|
if (!object) throw new ValidationError("The uploaded file could not be found.");
|
|
23537
24533
|
const bytes = new Uint8Array(await new Response(object.body).arrayBuffer());
|
|
23538
|
-
|
|
23539
|
-
|
|
23540
|
-
|
|
23541
|
-
|
|
23542
|
-
|
|
24534
|
+
if (signaturePeekLength > 0) {
|
|
24535
|
+
const signatureError = validateStoredUploadSignature(data.contentType, bytes.subarray(0, signaturePeekLength));
|
|
24536
|
+
if (signatureError) {
|
|
24537
|
+
await storage.delete(data.storageKey).catch(() => {});
|
|
24538
|
+
if (data.posterKey) await storage.delete(data.posterKey).catch(() => {});
|
|
24539
|
+
throw new ValidationError(signatureError);
|
|
24540
|
+
}
|
|
24541
|
+
}
|
|
24542
|
+
if (needsDimensionSniff) {
|
|
24543
|
+
const dimensions = parseImageDimensions(data.contentType, bytes);
|
|
24544
|
+
if (dimensions) {
|
|
24545
|
+
width ??= dimensions.width;
|
|
24546
|
+
height ??= dimensions.height;
|
|
24547
|
+
}
|
|
23543
24548
|
}
|
|
23544
24549
|
}
|
|
23545
24550
|
const media = await c.var.services.media.create({
|
|
@@ -23550,8 +24555,8 @@ multipartUploadApiRoutes.post("/:id/complete", async (c) => {
|
|
|
23550
24555
|
size: data.size,
|
|
23551
24556
|
storageKey: data.storageKey,
|
|
23552
24557
|
provider: c.var.appConfig.storageDriver,
|
|
23553
|
-
width
|
|
23554
|
-
height
|
|
24558
|
+
width,
|
|
24559
|
+
height,
|
|
23555
24560
|
durationSeconds: data.durationSeconds && data.durationSeconds > 0 ? data.durationSeconds : void 0,
|
|
23556
24561
|
blurhash: data.blurhash,
|
|
23557
24562
|
waveform: data.waveform,
|
|
@@ -25087,103 +26092,750 @@ githubSyncWebhookRoutes.post("/app-webhook", async (c) => {
|
|
|
25087
26092
|
affectedSites: affected.length
|
|
25088
26093
|
});
|
|
25089
26094
|
}
|
|
25090
|
-
return c.json({
|
|
25091
|
-
ok: true,
|
|
25092
|
-
skipped: event ?? "no event header"
|
|
26095
|
+
return c.json({
|
|
26096
|
+
ok: true,
|
|
26097
|
+
skipped: event ?? "no event header"
|
|
26098
|
+
});
|
|
26099
|
+
});
|
|
26100
|
+
var githubSyncAdminRoutes = new Hono();
|
|
26101
|
+
var ConnectSchema = z.object({
|
|
26102
|
+
token: z.string().min(1),
|
|
26103
|
+
repo: z.string().min(3)
|
|
26104
|
+
});
|
|
26105
|
+
githubSyncAdminRoutes.post("/setup", requireAuthApi(), async (c) => {
|
|
26106
|
+
if (getGitHubAppConfig(c.env)) return c.json({ error: "This deployment uses GitHub App authentication. Use the App install flow instead." }, 400);
|
|
26107
|
+
const body = parseValidated(ConnectSchema, await c.req.json());
|
|
26108
|
+
const parsed = parseRepoSlug(body.repo);
|
|
26109
|
+
if (!parsed) return c.json({ error: "Invalid repository format. Use owner/repo." }, 400);
|
|
26110
|
+
const client = createGitHubClient(body.token);
|
|
26111
|
+
try {
|
|
26112
|
+
await client.getRepo(parsed.owner, parsed.repo);
|
|
26113
|
+
} catch {
|
|
26114
|
+
return c.json({ error: "Could not access the repository. Check your token and repo name." }, 400);
|
|
26115
|
+
}
|
|
26116
|
+
await c.var.services.settings.set("GITHUB_SYNC_TOKEN", body.token);
|
|
26117
|
+
await c.var.services.settings.set("GITHUB_SYNC_REPO", body.repo);
|
|
26118
|
+
await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "pat");
|
|
26119
|
+
await c.var.services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", "");
|
|
26120
|
+
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
26121
|
+
const callbackUrl = `${c.var.appConfig.siteUrl}/api/github-sync/webhook`;
|
|
26122
|
+
const { webhookId } = await createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
26123
|
+
storage: c.var.storage,
|
|
26124
|
+
githubApp: getGitHubAppConfig(c.env)
|
|
26125
|
+
}).setupWebhook(callbackUrl);
|
|
26126
|
+
return c.json({
|
|
26127
|
+
ok: true,
|
|
26128
|
+
repo: body.repo,
|
|
26129
|
+
webhookId
|
|
26130
|
+
});
|
|
26131
|
+
});
|
|
26132
|
+
githubSyncAdminRoutes.post("/push", requireAuthApi(), async (c) => {
|
|
26133
|
+
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
26134
|
+
storage: c.var.storage,
|
|
26135
|
+
githubApp: getGitHubAppConfig(c.env)
|
|
26136
|
+
});
|
|
26137
|
+
if (!await syncService.getConfig()) return c.json({ error: "GitHub Sync not configured" }, 400);
|
|
26138
|
+
const { commitSha } = await syncService.pushFullSync();
|
|
26139
|
+
return c.json({
|
|
26140
|
+
ok: true,
|
|
26141
|
+
commitSha
|
|
26142
|
+
});
|
|
26143
|
+
});
|
|
26144
|
+
githubSyncAdminRoutes.delete("/", requireAuthApi(), async (c) => {
|
|
26145
|
+
await createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
26146
|
+
storage: c.var.storage,
|
|
26147
|
+
githubApp: getGitHubAppConfig(c.env)
|
|
26148
|
+
}).teardownWebhook();
|
|
26149
|
+
return c.json({ ok: true });
|
|
26150
|
+
});
|
|
26151
|
+
githubSyncAdminRoutes.get("/status", requireAuthApi(), async (c) => {
|
|
26152
|
+
const status = await readGitHubSyncStatus(c);
|
|
26153
|
+
return c.json(status);
|
|
26154
|
+
});
|
|
26155
|
+
/**
|
|
26156
|
+
* Live status stream — drives the settings page's status card while a push
|
|
26157
|
+
* is running. Subscribed to via Datastar's `data-init="@get(...)"` on the
|
|
26158
|
+
* card element; each frame is a `patchElements` with `mode: outer` on the
|
|
26159
|
+
* stable id `#github-sync-status`.
|
|
26160
|
+
*
|
|
26161
|
+
* Loop ends as soon as `pending` is false (we still send one final frame
|
|
26162
|
+
* so the card flips from "Syncing…" to "N ago"), or after the hard budget
|
|
26163
|
+
* below if a sync is genuinely stuck — `isSyncPending` already self-heals
|
|
26164
|
+
* after the 10-minute stale window in `github-sync-trigger.ts`, which is
|
|
26165
|
+
* the upper bound callers rely on.
|
|
26166
|
+
*/ githubSyncAdminRoutes.get("/status/stream", requireAuthApi(), async (c) => {
|
|
26167
|
+
const streamUrl = getSyncStatusStreamUrl(c);
|
|
26168
|
+
const MAX_DURATION_MS = 300 * 1e3;
|
|
26169
|
+
const POLL_INTERVAL_MS = 1500;
|
|
26170
|
+
return sse(c, async (stream) => {
|
|
26171
|
+
const startedAt = Date.now();
|
|
26172
|
+
let lastHtml = null;
|
|
26173
|
+
while (true) {
|
|
26174
|
+
const status = await readGitHubSyncStatus(c);
|
|
26175
|
+
const html = renderStatusCardHtml(c, status, streamUrl);
|
|
26176
|
+
if (html !== lastHtml) {
|
|
26177
|
+
stream.patchElements(html, {
|
|
26178
|
+
mode: "outer",
|
|
26179
|
+
selector: "#github-sync-status"
|
|
26180
|
+
});
|
|
26181
|
+
lastHtml = html;
|
|
26182
|
+
}
|
|
26183
|
+
if (!status.pending) {
|
|
26184
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
26185
|
+
break;
|
|
26186
|
+
}
|
|
26187
|
+
if (Date.now() - startedAt >= MAX_DURATION_MS) break;
|
|
26188
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
26189
|
+
}
|
|
26190
|
+
});
|
|
26191
|
+
});
|
|
26192
|
+
//#endregion
|
|
26193
|
+
//#region src/lib/crypto.ts
|
|
26194
|
+
/**
|
|
26195
|
+
* Compare two byte arrays using a constant-time loop.
|
|
26196
|
+
*
|
|
26197
|
+
* This avoids relying on runtime-specific crypto helpers so the same behavior
|
|
26198
|
+
* works in Node.js and Workers.
|
|
26199
|
+
*
|
|
26200
|
+
* @param a - First byte array
|
|
26201
|
+
* @param b - Second byte array
|
|
26202
|
+
* @returns `true` when the inputs are byte-for-byte identical
|
|
26203
|
+
*
|
|
26204
|
+
* @example
|
|
26205
|
+
* ```ts
|
|
26206
|
+
* const isEqual = timingSafeEqualBytes(
|
|
26207
|
+
* new TextEncoder().encode("abc"),
|
|
26208
|
+
* new TextEncoder().encode("abc"),
|
|
26209
|
+
* );
|
|
26210
|
+
* ```
|
|
26211
|
+
*/ function timingSafeEqualBytes(a, b) {
|
|
26212
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
26213
|
+
let mismatch = 0;
|
|
26214
|
+
for (let index = 0; index < a.byteLength; index += 1) mismatch |= (a[index] ?? 0) ^ (b[index] ?? 0);
|
|
26215
|
+
return mismatch === 0;
|
|
26216
|
+
}
|
|
26217
|
+
/**
|
|
26218
|
+
* Compare two strings using a constant-time byte comparison.
|
|
26219
|
+
*
|
|
26220
|
+
* @param a - First string
|
|
26221
|
+
* @param b - Second string
|
|
26222
|
+
* @returns `true` when the UTF-8 byte sequences are identical
|
|
26223
|
+
*
|
|
26224
|
+
* @example
|
|
26225
|
+
* ```ts
|
|
26226
|
+
* const isEqual = timingSafeEqualText("token-a", "token-b");
|
|
26227
|
+
* ```
|
|
26228
|
+
*/ function timingSafeEqualText(a, b) {
|
|
26229
|
+
const encoder = new TextEncoder();
|
|
26230
|
+
return timingSafeEqualBytes(encoder.encode(a), encoder.encode(b));
|
|
26231
|
+
}
|
|
26232
|
+
//#endregion
|
|
26233
|
+
//#region src/lib/telegram-entities.ts
|
|
26234
|
+
/**
|
|
26235
|
+
* Telegram message entities → CommonMark.
|
|
26236
|
+
*
|
|
26237
|
+
* Telegram clients ship rich text as a plain `text` string plus an `entities`
|
|
26238
|
+
* array of `{type, offset, length}` spans. Jant stores post bodies as
|
|
26239
|
+
* CommonMark, so the webhook needs to fold the entity styling back into the
|
|
26240
|
+
* text before saving.
|
|
26241
|
+
*
|
|
26242
|
+
* Two design choices worth knowing:
|
|
26243
|
+
*
|
|
26244
|
+
* 1. **Top-level text passes through verbatim**, so anything the user typed as
|
|
26245
|
+
* literal markdown (`**foo**`, `# Heading`, …) lands in the post unchanged.
|
|
26246
|
+
* Only text *inside* an entity span is markdown-escaped, because that text
|
|
26247
|
+
* is then wrapped in delimiters and we don't want stray `*` / `_` / `` ` ``
|
|
26248
|
+
* to break out of the styled span.
|
|
26249
|
+
* 2. **Unsupported entity types degrade to plain text** rather than throwing.
|
|
26250
|
+
* Underline, spoiler, custom emoji, and the auto-detected `url` /
|
|
26251
|
+
* `hashtag` / `mention` family have no CommonMark equivalent that adds
|
|
26252
|
+
* information beyond the raw text — markdown auto-links bare URLs anyway.
|
|
26253
|
+
*
|
|
26254
|
+
* Telegram offsets are UTF-16 code units, which is exactly what JavaScript
|
|
26255
|
+
* string indexing uses, so no transcoding is needed.
|
|
26256
|
+
*/ /**
|
|
26257
|
+
* Converts a Telegram message's `text` + `entities` into CommonMark.
|
|
26258
|
+
*
|
|
26259
|
+
* @param text - The raw `message.text` (or `message.caption`)
|
|
26260
|
+
* @param entities - The parallel `message.entities` array, may be empty
|
|
26261
|
+
* @returns The text rewritten as CommonMark; unchanged when `entities` is empty
|
|
26262
|
+
* @example
|
|
26263
|
+
* entitiesToMarkdown("hello world", [
|
|
26264
|
+
* { type: "bold", offset: 6, length: 5 },
|
|
26265
|
+
* ]); // "hello **world**"
|
|
26266
|
+
*/ function entitiesToMarkdown(text, entities) {
|
|
26267
|
+
if (!entities || entities.length === 0) return text;
|
|
26268
|
+
return renderRoots(text, buildTree(entities));
|
|
26269
|
+
}
|
|
26270
|
+
/**
|
|
26271
|
+
* Groups entities into a forest by containment.
|
|
26272
|
+
*
|
|
26273
|
+
* Telegram guarantees entities are either nested or disjoint — they never
|
|
26274
|
+
* partially overlap — so a simple "find the nearest enclosing entity" pass is
|
|
26275
|
+
* enough to recover the tree.
|
|
26276
|
+
*/ function buildTree(entities) {
|
|
26277
|
+
const nodes = [...entities].sort((a, b) => a.offset - b.offset || b.length - a.length).map((entity) => ({
|
|
26278
|
+
entity,
|
|
26279
|
+
children: []
|
|
26280
|
+
}));
|
|
26281
|
+
const roots = [];
|
|
26282
|
+
for (const [i, node] of nodes.entries()) {
|
|
26283
|
+
const nodeEnd = node.entity.offset + node.entity.length;
|
|
26284
|
+
let parent = null;
|
|
26285
|
+
for (const cand of nodes.slice(0, i).reverse()) {
|
|
26286
|
+
const candEnd = cand.entity.offset + cand.entity.length;
|
|
26287
|
+
if (cand.entity.offset <= node.entity.offset && candEnd >= nodeEnd) {
|
|
26288
|
+
parent = cand;
|
|
26289
|
+
break;
|
|
26290
|
+
}
|
|
26291
|
+
}
|
|
26292
|
+
if (parent) parent.children.push(node);
|
|
26293
|
+
else roots.push(node);
|
|
26294
|
+
}
|
|
26295
|
+
return roots;
|
|
26296
|
+
}
|
|
26297
|
+
function renderRoots(text, roots) {
|
|
26298
|
+
return renderRange(text, 0, text.length, roots, { escapeGaps: false });
|
|
26299
|
+
}
|
|
26300
|
+
/**
|
|
26301
|
+
* Emits the substring `[start, end)` from `text`, splicing in rendered child
|
|
26302
|
+
* entities and (optionally) escaping the gaps between them.
|
|
26303
|
+
*
|
|
26304
|
+
* @param escapeGaps - True when this range is itself inside an entity span,
|
|
26305
|
+
* so any stray markdown chars would otherwise leak out of the wrapping
|
|
26306
|
+
* delimiters. False at the top level so user-typed markdown is preserved.
|
|
26307
|
+
*/ function renderRange(text, start, end, children, options) {
|
|
26308
|
+
const sorted = [...children].sort((a, b) => a.entity.offset - b.entity.offset);
|
|
26309
|
+
let out = "";
|
|
26310
|
+
let cursor = start;
|
|
26311
|
+
for (const child of sorted) {
|
|
26312
|
+
if (child.entity.offset > cursor) {
|
|
26313
|
+
const gap = text.slice(cursor, child.entity.offset);
|
|
26314
|
+
out += options.escapeGaps ? escapeInline(gap) : gap;
|
|
26315
|
+
}
|
|
26316
|
+
out += renderNode(text, child);
|
|
26317
|
+
cursor = child.entity.offset + child.entity.length;
|
|
26318
|
+
}
|
|
26319
|
+
if (cursor < end) {
|
|
26320
|
+
const tail = text.slice(cursor, end);
|
|
26321
|
+
out += options.escapeGaps ? escapeInline(tail) : tail;
|
|
26322
|
+
}
|
|
26323
|
+
return out;
|
|
26324
|
+
}
|
|
26325
|
+
function renderNode(text, node) {
|
|
26326
|
+
const { entity, children } = node;
|
|
26327
|
+
const spanStart = entity.offset;
|
|
26328
|
+
const spanEnd = entity.offset + entity.length;
|
|
26329
|
+
const raw = text.slice(spanStart, spanEnd);
|
|
26330
|
+
switch (entity.type) {
|
|
26331
|
+
case "bold": return `**${renderInline(text, spanStart, spanEnd, children)}**`;
|
|
26332
|
+
case "italic": return `*${renderInline(text, spanStart, spanEnd, children)}*`;
|
|
26333
|
+
case "strikethrough": return `~~${renderInline(text, spanStart, spanEnd, children)}~~`;
|
|
26334
|
+
case "code": return wrapInlineCode(raw);
|
|
26335
|
+
case "pre": return wrapCodeBlock(raw, entity.language);
|
|
26336
|
+
case "text_link": return renderTextLink(text, spanStart, spanEnd, children, entity.url);
|
|
26337
|
+
case "blockquote":
|
|
26338
|
+
case "expandable_blockquote": return renderBlockquote(text, spanStart, spanEnd, children);
|
|
26339
|
+
default: return renderInline(text, spanStart, spanEnd, children);
|
|
26340
|
+
}
|
|
26341
|
+
}
|
|
26342
|
+
function renderInline(text, start, end, children) {
|
|
26343
|
+
return renderRange(text, start, end, children, { escapeGaps: true });
|
|
26344
|
+
}
|
|
26345
|
+
function renderTextLink(text, start, end, children, url) {
|
|
26346
|
+
const label = renderInline(text, start, end, children);
|
|
26347
|
+
if (!url) return label;
|
|
26348
|
+
return `[${label.replace(/[\\\]]/g, "\\$&")}](${escapeLinkUrl(url)})`;
|
|
26349
|
+
}
|
|
26350
|
+
function renderBlockquote(text, start, end, children) {
|
|
26351
|
+
return renderInline(text, start, end, children).split("\n").map((line) => `> ${line}`).join("\n");
|
|
26352
|
+
}
|
|
26353
|
+
/**
|
|
26354
|
+
* Wraps content in the shortest backtick fence that doesn't collide with a
|
|
26355
|
+
* backtick run already present in the content. Required for any `code` span
|
|
26356
|
+
* containing backticks.
|
|
26357
|
+
*/ function wrapInlineCode(content) {
|
|
26358
|
+
const longestRun = longestBacktickRun(content);
|
|
26359
|
+
const fence = "`".repeat(longestRun + 1);
|
|
26360
|
+
const pad = content.startsWith("`") || content.endsWith("`") ? " " : "";
|
|
26361
|
+
return `${fence}${pad}${content}${pad}${fence}`;
|
|
26362
|
+
}
|
|
26363
|
+
function wrapCodeBlock(content, language) {
|
|
26364
|
+
const fenceLen = Math.max(3, longestBacktickRun(content) + 1);
|
|
26365
|
+
const fence = "`".repeat(fenceLen);
|
|
26366
|
+
return `${fence}${language ? language : ""}\n${content}\n${fence}`;
|
|
26367
|
+
}
|
|
26368
|
+
function longestBacktickRun(s) {
|
|
26369
|
+
let max = 0;
|
|
26370
|
+
const matches = s.match(/`+/g);
|
|
26371
|
+
if (!matches) return 0;
|
|
26372
|
+
for (const m of matches) if (m.length > max) max = m.length;
|
|
26373
|
+
return max;
|
|
26374
|
+
}
|
|
26375
|
+
/**
|
|
26376
|
+
* Escapes the markdown delimiters that would otherwise let the inner text
|
|
26377
|
+
* break out of a styled span. We deliberately escape only the characters that
|
|
26378
|
+
* carry inline meaning here — `*`, `_`, `` ` ``, `~`, `[`, `]`, `\` — rather
|
|
26379
|
+
* than the full CommonMark punctuation set, so emoji-adjacent punctuation and
|
|
26380
|
+
* other harmless characters stay readable.
|
|
26381
|
+
*/ function escapeInline(s) {
|
|
26382
|
+
return s.replace(/[\\`*_~[\]]/g, "\\$&");
|
|
26383
|
+
}
|
|
26384
|
+
function escapeLinkUrl(url) {
|
|
26385
|
+
return url.replace(/[\\()]/g, "\\$&");
|
|
26386
|
+
}
|
|
26387
|
+
//#endregion
|
|
26388
|
+
//#region src/routes/api/telegram.ts
|
|
26389
|
+
/**
|
|
26390
|
+
* Telegram Webhook Route
|
|
26391
|
+
*
|
|
26392
|
+
* Receives Telegram bot updates and turns text messages into Notes.
|
|
26393
|
+
*
|
|
26394
|
+
* One route serves both deployment modes. It is host-agnostic: it never
|
|
26395
|
+
* trusts the request hostname (in hosted mode the update is forwarded through
|
|
26396
|
+
* the control plane and arrives without a tenant host). The target site is
|
|
26397
|
+
* resolved from the binding tables — by `(botId, telegramUserId)` for a normal
|
|
26398
|
+
* message, or by the pending binding `code` for a `/start <code>`.
|
|
26399
|
+
*
|
|
26400
|
+
* Authentication is the Telegram `secret_token` echoed in the
|
|
26401
|
+
* `X-Telegram-Bot-Api-Secret-Token` header — the same auth model whether the
|
|
26402
|
+
* webhook is delivered straight to a self-hosted site or forwarded by the
|
|
26403
|
+
* hosted control plane.
|
|
26404
|
+
*/
|
|
26405
|
+
/**
|
|
26406
|
+
* How long to hold each album item in the buffer before claiming the group.
|
|
26407
|
+
*
|
|
26408
|
+
* Telegram delivers album webhook updates within tens of milliseconds of one
|
|
26409
|
+
* another, so 2 s is generous enough to collect every item without making the
|
|
26410
|
+
* publish noticeably slow. The wait runs in-line on the webhook handler, so a
|
|
26411
|
+
* shorter value risks splitting an album into multiple posts and a longer one
|
|
26412
|
+
* delays the bot's "Posted." reply.
|
|
26413
|
+
*/ var ALBUM_BUFFER_DELAY_MS = 2e3;
|
|
26414
|
+
/**
|
|
26415
|
+
* The Telegram webhook intentionally bypasses the site-resolution middleware
|
|
26416
|
+
* chain, so `c.var.appConfig` is not populated here. The two upload settings
|
|
26417
|
+
* the media flow needs are env-driven anyway, so read them straight from the
|
|
26418
|
+
* bindings without rebuilding the full appConfig.
|
|
26419
|
+
*/ function uploadConfigFromEnv(env) {
|
|
26420
|
+
const maxFileSizeMB = parseInt(getEnvString(env, "UPLOAD_MAX_FILE_SIZE_MB") ?? "500", 10) || 500;
|
|
26421
|
+
return {
|
|
26422
|
+
storageDriver: getConfiguredStorageDriver(env),
|
|
26423
|
+
maxFileSizeMB
|
|
26424
|
+
};
|
|
26425
|
+
}
|
|
26426
|
+
var telegramWebhookRoutes = new Hono();
|
|
26427
|
+
/**
|
|
26428
|
+
* Bot id → username cache. `getMe` results are stable for a bot's lifetime,
|
|
26429
|
+
* so this avoids an API round-trip per webhook when building deep links.
|
|
26430
|
+
*/ var botUsernameCache = /* @__PURE__ */ new Map();
|
|
26431
|
+
async function resolveBotUsername(botId, token) {
|
|
26432
|
+
const cached = botUsernameCache.get(botId);
|
|
26433
|
+
if (cached) return cached;
|
|
26434
|
+
try {
|
|
26435
|
+
const identity = await getMe(token);
|
|
26436
|
+
if (identity.username) botUsernameCache.set(botId, identity.username);
|
|
26437
|
+
return identity.username;
|
|
26438
|
+
} catch {
|
|
26439
|
+
return "";
|
|
26440
|
+
}
|
|
26441
|
+
}
|
|
26442
|
+
/** Resolves the bot token + expected webhook secret for an incoming request. */ async function resolveBot(c, botId) {
|
|
26443
|
+
const pool = getTelegramBotPool(c.env);
|
|
26444
|
+
if (pool.length > 0) {
|
|
26445
|
+
const bot = pool.find((entry) => entry.botId === botId);
|
|
26446
|
+
const secret = getTelegramWebhookSecret(c.env);
|
|
26447
|
+
if (!bot || !secret) return null;
|
|
26448
|
+
return {
|
|
26449
|
+
token: bot.token,
|
|
26450
|
+
secret
|
|
26451
|
+
};
|
|
26452
|
+
}
|
|
26453
|
+
const settings = c.var.services.settings;
|
|
26454
|
+
if (await settings.get("TELEGRAM_BOT_ID") !== botId) return null;
|
|
26455
|
+
const token = await settings.get("TELEGRAM_BOT_TOKEN");
|
|
26456
|
+
const secret = await settings.get("TELEGRAM_BOT_WEBHOOK_SECRET");
|
|
26457
|
+
if (!token || !secret) return null;
|
|
26458
|
+
return {
|
|
26459
|
+
token,
|
|
26460
|
+
secret
|
|
26461
|
+
};
|
|
26462
|
+
}
|
|
26463
|
+
async function siteName(c, siteId) {
|
|
26464
|
+
const name = await c.var.servicesForSite(siteId).settings.get("SITE_NAME");
|
|
26465
|
+
return name && name.trim() ? name.trim() : "your site";
|
|
26466
|
+
}
|
|
26467
|
+
telegramWebhookRoutes.post("/webhook/:botId", async (c) => {
|
|
26468
|
+
const botId = c.req.param("botId");
|
|
26469
|
+
const bot = await resolveBot(c, botId);
|
|
26470
|
+
if (!bot) return c.json({ error: "Unknown bot" }, 404);
|
|
26471
|
+
if (!timingSafeEqualText(c.req.header("X-Telegram-Bot-Api-Secret-Token") ?? "", bot.secret)) return c.json({ error: "Invalid secret token" }, 401);
|
|
26472
|
+
let update;
|
|
26473
|
+
try {
|
|
26474
|
+
update = await c.req.json();
|
|
26475
|
+
} catch {
|
|
26476
|
+
return c.json({ error: "Invalid JSON payload" }, 400);
|
|
26477
|
+
}
|
|
26478
|
+
try {
|
|
26479
|
+
await processUpdate(c, update, botId, bot.token);
|
|
26480
|
+
} catch (err) {
|
|
26481
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26482
|
+
console.error(`[Jant] Telegram webhook error: ${message}`);
|
|
26483
|
+
const chatId = update.message?.chat.id ?? update.callback_query?.message?.chat.id;
|
|
26484
|
+
if (chatId) await sendMessage(bot.token, chatId, `Couldn't process that message: ${message}`).catch(() => void 0);
|
|
26485
|
+
}
|
|
26486
|
+
return c.json({ ok: true });
|
|
26487
|
+
});
|
|
26488
|
+
async function processUpdate(c, update, botId, botToken) {
|
|
26489
|
+
const telegram = c.var.services.telegram;
|
|
26490
|
+
if (update.callback_query) {
|
|
26491
|
+
const query = update.callback_query;
|
|
26492
|
+
await answerCallbackQuery(botToken, query.id);
|
|
26493
|
+
const chatId = query.message?.chat.id;
|
|
26494
|
+
if (!chatId) return;
|
|
26495
|
+
const [action, code] = (query.data ?? "").split(":");
|
|
26496
|
+
const pending = code ? await telegram.resolvePendingCode(code) : null;
|
|
26497
|
+
if (!pending) {
|
|
26498
|
+
await sendMessage(botToken, chatId, "That binding code expired. Open Telegram settings in Jant for a fresh one.");
|
|
26499
|
+
return;
|
|
26500
|
+
}
|
|
26501
|
+
if (action === "rebind") {
|
|
26502
|
+
await telegram.bindAccount({
|
|
26503
|
+
siteId: pending.siteId,
|
|
26504
|
+
botId,
|
|
26505
|
+
telegramUserId: String(query.from.id),
|
|
26506
|
+
telegramUsername: query.from.username ?? null
|
|
26507
|
+
});
|
|
26508
|
+
await sendMessage(botToken, chatId, `Connected to ${await siteName(c, pending.siteId)}. Send me any text and I'll post it as a note.`);
|
|
26509
|
+
}
|
|
26510
|
+
return;
|
|
26511
|
+
}
|
|
26512
|
+
const message = update.message;
|
|
26513
|
+
if (!message?.from) return;
|
|
26514
|
+
const chatId = message.chat.id;
|
|
26515
|
+
const telegramUserId = String(message.from.id);
|
|
26516
|
+
const text = (message.text ?? "").trim();
|
|
26517
|
+
if (text === "/start" || text.startsWith("/start ")) {
|
|
26518
|
+
const code = text.slice(6).trim();
|
|
26519
|
+
if (!code) {
|
|
26520
|
+
await sendMessage(botToken, chatId, "Open Telegram settings in Jant and tap Connect to link this chat.");
|
|
26521
|
+
return;
|
|
26522
|
+
}
|
|
26523
|
+
await handleStart(c, {
|
|
26524
|
+
botId,
|
|
26525
|
+
botToken,
|
|
26526
|
+
chatId,
|
|
26527
|
+
code,
|
|
26528
|
+
telegramUserId,
|
|
26529
|
+
telegramUsername: message.from.username ?? null
|
|
26530
|
+
});
|
|
26531
|
+
return;
|
|
26532
|
+
}
|
|
26533
|
+
const binding = await telegram.findBindingByUser(botId, telegramUserId);
|
|
26534
|
+
if (!binding) {
|
|
26535
|
+
if (/^[0-9a-z]+$/.test(text) && text.length <= 24) {
|
|
26536
|
+
if (await telegram.resolvePendingCode(text)) {
|
|
26537
|
+
await handleStart(c, {
|
|
26538
|
+
botId,
|
|
26539
|
+
botToken,
|
|
26540
|
+
chatId,
|
|
26541
|
+
code: text,
|
|
26542
|
+
telegramUserId,
|
|
26543
|
+
telegramUsername: message.from.username ?? null
|
|
26544
|
+
});
|
|
26545
|
+
return;
|
|
26546
|
+
}
|
|
26547
|
+
}
|
|
26548
|
+
await sendMessage(botToken, chatId, "This chat isn't connected yet. Open Telegram settings in Jant to get a binding code.");
|
|
26549
|
+
return;
|
|
26550
|
+
}
|
|
26551
|
+
if (binding.lastUpdateId !== null && update.update_id <= binding.lastUpdateId) return;
|
|
26552
|
+
const media = extractMediaIngestInput(message);
|
|
26553
|
+
if (message.media_group_id && media) {
|
|
26554
|
+
const mediaGroupId = message.media_group_id;
|
|
26555
|
+
await telegram.bufferAlbumItem({
|
|
26556
|
+
siteId: binding.siteId,
|
|
26557
|
+
botId,
|
|
26558
|
+
telegramUserId,
|
|
26559
|
+
mediaGroupId,
|
|
26560
|
+
chatId,
|
|
26561
|
+
messageId: message.message_id,
|
|
26562
|
+
updateId: update.update_id,
|
|
26563
|
+
fileId: media.fileId,
|
|
26564
|
+
mediaKind: mediaKindToAlbumKind(media.mediaKind),
|
|
26565
|
+
mimeType: media.mimeType,
|
|
26566
|
+
originalName: media.originalName,
|
|
26567
|
+
captionMarkdown: captionMarkdown(message),
|
|
26568
|
+
width: media.width ?? null,
|
|
26569
|
+
height: media.height ?? null,
|
|
26570
|
+
durationSeconds: media.durationSeconds ?? null,
|
|
26571
|
+
posterFileId: media.posterFileId ?? null
|
|
26572
|
+
});
|
|
26573
|
+
runDeferred(c, async () => {
|
|
26574
|
+
await sleep(ALBUM_BUFFER_DELAY_MS);
|
|
26575
|
+
const claimed = await telegram.claimAlbumGroup(botId, mediaGroupId);
|
|
26576
|
+
if (claimed.length === 0) return;
|
|
26577
|
+
await publishAlbum(c, {
|
|
26578
|
+
botToken,
|
|
26579
|
+
chatId,
|
|
26580
|
+
binding,
|
|
26581
|
+
items: claimed
|
|
26582
|
+
});
|
|
26583
|
+
});
|
|
26584
|
+
return;
|
|
26585
|
+
}
|
|
26586
|
+
if (media) {
|
|
26587
|
+
await publishSingleMedia(c, {
|
|
26588
|
+
botToken,
|
|
26589
|
+
chatId,
|
|
26590
|
+
binding,
|
|
26591
|
+
media,
|
|
26592
|
+
captionMarkdown: captionMarkdown(message),
|
|
26593
|
+
updateId: update.update_id
|
|
26594
|
+
});
|
|
26595
|
+
return;
|
|
26596
|
+
}
|
|
26597
|
+
if (text) {
|
|
26598
|
+
const bodyMarkdown = entitiesToMarkdown(message.text ?? "", message.entities).trim();
|
|
26599
|
+
await c.var.servicesForSite(binding.siteId).posts.create({
|
|
26600
|
+
format: "note",
|
|
26601
|
+
bodyMarkdown,
|
|
26602
|
+
status: "published",
|
|
26603
|
+
visibility: "public"
|
|
26604
|
+
});
|
|
26605
|
+
await telegram.markUpdateProcessed(binding.id, update.update_id);
|
|
26606
|
+
await sendMessage(botToken, chatId, "Posted.");
|
|
26607
|
+
return;
|
|
26608
|
+
}
|
|
26609
|
+
await sendMessage(botToken, chatId, "I can post text, photos, videos, and documents. Other message types aren't supported yet.");
|
|
26610
|
+
}
|
|
26611
|
+
function sleep(ms) {
|
|
26612
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26613
|
+
}
|
|
26614
|
+
/**
|
|
26615
|
+
* Schedules background work that must outlive the current HTTP response so
|
|
26616
|
+
* Telegram can deliver the next webhook in this chat without waiting.
|
|
26617
|
+
*
|
|
26618
|
+
* On Cloudflare Workers `c.executionCtx.waitUntil` keeps the worker alive
|
|
26619
|
+
* until the promise settles. In Node / tests there's no such API, so we just
|
|
26620
|
+
* let the promise float — Node's event loop keeps running it. Either way the
|
|
26621
|
+
* promise has its own try/catch so unhandled rejections never bubble up.
|
|
26622
|
+
*
|
|
26623
|
+
* Background work also tracks the pending-promises array we register on the
|
|
26624
|
+
* env binding for tests so a vitest can `await` them after advancing timers.
|
|
26625
|
+
*/ function runDeferred(c, work) {
|
|
26626
|
+
const promise = work().catch((err) => {
|
|
26627
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26628
|
+
console.error(`[Jant] Telegram background error: ${message}`);
|
|
26629
|
+
});
|
|
26630
|
+
try {
|
|
26631
|
+
c.executionCtx?.waitUntil(promise);
|
|
26632
|
+
} catch {}
|
|
26633
|
+
const pending = c.env.__telegramPending;
|
|
26634
|
+
if (Array.isArray(pending)) pending.push(promise);
|
|
26635
|
+
}
|
|
26636
|
+
/**
|
|
26637
|
+
* Convert a message's caption + caption_entities into markdown, returning
|
|
26638
|
+
* `null` when there's nothing to record. Centralized so single-media and album
|
|
26639
|
+
* code paths can't drift apart on entity handling.
|
|
26640
|
+
*/ function captionMarkdown(message) {
|
|
26641
|
+
if (!message.caption) return null;
|
|
26642
|
+
return entitiesToMarkdown(message.caption, message.caption_entities).trim() || null;
|
|
26643
|
+
}
|
|
26644
|
+
/**
|
|
26645
|
+
* Pick the single ingestable media payload from a Telegram message, if any.
|
|
26646
|
+
*
|
|
26647
|
+
* Telegram never sends multiple media kinds on one message, so a `switch`-like
|
|
26648
|
+
* waterfall is sufficient — albums duplicate the message N times rather than
|
|
26649
|
+
* stuffing arrays into one message.
|
|
26650
|
+
*/ function extractMediaIngestInput(message) {
|
|
26651
|
+
if (message.photo && message.photo.length > 0) {
|
|
26652
|
+
const largest = message.photo[message.photo.length - 1];
|
|
26653
|
+
if (!largest) return null;
|
|
26654
|
+
return {
|
|
26655
|
+
fileId: largest.file_id,
|
|
26656
|
+
originalName: `telegram-photo-${message.message_id}.jpg`,
|
|
26657
|
+
mimeType: "image/jpeg",
|
|
26658
|
+
mediaKind: "image",
|
|
26659
|
+
width: largest.width,
|
|
26660
|
+
height: largest.height
|
|
26661
|
+
};
|
|
26662
|
+
}
|
|
26663
|
+
if (message.video) {
|
|
26664
|
+
const v = message.video;
|
|
26665
|
+
return {
|
|
26666
|
+
fileId: v.file_id,
|
|
26667
|
+
originalName: v.file_name ?? `telegram-video-${message.message_id}.mp4`,
|
|
26668
|
+
mimeType: v.mime_type ?? "video/mp4",
|
|
26669
|
+
mediaKind: "video",
|
|
26670
|
+
width: v.width,
|
|
26671
|
+
height: v.height,
|
|
26672
|
+
durationSeconds: v.duration,
|
|
26673
|
+
posterFileId: v.thumbnail?.file_id
|
|
26674
|
+
};
|
|
26675
|
+
}
|
|
26676
|
+
if (message.document) {
|
|
26677
|
+
const d = message.document;
|
|
26678
|
+
const docKind = documentMediaKind(d.mime_type);
|
|
26679
|
+
return {
|
|
26680
|
+
fileId: d.file_id,
|
|
26681
|
+
originalName: d.file_name ?? `telegram-document-${message.message_id}.bin`,
|
|
26682
|
+
mimeType: d.mime_type ?? "application/octet-stream",
|
|
26683
|
+
mediaKind: docKind,
|
|
26684
|
+
posterFileId: docKind === "video" ? d.thumbnail?.file_id : void 0
|
|
26685
|
+
};
|
|
26686
|
+
}
|
|
26687
|
+
return null;
|
|
26688
|
+
}
|
|
26689
|
+
/**
|
|
26690
|
+
* Decide which `mediaKind` slot a document belongs in. Telegram lets users
|
|
26691
|
+
* send a photo as a "file" to skip compression, in which case the document's
|
|
26692
|
+
* mime_type is still `image/*`; classify those as images so they render in the
|
|
26693
|
+
* site's image flow rather than the attachment list.
|
|
26694
|
+
*/ function documentMediaKind(mime) {
|
|
26695
|
+
if (!mime) return "document";
|
|
26696
|
+
if (mime.startsWith("image/")) return "image";
|
|
26697
|
+
if (mime.startsWith("video/")) return "video";
|
|
26698
|
+
if (mime.startsWith("audio/")) return "audio";
|
|
26699
|
+
if (mime.startsWith("text/")) return "text";
|
|
26700
|
+
return "document";
|
|
26701
|
+
}
|
|
26702
|
+
async function publishSingleMedia(c, input) {
|
|
26703
|
+
const siteSvcs = c.var.servicesForSite(input.binding.siteId);
|
|
26704
|
+
const storage = c.var.storage;
|
|
26705
|
+
if (!storage) {
|
|
26706
|
+
await sendMessage(input.botToken, input.chatId, "File storage isn't set up on this site, so I can't accept attachments.");
|
|
26707
|
+
return;
|
|
26708
|
+
}
|
|
26709
|
+
const uploadConfig = uploadConfigFromEnv(c.env);
|
|
26710
|
+
const attachments = [{
|
|
26711
|
+
type: "media",
|
|
26712
|
+
mediaId: (await siteSvcs.telegram.ingestMediaFile({
|
|
26713
|
+
...input.media,
|
|
26714
|
+
botToken: input.botToken
|
|
26715
|
+
}, {
|
|
26716
|
+
storage,
|
|
26717
|
+
...uploadConfig,
|
|
26718
|
+
media: siteSvcs.media
|
|
26719
|
+
})).id
|
|
26720
|
+
}];
|
|
26721
|
+
await siteSvcs.posts.createWithAttachments({
|
|
26722
|
+
format: "note",
|
|
26723
|
+
bodyMarkdown: input.captionMarkdown ?? "",
|
|
26724
|
+
status: "published",
|
|
26725
|
+
visibility: "public"
|
|
26726
|
+
}, attachments, {
|
|
26727
|
+
media: siteSvcs.media,
|
|
26728
|
+
storage,
|
|
26729
|
+
...uploadConfig
|
|
25093
26730
|
});
|
|
25094
|
-
|
|
25095
|
-
|
|
25096
|
-
|
|
25097
|
-
|
|
25098
|
-
|
|
25099
|
-
|
|
25100
|
-
|
|
25101
|
-
|
|
25102
|
-
|
|
25103
|
-
const parsed = parseRepoSlug(body.repo);
|
|
25104
|
-
if (!parsed) return c.json({ error: "Invalid repository format. Use owner/repo." }, 400);
|
|
25105
|
-
const client = createGitHubClient(body.token);
|
|
25106
|
-
try {
|
|
25107
|
-
await client.getRepo(parsed.owner, parsed.repo);
|
|
25108
|
-
} catch {
|
|
25109
|
-
return c.json({ error: "Could not access the repository. Check your token and repo name." }, 400);
|
|
26731
|
+
await c.var.services.telegram.markUpdateProcessed(input.binding.id, input.updateId);
|
|
26732
|
+
await sendMessage(input.botToken, input.chatId, "Posted.");
|
|
26733
|
+
}
|
|
26734
|
+
async function publishAlbum(c, input) {
|
|
26735
|
+
const siteSvcs = c.var.servicesForSite(input.binding.siteId);
|
|
26736
|
+
const storage = c.var.storage;
|
|
26737
|
+
if (!storage) {
|
|
26738
|
+
await sendMessage(input.botToken, input.chatId, "File storage isn't set up on this site, so I can't accept attachments.");
|
|
26739
|
+
return;
|
|
25110
26740
|
}
|
|
25111
|
-
|
|
25112
|
-
|
|
25113
|
-
await
|
|
25114
|
-
|
|
25115
|
-
|
|
25116
|
-
|
|
25117
|
-
|
|
25118
|
-
|
|
25119
|
-
|
|
25120
|
-
|
|
25121
|
-
|
|
25122
|
-
|
|
25123
|
-
|
|
25124
|
-
|
|
25125
|
-
|
|
25126
|
-
|
|
25127
|
-
|
|
25128
|
-
|
|
25129
|
-
|
|
25130
|
-
|
|
25131
|
-
|
|
25132
|
-
|
|
25133
|
-
|
|
25134
|
-
|
|
25135
|
-
|
|
25136
|
-
|
|
26741
|
+
const bodyMarkdown = input.items.find((i) => i.captionMarkdown)?.captionMarkdown ?? "";
|
|
26742
|
+
const uploadConfig = uploadConfigFromEnv(c.env);
|
|
26743
|
+
const attachments = (await Promise.all(input.items.map((item) => siteSvcs.telegram.ingestMediaFile({
|
|
26744
|
+
botToken: input.botToken,
|
|
26745
|
+
fileId: item.fileId,
|
|
26746
|
+
originalName: item.originalName ?? defaultAlbumName(item.messageId, item.mediaKind),
|
|
26747
|
+
mimeType: item.mimeType ?? defaultAlbumMime(item.mediaKind),
|
|
26748
|
+
mediaKind: albumKindToMediaKind(item.mediaKind),
|
|
26749
|
+
width: item.width ?? void 0,
|
|
26750
|
+
height: item.height ?? void 0,
|
|
26751
|
+
durationSeconds: item.durationSeconds ?? void 0,
|
|
26752
|
+
posterFileId: item.posterFileId ?? void 0
|
|
26753
|
+
}, {
|
|
26754
|
+
storage,
|
|
26755
|
+
...uploadConfig,
|
|
26756
|
+
media: siteSvcs.media
|
|
26757
|
+
})))).map((m) => ({
|
|
26758
|
+
type: "media",
|
|
26759
|
+
mediaId: m.id
|
|
26760
|
+
}));
|
|
26761
|
+
await siteSvcs.posts.createWithAttachments({
|
|
26762
|
+
format: "note",
|
|
26763
|
+
bodyMarkdown,
|
|
26764
|
+
status: "published",
|
|
26765
|
+
visibility: "public"
|
|
26766
|
+
}, attachments, {
|
|
26767
|
+
media: siteSvcs.media,
|
|
26768
|
+
storage,
|
|
26769
|
+
...uploadConfig
|
|
25137
26770
|
});
|
|
25138
|
-
|
|
25139
|
-
|
|
25140
|
-
await
|
|
25141
|
-
|
|
25142
|
-
|
|
25143
|
-
|
|
25144
|
-
|
|
25145
|
-
}
|
|
25146
|
-
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
|
|
26771
|
+
const maxUpdateId = Math.max(...input.items.map((i) => i.updateId));
|
|
26772
|
+
await c.var.services.telegram.markUpdateProcessed(input.binding.id, maxUpdateId);
|
|
26773
|
+
await sendMessage(input.botToken, input.chatId, "Posted.");
|
|
26774
|
+
}
|
|
26775
|
+
function defaultAlbumName(messageId, kind) {
|
|
26776
|
+
switch (kind) {
|
|
26777
|
+
case "image": return `telegram-photo-${messageId}.jpg`;
|
|
26778
|
+
case "video": return `telegram-video-${messageId}.mp4`;
|
|
26779
|
+
default: return `telegram-document-${messageId}.bin`;
|
|
26780
|
+
}
|
|
26781
|
+
}
|
|
26782
|
+
function defaultAlbumMime(kind) {
|
|
26783
|
+
switch (kind) {
|
|
26784
|
+
case "image": return "image/jpeg";
|
|
26785
|
+
case "video": return "video/mp4";
|
|
26786
|
+
default: return "application/octet-stream";
|
|
26787
|
+
}
|
|
26788
|
+
}
|
|
26789
|
+
function albumKindToMediaKind(kind) {
|
|
26790
|
+
return kind === "image" ? "image" : kind === "video" ? "video" : "document";
|
|
26791
|
+
}
|
|
25150
26792
|
/**
|
|
25151
|
-
*
|
|
25152
|
-
*
|
|
25153
|
-
*
|
|
25154
|
-
|
|
25155
|
-
|
|
25156
|
-
|
|
25157
|
-
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
|
|
25161
|
-
|
|
25162
|
-
|
|
25163
|
-
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
|
|
25168
|
-
|
|
25169
|
-
|
|
25170
|
-
|
|
25171
|
-
|
|
25172
|
-
|
|
25173
|
-
|
|
25174
|
-
|
|
25175
|
-
|
|
25176
|
-
|
|
25177
|
-
|
|
25178
|
-
if (
|
|
25179
|
-
|
|
25180
|
-
|
|
25181
|
-
|
|
25182
|
-
|
|
25183
|
-
|
|
26793
|
+
* Reduce a fine-grained `MediaKind` to the coarse `TelegramMediaGroupKind`
|
|
26794
|
+
* the buffer table understands. `audio` and `text` documents both fold into
|
|
26795
|
+
* `document` because the buffer only cares about which download path applies.
|
|
26796
|
+
*/ function mediaKindToAlbumKind(kind) {
|
|
26797
|
+
if (kind === "image") return "image";
|
|
26798
|
+
if (kind === "video") return "video";
|
|
26799
|
+
return "document";
|
|
26800
|
+
}
|
|
26801
|
+
async function handleStart(c, input) {
|
|
26802
|
+
const telegram = c.var.services.telegram;
|
|
26803
|
+
const pending = await telegram.resolvePendingCode(input.code);
|
|
26804
|
+
if (!pending) {
|
|
26805
|
+
await sendMessage(input.botToken, input.chatId, "That binding code is invalid or expired. Get a fresh one from Jant settings.");
|
|
26806
|
+
return;
|
|
26807
|
+
}
|
|
26808
|
+
const existing = await telegram.findBindingByUser(input.botId, input.telegramUserId);
|
|
26809
|
+
if (existing && existing.siteId === pending.siteId) {
|
|
26810
|
+
await sendMessage(input.botToken, input.chatId, "This chat is already connected. Send me any text to post a note.");
|
|
26811
|
+
return;
|
|
26812
|
+
}
|
|
26813
|
+
if (existing) {
|
|
26814
|
+
const buttons = [[{
|
|
26815
|
+
text: `Rebind this bot to ${await siteName(c, pending.siteId)}`,
|
|
26816
|
+
callback_data: `rebind:${input.code}`
|
|
26817
|
+
}]];
|
|
26818
|
+
for (const other of getTelegramBotPool(c.env)) {
|
|
26819
|
+
if (other.botId === input.botId) continue;
|
|
26820
|
+
if (await telegram.findBindingByUser(other.botId, input.telegramUserId)) continue;
|
|
26821
|
+
const username = await resolveBotUsername(other.botId, other.token);
|
|
26822
|
+
if (!username) continue;
|
|
26823
|
+
buttons.push([{
|
|
26824
|
+
text: `Connect to @${username} instead`,
|
|
26825
|
+
url: buildDeepLink(username, input.code)
|
|
26826
|
+
}]);
|
|
25184
26827
|
}
|
|
26828
|
+
await sendMessage(input.botToken, input.chatId, `This bot is already connected to ${await siteName(c, existing.siteId)}. Choose how to connect ${await siteName(c, pending.siteId)}:`, { inline_keyboard: buttons });
|
|
26829
|
+
return;
|
|
26830
|
+
}
|
|
26831
|
+
await telegram.bindAccount({
|
|
26832
|
+
siteId: pending.siteId,
|
|
26833
|
+
botId: input.botId,
|
|
26834
|
+
telegramUserId: input.telegramUserId,
|
|
26835
|
+
telegramUsername: input.telegramUsername
|
|
25185
26836
|
});
|
|
25186
|
-
});
|
|
26837
|
+
await sendMessage(input.botToken, input.chatId, `Connected to ${await siteName(c, pending.siteId)}. Send me any text and I'll post it as a note.`);
|
|
26838
|
+
}
|
|
25187
26839
|
//#endregion
|
|
25188
26840
|
//#region src/routes/api/internal/text-attachments.ts
|
|
25189
26841
|
var MigrateLegacySchema = z.object({ limit: z.number().int().positive().max(500).optional() });
|
|
@@ -26993,46 +28645,6 @@ function createD1RateLimiter(db, schema, now = () => Math.floor(Date.now() / 1e3
|
|
|
26993
28645
|
} };
|
|
26994
28646
|
}
|
|
26995
28647
|
//#endregion
|
|
26996
|
-
//#region src/lib/crypto.ts
|
|
26997
|
-
/**
|
|
26998
|
-
* Compare two byte arrays using a constant-time loop.
|
|
26999
|
-
*
|
|
27000
|
-
* This avoids relying on runtime-specific crypto helpers so the same behavior
|
|
27001
|
-
* works in Node.js and Workers.
|
|
27002
|
-
*
|
|
27003
|
-
* @param a - First byte array
|
|
27004
|
-
* @param b - Second byte array
|
|
27005
|
-
* @returns `true` when the inputs are byte-for-byte identical
|
|
27006
|
-
*
|
|
27007
|
-
* @example
|
|
27008
|
-
* ```ts
|
|
27009
|
-
* const isEqual = timingSafeEqualBytes(
|
|
27010
|
-
* new TextEncoder().encode("abc"),
|
|
27011
|
-
* new TextEncoder().encode("abc"),
|
|
27012
|
-
* );
|
|
27013
|
-
* ```
|
|
27014
|
-
*/ function timingSafeEqualBytes(a, b) {
|
|
27015
|
-
if (a.byteLength !== b.byteLength) return false;
|
|
27016
|
-
let mismatch = 0;
|
|
27017
|
-
for (let index = 0; index < a.byteLength; index += 1) mismatch |= (a[index] ?? 0) ^ (b[index] ?? 0);
|
|
27018
|
-
return mismatch === 0;
|
|
27019
|
-
}
|
|
27020
|
-
/**
|
|
27021
|
-
* Compare two strings using a constant-time byte comparison.
|
|
27022
|
-
*
|
|
27023
|
-
* @param a - First string
|
|
27024
|
-
* @param b - Second string
|
|
27025
|
-
* @returns `true` when the UTF-8 byte sequences are identical
|
|
27026
|
-
*
|
|
27027
|
-
* @example
|
|
27028
|
-
* ```ts
|
|
27029
|
-
* const isEqual = timingSafeEqualText("token-a", "token-b");
|
|
27030
|
-
* ```
|
|
27031
|
-
*/ function timingSafeEqualText(a, b) {
|
|
27032
|
-
const encoder = new TextEncoder();
|
|
27033
|
-
return timingSafeEqualBytes(encoder.encode(a), encoder.encode(b));
|
|
27034
|
-
}
|
|
27035
|
-
//#endregion
|
|
27036
28648
|
//#region src/lib/hosted-sso.ts
|
|
27037
28649
|
var textEncoder = new TextEncoder();
|
|
27038
28650
|
var textDecoder = new TextDecoder();
|
|
@@ -30863,7 +32475,7 @@ function createSiteAdminService(db, databaseSchema = sqliteSchemaBundle, databas
|
|
|
30863
32475
|
const themeCss = buildThemeStyle(activeTheme, appConfig.themeMode, fontOverrides);
|
|
30864
32476
|
const navItemList = await navItems.list();
|
|
30865
32477
|
const appleTouchKey = allSettings[SETTINGS_KEYS.SITE_FAVICON_APPLE_TOUCH];
|
|
30866
|
-
const { createExportService } = await import("./export-
|
|
32478
|
+
const { createExportService } = await import("./export-Bbn86HmS.js").then((n) => n.n);
|
|
30867
32479
|
const exportService = createExportService({
|
|
30868
32480
|
collections,
|
|
30869
32481
|
media: mediaService,
|
|
@@ -31122,17 +32734,21 @@ function createUploadSessionService(db, siteId, media, databaseSchema = sqliteSc
|
|
|
31122
32734
|
if (!object) throw new ValidationError("The uploaded file could not be found.");
|
|
31123
32735
|
if (await sha256Base64(new Uint8Array(await new Response(object.body).arrayBuffer())) !== session.expectedChecksumSha256) throw new ValidationError("The uploaded file checksum does not match.");
|
|
31124
32736
|
}
|
|
31125
|
-
async function validateStoredObject(storage, session) {
|
|
32737
|
+
async function validateStoredObject(storage, session, sniffDimensions) {
|
|
31126
32738
|
const head = await storage.head(session.tempStorageKey);
|
|
31127
32739
|
if (!head) throw new ValidationError("The uploaded file could not be found.");
|
|
31128
32740
|
if (head.size !== session.expectedSize) throw new ValidationError("The uploaded file size does not match.");
|
|
31129
32741
|
if (head.contentType !== session.expectedContentType) throw new ValidationError("The uploaded file type does not match.");
|
|
31130
|
-
const
|
|
31131
|
-
|
|
31132
|
-
|
|
31133
|
-
|
|
32742
|
+
const signaturePeekLength = getStoredUploadSignaturePeekLength(session.expectedContentType);
|
|
32743
|
+
const peekLength = sniffDimensions ? Math.max(signaturePeekLength, IMAGE_DIMENSION_PEEK_BYTES) : signaturePeekLength;
|
|
32744
|
+
if (peekLength === 0) return null;
|
|
32745
|
+
const bytes = await readBytes(storage, session.tempStorageKey, peekLength);
|
|
32746
|
+
if (signaturePeekLength > 0) {
|
|
32747
|
+
const signatureError = validateStoredUploadSignature(session.expectedContentType, bytes.subarray(0, signaturePeekLength));
|
|
31134
32748
|
if (signatureError) throw new ValidationError(signatureError);
|
|
31135
32749
|
}
|
|
32750
|
+
if (sniffDimensions) return parseImageDimensions(session.expectedContentType, bytes);
|
|
32751
|
+
return null;
|
|
31136
32752
|
}
|
|
31137
32753
|
async function validatePoster(storage, uploadId) {
|
|
31138
32754
|
for (const ext of ["webp", "png"]) {
|
|
@@ -31264,7 +32880,14 @@ function createUploadSessionService(db, siteId, media, databaseSchema = sqliteSc
|
|
|
31264
32880
|
} else if (session.state !== "uploaded" && deps.storageDriver !== "s3") throw new ConflictError("Upload the file body before completing.");
|
|
31265
32881
|
try {
|
|
31266
32882
|
if (session.multipartUploadId) await validateStoredChecksum(deps.storage, session);
|
|
31267
|
-
|
|
32883
|
+
let width = data.width;
|
|
32884
|
+
let height = data.height;
|
|
32885
|
+
const needsDimensionSniff = (!width || !height) && session.expectedContentType.startsWith("image/");
|
|
32886
|
+
const sniffed = await validateStoredObject(deps.storage, session, needsDimensionSniff);
|
|
32887
|
+
if (sniffed) {
|
|
32888
|
+
width ??= sniffed.width;
|
|
32889
|
+
height ??= sniffed.height;
|
|
32890
|
+
}
|
|
31268
32891
|
const posterInfo = await validatePoster(deps.storage, id);
|
|
31269
32892
|
const objectOptions = getObjectOptions(session);
|
|
31270
32893
|
await copyObject(deps.storage, session.tempStorageKey, session.finalStorageKey, objectOptions);
|
|
@@ -31284,8 +32907,8 @@ function createUploadSessionService(db, siteId, media, databaseSchema = sqliteSc
|
|
|
31284
32907
|
size: session.expectedSize,
|
|
31285
32908
|
storageKey: session.finalStorageKey,
|
|
31286
32909
|
provider: deps.storageDriver,
|
|
31287
|
-
width
|
|
31288
|
-
height
|
|
32910
|
+
width,
|
|
32911
|
+
height,
|
|
31289
32912
|
durationSeconds: data.durationSeconds,
|
|
31290
32913
|
blurhash: data.blurhash,
|
|
31291
32914
|
waveform: data.waveform,
|
|
@@ -31466,6 +33089,243 @@ function toStored(row) {
|
|
|
31466
33089
|
};
|
|
31467
33090
|
}
|
|
31468
33091
|
//#endregion
|
|
33092
|
+
//#region src/services/telegram.ts
|
|
33093
|
+
/**
|
|
33094
|
+
* Telegram Service
|
|
33095
|
+
*
|
|
33096
|
+
* Owns the two Telegram binding tables:
|
|
33097
|
+
*
|
|
33098
|
+
* - `telegram_pending_binding` — short-lived, single-use binding codes. One
|
|
33099
|
+
* per site; regenerating replaces the previous code.
|
|
33100
|
+
* - `telegram_binding` — active links between a Telegram account and a site.
|
|
33101
|
+
* One per site, and unique per `(bot_id, telegram_user_id)` so a Telegram
|
|
33102
|
+
* account uses a distinct pool bot for each site it posts to.
|
|
33103
|
+
*
|
|
33104
|
+
* Code-lookup and binding-upsert methods are deliberately cross-site: the
|
|
33105
|
+
* webhook handler runs without a host-resolved site (hosted mode forwards the
|
|
33106
|
+
* webhook through the control plane), so it resolves the target site from the
|
|
33107
|
+
* binding tables instead. Site-scoped methods (`getStatus`, `generateCode`,
|
|
33108
|
+
* `disconnect`, and the bring-your-own-bot token management) operate on the
|
|
33109
|
+
* site this service instance was created for.
|
|
33110
|
+
*/
|
|
33111
|
+
/** How long a freshly generated binding code stays valid (seconds). */ var BINDING_CODE_TTL = 1800;
|
|
33112
|
+
var BINDING_CODE_LENGTH = 12;
|
|
33113
|
+
function createTelegramService(db, siteId, databaseSchema = sqliteSchemaBundle) {
|
|
33114
|
+
const { telegramBindings, telegramPendingBindings, telegramMediaGroupItems, settings } = databaseSchema;
|
|
33115
|
+
async function readSetting(key) {
|
|
33116
|
+
return (await db.select({ value: settings.value }).from(settings).where(and(eq(settings.siteId, siteId), eq(settings.key, key))).limit(1))[0]?.value ?? null;
|
|
33117
|
+
}
|
|
33118
|
+
async function writeSetting(key, value) {
|
|
33119
|
+
const timestamp = now();
|
|
33120
|
+
await db.insert(settings).values({
|
|
33121
|
+
siteId,
|
|
33122
|
+
key,
|
|
33123
|
+
value,
|
|
33124
|
+
updatedAt: timestamp
|
|
33125
|
+
}).onConflictDoUpdate({
|
|
33126
|
+
target: [settings.siteId, settings.key],
|
|
33127
|
+
set: {
|
|
33128
|
+
value,
|
|
33129
|
+
updatedAt: timestamp
|
|
33130
|
+
}
|
|
33131
|
+
});
|
|
33132
|
+
}
|
|
33133
|
+
async function findBindingForSite(targetSiteId) {
|
|
33134
|
+
return (await db.select().from(telegramBindings).where(eq(telegramBindings.siteId, targetSiteId)).limit(1))[0] ?? null;
|
|
33135
|
+
}
|
|
33136
|
+
async function generateCode() {
|
|
33137
|
+
const code = generateRandomId(BINDING_CODE_LENGTH);
|
|
33138
|
+
const timestamp = now();
|
|
33139
|
+
await db.insert(telegramPendingBindings).values({
|
|
33140
|
+
id: createEntityId("telegramBindingCode"),
|
|
33141
|
+
siteId,
|
|
33142
|
+
code,
|
|
33143
|
+
createdAt: timestamp,
|
|
33144
|
+
expiresAt: timestamp + BINDING_CODE_TTL
|
|
33145
|
+
}).onConflictDoUpdate({
|
|
33146
|
+
target: telegramPendingBindings.siteId,
|
|
33147
|
+
set: {
|
|
33148
|
+
code,
|
|
33149
|
+
createdAt: timestamp,
|
|
33150
|
+
expiresAt: timestamp + BINDING_CODE_TTL
|
|
33151
|
+
}
|
|
33152
|
+
});
|
|
33153
|
+
return code;
|
|
33154
|
+
}
|
|
33155
|
+
return {
|
|
33156
|
+
async getStatus() {
|
|
33157
|
+
const binding = await findBindingForSite(siteId);
|
|
33158
|
+
const botId = await readSetting("TELEGRAM_BOT_ID");
|
|
33159
|
+
const username = await readSetting("TELEGRAM_BOT_USERNAME");
|
|
33160
|
+
return {
|
|
33161
|
+
binding,
|
|
33162
|
+
userBot: botId ? {
|
|
33163
|
+
botId,
|
|
33164
|
+
username: username ?? ""
|
|
33165
|
+
} : null
|
|
33166
|
+
};
|
|
33167
|
+
},
|
|
33168
|
+
async getOrCreateCode() {
|
|
33169
|
+
const existing = (await db.select().from(telegramPendingBindings).where(eq(telegramPendingBindings.siteId, siteId)).limit(1))[0];
|
|
33170
|
+
if (existing && existing.expiresAt > now()) return existing.code;
|
|
33171
|
+
return generateCode();
|
|
33172
|
+
},
|
|
33173
|
+
generateCode,
|
|
33174
|
+
async disconnect() {
|
|
33175
|
+
await db.delete(telegramBindings).where(eq(telegramBindings.siteId, siteId));
|
|
33176
|
+
},
|
|
33177
|
+
async resolvePendingCode(code) {
|
|
33178
|
+
const row = (await db.select().from(telegramPendingBindings).where(eq(telegramPendingBindings.code, code)).limit(1))[0];
|
|
33179
|
+
if (!row) return null;
|
|
33180
|
+
if (row.expiresAt < now()) {
|
|
33181
|
+
await db.delete(telegramPendingBindings).where(eq(telegramPendingBindings.id, row.id));
|
|
33182
|
+
return null;
|
|
33183
|
+
}
|
|
33184
|
+
return { siteId: row.siteId };
|
|
33185
|
+
},
|
|
33186
|
+
async findBindingByUser(botId, telegramUserId) {
|
|
33187
|
+
return (await db.select().from(telegramBindings).where(and(eq(telegramBindings.botId, botId), eq(telegramBindings.telegramUserId, telegramUserId))).limit(1))[0] ?? null;
|
|
33188
|
+
},
|
|
33189
|
+
async bindAccount(input) {
|
|
33190
|
+
await db.delete(telegramBindings).where(and(eq(telegramBindings.botId, input.botId), eq(telegramBindings.telegramUserId, input.telegramUserId)));
|
|
33191
|
+
await db.delete(telegramBindings).where(eq(telegramBindings.siteId, input.siteId));
|
|
33192
|
+
const binding = {
|
|
33193
|
+
id: createEntityId("telegramBinding"),
|
|
33194
|
+
siteId: input.siteId,
|
|
33195
|
+
botId: input.botId,
|
|
33196
|
+
telegramUserId: input.telegramUserId,
|
|
33197
|
+
telegramUsername: input.telegramUsername,
|
|
33198
|
+
lastUpdateId: null,
|
|
33199
|
+
boundAt: now()
|
|
33200
|
+
};
|
|
33201
|
+
await db.insert(telegramBindings).values(binding);
|
|
33202
|
+
await db.delete(telegramPendingBindings).where(eq(telegramPendingBindings.siteId, input.siteId));
|
|
33203
|
+
return binding;
|
|
33204
|
+
},
|
|
33205
|
+
async markUpdateProcessed(bindingId, updateId) {
|
|
33206
|
+
await db.update(telegramBindings).set({ lastUpdateId: updateId }).where(eq(telegramBindings.id, bindingId));
|
|
33207
|
+
},
|
|
33208
|
+
async connectUserBot(token, webhookBaseUrl) {
|
|
33209
|
+
const botId = parseBotId(token);
|
|
33210
|
+
if (!botId) throw new Error("That doesn't look like a bot token.");
|
|
33211
|
+
const identity = await getMe(token);
|
|
33212
|
+
const secret = generateRandomId(32);
|
|
33213
|
+
await setWebhook(token, `${webhookBaseUrl.replace(/\/+$/, "")}/api/telegram/webhook/${botId}`, secret);
|
|
33214
|
+
try {
|
|
33215
|
+
await setMyCommands(token);
|
|
33216
|
+
} catch {}
|
|
33217
|
+
await writeSetting("TELEGRAM_BOT_TOKEN", token);
|
|
33218
|
+
await writeSetting("TELEGRAM_BOT_ID", botId);
|
|
33219
|
+
await writeSetting("TELEGRAM_BOT_USERNAME", identity.username);
|
|
33220
|
+
await writeSetting("TELEGRAM_BOT_WEBHOOK_SECRET", secret);
|
|
33221
|
+
},
|
|
33222
|
+
async bufferAlbumItem(input) {
|
|
33223
|
+
await db.insert(telegramMediaGroupItems).values({
|
|
33224
|
+
id: createEntityId("telegramMediaGroupItem"),
|
|
33225
|
+
siteId: input.siteId,
|
|
33226
|
+
botId: input.botId,
|
|
33227
|
+
telegramUserId: input.telegramUserId,
|
|
33228
|
+
mediaGroupId: input.mediaGroupId,
|
|
33229
|
+
chatId: input.chatId,
|
|
33230
|
+
messageId: input.messageId,
|
|
33231
|
+
updateId: input.updateId,
|
|
33232
|
+
fileId: input.fileId,
|
|
33233
|
+
mediaKind: input.mediaKind,
|
|
33234
|
+
mimeType: input.mimeType,
|
|
33235
|
+
originalName: input.originalName,
|
|
33236
|
+
captionMarkdown: input.captionMarkdown,
|
|
33237
|
+
width: input.width,
|
|
33238
|
+
height: input.height,
|
|
33239
|
+
durationSeconds: input.durationSeconds,
|
|
33240
|
+
posterFileId: input.posterFileId,
|
|
33241
|
+
createdAt: now()
|
|
33242
|
+
}).onConflictDoNothing({ target: [
|
|
33243
|
+
telegramMediaGroupItems.botId,
|
|
33244
|
+
telegramMediaGroupItems.mediaGroupId,
|
|
33245
|
+
telegramMediaGroupItems.messageId
|
|
33246
|
+
] });
|
|
33247
|
+
},
|
|
33248
|
+
async claimAlbumGroup(botId, mediaGroupId) {
|
|
33249
|
+
return (await db.delete(telegramMediaGroupItems).where(and(eq(telegramMediaGroupItems.botId, botId), eq(telegramMediaGroupItems.mediaGroupId, mediaGroupId))).returning()).map((row) => ({
|
|
33250
|
+
id: row.id,
|
|
33251
|
+
siteId: row.siteId,
|
|
33252
|
+
botId: row.botId,
|
|
33253
|
+
telegramUserId: row.telegramUserId,
|
|
33254
|
+
mediaGroupId: row.mediaGroupId,
|
|
33255
|
+
chatId: row.chatId,
|
|
33256
|
+
messageId: row.messageId,
|
|
33257
|
+
updateId: row.updateId,
|
|
33258
|
+
fileId: row.fileId,
|
|
33259
|
+
mediaKind: row.mediaKind,
|
|
33260
|
+
mimeType: row.mimeType,
|
|
33261
|
+
originalName: row.originalName,
|
|
33262
|
+
captionMarkdown: row.captionMarkdown,
|
|
33263
|
+
width: row.width,
|
|
33264
|
+
height: row.height,
|
|
33265
|
+
durationSeconds: row.durationSeconds,
|
|
33266
|
+
posterFileId: row.posterFileId,
|
|
33267
|
+
createdAt: row.createdAt
|
|
33268
|
+
})).sort((a, b) => a.messageId - b.messageId);
|
|
33269
|
+
},
|
|
33270
|
+
async ingestMediaFile(input, deps) {
|
|
33271
|
+
const fileInfo = await getFile(input.botToken, input.fileId);
|
|
33272
|
+
if (!fileInfo.file_path) throw new ValidationError("Telegram returned no file path for the attachment.");
|
|
33273
|
+
const maxBytes = deps.maxFileSizeMB * 1024 * 1024;
|
|
33274
|
+
if (fileInfo.file_size !== void 0 && fileInfo.file_size > maxBytes) throw new ValidationError(`Attachment exceeds the ${deps.maxFileSizeMB} MB upload limit.`);
|
|
33275
|
+
const response = await downloadFile(input.botToken, fileInfo.file_path);
|
|
33276
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
33277
|
+
if (bytes.byteLength > maxBytes) throw new ValidationError(`Attachment exceeds the ${deps.maxFileSizeMB} MB upload limit.`);
|
|
33278
|
+
await deps.media.assertCanWriteBytes(bytes.byteLength);
|
|
33279
|
+
const { id, filename, storageKey } = generateStorageKey(siteId, input.originalName);
|
|
33280
|
+
await deps.storage.put(storageKey, bytes, {
|
|
33281
|
+
contentType: input.mimeType,
|
|
33282
|
+
contentDisposition: input.mediaKind === "document" ? "attachment" : "inline",
|
|
33283
|
+
cacheControl: "public, max-age=31536000, immutable"
|
|
33284
|
+
});
|
|
33285
|
+
let posterKey;
|
|
33286
|
+
if (input.posterFileId) try {
|
|
33287
|
+
const posterFile = await getFile(input.botToken, input.posterFileId);
|
|
33288
|
+
if (posterFile.file_path) {
|
|
33289
|
+
const posterResp = await downloadFile(input.botToken, posterFile.file_path);
|
|
33290
|
+
const posterBytes = new Uint8Array(await posterResp.arrayBuffer());
|
|
33291
|
+
posterKey = getPosterStorageKey(siteId, id, "jpg");
|
|
33292
|
+
await deps.storage.put(posterKey, posterBytes, {
|
|
33293
|
+
contentType: "image/jpeg",
|
|
33294
|
+
cacheControl: "public, max-age=31536000, immutable"
|
|
33295
|
+
});
|
|
33296
|
+
}
|
|
33297
|
+
} catch {
|
|
33298
|
+
posterKey = void 0;
|
|
33299
|
+
}
|
|
33300
|
+
return deps.media.create({
|
|
33301
|
+
id,
|
|
33302
|
+
filename,
|
|
33303
|
+
originalName: input.originalName,
|
|
33304
|
+
mimeType: input.mimeType,
|
|
33305
|
+
size: bytes.byteLength,
|
|
33306
|
+
storageKey,
|
|
33307
|
+
provider: deps.storageDriver,
|
|
33308
|
+
mediaKind: input.mediaKind,
|
|
33309
|
+
width: input.width,
|
|
33310
|
+
height: input.height,
|
|
33311
|
+
durationSeconds: input.durationSeconds,
|
|
33312
|
+
posterKey
|
|
33313
|
+
});
|
|
33314
|
+
},
|
|
33315
|
+
async removeUserBot() {
|
|
33316
|
+
const token = await readSetting("TELEGRAM_BOT_TOKEN");
|
|
33317
|
+
if (token) try {
|
|
33318
|
+
await deleteWebhook(token);
|
|
33319
|
+
} catch {}
|
|
33320
|
+
await writeSetting("TELEGRAM_BOT_TOKEN", "");
|
|
33321
|
+
await writeSetting("TELEGRAM_BOT_ID", "");
|
|
33322
|
+
await writeSetting("TELEGRAM_BOT_USERNAME", "");
|
|
33323
|
+
await writeSetting("TELEGRAM_BOT_WEBHOOK_SECRET", "");
|
|
33324
|
+
await db.delete(telegramBindings).where(eq(telegramBindings.siteId, siteId));
|
|
33325
|
+
}
|
|
33326
|
+
};
|
|
33327
|
+
}
|
|
33328
|
+
//#endregion
|
|
31469
33329
|
//#region src/services/index.ts
|
|
31470
33330
|
/**
|
|
31471
33331
|
* Services (v2)
|
|
@@ -31511,7 +33371,8 @@ function toStored(row) {
|
|
|
31511
33371
|
process.stderr.write(`[Jant] Hosted control plane metadata sync failed: ${message}\n`);
|
|
31512
33372
|
}
|
|
31513
33373
|
}),
|
|
31514
|
-
githubAppInstallations: createGitHubAppInstallationsService(db, databaseSchema)
|
|
33374
|
+
githubAppInstallations: createGitHubAppInstallationsService(db, databaseSchema),
|
|
33375
|
+
telegram: createTelegramService(db, siteId, databaseSchema)
|
|
31515
33376
|
};
|
|
31516
33377
|
}
|
|
31517
33378
|
//#endregion
|
|
@@ -31545,7 +33406,7 @@ async function resolveRequestSite(db, env, publicRequestUrl, databaseSchema = sq
|
|
|
31545
33406
|
});
|
|
31546
33407
|
const resolved = await siteService.resolveByHost(requestUrl.host);
|
|
31547
33408
|
if (!resolved) {
|
|
31548
|
-
if (requestUrl.pathname.startsWith("/api/internal/") || requestUrl.pathname === "/api/github-sync/app-webhook") return {
|
|
33409
|
+
if (requestUrl.pathname.startsWith("/api/internal/") || requestUrl.pathname === "/api/github-sync/app-webhook" || requestUrl.pathname.startsWith("/api/telegram/webhook/")) return {
|
|
31549
33410
|
site: createTransientSite("internal"),
|
|
31550
33411
|
domain: null
|
|
31551
33412
|
};
|
|
@@ -31617,6 +33478,16 @@ function getResolvedSiteBaseUrl(env, publicRequestUrl, pathPrefix) {
|
|
|
31617
33478
|
schema: sqliteSchemaBundle,
|
|
31618
33479
|
useSecureCookies: shouldUseSecureCookies(env, publicRequestUrl)
|
|
31619
33480
|
});
|
|
33481
|
+
const servicesConfig = {
|
|
33482
|
+
databaseDialect: "sqlite",
|
|
33483
|
+
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
33484
|
+
enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
|
|
33485
|
+
hostedControlPlane: createHostedControlPlaneClient(env),
|
|
33486
|
+
siteResolutionMode: getSiteResolutionMode(env),
|
|
33487
|
+
slugIdLength,
|
|
33488
|
+
schema: sqliteSchemaBundle
|
|
33489
|
+
};
|
|
33490
|
+
const servicesForSite = (siteId) => createServices(db, session, siteId, servicesConfig);
|
|
31620
33491
|
return {
|
|
31621
33492
|
auth,
|
|
31622
33493
|
currentSite: siteLookup.site,
|
|
@@ -31628,15 +33499,8 @@ function getResolvedSiteBaseUrl(env, publicRequestUrl, pathPrefix) {
|
|
|
31628
33499
|
secret: hostedControlPlaneSsoSecret
|
|
31629
33500
|
}),
|
|
31630
33501
|
rateLimiter: createD1RateLimiter(db, sqliteSchemaBundle),
|
|
31631
|
-
services:
|
|
31632
|
-
|
|
31633
|
-
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
31634
|
-
enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
|
|
31635
|
-
hostedControlPlane: createHostedControlPlaneClient(env),
|
|
31636
|
-
siteResolutionMode: getSiteResolutionMode(env),
|
|
31637
|
-
slugIdLength,
|
|
31638
|
-
schema: sqliteSchemaBundle
|
|
31639
|
-
}),
|
|
33502
|
+
services: servicesForSite(siteLookup.site.id),
|
|
33503
|
+
servicesForSite,
|
|
31640
33504
|
storage: createStorageDriver(env)
|
|
31641
33505
|
};
|
|
31642
33506
|
}
|
|
@@ -31752,6 +33616,16 @@ function createBetterSqliteRawQuery(sqlite) {
|
|
|
31752
33616
|
schema: databaseSchema,
|
|
31753
33617
|
useSecureCookies: shouldUseSecureCookies(env, publicRequestUrl)
|
|
31754
33618
|
});
|
|
33619
|
+
const servicesConfig = {
|
|
33620
|
+
databaseDialect,
|
|
33621
|
+
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
33622
|
+
enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
|
|
33623
|
+
hostedControlPlane: createHostedControlPlaneClient(env),
|
|
33624
|
+
siteResolutionMode: getSiteResolutionMode(env),
|
|
33625
|
+
slugIdLength,
|
|
33626
|
+
schema: databaseSchema
|
|
33627
|
+
};
|
|
33628
|
+
const servicesForSite = (siteId) => createServices(db, rawQuery, siteId, servicesConfig);
|
|
31755
33629
|
return {
|
|
31756
33630
|
auth,
|
|
31757
33631
|
currentSite: siteLookup.site,
|
|
@@ -31763,15 +33637,8 @@ function createBetterSqliteRawQuery(sqlite) {
|
|
|
31763
33637
|
secret: hostedControlPlaneSsoSecret
|
|
31764
33638
|
}),
|
|
31765
33639
|
rateLimiter: getNodeRateLimiter(),
|
|
31766
|
-
services:
|
|
31767
|
-
|
|
31768
|
-
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
31769
|
-
enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
|
|
31770
|
-
hostedControlPlane: createHostedControlPlaneClient(env),
|
|
31771
|
-
siteResolutionMode: getSiteResolutionMode(env),
|
|
31772
|
-
slugIdLength,
|
|
31773
|
-
schema: databaseSchema
|
|
31774
|
-
}),
|
|
33640
|
+
services: servicesForSite(siteLookup.site.id),
|
|
33641
|
+
servicesForSite,
|
|
31775
33642
|
storage: createStorageDriver(env)
|
|
31776
33643
|
};
|
|
31777
33644
|
}
|
|
@@ -32035,6 +33902,7 @@ async function servePublicStorage(c) {
|
|
|
32035
33902
|
if (startupConfigError) return c.html(startupConfigError, 500);
|
|
32036
33903
|
const runtime = await createRequestRuntime(c.env, publicRequestUrl);
|
|
32037
33904
|
c.set("services", runtime.services);
|
|
33905
|
+
c.set("servicesForSite", runtime.servicesForSite);
|
|
32038
33906
|
c.set("hostedHandoff", runtime.hostedHandoff);
|
|
32039
33907
|
c.set("storage", runtime.storage);
|
|
32040
33908
|
c.set("auth", runtime.auth);
|
|
@@ -32064,6 +33932,7 @@ async function servePublicStorage(c) {
|
|
|
32064
33932
|
app.route("/api/internal/search/reindex", internalSearchReindexRoutes);
|
|
32065
33933
|
app.route("/api/internal/uploads", internalUploadsRoutes);
|
|
32066
33934
|
app.route("/api/github-sync", githubSyncWebhookRoutes);
|
|
33935
|
+
app.route("/api/telegram", telegramWebhookRoutes);
|
|
32067
33936
|
app.get("/api/media/:id/content", async (c) => {
|
|
32068
33937
|
const media = await c.var.services.media.getById(c.req.param("id"));
|
|
32069
33938
|
if (!media) return c.notFound();
|
|
@@ -32192,4 +34061,4 @@ async function servePublicStorage(c) {
|
|
|
32192
34061
|
return app;
|
|
32193
34062
|
}
|
|
32194
34063
|
//#endregion
|
|
32195
|
-
export {
|
|
34064
|
+
export { MAX_MEDIA_ATTACHMENTS as A, isAssetPath as B, toMediaView as C, toPostViews as D, toPostView as E, STATUSES$2 as F, TEXT_ATTACHMENT_CONTENT_FORMATS as I, buildThemeStyle as L, MEDIA_KINDS as M, NAV_ITEM_TYPES$2 as N, toSearchResultView as O, SORT_ORDERS as P, BUILTIN_COLOR_THEMES as R, toArchiveGroupsWithMedia as S, toNavItemViews as T, sqliteSchemaBundle as _, resolveDatabaseDialect as a, createMediaContext as b, resolveConfig as c, setWebhook as d, BUILTIN_FONT_THEMES as f, pgSchemaBundle as g, defaultFeedRenderer as h, createSiteService as i, MAX_PINNED_POSTS as j, FORMATS$2 as k, getWebhookUrl as l, getFontThemeCssVariables as m, createNodeCliRuntime as n, getHostBasedStartupConfigurationIssues as o, getCjkSerifCssVariables as p, createNodeRequestRuntime as r, createStorageDriver as s, createApp as t, setMyCommands as u, createNodeDatabase as v, toNavItemView as w, toArchiveGroups as x, schema_exports$1 as y, getPublicAssetBasePath as z };
|