@alfe.ai/openclaw-telegram 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/bin/alfe-telegram.mjs +180 -0
- package/dist/index.cjs +4 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/plugin.cjs +503 -0
- package/dist/plugin.d.cts +58 -0
- package/dist/plugin.d.ts +58 -0
- package/dist/plugin.js +503 -0
- package/dist/telegram-bridge.cjs +300 -0
- package/dist/telegram-bridge.d.cts +72 -0
- package/dist/telegram-bridge.d.ts +72 -0
- package/dist/telegram-bridge.js +289 -0
- package/openclaw.plugin.json +21 -0
- package/package.json +65 -0
- package/python/bridge.py +633 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
//#region src/telegram-bridge.ts
|
|
7
|
+
const MAX_LINE_BYTES = 1024 * 1024;
|
|
8
|
+
const START_TIMEOUT_MS = 2e4;
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
10
|
+
const STOP_TIMEOUT_MS = 2e3;
|
|
11
|
+
var TelegramBridgeUnavailable = class extends Error {
|
|
12
|
+
name = "TelegramBridgeUnavailable";
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super("Telegram local runtime is unavailable");
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var TelegramBridge = class {
|
|
19
|
+
logger;
|
|
20
|
+
stateDir;
|
|
21
|
+
pythonExecutable;
|
|
22
|
+
bridgeScript;
|
|
23
|
+
spawnBridge;
|
|
24
|
+
startTimeoutMs;
|
|
25
|
+
onEvent;
|
|
26
|
+
onExit;
|
|
27
|
+
child = null;
|
|
28
|
+
buffer = "";
|
|
29
|
+
ready = false;
|
|
30
|
+
stopping = false;
|
|
31
|
+
requests = /* @__PURE__ */ new Map();
|
|
32
|
+
startResolve = null;
|
|
33
|
+
startReject = null;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.logger = options.logger;
|
|
36
|
+
this.stateDir = options.stateDir;
|
|
37
|
+
this.pythonExecutable = options.pythonExecutable ?? resolvePythonExecutable(this.stateDir);
|
|
38
|
+
this.bridgeScript = options.bridgeScript ?? fileURLToPath(new URL("../python/bridge.py", import.meta.url));
|
|
39
|
+
this.spawnBridge = options.spawnBridge ?? ((command, args) => spawn(command, args, {
|
|
40
|
+
stdio: [
|
|
41
|
+
"pipe",
|
|
42
|
+
"pipe",
|
|
43
|
+
"pipe"
|
|
44
|
+
],
|
|
45
|
+
env: minimalChildEnvironment()
|
|
46
|
+
}));
|
|
47
|
+
this.startTimeoutMs = options.startTimeoutMs ?? START_TIMEOUT_MS;
|
|
48
|
+
this.onEvent = options.onEvent;
|
|
49
|
+
this.onExit = options.onExit;
|
|
50
|
+
}
|
|
51
|
+
isReady() {
|
|
52
|
+
return this.ready;
|
|
53
|
+
}
|
|
54
|
+
async start() {
|
|
55
|
+
if (this.child) return;
|
|
56
|
+
if (!existsSync(this.pythonExecutable)) throw new TelegramBridgeUnavailable("runtime_missing");
|
|
57
|
+
if (!existsSync(this.bridgeScript)) throw new TelegramBridgeUnavailable("bridge_missing");
|
|
58
|
+
this.stopping = false;
|
|
59
|
+
this.buffer = "";
|
|
60
|
+
const child = this.spawnBridge(this.pythonExecutable, [
|
|
61
|
+
this.bridgeScript,
|
|
62
|
+
"run",
|
|
63
|
+
"--state-dir",
|
|
64
|
+
this.stateDir
|
|
65
|
+
]);
|
|
66
|
+
this.child = child;
|
|
67
|
+
child.stdout.setEncoding("utf8");
|
|
68
|
+
child.stdout.on("data", (chunk) => {
|
|
69
|
+
this.consume(chunk);
|
|
70
|
+
});
|
|
71
|
+
child.stderr.on("data", () => {
|
|
72
|
+
this.logger.debug("Telegram bridge emitted a redacted diagnostic");
|
|
73
|
+
});
|
|
74
|
+
child.on("error", () => {
|
|
75
|
+
this.handleExit("spawn_failed");
|
|
76
|
+
});
|
|
77
|
+
child.on("exit", () => {
|
|
78
|
+
this.handleExit("bridge_exited");
|
|
79
|
+
});
|
|
80
|
+
await new Promise((resolve, reject) => {
|
|
81
|
+
let settled = false;
|
|
82
|
+
const timer = setTimeout(() => {
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
this.startResolve = null;
|
|
86
|
+
this.startReject = null;
|
|
87
|
+
reject(new TelegramBridgeUnavailable("start_timeout"));
|
|
88
|
+
this.stop();
|
|
89
|
+
}, this.startTimeoutMs);
|
|
90
|
+
timer.unref();
|
|
91
|
+
this.startResolve = () => {
|
|
92
|
+
if (settled) return;
|
|
93
|
+
settled = true;
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
this.startResolve = null;
|
|
96
|
+
this.startReject = null;
|
|
97
|
+
resolve();
|
|
98
|
+
};
|
|
99
|
+
this.startReject = (error) => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
this.startResolve = null;
|
|
104
|
+
this.startReject = null;
|
|
105
|
+
reject(error);
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async request(method, params = {}) {
|
|
110
|
+
const child = this.child;
|
|
111
|
+
if (!child || !this.ready) throw new TelegramBridgeUnavailable("not_ready");
|
|
112
|
+
const id = randomUUID();
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
this.requests.delete(id);
|
|
116
|
+
reject(new TelegramBridgeUnavailable("request_timeout"));
|
|
117
|
+
}, REQUEST_TIMEOUT_MS);
|
|
118
|
+
timer.unref();
|
|
119
|
+
this.requests.set(id, {
|
|
120
|
+
resolve: (value) => {
|
|
121
|
+
resolve(value);
|
|
122
|
+
},
|
|
123
|
+
reject,
|
|
124
|
+
timer
|
|
125
|
+
});
|
|
126
|
+
const encoded = JSON.stringify({
|
|
127
|
+
id,
|
|
128
|
+
method,
|
|
129
|
+
params
|
|
130
|
+
});
|
|
131
|
+
child.stdin.write(`${encoded}\n`, (error) => {
|
|
132
|
+
if (!error) return;
|
|
133
|
+
const pending = this.requests.get(id);
|
|
134
|
+
if (!pending) return;
|
|
135
|
+
clearTimeout(pending.timer);
|
|
136
|
+
this.requests.delete(id);
|
|
137
|
+
pending.reject(new TelegramBridgeUnavailable("write_failed"));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async stop() {
|
|
142
|
+
await this.terminate(true, "stopped");
|
|
143
|
+
}
|
|
144
|
+
async terminate(callerRequested, code) {
|
|
145
|
+
const child = this.child;
|
|
146
|
+
if (!child) return;
|
|
147
|
+
this.stopping = callerRequested;
|
|
148
|
+
if (callerRequested && this.ready) await Promise.race([this.request("shutdown").catch(() => void 0), deadline(STOP_TIMEOUT_MS)]);
|
|
149
|
+
child.stdin.end();
|
|
150
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
151
|
+
this.handleExit(code);
|
|
152
|
+
}
|
|
153
|
+
consume(chunk) {
|
|
154
|
+
this.buffer += chunk;
|
|
155
|
+
if (Buffer.byteLength(this.buffer, "utf8") > MAX_LINE_BYTES && !this.buffer.includes("\n")) {
|
|
156
|
+
this.logger.warn("Telegram bridge exceeded the local message boundary");
|
|
157
|
+
this.terminate(false, "message_boundary_exceeded");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (;;) {
|
|
161
|
+
const newline = this.buffer.indexOf("\n");
|
|
162
|
+
if (newline < 0) break;
|
|
163
|
+
const line = this.buffer.slice(0, newline);
|
|
164
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
165
|
+
if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) {
|
|
166
|
+
this.logger.warn("Telegram bridge line exceeded the local message boundary");
|
|
167
|
+
this.terminate(false, "message_boundary_exceeded");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.handleLine(line);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
handleLine(line) {
|
|
174
|
+
let decoded;
|
|
175
|
+
try {
|
|
176
|
+
decoded = JSON.parse(line);
|
|
177
|
+
} catch {
|
|
178
|
+
this.logger.warn("Telegram bridge returned invalid local data");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isRecord(decoded) || typeof decoded.type !== "string") return;
|
|
182
|
+
if (decoded.type === "ready") {
|
|
183
|
+
this.ready = true;
|
|
184
|
+
this.startResolve?.();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (decoded.type === "fatal") {
|
|
188
|
+
const code = safeCode(decoded.code);
|
|
189
|
+
this.startReject?.(new TelegramBridgeUnavailable(code));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (decoded.type === "response" && typeof decoded.id === "string") {
|
|
193
|
+
const pending = this.requests.get(decoded.id);
|
|
194
|
+
if (!pending) return;
|
|
195
|
+
clearTimeout(pending.timer);
|
|
196
|
+
this.requests.delete(decoded.id);
|
|
197
|
+
if (decoded.ok === true) pending.resolve(decoded.result);
|
|
198
|
+
else pending.reject(new TelegramBridgeUnavailable(safeCode(decoded.code)));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (decoded.type === "health") {
|
|
202
|
+
this.logger.warn(`Telegram bridge health warning: ${safeCode(decoded.code)}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (decoded.type === "event") {
|
|
206
|
+
const event = parseEvent(decoded.event);
|
|
207
|
+
if (!event) {
|
|
208
|
+
this.logger.warn("Telegram bridge returned an invalid event");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
this.onEvent(event);
|
|
213
|
+
} catch {
|
|
214
|
+
this.logger.warn("Telegram local event callback failed");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
handleExit(code) {
|
|
219
|
+
if (!this.child && !this.ready) return;
|
|
220
|
+
this.child = null;
|
|
221
|
+
this.ready = false;
|
|
222
|
+
this.buffer = "";
|
|
223
|
+
this.startReject?.(new TelegramBridgeUnavailable(code));
|
|
224
|
+
this.startResolve = null;
|
|
225
|
+
this.startReject = null;
|
|
226
|
+
for (const pending of this.requests.values()) {
|
|
227
|
+
clearTimeout(pending.timer);
|
|
228
|
+
pending.reject(new TelegramBridgeUnavailable(code));
|
|
229
|
+
}
|
|
230
|
+
this.requests.clear();
|
|
231
|
+
if (!this.stopping) this.onExit?.();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
function resolvePythonExecutable(stateDir) {
|
|
235
|
+
const unix = join(stateDir, "runtime", "bin", "python3");
|
|
236
|
+
if (existsSync(unix)) return unix;
|
|
237
|
+
return join(stateDir, "runtime", "Scripts", "python.exe");
|
|
238
|
+
}
|
|
239
|
+
function minimalChildEnvironment() {
|
|
240
|
+
const allowed = [
|
|
241
|
+
"HOME",
|
|
242
|
+
"PATH",
|
|
243
|
+
"LANG",
|
|
244
|
+
"LC_ALL",
|
|
245
|
+
"TMPDIR",
|
|
246
|
+
"SYSTEMROOT",
|
|
247
|
+
"WINDIR"
|
|
248
|
+
];
|
|
249
|
+
const result = {};
|
|
250
|
+
for (const key of allowed) {
|
|
251
|
+
const value = process.env[key];
|
|
252
|
+
if (value !== void 0) result[key] = value;
|
|
253
|
+
}
|
|
254
|
+
result.PYTHONUNBUFFERED = "1";
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
function parseEvent(value) {
|
|
258
|
+
if (!isRecord(value)) return null;
|
|
259
|
+
const { deliveryId, messageId, chatId, chatTitle, chatKind, senderId, senderName, senderUsername, text, timestamp } = value;
|
|
260
|
+
if (typeof deliveryId !== "string" || typeof messageId !== "string" || typeof chatId !== "string" || typeof chatTitle !== "string" || typeof chatKind !== "string" || typeof senderId !== "string" || typeof senderName !== "string" || typeof text !== "string" || typeof timestamp !== "string") return null;
|
|
261
|
+
if (chatKind !== "private" && chatKind !== "group" && chatKind !== "channel") return null;
|
|
262
|
+
if (text.length === 0 || text.length > 32e3) return null;
|
|
263
|
+
if (!Number.isFinite(Date.parse(timestamp))) return null;
|
|
264
|
+
return {
|
|
265
|
+
deliveryId,
|
|
266
|
+
messageId,
|
|
267
|
+
chatId,
|
|
268
|
+
chatTitle,
|
|
269
|
+
chatKind,
|
|
270
|
+
senderId,
|
|
271
|
+
senderName,
|
|
272
|
+
...typeof senderUsername === "string" ? { senderUsername } : {},
|
|
273
|
+
text,
|
|
274
|
+
timestamp
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function safeCode(value) {
|
|
278
|
+
return typeof value === "string" && /^[a-z0-9_]{1,64}$/.test(value) ? value : "bridge_error";
|
|
279
|
+
}
|
|
280
|
+
function isRecord(value) {
|
|
281
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
282
|
+
}
|
|
283
|
+
async function deadline(milliseconds) {
|
|
284
|
+
await new Promise((resolve) => {
|
|
285
|
+
setTimeout(resolve, milliseconds).unref();
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
export { TelegramBridgeUnavailable as n, TelegramBridge as t };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "@alfe.ai/openclaw-telegram",
|
|
3
|
+
"name": "Telegram (local user session)",
|
|
4
|
+
"description": "Explicitly subscribe to Telegram chats through a user session held only on the agent machine",
|
|
5
|
+
"entry": "./dist/plugin.js",
|
|
6
|
+
"activation": { "onStartup": true },
|
|
7
|
+
"contracts": {
|
|
8
|
+
"tools": [
|
|
9
|
+
"telegram_status",
|
|
10
|
+
"telegram_list_chats",
|
|
11
|
+
"telegram_list_subscriptions",
|
|
12
|
+
"telegram_subscribe",
|
|
13
|
+
"telegram_unsubscribe"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"configSchema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {}
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alfe.ai/openclaw-telegram",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Local-custody Telegram MTProto user-session listener for OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/plugin.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./plugin": {
|
|
15
|
+
"types": "./dist/plugin.d.ts",
|
|
16
|
+
"require": "./dist/plugin.cjs",
|
|
17
|
+
"import": "./dist/plugin.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"alfe-telegram": "./bin/alfe-telegram.mjs"
|
|
22
|
+
},
|
|
23
|
+
"openclaw": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./dist/plugin.js"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"dist",
|
|
31
|
+
"python/bridge.py",
|
|
32
|
+
"README.md",
|
|
33
|
+
"openclaw.plugin.json"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@alfe.ai/agent-api-client": "0.17.0",
|
|
37
|
+
"@alfe.ai/config": "0.4.1",
|
|
38
|
+
"@alfe.ai/openclaw-plugin-kit": "0.2.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"openclaw": ">=2026.3.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"openclaw": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"license": "UNLICENSED",
|
|
49
|
+
"homepage": "https://alfe.ai",
|
|
50
|
+
"author": "Alfe (https://alfe.ai)",
|
|
51
|
+
"keywords": [
|
|
52
|
+
"alfe",
|
|
53
|
+
"openclaw",
|
|
54
|
+
"telegram",
|
|
55
|
+
"telethon",
|
|
56
|
+
"mtproto"
|
|
57
|
+
],
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown",
|
|
60
|
+
"dev": "tsdown --watch",
|
|
61
|
+
"test": "python3 -m unittest discover -s python -p 'test_*.py' && vitest run",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"lint": "eslint ."
|
|
64
|
+
}
|
|
65
|
+
}
|