@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.1
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/CHANGELOG.md +91 -0
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/adapters/clock.d.ts +3 -0
- package/dist/adapters/clock.js +4 -0
- package/dist/adapters/console-log.d.ts +13 -0
- package/dist/adapters/console-log.js +41 -0
- package/dist/adapters/d1-db.d.ts +5 -0
- package/dist/adapters/d1-db.js +37 -0
- package/dist/adapters/ids.d.ts +5 -0
- package/dist/adapters/ids.js +25 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/keyring.d.ts +50 -0
- package/dist/adapters/keyring.js +174 -0
- package/dist/adapters/metrics.d.ts +28 -0
- package/dist/adapters/metrics.js +48 -0
- package/dist/adapters/r2-files.d.ts +20 -0
- package/dist/adapters/r2-files.js +53 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +45 -0
- package/dist/client/broadcasts.d.ts +35 -0
- package/dist/client/commands.d.ts +23 -0
- package/dist/client/endpoint.d.ts +34 -0
- package/dist/client/events.d.ts +33 -0
- package/dist/client/index.d.ts +11 -0
- package/dist/client/index.js +3 -0
- package/dist/client/schedules.d.ts +42 -0
- package/dist/client/schema.d.ts +181 -0
- package/dist/client/send.d.ts +15 -0
- package/dist/client/templates.d.ts +47 -0
- package/dist/contracts/bots.d.ts +129 -0
- package/dist/contracts/bots.js +70 -0
- package/dist/contracts/broadcasts.d.ts +116 -0
- package/dist/contracts/broadcasts.js +63 -0
- package/dist/contracts/chats.d.ts +97 -0
- package/dist/contracts/chats.js +66 -0
- package/dist/contracts/commands.d.ts +52 -0
- package/dist/contracts/commands.js +53 -0
- package/dist/contracts/common.d.ts +38 -0
- package/dist/contracts/common.js +41 -0
- package/dist/contracts/deliveries.d.ts +103 -0
- package/dist/contracts/deliveries.js +74 -0
- package/dist/contracts/destinations.d.ts +24 -0
- package/dist/contracts/destinations.js +28 -0
- package/dist/contracts/events.d.ts +165 -0
- package/dist/contracts/events.js +98 -0
- package/dist/contracts/features.d.ts +20 -0
- package/dist/contracts/features.js +21 -0
- package/dist/contracts/health.d.ts +55 -0
- package/dist/contracts/health.js +58 -0
- package/dist/contracts/index.d.ts +22 -0
- package/dist/contracts/index.js +22 -0
- package/dist/contracts/labels.d.ts +38 -0
- package/dist/contracts/labels.js +52 -0
- package/dist/contracts/links.d.ts +51 -0
- package/dist/contracts/links.js +50 -0
- package/dist/contracts/meta.d.ts +132 -0
- package/dist/contracts/meta.js +85 -0
- package/dist/contracts/permissions.d.ts +24 -0
- package/dist/contracts/permissions.js +27 -0
- package/dist/contracts/problems.d.ts +106 -0
- package/dist/contracts/problems.js +101 -0
- package/dist/contracts/recipients.d.ts +36 -0
- package/dist/contracts/recipients.js +22 -0
- package/dist/contracts/schedules.d.ts +276 -0
- package/dist/contracts/schedules.js +143 -0
- package/dist/contracts/send.d.ts +126 -0
- package/dist/contracts/send.js +69 -0
- package/dist/contracts/subscriptions.d.ts +38 -0
- package/dist/contracts/subscriptions.js +31 -0
- package/dist/contracts/templates.d.ts +358 -0
- package/dist/contracts/templates.js +174 -0
- package/dist/contracts/users.d.ts +15 -0
- package/dist/contracts/users.js +19 -0
- package/dist/core/guards.d.ts +22 -0
- package/dist/core/guards.js +47 -0
- package/dist/core/html/escape.d.ts +2 -0
- package/dist/core/html/escape.js +36 -0
- package/dist/core/html/tokenize.js +65 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/pacing.d.ts +1 -0
- package/dist/core/pacing.js +86 -0
- package/dist/core/random.d.ts +2 -0
- package/dist/core/random.js +38 -0
- package/dist/core/recipients.d.ts +1 -0
- package/dist/core/recipients.js +86 -0
- package/dist/core/schedule/describe.d.ts +3 -0
- package/dist/core/schedule/describe.js +74 -0
- package/dist/core/schedule/index.d.ts +4 -0
- package/dist/core/schedule/policy.d.ts +1 -0
- package/dist/core/schedule/policy.js +37 -0
- package/dist/core/schedule/rule.d.ts +25 -0
- package/dist/core/schedule/rule.js +142 -0
- package/dist/core/schedule/tz.d.ts +2 -0
- package/dist/core/schedule/tz.js +59 -0
- package/dist/core/split/plain.d.ts +2 -0
- package/dist/core/split/plain.js +23 -0
- package/dist/core/split/split.d.ts +15 -0
- package/dist/core/split/split.js +158 -0
- package/dist/core/template/ast.d.ts +58 -0
- package/dist/core/template/ast.js +5 -0
- package/dist/core/template/expr.js +108 -0
- package/dist/core/template/filters.d.ts +20 -0
- package/dist/core/template/filters.js +127 -0
- package/dist/core/template/format.d.ts +17 -0
- package/dist/core/template/format.js +88 -0
- package/dist/core/template/index.d.ts +20 -0
- package/dist/core/template/index.js +29 -0
- package/dist/core/template/parse.d.ts +8 -0
- package/dist/core/template/parse.js +369 -0
- package/dist/core/template/render.d.ts +17 -0
- package/dist/core/template/render.js +118 -0
- package/dist/core/template/shape.d.ts +28 -0
- package/dist/core/template/shape.js +151 -0
- package/dist/core/template/tags.js +105 -0
- package/dist/core/template/validate.d.ts +10 -0
- package/dist/core/template/validate.js +144 -0
- package/dist/core/template/variables.d.ts +15 -0
- package/dist/core/template/variables.js +41 -0
- package/dist/do/index.d.ts +3 -0
- package/dist/do/index.js +3 -0
- package/dist/do/telegram-bot-do.d.ts +45 -0
- package/dist/do/telegram-bot-do.js +219 -0
- package/dist/engine/actions/callback.js +256 -0
- package/dist/engine/actions/tokens.d.ts +2 -0
- package/dist/engine/actions/tokens.js +51 -0
- package/dist/engine/bot-api.js +20 -0
- package/dist/engine/bot-features.js +39 -0
- package/dist/engine/chat-state.js +66 -0
- package/dist/engine/content/audience.js +54 -0
- package/dist/engine/content/buttons.d.ts +1 -0
- package/dist/engine/content/buttons.js +113 -0
- package/dist/engine/content/fanout.js +284 -0
- package/dist/engine/content/intersect.d.ts +6 -0
- package/dist/engine/content/intersect.js +26 -0
- package/dist/engine/content/providers.d.ts +6 -0
- package/dist/engine/content/providers.js +142 -0
- package/dist/engine/content/recipients.js +128 -0
- package/dist/engine/content/render.js +138 -0
- package/dist/engine/content/reply.js +156 -0
- package/dist/engine/content/report-files.js +78 -0
- package/dist/engine/content/template-rows.js +74 -0
- package/dist/engine/dialogs/dialog.d.ts +2 -0
- package/dist/engine/dialogs/dialog.js +375 -0
- package/dist/engine/dispatch/builtins.js +198 -0
- package/dist/engine/dispatch/commands.js +194 -0
- package/dist/engine/dispatch/context.d.ts +2 -0
- package/dist/engine/dispatch/context.js +194 -0
- package/dist/engine/dispatch/dispatcher.js +66 -0
- package/dist/engine/dispatch/handlers.d.ts +1 -0
- package/dist/engine/dispatch/handlers.js +103 -0
- package/dist/engine/dispatch/lifecycle.js +85 -0
- package/dist/engine/dispatch/link-chat.js +31 -0
- package/dist/engine/dispatch/linking.js +273 -0
- package/dist/engine/errors.d.ts +60 -0
- package/dist/engine/errors.js +72 -0
- package/dist/engine/events/filters.d.ts +5 -0
- package/dist/engine/events/filters.js +82 -0
- package/dist/engine/events/kick.js +30 -0
- package/dist/engine/events/payload.d.ts +3 -0
- package/dist/engine/events/payload.js +16 -0
- package/dist/engine/events/tick.js +208 -0
- package/dist/engine/i18n.d.ts +12 -0
- package/dist/engine/i18n.js +55 -0
- package/dist/engine/inbound/files.js +121 -0
- package/dist/engine/inbound/handler.js +46 -0
- package/dist/engine/jobs.d.ts +32 -0
- package/dist/engine/jobs.js +85 -0
- package/dist/engine/link-codes.js +46 -0
- package/dist/engine/metrics.js +12 -0
- package/dist/engine/options.d.ts +369 -0
- package/dist/engine/options.js +81 -0
- package/dist/engine/outbox/dead-letter.js +78 -0
- package/dist/engine/outbox/do-registry.d.ts +10 -0
- package/dist/engine/outbox/do-registry.js +10 -0
- package/dist/engine/outbox/do-transport.d.ts +33 -0
- package/dist/engine/outbox/do-transport.js +73 -0
- package/dist/engine/outbox/drain.d.ts +13 -0
- package/dist/engine/outbox/drain.js +381 -0
- package/dist/engine/outbox/enqueue.d.ts +1 -0
- package/dist/engine/outbox/enqueue.js +63 -0
- package/dist/engine/outbox/select.js +24 -0
- package/dist/engine/outbox/send.js +362 -0
- package/dist/engine/outbox/transport.d.ts +35 -0
- package/dist/engine/outbox/transport.js +175 -0
- package/dist/engine/outbox/wake.js +28 -0
- package/dist/engine/registry.d.ts +502 -0
- package/dist/engine/registry.js +258 -0
- package/dist/engine/rows.d.ts +25 -0
- package/dist/engine/rows.js +131 -0
- package/dist/engine/runtime.d.ts +95 -0
- package/dist/engine/runtime.js +149 -0
- package/dist/engine/schedules/fire.js +208 -0
- package/dist/engine/schedules/resume.js +75 -0
- package/dist/engine/schedules/rows.js +69 -0
- package/dist/engine/schedules/tick.js +30 -0
- package/dist/engine/schedules/wake.js +50 -0
- package/dist/engine/state.d.ts +11 -0
- package/dist/engine/state.js +78 -0
- package/dist/locales/en.js +339 -0
- package/dist/locales/en.json +341 -0
- package/dist/locales/mr.js +339 -0
- package/dist/locales/mr.json +341 -0
- package/dist/ports/db.d.ts +22 -0
- package/dist/ports/index.d.ts +4 -0
- package/dist/ports/ports.d.ts +257 -0
- package/dist/ports/sql.d.ts +39 -0
- package/dist/ports/sql.js +76 -0
- package/dist/renderers/csv.d.ts +14 -0
- package/dist/renderers/csv.js +51 -0
- package/dist/renderers/html.d.ts +44 -0
- package/dist/renderers/html.js +81 -0
- package/dist/renderers/index.d.ts +5 -0
- package/dist/renderers/index.js +4 -0
- package/dist/renderers/table.d.ts +30 -0
- package/dist/renderers/table.js +114 -0
- package/dist/routes/admin.js +146 -0
- package/dist/routes/app.d.ts +3111 -0
- package/dist/routes/app.js +9 -0
- package/dist/routes/broadcasts.js +37 -0
- package/dist/routes/commands.js +35 -0
- package/dist/routes/content.js +12 -0
- package/dist/routes/events.js +36 -0
- package/dist/routes/http.d.ts +30 -0
- package/dist/routes/http.js +223 -0
- package/dist/routes/me.js +59 -0
- package/dist/routes/schedules.js +62 -0
- package/dist/routes/send.js +24 -0
- package/dist/routes/templates.js +70 -0
- package/dist/routes/webhook.js +147 -0
- package/dist/schema/columns.js +307 -0
- package/dist/schema/ddl.d.ts +7 -0
- package/dist/schema/ddl.js +45 -0
- package/dist/schema/index.d.ts +4255 -0
- package/dist/schema/index.js +44 -0
- package/dist/schema/reference.sql +359 -0
- package/dist/schema/tables.d.ts +4243 -0
- package/dist/schema/tables.js +327 -0
- package/dist/schema/version.d.ts +5 -0
- package/dist/schema/version.js +7 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.js +46 -0
- package/dist/server/plugin.d.ts +81 -0
- package/dist/server/plugin.js +247 -0
- package/dist/server/version.d.ts +2 -0
- package/dist/server/version.js +4 -0
- package/dist/services/ai-tool.d.ts +26 -0
- package/dist/services/ai-tool.js +148 -0
- package/dist/services/bots.js +300 -0
- package/dist/services/broadcasts.js +390 -0
- package/dist/services/chats.js +83 -0
- package/dist/services/cleanup.js +83 -0
- package/dist/services/commands.js +140 -0
- package/dist/services/deliveries.js +53 -0
- package/dist/services/destinations.js +91 -0
- package/dist/services/event-rules.js +229 -0
- package/dist/services/events.js +20 -0
- package/dist/services/idempotency.js +64 -0
- package/dist/services/links.js +200 -0
- package/dist/services/menu.js +95 -0
- package/dist/services/ops.js +90 -0
- package/dist/services/rekey.js +63 -0
- package/dist/services/reports.js +87 -0
- package/dist/services/schedules.js +214 -0
- package/dist/services/send.d.ts +14 -0
- package/dist/services/send.js +421 -0
- package/dist/services/subscriptions.js +28 -0
- package/dist/services/templates.js +259 -0
- package/dist/telegram/classify.d.ts +30 -0
- package/dist/telegram/classify.js +164 -0
- package/dist/telegram/client.d.ts +73 -0
- package/dist/telegram/client.js +135 -0
- package/dist/telegram/errors.d.ts +37 -0
- package/dist/telegram/errors.js +62 -0
- package/dist/telegram/inbound.d.ts +40 -0
- package/dist/telegram/inbound.js +72 -0
- package/dist/telegram/index.d.ts +8 -0
- package/dist/telegram/multipart.d.ts +5 -0
- package/dist/telegram/multipart.js +47 -0
- package/dist/telegram/types.d.ts +271 -0
- package/dist/telegram/update.d.ts +833 -0
- package/dist/telegram/update.js +129 -0
- package/dist/testing/fake-html.d.ts +9 -0
- package/dist/testing/fake-html.js +83 -0
- package/dist/testing/fake-telegram.d.ts +206 -0
- package/dist/testing/fake-telegram.js +856 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/reference-sql.d.ts +4 -0
- package/dist/testing/reference-sql.js +8 -0
- package/dist/testing/test-plugin.d.ts +126 -0
- package/dist/testing/test-plugin.js +182 -0
- package/dist/testing/updates.d.ts +100 -0
- package/dist/testing/updates.js +212 -0
- package/package.json +113 -3
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { TemplateOptions } from "../../contracts/templates.js";
|
|
2
|
+
import { nextBackoff } from "../../core/pacing.js";
|
|
3
|
+
import { toPlainText } from "../../core/split/plain.js";
|
|
4
|
+
import { sql } from "../../ports/sql.js";
|
|
5
|
+
import { settle } from "../../telegram/classify.js";
|
|
6
|
+
import { withToken } from "../bot-api.js";
|
|
7
|
+
import { botDto, deliveryDto, parseJson, parseObject } from "../rows.js";
|
|
8
|
+
import { afterChatGone, chatGoneSql, migrateChat } from "../chat-state.js";
|
|
9
|
+
import { metricCount } from "../metrics.js";
|
|
10
|
+
import { inputFile } from "../../telegram/multipart.js";
|
|
11
|
+
// src/engine/outbox/send.ts
|
|
12
|
+
/** Telegram's multipart upload cap. */
|
|
13
|
+
const MAX_UPLOAD_BYTES = 52428800;
|
|
14
|
+
function skip(status, code, detail) {
|
|
15
|
+
return detail === void 0 ? {
|
|
16
|
+
type: "skip",
|
|
17
|
+
status,
|
|
18
|
+
code
|
|
19
|
+
} : {
|
|
20
|
+
type: "skip",
|
|
21
|
+
status,
|
|
22
|
+
code,
|
|
23
|
+
detail
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function openFile(rt, row) {
|
|
27
|
+
if (!row.file_ref) return null;
|
|
28
|
+
const render = parseObject(row.render_json);
|
|
29
|
+
const sourceKey = typeof render?.fileSource === "string" ? render.fileSource : null;
|
|
30
|
+
if (sourceKey) {
|
|
31
|
+
const source = rt.registry.fileSources.get(sourceKey);
|
|
32
|
+
return source ? source.resolve({
|
|
33
|
+
actor: null,
|
|
34
|
+
ref: row.file_ref,
|
|
35
|
+
env: rt.env
|
|
36
|
+
}) : null;
|
|
37
|
+
}
|
|
38
|
+
return rt.files ? rt.files.open(row.file_ref, null) : null;
|
|
39
|
+
}
|
|
40
|
+
function commonOptions(row) {
|
|
41
|
+
const opts = parseJson(row.options_json, TemplateOptions, {});
|
|
42
|
+
const markup = parseObject(row.reply_markup_json);
|
|
43
|
+
return {
|
|
44
|
+
...row.message_thread_id === null ? {} : { message_thread_id: row.message_thread_id },
|
|
45
|
+
...opts.silent ? { disable_notification: true } : {},
|
|
46
|
+
...opts.protectContent ? { protect_content: true } : {},
|
|
47
|
+
...opts.linkPreview === true ? {} : { link_preview_options: { is_disabled: true } },
|
|
48
|
+
...markup ? { reply_markup: markup } : {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function call(rt, api, row, chatId, plain) {
|
|
52
|
+
const common = commonOptions(row);
|
|
53
|
+
const html = row.body_html ?? "";
|
|
54
|
+
const text = plain ? toPlainText(html) : html;
|
|
55
|
+
const mode = plain || html === "" ? {} : { parse_mode: "HTML" };
|
|
56
|
+
const idOf = (v) => typeof v === "object" && v !== null && typeof v.message_id === "number" ? v.message_id : null;
|
|
57
|
+
let promise;
|
|
58
|
+
switch (row.kind) {
|
|
59
|
+
case "text":
|
|
60
|
+
promise = api.sendMessage({
|
|
61
|
+
chat_id: chatId,
|
|
62
|
+
text,
|
|
63
|
+
...mode,
|
|
64
|
+
...common
|
|
65
|
+
});
|
|
66
|
+
break;
|
|
67
|
+
case "edit": {
|
|
68
|
+
if (row.target_message_id === null) return skip("failed", "target_missing");
|
|
69
|
+
const { link_preview_options, reply_markup } = common;
|
|
70
|
+
promise = api.editMessageText({
|
|
71
|
+
chat_id: chatId,
|
|
72
|
+
message_id: row.target_message_id,
|
|
73
|
+
text,
|
|
74
|
+
...mode,
|
|
75
|
+
...link_preview_options ? { link_preview_options } : {},
|
|
76
|
+
...reply_markup && "inline_keyboard" in reply_markup ? { reply_markup } : {}
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "delete":
|
|
81
|
+
if (row.target_message_id === null) return skip("failed", "target_missing");
|
|
82
|
+
promise = api.deleteMessage({
|
|
83
|
+
chat_id: chatId,
|
|
84
|
+
message_id: row.target_message_id
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
case "document":
|
|
88
|
+
case "photo": {
|
|
89
|
+
const file = await openFile(rt, row);
|
|
90
|
+
if (!file) return skip("failed", "file_missing");
|
|
91
|
+
if (file.size !== null && file.size > 52428800) {
|
|
92
|
+
await file.body.cancel().catch(() => {});
|
|
93
|
+
return skip("failed", "file_too_large");
|
|
94
|
+
}
|
|
95
|
+
const upload = inputFile(file.body, row.file_name ?? file.name, row.file_mime ?? file.mime);
|
|
96
|
+
const { link_preview_options: _ignored, ...rest } = common;
|
|
97
|
+
const caption = html === "" ? {} : {
|
|
98
|
+
caption: text,
|
|
99
|
+
...mode
|
|
100
|
+
};
|
|
101
|
+
promise = row.kind === "document" ? api.sendDocument({
|
|
102
|
+
chat_id: chatId,
|
|
103
|
+
document: upload,
|
|
104
|
+
...caption,
|
|
105
|
+
...rest
|
|
106
|
+
}) : api.sendPhoto({
|
|
107
|
+
chat_id: chatId,
|
|
108
|
+
photo: upload,
|
|
109
|
+
...caption,
|
|
110
|
+
...rest
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
default: return skip("failed", "kind_unknown");
|
|
115
|
+
}
|
|
116
|
+
const r = await settle(promise);
|
|
117
|
+
if (r.outcome.kind === "ok") return {
|
|
118
|
+
ok: true,
|
|
119
|
+
messageId: idOf(r.value)
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
outcome: r.outcome
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Sends one claimed row. Never throws for Telegram or network failures. */
|
|
127
|
+
async function sendDelivery(rt, bot, chat, row, now) {
|
|
128
|
+
if (!chat) return skip("failed", "chat_not_found");
|
|
129
|
+
if (chat.status !== "active") return skip("dropped", `chat_${chat.status}`);
|
|
130
|
+
if (rt.hooks.beforeSend) {
|
|
131
|
+
const verdict = { allow: true };
|
|
132
|
+
await rt.hook("beforeSend", async (fn) => {
|
|
133
|
+
verdict.allow = await fn(deliveryDto(row, true));
|
|
134
|
+
});
|
|
135
|
+
if (verdict.allow === false) return skip("cancelled", "before_send");
|
|
136
|
+
}
|
|
137
|
+
let current = chat;
|
|
138
|
+
let plain = false;
|
|
139
|
+
let migrated = false;
|
|
140
|
+
return withToken(rt, bot, async (api) => {
|
|
141
|
+
for (;;) {
|
|
142
|
+
const r = await call(rt, api, row, current.chat_id, plain);
|
|
143
|
+
if ("type" in r) return r;
|
|
144
|
+
if (r.ok) return {
|
|
145
|
+
type: "api",
|
|
146
|
+
outcome: {
|
|
147
|
+
kind: "ok",
|
|
148
|
+
retryable: false
|
|
149
|
+
},
|
|
150
|
+
messageId: r.messageId,
|
|
151
|
+
chat: current,
|
|
152
|
+
formatFallback: plain
|
|
153
|
+
};
|
|
154
|
+
const o = r.outcome;
|
|
155
|
+
if (o.kind === "migrated" && o.migrateToChatId && !migrated) {
|
|
156
|
+
migrated = true;
|
|
157
|
+
current = await migrateChat(rt, current, o.migrateToChatId, now);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (o.kind === "parse_error" && !plain && (row.body_html ?? "") !== "") {
|
|
161
|
+
plain = true;
|
|
162
|
+
rt.log.warn("tg.format_fallback", {
|
|
163
|
+
botId: bot.id,
|
|
164
|
+
deliveryId: row.id
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
type: "api",
|
|
170
|
+
outcome: o,
|
|
171
|
+
messageId: null,
|
|
172
|
+
chat: current,
|
|
173
|
+
formatFallback: plain
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Back to the queue without counting an attempt (pacing, 401, early stop). */
|
|
179
|
+
function releaseSql(rt, row, token, now) {
|
|
180
|
+
return sql`UPDATE tg_delivery SET status = 'queued', lease_until = NULL, lease_token = NULL, updated_at = ${now}
|
|
181
|
+
WHERE tenant = ${rt.tenant} AND id = ${row.id} AND lease_token = ${token}`;
|
|
182
|
+
}
|
|
183
|
+
function finalSql(rt, row, token, f, now) {
|
|
184
|
+
return sql`UPDATE tg_delivery SET status = ${f.status}, attempts = ${f.attempts},
|
|
185
|
+
next_attempt_at = ${f.nextAttemptAt}, error_code = ${f.code}, error_detail = ${f.detail},
|
|
186
|
+
tg_message_id = COALESCE(${f.messageId ?? null}, tg_message_id), sent_at = ${f.sentAt ?? null},
|
|
187
|
+
lease_until = NULL, lease_token = NULL, updated_at = ${now}
|
|
188
|
+
WHERE tenant = ${rt.tenant} AND id = ${row.id} AND lease_token = ${token}`;
|
|
189
|
+
}
|
|
190
|
+
/** Fence for side effects in the same batch (they run before the delivery update clears the token). */
|
|
191
|
+
function leased(row, token) {
|
|
192
|
+
return sql`EXISTS (SELECT 1 FROM tg_delivery WHERE id = ${row.id} AND lease_token = ${token})`;
|
|
193
|
+
}
|
|
194
|
+
function detailOf(o) {
|
|
195
|
+
return o.description ? o.description.slice(0, 500) : null;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Applies one result (docs/06 §8 table) as one batch, then runs hooks. With `defer`, the writes of a successful
|
|
199
|
+
* send are pushed there instead (the drain commits them in a bounded window, see `FLUSH_EVERY_SENDS`).
|
|
200
|
+
*/
|
|
201
|
+
async function applyOutcome(rt, bot, chat, row, token, s, now, defer) {
|
|
202
|
+
const failedHook = async (code) => rt.hook("onDeliveryFailed", (fn) => fn({
|
|
203
|
+
...deliveryDto(row, false),
|
|
204
|
+
status: "failed",
|
|
205
|
+
errorCode: code,
|
|
206
|
+
nextAttemptAt: null
|
|
207
|
+
}));
|
|
208
|
+
const metric = (outcome, code) => metricCount(rt, "tg.send", 1, {
|
|
209
|
+
botId: bot.id,
|
|
210
|
+
outcome,
|
|
211
|
+
...code ? { code } : {}
|
|
212
|
+
});
|
|
213
|
+
if (s.type === "skip") {
|
|
214
|
+
await rt.db.batch([finalSql(rt, row, token, {
|
|
215
|
+
status: s.status,
|
|
216
|
+
attempts: row.attempts,
|
|
217
|
+
nextAttemptAt: row.next_attempt_at,
|
|
218
|
+
code: s.code,
|
|
219
|
+
detail: s.detail ?? null
|
|
220
|
+
}, now)]);
|
|
221
|
+
rt.log.info("tg.send", {
|
|
222
|
+
botId: bot.id,
|
|
223
|
+
deliveryId: row.id,
|
|
224
|
+
status: s.status,
|
|
225
|
+
code: s.code
|
|
226
|
+
});
|
|
227
|
+
if (s.status === "failed") {
|
|
228
|
+
metric("failed", s.code);
|
|
229
|
+
await failedHook(s.code);
|
|
230
|
+
return { result: "failed" };
|
|
231
|
+
}
|
|
232
|
+
metric(s.status, s.code);
|
|
233
|
+
return { result: "dropped" };
|
|
234
|
+
}
|
|
235
|
+
const o = s.outcome;
|
|
236
|
+
const target = s.chat;
|
|
237
|
+
const code = o.reason ?? o.kind;
|
|
238
|
+
const base = {
|
|
239
|
+
attempts: row.attempts,
|
|
240
|
+
nextAttemptAt: row.next_attempt_at
|
|
241
|
+
};
|
|
242
|
+
const log = (status, extra = {}) => rt.log.info("tg.send", {
|
|
243
|
+
botId: bot.id,
|
|
244
|
+
deliveryId: row.id,
|
|
245
|
+
status,
|
|
246
|
+
attempt: row.attempts + 1,
|
|
247
|
+
...extra
|
|
248
|
+
});
|
|
249
|
+
switch (o.kind) {
|
|
250
|
+
case "ok": {
|
|
251
|
+
const writes = [sql`UPDATE tg_chat SET next_allowed_at = ${chat?.next_allowed_at ?? null},
|
|
252
|
+
minute_window_start = ${chat?.minute_window_start ?? null},
|
|
253
|
+
minute_count = ${chat?.minute_count ?? 0}
|
|
254
|
+
WHERE id = ${target.id} AND ${leased(row, token)}`, finalSql(rt, row, token, {
|
|
255
|
+
...base,
|
|
256
|
+
attempts: row.attempts + 1,
|
|
257
|
+
status: "sent",
|
|
258
|
+
code: s.formatFallback ? "format_fallback" : null,
|
|
259
|
+
detail: null,
|
|
260
|
+
messageId: s.messageId,
|
|
261
|
+
sentAt: now
|
|
262
|
+
}, now)];
|
|
263
|
+
if (defer) defer.push(...writes);
|
|
264
|
+
else await rt.db.batch(writes);
|
|
265
|
+
metric("sent");
|
|
266
|
+
log("sent");
|
|
267
|
+
return { result: "sent" };
|
|
268
|
+
}
|
|
269
|
+
case "retry_after": {
|
|
270
|
+
const ms = o.retryAfterMs ?? 1e3;
|
|
271
|
+
await rt.db.batch([sql`UPDATE tg_chat SET next_allowed_at = ${now + ms} WHERE id = ${target.id} AND ${leased(row, token)}`, finalSql(rt, row, token, {
|
|
272
|
+
...base,
|
|
273
|
+
status: "queued",
|
|
274
|
+
nextAttemptAt: now + ms,
|
|
275
|
+
code: "retry_after",
|
|
276
|
+
detail: detailOf(o)
|
|
277
|
+
}, now)]);
|
|
278
|
+
metric("retry", "retry_after");
|
|
279
|
+
log("retry", {
|
|
280
|
+
code: "retry_after",
|
|
281
|
+
ms
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
result: "retry",
|
|
285
|
+
retryAfterMs: ms
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
case "blocked":
|
|
289
|
+
case "left": {
|
|
290
|
+
const status = o.kind === "blocked" ? "blocked" : "left";
|
|
291
|
+
await rt.db.batch([...chatGoneSql(rt, target, status, now, row.id).map((q) => sql`${q} AND ${leased(row, token)}`), finalSql(rt, row, token, {
|
|
292
|
+
...base,
|
|
293
|
+
status: "dropped",
|
|
294
|
+
code,
|
|
295
|
+
detail: detailOf(o)
|
|
296
|
+
}, now)]);
|
|
297
|
+
metric("dropped", code);
|
|
298
|
+
log("dropped", { code });
|
|
299
|
+
await afterChatGone(rt, target, status, now);
|
|
300
|
+
return { result: "dropped" };
|
|
301
|
+
}
|
|
302
|
+
case "unauthorized": {
|
|
303
|
+
const changed = await rt.db.batch([releaseSql(rt, row, token, now), sql`UPDATE tg_bot SET status = 'invalid_token', status_detail = ${detailOf(o) ?? "Unauthorized"},
|
|
304
|
+
checked_at = ${now}, updated_at = ${now}
|
|
305
|
+
WHERE tenant = ${rt.tenant} AND id = ${bot.id} AND status != 'invalid_token' RETURNING id`]);
|
|
306
|
+
metric("retry", "unauthorized");
|
|
307
|
+
rt.log.error("tg.bot_unauthorized", { botId: bot.id });
|
|
308
|
+
if ((changed[1]?.rows.length ?? 0) > 0) await rt.hook("onBotStatus", (fn) => fn({
|
|
309
|
+
...botDto(bot),
|
|
310
|
+
status: "invalid_token",
|
|
311
|
+
updatedAt: now
|
|
312
|
+
}));
|
|
313
|
+
return {
|
|
314
|
+
result: "retry",
|
|
315
|
+
unauthorized: true
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
case "server_error":
|
|
319
|
+
case "network":
|
|
320
|
+
case "timeout": {
|
|
321
|
+
const attempts = row.attempts + 1;
|
|
322
|
+
if (attempts >= rt.limits.maxAttempts) {
|
|
323
|
+
await rt.db.batch([finalSql(rt, row, token, {
|
|
324
|
+
...base,
|
|
325
|
+
attempts,
|
|
326
|
+
status: "failed",
|
|
327
|
+
code,
|
|
328
|
+
detail: detailOf(o)
|
|
329
|
+
}, now)]);
|
|
330
|
+
metric("failed", code);
|
|
331
|
+
log("failed", { code });
|
|
332
|
+
await failedHook(code);
|
|
333
|
+
return { result: "failed" };
|
|
334
|
+
}
|
|
335
|
+
metric("retry", code);
|
|
336
|
+
const next = now + nextBackoff(attempts - 1, Math.random);
|
|
337
|
+
await rt.db.batch([finalSql(rt, row, token, {
|
|
338
|
+
...base,
|
|
339
|
+
attempts,
|
|
340
|
+
status: "queued",
|
|
341
|
+
nextAttemptAt: next,
|
|
342
|
+
code,
|
|
343
|
+
detail: detailOf(o)
|
|
344
|
+
}, now)]);
|
|
345
|
+
log("retry", { code });
|
|
346
|
+
return { result: "retry" };
|
|
347
|
+
}
|
|
348
|
+
default:
|
|
349
|
+
await rt.db.batch([finalSql(rt, row, token, {
|
|
350
|
+
...base,
|
|
351
|
+
attempts: row.attempts + 1,
|
|
352
|
+
status: "failed",
|
|
353
|
+
code,
|
|
354
|
+
detail: detailOf(o)
|
|
355
|
+
}, now)]);
|
|
356
|
+
metric("failed", code);
|
|
357
|
+
log("failed", { code });
|
|
358
|
+
await failedHook(code);
|
|
359
|
+
return { result: "failed" };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
export { MAX_UPLOAD_BYTES, applyOutcome, releaseSql, sendDelivery };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TransportPort } from "../../ports/ports.js";
|
|
2
|
+
import { Runtime } from "../runtime.js";
|
|
3
|
+
/** Marks plugin messages on a shared queue. */
|
|
4
|
+
export declare const QUEUE_MARK = "cf-telegram-plugin";
|
|
5
|
+
/**
|
|
6
|
+
* Body of a plugin message on the host's Queue (marked with `mark`): `drain` a bot, process a queued webhook
|
|
7
|
+
* `update`, or process due `events` of a tenant. `plugin.queue()` consumes them.
|
|
8
|
+
*/
|
|
9
|
+
export type PluginQueueMessage = {
|
|
10
|
+
mark: typeof QUEUE_MARK;
|
|
11
|
+
type: "drain";
|
|
12
|
+
tenant: string;
|
|
13
|
+
botId: string;
|
|
14
|
+
} | {
|
|
15
|
+
mark: typeof QUEUE_MARK;
|
|
16
|
+
type: "update";
|
|
17
|
+
tenant: string;
|
|
18
|
+
botId: string;
|
|
19
|
+
update: unknown;
|
|
20
|
+
} |
|
|
21
|
+
/** Process due events now (sent by `plugin.kick()` after an `emit()` commit); fan-out then kicks the drains. */
|
|
22
|
+
{
|
|
23
|
+
mark: typeof QUEUE_MARK;
|
|
24
|
+
type: "events";
|
|
25
|
+
tenant: string;
|
|
26
|
+
};
|
|
27
|
+
/** True for a message body the plugin sent (so a shared queue consumer can route it to `plugin.queue()`). */
|
|
28
|
+
export declare function isPluginMessage(body: unknown): body is PluginQueueMessage;
|
|
29
|
+
/**
|
|
30
|
+
* The default transport (`name: "outbox"`): `kick()` drains inline within `KICK_BUDGET_MS` (run it in
|
|
31
|
+
* `waitUntil`); otherwise the minute tick drains.
|
|
32
|
+
*/
|
|
33
|
+
export declare function cronTransport<A>(rt: Runtime<A>): TransportPort;
|
|
34
|
+
/** Queue transport: `kick()` sends one `drain` message per bot (default: every bot with due work). */
|
|
35
|
+
export declare function queueTransport<A>(rt: Runtime<A>, queue: Queue<unknown>): TransportPort;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isBotUsable, loadBot } from "../bot-api.js";
|
|
2
|
+
import { botsWithDueWork, drainBot, sendCap } from "./drain.js";
|
|
3
|
+
import { parseUpdate } from "../../telegram/update.js";
|
|
4
|
+
import { processUpdate } from "../dispatch/dispatcher.js";
|
|
5
|
+
import { runEvents } from "../events/tick.js";
|
|
6
|
+
import { handleDeadLetters } from "./dead-letter.js";
|
|
7
|
+
// src/engine/outbox/transport.ts
|
|
8
|
+
/** Budget for drains started from a request (`waitUntil` allows ~30 s after the response). */
|
|
9
|
+
const KICK_BUDGET_MS = 25e3;
|
|
10
|
+
/** Marks plugin messages on a shared queue. */
|
|
11
|
+
const QUEUE_MARK = "cf-telegram-plugin";
|
|
12
|
+
/** True for a message body the plugin sent (so a shared queue consumer can route it to `plugin.queue()`). */
|
|
13
|
+
function isPluginMessage(body) {
|
|
14
|
+
return typeof body === "object" && body !== null && body.mark === "cf-telegram-plugin";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Drains the given bots (default: every bot with due work) one after another within one budget. The Free-plan
|
|
18
|
+
* send cap applies to the whole invocation, not per bot.
|
|
19
|
+
*/
|
|
20
|
+
async function drainBots(rt, botIds, budgetMs) {
|
|
21
|
+
const started = rt.clock.now();
|
|
22
|
+
const ids = botIds ?? await botsWithDueWork(rt, started);
|
|
23
|
+
const results = [];
|
|
24
|
+
let capLeft = sendCap(rt);
|
|
25
|
+
for (const id of new Set(ids)) {
|
|
26
|
+
const left = budgetMs - (rt.clock.now() - started);
|
|
27
|
+
if (left <= 0 || capLeft <= 0) break;
|
|
28
|
+
const r = await drainBot(rt, id, {
|
|
29
|
+
budgetMs: left,
|
|
30
|
+
...Number.isFinite(capLeft) ? { maxSends: capLeft } : {}
|
|
31
|
+
});
|
|
32
|
+
capLeft -= r.claimed;
|
|
33
|
+
results.push(r);
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The default transport (`name: "outbox"`): `kick()` drains inline within `KICK_BUDGET_MS` (run it in
|
|
39
|
+
* `waitUntil`); otherwise the minute tick drains.
|
|
40
|
+
*/
|
|
41
|
+
function cronTransport(rt) {
|
|
42
|
+
return {
|
|
43
|
+
name: "outbox",
|
|
44
|
+
async kick(botIds) {
|
|
45
|
+
await drainBots(rt, botIds, Math.min(KICK_BUDGET_MS, rt.limits.tickBudgetMs));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Queue transport: `kick()` sends one `drain` message per bot (default: every bot with due work). */
|
|
50
|
+
function queueTransport(rt, queue) {
|
|
51
|
+
return {
|
|
52
|
+
name: "queue",
|
|
53
|
+
async kick(botIds) {
|
|
54
|
+
const ids = [...new Set(botIds ?? await botsWithDueWork(rt, rt.clock.now()))];
|
|
55
|
+
if (ids.length === 0) return;
|
|
56
|
+
const body = (botId) => ({
|
|
57
|
+
mark: QUEUE_MARK,
|
|
58
|
+
type: "drain",
|
|
59
|
+
tenant: rt.tenant,
|
|
60
|
+
botId
|
|
61
|
+
});
|
|
62
|
+
await queue.sendBatch(ids.map((id) => ({ body: body(id) })));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Delay before the queue re-kicks a bot whose next row is due later (null = let the tick handle it). */
|
|
67
|
+
function rekickDelaySeconds(r, now) {
|
|
68
|
+
if (r.stoppedBy === "budget" || r.stoppedBy === "cap") return 0;
|
|
69
|
+
if (r.stoppedBy !== "idle" || r.nextDueAt === null) return null;
|
|
70
|
+
const sec = Math.ceil((r.nextDueAt - now) / 1e3);
|
|
71
|
+
return sec <= 60 ? Math.max(1, sec) : null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Queue consumer. Handles (acks or retries, one by one) every plugin message of the batch (`isPluginMessage`) and
|
|
75
|
+
* returns true when the batch held nothing else. A batch with host messages too returns false after handling the
|
|
76
|
+
* plugin's: the host then handles the rest and must skip `isPluginMessage(m.body)` messages (and must not
|
|
77
|
+
* `ackAll()` / `retryAll()`). A dedicated plugin queue avoids mixed batches. Drain messages for the same bot are
|
|
78
|
+
* merged; a drain that stops early or has work due soon re-kicks itself with `delaySeconds`.
|
|
79
|
+
*/
|
|
80
|
+
async function handleQueueBatch(rt, batch, opts = {}) {
|
|
81
|
+
const mine = batch.messages.filter((m) => isPluginMessage(m.body));
|
|
82
|
+
if (mine.length === 0) return false;
|
|
83
|
+
await handlePluginMessages(rt, batch, mine, opts);
|
|
84
|
+
return mine.length === batch.messages.length;
|
|
85
|
+
}
|
|
86
|
+
async function handlePluginMessages(rt, batch, messages, opts) {
|
|
87
|
+
if (opts.deadLetterQueue !== void 0 && batch.queue === opts.deadLetterQueue) {
|
|
88
|
+
const own = {
|
|
89
|
+
queue: batch.queue,
|
|
90
|
+
metadata: batch.metadata,
|
|
91
|
+
messages,
|
|
92
|
+
ackAll: () => {
|
|
93
|
+
for (const m of messages) m.ack();
|
|
94
|
+
},
|
|
95
|
+
retryAll: (o) => {
|
|
96
|
+
for (const m of messages) m.retry(o);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
await handleDeadLetters(rt, own);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const drains = /* @__PURE__ */ new Map();
|
|
103
|
+
const events = [];
|
|
104
|
+
for (const m of messages) {
|
|
105
|
+
const body = m.body;
|
|
106
|
+
if (body.tenant !== rt.tenant) {
|
|
107
|
+
m.ack();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (body.type === "events") {
|
|
111
|
+
events.push(m);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (body.type === "drain") {
|
|
115
|
+
drains.set(body.botId, [...drains.get(body.botId) ?? [], m]);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const bot = await loadBot(rt, body.botId);
|
|
120
|
+
const parsed = parseUpdate(body.update);
|
|
121
|
+
if (bot && isBotUsable(bot) && parsed.ok) await processUpdate(rt, bot, parsed.update);
|
|
122
|
+
m.ack();
|
|
123
|
+
} catch {
|
|
124
|
+
m.retry({ delaySeconds: 5 });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (events.length > 0) try {
|
|
128
|
+
await runEvents(rt, rt.clock.now());
|
|
129
|
+
for (const m of events) m.ack();
|
|
130
|
+
} catch (e) {
|
|
131
|
+
rt.log.error("tg.queue_events_failed", { error: e instanceof Error ? e.name : "error" });
|
|
132
|
+
for (const m of events) m.retry({ delaySeconds: 5 });
|
|
133
|
+
}
|
|
134
|
+
if (rt.transport.name === "do" && drains.size > 0) {
|
|
135
|
+
try {
|
|
136
|
+
await rt.transport.kick([...drains.keys()]);
|
|
137
|
+
for (const msgs of drains.values()) for (const m of msgs) m.ack();
|
|
138
|
+
} catch {
|
|
139
|
+
for (const msgs of drains.values()) for (const m of msgs) m.retry({ delaySeconds: 5 });
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const started = rt.clock.now();
|
|
144
|
+
let capLeft = sendCap(rt);
|
|
145
|
+
for (const [botId, msgs] of drains) try {
|
|
146
|
+
const left = rt.limits.tickBudgetMs - (rt.clock.now() - started);
|
|
147
|
+
if (left <= 0 || capLeft <= 0) {
|
|
148
|
+
for (const m of msgs) m.retry({ delaySeconds: 1 });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const r = await drainBot(rt, botId, {
|
|
152
|
+
budgetMs: left,
|
|
153
|
+
...Number.isFinite(capLeft) ? { maxSends: capLeft } : {}
|
|
154
|
+
});
|
|
155
|
+
capLeft -= r.claimed;
|
|
156
|
+
const delay = rekickDelaySeconds(r, rt.clock.now());
|
|
157
|
+
if (delay !== null && rt.queue) {
|
|
158
|
+
const body = {
|
|
159
|
+
mark: QUEUE_MARK,
|
|
160
|
+
type: "drain",
|
|
161
|
+
tenant: rt.tenant,
|
|
162
|
+
botId
|
|
163
|
+
};
|
|
164
|
+
await rt.queue.send(body, delay > 0 ? { delaySeconds: delay } : {});
|
|
165
|
+
}
|
|
166
|
+
for (const m of msgs) m.ack();
|
|
167
|
+
} catch (e) {
|
|
168
|
+
rt.log.error("tg.queue_drain_failed", {
|
|
169
|
+
botId,
|
|
170
|
+
error: e instanceof Error ? e.name : "error"
|
|
171
|
+
});
|
|
172
|
+
for (const m of msgs) m.retry({ delaySeconds: 5 });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export { KICK_BUDGET_MS, QUEUE_MARK, cronTransport, drainBots, handleQueueBatch, isPluginMessage, queueTransport };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { runEvents } from "../events/tick.js";
|
|
2
|
+
import { scheduleWakeStage } from "../schedules/wake.js";
|
|
3
|
+
// src/engine/outbox/wake.ts
|
|
4
|
+
/** Events stage: due events of the tenant (fan-out kicks the bots' objects). No wake of its own (the tick is). */
|
|
5
|
+
async function eventWakeStage(rt, _botId, now) {
|
|
6
|
+
if (rt.features.events) await runEvents(rt, now);
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
/** The stages a TelegramBotDO alarm runs, in order: due schedules of the bot, then due events of the tenant. */
|
|
10
|
+
function wakeStages() {
|
|
11
|
+
return [scheduleWakeStage, eventWakeStage];
|
|
12
|
+
}
|
|
13
|
+
/** Runs every wake stage for `botId`; failures are logged. Returns the earliest next wake the stages asked for. */
|
|
14
|
+
async function runWake(rt, botId, now, stages = wakeStages()) {
|
|
15
|
+
let next = null;
|
|
16
|
+
for (const stage of stages) try {
|
|
17
|
+
const at = await stage(rt, botId, rt.clock.now());
|
|
18
|
+
if (typeof at === "number" && Number.isFinite(at)) next = next === null ? at : Math.min(next, at);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
rt.log.error("tg.wake_stage_failed", {
|
|
21
|
+
botId,
|
|
22
|
+
error: e instanceof Error ? e.name : "error"
|
|
23
|
+
});
|
|
24
|
+
next = next === null ? now + 6e4 : Math.min(next, now + 6e4);
|
|
25
|
+
}
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
export { runWake, wakeStages };
|