easy-ping 0.6.1 → 0.8.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/README.md +24 -4
- package/dist/{adapter-D3lTy-4E.d.cts → adapter-CV5Yt2c6.d.cts} +1 -1
- package/dist/{adapter-D3lTy-4E.d.ts → adapter-CV5Yt2c6.d.ts} +1 -1
- package/dist/adapters/drizzle.d.cts +1 -1
- package/dist/adapters/drizzle.d.ts +1 -1
- package/dist/adapters/mongodb.d.cts +2 -2
- package/dist/adapters/mongodb.d.ts +2 -2
- package/dist/adapters/mysql.d.cts +2 -2
- package/dist/adapters/mysql.d.ts +2 -2
- package/dist/adapters/postgres.d.cts +2 -2
- package/dist/adapters/postgres.d.ts +2 -2
- package/dist/adapters/sqlite.d.cts +2 -2
- package/dist/adapters/sqlite.d.ts +2 -2
- package/dist/chunk-R3BE67L7.js +13 -0
- package/dist/chunk-R3BE67L7.js.map +1 -0
- package/dist/chunk-SSHVZHSJ.js +100 -0
- package/dist/chunk-SSHVZHSJ.js.map +1 -0
- package/dist/client.cjs +7 -2
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +7 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -11
- package/dist/index.js.map +1 -1
- package/dist/{plugin-CHd2ZiGC.d.cts → plugin-6S8tpH0n.d.cts} +1 -1
- package/dist/{plugin-Cg9usuUi.d.ts → plugin-C4fikLl7.d.ts} +1 -1
- package/dist/plugins/digests.d.cts +2 -2
- package/dist/plugins/digests.d.ts +2 -2
- package/dist/plugins/mobile-push.cjs +307 -0
- package/dist/plugins/mobile-push.cjs.map +1 -0
- package/dist/plugins/mobile-push.d.cts +117 -0
- package/dist/plugins/mobile-push.d.ts +117 -0
- package/dist/plugins/mobile-push.js +261 -0
- package/dist/plugins/mobile-push.js.map +1 -0
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- package/dist/plugins/preferences-react.d.cts +1 -1
- package/dist/plugins/preferences-react.d.ts +1 -1
- package/dist/plugins/preferences.cjs +9 -1
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +2 -2
- package/dist/plugins/preferences.d.ts +2 -2
- package/dist/plugins/preferences.js +9 -1
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.d.cts +2 -2
- package/dist/plugins/push.d.ts +2 -2
- package/dist/plugins/telegram.cjs +379 -0
- package/dist/plugins/telegram.cjs.map +1 -0
- package/dist/plugins/telegram.d.cts +153 -0
- package/dist/plugins/telegram.d.ts +153 -0
- package/dist/plugins/telegram.js +313 -0
- package/dist/plugins/telegram.js.map +1 -0
- package/dist/providers/expo-push.cjs +114 -0
- package/dist/providers/expo-push.cjs.map +1 -0
- package/dist/providers/expo-push.d.cts +61 -0
- package/dist/providers/expo-push.d.ts +61 -0
- package/dist/providers/expo-push.js +109 -0
- package/dist/providers/expo-push.js.map +1 -0
- package/dist/providers/telegram.cjs +103 -0
- package/dist/providers/telegram.cjs.map +1 -0
- package/dist/providers/telegram.d.cts +83 -0
- package/dist/providers/telegram.d.ts +83 -0
- package/dist/providers/telegram.js +3 -0
- package/dist/providers/telegram.js.map +1 -0
- package/dist/providers/web-push.d.cts +2 -2
- package/dist/providers/web-push.d.ts +2 -2
- package/dist/react-native.cjs +637 -0
- package/dist/react-native.cjs.map +1 -0
- package/dist/react-native.d.cts +145 -0
- package/dist/react-native.d.ts +145 -0
- package/dist/react-native.js +609 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react.cjs +7 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +7 -2
- package/dist/react.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +38 -4
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/providers/expo-push/index.ts
|
|
4
|
+
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,}\]$/.test(token);
|
|
5
|
+
var EXPO_SEND_BATCH = 100;
|
|
6
|
+
var EXPO_RECEIPT_BATCH = 1e3;
|
|
7
|
+
var GONE = /* @__PURE__ */ new Set(["DeviceNotRegistered"]);
|
|
8
|
+
var RETRYABLE = /* @__PURE__ */ new Set(["MessageRateExceeded", "ProviderError", "ExpoError"]);
|
|
9
|
+
var classify = (code, message) => ({
|
|
10
|
+
error: code ? `${code}: ${message ?? ""}`.trim() : message ?? "expo push failed",
|
|
11
|
+
gone: code !== void 0 && GONE.has(code),
|
|
12
|
+
retryable: code !== void 0 && RETRYABLE.has(code)
|
|
13
|
+
});
|
|
14
|
+
function expoPush(options = {}) {
|
|
15
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
16
|
+
const base = (options.apiBase ?? "https://exp.host").replace(/\/$/, "");
|
|
17
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
18
|
+
const redact = (text) => options.accessToken ? text.split(options.accessToken).join("<token>") : text;
|
|
19
|
+
async function post(path, body, signal) {
|
|
20
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
21
|
+
const headers = {
|
|
22
|
+
accept: "application/json",
|
|
23
|
+
"content-type": "application/json"
|
|
24
|
+
};
|
|
25
|
+
if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;
|
|
26
|
+
const response = await doFetch(`${base}${path}`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers,
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const text = redact((await response.text().catch(() => "")).slice(0, 200));
|
|
34
|
+
const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());
|
|
35
|
+
error.status = response.status;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return await response.json();
|
|
39
|
+
}
|
|
40
|
+
const transportFailure = (error) => {
|
|
41
|
+
const status = error?.status;
|
|
42
|
+
const message = redact(error instanceof Error ? error.message : String(error));
|
|
43
|
+
const retryable = status === void 0 || status === 429 || status >= 500;
|
|
44
|
+
return { ok: false, error: message, gone: false, retryable };
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
name: "expo-push",
|
|
48
|
+
isValidToken: isExpoPushToken,
|
|
49
|
+
async send(messages, signal) {
|
|
50
|
+
const results = [];
|
|
51
|
+
for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {
|
|
52
|
+
const batch = messages.slice(start, start + EXPO_SEND_BATCH);
|
|
53
|
+
let tickets;
|
|
54
|
+
try {
|
|
55
|
+
const response = await post(
|
|
56
|
+
"/--/api/v2/push/send",
|
|
57
|
+
batch.map((message) => ({
|
|
58
|
+
to: message.token,
|
|
59
|
+
title: message.title,
|
|
60
|
+
body: message.body,
|
|
61
|
+
...message.data ? { data: message.data } : {},
|
|
62
|
+
...message.badge === void 0 ? {} : { badge: message.badge },
|
|
63
|
+
...message.sound === void 0 ? { sound: "default" } : { sound: message.sound },
|
|
64
|
+
...message.channelId ? { channelId: message.channelId } : {}
|
|
65
|
+
})),
|
|
66
|
+
signal
|
|
67
|
+
);
|
|
68
|
+
tickets = response.data;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const failure = transportFailure(error);
|
|
71
|
+
for (const _ of batch) results.push(failure);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
batch.forEach((_, index) => {
|
|
75
|
+
const ticket = tickets[index];
|
|
76
|
+
if (!ticket) {
|
|
77
|
+
results.push({ ok: false, error: "no ticket returned", gone: false, retryable: true });
|
|
78
|
+
} else if (ticket.status === "ok") {
|
|
79
|
+
results.push({ ok: true, ticketId: ticket.id ?? null });
|
|
80
|
+
} else {
|
|
81
|
+
results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
},
|
|
87
|
+
async receipts(ticketIds, signal) {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {
|
|
90
|
+
const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);
|
|
91
|
+
const response = await post(
|
|
92
|
+
"/--/api/v2/push/getReceipts",
|
|
93
|
+
{ ids },
|
|
94
|
+
signal
|
|
95
|
+
);
|
|
96
|
+
for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {
|
|
97
|
+
if (receipt.status === "ok") out.push({ ticketId, ok: true });
|
|
98
|
+
else {
|
|
99
|
+
const { error, gone } = classify(receipt.details?.error, receipt.message);
|
|
100
|
+
out.push({ ticketId, ok: false, error, gone });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
exports.EXPO_RECEIPT_BATCH = EXPO_RECEIPT_BATCH;
|
|
110
|
+
exports.EXPO_SEND_BATCH = EXPO_SEND_BATCH;
|
|
111
|
+
exports.expoPush = expoPush;
|
|
112
|
+
exports.isExpoPushToken = isExpoPushToken;
|
|
113
|
+
//# sourceMappingURL=expo-push.cjs.map
|
|
114
|
+
//# sourceMappingURL=expo-push.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/expo-push/index.ts"],"names":[],"mappings":"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","file":"expo-push.cjs","sourcesContent":["export type MobilePushMessage = {\n /** The device's push token as the app registered it. */\n token: string;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n badge?: number | undefined;\n /** \"default\" plays the platform sound; null is silent. */\n sound?: \"default\" | null | undefined;\n /** Android notification channel id. */\n channelId?: string | undefined;\n};\n\nexport type MobilePushTicket =\n | { ok: true; ticketId: string | null }\n | {\n ok: false;\n error: string;\n /** The token will never work again (app uninstalled, token rotated). Prune it. */\n gone: boolean;\n retryable: boolean;\n };\n\nexport type MobilePushReceipt =\n | { ticketId: string; ok: true }\n | { ticketId: string; ok: false; error: string; gone: boolean };\n\nexport type MobilePushProvider = {\n name: string;\n /** True for a token this provider can deliver to. Rejected at registration otherwise. */\n isValidToken(token: string): boolean;\n /** One result per message, in order. Never throws for a per-token failure. */\n send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;\n /**\n * Some services (Expo) accept a message, then learn from APNs/FCM later\n * that the device is gone. Receipts carry that late verdict.\n */\n receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;\n};\n\nexport type ExpoPushOptions = {\n /** Expo \"enhanced security\" access token. Optional; when set, requests without it are refused by Expo. */\n accessToken?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://exp.host. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ExpoTicket =\n | { status: \"ok\"; id: string }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\ntype ExpoReceipt =\n | { status: \"ok\" }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\n/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */\nexport const isExpoPushToken = (token: string): boolean =>\n /^Expo(nent)?PushToken\\[[A-Za-z0-9_-]{8,}\\]$/.test(token);\n\n/** Expo's send endpoint takes at most this many messages per request. */\nexport const EXPO_SEND_BATCH = 100;\n/** And its receipts endpoint at most this many ids. */\nexport const EXPO_RECEIPT_BATCH = 1000;\n\n// Expo's error codes, per https://docs.expo.dev/push-notifications/sending-notifications/#individual-errors\nconst GONE = new Set([\"DeviceNotRegistered\"]);\nconst RETRYABLE = new Set([\"MessageRateExceeded\", \"ProviderError\", \"ExpoError\"]);\n\nconst classify = (\n code: string | undefined,\n message: string | undefined,\n): { error: string; gone: boolean; retryable: boolean } => ({\n error: code ? `${code}: ${message ?? \"\"}`.trim() : (message ?? \"expo push failed\"),\n gone: code !== undefined && GONE.has(code),\n retryable: code !== undefined && RETRYABLE.has(code),\n});\n\n/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */\nexport function expoPush(options: ExpoPushOptions = {}): MobilePushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = (options.apiBase ?? \"https://exp.host\").replace(/\\/$/, \"\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) =>\n options.accessToken ? text.split(options.accessToken).join(\"<token>\") : text;\n\n async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const timeout = AbortSignal.timeout(timeoutMs);\n const headers: Record<string, string> = {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n };\n if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;\n const response = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n });\n if (!response.ok) {\n const text = redact((await response.text().catch(() => \"\")).slice(0, 200));\n const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());\n (error as { status?: number }).status = response.status;\n throw error;\n }\n return (await response.json()) as T;\n }\n\n const transportFailure = (error: unknown): MobilePushTicket => {\n const status = (error as { status?: number })?.status;\n const message = redact(error instanceof Error ? error.message : String(error));\n // 4xx from the service itself (bad access token, malformed request) cannot be fixed by retrying.\n const retryable = status === undefined || status === 429 || status >= 500;\n return { ok: false, error: message, gone: false, retryable };\n };\n\n return {\n name: \"expo-push\",\n isValidToken: isExpoPushToken,\n\n async send(messages, signal) {\n const results: MobilePushTicket[] = [];\n for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {\n const batch = messages.slice(start, start + EXPO_SEND_BATCH);\n let tickets: ExpoTicket[];\n try {\n const response = await post<{ data: ExpoTicket[] }>(\n \"/--/api/v2/push/send\",\n batch.map((message) => ({\n to: message.token,\n title: message.title,\n body: message.body,\n ...(message.data ? { data: message.data } : {}),\n ...(message.badge === undefined ? {} : { badge: message.badge }),\n ...(message.sound === undefined ? { sound: \"default\" } : { sound: message.sound }),\n ...(message.channelId ? { channelId: message.channelId } : {}),\n })),\n signal,\n );\n tickets = response.data;\n } catch (error) {\n const failure = transportFailure(error);\n for (const _ of batch) results.push(failure);\n continue;\n }\n batch.forEach((_, index) => {\n const ticket = tickets[index];\n if (!ticket) {\n results.push({ ok: false, error: \"no ticket returned\", gone: false, retryable: true });\n } else if (ticket.status === \"ok\") {\n results.push({ ok: true, ticketId: ticket.id ?? null });\n } else {\n results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });\n }\n });\n }\n return results;\n },\n\n async receipts(ticketIds, signal) {\n const out: MobilePushReceipt[] = [];\n for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {\n const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);\n const response = await post<{ data: Record<string, ExpoReceipt> }>(\n \"/--/api/v2/push/getReceipts\",\n { ids },\n signal,\n );\n for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {\n if (receipt.status === \"ok\") out.push({ ticketId, ok: true });\n else {\n const { error, gone } = classify(receipt.details?.error, receipt.message);\n out.push({ ticketId, ok: false, error, gone });\n }\n }\n }\n return out;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type MobilePushMessage = {
|
|
2
|
+
/** The device's push token as the app registered it. */
|
|
3
|
+
token: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
data?: Record<string, unknown> | undefined;
|
|
7
|
+
badge?: number | undefined;
|
|
8
|
+
/** "default" plays the platform sound; null is silent. */
|
|
9
|
+
sound?: "default" | null | undefined;
|
|
10
|
+
/** Android notification channel id. */
|
|
11
|
+
channelId?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
type MobilePushTicket = {
|
|
14
|
+
ok: true;
|
|
15
|
+
ticketId: string | null;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
/** The token will never work again (app uninstalled, token rotated). Prune it. */
|
|
20
|
+
gone: boolean;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
};
|
|
23
|
+
type MobilePushReceipt = {
|
|
24
|
+
ticketId: string;
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ticketId: string;
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
gone: boolean;
|
|
31
|
+
};
|
|
32
|
+
type MobilePushProvider = {
|
|
33
|
+
name: string;
|
|
34
|
+
/** True for a token this provider can deliver to. Rejected at registration otherwise. */
|
|
35
|
+
isValidToken(token: string): boolean;
|
|
36
|
+
/** One result per message, in order. Never throws for a per-token failure. */
|
|
37
|
+
send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Some services (Expo) accept a message, then learn from APNs/FCM later
|
|
40
|
+
* that the device is gone. Receipts carry that late verdict.
|
|
41
|
+
*/
|
|
42
|
+
receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;
|
|
43
|
+
};
|
|
44
|
+
type ExpoPushOptions = {
|
|
45
|
+
/** Expo "enhanced security" access token. Optional; when set, requests without it are refused by Expo. */
|
|
46
|
+
accessToken?: string | undefined;
|
|
47
|
+
fetch?: typeof globalThis.fetch | undefined;
|
|
48
|
+
/** Default https://exp.host. A local fake for tests. */
|
|
49
|
+
apiBase?: string | undefined;
|
|
50
|
+
timeoutMs?: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */
|
|
53
|
+
declare const isExpoPushToken: (token: string) => boolean;
|
|
54
|
+
/** Expo's send endpoint takes at most this many messages per request. */
|
|
55
|
+
declare const EXPO_SEND_BATCH = 100;
|
|
56
|
+
/** And its receipts endpoint at most this many ids. */
|
|
57
|
+
declare const EXPO_RECEIPT_BATCH = 1000;
|
|
58
|
+
/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */
|
|
59
|
+
declare function expoPush(options?: ExpoPushOptions): MobilePushProvider;
|
|
60
|
+
|
|
61
|
+
export { EXPO_RECEIPT_BATCH, EXPO_SEND_BATCH, type ExpoPushOptions, type MobilePushMessage, type MobilePushProvider, type MobilePushReceipt, type MobilePushTicket, expoPush, isExpoPushToken };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type MobilePushMessage = {
|
|
2
|
+
/** The device's push token as the app registered it. */
|
|
3
|
+
token: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
data?: Record<string, unknown> | undefined;
|
|
7
|
+
badge?: number | undefined;
|
|
8
|
+
/** "default" plays the platform sound; null is silent. */
|
|
9
|
+
sound?: "default" | null | undefined;
|
|
10
|
+
/** Android notification channel id. */
|
|
11
|
+
channelId?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
type MobilePushTicket = {
|
|
14
|
+
ok: true;
|
|
15
|
+
ticketId: string | null;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
/** The token will never work again (app uninstalled, token rotated). Prune it. */
|
|
20
|
+
gone: boolean;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
};
|
|
23
|
+
type MobilePushReceipt = {
|
|
24
|
+
ticketId: string;
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ticketId: string;
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
gone: boolean;
|
|
31
|
+
};
|
|
32
|
+
type MobilePushProvider = {
|
|
33
|
+
name: string;
|
|
34
|
+
/** True for a token this provider can deliver to. Rejected at registration otherwise. */
|
|
35
|
+
isValidToken(token: string): boolean;
|
|
36
|
+
/** One result per message, in order. Never throws for a per-token failure. */
|
|
37
|
+
send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Some services (Expo) accept a message, then learn from APNs/FCM later
|
|
40
|
+
* that the device is gone. Receipts carry that late verdict.
|
|
41
|
+
*/
|
|
42
|
+
receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;
|
|
43
|
+
};
|
|
44
|
+
type ExpoPushOptions = {
|
|
45
|
+
/** Expo "enhanced security" access token. Optional; when set, requests without it are refused by Expo. */
|
|
46
|
+
accessToken?: string | undefined;
|
|
47
|
+
fetch?: typeof globalThis.fetch | undefined;
|
|
48
|
+
/** Default https://exp.host. A local fake for tests. */
|
|
49
|
+
apiBase?: string | undefined;
|
|
50
|
+
timeoutMs?: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */
|
|
53
|
+
declare const isExpoPushToken: (token: string) => boolean;
|
|
54
|
+
/** Expo's send endpoint takes at most this many messages per request. */
|
|
55
|
+
declare const EXPO_SEND_BATCH = 100;
|
|
56
|
+
/** And its receipts endpoint at most this many ids. */
|
|
57
|
+
declare const EXPO_RECEIPT_BATCH = 1000;
|
|
58
|
+
/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */
|
|
59
|
+
declare function expoPush(options?: ExpoPushOptions): MobilePushProvider;
|
|
60
|
+
|
|
61
|
+
export { EXPO_RECEIPT_BATCH, EXPO_SEND_BATCH, type ExpoPushOptions, type MobilePushMessage, type MobilePushProvider, type MobilePushReceipt, type MobilePushTicket, expoPush, isExpoPushToken };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/providers/expo-push/index.ts
|
|
2
|
+
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,}\]$/.test(token);
|
|
3
|
+
var EXPO_SEND_BATCH = 100;
|
|
4
|
+
var EXPO_RECEIPT_BATCH = 1e3;
|
|
5
|
+
var GONE = /* @__PURE__ */ new Set(["DeviceNotRegistered"]);
|
|
6
|
+
var RETRYABLE = /* @__PURE__ */ new Set(["MessageRateExceeded", "ProviderError", "ExpoError"]);
|
|
7
|
+
var classify = (code, message) => ({
|
|
8
|
+
error: code ? `${code}: ${message ?? ""}`.trim() : message ?? "expo push failed",
|
|
9
|
+
gone: code !== void 0 && GONE.has(code),
|
|
10
|
+
retryable: code !== void 0 && RETRYABLE.has(code)
|
|
11
|
+
});
|
|
12
|
+
function expoPush(options = {}) {
|
|
13
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
14
|
+
const base = (options.apiBase ?? "https://exp.host").replace(/\/$/, "");
|
|
15
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
16
|
+
const redact = (text) => options.accessToken ? text.split(options.accessToken).join("<token>") : text;
|
|
17
|
+
async function post(path, body, signal) {
|
|
18
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
19
|
+
const headers = {
|
|
20
|
+
accept: "application/json",
|
|
21
|
+
"content-type": "application/json"
|
|
22
|
+
};
|
|
23
|
+
if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;
|
|
24
|
+
const response = await doFetch(`${base}${path}`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers,
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const text = redact((await response.text().catch(() => "")).slice(0, 200));
|
|
32
|
+
const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());
|
|
33
|
+
error.status = response.status;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
return await response.json();
|
|
37
|
+
}
|
|
38
|
+
const transportFailure = (error) => {
|
|
39
|
+
const status = error?.status;
|
|
40
|
+
const message = redact(error instanceof Error ? error.message : String(error));
|
|
41
|
+
const retryable = status === void 0 || status === 429 || status >= 500;
|
|
42
|
+
return { ok: false, error: message, gone: false, retryable };
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
name: "expo-push",
|
|
46
|
+
isValidToken: isExpoPushToken,
|
|
47
|
+
async send(messages, signal) {
|
|
48
|
+
const results = [];
|
|
49
|
+
for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {
|
|
50
|
+
const batch = messages.slice(start, start + EXPO_SEND_BATCH);
|
|
51
|
+
let tickets;
|
|
52
|
+
try {
|
|
53
|
+
const response = await post(
|
|
54
|
+
"/--/api/v2/push/send",
|
|
55
|
+
batch.map((message) => ({
|
|
56
|
+
to: message.token,
|
|
57
|
+
title: message.title,
|
|
58
|
+
body: message.body,
|
|
59
|
+
...message.data ? { data: message.data } : {},
|
|
60
|
+
...message.badge === void 0 ? {} : { badge: message.badge },
|
|
61
|
+
...message.sound === void 0 ? { sound: "default" } : { sound: message.sound },
|
|
62
|
+
...message.channelId ? { channelId: message.channelId } : {}
|
|
63
|
+
})),
|
|
64
|
+
signal
|
|
65
|
+
);
|
|
66
|
+
tickets = response.data;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const failure = transportFailure(error);
|
|
69
|
+
for (const _ of batch) results.push(failure);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
batch.forEach((_, index) => {
|
|
73
|
+
const ticket = tickets[index];
|
|
74
|
+
if (!ticket) {
|
|
75
|
+
results.push({ ok: false, error: "no ticket returned", gone: false, retryable: true });
|
|
76
|
+
} else if (ticket.status === "ok") {
|
|
77
|
+
results.push({ ok: true, ticketId: ticket.id ?? null });
|
|
78
|
+
} else {
|
|
79
|
+
results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
},
|
|
85
|
+
async receipts(ticketIds, signal) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {
|
|
88
|
+
const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);
|
|
89
|
+
const response = await post(
|
|
90
|
+
"/--/api/v2/push/getReceipts",
|
|
91
|
+
{ ids },
|
|
92
|
+
signal
|
|
93
|
+
);
|
|
94
|
+
for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {
|
|
95
|
+
if (receipt.status === "ok") out.push({ ticketId, ok: true });
|
|
96
|
+
else {
|
|
97
|
+
const { error, gone } = classify(receipt.details?.error, receipt.message);
|
|
98
|
+
out.push({ ticketId, ok: false, error, gone });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { EXPO_RECEIPT_BATCH, EXPO_SEND_BATCH, expoPush, isExpoPushToken };
|
|
108
|
+
//# sourceMappingURL=expo-push.js.map
|
|
109
|
+
//# sourceMappingURL=expo-push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/expo-push/index.ts"],"names":[],"mappings":"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","file":"expo-push.js","sourcesContent":["export type MobilePushMessage = {\n /** The device's push token as the app registered it. */\n token: string;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n badge?: number | undefined;\n /** \"default\" plays the platform sound; null is silent. */\n sound?: \"default\" | null | undefined;\n /** Android notification channel id. */\n channelId?: string | undefined;\n};\n\nexport type MobilePushTicket =\n | { ok: true; ticketId: string | null }\n | {\n ok: false;\n error: string;\n /** The token will never work again (app uninstalled, token rotated). Prune it. */\n gone: boolean;\n retryable: boolean;\n };\n\nexport type MobilePushReceipt =\n | { ticketId: string; ok: true }\n | { ticketId: string; ok: false; error: string; gone: boolean };\n\nexport type MobilePushProvider = {\n name: string;\n /** True for a token this provider can deliver to. Rejected at registration otherwise. */\n isValidToken(token: string): boolean;\n /** One result per message, in order. Never throws for a per-token failure. */\n send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;\n /**\n * Some services (Expo) accept a message, then learn from APNs/FCM later\n * that the device is gone. Receipts carry that late verdict.\n */\n receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;\n};\n\nexport type ExpoPushOptions = {\n /** Expo \"enhanced security\" access token. Optional; when set, requests without it are refused by Expo. */\n accessToken?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://exp.host. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ExpoTicket =\n | { status: \"ok\"; id: string }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\ntype ExpoReceipt =\n | { status: \"ok\" }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\n/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */\nexport const isExpoPushToken = (token: string): boolean =>\n /^Expo(nent)?PushToken\\[[A-Za-z0-9_-]{8,}\\]$/.test(token);\n\n/** Expo's send endpoint takes at most this many messages per request. */\nexport const EXPO_SEND_BATCH = 100;\n/** And its receipts endpoint at most this many ids. */\nexport const EXPO_RECEIPT_BATCH = 1000;\n\n// Expo's error codes, per https://docs.expo.dev/push-notifications/sending-notifications/#individual-errors\nconst GONE = new Set([\"DeviceNotRegistered\"]);\nconst RETRYABLE = new Set([\"MessageRateExceeded\", \"ProviderError\", \"ExpoError\"]);\n\nconst classify = (\n code: string | undefined,\n message: string | undefined,\n): { error: string; gone: boolean; retryable: boolean } => ({\n error: code ? `${code}: ${message ?? \"\"}`.trim() : (message ?? \"expo push failed\"),\n gone: code !== undefined && GONE.has(code),\n retryable: code !== undefined && RETRYABLE.has(code),\n});\n\n/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */\nexport function expoPush(options: ExpoPushOptions = {}): MobilePushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = (options.apiBase ?? \"https://exp.host\").replace(/\\/$/, \"\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) =>\n options.accessToken ? text.split(options.accessToken).join(\"<token>\") : text;\n\n async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const timeout = AbortSignal.timeout(timeoutMs);\n const headers: Record<string, string> = {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n };\n if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;\n const response = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n });\n if (!response.ok) {\n const text = redact((await response.text().catch(() => \"\")).slice(0, 200));\n const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());\n (error as { status?: number }).status = response.status;\n throw error;\n }\n return (await response.json()) as T;\n }\n\n const transportFailure = (error: unknown): MobilePushTicket => {\n const status = (error as { status?: number })?.status;\n const message = redact(error instanceof Error ? error.message : String(error));\n // 4xx from the service itself (bad access token, malformed request) cannot be fixed by retrying.\n const retryable = status === undefined || status === 429 || status >= 500;\n return { ok: false, error: message, gone: false, retryable };\n };\n\n return {\n name: \"expo-push\",\n isValidToken: isExpoPushToken,\n\n async send(messages, signal) {\n const results: MobilePushTicket[] = [];\n for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {\n const batch = messages.slice(start, start + EXPO_SEND_BATCH);\n let tickets: ExpoTicket[];\n try {\n const response = await post<{ data: ExpoTicket[] }>(\n \"/--/api/v2/push/send\",\n batch.map((message) => ({\n to: message.token,\n title: message.title,\n body: message.body,\n ...(message.data ? { data: message.data } : {}),\n ...(message.badge === undefined ? {} : { badge: message.badge }),\n ...(message.sound === undefined ? { sound: \"default\" } : { sound: message.sound }),\n ...(message.channelId ? { channelId: message.channelId } : {}),\n })),\n signal,\n );\n tickets = response.data;\n } catch (error) {\n const failure = transportFailure(error);\n for (const _ of batch) results.push(failure);\n continue;\n }\n batch.forEach((_, index) => {\n const ticket = tickets[index];\n if (!ticket) {\n results.push({ ok: false, error: \"no ticket returned\", gone: false, retryable: true });\n } else if (ticket.status === \"ok\") {\n results.push({ ok: true, ticketId: ticket.id ?? null });\n } else {\n results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });\n }\n });\n }\n return results;\n },\n\n async receipts(ticketIds, signal) {\n const out: MobilePushReceipt[] = [];\n for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {\n const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);\n const response = await post<{ data: Record<string, ExpoReceipt> }>(\n \"/--/api/v2/push/getReceipts\",\n { ids },\n signal,\n );\n for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {\n if (receipt.status === \"ok\") out.push({ ticketId, ok: true });\n else {\n const { error, gone } = classify(receipt.details?.error, receipt.message);\n out.push({ ticketId, ok: false, error, gone });\n }\n }\n }\n return out;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/providers/telegram/index.ts
|
|
4
|
+
var telegramStartLink = (botUsername, code) => `https://t.me/${botUsername.replace(/^@/, "")}?start=${encodeURIComponent(code)}`;
|
|
5
|
+
function telegramBot(options) {
|
|
6
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
7
|
+
const base = `${(options.apiBase ?? "https://api.telegram.org").replace(/\/$/, "")}/bot${options.token}`;
|
|
8
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
9
|
+
const redact = (text) => text.split(options.token).join("<token>");
|
|
10
|
+
async function call(method, body, signal, budgetMs = timeoutMs) {
|
|
11
|
+
const timeout = AbortSignal.timeout(budgetMs);
|
|
12
|
+
const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
13
|
+
const response = await doFetch(`${base}/${method}`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "content-type": "application/json" },
|
|
16
|
+
body: JSON.stringify(body),
|
|
17
|
+
signal: merged
|
|
18
|
+
});
|
|
19
|
+
const data = await response.json().catch(() => ({ ok: false }));
|
|
20
|
+
return { status: response.status, data };
|
|
21
|
+
}
|
|
22
|
+
async function must(method, body) {
|
|
23
|
+
const { data } = await call(method, body);
|
|
24
|
+
if (!data.ok)
|
|
25
|
+
throw new Error(redact(`telegram ${method} failed: ${data.description ?? "unknown"}`));
|
|
26
|
+
return data.result;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
name: "telegram",
|
|
30
|
+
async send(message) {
|
|
31
|
+
const body = {
|
|
32
|
+
chat_id: message.chatId,
|
|
33
|
+
text: message.text,
|
|
34
|
+
parse_mode: "HTML",
|
|
35
|
+
link_preview_options: { is_disabled: true }
|
|
36
|
+
};
|
|
37
|
+
if (message.buttons?.length) {
|
|
38
|
+
body.reply_markup = {
|
|
39
|
+
inline_keyboard: [
|
|
40
|
+
message.buttons.map((button) => ({ text: button.text, url: button.url }))
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
let status;
|
|
45
|
+
let data;
|
|
46
|
+
try {
|
|
47
|
+
({ status, data } = await call(
|
|
48
|
+
"sendMessage",
|
|
49
|
+
body,
|
|
50
|
+
message.signal
|
|
51
|
+
));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return { ok: false, error: redact(text), retryable: true, gone: false };
|
|
55
|
+
}
|
|
56
|
+
if (data.ok) return { ok: true, messageId: data.result.message_id };
|
|
57
|
+
const description = redact(data.description ?? `HTTP ${status}`);
|
|
58
|
+
const code = data.error_code ?? status;
|
|
59
|
+
if (code === 429) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: description,
|
|
63
|
+
retryable: true,
|
|
64
|
+
gone: false,
|
|
65
|
+
retryAfterSeconds: data.parameters?.retry_after
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const gone = code === 403 || code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description);
|
|
69
|
+
return { ok: false, error: description, retryable: code >= 500, gone };
|
|
70
|
+
},
|
|
71
|
+
async getUpdates({ offset, timeoutSeconds = 25, signal }) {
|
|
72
|
+
const { data } = await call(
|
|
73
|
+
"getUpdates",
|
|
74
|
+
{
|
|
75
|
+
...offset === void 0 ? {} : { offset },
|
|
76
|
+
timeout: timeoutSeconds,
|
|
77
|
+
allowed_updates: ["message", "my_chat_member"]
|
|
78
|
+
},
|
|
79
|
+
signal,
|
|
80
|
+
(timeoutSeconds + 10) * 1e3
|
|
81
|
+
);
|
|
82
|
+
if (!data.ok)
|
|
83
|
+
throw new Error(redact(`telegram getUpdates failed: ${data.description ?? "unknown"}`));
|
|
84
|
+
return data.result;
|
|
85
|
+
},
|
|
86
|
+
setWebhook: async (url, secretToken) => {
|
|
87
|
+
await must("setWebhook", {
|
|
88
|
+
url,
|
|
89
|
+
secret_token: secretToken,
|
|
90
|
+
allowed_updates: ["message", "my_chat_member"]
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
deleteWebhook: async () => {
|
|
94
|
+
await must("deleteWebhook", {});
|
|
95
|
+
},
|
|
96
|
+
getMe: () => must("getMe", {})
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
exports.telegramBot = telegramBot;
|
|
101
|
+
exports.telegramStartLink = telegramStartLink;
|
|
102
|
+
//# sourceMappingURL=telegram.cjs.map
|
|
103
|
+
//# sourceMappingURL=telegram.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/telegram/index.ts"],"names":[],"mappings":"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","file":"telegram.cjs","sourcesContent":["export type TelegramButton = { text: string; url: string };\n\nexport type TelegramMessage = {\n chatId: string;\n /** Telegram HTML: <b>, <i>, <a href>, <code>, <pre>. Escape user text with escapeHtml(). */\n text: string;\n buttons?: readonly TelegramButton[] | undefined;\n signal?: AbortSignal | undefined;\n};\n\nexport type TelegramSendResult =\n | { ok: true; messageId: number }\n | {\n ok: false;\n error: string;\n /** Worth another attempt: rate limit, 5xx, network. */\n retryable: boolean;\n /** The chat can never receive again (user blocked the bot, chat deleted). Prune it. */\n gone: boolean;\n retryAfterSeconds?: number | undefined;\n };\n\n/** The slice of a Bot API update the plugin reads. Everything else passes through untouched. */\nexport type TelegramUpdate = {\n update_id: number;\n message?: {\n text?: string;\n chat: { id: number | string; type: string; username?: string; title?: string };\n from?: { id: number; username?: string; first_name?: string };\n };\n my_chat_member?: {\n chat: { id: number | string; type: string; username?: string; title?: string };\n new_chat_member: { status: string };\n };\n};\n\nexport type TelegramProvider = {\n name: string;\n send(message: TelegramMessage): Promise<TelegramSendResult>;\n /** Long-polls for updates. For development or hosts without a public URL. */\n getUpdates(options: {\n offset?: number | undefined;\n timeoutSeconds?: number | undefined;\n signal?: AbortSignal | undefined;\n }): Promise<TelegramUpdate[]>;\n /** Points Telegram at your webhook route. Run once per deployment URL. */\n setWebhook(url: string, secretToken: string): Promise<void>;\n deleteWebhook(): Promise<void>;\n getMe(): Promise<{ id: number; username: string }>;\n};\n\nexport type TelegramBotOptions = {\n /** From @BotFather. Never appears in errors or logs. */\n token: string;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://api.telegram.org. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ApiResponse<T> =\n | { ok: true; result: T }\n | { ok: false; description?: string; error_code?: number; parameters?: { retry_after?: number } };\n\n/** Deep link that opens the bot with a start parameter. Codes are [A-Za-z0-9_-], at most 64 chars. */\nexport const telegramStartLink = (botUsername: string, code: string) =>\n `https://t.me/${botUsername.replace(/^@/, \"\")}?start=${encodeURIComponent(code)}`;\n\n/** Bot API over fetch, so it runs on edge runtimes. Failures are classified, never thrown, on send. */\nexport function telegramBot(options: TelegramBotOptions): TelegramProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = `${(options.apiBase ?? \"https://api.telegram.org\").replace(/\\/$/, \"\")}/bot${options.token}`;\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) => text.split(options.token).join(\"<token>\");\n\n async function call<T>(\n method: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n budgetMs = timeoutMs,\n ): Promise<{ status: number; data: ApiResponse<T> }> {\n const timeout = AbortSignal.timeout(budgetMs);\n const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;\n const response = await doFetch(`${base}/${method}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: merged,\n });\n const data = (await response.json().catch(() => ({ ok: false }))) as ApiResponse<T>;\n return { status: response.status, data };\n }\n\n async function must<T>(method: string, body: Record<string, unknown>): Promise<T> {\n const { data } = await call<T>(method, body);\n if (!data.ok)\n throw new Error(redact(`telegram ${method} failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n }\n\n return {\n name: \"telegram\",\n\n async send(message) {\n const body: Record<string, unknown> = {\n chat_id: message.chatId,\n text: message.text,\n parse_mode: \"HTML\",\n link_preview_options: { is_disabled: true },\n };\n if (message.buttons?.length) {\n body.reply_markup = {\n inline_keyboard: [\n message.buttons.map((button) => ({ text: button.text, url: button.url })),\n ],\n };\n }\n\n let status: number;\n let data: ApiResponse<{ message_id: number }>;\n try {\n ({ status, data } = await call<{ message_id: number }>(\n \"sendMessage\",\n body,\n message.signal,\n ));\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error);\n return { ok: false, error: redact(text), retryable: true, gone: false };\n }\n\n if (data.ok) return { ok: true, messageId: data.result.message_id };\n\n const description = redact(data.description ?? `HTTP ${status}`);\n const code = data.error_code ?? status;\n if (code === 429) {\n return {\n ok: false,\n error: description,\n retryable: true,\n gone: false,\n retryAfterSeconds: data.parameters?.retry_after,\n };\n }\n // 403: the user blocked the bot or was deactivated. 400 \"chat not found\": deleted or never existed.\n const gone =\n code === 403 ||\n (code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description));\n return { ok: false, error: description, retryable: code >= 500, gone };\n },\n\n async getUpdates({ offset, timeoutSeconds = 25, signal }) {\n // Telegram holds the request open for `timeout` seconds; the budget must outlive it.\n const { data } = await call<TelegramUpdate[]>(\n \"getUpdates\",\n {\n ...(offset === undefined ? {} : { offset }),\n timeout: timeoutSeconds,\n allowed_updates: [\"message\", \"my_chat_member\"],\n },\n signal,\n (timeoutSeconds + 10) * 1000,\n );\n if (!data.ok)\n throw new Error(redact(`telegram getUpdates failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n },\n\n setWebhook: async (url, secretToken) => {\n await must(\"setWebhook\", {\n url,\n secret_token: secretToken,\n allowed_updates: [\"message\", \"my_chat_member\"],\n });\n },\n deleteWebhook: async () => {\n await must(\"deleteWebhook\", {});\n },\n getMe: () => must(\"getMe\", {}),\n };\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
type TelegramButton = {
|
|
2
|
+
text: string;
|
|
3
|
+
url: string;
|
|
4
|
+
};
|
|
5
|
+
type TelegramMessage = {
|
|
6
|
+
chatId: string;
|
|
7
|
+
/** Telegram HTML: <b>, <i>, <a href>, <code>, <pre>. Escape user text with escapeHtml(). */
|
|
8
|
+
text: string;
|
|
9
|
+
buttons?: readonly TelegramButton[] | undefined;
|
|
10
|
+
signal?: AbortSignal | undefined;
|
|
11
|
+
};
|
|
12
|
+
type TelegramSendResult = {
|
|
13
|
+
ok: true;
|
|
14
|
+
messageId: number;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
error: string;
|
|
18
|
+
/** Worth another attempt: rate limit, 5xx, network. */
|
|
19
|
+
retryable: boolean;
|
|
20
|
+
/** The chat can never receive again (user blocked the bot, chat deleted). Prune it. */
|
|
21
|
+
gone: boolean;
|
|
22
|
+
retryAfterSeconds?: number | undefined;
|
|
23
|
+
};
|
|
24
|
+
/** The slice of a Bot API update the plugin reads. Everything else passes through untouched. */
|
|
25
|
+
type TelegramUpdate = {
|
|
26
|
+
update_id: number;
|
|
27
|
+
message?: {
|
|
28
|
+
text?: string;
|
|
29
|
+
chat: {
|
|
30
|
+
id: number | string;
|
|
31
|
+
type: string;
|
|
32
|
+
username?: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
};
|
|
35
|
+
from?: {
|
|
36
|
+
id: number;
|
|
37
|
+
username?: string;
|
|
38
|
+
first_name?: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
my_chat_member?: {
|
|
42
|
+
chat: {
|
|
43
|
+
id: number | string;
|
|
44
|
+
type: string;
|
|
45
|
+
username?: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
};
|
|
48
|
+
new_chat_member: {
|
|
49
|
+
status: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
type TelegramProvider = {
|
|
54
|
+
name: string;
|
|
55
|
+
send(message: TelegramMessage): Promise<TelegramSendResult>;
|
|
56
|
+
/** Long-polls for updates. For development or hosts without a public URL. */
|
|
57
|
+
getUpdates(options: {
|
|
58
|
+
offset?: number | undefined;
|
|
59
|
+
timeoutSeconds?: number | undefined;
|
|
60
|
+
signal?: AbortSignal | undefined;
|
|
61
|
+
}): Promise<TelegramUpdate[]>;
|
|
62
|
+
/** Points Telegram at your webhook route. Run once per deployment URL. */
|
|
63
|
+
setWebhook(url: string, secretToken: string): Promise<void>;
|
|
64
|
+
deleteWebhook(): Promise<void>;
|
|
65
|
+
getMe(): Promise<{
|
|
66
|
+
id: number;
|
|
67
|
+
username: string;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
type TelegramBotOptions = {
|
|
71
|
+
/** From @BotFather. Never appears in errors or logs. */
|
|
72
|
+
token: string;
|
|
73
|
+
fetch?: typeof globalThis.fetch | undefined;
|
|
74
|
+
/** Default https://api.telegram.org. A local fake for tests. */
|
|
75
|
+
apiBase?: string | undefined;
|
|
76
|
+
timeoutMs?: number | undefined;
|
|
77
|
+
};
|
|
78
|
+
/** Deep link that opens the bot with a start parameter. Codes are [A-Za-z0-9_-], at most 64 chars. */
|
|
79
|
+
declare const telegramStartLink: (botUsername: string, code: string) => string;
|
|
80
|
+
/** Bot API over fetch, so it runs on edge runtimes. Failures are classified, never thrown, on send. */
|
|
81
|
+
declare function telegramBot(options: TelegramBotOptions): TelegramProvider;
|
|
82
|
+
|
|
83
|
+
export { type TelegramBotOptions, type TelegramButton, type TelegramMessage, type TelegramProvider, type TelegramSendResult, type TelegramUpdate, telegramBot, telegramStartLink };
|