opencode-gateway 0.2.2 → 0.2.4
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/dist/cli.js +0 -0
- package/dist/index.js +20907 -52
- package/dist/telegram/client.d.ts +1 -1
- package/dist/telegram/poller.d.ts +16 -1
- package/dist/telegram/runtime.d.ts +3 -1
- package/dist/telegram/state.d.ts +7 -0
- package/package.json +1 -1
- package/dist/binding/execution.js +0 -1
- package/dist/binding/gateway.js +0 -1
- package/dist/binding/index.js +0 -4
- package/dist/binding/opencode.js +0 -1
- package/dist/cli/args.js +0 -53
- package/dist/cli/doctor.js +0 -49
- package/dist/cli/init.js +0 -40
- package/dist/cli/opencode-config-file.js +0 -18
- package/dist/cli/opencode-config.js +0 -194
- package/dist/cli/paths.js +0 -22
- package/dist/cli/templates.js +0 -41
- package/dist/config/cron.js +0 -52
- package/dist/config/gateway.js +0 -148
- package/dist/config/memory.js +0 -105
- package/dist/config/paths.js +0 -39
- package/dist/config/telegram.js +0 -91
- package/dist/cron/runtime.js +0 -402
- package/dist/delivery/telegram.js +0 -75
- package/dist/delivery/text.js +0 -175
- package/dist/gateway.js +0 -117
- package/dist/host/file-sender.js +0 -59
- package/dist/host/logger.js +0 -53
- package/dist/host/transport.js +0 -35
- package/dist/mailbox/router.js +0 -16
- package/dist/media/mime.js +0 -45
- package/dist/memory/prompt.js +0 -122
- package/dist/opencode/adapter.js +0 -340
- package/dist/opencode/driver-hub.js +0 -82
- package/dist/opencode/event-normalize.js +0 -48
- package/dist/opencode/event-stream.js +0 -65
- package/dist/opencode/events.js +0 -1
- package/dist/questions/client.js +0 -36
- package/dist/questions/format.js +0 -36
- package/dist/questions/normalize.js +0 -45
- package/dist/questions/parser.js +0 -96
- package/dist/questions/runtime.js +0 -195
- package/dist/questions/types.js +0 -1
- package/dist/runtime/attachments.js +0 -12
- package/dist/runtime/conversation-coordinator.js +0 -22
- package/dist/runtime/executor.js +0 -407
- package/dist/runtime/mailbox.js +0 -112
- package/dist/runtime/opencode-runner.js +0 -79
- package/dist/runtime/runtime-singleton.js +0 -28
- package/dist/session/context.js +0 -23
- package/dist/session/conversation-key.js +0 -3
- package/dist/session/switcher.js +0 -59
- package/dist/session/system-prompt.js +0 -52
- package/dist/store/migrations.js +0 -197
- package/dist/store/sqlite.js +0 -777
- package/dist/telegram/client.js +0 -179
- package/dist/telegram/media.js +0 -65
- package/dist/telegram/normalize.js +0 -119
- package/dist/telegram/poller.js +0 -97
- package/dist/telegram/runtime.js +0 -133
- package/dist/telegram/state.js +0 -128
- package/dist/telegram/types.js +0 -1
- package/dist/tools/channel-new-session.js +0 -27
- package/dist/tools/channel-send-file.js +0 -27
- package/dist/tools/channel-target.js +0 -34
- package/dist/tools/cron-run.js +0 -20
- package/dist/tools/cron-upsert.js +0 -51
- package/dist/tools/gateway-dispatch-cron.js +0 -33
- package/dist/tools/gateway-status.js +0 -25
- package/dist/tools/schedule-cancel.js +0 -12
- package/dist/tools/schedule-format.js +0 -48
- package/dist/tools/schedule-list.js +0 -17
- package/dist/tools/schedule-once.js +0 -43
- package/dist/tools/schedule-status.js +0 -23
- package/dist/tools/telegram-send-test.js +0 -26
- package/dist/tools/telegram-status.js +0 -49
- package/dist/tools/time.js +0 -25
- package/dist/utils/error.js +0 -57
package/dist/telegram/client.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { formatError } from "../utils/error";
|
|
2
|
-
export class TelegramApiError extends Error {
|
|
3
|
-
retryable;
|
|
4
|
-
constructor(message, retryable) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.retryable = retryable;
|
|
7
|
-
this.name = "TelegramApiError";
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
export class TelegramBotClient {
|
|
11
|
-
botToken;
|
|
12
|
-
constructor(botToken) {
|
|
13
|
-
this.botToken = botToken;
|
|
14
|
-
}
|
|
15
|
-
async getUpdates(offset, timeoutSeconds) {
|
|
16
|
-
return this.call("getUpdates", {
|
|
17
|
-
offset,
|
|
18
|
-
timeout: timeoutSeconds,
|
|
19
|
-
allowed_updates: ["message", "callback_query"],
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
async getMe() {
|
|
23
|
-
return this.call("getMe", {});
|
|
24
|
-
}
|
|
25
|
-
async getChat(chatId) {
|
|
26
|
-
return this.call("getChat", {
|
|
27
|
-
chat_id: chatId,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
async getFile(fileId) {
|
|
31
|
-
return this.call("getFile", {
|
|
32
|
-
file_id: fileId,
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
async downloadFile(remotePath, localPath) {
|
|
36
|
-
let response;
|
|
37
|
-
try {
|
|
38
|
-
response = await fetch(`https://api.telegram.org/file/bot${this.botToken}/${remotePath}`);
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
throw new TelegramApiError(`Telegram file download failed: ${formatError(error)}`, true);
|
|
42
|
-
}
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
throw new TelegramApiError(`Telegram file download failed (${response.status}): ${response.statusText}`, isRetryableError(response.status, response.status));
|
|
45
|
-
}
|
|
46
|
-
await Bun.write(localPath, await response.bytes());
|
|
47
|
-
}
|
|
48
|
-
async sendMessage(chatId, text, messageThreadId) {
|
|
49
|
-
await this.call("sendMessage", {
|
|
50
|
-
chat_id: chatId,
|
|
51
|
-
text,
|
|
52
|
-
message_thread_id: parseMessageThreadId(messageThreadId),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
async sendInteractiveMessage(chatId, text, messageThreadId, replyMarkup) {
|
|
56
|
-
return await this.call("sendMessage", {
|
|
57
|
-
chat_id: chatId,
|
|
58
|
-
text,
|
|
59
|
-
message_thread_id: parseMessageThreadId(messageThreadId),
|
|
60
|
-
reply_markup: replyMarkup,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
async sendPhoto(chatId, filePath, caption, messageThreadId, mimeType) {
|
|
64
|
-
const form = new FormData();
|
|
65
|
-
form.set("chat_id", chatId);
|
|
66
|
-
setOptionalFormField(form, "caption", caption);
|
|
67
|
-
setOptionalFormField(form, "message_thread_id", formatMessageThreadId(messageThreadId));
|
|
68
|
-
form.set("photo", Bun.file(filePath, { type: mimeType }));
|
|
69
|
-
await this.callMultipart("sendPhoto", form);
|
|
70
|
-
}
|
|
71
|
-
async sendDocument(chatId, filePath, caption, messageThreadId, mimeType) {
|
|
72
|
-
const form = new FormData();
|
|
73
|
-
form.set("chat_id", chatId);
|
|
74
|
-
setOptionalFormField(form, "caption", caption);
|
|
75
|
-
setOptionalFormField(form, "message_thread_id", formatMessageThreadId(messageThreadId));
|
|
76
|
-
form.set("document", Bun.file(filePath, { type: mimeType }));
|
|
77
|
-
await this.callMultipart("sendDocument", form);
|
|
78
|
-
}
|
|
79
|
-
async sendChatAction(chatId, action, messageThreadId) {
|
|
80
|
-
await this.call("sendChatAction", {
|
|
81
|
-
chat_id: chatId,
|
|
82
|
-
action,
|
|
83
|
-
message_thread_id: parseMessageThreadId(messageThreadId),
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
async sendMessageDraft(chatId, draftId, text, messageThreadId) {
|
|
87
|
-
if (!Number.isSafeInteger(draftId) || draftId === 0) {
|
|
88
|
-
throw new Error(`invalid Telegram draft id: ${draftId}`);
|
|
89
|
-
}
|
|
90
|
-
await this.call("sendMessageDraft", {
|
|
91
|
-
chat_id: chatId,
|
|
92
|
-
draft_id: draftId,
|
|
93
|
-
text,
|
|
94
|
-
message_thread_id: parseMessageThreadId(messageThreadId),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
async answerCallbackQuery(callbackQueryId, text) {
|
|
98
|
-
await this.call("answerCallbackQuery", {
|
|
99
|
-
callback_query_id: callbackQueryId,
|
|
100
|
-
text,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
async call(method, body) {
|
|
104
|
-
let response;
|
|
105
|
-
try {
|
|
106
|
-
response = await fetch(`https://api.telegram.org/bot${this.botToken}/${method}`, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: {
|
|
109
|
-
"content-type": "application/json",
|
|
110
|
-
},
|
|
111
|
-
body: JSON.stringify(stripUndefined(body)),
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
throw new TelegramApiError(`Telegram ${method} request failed: ${formatError(error)}`, true);
|
|
116
|
-
}
|
|
117
|
-
const payload = (await response.json());
|
|
118
|
-
if (payload.ok) {
|
|
119
|
-
return payload.result;
|
|
120
|
-
}
|
|
121
|
-
const description = payload.description ?? `HTTP ${response.status}`;
|
|
122
|
-
const errorCode = payload.error_code ?? response.status;
|
|
123
|
-
throw new TelegramApiError(`Telegram ${method} failed (${errorCode}): ${description}`, isRetryableError(errorCode, response.status));
|
|
124
|
-
}
|
|
125
|
-
async callMultipart(method, body) {
|
|
126
|
-
let response;
|
|
127
|
-
try {
|
|
128
|
-
response = await fetch(`https://api.telegram.org/bot${this.botToken}/${method}`, {
|
|
129
|
-
method: "POST",
|
|
130
|
-
body,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
throw new TelegramApiError(`Telegram ${method} request failed: ${formatError(error)}`, true);
|
|
135
|
-
}
|
|
136
|
-
const payload = (await response.json());
|
|
137
|
-
if (payload.ok) {
|
|
138
|
-
return payload.result;
|
|
139
|
-
}
|
|
140
|
-
const description = payload.description ?? `HTTP ${response.status}`;
|
|
141
|
-
const errorCode = payload.error_code ?? response.status;
|
|
142
|
-
throw new TelegramApiError(`Telegram ${method} failed (${errorCode}): ${description}`, isRetryableError(errorCode, response.status));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function parseMessageThreadId(value) {
|
|
146
|
-
if (value == null) {
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
const normalized = value.trim();
|
|
150
|
-
if (normalized.length === 0 || normalized === "undefined") {
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
154
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
155
|
-
throw new Error(`invalid Telegram topic id: ${value}`);
|
|
156
|
-
}
|
|
157
|
-
return parsed;
|
|
158
|
-
}
|
|
159
|
-
function formatMessageThreadId(value) {
|
|
160
|
-
const parsed = parseMessageThreadId(value);
|
|
161
|
-
return parsed === undefined ? null : String(parsed);
|
|
162
|
-
}
|
|
163
|
-
function stripUndefined(body) {
|
|
164
|
-
return Object.fromEntries(Object.entries(body).filter((entry) => entry[1] !== undefined));
|
|
165
|
-
}
|
|
166
|
-
function setOptionalFormField(form, key, value) {
|
|
167
|
-
if (value != null && value.trim().length > 0) {
|
|
168
|
-
form.set(key, value);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function isRetryableError(errorCode, httpStatus) {
|
|
172
|
-
if (errorCode === 401 || errorCode === 403 || errorCode === 404) {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
if (httpStatus >= 500 || httpStatus === 429) {
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
return errorCode !== 400;
|
|
179
|
-
}
|
package/dist/telegram/media.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
-
import { basename, extname, join } from "node:path";
|
|
3
|
-
import { inferLocalFileMimeType } from "../media/mime";
|
|
4
|
-
export class TelegramInboundMediaStore {
|
|
5
|
-
client;
|
|
6
|
-
mediaRootPath;
|
|
7
|
-
constructor(client, mediaRootPath) {
|
|
8
|
-
this.client = client;
|
|
9
|
-
this.mediaRootPath = mediaRootPath;
|
|
10
|
-
}
|
|
11
|
-
async materializeInboundMessage(message, sourceKind, externalId) {
|
|
12
|
-
return {
|
|
13
|
-
deliveryTarget: message.deliveryTarget,
|
|
14
|
-
sender: message.sender,
|
|
15
|
-
text: message.text,
|
|
16
|
-
attachments: await Promise.all(message.attachments.map((attachment, index) => this.materializeAttachment(attachment, sourceKind, externalId, index))),
|
|
17
|
-
mailboxKey: message.mailboxKey,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
async materializeAttachment(attachment, sourceKind, externalId, ordinal) {
|
|
21
|
-
switch (attachment.kind) {
|
|
22
|
-
case "image":
|
|
23
|
-
return await this.materializeImageAttachment(attachment, sourceKind, externalId, ordinal);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
async materializeImageAttachment(attachment, sourceKind, externalId, ordinal) {
|
|
27
|
-
const file = await this.client.getFile(attachment.fileId);
|
|
28
|
-
const remotePath = file.file_path?.trim();
|
|
29
|
-
if (!remotePath) {
|
|
30
|
-
throw new Error(`Telegram file ${attachment.fileId} did not include file_path`);
|
|
31
|
-
}
|
|
32
|
-
const fileName = normalizeOptionalFileName(attachment.fileName) ??
|
|
33
|
-
normalizeOptionalFileName(basename(remotePath)) ??
|
|
34
|
-
`telegram-image-${ordinal + 1}${extensionFromRemotePath(remotePath)}`;
|
|
35
|
-
const localPath = join(this.mediaRootPath, "telegram", sanitizePathSegment(sourceKind), sanitizePathSegment(externalId), `${ordinal}-${sanitizePathSegment(fileName)}`);
|
|
36
|
-
await mkdir(join(this.mediaRootPath, "telegram", sanitizePathSegment(sourceKind), sanitizePathSegment(externalId)), {
|
|
37
|
-
recursive: true,
|
|
38
|
-
});
|
|
39
|
-
await this.client.downloadFile(remotePath, localPath);
|
|
40
|
-
return {
|
|
41
|
-
kind: "image",
|
|
42
|
-
mimeType: attachment.mimeType ?? (await inferLocalFileMimeType(localPath)),
|
|
43
|
-
fileName,
|
|
44
|
-
localPath,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function sanitizePathSegment(value) {
|
|
49
|
-
return (value
|
|
50
|
-
.trim()
|
|
51
|
-
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
52
|
-
.replace(/^_+|_+$/g, "")
|
|
53
|
-
.slice(0, 120) || "file");
|
|
54
|
-
}
|
|
55
|
-
function normalizeOptionalFileName(value) {
|
|
56
|
-
if (value === null) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
const trimmed = value.trim();
|
|
60
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
61
|
-
}
|
|
62
|
-
function extensionFromRemotePath(remotePath) {
|
|
63
|
-
const extension = extname(remotePath);
|
|
64
|
-
return extension.length === 0 ? ".bin" : extension;
|
|
65
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
export function buildTelegramAllowlist(config) {
|
|
2
|
-
return {
|
|
3
|
-
allowedChats: new Set(config.allowedChats),
|
|
4
|
-
allowedUsers: new Set(config.allowedUsers),
|
|
5
|
-
};
|
|
6
|
-
}
|
|
7
|
-
export function normalizeTelegramUpdate(update, allowlist, mailboxRouter) {
|
|
8
|
-
if (update.callback_query) {
|
|
9
|
-
return normalizeTelegramCallbackQuery(update.callback_query, allowlist);
|
|
10
|
-
}
|
|
11
|
-
const message = update.message;
|
|
12
|
-
if (!message) {
|
|
13
|
-
return ignored("unsupported update type");
|
|
14
|
-
}
|
|
15
|
-
if (!message.from) {
|
|
16
|
-
return ignored("message sender is missing");
|
|
17
|
-
}
|
|
18
|
-
if (message.from.is_bot === true) {
|
|
19
|
-
return ignored("message sender is a bot");
|
|
20
|
-
}
|
|
21
|
-
const chatId = String(message.chat.id);
|
|
22
|
-
const userId = String(message.from.id);
|
|
23
|
-
if (!isAllowed(chatId, userId, allowlist)) {
|
|
24
|
-
return ignored("message is not allowlisted");
|
|
25
|
-
}
|
|
26
|
-
const deliveryTarget = {
|
|
27
|
-
channel: "telegram",
|
|
28
|
-
target: chatId,
|
|
29
|
-
topic: message.message_thread_id === undefined ? null : String(message.message_thread_id),
|
|
30
|
-
};
|
|
31
|
-
const attachments = extractAttachments(message.photo, message.document);
|
|
32
|
-
const text = normalizeOptionalText(message.text ?? message.caption ?? null);
|
|
33
|
-
if (text === null && attachments.length === 0) {
|
|
34
|
-
return ignored("message has no supported content");
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
kind: "message",
|
|
38
|
-
chatType: message.chat.type,
|
|
39
|
-
message: {
|
|
40
|
-
mailboxKey: mailboxRouter?.resolve(deliveryTarget) ?? null,
|
|
41
|
-
deliveryTarget,
|
|
42
|
-
sender: `telegram:${userId}`,
|
|
43
|
-
text,
|
|
44
|
-
attachments,
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
function normalizeTelegramCallbackQuery(callbackQuery, allowlist) {
|
|
49
|
-
const message = callbackQuery.message;
|
|
50
|
-
if (!message) {
|
|
51
|
-
return ignored("callback query message is missing");
|
|
52
|
-
}
|
|
53
|
-
const chatId = String(message.chat.id);
|
|
54
|
-
const userId = String(callbackQuery.from.id);
|
|
55
|
-
if (!isAllowed(chatId, userId, allowlist)) {
|
|
56
|
-
return ignored("callback query is not allowlisted");
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
kind: "callbackQuery",
|
|
60
|
-
callbackQuery: {
|
|
61
|
-
callbackQueryId: callbackQuery.id,
|
|
62
|
-
sender: `telegram:${userId}`,
|
|
63
|
-
deliveryTarget: {
|
|
64
|
-
channel: "telegram",
|
|
65
|
-
target: chatId,
|
|
66
|
-
topic: message.message_thread_id === undefined ? null : String(message.message_thread_id),
|
|
67
|
-
},
|
|
68
|
-
messageId: message.message_id,
|
|
69
|
-
data: normalizeOptionalText(callbackQuery.data ?? null),
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function extractAttachments(photo, document) {
|
|
74
|
-
const photoAttachment = selectLargestPhoto(photo);
|
|
75
|
-
if (photoAttachment !== null) {
|
|
76
|
-
return [photoAttachment];
|
|
77
|
-
}
|
|
78
|
-
if (document?.mime_type?.startsWith("image/") === true) {
|
|
79
|
-
return [
|
|
80
|
-
{
|
|
81
|
-
kind: "image",
|
|
82
|
-
fileId: document.file_id,
|
|
83
|
-
fileUniqueId: document.file_unique_id ?? null,
|
|
84
|
-
mimeType: document.mime_type,
|
|
85
|
-
fileName: normalizeOptionalText(document.file_name ?? null),
|
|
86
|
-
},
|
|
87
|
-
];
|
|
88
|
-
}
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
function selectLargestPhoto(photo) {
|
|
92
|
-
if (!Array.isArray(photo) || photo.length === 0) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
const largest = photo[photo.length - 1];
|
|
96
|
-
return {
|
|
97
|
-
kind: "image",
|
|
98
|
-
fileId: largest.file_id,
|
|
99
|
-
fileUniqueId: largest.file_unique_id ?? null,
|
|
100
|
-
mimeType: null,
|
|
101
|
-
fileName: null,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function isAllowed(chatId, userId, allowlist) {
|
|
105
|
-
return allowlist.allowedChats.has(chatId) || allowlist.allowedUsers.has(userId);
|
|
106
|
-
}
|
|
107
|
-
function ignored(reason) {
|
|
108
|
-
return {
|
|
109
|
-
kind: "ignore",
|
|
110
|
-
reason,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function normalizeOptionalText(value) {
|
|
114
|
-
if (value === null) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
const trimmed = value.trim();
|
|
118
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
119
|
-
}
|
package/dist/telegram/poller.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { formatError } from "../utils/error";
|
|
2
|
-
import { TelegramApiError } from "./client";
|
|
3
|
-
import { buildTelegramAllowlist, normalizeTelegramUpdate } from "./normalize";
|
|
4
|
-
import { recordTelegramChatType, recordTelegramPollFailure, recordTelegramPollSuccess } from "./state";
|
|
5
|
-
export class TelegramPollingService {
|
|
6
|
-
client;
|
|
7
|
-
mailbox;
|
|
8
|
-
store;
|
|
9
|
-
logger;
|
|
10
|
-
config;
|
|
11
|
-
mailboxRouter;
|
|
12
|
-
mediaStore;
|
|
13
|
-
questions;
|
|
14
|
-
allowlist;
|
|
15
|
-
running = false;
|
|
16
|
-
constructor(client, mailbox, store, logger, config, mailboxRouter, mediaStore, questions) {
|
|
17
|
-
this.client = client;
|
|
18
|
-
this.mailbox = mailbox;
|
|
19
|
-
this.store = store;
|
|
20
|
-
this.logger = logger;
|
|
21
|
-
this.config = config;
|
|
22
|
-
this.mailboxRouter = mailboxRouter;
|
|
23
|
-
this.mediaStore = mediaStore;
|
|
24
|
-
this.questions = questions;
|
|
25
|
-
this.allowlist = buildTelegramAllowlist(config);
|
|
26
|
-
}
|
|
27
|
-
start() {
|
|
28
|
-
if (this.running) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
this.running = true;
|
|
32
|
-
void this.runLoop().finally(() => {
|
|
33
|
-
this.running = false;
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
isRunning() {
|
|
37
|
-
return this.running;
|
|
38
|
-
}
|
|
39
|
-
async runLoop() {
|
|
40
|
-
let offset = this.store.getTelegramUpdateOffset();
|
|
41
|
-
let retryDelayMs = 1_000;
|
|
42
|
-
for (;;) {
|
|
43
|
-
try {
|
|
44
|
-
const updates = await this.client.getUpdates(offset, this.config.pollTimeoutSeconds);
|
|
45
|
-
recordTelegramPollSuccess(this.store, Date.now());
|
|
46
|
-
for (const update of updates) {
|
|
47
|
-
const nextOffset = update.update_id + 1;
|
|
48
|
-
const normalized = normalizeTelegramUpdate(update, this.allowlist, this.mailboxRouter);
|
|
49
|
-
if (normalized.kind === "ignore") {
|
|
50
|
-
this.logger.log("info", `ignoring telegram update ${update.update_id}: ${normalized.reason}`);
|
|
51
|
-
offset = this.advanceOffset(nextOffset);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (this.store.hasMailboxEntry("telegram_update", String(update.update_id))) {
|
|
55
|
-
offset = this.advanceOffset(nextOffset);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (normalized.kind === "callbackQuery") {
|
|
59
|
-
await this.questions.handleTelegramCallbackQuery(normalized.callbackQuery);
|
|
60
|
-
offset = this.advanceOffset(nextOffset);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
recordTelegramChatType(this.store, normalized.message.deliveryTarget.target, normalized.chatType, Date.now());
|
|
64
|
-
await this.mailbox.enqueueInboundMessage(await this.mediaStore.materializeInboundMessage(normalized.message, "telegram_update", String(update.update_id)), "telegram_update", String(update.update_id));
|
|
65
|
-
offset = this.advanceOffset(nextOffset);
|
|
66
|
-
}
|
|
67
|
-
retryDelayMs = 1_000;
|
|
68
|
-
}
|
|
69
|
-
catch (error) {
|
|
70
|
-
recordTelegramPollFailure(this.store, formatTelegramPollerError(error), Date.now());
|
|
71
|
-
if (isPermanentTelegramFailure(error)) {
|
|
72
|
-
this.logger.log("error", formatTelegramPollerError(error));
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
this.logger.log("warn", formatTelegramPollerError(error));
|
|
76
|
-
await sleep(retryDelayMs);
|
|
77
|
-
retryDelayMs = Math.min(retryDelayMs * 2, 15_000);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
advanceOffset(offset) {
|
|
82
|
-
const recordedAtMs = Date.now();
|
|
83
|
-
this.store.putTelegramUpdateOffset(offset, recordedAtMs);
|
|
84
|
-
return offset;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function isPermanentTelegramFailure(error) {
|
|
88
|
-
return error instanceof TelegramApiError && !error.retryable;
|
|
89
|
-
}
|
|
90
|
-
function formatTelegramPollerError(error) {
|
|
91
|
-
return `telegram poller failure: ${formatError(error)}`;
|
|
92
|
-
}
|
|
93
|
-
function sleep(durationMs) {
|
|
94
|
-
return new Promise((resolve) => {
|
|
95
|
-
setTimeout(resolve, durationMs);
|
|
96
|
-
});
|
|
97
|
-
}
|
package/dist/telegram/runtime.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { formatUnixMsAsUtc } from "../tools/time";
|
|
2
|
-
import { formatError } from "../utils/error";
|
|
3
|
-
import { readTelegramHealthSnapshot, recordTelegramProbeFailure, recordTelegramProbeSuccess, recordTelegramSendFailure, } from "./state";
|
|
4
|
-
export class GatewayTelegramRuntime {
|
|
5
|
-
client;
|
|
6
|
-
delivery;
|
|
7
|
-
store;
|
|
8
|
-
logger;
|
|
9
|
-
config;
|
|
10
|
-
polling;
|
|
11
|
-
opencodeEvents;
|
|
12
|
-
constructor(client, delivery, store, logger, config, polling, opencodeEvents) {
|
|
13
|
-
this.client = client;
|
|
14
|
-
this.delivery = delivery;
|
|
15
|
-
this.store = store;
|
|
16
|
-
this.logger = logger;
|
|
17
|
-
this.config = config;
|
|
18
|
-
this.polling = polling;
|
|
19
|
-
this.opencodeEvents = opencodeEvents;
|
|
20
|
-
}
|
|
21
|
-
isEnabled() {
|
|
22
|
-
return this.config.enabled;
|
|
23
|
-
}
|
|
24
|
-
isPolling() {
|
|
25
|
-
return this.polling?.isRunning() ?? false;
|
|
26
|
-
}
|
|
27
|
-
allowlistMode() {
|
|
28
|
-
return this.config.enabled ? "explicit" : "disabled";
|
|
29
|
-
}
|
|
30
|
-
start() {
|
|
31
|
-
this.polling?.start();
|
|
32
|
-
}
|
|
33
|
-
async status() {
|
|
34
|
-
const snapshot = readTelegramHealthSnapshot(this.store);
|
|
35
|
-
if (!this.config.enabled || this.client === null) {
|
|
36
|
-
return {
|
|
37
|
-
...snapshot,
|
|
38
|
-
enabled: false,
|
|
39
|
-
polling: false,
|
|
40
|
-
allowlistMode: "disabled",
|
|
41
|
-
allowedChatsCount: 0,
|
|
42
|
-
allowedUsersCount: 0,
|
|
43
|
-
liveProbe: "disabled",
|
|
44
|
-
liveProbeError: null,
|
|
45
|
-
liveBotId: null,
|
|
46
|
-
liveBotUsername: null,
|
|
47
|
-
streamingEnabled: false,
|
|
48
|
-
opencodeEventStreamConnected: this.opencodeEvents.isConnected(),
|
|
49
|
-
lastEventStreamError: this.opencodeEvents.lastStreamError(),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
const bot = await this.client.getMe();
|
|
54
|
-
const recordedAtMs = Date.now();
|
|
55
|
-
recordTelegramProbeSuccess(this.store, bot, recordedAtMs);
|
|
56
|
-
return buildEnabledStatus(this.config, readTelegramHealthSnapshot(this.store), this.isPolling(), "ok", null, bot, this.opencodeEvents);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
const message = formatError(error);
|
|
60
|
-
const recordedAtMs = Date.now();
|
|
61
|
-
recordTelegramProbeFailure(this.store, message, recordedAtMs);
|
|
62
|
-
this.logger.log("warn", `telegram live probe failed: ${message}`);
|
|
63
|
-
return buildEnabledStatus(this.config, readTelegramHealthSnapshot(this.store), this.isPolling(), "failed", message, null, this.opencodeEvents);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
async sendTest(chatId, topic, text, mode) {
|
|
67
|
-
const normalizedChatId = normalizeRequiredField(chatId, "chat_id");
|
|
68
|
-
const normalizedTopic = normalizeOptionalField(topic);
|
|
69
|
-
const body = normalizeOptionalField(text) ?? defaultTestMessage();
|
|
70
|
-
if (!this.config.enabled || this.client === null) {
|
|
71
|
-
throw new Error("telegram is not enabled");
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
const sentAtMs = Date.now();
|
|
75
|
-
const result = await this.delivery.sendTest({
|
|
76
|
-
channel: "telegram",
|
|
77
|
-
target: normalizedChatId,
|
|
78
|
-
topic: normalizedTopic,
|
|
79
|
-
}, body, mode);
|
|
80
|
-
if (!result.delivered) {
|
|
81
|
-
throw new Error("telegram test delivery produced no final message");
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
chatId: normalizedChatId,
|
|
85
|
-
topic: normalizedTopic,
|
|
86
|
-
text: body,
|
|
87
|
-
sentAtMs,
|
|
88
|
-
mode: result.mode,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
const message = formatError(error);
|
|
93
|
-
recordTelegramSendFailure(this.store, message, Date.now());
|
|
94
|
-
this.logger.log("warn", `telegram_send_test failed: ${message}`);
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function buildEnabledStatus(config, snapshot, polling, liveProbe, liveProbeError, bot, opencodeEvents) {
|
|
100
|
-
return {
|
|
101
|
-
...snapshot,
|
|
102
|
-
enabled: true,
|
|
103
|
-
polling,
|
|
104
|
-
allowlistMode: "explicit",
|
|
105
|
-
allowedChatsCount: config.allowedChats.length,
|
|
106
|
-
allowedUsersCount: config.allowedUsers.length,
|
|
107
|
-
liveProbe,
|
|
108
|
-
liveProbeError,
|
|
109
|
-
liveBotId: bot ? String(bot.id) : null,
|
|
110
|
-
liveBotUsername: bot?.username ?? null,
|
|
111
|
-
streamingEnabled: true,
|
|
112
|
-
opencodeEventStreamConnected: opencodeEvents.isConnected(),
|
|
113
|
-
lastEventStreamError: opencodeEvents.lastStreamError(),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
function normalizeRequiredField(value, field) {
|
|
117
|
-
const trimmed = value.trim();
|
|
118
|
-
if (trimmed.length === 0) {
|
|
119
|
-
throw new Error(`${field} must not be empty`);
|
|
120
|
-
}
|
|
121
|
-
return trimmed;
|
|
122
|
-
}
|
|
123
|
-
function normalizeOptionalField(value) {
|
|
124
|
-
if (value === null) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
const trimmed = value.trim();
|
|
128
|
-
return trimmed.length === 0 ? null : trimmed;
|
|
129
|
-
}
|
|
130
|
-
function defaultTestMessage() {
|
|
131
|
-
const recordedAtMs = Date.now();
|
|
132
|
-
return `opencode-gateway telegram_send_test at ${formatUnixMsAsUtc(recordedAtMs)}`;
|
|
133
|
-
}
|