@prajwalshete/telegram-plugin 0.0.0-stage → 0.1.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/CHANGELOG.md +87 -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,284 @@
|
|
|
1
|
+
import { escapeHtml } from "../../core/html/escape.js";
|
|
2
|
+
import { isTelegramPluginError } from "../errors.js";
|
|
3
|
+
import { pickVariant, renderBody } from "./render.js";
|
|
4
|
+
import { ProviderTimeout, hostCan, parseProviderParams, requireProvider, runProvider } from "./providers.js";
|
|
5
|
+
import { buildButtons, orphanTokenGuard } from "./buttons.js";
|
|
6
|
+
import { fileUrl, renderReportFile, storeReportFile } from "./report-files.js";
|
|
7
|
+
import { audienceOf } from "./audience.js";
|
|
8
|
+
import { deliveryFragments } from "../outbox/enqueue.js";
|
|
9
|
+
// src/engine/content/fanout.ts
|
|
10
|
+
const BATCH_STATEMENTS = 50;
|
|
11
|
+
/**
|
|
12
|
+
* Failure codes worth another attempt later (a provider that threw or timed out). Everything else (forbidden,
|
|
13
|
+
* invalid data, a render error) fails the same way again.
|
|
14
|
+
*/
|
|
15
|
+
const TRANSIENT_FANOUT_ERRORS = ["provider_timeout", "provider_error"];
|
|
16
|
+
function errorCode(e) {
|
|
17
|
+
if (isTelegramPluginError(e)) return e.code;
|
|
18
|
+
if (e instanceof ProviderTimeout) return "provider_timeout";
|
|
19
|
+
return "provider_error";
|
|
20
|
+
}
|
|
21
|
+
/** Writes per-chat statement groups in batches of ≤ 50 statements (a chat's rows always share a batch). */
|
|
22
|
+
async function insertGroups(rt, groups) {
|
|
23
|
+
let inserted = 0;
|
|
24
|
+
let batch = [];
|
|
25
|
+
let counted = [];
|
|
26
|
+
const flush = async () => {
|
|
27
|
+
if (batch.length === 0) return;
|
|
28
|
+
(await rt.db.batch(batch)).forEach((r, i) => {
|
|
29
|
+
if (counted[i]) inserted += r.changes;
|
|
30
|
+
});
|
|
31
|
+
batch = [];
|
|
32
|
+
counted = [];
|
|
33
|
+
};
|
|
34
|
+
for (const g of groups) {
|
|
35
|
+
const size = g.before.length + g.rows.length + g.after.length;
|
|
36
|
+
if (batch.length + size > BATCH_STATEMENTS) await flush();
|
|
37
|
+
const parts = [
|
|
38
|
+
[g.before, false],
|
|
39
|
+
[g.rows, true],
|
|
40
|
+
[g.after, false]
|
|
41
|
+
];
|
|
42
|
+
for (const [list, count] of parts) for (const q of list) {
|
|
43
|
+
batch.push(q);
|
|
44
|
+
counted.push(count);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await flush();
|
|
48
|
+
return inserted;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Renders a template per audience and locale and inserts `tg_delivery` rows (chunked `db.batch()`es of ≤ 50
|
|
52
|
+
* statements, plus action tokens). Runs host providers as each audience; stores report files through the files
|
|
53
|
+
* port. Does not kick the transport. Per-audience failures are counted in the result, not thrown.
|
|
54
|
+
*/
|
|
55
|
+
async function fanOut(rt, input) {
|
|
56
|
+
const result = {
|
|
57
|
+
deliveries: 0,
|
|
58
|
+
chats: 0,
|
|
59
|
+
held: input.recipients.held.length,
|
|
60
|
+
muted: input.recipients.muted.length,
|
|
61
|
+
skipped: 0,
|
|
62
|
+
failed: 0,
|
|
63
|
+
errors: [],
|
|
64
|
+
files: []
|
|
65
|
+
};
|
|
66
|
+
const fail = (code, n) => {
|
|
67
|
+
result.failed += n;
|
|
68
|
+
if (!result.errors.includes(code)) result.errors.push(code);
|
|
69
|
+
};
|
|
70
|
+
const t = input.template;
|
|
71
|
+
let def = null;
|
|
72
|
+
let params = {};
|
|
73
|
+
try {
|
|
74
|
+
if (input.provider === void 0) {
|
|
75
|
+
if (t.provider) {
|
|
76
|
+
def = requireProvider(rt, t.provider);
|
|
77
|
+
params = parseProviderParams(def, {
|
|
78
|
+
...t.providerParams,
|
|
79
|
+
...input.params
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} else if (input.provider !== null) {
|
|
83
|
+
def = requireProvider(rt, input.provider.key);
|
|
84
|
+
params = parseProviderParams(def, input.provider.params);
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
const all = input.recipients.send.length + input.recipients.held.length;
|
|
88
|
+
fail(errorCode(e), all);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
const byRole = !input.personalize || def?.cacheFor === "audience-role";
|
|
92
|
+
const groups = /* @__PURE__ */ new Map();
|
|
93
|
+
const targets = [...input.recipients.send.map((chat) => ({
|
|
94
|
+
chat,
|
|
95
|
+
notBefore: void 0
|
|
96
|
+
})), ...input.recipients.held.map((h) => ({
|
|
97
|
+
chat: h.chat,
|
|
98
|
+
notBefore: h.until
|
|
99
|
+
}))];
|
|
100
|
+
for (const target of targets) {
|
|
101
|
+
const aud = await audienceOf(rt, target.chat.row, byRole);
|
|
102
|
+
if (!aud) {
|
|
103
|
+
result.skipped++;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const g = groups.get(aud.key);
|
|
107
|
+
if (g) g.targets.push(target);
|
|
108
|
+
else groups.set(aud.key, {
|
|
109
|
+
aud,
|
|
110
|
+
targets: [target]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const cache = input.cache ?? /* @__PURE__ */ new Map();
|
|
114
|
+
const statements = [];
|
|
115
|
+
for (const { aud, targets: chats } of groups.values()) {
|
|
116
|
+
let vars = {};
|
|
117
|
+
try {
|
|
118
|
+
if (def) {
|
|
119
|
+
if (!await hostCan(rt, aud.actor, def.permission)) {
|
|
120
|
+
fail("provider_forbidden", chats.length);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
vars = await runProvider(rt, {
|
|
124
|
+
def,
|
|
125
|
+
actor: aud.actor,
|
|
126
|
+
params,
|
|
127
|
+
now: input.now,
|
|
128
|
+
tz: input.tz,
|
|
129
|
+
locale: rt.i18n.pick(aud.info.locale),
|
|
130
|
+
audienceKey: aud.key,
|
|
131
|
+
role: byRole ? aud.role : null,
|
|
132
|
+
cache,
|
|
133
|
+
checked: true
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (input.vars) {
|
|
137
|
+
const extra = await input.vars(aud);
|
|
138
|
+
if (extra === null) {
|
|
139
|
+
result.skipped += chats.length;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
vars = {
|
|
143
|
+
...extra,
|
|
144
|
+
...vars
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
const code = errorCode(e);
|
|
149
|
+
rt.log.warn("tg.fanout_provider_failed", {
|
|
150
|
+
provider: def?.key ?? null,
|
|
151
|
+
code
|
|
152
|
+
});
|
|
153
|
+
fail(code, chats.length);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const rendered = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const { chat, notBefore } of chats) {
|
|
158
|
+
const tz = chat.row.timezone ?? input.tz;
|
|
159
|
+
const locale = pickVariant(t.body, [
|
|
160
|
+
chat.row.locale,
|
|
161
|
+
aud.info.locale,
|
|
162
|
+
chat.bot.default_locale,
|
|
163
|
+
rt.i18n.defaultLocale
|
|
164
|
+
])?.locale ?? rt.i18n.defaultLocale;
|
|
165
|
+
const rkey = `${locale}|${tz}`;
|
|
166
|
+
let r = rendered.get(rkey);
|
|
167
|
+
if (r === void 0) {
|
|
168
|
+
try {
|
|
169
|
+
const body = renderBody(rt, {
|
|
170
|
+
body: t.body,
|
|
171
|
+
format: t.format,
|
|
172
|
+
vars,
|
|
173
|
+
locales: [locale],
|
|
174
|
+
tz
|
|
175
|
+
});
|
|
176
|
+
let file = null;
|
|
177
|
+
if (t.kind === "report") {
|
|
178
|
+
const out = await renderReportFile(rt, {
|
|
179
|
+
template: t,
|
|
180
|
+
vars,
|
|
181
|
+
locale,
|
|
182
|
+
tz,
|
|
183
|
+
format: input.format,
|
|
184
|
+
html: body.html
|
|
185
|
+
});
|
|
186
|
+
file = await storeReportFile(rt, out, {
|
|
187
|
+
template: t.key,
|
|
188
|
+
...input.fileTags
|
|
189
|
+
});
|
|
190
|
+
result.files.push(file);
|
|
191
|
+
}
|
|
192
|
+
r = body.parts.length === 0 && !file ? "empty" : {
|
|
193
|
+
html: body.html,
|
|
194
|
+
parts: body.parts,
|
|
195
|
+
file,
|
|
196
|
+
locale
|
|
197
|
+
};
|
|
198
|
+
} catch (e) {
|
|
199
|
+
const code = isTelegramPluginError(e) ? e.code : "render_failed";
|
|
200
|
+
rt.log.warn("tg.fanout_render_failed", {
|
|
201
|
+
template: t.key,
|
|
202
|
+
code
|
|
203
|
+
});
|
|
204
|
+
r = "failed";
|
|
205
|
+
fail(code, 0);
|
|
206
|
+
}
|
|
207
|
+
rendered.set(rkey, r);
|
|
208
|
+
}
|
|
209
|
+
if (r === "failed") {
|
|
210
|
+
result.failed++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (r === "empty") {
|
|
214
|
+
result.skipped++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const buttons = await buildButtons(rt, t.buttons, {
|
|
218
|
+
vars,
|
|
219
|
+
locale: r.locale,
|
|
220
|
+
tz,
|
|
221
|
+
botId: chat.botId,
|
|
222
|
+
chatRef: chat.chatRef,
|
|
223
|
+
userId: aud.userId,
|
|
224
|
+
now: input.now
|
|
225
|
+
});
|
|
226
|
+
const base = {
|
|
227
|
+
botId: chat.botId,
|
|
228
|
+
chatRef: chat.chatRef,
|
|
229
|
+
threadId: chat.messageThreadId ?? null,
|
|
230
|
+
source: input.source,
|
|
231
|
+
sourceId: input.sourceId,
|
|
232
|
+
html: r.html,
|
|
233
|
+
replyMarkup: buttons.markup,
|
|
234
|
+
options: t.options,
|
|
235
|
+
dedupeKey: input.dedupeKey(chat),
|
|
236
|
+
notBefore,
|
|
237
|
+
render: {
|
|
238
|
+
template: t.key,
|
|
239
|
+
templateVersion: t.version,
|
|
240
|
+
...input.meta
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
let delivery;
|
|
244
|
+
if (r.file && r.file.size !== null && r.file.size > 52428800) {
|
|
245
|
+
rt.log.warn("tg.file_too_large", {
|
|
246
|
+
template: t.key,
|
|
247
|
+
size: r.file.size,
|
|
248
|
+
code: "file_too_large"
|
|
249
|
+
});
|
|
250
|
+
const url = await fileUrl(rt, r.file.ref);
|
|
251
|
+
const note = rt.i18n.t(r.locale, "bot.report.too_large");
|
|
252
|
+
const link = url ? `\n${rt.i18n.t(r.locale, "bot.report.download")} <a href="${escapeHtml(url)}">${escapeHtml(r.file.name)}</a>` : "";
|
|
253
|
+
delivery = {
|
|
254
|
+
...base,
|
|
255
|
+
kind: "text",
|
|
256
|
+
html: [r.html, `${note}${link}`].filter(Boolean).join("\n\n")
|
|
257
|
+
};
|
|
258
|
+
} else if (r.file) delivery = {
|
|
259
|
+
...base,
|
|
260
|
+
kind: "document",
|
|
261
|
+
file: {
|
|
262
|
+
ref: r.file.ref,
|
|
263
|
+
name: r.file.name,
|
|
264
|
+
mime: r.file.mime
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
else delivery = {
|
|
268
|
+
...base,
|
|
269
|
+
kind: "text"
|
|
270
|
+
};
|
|
271
|
+
const rows = deliveryFragments(rt, delivery, rt.clock.now());
|
|
272
|
+
const guard = orphanTokenGuard(rt, buttons.markup, `${delivery.dedupeKey}:${rows.length - 1}`);
|
|
273
|
+
statements.push({
|
|
274
|
+
before: buttons.sql,
|
|
275
|
+
rows,
|
|
276
|
+
after: guard
|
|
277
|
+
});
|
|
278
|
+
result.chats++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
result.deliveries = await insertGroups(rt, statements);
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
export { TRANSIENT_FANOUT_ERRORS, fanOut };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps only what both views contain (objects by key, arrays index-wise); values come from `b` (the audience).
|
|
3
|
+
* A field is kept only when BOTH sides returned it with a non-null value of the same kind: `null` / `undefined` on
|
|
4
|
+
* either side means "not visible" (hosts redact by nulling), so a field redacted for the sender never leaks.
|
|
5
|
+
*/
|
|
6
|
+
export declare function intersectData(a: unknown, b: unknown): unknown;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/engine/content/intersect.ts
|
|
2
|
+
/**
|
|
3
|
+
* Keeps only what both views contain (objects by key, arrays index-wise); values come from `b` (the audience).
|
|
4
|
+
* A field is kept only when BOTH sides returned it with a non-null value of the same kind: `null` / `undefined` on
|
|
5
|
+
* either side means "not visible" (hosts redact by nulling), so a field redacted for the sender never leaks.
|
|
6
|
+
*/
|
|
7
|
+
function intersectData(a, b) {
|
|
8
|
+
if (a === void 0 || b === void 0 || a === null || b === null) return void 0;
|
|
9
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
10
|
+
const n = Math.min(a.length, b.length);
|
|
11
|
+
return b.slice(0, n).map((v, i) => intersectData(a[i], v) ?? null);
|
|
12
|
+
}
|
|
13
|
+
const isObj = (v) => typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
if (isObj(a) && isObj(b)) {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [k, v] of Object.entries(b)) {
|
|
17
|
+
if (!Object.hasOwn(a, k)) continue;
|
|
18
|
+
const r = intersectData(a[k], v);
|
|
19
|
+
if (r !== void 0) out[k] = r;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
if (isObj(a) || isObj(b) || Array.isArray(a) || Array.isArray(b)) return void 0;
|
|
24
|
+
return typeof a === typeof b ? b : void 0;
|
|
25
|
+
}
|
|
26
|
+
export { intersectData };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Thrown by `runProvider` when a provider exceeds `limits.providerTimeoutMs` (counted as `provider_timeout`). */
|
|
2
|
+
export declare class ProviderTimeout extends Error {
|
|
3
|
+
/** Always `"ProviderTimeout"`. */
|
|
4
|
+
readonly name = "ProviderTimeout";
|
|
5
|
+
constructor(key: string);
|
|
6
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { zodToShape } from "../../core/template/shape.js";
|
|
2
|
+
import { TelegramPluginError } from "../errors.js";
|
|
3
|
+
// src/engine/content/providers.ts
|
|
4
|
+
function requireProvider(rt, key) {
|
|
5
|
+
const def = rt.registry.providers.get(key);
|
|
6
|
+
if (!def) throw new TelegramPluginError("provider_unknown", `provider "${key}" is not registered`);
|
|
7
|
+
return def;
|
|
8
|
+
}
|
|
9
|
+
/** Host data permission (not prefixed); errors count as "no". */
|
|
10
|
+
async function hostCan(rt, actor, permission) {
|
|
11
|
+
if (permission === null) return true;
|
|
12
|
+
try {
|
|
13
|
+
return await rt.authorize(actor, permission) === true;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
rt.log.error("tg.authorize_error", {
|
|
16
|
+
permission,
|
|
17
|
+
error: e instanceof Error ? e.name : "unknown"
|
|
18
|
+
});
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Validates provider params (defaults applied). Throws `validation_failed` with the issues. */
|
|
23
|
+
function parseProviderParams(def, params) {
|
|
24
|
+
if (!def.params) return params ?? {};
|
|
25
|
+
const r = def.params.safeParse(params ?? {});
|
|
26
|
+
if (r.success) return r.data;
|
|
27
|
+
throw new TelegramPluginError("validation_failed", `params of provider "${def.key}" are not valid`, { extensions: { issues: r.error.issues.slice(0, 20).map((i) => ({
|
|
28
|
+
path: ["params", ...i.path.map(String)].join("."),
|
|
29
|
+
message: i.message
|
|
30
|
+
})) } });
|
|
31
|
+
}
|
|
32
|
+
const shapes = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
function shapeOf(schema) {
|
|
34
|
+
let s = shapes.get(schema);
|
|
35
|
+
if (!s) {
|
|
36
|
+
s = zodToShape(schema);
|
|
37
|
+
shapes.set(schema, s);
|
|
38
|
+
}
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
/** Keeps only what the shape declares (objects recursively, arrays per item). */
|
|
42
|
+
function stripToShape(value, shape) {
|
|
43
|
+
if (value === null || value === void 0) return value;
|
|
44
|
+
if (shape.kind === "object") {
|
|
45
|
+
if (typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const [k, s] of Object.entries(shape.fields)) {
|
|
48
|
+
if (!Object.hasOwn(value, k)) continue;
|
|
49
|
+
const v = stripToShape(value[k], s);
|
|
50
|
+
if (v !== void 0) out[k] = v;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
if (shape.kind === "array") return Array.isArray(value) ? value.map((v) => stripToShape(v, shape.item)) : void 0;
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Drops every variable the provider did not declare. A value that passes the schema is used as parsed (defaults,
|
|
59
|
+
* coercions); otherwise it is stripped structurally so a slightly off provider still renders what it declared.
|
|
60
|
+
*/
|
|
61
|
+
function declaredOnly(rt, def, data) {
|
|
62
|
+
const parsed = def.variables.safeParse(data);
|
|
63
|
+
const value = parsed.success ? parsed.data : stripToShape(data, shapeOf(def.variables));
|
|
64
|
+
if (!parsed.success) rt.log.warn("tg.provider_shape", {
|
|
65
|
+
provider: def.key,
|
|
66
|
+
issues: parsed.error.issues.length
|
|
67
|
+
});
|
|
68
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? stripToShape(value, shapeOf(def.variables)) : {};
|
|
69
|
+
}
|
|
70
|
+
function stableJson(v) {
|
|
71
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
|
|
72
|
+
if (Array.isArray(v)) return `[${v.map(stableJson).join(",")}]`;
|
|
73
|
+
const o = v;
|
|
74
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stableJson(o[k])}`).join(",")}}`;
|
|
75
|
+
}
|
|
76
|
+
function withTimeout(p, ms, key) {
|
|
77
|
+
let timer;
|
|
78
|
+
const timeout = new Promise((_, reject) => {
|
|
79
|
+
timer = setTimeout(() => reject(new ProviderTimeout(key)), ms);
|
|
80
|
+
});
|
|
81
|
+
return Promise.race([p, timeout]).finally(() => {
|
|
82
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Thrown by `runProvider` when a provider exceeds `limits.providerTimeoutMs` (counted as `provider_timeout`). */
|
|
86
|
+
var ProviderTimeout = class extends Error {
|
|
87
|
+
/** Always `"ProviderTimeout"`. */
|
|
88
|
+
name = "ProviderTimeout";
|
|
89
|
+
constructor(key) {
|
|
90
|
+
super(`provider ${key} timed out`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function cacheKey(input) {
|
|
94
|
+
const p = `${input.def.key}|${stableJson(input.params)}|${input.locale}|${input.tz}`;
|
|
95
|
+
switch (input.def.cacheFor) {
|
|
96
|
+
case "params": return p;
|
|
97
|
+
case "audience-role": return input.role ? `${p}|r:${input.role}` : input.audienceKey ? `${p}|${input.audienceKey}` : null;
|
|
98
|
+
default: return input.audienceKey ? `${p}|${input.audienceKey}` : null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Runs a provider as `actor`. Throws `provider_forbidden` when the actor lacks the provider's permission,
|
|
103
|
+
* `ProviderTimeout` after `limits.providerTimeoutMs`, or whatever the provider throws.
|
|
104
|
+
*/
|
|
105
|
+
async function runProvider(rt, input) {
|
|
106
|
+
const { def } = input;
|
|
107
|
+
if (!input.checked && !await hostCan(rt, input.actor, def.permission)) throw new TelegramPluginError("provider_forbidden", `the audience may not read "${def.key}"`);
|
|
108
|
+
const key = input.cache ? cacheKey(input) : null;
|
|
109
|
+
if (key && input.cache) {
|
|
110
|
+
const hit = input.cache.get(key);
|
|
111
|
+
if (hit) return hit;
|
|
112
|
+
}
|
|
113
|
+
const db = rt.db;
|
|
114
|
+
const started = rt.clock.now();
|
|
115
|
+
const run = withTimeout(Promise.resolve().then(() => def.run({
|
|
116
|
+
actor: input.actor,
|
|
117
|
+
params: input.params,
|
|
118
|
+
now: input.now,
|
|
119
|
+
tz: input.tz,
|
|
120
|
+
locale: input.locale,
|
|
121
|
+
db,
|
|
122
|
+
env: rt.env
|
|
123
|
+
})), rt.limits.providerTimeoutMs, def.key).then((data) => declaredOnly(rt, def, data));
|
|
124
|
+
if (key && input.cache) {
|
|
125
|
+
input.cache.set(key, run);
|
|
126
|
+
run.catch(() => input.cache?.delete(key));
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return await run;
|
|
130
|
+
} finally {
|
|
131
|
+
rt.log.debug("tg.provider", {
|
|
132
|
+
provider: def.key,
|
|
133
|
+
ms: rt.clock.now() - started
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Sample data for previews (`provider.sample()`), stripped like live data. */
|
|
138
|
+
function sampleData(rt, def) {
|
|
139
|
+
if (!def.sample) throw new TelegramPluginError("validation_failed", `provider "${def.key}" has no sample()`);
|
|
140
|
+
return declaredOnly(rt, def, def.sample());
|
|
141
|
+
}
|
|
142
|
+
export { ProviderTimeout, declaredOnly, hostCan, parseProviderParams, requireProvider, runProvider, sampleData, stripToShape };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { QuietHours } from "../../contracts/subscriptions.js";
|
|
2
|
+
import { applySubscriptions, dedupeChats, planRecipients } from "../../core/recipients.js";
|
|
3
|
+
import { sql } from "../../ports/sql.js";
|
|
4
|
+
import { parseJson } from "../rows.js";
|
|
5
|
+
// src/engine/content/recipients.ts
|
|
6
|
+
const CHUNK = 80;
|
|
7
|
+
async function chunked(rt, ids, q) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (let i = 0; i < ids.length; i += CHUNK) out.push(...await rt.db.all(q(ids.slice(i, i + CHUNK))));
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
const ACTIVE_CHAT = sql`c.status = 'active' AND b.deleted_at IS NULL AND b.status IN ('active', 'webhook_error')`;
|
|
13
|
+
function chatsSql(rt, where) {
|
|
14
|
+
return sql`SELECT c.* FROM tg_chat c JOIN tg_bot b ON b.id = c.bot_id
|
|
15
|
+
WHERE c.tenant = ${rt.tenant} AND ${ACTIVE_CHAT} AND ${where} ORDER BY c.id`;
|
|
16
|
+
}
|
|
17
|
+
/** Active private chats of these users (every bot they linked). */
|
|
18
|
+
async function privateChatsOf(rt, userIds) {
|
|
19
|
+
return chunked(rt, [...new Set(userIds)], (ids) => chatsSql(rt, sql`c.kind = 'private' AND c.user_id IN (${sql.join(ids)})`));
|
|
20
|
+
}
|
|
21
|
+
async function usersWithRole(rt, role) {
|
|
22
|
+
if (rt.users?.byRole) return [...await rt.users.byRole(role)];
|
|
23
|
+
const chats = await rt.db.all(chatsSql(rt, sql`c.kind = 'private' AND c.user_id IS NOT NULL`));
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const userId of new Set(chats.map((c) => c.user_id))) {
|
|
26
|
+
const actor = await rt.actor.forUser(userId);
|
|
27
|
+
if (actor !== null && rt.describe(actor).role === role) out.push(userId);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
async function botsOf(rt, ids) {
|
|
32
|
+
const rows = await chunked(rt, [...new Set(ids)], (chunk) => sql`SELECT * FROM tg_bot WHERE tenant = ${rt.tenant} AND id IN (${sql.join(chunk)})`);
|
|
33
|
+
return new Map(rows.map((b) => [b.id, b]));
|
|
34
|
+
}
|
|
35
|
+
function resolved(row, via, threadId) {
|
|
36
|
+
return {
|
|
37
|
+
chatRef: row.id,
|
|
38
|
+
botId: row.bot_id,
|
|
39
|
+
kind: row.kind === "group" || row.kind === "supergroup" || row.kind === "channel" ? row.kind : "private",
|
|
40
|
+
status: "active",
|
|
41
|
+
userId: row.user_id,
|
|
42
|
+
messageThreadId: threadId,
|
|
43
|
+
locale: row.locale,
|
|
44
|
+
timezone: row.timezone,
|
|
45
|
+
via: [via],
|
|
46
|
+
row
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Every active chat the specs name, deduped per chat + topic thread. Unknown names resolve to nothing. */
|
|
50
|
+
async function resolveChats(rt, specs, now) {
|
|
51
|
+
const plan = planRecipients(specs);
|
|
52
|
+
const found = [];
|
|
53
|
+
if (plan.destinations.length > 0) {
|
|
54
|
+
const dests = await chunked(rt, plan.destinations, (slugs) => sql`SELECT * FROM tg_destination WHERE tenant = ${rt.tenant} AND deleted_at IS NULL AND slug IN (${sql.join(slugs)})`);
|
|
55
|
+
const chats = await chunked(rt, [...new Set(dests.map((d) => d.chat_ref))], (ids) => chatsSql(rt, sql`c.id IN (${sql.join(ids)})`));
|
|
56
|
+
const byId = new Map(chats.map((c) => [c.id, c]));
|
|
57
|
+
for (const d of dests) {
|
|
58
|
+
const c = byId.get(d.chat_ref);
|
|
59
|
+
if (c) found.push(resolved(c, { destination: d.slug }, d.message_thread_id));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (plan.chats.length > 0) {
|
|
63
|
+
const chats = await chunked(rt, plan.chats, (ids) => chatsSql(rt, sql`c.id IN (${sql.join(ids)})`));
|
|
64
|
+
for (const c of chats) found.push(resolved(c, { chat: c.id }, null));
|
|
65
|
+
}
|
|
66
|
+
const userVia = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const u of plan.users) userVia.set(u, { user: u });
|
|
68
|
+
for (const role of plan.roles) for (const u of await usersWithRole(rt, role)) if (!userVia.has(u)) userVia.set(u, { role });
|
|
69
|
+
for (const [key, refs] of Object.entries(plan.audiences)) {
|
|
70
|
+
const def = rt.registry.audiences.get(key);
|
|
71
|
+
if (!def) {
|
|
72
|
+
rt.log.warn("tg.audience_unknown", { audience: key });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const ref of refs) for (const u of await def.resolve({
|
|
76
|
+
ref,
|
|
77
|
+
db: rt.db,
|
|
78
|
+
now,
|
|
79
|
+
env: rt.env
|
|
80
|
+
})) if (!userVia.has(u)) userVia.set(u, {
|
|
81
|
+
audience: key,
|
|
82
|
+
ref
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (userVia.size > 0) for (const c of await privateChatsOf(rt, [...userVia.keys()])) found.push(resolved(c, userVia.get(c.user_id), null));
|
|
86
|
+
if (plan.allLinked) {
|
|
87
|
+
const all = await rt.db.all(chatsSql(rt, sql`(c.kind != 'private' OR c.user_id IS NOT NULL) AND c.kind != 'channel'`));
|
|
88
|
+
for (const c of all) found.push(resolved(c, { allLinked: true }, null));
|
|
89
|
+
}
|
|
90
|
+
const bots = await botsOf(rt, found.map((f) => f.botId));
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const f of dedupeChats(found)) {
|
|
93
|
+
const bot = bots.get(f.botId);
|
|
94
|
+
if (bot) out.push({
|
|
95
|
+
...f,
|
|
96
|
+
bot
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
async function subscriptionsOf(rt, chatRefs) {
|
|
102
|
+
return (await chunked(rt, [...new Set(chatRefs)], (ids) => sql`SELECT chat_ref, topic, muted, quiet_json FROM tg_subscription
|
|
103
|
+
WHERE tenant = ${rt.tenant} AND chat_ref IN (${sql.join(ids)})`)).map((r) => ({
|
|
104
|
+
chatRef: r.chat_ref,
|
|
105
|
+
topic: r.topic,
|
|
106
|
+
muted: r.muted === 1,
|
|
107
|
+
quiet: parseJson(r.quiet_json, QuietHours.nullable(), null)
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Specs → chats → subscriptions for `topic` (`schedule:<id>`, `event:<key>`, `broadcast`). Muted chats are
|
|
112
|
+
* dropped; chats in quiet hours are held until the window ends (chat zone, else the host zone).
|
|
113
|
+
*/
|
|
114
|
+
async function resolveRecipients(rt, specs, opts) {
|
|
115
|
+
const chats = await resolveChats(rt, specs, opts.now);
|
|
116
|
+
if (chats.length === 0) return {
|
|
117
|
+
send: [],
|
|
118
|
+
held: [],
|
|
119
|
+
muted: []
|
|
120
|
+
};
|
|
121
|
+
const subs = await subscriptionsOf(rt, chats.map((c) => c.chatRef));
|
|
122
|
+
return applySubscriptions(chats, subs, {
|
|
123
|
+
topic: opts.topic,
|
|
124
|
+
now: opts.now,
|
|
125
|
+
defaultTz: rt.settings.timezone
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export { privateChatsOf, resolveChats, resolveRecipients, subscriptionsOf };
|