-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpolling.ts
More file actions
192 lines (176 loc) · 6.23 KB
/
polling.ts
File metadata and controls
192 lines (176 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// src/bot/polling.ts
import { recordUsageEvent } from "../usage/db.js";
import { readBotConfig } from "./config.js";
import { getUpdates, sendMessage, deleteWebhook, setMyCommands, type TelegramUpdate } from "./telegram.js";
import {
parseCommand,
welcomeMessage,
unknownMessage,
createdMessage,
launchMessage,
launchUsageMessage,
} from "./commands.js";
import {
ackCheckingLaunch,
ackCreatingProject,
errCreateProject,
errLaunchGeneric,
errLaunchNotFound,
linkButtonLabels,
getTokenMessage,
} from "./i18n.js";
import { buildGatewayMiniAppUrl, buildTelegramMiniAppUrl } from "./links.js";
async function createProjectViaApi(
controlPlaneUrl: string,
botSecret: string,
ownerTelegramId: number,
title: string,
) {
const res = await fetch(`${controlPlaneUrl}/api/projects`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-spawndock-bot-secret": botSecret,
},
body: JSON.stringify({ title, ownerTelegramId }),
});
if (!res.ok) throw new Error(`Create project failed: ${res.status}`);
return (await res.json()) as any;
}
async function getLaunchInfo(controlPlaneUrl: string, botSecret: string, ownerTelegramId: number, slug: string) {
const url = new URL(`${controlPlaneUrl}/api/projects/${slug}/launch-url`);
url.searchParams.set("ownerTelegramId", String(ownerTelegramId));
const res = await fetch(url, {
headers: {
"x-spawndock-bot-secret": botSecret,
},
});
if (!res.ok) throw new Error(`Launch info failed: ${res.status}`);
return (await res.json()) as any;
}
async function processUpdate(cfg: ReturnType<typeof readBotConfig>, update: TelegramUpdate): Promise<void> {
const msg = update.message;
if (!msg?.text || !msg.from) return;
const cmd = parseCommand(msg.text);
recordUsageEvent({
service: "bot",
eventType: `cmd_${cmd.tag}`,
actorType: "telegram_user",
actorKey: `tg:${msg.from.id}`,
});
if (cmd.tag === "start" || cmd.tag === "help") {
await sendMessage(cfg.telegramBotToken, msg.chat.id, welcomeMessage());
return;
}
if (cmd.tag === "get-token") {
await sendMessage(cfg.telegramBotToken, msg.chat.id, getTokenMessage(cfg.apiToken));
return;
}
if (cmd.tag === "new") {
let data: any;
const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCreatingProject());
try {
data = await createProjectViaApi(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.title);
} catch (err: any) {
clearTimeout(ackTimeout);
await sendMessage(cfg.telegramBotToken, msg.chat.id, errCreateProject());
return;
}
clearTimeout(ackTimeout);
const slug = data.project.slug;
const token = data.pairingToken.token;
const bootstrapCmd = `npx -y @spawn-dock/create --token ${token}`;
await sendMessage(
cfg.telegramBotToken,
msg.chat.id,
createdMessage(slug, bootstrapCmd),
);
return;
}
if (cmd.tag === "launch-missing") {
await sendMessage(cfg.telegramBotToken, msg.chat.id, launchUsageMessage());
return;
}
if (cmd.tag === "launch") {
let data: any;
const ackTimeout = scheduleAcknowledgement(cfg, msg.chat.id, ackCheckingLaunch());
try {
data = await getLaunchInfo(cfg.controlPlaneUrl, cfg.controlPlaneBotSecret, msg.from.id, cmd.slug);
} catch (err: any) {
clearTimeout(ackTimeout);
if (String(err.message).includes("404")) {
await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchNotFound());
return;
}
await sendMessage(cfg.telegramBotToken, msg.chat.id, errLaunchGeneric());
return;
}
clearTimeout(ackTimeout);
const previewUrl = data.launchUrl;
const tmaUrl = buildGatewayMiniAppUrl(previewUrl, cmd.slug);
const telegramMiniAppUrl = buildTelegramMiniAppUrl(cfg.telegramBotUsername, cfg.telegramMiniAppShortName, cmd.slug);
const buttons = linkButtonLabels();
await sendMessage(
cfg.telegramBotToken,
msg.chat.id,
launchMessage(cmd.slug, data.status, tmaUrl, previewUrl, telegramMiniAppUrl),
{ tmaUrl, previewUrl, telegramMiniAppUrl, buttonLabels: buttons },
);
return;
}
await sendMessage(cfg.telegramBotToken, msg.chat.id, unknownMessage());
}
function scheduleAcknowledgement(
cfg: ReturnType<typeof readBotConfig>,
chatId: number,
message: string,
): NodeJS.Timeout {
return setTimeout(() => {
void sendMessage(cfg.telegramBotToken, chatId, message).catch(() => {});
}, 4000);
}
async function getLatestOffset(token: string): Promise<number> {
const res = await fetch(
`https://api.telegram.org/bot${token}/getUpdates?limit=1&offset=-1&timeout=0`,
);
if (!res.ok) return 0;
const data = (await res.json()) as any;
const updates = data.result as any[];
if (!updates || updates.length === 0) return 0;
return updates[updates.length - 1].update_id + 1;
}
async function main(): Promise<void> {
const cfg = readBotConfig();
await deleteWebhook(cfg.telegramBotToken);
try {
await setMyCommands(cfg.telegramBotToken);
} catch (err: any) {
// Do not exit: polling and typed commands still work; menu may stay stale until Telegram API succeeds.
console.error(`setMyCommands failed (menu may be stale): ${err?.message ?? err}`);
}
const startOffset = await getLatestOffset(cfg.telegramBotToken);
console.log(`SpawnDock bot polling started against ${cfg.controlPlaneUrl} (offset=${startOffset})`);
let offset = startOffset;
while (true) {
try {
const updates = await getUpdates(cfg.telegramBotToken, offset > 0 ? offset + 1 : 0, cfg.pollingTimeout);
if (updates.length > 0) console.log(`Received ${updates.length} update(s)`);
for (const update of updates) {
console.log(`Update ${update.updateId}: "${update.message?.text ?? "(no text)"}" from ${update.message?.from?.id ?? "?"}`);
try {
await processUpdate(cfg, update);
} catch (err: any) {
console.error(`Error processing update ${update.updateId}: ${err.message}`);
}
offset = update.updateId;
}
} catch (err: any) {
console.error(`Polling error: ${err.message}`);
await new Promise((r) => setTimeout(r, 1000));
}
}
}
main().catch((err) => {
console.error(`Bot fatal error: ${err.message}`);
process.exit(1);
});