hood-alerts 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/LICENSE +20 -0
- package/README.md +369 -0
- package/dist/bot/index.cjs +1008 -0
- package/dist/bot/index.cjs.map +1 -0
- package/dist/bot/index.d.cts +241 -0
- package/dist/bot/index.d.ts +241 -0
- package/dist/bot/index.js +30 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/chunk-32GS6XVE.js +732 -0
- package/dist/chunk-32GS6XVE.js.map +1 -0
- package/dist/chunk-3LHCQA4Z.js +76 -0
- package/dist/chunk-3LHCQA4Z.js.map +1 -0
- package/dist/chunk-3NHLSKZE.js +1 -0
- package/dist/chunk-3NHLSKZE.js.map +1 -0
- package/dist/chunk-6JQK5G3Z.js +72 -0
- package/dist/chunk-6JQK5G3Z.js.map +1 -0
- package/dist/chunk-CBNZ6AZT.js +598 -0
- package/dist/chunk-CBNZ6AZT.js.map +1 -0
- package/dist/chunk-CIURPPOW.js +1 -0
- package/dist/chunk-CIURPPOW.js.map +1 -0
- package/dist/chunk-IUOEGBF6.js +49 -0
- package/dist/chunk-IUOEGBF6.js.map +1 -0
- package/dist/chunk-NBMINJ5E.js +230 -0
- package/dist/chunk-NBMINJ5E.js.map +1 -0
- package/dist/chunk-O2RY2SWN.js +1 -0
- package/dist/chunk-O2RY2SWN.js.map +1 -0
- package/dist/chunk-PLMVKTQM.js +1 -0
- package/dist/chunk-PLMVKTQM.js.map +1 -0
- package/dist/chunk-RSTLW7XH.js +130 -0
- package/dist/chunk-RSTLW7XH.js.map +1 -0
- package/dist/chunk-SOBZ2FPP.js +355 -0
- package/dist/chunk-SOBZ2FPP.js.map +1 -0
- package/dist/chunk-VC5MKEY2.js +170 -0
- package/dist/chunk-VC5MKEY2.js.map +1 -0
- package/dist/chunk-W2GUAWUR.js +147 -0
- package/dist/chunk-W2GUAWUR.js.map +1 -0
- package/dist/chunk-Y2M3USWZ.js +355 -0
- package/dist/chunk-Y2M3USWZ.js.map +1 -0
- package/dist/chunk-Z4DJPESQ.js +1216 -0
- package/dist/chunk-Z4DJPESQ.js.map +1 -0
- package/dist/chunk-ZDM5VNWB.js +1 -0
- package/dist/chunk-ZDM5VNWB.js.map +1 -0
- package/dist/events/index.cjs +820 -0
- package/dist/events/index.cjs.map +1 -0
- package/dist/events/index.d.cts +58 -0
- package/dist/events/index.d.ts +58 -0
- package/dist/events/index.js +69 -0
- package/dist/events/index.js.map +1 -0
- package/dist/format-6RG4OcyE.d.ts +57 -0
- package/dist/format-B7d40f65.d.cts +57 -0
- package/dist/index.cjs +4122 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/dist/logger-3phvV_fl.d.cts +185 -0
- package/dist/logger-CYza_0eU.d.ts +185 -0
- package/dist/logs-D7sYBvSy.d.cts +66 -0
- package/dist/logs-D7sYBvSy.d.ts +66 -0
- package/dist/notifiers/index.cjs +620 -0
- package/dist/notifiers/index.cjs.map +1 -0
- package/dist/notifiers/index.d.cts +186 -0
- package/dist/notifiers/index.d.ts +186 -0
- package/dist/notifiers/index.js +70 -0
- package/dist/notifiers/index.js.map +1 -0
- package/dist/pricing-BumgLpvB.d.cts +79 -0
- package/dist/pricing-BumgLpvB.d.ts +79 -0
- package/dist/ratelimit-BKVQG2oY.d.cts +42 -0
- package/dist/ratelimit-CGLlXNIS.d.ts +42 -0
- package/dist/registry-D3We8Lg6.d.ts +50 -0
- package/dist/registry-DePA3IyS.d.cts +50 -0
- package/dist/rules/index.cjs +614 -0
- package/dist/rules/index.cjs.map +1 -0
- package/dist/rules/index.d.cts +166 -0
- package/dist/rules/index.d.ts +166 -0
- package/dist/rules/index.js +60 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/schema-DuDMPqpq.d.ts +183 -0
- package/dist/schema-NnIdKFl2.d.cts +183 -0
- package/dist/service/index.cjs +3774 -0
- package/dist/service/index.cjs.map +1 -0
- package/dist/service/index.d.cts +262 -0
- package/dist/service/index.d.ts +262 -0
- package/dist/service/index.js +45 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/main.cjs +3759 -0
- package/dist/service/main.cjs.map +1 -0
- package/dist/service/main.d.cts +1 -0
- package/dist/service/main.d.ts +1 -0
- package/dist/service/main.js +60 -0
- package/dist/service/main.js.map +1 -0
- package/dist/sources-CAaQR6-N.d.ts +159 -0
- package/dist/sources-CpDOUlzx.d.cts +159 -0
- package/dist/telegram-CpodYJrP.d.cts +64 -0
- package/dist/telegram-pvphmBwn.d.ts +64 -0
- package/dist/tiers/index.cjs +471 -0
- package/dist/tiers/index.cjs.map +1 -0
- package/dist/tiers/index.d.cts +231 -0
- package/dist/tiers/index.d.ts +231 -0
- package/dist/tiers/index.js +34 -0
- package/dist/tiers/index.js.map +1 -0
- package/dist/types-C0ETog04.d.ts +109 -0
- package/dist/types-CcoKpStA.d.cts +109 -0
- package/dist/types-DFPpTMWo.d.cts +186 -0
- package/dist/types-DFPpTMWo.d.ts +186 -0
- package/package.json +114 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTelegramClient,
|
|
3
|
+
escapeHtml,
|
|
4
|
+
resolveFetch,
|
|
5
|
+
truncate
|
|
6
|
+
} from "./chunk-NBMINJ5E.js";
|
|
7
|
+
import {
|
|
8
|
+
linkMessage,
|
|
9
|
+
verifyWalletLink
|
|
10
|
+
} from "./chunk-W2GUAWUR.js";
|
|
11
|
+
import {
|
|
12
|
+
defaultRules,
|
|
13
|
+
formatValidationError,
|
|
14
|
+
parseSubscription,
|
|
15
|
+
ruleComplexity,
|
|
16
|
+
safeParseRule
|
|
17
|
+
} from "./chunk-VC5MKEY2.js";
|
|
18
|
+
|
|
19
|
+
// src/bot/commands.ts
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
import { getAddress } from "viem";
|
|
22
|
+
var reply = (text) => ({ html: escapeHtml(text), text });
|
|
23
|
+
var html = (htmlText, plain) => ({ html: htmlText, text: plain });
|
|
24
|
+
var COMMANDS = [
|
|
25
|
+
{ name: "start", description: "Set up alerts in this chat", usage: "/start" },
|
|
26
|
+
{ name: "help", description: "Show every command", usage: "/help" },
|
|
27
|
+
{
|
|
28
|
+
name: "subscribe",
|
|
29
|
+
description: "Send alerts here, with a starter rule set",
|
|
30
|
+
usage: "/subscribe [target]",
|
|
31
|
+
options: [
|
|
32
|
+
{
|
|
33
|
+
name: "target",
|
|
34
|
+
description: "Discord webhook URL or channel id. Defaults to this channel.",
|
|
35
|
+
required: false
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "unsubscribe",
|
|
41
|
+
description: "Stop alerts for a subscription",
|
|
42
|
+
usage: "/unsubscribe [subscription id]",
|
|
43
|
+
options: [{ name: "id", description: "Subscription id from /status", required: false }]
|
|
44
|
+
},
|
|
45
|
+
{ name: "rules", description: "List your rules", usage: "/rules" },
|
|
46
|
+
{
|
|
47
|
+
name: "rule",
|
|
48
|
+
description: "Add, remove, enable or disable a rule",
|
|
49
|
+
usage: "/rule add <json> | /rule rm <id> | /rule on <id> | /rule off <id>",
|
|
50
|
+
options: [{ name: "input", description: "add <json> | rm <id> | on <id> | off <id>", required: true }]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "threshold",
|
|
54
|
+
description: "Set a rule minimum USD value",
|
|
55
|
+
usage: "/threshold <rule id> <usd>",
|
|
56
|
+
options: [
|
|
57
|
+
{ name: "rule", description: "Rule id", required: true },
|
|
58
|
+
{ name: "usd", description: "Minimum USD value", required: true }
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "watch",
|
|
63
|
+
description: "Add a token to a rule watchlist",
|
|
64
|
+
usage: "/watch <token address> [rule id]",
|
|
65
|
+
options: [
|
|
66
|
+
{ name: "token", description: "Token contract address", required: true },
|
|
67
|
+
{ name: "rule", description: "Rule id (defaults to the first rule)", required: false }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "unwatch",
|
|
72
|
+
description: "Remove a token from a rule watchlist",
|
|
73
|
+
usage: "/unwatch <token address> [rule id]",
|
|
74
|
+
options: [
|
|
75
|
+
{ name: "token", description: "Token contract address", required: true },
|
|
76
|
+
{ name: "rule", description: "Rule id (defaults to the first rule)", required: false }
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{ name: "tier", description: "Show your tier and its limits", usage: "/tier" },
|
|
80
|
+
{ name: "status", description: "Show your subscriptions and delivery state", usage: "/status" },
|
|
81
|
+
{
|
|
82
|
+
name: "link",
|
|
83
|
+
description: "Link a paying wallet (signature verified)",
|
|
84
|
+
usage: "/link [address] [signature]",
|
|
85
|
+
options: [
|
|
86
|
+
{ name: "address", description: "Wallet address that pays", required: false },
|
|
87
|
+
{ name: "signature", description: "Signature of the link message", required: false }
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{ name: "upgrade", description: "How to go premium", usage: "/upgrade" }
|
|
91
|
+
];
|
|
92
|
+
function ruleSummary(rule) {
|
|
93
|
+
const parts = [
|
|
94
|
+
`${rule.enabled ? "on " : "off"} ${rule.id}`,
|
|
95
|
+
rule.name ? `"${rule.name}"` : null,
|
|
96
|
+
`kinds=${rule.kinds.join("/")}`,
|
|
97
|
+
`pads=${rule.launchpads.join("/")}`,
|
|
98
|
+
rule.minUsd !== void 0 ? `minUsd=${rule.minUsd}` : null,
|
|
99
|
+
rule.maxUsd !== void 0 ? `maxUsd=${rule.maxUsd}` : null,
|
|
100
|
+
rule.minLiquidityUsd !== void 0 ? `minLiq=${rule.minLiquidityUsd}` : null,
|
|
101
|
+
rule.maxLiquidityUsd !== void 0 ? `maxLiq=${rule.maxLiquidityUsd}` : null,
|
|
102
|
+
rule.side !== "any" ? `side=${rule.side}` : null,
|
|
103
|
+
rule.tokens.length > 0 ? `tokens=${rule.tokens.length}` : null,
|
|
104
|
+
rule.deployers.length > 0 ? `deployers=${rule.deployers.length}` : null,
|
|
105
|
+
rule.rateLimit ? `rate=${JSON.stringify(rule.rateLimit)}` : null,
|
|
106
|
+
`filters=${ruleComplexity(rule)}`
|
|
107
|
+
].filter((part) => part !== null);
|
|
108
|
+
return parts.join(" ");
|
|
109
|
+
}
|
|
110
|
+
function createCommandRouter(deps) {
|
|
111
|
+
const now = deps.now ?? Date.now;
|
|
112
|
+
const newId = deps.newId ?? randomUUID;
|
|
113
|
+
function subscriptionsOf(context) {
|
|
114
|
+
return deps.store.listSubscriptionsFor(context.subscriberId);
|
|
115
|
+
}
|
|
116
|
+
function firstSubscription(context) {
|
|
117
|
+
return subscriptionsOf(context)[0] ?? null;
|
|
118
|
+
}
|
|
119
|
+
async function handleSubscribe(context) {
|
|
120
|
+
const target = context.args[0]?.trim() || context.defaultTarget;
|
|
121
|
+
if (!target) {
|
|
122
|
+
return reply(
|
|
123
|
+
"I could not work out where to send alerts. Pass a target: /subscribe <discord webhook URL or channel id>"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const existing = subscriptionsOf(context);
|
|
127
|
+
if (existing.some((sub) => sub.target === target)) {
|
|
128
|
+
return reply(`This target is already subscribed. Use /rules to edit it, or /unsubscribe to stop.`);
|
|
129
|
+
}
|
|
130
|
+
const decision = await deps.gate.checkAddSubscription(context.subscriberId, existing.length);
|
|
131
|
+
if (!decision.allowed) return reply(`Cannot subscribe: ${decision.reason}`);
|
|
132
|
+
const { policy } = await deps.gate.resolve(context.subscriberId);
|
|
133
|
+
const rules = defaultRules().filter(
|
|
134
|
+
(rule) => rule.kinds.every((kind) => policy.allowedKinds.includes(kind))
|
|
135
|
+
);
|
|
136
|
+
const subscription = parseSubscription({
|
|
137
|
+
id: newId(),
|
|
138
|
+
subscriberId: context.subscriberId,
|
|
139
|
+
platform: context.platform,
|
|
140
|
+
target,
|
|
141
|
+
enabled: true,
|
|
142
|
+
rules: rules.slice(0, policy.maxRulesPerSubscription),
|
|
143
|
+
createdAtMs: now()
|
|
144
|
+
});
|
|
145
|
+
deps.store.saveSubscription(subscription);
|
|
146
|
+
return html(
|
|
147
|
+
[
|
|
148
|
+
`<b>Subscribed.</b> Alerts will arrive here.`,
|
|
149
|
+
`Subscription id: <code>${escapeHtml(subscription.id)}</code>`,
|
|
150
|
+
`Tier: <b>${policy.tier}</b> (delivery delay ${policy.deliveryDelayMs / 1e3}s, ${policy.maxAlertsPerHour} alerts/hour)`,
|
|
151
|
+
"",
|
|
152
|
+
"Starter rules:",
|
|
153
|
+
...subscription.rules.map((rule) => `\u2022 <code>${escapeHtml(ruleSummary(rule))}</code>`),
|
|
154
|
+
"",
|
|
155
|
+
"Edit them with /threshold, /watch and /rule. See /help."
|
|
156
|
+
].join("\n"),
|
|
157
|
+
`Subscribed. id=${subscription.id} tier=${policy.tier}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
async function handleUnsubscribe(context) {
|
|
161
|
+
const subs = subscriptionsOf(context);
|
|
162
|
+
if (subs.length === 0) return reply("You have no subscriptions. Use /subscribe to start one.");
|
|
163
|
+
const id = context.args[0]?.trim();
|
|
164
|
+
if (!id) {
|
|
165
|
+
if (subs.length > 1) {
|
|
166
|
+
return html(
|
|
167
|
+
[
|
|
168
|
+
"You have several subscriptions. Name the one to remove:",
|
|
169
|
+
...subs.map((sub) => `\u2022 <code>${escapeHtml(sub.id)}</code> \u2192 ${escapeHtml(sub.target)}`),
|
|
170
|
+
"",
|
|
171
|
+
"Usage: /unsubscribe <subscription id>"
|
|
172
|
+
].join("\n"),
|
|
173
|
+
"Name the subscription to remove: " + subs.map((sub) => sub.id).join(", ")
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const only = subs[0];
|
|
177
|
+
deps.store.deleteSubscription(only.id);
|
|
178
|
+
return reply("Unsubscribed. No more alerts will be delivered here.");
|
|
179
|
+
}
|
|
180
|
+
const match = subs.find((sub) => sub.id === id);
|
|
181
|
+
if (!match) return reply(`No subscription with id "${id}". Use /status to list yours.`);
|
|
182
|
+
deps.store.deleteSubscription(match.id);
|
|
183
|
+
return reply(`Unsubscribed ${match.id}.`);
|
|
184
|
+
}
|
|
185
|
+
function handleRules(context) {
|
|
186
|
+
const subs = subscriptionsOf(context);
|
|
187
|
+
if (subs.length === 0) return reply("You have no subscriptions yet. Use /subscribe to start one.");
|
|
188
|
+
const lines = [];
|
|
189
|
+
for (const sub of subs) {
|
|
190
|
+
lines.push(`<b>${escapeHtml(sub.target)}</b> (<code>${escapeHtml(sub.id)}</code>)`);
|
|
191
|
+
if (sub.rules.length === 0) lines.push(" no rules: nothing will be delivered");
|
|
192
|
+
for (const rule of sub.rules) lines.push(` \u2022 <code>${escapeHtml(ruleSummary(rule))}</code>`);
|
|
193
|
+
}
|
|
194
|
+
return html(lines.join("\n"), lines.join("\n").replace(/<[^>]+>/g, ""));
|
|
195
|
+
}
|
|
196
|
+
async function handleRule(context) {
|
|
197
|
+
const action = context.args[0]?.toLowerCase();
|
|
198
|
+
const subscription = firstSubscription(context);
|
|
199
|
+
if (!subscription) return reply("Subscribe first with /subscribe, then add rules.");
|
|
200
|
+
if (action === "add") {
|
|
201
|
+
const json = context.rest.slice(context.rest.indexOf("add") + 3).trim();
|
|
202
|
+
if (!json) {
|
|
203
|
+
return html(
|
|
204
|
+
[
|
|
205
|
+
'Usage: <code>/rule add {"id":"big-whales","kinds":["whale_trade"],"minUsd":25000}</code>',
|
|
206
|
+
"",
|
|
207
|
+
"Fields: id, name, enabled, kinds, launchpads, minUsd, maxUsd, minLiquidityUsd,",
|
|
208
|
+
"maxLiquidityUsd, tokens, deployers, excludeDeployers, side, reputation, rateLimit."
|
|
209
|
+
].join("\n"),
|
|
210
|
+
'Usage: /rule add {"id":"big-whales","kinds":["whale_trade"],"minUsd":25000}'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
let parsed;
|
|
214
|
+
try {
|
|
215
|
+
parsed = JSON.parse(json);
|
|
216
|
+
} catch {
|
|
217
|
+
return reply('That is not valid JSON. Example: /rule add {"id":"whales","kinds":["whale_trade"],"minUsd":5000}');
|
|
218
|
+
}
|
|
219
|
+
const result = safeParseRule(parsed);
|
|
220
|
+
if (!result.success) return reply(`Rule rejected: ${formatValidationError(result.error)}`);
|
|
221
|
+
const rule = result.data;
|
|
222
|
+
if (subscription.rules.some((existing) => existing.id === rule.id)) {
|
|
223
|
+
return reply(`A rule with id "${rule.id}" already exists. Remove it first with /rule rm ${rule.id}.`);
|
|
224
|
+
}
|
|
225
|
+
const decision = await deps.gate.checkAddRule(context.subscriberId, rule, subscription.rules.length);
|
|
226
|
+
if (!decision.allowed) return reply(`Rule rejected: ${decision.reason}`);
|
|
227
|
+
subscription.rules.push(rule);
|
|
228
|
+
deps.store.saveSubscription(subscription);
|
|
229
|
+
return html(
|
|
230
|
+
`Rule added: <code>${escapeHtml(ruleSummary(rule))}</code>`,
|
|
231
|
+
`Rule added: ${ruleSummary(rule)}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (action === "rm" || action === "remove" || action === "delete") {
|
|
235
|
+
const id = context.args[1]?.trim();
|
|
236
|
+
if (!id) return reply("Usage: /rule rm <rule id>. List them with /rules.");
|
|
237
|
+
const index = subscription.rules.findIndex((rule) => rule.id === id);
|
|
238
|
+
if (index === -1) return reply(`No rule with id "${id}". List them with /rules.`);
|
|
239
|
+
subscription.rules.splice(index, 1);
|
|
240
|
+
deps.store.saveSubscription(subscription);
|
|
241
|
+
return reply(`Rule ${id} removed.`);
|
|
242
|
+
}
|
|
243
|
+
if (action === "on" || action === "off") {
|
|
244
|
+
const id = context.args[1]?.trim();
|
|
245
|
+
if (!id) return reply(`Usage: /rule ${action} <rule id>. List them with /rules.`);
|
|
246
|
+
const rule = subscription.rules.find((entry) => entry.id === id);
|
|
247
|
+
if (!rule) return reply(`No rule with id "${id}". List them with /rules.`);
|
|
248
|
+
rule.enabled = action === "on";
|
|
249
|
+
deps.store.saveSubscription(subscription);
|
|
250
|
+
return reply(`Rule ${id} is now ${rule.enabled ? "enabled" : "disabled"}.`);
|
|
251
|
+
}
|
|
252
|
+
return html(
|
|
253
|
+
[
|
|
254
|
+
"Usage:",
|
|
255
|
+
"\u2022 <code>/rule add {json}</code>",
|
|
256
|
+
"\u2022 <code>/rule rm <id></code>",
|
|
257
|
+
"\u2022 <code>/rule on <id></code>",
|
|
258
|
+
"\u2022 <code>/rule off <id></code>"
|
|
259
|
+
].join("\n"),
|
|
260
|
+
"Usage: /rule add {json} | /rule rm <id> | /rule on <id> | /rule off <id>"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
async function handleThreshold(context) {
|
|
264
|
+
const subscription = firstSubscription(context);
|
|
265
|
+
if (!subscription) return reply("Subscribe first with /subscribe.");
|
|
266
|
+
const [ruleId, rawUsd] = context.args;
|
|
267
|
+
if (!ruleId || !rawUsd) {
|
|
268
|
+
return reply("Usage: /threshold <rule id> <usd>. Example: /threshold whales 5000");
|
|
269
|
+
}
|
|
270
|
+
const rule = subscription.rules.find((entry) => entry.id === ruleId);
|
|
271
|
+
if (!rule) return reply(`No rule with id "${ruleId}". List them with /rules.`);
|
|
272
|
+
const usd = Number(rawUsd);
|
|
273
|
+
if (!Number.isFinite(usd) || usd < 0) {
|
|
274
|
+
return reply(`"${rawUsd}" is not a valid amount. Use a positive number, for example /threshold ${ruleId} 5000`);
|
|
275
|
+
}
|
|
276
|
+
if (rule.maxUsd !== void 0 && usd > rule.maxUsd) {
|
|
277
|
+
return reply(`minUsd ${usd} would exceed this rule's maxUsd ${rule.maxUsd}. Lower it or raise maxUsd first.`);
|
|
278
|
+
}
|
|
279
|
+
rule.minUsd = usd;
|
|
280
|
+
const decision = await deps.gate.checkRule(context.subscriberId, rule);
|
|
281
|
+
if (!decision.allowed) return reply(`Change rejected: ${decision.reason}`);
|
|
282
|
+
deps.store.saveSubscription(subscription);
|
|
283
|
+
return reply(`Rule ${ruleId} now requires at least $${usd} of value.`);
|
|
284
|
+
}
|
|
285
|
+
async function handleWatch(context, add) {
|
|
286
|
+
const subscription = firstSubscription(context);
|
|
287
|
+
if (!subscription) return reply("Subscribe first with /subscribe.");
|
|
288
|
+
const rawToken = context.args[0]?.trim();
|
|
289
|
+
if (!rawToken) {
|
|
290
|
+
return reply(`Usage: /${add ? "watch" : "unwatch"} <token address> [rule id]`);
|
|
291
|
+
}
|
|
292
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(rawToken)) {
|
|
293
|
+
return reply(`"${rawToken}" is not a token address. Pass the 0x contract address from the alert.`);
|
|
294
|
+
}
|
|
295
|
+
const token = getAddress(rawToken).toLowerCase();
|
|
296
|
+
const ruleId = context.args[1]?.trim();
|
|
297
|
+
const rule = ruleId ? subscription.rules.find((entry) => entry.id === ruleId) : subscription.rules[0];
|
|
298
|
+
if (!rule) {
|
|
299
|
+
return reply(
|
|
300
|
+
ruleId ? `No rule with id "${ruleId}". List them with /rules.` : "This subscription has no rules yet. Add one with /rule add."
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (add) {
|
|
304
|
+
if (rule.tokens.includes(token)) return reply(`${rawToken} is already on rule ${rule.id}.`);
|
|
305
|
+
rule.tokens.push(token);
|
|
306
|
+
const decision = await deps.gate.checkRule(context.subscriberId, rule);
|
|
307
|
+
if (!decision.allowed) {
|
|
308
|
+
rule.tokens.pop();
|
|
309
|
+
return reply(`Cannot add: ${decision.reason}`);
|
|
310
|
+
}
|
|
311
|
+
deps.store.saveSubscription(subscription);
|
|
312
|
+
return reply(`Watching ${rawToken} on rule ${rule.id} (${rule.tokens.length} tokens).`);
|
|
313
|
+
}
|
|
314
|
+
const index = rule.tokens.indexOf(token);
|
|
315
|
+
if (index === -1) return reply(`${rawToken} is not on rule ${rule.id}.`);
|
|
316
|
+
rule.tokens.splice(index, 1);
|
|
317
|
+
deps.store.saveSubscription(subscription);
|
|
318
|
+
return reply(`Stopped watching ${rawToken} on rule ${rule.id}.`);
|
|
319
|
+
}
|
|
320
|
+
async function handleTier(context) {
|
|
321
|
+
const { entitlement, policy } = await deps.gate.resolve(context.subscriberId);
|
|
322
|
+
const expiry = entitlement.expiresAtMs === null ? "no expiry" : `expires ${new Date(entitlement.expiresAtMs).toISOString()}`;
|
|
323
|
+
const lines = [
|
|
324
|
+
`<b>Tier: ${escapeHtml(policy.tier)}</b> (${escapeHtml(expiry)}, source ${escapeHtml(entitlement.source)})`,
|
|
325
|
+
`Subscriptions: up to ${policy.maxSubscriptions}`,
|
|
326
|
+
`Rules per subscription: up to ${policy.maxRulesPerSubscription}`,
|
|
327
|
+
`Filters per rule: up to ${policy.maxRuleComplexity}`,
|
|
328
|
+
`Watchlist size: up to ${policy.maxWatchlistTokens}`,
|
|
329
|
+
`Alerts per hour: up to ${policy.maxAlertsPerHour}`,
|
|
330
|
+
`Delivery delay: ${policy.deliveryDelayMs / 1e3}s`,
|
|
331
|
+
`Liquidity and reputation filters: ${policy.allowOnChainLookupFilters ? "included" : "premium only"}`,
|
|
332
|
+
`Event kinds: ${policy.allowedKinds.join(", ")}`
|
|
333
|
+
];
|
|
334
|
+
return html(lines.join("\n"), lines.join("\n").replace(/<[^>]+>/g, ""));
|
|
335
|
+
}
|
|
336
|
+
async function handleStatus(context) {
|
|
337
|
+
const subs = subscriptionsOf(context);
|
|
338
|
+
if (subs.length === 0) return reply("No subscriptions. Use /subscribe to start one.");
|
|
339
|
+
const delivered = deps.store.countDeliveriesForSubscriber(context.subscriberId, now() - 36e5);
|
|
340
|
+
const { policy } = await deps.gate.resolve(context.subscriberId);
|
|
341
|
+
const lines = [
|
|
342
|
+
`<b>${subs.length} subscription${subs.length === 1 ? "" : "s"}</b>, ${delivered} alerts in the last hour (cap ${policy.maxAlertsPerHour})`,
|
|
343
|
+
...subs.map(
|
|
344
|
+
(sub) => `\u2022 <code>${escapeHtml(sub.id)}</code> \u2192 ${escapeHtml(sub.target)} (${sub.rules.length} rules, ${sub.enabled ? "enabled" : "disabled"})`
|
|
345
|
+
)
|
|
346
|
+
];
|
|
347
|
+
return html(lines.join("\n"), lines.join("\n").replace(/<[^>]+>/g, ""));
|
|
348
|
+
}
|
|
349
|
+
async function handleLink(context) {
|
|
350
|
+
const [address, signature] = context.args;
|
|
351
|
+
if (!address) {
|
|
352
|
+
const nonce2 = deps.store.issueLinkNonce(context.subscriberId, newId());
|
|
353
|
+
const message = linkMessage(context.subscriberId, nonce2);
|
|
354
|
+
return html(
|
|
355
|
+
[
|
|
356
|
+
"Sign this exact message with the wallet that pays, then send",
|
|
357
|
+
`<code>/link <address> <signature></code>`,
|
|
358
|
+
"",
|
|
359
|
+
`<pre>${escapeHtml(message)}</pre>`
|
|
360
|
+
].join("\n"),
|
|
361
|
+
`Sign this message and reply with /link <address> <signature>:
|
|
362
|
+
${message}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (!signature) {
|
|
366
|
+
return reply("Usage: /link <address> <signature>. Send /link on its own to get the message to sign.");
|
|
367
|
+
}
|
|
368
|
+
const nonce = deps.store.getLinkNonce(context.subscriberId);
|
|
369
|
+
if (!nonce) {
|
|
370
|
+
return reply("No pending link request. Send /link on its own first to get a fresh message to sign.");
|
|
371
|
+
}
|
|
372
|
+
const verified = await verifyWalletLink(context.subscriberId, nonce, address, signature);
|
|
373
|
+
if (!verified) {
|
|
374
|
+
return reply("That signature does not match. Sign the exact message from /link with the wallet you named.");
|
|
375
|
+
}
|
|
376
|
+
deps.store.linkWallet(context.subscriberId, verified);
|
|
377
|
+
deps.store.clearLinkNonce(context.subscriberId);
|
|
378
|
+
return reply(`Wallet ${verified} linked. Payments from it now count towards your subscription.`);
|
|
379
|
+
}
|
|
380
|
+
function handleUpgrade() {
|
|
381
|
+
if (!deps.upgradeInstructions) {
|
|
382
|
+
return reply(
|
|
383
|
+
"This deployment does not sell premium access. The operator grants it through configuration; ask them."
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return html(escapeHtml(deps.upgradeInstructions), deps.upgradeInstructions);
|
|
387
|
+
}
|
|
388
|
+
function handleHelp() {
|
|
389
|
+
const header = "hood-alerts: Robinhood Chain memecoin alerts.";
|
|
390
|
+
const kinds = "Event kinds: launch, curve_trade, graduation, whale_trade.";
|
|
391
|
+
const lifecycle = "NOXA lists instantly (no bonding curve), so it never produces curve_trade or graduation. The Odyssey produces all four.";
|
|
392
|
+
return html(
|
|
393
|
+
[
|
|
394
|
+
`<b>${escapeHtml(header)}</b>`,
|
|
395
|
+
"",
|
|
396
|
+
...COMMANDS.map(
|
|
397
|
+
(command) => `<code>${escapeHtml(command.usage)}</code> - ${escapeHtml(command.description)}`
|
|
398
|
+
),
|
|
399
|
+
"",
|
|
400
|
+
escapeHtml(kinds),
|
|
401
|
+
escapeHtml(lifecycle)
|
|
402
|
+
].join("\n"),
|
|
403
|
+
[
|
|
404
|
+
header,
|
|
405
|
+
"",
|
|
406
|
+
...COMMANDS.map((command) => `${command.usage} - ${command.description}`),
|
|
407
|
+
"",
|
|
408
|
+
kinds,
|
|
409
|
+
lifecycle
|
|
410
|
+
].join("\n")
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
commands: COMMANDS,
|
|
415
|
+
async handle(command, context) {
|
|
416
|
+
const name = command.replace(/^\//, "").split("@")[0]?.toLowerCase() ?? "";
|
|
417
|
+
try {
|
|
418
|
+
switch (name) {
|
|
419
|
+
case "start":
|
|
420
|
+
case "help":
|
|
421
|
+
return handleHelp();
|
|
422
|
+
case "subscribe":
|
|
423
|
+
return await handleSubscribe(context);
|
|
424
|
+
case "unsubscribe":
|
|
425
|
+
return await handleUnsubscribe(context);
|
|
426
|
+
case "rules":
|
|
427
|
+
return handleRules(context);
|
|
428
|
+
case "rule":
|
|
429
|
+
return await handleRule(context);
|
|
430
|
+
case "threshold":
|
|
431
|
+
return await handleThreshold(context);
|
|
432
|
+
case "watch":
|
|
433
|
+
return await handleWatch(context, true);
|
|
434
|
+
case "unwatch":
|
|
435
|
+
return await handleWatch(context, false);
|
|
436
|
+
case "tier":
|
|
437
|
+
return await handleTier(context);
|
|
438
|
+
case "status":
|
|
439
|
+
return await handleStatus(context);
|
|
440
|
+
case "link":
|
|
441
|
+
return await handleLink(context);
|
|
442
|
+
case "upgrade":
|
|
443
|
+
return handleUpgrade();
|
|
444
|
+
default:
|
|
445
|
+
return reply(`Unknown command "${name}". Send /help for the list.`);
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
449
|
+
return reply(`That command failed: ${message}. Try /help, or report this if it keeps happening.`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/service/logger.ts
|
|
456
|
+
var LEVELS = ["debug", "info", "warn", "error"];
|
|
457
|
+
function safeStringify(value) {
|
|
458
|
+
return JSON.stringify(value, (_key, raw) => {
|
|
459
|
+
if (typeof raw === "bigint") return raw.toString();
|
|
460
|
+
if (raw instanceof Error) return { name: raw.name, message: raw.message, stack: raw.stack };
|
|
461
|
+
return raw;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function createLogger(options = {}) {
|
|
465
|
+
const minimum = LEVELS.indexOf(options.level ?? "info");
|
|
466
|
+
const write = options.write ?? ((line) => console.log(line));
|
|
467
|
+
const now = options.now ?? Date.now;
|
|
468
|
+
const base = options.base ?? {};
|
|
469
|
+
const emit = (level, message, fields) => {
|
|
470
|
+
if (LEVELS.indexOf(level) < minimum) return;
|
|
471
|
+
write(
|
|
472
|
+
safeStringify({
|
|
473
|
+
time: new Date(now()).toISOString(),
|
|
474
|
+
level,
|
|
475
|
+
message,
|
|
476
|
+
...base,
|
|
477
|
+
...fields ?? {}
|
|
478
|
+
})
|
|
479
|
+
);
|
|
480
|
+
};
|
|
481
|
+
return {
|
|
482
|
+
debug: (message, fields) => emit("debug", message, fields),
|
|
483
|
+
info: (message, fields) => emit("info", message, fields),
|
|
484
|
+
warn: (message, fields) => emit("warn", message, fields),
|
|
485
|
+
error: (message, fields) => emit("error", message, fields),
|
|
486
|
+
child(fields) {
|
|
487
|
+
return createLogger({ ...options, base: { ...base, ...fields } });
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function createSilentLogger() {
|
|
492
|
+
const silent = {
|
|
493
|
+
debug: () => void 0,
|
|
494
|
+
info: () => void 0,
|
|
495
|
+
warn: () => void 0,
|
|
496
|
+
error: () => void 0,
|
|
497
|
+
child: () => silent
|
|
498
|
+
};
|
|
499
|
+
return silent;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/bot/telegram-bot.ts
|
|
503
|
+
function parseCommand(text) {
|
|
504
|
+
const trimmed = text.trim();
|
|
505
|
+
if (!trimmed.startsWith("/")) return null;
|
|
506
|
+
const [head, ...tail] = trimmed.split(/\s+/);
|
|
507
|
+
if (!head) return null;
|
|
508
|
+
return { command: head, args: tail, rest: trimmed.slice(head.length).trim() };
|
|
509
|
+
}
|
|
510
|
+
function createTelegramBot(options) {
|
|
511
|
+
const client = options.client ?? createTelegramClient(options);
|
|
512
|
+
const logger = options.logger ?? createSilentLogger();
|
|
513
|
+
const pollTimeout = options.pollTimeoutSeconds ?? 30;
|
|
514
|
+
let offset = options.offsetStore?.get() ?? 0;
|
|
515
|
+
let running = false;
|
|
516
|
+
async function handleUpdate(update) {
|
|
517
|
+
const message = update.message;
|
|
518
|
+
const text = message?.text;
|
|
519
|
+
const chatId = message?.chat?.id;
|
|
520
|
+
const fromId = message?.from?.id;
|
|
521
|
+
if (!text || chatId === void 0 || fromId === void 0 || message?.from?.is_bot === true) return;
|
|
522
|
+
const parsed = parseCommand(text);
|
|
523
|
+
if (!parsed) return;
|
|
524
|
+
const context = {
|
|
525
|
+
subscriberId: `telegram:${fromId}`,
|
|
526
|
+
platform: "telegram",
|
|
527
|
+
defaultTarget: String(chatId),
|
|
528
|
+
args: parsed.args,
|
|
529
|
+
rest: parsed.rest
|
|
530
|
+
};
|
|
531
|
+
const reply2 = await options.router.handle(parsed.command, context);
|
|
532
|
+
const response = await client.call("sendMessage", {
|
|
533
|
+
chat_id: chatId,
|
|
534
|
+
text: reply2.html,
|
|
535
|
+
parse_mode: "HTML",
|
|
536
|
+
link_preview_options: { is_disabled: true }
|
|
537
|
+
});
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
logger.warn("telegram reply failed", {
|
|
540
|
+
chatId,
|
|
541
|
+
command: parsed.command,
|
|
542
|
+
error: response.description
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
async publishCommands() {
|
|
548
|
+
const response = await client.call("setMyCommands", {
|
|
549
|
+
commands: options.router.commands.map((command) => ({
|
|
550
|
+
command: command.name,
|
|
551
|
+
description: command.description.slice(0, 256)
|
|
552
|
+
}))
|
|
553
|
+
});
|
|
554
|
+
if (!response.ok) logger.warn("setMyCommands failed", { error: response.description });
|
|
555
|
+
return response.ok;
|
|
556
|
+
},
|
|
557
|
+
async pollOnce() {
|
|
558
|
+
const response = await client.call("getUpdates", {
|
|
559
|
+
...offset > 0 ? { offset } : {},
|
|
560
|
+
timeout: pollTimeout,
|
|
561
|
+
allowed_updates: ["message"]
|
|
562
|
+
});
|
|
563
|
+
if (!response.ok) {
|
|
564
|
+
logger.warn("getUpdates failed", { error: response.description });
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
const updates = Array.isArray(response.result) ? response.result : [];
|
|
568
|
+
for (const update of updates) {
|
|
569
|
+
try {
|
|
570
|
+
await handleUpdate(update);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
logger.error("telegram update handler threw", {
|
|
573
|
+
updateId: update.update_id,
|
|
574
|
+
error: error instanceof Error ? error.message : String(error)
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
offset = update.update_id + 1;
|
|
578
|
+
options.offsetStore?.set(offset);
|
|
579
|
+
}
|
|
580
|
+
return updates.length;
|
|
581
|
+
},
|
|
582
|
+
async start() {
|
|
583
|
+
running = true;
|
|
584
|
+
logger.info("telegram bot started", { offset });
|
|
585
|
+
while (running) {
|
|
586
|
+
try {
|
|
587
|
+
await this.pollOnce();
|
|
588
|
+
} catch (error) {
|
|
589
|
+
logger.error("telegram poll failed", {
|
|
590
|
+
error: error instanceof Error ? error.message : String(error)
|
|
591
|
+
});
|
|
592
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
logger.info("telegram bot stopped");
|
|
596
|
+
},
|
|
597
|
+
stop() {
|
|
598
|
+
running = false;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/bot/discord-bot.ts
|
|
604
|
+
import { createPublicKey, verify as cryptoVerify } from "crypto";
|
|
605
|
+
var INTERACTION_TYPE = { PING: 1, APPLICATION_COMMAND: 2 };
|
|
606
|
+
var CALLBACK_TYPE = { PONG: 1, CHANNEL_MESSAGE_WITH_SOURCE: 4 };
|
|
607
|
+
var EPHEMERAL_FLAG = 1 << 6;
|
|
608
|
+
var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
609
|
+
function verifyDiscordSignature(publicKey, signature, timestamp, rawBody) {
|
|
610
|
+
if (!/^[0-9a-fA-F]{64}$/.test(publicKey)) return false;
|
|
611
|
+
if (!/^[0-9a-fA-F]{128}$/.test(signature)) return false;
|
|
612
|
+
if (!timestamp) return false;
|
|
613
|
+
try {
|
|
614
|
+
const key = createPublicKey({
|
|
615
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(publicKey, "hex")]),
|
|
616
|
+
format: "der",
|
|
617
|
+
type: "spki"
|
|
618
|
+
});
|
|
619
|
+
return cryptoVerify(
|
|
620
|
+
null,
|
|
621
|
+
Buffer.from(timestamp + rawBody, "utf8"),
|
|
622
|
+
key,
|
|
623
|
+
Buffer.from(signature, "hex")
|
|
624
|
+
);
|
|
625
|
+
} catch {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function discordCommandDefinitions(commands) {
|
|
630
|
+
return commands.map((command) => ({
|
|
631
|
+
name: command.name,
|
|
632
|
+
// Discord caps descriptions at 100 characters and rejects the whole
|
|
633
|
+
// registration if any one is longer.
|
|
634
|
+
description: truncate(command.description, 100),
|
|
635
|
+
...command.options && command.options.length > 0 ? {
|
|
636
|
+
options: command.options.map((option) => ({
|
|
637
|
+
type: 3,
|
|
638
|
+
name: option.name,
|
|
639
|
+
description: truncate(option.description, 100),
|
|
640
|
+
required: option.required
|
|
641
|
+
}))
|
|
642
|
+
} : {}
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
async function registerDiscordCommands(commands, options) {
|
|
646
|
+
const fetchImpl = resolveFetch(options.fetch);
|
|
647
|
+
const apiBase = (options.apiBase ?? "https://discord.com/api/v10").replace(/\/+$/, "");
|
|
648
|
+
const url = options.guildId ? `${apiBase}/applications/${options.applicationId}/guilds/${options.guildId}/commands` : `${apiBase}/applications/${options.applicationId}/commands`;
|
|
649
|
+
const response = await fetchImpl(url, {
|
|
650
|
+
method: "PUT",
|
|
651
|
+
headers: {
|
|
652
|
+
"content-type": "application/json",
|
|
653
|
+
authorization: `Bot ${options.botToken}`
|
|
654
|
+
},
|
|
655
|
+
body: JSON.stringify(discordCommandDefinitions(commands))
|
|
656
|
+
});
|
|
657
|
+
const raw = await response.text();
|
|
658
|
+
if (!response.ok) {
|
|
659
|
+
throw new Error(`hood-alerts: Discord rejected the command registration (${response.status}): ${raw}`);
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const parsed = JSON.parse(raw);
|
|
663
|
+
return Array.isArray(parsed) ? parsed.length : 0;
|
|
664
|
+
} catch {
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function createDiscordInteractionHandler(options) {
|
|
669
|
+
const ephemeral = options.ephemeral ?? true;
|
|
670
|
+
return {
|
|
671
|
+
async handle(request) {
|
|
672
|
+
if (!request.signature || !request.timestamp || !verifyDiscordSignature(options.publicKey, request.signature, request.timestamp, request.rawBody)) {
|
|
673
|
+
return { status: 401, body: { error: "invalid request signature" } };
|
|
674
|
+
}
|
|
675
|
+
let interaction;
|
|
676
|
+
try {
|
|
677
|
+
interaction = JSON.parse(request.rawBody);
|
|
678
|
+
} catch {
|
|
679
|
+
return { status: 400, body: { error: "body is not JSON" } };
|
|
680
|
+
}
|
|
681
|
+
if (interaction.type === INTERACTION_TYPE.PING) {
|
|
682
|
+
return { status: 200, body: { type: CALLBACK_TYPE.PONG } };
|
|
683
|
+
}
|
|
684
|
+
if (interaction.type !== INTERACTION_TYPE.APPLICATION_COMMAND) {
|
|
685
|
+
return { status: 400, body: { error: `unsupported interaction type ${interaction.type}` } };
|
|
686
|
+
}
|
|
687
|
+
const name = interaction.data?.name;
|
|
688
|
+
if (!name) return { status: 400, body: { error: "interaction carried no command name" } };
|
|
689
|
+
const values = (interaction.data?.options ?? []).map((option) => option.value === void 0 ? "" : String(option.value)).filter((value) => value.length > 0);
|
|
690
|
+
const rest = values.join(" ");
|
|
691
|
+
const args = values.length === 1 ? values[0].split(/\s+/) : values;
|
|
692
|
+
const userId = interaction.member?.user?.id ?? interaction.user?.id ?? "";
|
|
693
|
+
const context = {
|
|
694
|
+
// A guild pays once for all its channels; a DM is billed to the user.
|
|
695
|
+
subscriberId: `discord:${interaction.guild_id ?? userId}`,
|
|
696
|
+
platform: "discord",
|
|
697
|
+
defaultTarget: interaction.channel_id ?? "",
|
|
698
|
+
args,
|
|
699
|
+
rest: `${name} ${rest}`.trim()
|
|
700
|
+
};
|
|
701
|
+
const reply2 = await options.router.handle(name, context);
|
|
702
|
+
return {
|
|
703
|
+
status: 200,
|
|
704
|
+
body: {
|
|
705
|
+
type: CALLBACK_TYPE.CHANNEL_MESSAGE_WITH_SOURCE,
|
|
706
|
+
data: {
|
|
707
|
+
// Discord hard-caps message content at 2000 characters.
|
|
708
|
+
content: truncate(reply2.text, 1900),
|
|
709
|
+
...ephemeral ? { flags: EPHEMERAL_FLAG } : {}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export {
|
|
718
|
+
COMMANDS,
|
|
719
|
+
createCommandRouter,
|
|
720
|
+
LEVELS,
|
|
721
|
+
createLogger,
|
|
722
|
+
createSilentLogger,
|
|
723
|
+
createTelegramBot,
|
|
724
|
+
INTERACTION_TYPE,
|
|
725
|
+
CALLBACK_TYPE,
|
|
726
|
+
EPHEMERAL_FLAG,
|
|
727
|
+
verifyDiscordSignature,
|
|
728
|
+
discordCommandDefinitions,
|
|
729
|
+
registerDiscordCommands,
|
|
730
|
+
createDiscordInteractionHandler
|
|
731
|
+
};
|
|
732
|
+
//# sourceMappingURL=chunk-32GS6XVE.js.map
|