@jant/core 0.5.3 → 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.
Files changed (95) hide show
  1. package/bin/commands/telegram/register-webhooks.js +93 -0
  2. package/dist/{app-C481ssbr.js → app-BIkkbVQk.js} +2252 -383
  3. package/dist/app-Bcr5_wZI.js +6 -0
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/client-Bo7sKkAQ.js +274 -0
  6. package/dist/client/_assets/client-QHRvzZwk.css +2 -0
  7. package/dist/client/_assets/{client-auth-CfBiCAB7.js → client-auth-D1jDQgbH.js} +49 -49
  8. package/dist/{env-CgaH9Mut.js → env-C7e2Nlnt.js} +30 -1
  9. package/dist/{export-CR9Megtb.js → export-Bbn86HmS.js} +1 -1
  10. package/dist/{github-sync-DYZq9rQp.js → github-sync-CBQPRZ8H.js} +1 -1
  11. package/dist/{github-sync-8Vv06aCr.js → github-sync-dXsiZa_e.js} +2 -2
  12. package/dist/index.js +4 -4
  13. package/dist/node.js +61 -5
  14. package/package.json +2 -1
  15. package/src/__tests__/helpers/app.ts +15 -2
  16. package/src/app.tsx +3 -0
  17. package/src/client/components/jant-compose-editor.ts +72 -0
  18. package/src/client/thread-context.ts +146 -2
  19. package/src/client/tiptap/__tests__/link-toolbar.test.ts +1 -1
  20. package/src/client/tiptap/bubble-menu.ts +1 -16
  21. package/src/client/tiptap/extensions.ts +2 -6
  22. package/src/client/tiptap/link-toolbar.ts +0 -21
  23. package/src/client/tiptap/paste-media.ts +49 -33
  24. package/src/client/tiptap/toolbar-mode.ts +0 -43
  25. package/src/client/video-processor.ts +9 -0
  26. package/src/db/migrations/0022_old_gressill.sql +24 -0
  27. package/src/db/migrations/0023_broad_terror.sql +20 -0
  28. package/src/db/migrations/0024_red_the_twelve.sql +3 -0
  29. package/src/db/migrations/0025_exotic_wendell_rand.sql +1 -0
  30. package/src/db/migrations/meta/0022_snapshot.json +2267 -0
  31. package/src/db/migrations/meta/0023_snapshot.json +2396 -0
  32. package/src/db/migrations/meta/0024_snapshot.json +2417 -0
  33. package/src/db/migrations/meta/0025_snapshot.json +2424 -0
  34. package/src/db/migrations/meta/_journal.json +28 -0
  35. package/src/db/migrations/pg/0020_bizarre_smasher.sql +24 -0
  36. package/src/db/migrations/pg/0021_sharp_puppet_master.sql +20 -0
  37. package/src/db/migrations/pg/0022_blushing_blue_shield.sql +3 -0
  38. package/src/db/migrations/pg/0023_organic_zemo.sql +1 -0
  39. package/src/db/migrations/pg/meta/0020_snapshot.json +2904 -0
  40. package/src/db/migrations/pg/meta/0021_snapshot.json +3060 -0
  41. package/src/db/migrations/pg/meta/0022_snapshot.json +3078 -0
  42. package/src/db/migrations/pg/meta/0023_snapshot.json +3084 -0
  43. package/src/db/migrations/pg/meta/_journal.json +28 -0
  44. package/src/db/pg/schema.ts +82 -0
  45. package/src/db/schema.ts +90 -0
  46. package/src/i18n/coverage.generated.ts +2 -2
  47. package/src/i18n/locales/public/en.po +8 -0
  48. package/src/i18n/locales/public/zh-Hans.po +8 -0
  49. package/src/i18n/locales/public/zh-Hant.po +8 -0
  50. package/src/i18n/locales/settings/en.po +135 -0
  51. package/src/i18n/locales/settings/en.ts +1 -1
  52. package/src/i18n/locales/settings/zh-Hans.po +136 -1
  53. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  54. package/src/i18n/locales/settings/zh-Hant.po +136 -1
  55. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  56. package/src/lib/__tests__/image-dimensions.test.ts +314 -0
  57. package/src/lib/__tests__/mp4-track-flags.test.ts +117 -0
  58. package/src/lib/__tests__/telegram-entities.test.ts +180 -0
  59. package/src/lib/__tests__/telegram-pool-webhooks.test.ts +127 -0
  60. package/src/lib/env.ts +45 -0
  61. package/src/lib/ids.ts +3 -0
  62. package/src/lib/image-dimensions.ts +258 -0
  63. package/src/lib/mp4-track-flags.ts +71 -0
  64. package/src/lib/telegram-entities.ts +240 -0
  65. package/src/lib/telegram-pool-webhooks.ts +86 -0
  66. package/src/lib/telegram-settings-status.tsx +109 -0
  67. package/src/lib/telegram.ts +363 -0
  68. package/src/node/runtime.ts +6 -0
  69. package/src/routes/api/__tests__/telegram.test.ts +612 -0
  70. package/src/routes/api/telegram.ts +782 -0
  71. package/src/routes/api/upload-multipart.ts +34 -12
  72. package/src/routes/api/upload.ts +23 -2
  73. package/src/routes/dash/settings.tsx +131 -1
  74. package/src/routes/pages/__tests__/post-page-title.test.ts +70 -0
  75. package/src/routes/pages/page.tsx +3 -2
  76. package/src/runtime/cloudflare.ts +20 -9
  77. package/src/runtime/node.ts +20 -9
  78. package/src/runtime/site.ts +2 -1
  79. package/src/services/__tests__/telegram.test.ts +148 -0
  80. package/src/services/index.ts +9 -0
  81. package/src/services/telegram.ts +613 -0
  82. package/src/services/upload-session.ts +39 -12
  83. package/src/styles/tokens.css +1 -0
  84. package/src/styles/ui.css +134 -38
  85. package/src/types/app-context.ts +6 -0
  86. package/src/types/bindings.ts +3 -0
  87. package/src/types/config.ts +40 -0
  88. package/src/ui/dash/settings/SettingsRootContent.tsx +48 -17
  89. package/src/ui/dash/settings/TelegramContent.tsx +549 -0
  90. package/src/ui/feed/ThreadPreview.tsx +91 -38
  91. package/src/ui/feed/__tests__/thread-preview.test.ts +67 -5
  92. package/src/ui/pages/PostPage.tsx +78 -15
  93. package/dist/app-BgMwEN-M.js +0 -6
  94. package/dist/client/_assets/client-CJQYvkEx.js +0 -274
  95. package/dist/client/_assets/client-CQvi1Buw.css +0 -2
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Telegram Bot API client.
3
+ *
4
+ * Thin `fetch` wrappers around the handful of Bot API methods the Telegram
5
+ * integration needs, plus helpers for working with bot tokens and deep links.
6
+ * All network calls go to `https://api.telegram.org/bot<token>/<method>`.
7
+ */
8
+
9
+ const TELEGRAM_API_BASE = "https://api.telegram.org";
10
+
11
+ /** A Telegram user as it appears in `message.from` / `callback_query.from`. */
12
+ export interface TelegramUser {
13
+ id: number;
14
+ is_bot: boolean;
15
+ first_name: string;
16
+ username?: string;
17
+ }
18
+
19
+ /**
20
+ * One styled span within a Telegram message.
21
+ *
22
+ * `offset` and `length` are measured in UTF-16 code units, which matches
23
+ * JavaScript string indexing — `text.slice(offset, offset + length)` returns
24
+ * the entity's visible text directly. See
25
+ * https://core.telegram.org/bots/api#messageentity for the full type list.
26
+ */
27
+ export interface TelegramMessageEntity {
28
+ type: string;
29
+ offset: number;
30
+ length: number;
31
+ /** Present on `text_link` entities. */
32
+ url?: string;
33
+ /** Present on `pre` entities when the user picked a language. */
34
+ language?: string;
35
+ }
36
+
37
+ /** One rendered size of a Telegram photo. */
38
+ export interface TelegramPhotoSize {
39
+ file_id: string;
40
+ file_unique_id: string;
41
+ width: number;
42
+ height: number;
43
+ file_size?: number;
44
+ }
45
+
46
+ /** A Telegram video message attachment. */
47
+ export interface TelegramVideo {
48
+ file_id: string;
49
+ file_unique_id: string;
50
+ width: number;
51
+ height: number;
52
+ duration: number;
53
+ mime_type?: string;
54
+ file_name?: string;
55
+ file_size?: number;
56
+ /** Auto-generated preview frame; downloadable as a normal PhotoSize. */
57
+ thumbnail?: TelegramPhotoSize;
58
+ }
59
+
60
+ /** A Telegram document message attachment (arbitrary file). */
61
+ export interface TelegramDocument {
62
+ file_id: string;
63
+ file_unique_id: string;
64
+ mime_type?: string;
65
+ file_name?: string;
66
+ file_size?: number;
67
+ /** Mime-appropriate preview, when one was generated. */
68
+ thumbnail?: TelegramPhotoSize;
69
+ }
70
+
71
+ /** Subset of the Telegram `Message` object the integration consumes. */
72
+ export interface TelegramMessage {
73
+ message_id: number;
74
+ from?: TelegramUser;
75
+ chat: { id: number };
76
+ text?: string;
77
+ entities?: TelegramMessageEntity[];
78
+ /** Album grouping key — present on every item in a multi-attachment send. */
79
+ media_group_id?: string;
80
+ /** Caption supplied with a photo/video/document; same entity grammar as `text`. */
81
+ caption?: string;
82
+ caption_entities?: TelegramMessageEntity[];
83
+ /** Ordered list of rendered sizes; the last entry is the highest resolution. */
84
+ photo?: TelegramPhotoSize[];
85
+ video?: TelegramVideo;
86
+ document?: TelegramDocument;
87
+ }
88
+
89
+ /** Subset of the Telegram `CallbackQuery` object the integration consumes. */
90
+ export interface TelegramCallbackQuery {
91
+ id: string;
92
+ from: TelegramUser;
93
+ message?: TelegramMessage;
94
+ data?: string;
95
+ }
96
+
97
+ /** Subset of the Telegram `Update` object delivered to the webhook. */
98
+ export interface TelegramUpdate {
99
+ update_id: number;
100
+ message?: TelegramMessage;
101
+ callback_query?: TelegramCallbackQuery;
102
+ }
103
+
104
+ /** An inline keyboard button — either a deep link or a callback action. */
105
+ export interface TelegramInlineButton {
106
+ text: string;
107
+ url?: string;
108
+ callback_data?: string;
109
+ }
110
+
111
+ export interface TelegramInlineKeyboard {
112
+ inline_keyboard: TelegramInlineButton[][];
113
+ }
114
+
115
+ export class TelegramApiError extends Error {
116
+ constructor(
117
+ public readonly method: string,
118
+ public readonly description: string,
119
+ ) {
120
+ super(`Telegram ${method} failed: ${description}`);
121
+ this.name = "TelegramApiError";
122
+ }
123
+ }
124
+
125
+ interface TelegramApiResponse<T> {
126
+ ok: boolean;
127
+ result?: T;
128
+ description?: string;
129
+ }
130
+
131
+ async function callTelegram<T>(
132
+ token: string,
133
+ method: string,
134
+ body?: Record<string, unknown>,
135
+ ): Promise<T> {
136
+ const response = await fetch(`${TELEGRAM_API_BASE}/bot${token}/${method}`, {
137
+ method: "POST",
138
+ headers: { "content-type": "application/json" },
139
+ body: JSON.stringify(body ?? {}),
140
+ });
141
+ const payload = (await response.json()) as TelegramApiResponse<T>;
142
+ if (!payload.ok) {
143
+ throw new TelegramApiError(method, payload.description ?? "unknown error");
144
+ }
145
+ return payload.result as T;
146
+ }
147
+
148
+ /**
149
+ * Extracts the numeric bot id from a bot token.
150
+ *
151
+ * A Telegram token is `<bot_id>:<secret>`, so the bot id is intrinsic and
152
+ * stable — no API call required.
153
+ *
154
+ * @param token - Full bot token
155
+ * @returns The bot id, or an empty string when the token is malformed
156
+ * @example
157
+ * parseBotId("123456:ABC-DEF"); // "123456"
158
+ */
159
+ export function parseBotId(token: string): string {
160
+ const botId = token.split(":")[0]?.trim() ?? "";
161
+ return /^\d+$/.test(botId) ? botId : "";
162
+ }
163
+
164
+ /**
165
+ * Builds a `t.me` deep link that pre-fills `/start <code>` for a bot.
166
+ *
167
+ * @param botUsername - Bot username without the leading `@`
168
+ * @param code - Binding code to pass as the `start` parameter
169
+ * @returns The deep link URL
170
+ * @example
171
+ * buildDeepLink("JantBot", "abc123"); // "https://t.me/JantBot?start=abc123"
172
+ */
173
+ export function buildDeepLink(botUsername: string, code: string): string {
174
+ return `https://t.me/${botUsername}?start=${encodeURIComponent(code)}`;
175
+ }
176
+
177
+ /** Bot identity returned by `getMe`. */
178
+ export interface TelegramBotIdentity {
179
+ id: number;
180
+ username: string;
181
+ }
182
+
183
+ /**
184
+ * Validates a bot token and returns the bot's identity.
185
+ *
186
+ * @param token - Bot token to validate
187
+ * @returns The bot's numeric id and username
188
+ * @throws {TelegramApiError} When the token is invalid
189
+ */
190
+ export async function getMe(token: string): Promise<TelegramBotIdentity> {
191
+ const result = await callTelegram<TelegramUser>(token, "getMe");
192
+ return { id: result.id, username: result.username ?? "" };
193
+ }
194
+
195
+ /**
196
+ * Registers a webhook URL for a bot.
197
+ *
198
+ * @param token - Bot token
199
+ * @param url - Public webhook URL Telegram should POST updates to
200
+ * @param secretToken - Value Telegram echoes back in the
201
+ * `X-Telegram-Bot-Api-Secret-Token` header so the handler can verify the call
202
+ */
203
+ export async function setWebhook(
204
+ token: string,
205
+ url: string,
206
+ secretToken: string,
207
+ ): Promise<void> {
208
+ await callTelegram(token, "setWebhook", {
209
+ url,
210
+ secret_token: secretToken,
211
+ allowed_updates: ["message", "callback_query"],
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Removes a bot's webhook.
217
+ *
218
+ * @param token - Bot token
219
+ */
220
+ export async function deleteWebhook(token: string): Promise<void> {
221
+ await callTelegram(token, "deleteWebhook");
222
+ }
223
+
224
+ /**
225
+ * Canonical bot command list. Telegram shows these in the `/` autocomplete
226
+ * popup and on the bot's profile. The list is persisted per-bot on Telegram's
227
+ * servers via `setMyCommands` — we only register commands the bot actually
228
+ * responds to. Anything else the user sends is treated as note content.
229
+ */
230
+ export const JANT_BOT_COMMANDS: ReadonlyArray<{
231
+ command: string;
232
+ description: string;
233
+ }> = [
234
+ {
235
+ command: "start",
236
+ description: "Connect this chat to a Jant site",
237
+ },
238
+ ];
239
+
240
+ /**
241
+ * Registers the bot's command list with Telegram so typing `/` in the chat
242
+ * shows autocomplete suggestions. Idempotent — safe to call on every boot.
243
+ *
244
+ * @param token - Bot token
245
+ * @param commands - Commands to register. Defaults to `JANT_BOT_COMMANDS`.
246
+ */
247
+ export async function setMyCommands(
248
+ token: string,
249
+ commands: ReadonlyArray<{
250
+ command: string;
251
+ description: string;
252
+ }> = JANT_BOT_COMMANDS,
253
+ ): Promise<void> {
254
+ await callTelegram(token, "setMyCommands", {
255
+ commands: commands.map((c) => ({
256
+ command: c.command,
257
+ description: c.description,
258
+ })),
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Returns the bot's currently registered webhook URL (empty when none).
264
+ *
265
+ * Lets callers skip a redundant `setWebhook` write when the webhook is
266
+ * already pointed at the right place.
267
+ *
268
+ * @param token - Bot token
269
+ * @returns The current webhook URL, or `""` when no webhook is set
270
+ */
271
+ export async function getWebhookUrl(token: string): Promise<string> {
272
+ const result = await callTelegram<{ url?: string }>(token, "getWebhookInfo");
273
+ return result.url ?? "";
274
+ }
275
+
276
+ /**
277
+ * Sends a text message, optionally with an inline keyboard.
278
+ *
279
+ * @param token - Bot token
280
+ * @param chatId - Target chat id
281
+ * @param text - Message body
282
+ * @param replyMarkup - Optional inline keyboard
283
+ */
284
+ export async function sendMessage(
285
+ token: string,
286
+ chatId: number,
287
+ text: string,
288
+ replyMarkup?: TelegramInlineKeyboard,
289
+ ): Promise<void> {
290
+ await callTelegram(token, "sendMessage", {
291
+ chat_id: chatId,
292
+ text,
293
+ ...(replyMarkup ? { reply_markup: replyMarkup } : {}),
294
+ });
295
+ }
296
+
297
+ /**
298
+ * Metadata returned by Telegram's `getFile`. The `file_path` is relative and
299
+ * must be appended to `/file/bot<token>/` to download the bytes.
300
+ */
301
+ export interface TelegramFile {
302
+ file_id: string;
303
+ file_unique_id: string;
304
+ file_size?: number;
305
+ file_path?: string;
306
+ }
307
+
308
+ /**
309
+ * Resolves a `file_id` to its downloadable path and size.
310
+ *
311
+ * Telegram's Bot API supports downloads up to 20 MB; the caller is expected to
312
+ * enforce its own limit using `file_size` before calling `downloadFile`.
313
+ *
314
+ * @param token - Bot token
315
+ * @param fileId - Identifier from a photo/video/document field
316
+ */
317
+ export async function getFile(
318
+ token: string,
319
+ fileId: string,
320
+ ): Promise<TelegramFile> {
321
+ return callTelegram<TelegramFile>(token, "getFile", { file_id: fileId });
322
+ }
323
+
324
+ /**
325
+ * Downloads the raw bytes for a `file_path` returned by `getFile`.
326
+ *
327
+ * Returns the underlying `Response` so callers can stream large bodies into
328
+ * storage without first materializing them in memory.
329
+ *
330
+ * @param token - Bot token
331
+ * @param filePath - The `file_path` from `getFile`
332
+ */
333
+ export async function downloadFile(
334
+ token: string,
335
+ filePath: string,
336
+ ): Promise<Response> {
337
+ const response = await fetch(
338
+ `${TELEGRAM_API_BASE}/file/bot${token}/${filePath}`,
339
+ );
340
+ if (!response.ok) {
341
+ throw new TelegramApiError(
342
+ "downloadFile",
343
+ `HTTP ${response.status} fetching ${filePath}`,
344
+ );
345
+ }
346
+ return response;
347
+ }
348
+
349
+ /**
350
+ * Acknowledges a callback query so Telegram stops showing a loading state on
351
+ * the tapped inline button.
352
+ *
353
+ * @param token - Bot token
354
+ * @param callbackQueryId - The callback query id to acknowledge
355
+ */
356
+ export async function answerCallbackQuery(
357
+ token: string,
358
+ callbackQueryId: string,
359
+ ): Promise<void> {
360
+ await callTelegram(token, "answerCallbackQuery", {
361
+ callback_query_id: callbackQueryId,
362
+ });
363
+ }
@@ -7,6 +7,7 @@ import {
7
7
  resolveHost,
8
8
  resolvePort,
9
9
  } from "./request-handler.js";
10
+ import { registerTelegramPoolWebhooks } from "../lib/telegram-pool-webhooks.js";
10
11
 
11
12
  export {
12
13
  applyNodeRuntimeEnvDefaults,
@@ -47,6 +48,11 @@ export async function start(
47
48
  didResolve = true;
48
49
  server.off("error", onError);
49
50
 
51
+ // Fire-and-forget: self-register managed-pool Telegram webhooks once
52
+ // the server is listening. Never awaited — it must not delay readiness
53
+ // or fail startup. No-ops unless this is a hosted deployment.
54
+ void registerTelegramPoolWebhooks(env);
55
+
50
56
  let closed = false;
51
57
  resolvePromise({
52
58
  server,