teleton 0.5.2 → 0.6.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/README.md +239 -101
- package/dist/chunk-2QUJLHCZ.js +362 -0
- package/dist/chunk-4IPJ25HE.js +2839 -0
- package/dist/{chunk-WOXBZOQX.js → chunk-6L6KGATM.js} +1026 -3071
- package/dist/{chunk-WUTMT6DW.js → chunk-ADCMUNYU.js} +65 -522
- package/dist/chunk-D5I7GBV7.js +322 -0
- package/dist/chunk-ECSCVEQQ.js +139 -0
- package/dist/chunk-GDCODBNO.js +72 -0
- package/dist/{chunk-O4R7V5Y2.js → chunk-RO62LO6Z.js} +11 -1
- package/dist/cli/index.js +344 -22
- package/dist/index.js +7 -4
- package/dist/{memory-Y5J7CXAR.js → memory-TVDOGQXS.js} +14 -10
- package/dist/{migrate-UEQCDWL2.js → migrate-QIEMPOMT.js} +5 -2
- package/dist/{server-BQY7CM2N.js → server-RSWVCVY3.js} +805 -26
- package/dist/{task-dependency-resolver-TRPILAHM.js → task-dependency-resolver-72DLY2HV.js} +1 -1
- package/dist/{task-executor-N7XNVK5N.js → task-executor-VXB6DAV2.js} +1 -1
- package/dist/tool-index-DKI2ZNOU.js +245 -0
- package/dist/web/assets/index-BNhrx9S1.js +67 -0
- package/dist/web/assets/index-CqrrRLOh.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +16 -14
- package/dist/chunk-5WWR4CU3.js +0 -124
- package/dist/web/assets/index-CDMbujHf.css +0 -1
- package/dist/web/assets/index-DDX8oQ2z.js +0 -67
|
@@ -0,0 +1,2839 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDbWrapper,
|
|
3
|
+
migrateFromMainDb,
|
|
4
|
+
openModuleDb
|
|
5
|
+
} from "./chunk-ECSCVEQQ.js";
|
|
6
|
+
import {
|
|
7
|
+
COINGECKO_API_URL,
|
|
8
|
+
fetchWithTimeout,
|
|
9
|
+
tonapiFetch
|
|
10
|
+
} from "./chunk-GDCODBNO.js";
|
|
11
|
+
import {
|
|
12
|
+
RETRY_BLOCKCHAIN_BASE_DELAY_MS,
|
|
13
|
+
RETRY_BLOCKCHAIN_MAX_DELAY_MS,
|
|
14
|
+
RETRY_BLOCKCHAIN_TIMEOUT_MS,
|
|
15
|
+
RETRY_DEFAULT_BASE_DELAY_MS,
|
|
16
|
+
RETRY_DEFAULT_MAX_ATTEMPTS,
|
|
17
|
+
RETRY_DEFAULT_MAX_DELAY_MS,
|
|
18
|
+
RETRY_DEFAULT_TIMEOUT_MS
|
|
19
|
+
} from "./chunk-4DU3C27M.js";
|
|
20
|
+
import {
|
|
21
|
+
PAYMENT_TOLERANCE_RATIO,
|
|
22
|
+
TELEGRAM_MAX_MESSAGE_LENGTH
|
|
23
|
+
} from "./chunk-RO62LO6Z.js";
|
|
24
|
+
import {
|
|
25
|
+
ALLOWED_EXTENSIONS,
|
|
26
|
+
TELETON_ROOT,
|
|
27
|
+
WORKSPACE_PATHS,
|
|
28
|
+
WORKSPACE_ROOT
|
|
29
|
+
} from "./chunk-EYWNOHMJ.js";
|
|
30
|
+
import {
|
|
31
|
+
getCachedHttpEndpoint
|
|
32
|
+
} from "./chunk-QUAPFI2N.js";
|
|
33
|
+
import {
|
|
34
|
+
__require
|
|
35
|
+
} from "./chunk-QGM4M3NI.js";
|
|
36
|
+
|
|
37
|
+
// src/config/providers.ts
|
|
38
|
+
var PROVIDER_REGISTRY = {
|
|
39
|
+
anthropic: {
|
|
40
|
+
id: "anthropic",
|
|
41
|
+
displayName: "Anthropic (Claude)",
|
|
42
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
43
|
+
keyPrefix: "sk-ant-",
|
|
44
|
+
keyHint: "sk-ant-api03-...",
|
|
45
|
+
consoleUrl: "https://console.anthropic.com/",
|
|
46
|
+
defaultModel: "claude-opus-4-5-20251101",
|
|
47
|
+
utilityModel: "claude-3-5-haiku-20241022",
|
|
48
|
+
toolLimit: null,
|
|
49
|
+
piAiProvider: "anthropic"
|
|
50
|
+
},
|
|
51
|
+
openai: {
|
|
52
|
+
id: "openai",
|
|
53
|
+
displayName: "OpenAI (GPT-4o)",
|
|
54
|
+
envVar: "OPENAI_API_KEY",
|
|
55
|
+
keyPrefix: "sk-",
|
|
56
|
+
keyHint: "sk-proj-...",
|
|
57
|
+
consoleUrl: "https://platform.openai.com/api-keys",
|
|
58
|
+
defaultModel: "gpt-4o",
|
|
59
|
+
utilityModel: "gpt-4o-mini",
|
|
60
|
+
toolLimit: 128,
|
|
61
|
+
piAiProvider: "openai"
|
|
62
|
+
},
|
|
63
|
+
google: {
|
|
64
|
+
id: "google",
|
|
65
|
+
displayName: "Google (Gemini)",
|
|
66
|
+
envVar: "GOOGLE_API_KEY",
|
|
67
|
+
keyPrefix: null,
|
|
68
|
+
keyHint: "AIza...",
|
|
69
|
+
consoleUrl: "https://aistudio.google.com/apikey",
|
|
70
|
+
defaultModel: "gemini-2.5-flash",
|
|
71
|
+
utilityModel: "gemini-2.0-flash-lite",
|
|
72
|
+
toolLimit: 128,
|
|
73
|
+
piAiProvider: "google"
|
|
74
|
+
},
|
|
75
|
+
xai: {
|
|
76
|
+
id: "xai",
|
|
77
|
+
displayName: "xAI (Grok)",
|
|
78
|
+
envVar: "XAI_API_KEY",
|
|
79
|
+
keyPrefix: "xai-",
|
|
80
|
+
keyHint: "xai-...",
|
|
81
|
+
consoleUrl: "https://console.x.ai/",
|
|
82
|
+
defaultModel: "grok-3",
|
|
83
|
+
utilityModel: "grok-3-mini-fast",
|
|
84
|
+
toolLimit: 128,
|
|
85
|
+
piAiProvider: "xai"
|
|
86
|
+
},
|
|
87
|
+
groq: {
|
|
88
|
+
id: "groq",
|
|
89
|
+
displayName: "Groq",
|
|
90
|
+
envVar: "GROQ_API_KEY",
|
|
91
|
+
keyPrefix: "gsk_",
|
|
92
|
+
keyHint: "gsk_...",
|
|
93
|
+
consoleUrl: "https://console.groq.com/keys",
|
|
94
|
+
defaultModel: "llama-3.3-70b-versatile",
|
|
95
|
+
utilityModel: "llama-3.1-8b-instant",
|
|
96
|
+
toolLimit: 128,
|
|
97
|
+
piAiProvider: "groq"
|
|
98
|
+
},
|
|
99
|
+
openrouter: {
|
|
100
|
+
id: "openrouter",
|
|
101
|
+
displayName: "OpenRouter",
|
|
102
|
+
envVar: "OPENROUTER_API_KEY",
|
|
103
|
+
keyPrefix: "sk-or-",
|
|
104
|
+
keyHint: "sk-or-v1-...",
|
|
105
|
+
consoleUrl: "https://openrouter.ai/keys",
|
|
106
|
+
defaultModel: "anthropic/claude-opus-4.5",
|
|
107
|
+
utilityModel: "google/gemini-2.5-flash-lite",
|
|
108
|
+
toolLimit: 128,
|
|
109
|
+
piAiProvider: "openrouter"
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function getProviderMetadata(provider) {
|
|
113
|
+
const meta = PROVIDER_REGISTRY[provider];
|
|
114
|
+
if (!meta) {
|
|
115
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
116
|
+
}
|
|
117
|
+
return meta;
|
|
118
|
+
}
|
|
119
|
+
function getSupportedProviders() {
|
|
120
|
+
return Object.values(PROVIDER_REGISTRY);
|
|
121
|
+
}
|
|
122
|
+
function validateApiKeyFormat(provider, key) {
|
|
123
|
+
const meta = PROVIDER_REGISTRY[provider];
|
|
124
|
+
if (!meta) return `Unknown provider: ${provider}`;
|
|
125
|
+
if (!key || key.trim().length === 0) return "API key is required";
|
|
126
|
+
if (meta.keyPrefix && !key.startsWith(meta.keyPrefix)) {
|
|
127
|
+
return `Invalid format (should start with ${meta.keyPrefix})`;
|
|
128
|
+
}
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/config/loader.ts
|
|
133
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
134
|
+
import { parse, stringify } from "yaml";
|
|
135
|
+
import { homedir } from "os";
|
|
136
|
+
import { dirname, join } from "path";
|
|
137
|
+
|
|
138
|
+
// src/config/schema.ts
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
var DMPolicy = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
|
141
|
+
var GroupPolicy = z.enum(["open", "allowlist", "disabled"]);
|
|
142
|
+
var SessionResetPolicySchema = z.object({
|
|
143
|
+
daily_reset_enabled: z.boolean().default(true).describe("Enable daily session reset"),
|
|
144
|
+
daily_reset_hour: z.number().min(0).max(23).default(4).describe("Hour of day (0-23) to reset sessions"),
|
|
145
|
+
idle_expiry_enabled: z.boolean().default(true).describe("Enable session reset after idle period"),
|
|
146
|
+
idle_expiry_minutes: z.number().default(1440).describe("Minutes of inactivity before session reset (default: 24h)")
|
|
147
|
+
});
|
|
148
|
+
var AgentConfigSchema = z.object({
|
|
149
|
+
provider: z.enum(["anthropic", "openai", "google", "xai", "groq", "openrouter"]).default("anthropic"),
|
|
150
|
+
api_key: z.string(),
|
|
151
|
+
model: z.string().default("claude-opus-4-5-20251101"),
|
|
152
|
+
utility_model: z.string().optional().describe("Cheap model for summarization (auto-detected if omitted)"),
|
|
153
|
+
max_tokens: z.number().default(4096),
|
|
154
|
+
temperature: z.number().default(0.7),
|
|
155
|
+
system_prompt: z.string().nullable().default(null),
|
|
156
|
+
max_agentic_iterations: z.number().default(5).describe("Maximum number of agentic loop iterations (tool call \u2192 result \u2192 tool call cycles)"),
|
|
157
|
+
session_reset_policy: SessionResetPolicySchema.default(SessionResetPolicySchema.parse({}))
|
|
158
|
+
});
|
|
159
|
+
var TelegramConfigSchema = z.object({
|
|
160
|
+
api_id: z.number(),
|
|
161
|
+
api_hash: z.string(),
|
|
162
|
+
phone: z.string(),
|
|
163
|
+
session_name: z.string().default("teleton_session"),
|
|
164
|
+
session_path: z.string().default("~/.teleton"),
|
|
165
|
+
dm_policy: DMPolicy.default("pairing"),
|
|
166
|
+
allow_from: z.array(z.number()).default([]),
|
|
167
|
+
group_policy: GroupPolicy.default("open"),
|
|
168
|
+
group_allow_from: z.array(z.number()).default([]),
|
|
169
|
+
require_mention: z.boolean().default(true),
|
|
170
|
+
max_message_length: z.number().default(TELEGRAM_MAX_MESSAGE_LENGTH),
|
|
171
|
+
typing_simulation: z.boolean().default(true),
|
|
172
|
+
rate_limit_messages_per_second: z.number().default(1),
|
|
173
|
+
rate_limit_groups_per_minute: z.number().default(20),
|
|
174
|
+
admin_ids: z.array(z.number()).default([]),
|
|
175
|
+
agent_channel: z.string().nullable().default(null),
|
|
176
|
+
owner_name: z.string().optional().describe("Owner's first name (e.g., 'Alex')"),
|
|
177
|
+
owner_username: z.string().optional().describe("Owner's Telegram username (without @)"),
|
|
178
|
+
owner_id: z.number().optional().describe("Owner's Telegram user ID"),
|
|
179
|
+
debounce_ms: z.number().default(1500).describe("Debounce delay in milliseconds for group messages (0 = disabled)"),
|
|
180
|
+
bot_token: z.string().optional().describe("Telegram Bot token from @BotFather for inline deal buttons"),
|
|
181
|
+
bot_username: z.string().optional().describe("Bot username without @ (e.g., 'teleton_deals_bot')")
|
|
182
|
+
});
|
|
183
|
+
var StorageConfigSchema = z.object({
|
|
184
|
+
sessions_file: z.string().default("~/.teleton/sessions.json"),
|
|
185
|
+
pairing_file: z.string().default("~/.teleton/pairing.json"),
|
|
186
|
+
memory_file: z.string().default("~/.teleton/memory.json"),
|
|
187
|
+
history_limit: z.number().default(100)
|
|
188
|
+
});
|
|
189
|
+
var MetaConfigSchema = z.object({
|
|
190
|
+
version: z.string().default("1.0.0"),
|
|
191
|
+
created_at: z.string().optional(),
|
|
192
|
+
last_modified_at: z.string().optional(),
|
|
193
|
+
onboard_command: z.string().default("teleton setup")
|
|
194
|
+
});
|
|
195
|
+
var _DealsObject = z.object({
|
|
196
|
+
enabled: z.boolean().default(true),
|
|
197
|
+
expiry_seconds: z.number().default(120),
|
|
198
|
+
buy_max_floor_percent: z.number().default(100),
|
|
199
|
+
sell_min_floor_percent: z.number().default(105),
|
|
200
|
+
poll_interval_ms: z.number().default(5e3),
|
|
201
|
+
max_verification_retries: z.number().default(12),
|
|
202
|
+
expiry_check_interval_ms: z.number().default(6e4)
|
|
203
|
+
});
|
|
204
|
+
var DealsConfigSchema = _DealsObject.default(_DealsObject.parse({}));
|
|
205
|
+
var _WebUIObject = z.object({
|
|
206
|
+
enabled: z.boolean().default(false).describe("Enable WebUI server"),
|
|
207
|
+
port: z.number().default(7777).describe("HTTP server port"),
|
|
208
|
+
host: z.string().default("127.0.0.1").describe("Bind address (localhost only for security)"),
|
|
209
|
+
auth_token: z.string().optional().describe("Bearer token for API auth (auto-generated if omitted)"),
|
|
210
|
+
cors_origins: z.array(z.string()).default(["http://localhost:5173", "http://localhost:7777"]).describe("Allowed CORS origins for development"),
|
|
211
|
+
log_requests: z.boolean().default(false).describe("Log all HTTP requests")
|
|
212
|
+
});
|
|
213
|
+
var WebUIConfigSchema = _WebUIObject.default(_WebUIObject.parse({}));
|
|
214
|
+
var _EmbeddingObject = z.object({
|
|
215
|
+
provider: z.enum(["local", "anthropic", "none"]).default("local").describe("Embedding provider: local (ONNX), anthropic (API), or none (FTS5-only)"),
|
|
216
|
+
model: z.string().optional().describe("Model override (default: Xenova/all-MiniLM-L6-v2 for local)")
|
|
217
|
+
});
|
|
218
|
+
var EmbeddingConfigSchema = _EmbeddingObject.default(_EmbeddingObject.parse({}));
|
|
219
|
+
var _DevObject = z.object({
|
|
220
|
+
hot_reload: z.boolean().default(false).describe("Enable plugin hot-reload (watches ~/.teleton/plugins/ for changes)")
|
|
221
|
+
});
|
|
222
|
+
var DevConfigSchema = _DevObject.default(_DevObject.parse({}));
|
|
223
|
+
var McpServerSchema = z.object({
|
|
224
|
+
command: z.string().optional().describe("Stdio command (e.g. 'npx @modelcontextprotocol/server-filesystem /tmp')"),
|
|
225
|
+
args: z.array(z.string()).optional().describe("Explicit args array (overrides command splitting)"),
|
|
226
|
+
env: z.record(z.string(), z.string()).optional().describe("Environment variables for stdio server"),
|
|
227
|
+
url: z.string().url().optional().describe("SSE/HTTP endpoint URL (alternative to command)"),
|
|
228
|
+
scope: z.enum(["always", "dm-only", "group-only", "admin-only"]).default("always").describe("Tool scope"),
|
|
229
|
+
enabled: z.boolean().default(true).describe("Enable/disable this server")
|
|
230
|
+
}).refine((s) => s.command || s.url, {
|
|
231
|
+
message: "Each MCP server needs either 'command' (stdio) or 'url' (SSE/HTTP)"
|
|
232
|
+
});
|
|
233
|
+
var _McpObject = z.object({
|
|
234
|
+
servers: z.record(z.string(), McpServerSchema).default({})
|
|
235
|
+
});
|
|
236
|
+
var McpConfigSchema = _McpObject.default(_McpObject.parse({}));
|
|
237
|
+
var _ToolRagObject = z.object({
|
|
238
|
+
enabled: z.boolean().default(false).describe("Enable semantic tool retrieval (Tool RAG)"),
|
|
239
|
+
top_k: z.number().default(25).describe("Max tools to retrieve per LLM call"),
|
|
240
|
+
always_include: z.array(z.string()).default([
|
|
241
|
+
"telegram_send_message",
|
|
242
|
+
"telegram_reply_message",
|
|
243
|
+
"telegram_send_photo",
|
|
244
|
+
"telegram_send_document",
|
|
245
|
+
"journal_*",
|
|
246
|
+
"workspace_*",
|
|
247
|
+
"web_*"
|
|
248
|
+
]).describe("Tool name patterns always included (prefix glob with *)"),
|
|
249
|
+
skip_unlimited_providers: z.boolean().default(false).describe("Skip Tool RAG for providers with no tool limit (e.g. Anthropic)")
|
|
250
|
+
});
|
|
251
|
+
var ToolRagConfigSchema = _ToolRagObject.default(_ToolRagObject.parse({}));
|
|
252
|
+
var ConfigSchema = z.object({
|
|
253
|
+
meta: MetaConfigSchema.default(MetaConfigSchema.parse({})),
|
|
254
|
+
agent: AgentConfigSchema,
|
|
255
|
+
telegram: TelegramConfigSchema,
|
|
256
|
+
storage: StorageConfigSchema.default(StorageConfigSchema.parse({})),
|
|
257
|
+
embedding: EmbeddingConfigSchema,
|
|
258
|
+
deals: DealsConfigSchema,
|
|
259
|
+
webui: WebUIConfigSchema,
|
|
260
|
+
dev: DevConfigSchema,
|
|
261
|
+
tool_rag: ToolRagConfigSchema,
|
|
262
|
+
mcp: McpConfigSchema,
|
|
263
|
+
plugins: z.record(z.string(), z.unknown()).default({}).describe("Per-plugin config (key = plugin name with underscores)"),
|
|
264
|
+
tonapi_key: z.string().optional().describe("TonAPI key for higher rate limits (from @tonapi_bot)"),
|
|
265
|
+
tavily_api_key: z.string().optional().describe("Tavily API key for web search & extract (free at https://tavily.com)")
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// src/config/loader.ts
|
|
269
|
+
var DEFAULT_CONFIG_PATH = join(TELETON_ROOT, "config.yaml");
|
|
270
|
+
function expandPath(path) {
|
|
271
|
+
if (path.startsWith("~")) {
|
|
272
|
+
return join(homedir(), path.slice(1));
|
|
273
|
+
}
|
|
274
|
+
return path;
|
|
275
|
+
}
|
|
276
|
+
function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
277
|
+
const fullPath = expandPath(configPath);
|
|
278
|
+
if (!existsSync(fullPath)) {
|
|
279
|
+
throw new Error(`Config file not found: ${fullPath}
|
|
280
|
+
Run 'teleton setup' to create one.`);
|
|
281
|
+
}
|
|
282
|
+
let content;
|
|
283
|
+
try {
|
|
284
|
+
content = readFileSync(fullPath, "utf-8");
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw new Error(`Cannot read config file ${fullPath}: ${error.message}`);
|
|
287
|
+
}
|
|
288
|
+
let raw;
|
|
289
|
+
try {
|
|
290
|
+
raw = parse(content);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new Error(`Invalid YAML in ${fullPath}: ${error.message}`);
|
|
293
|
+
}
|
|
294
|
+
if (raw && typeof raw === "object" && "market" in raw) {
|
|
295
|
+
console.warn("\u26A0\uFE0F config.market is deprecated and ignored. Use market-api plugin instead.");
|
|
296
|
+
delete raw.market;
|
|
297
|
+
}
|
|
298
|
+
const result = ConfigSchema.safeParse(raw);
|
|
299
|
+
if (!result.success) {
|
|
300
|
+
throw new Error(`Invalid config: ${result.error.message}`);
|
|
301
|
+
}
|
|
302
|
+
const config = result.data;
|
|
303
|
+
const provider = config.agent.provider;
|
|
304
|
+
if (provider !== "anthropic" && !raw.agent?.model) {
|
|
305
|
+
const meta = getProviderMetadata(provider);
|
|
306
|
+
config.agent.model = meta.defaultModel;
|
|
307
|
+
}
|
|
308
|
+
config.telegram.session_path = expandPath(config.telegram.session_path);
|
|
309
|
+
config.storage.sessions_file = expandPath(config.storage.sessions_file);
|
|
310
|
+
config.storage.pairing_file = expandPath(config.storage.pairing_file);
|
|
311
|
+
config.storage.memory_file = expandPath(config.storage.memory_file);
|
|
312
|
+
if (process.env.TELETON_API_KEY) {
|
|
313
|
+
config.agent.api_key = process.env.TELETON_API_KEY;
|
|
314
|
+
}
|
|
315
|
+
if (process.env.TELETON_TG_API_ID) {
|
|
316
|
+
const apiId = parseInt(process.env.TELETON_TG_API_ID, 10);
|
|
317
|
+
if (isNaN(apiId)) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Invalid TELETON_TG_API_ID environment variable: "${process.env.TELETON_TG_API_ID}" is not a valid integer`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
config.telegram.api_id = apiId;
|
|
323
|
+
}
|
|
324
|
+
if (process.env.TELETON_TG_API_HASH) {
|
|
325
|
+
config.telegram.api_hash = process.env.TELETON_TG_API_HASH;
|
|
326
|
+
}
|
|
327
|
+
if (process.env.TELETON_TG_PHONE) {
|
|
328
|
+
config.telegram.phone = process.env.TELETON_TG_PHONE;
|
|
329
|
+
}
|
|
330
|
+
if (process.env.TELETON_WEBUI_ENABLED) {
|
|
331
|
+
config.webui.enabled = process.env.TELETON_WEBUI_ENABLED === "true";
|
|
332
|
+
}
|
|
333
|
+
if (process.env.TELETON_WEBUI_PORT) {
|
|
334
|
+
const port = parseInt(process.env.TELETON_WEBUI_PORT, 10);
|
|
335
|
+
if (!isNaN(port)) {
|
|
336
|
+
config.webui.port = port;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (process.env.TELETON_WEBUI_HOST) {
|
|
340
|
+
config.webui.host = process.env.TELETON_WEBUI_HOST;
|
|
341
|
+
}
|
|
342
|
+
if (process.env.TELETON_TAVILY_API_KEY) {
|
|
343
|
+
config.tavily_api_key = process.env.TELETON_TAVILY_API_KEY;
|
|
344
|
+
}
|
|
345
|
+
if (process.env.TELETON_TONAPI_KEY) {
|
|
346
|
+
config.tonapi_key = process.env.TELETON_TONAPI_KEY;
|
|
347
|
+
}
|
|
348
|
+
return config;
|
|
349
|
+
}
|
|
350
|
+
function configExists(configPath = DEFAULT_CONFIG_PATH) {
|
|
351
|
+
return existsSync(expandPath(configPath));
|
|
352
|
+
}
|
|
353
|
+
function getDefaultConfigPath() {
|
|
354
|
+
return DEFAULT_CONFIG_PATH;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/ton/wallet-service.ts
|
|
358
|
+
import { mnemonicNew, mnemonicToPrivateKey, mnemonicValidate } from "@ton/crypto";
|
|
359
|
+
import { WalletContractV5R1, TonClient, fromNano } from "@ton/ton";
|
|
360
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "fs";
|
|
361
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
362
|
+
var WALLET_FILE = join2(TELETON_ROOT, "wallet.json");
|
|
363
|
+
var _walletCache;
|
|
364
|
+
var _keyPairCache = null;
|
|
365
|
+
async function generateWallet() {
|
|
366
|
+
const mnemonic = await mnemonicNew(24);
|
|
367
|
+
const keyPair = await mnemonicToPrivateKey(mnemonic);
|
|
368
|
+
const wallet = WalletContractV5R1.create({
|
|
369
|
+
workchain: 0,
|
|
370
|
+
publicKey: keyPair.publicKey
|
|
371
|
+
});
|
|
372
|
+
const address = wallet.address.toString({ bounceable: true, testOnly: false });
|
|
373
|
+
return {
|
|
374
|
+
version: "w5r1",
|
|
375
|
+
address,
|
|
376
|
+
publicKey: keyPair.publicKey.toString("hex"),
|
|
377
|
+
mnemonic,
|
|
378
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function saveWallet(wallet) {
|
|
382
|
+
const dir = dirname2(WALLET_FILE);
|
|
383
|
+
if (!existsSync2(dir)) {
|
|
384
|
+
mkdirSync2(dir, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
writeFileSync2(WALLET_FILE, JSON.stringify(wallet, null, 2), "utf-8");
|
|
387
|
+
chmodSync2(WALLET_FILE, 384);
|
|
388
|
+
_walletCache = void 0;
|
|
389
|
+
_keyPairCache = null;
|
|
390
|
+
}
|
|
391
|
+
function loadWallet() {
|
|
392
|
+
if (_walletCache !== void 0) return _walletCache;
|
|
393
|
+
if (!existsSync2(WALLET_FILE)) {
|
|
394
|
+
_walletCache = null;
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const content = readFileSync2(WALLET_FILE, "utf-8");
|
|
399
|
+
_walletCache = JSON.parse(content);
|
|
400
|
+
return _walletCache;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
console.error("Failed to load wallet:", error);
|
|
403
|
+
_walletCache = null;
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function walletExists() {
|
|
408
|
+
return existsSync2(WALLET_FILE);
|
|
409
|
+
}
|
|
410
|
+
async function importWallet(mnemonic) {
|
|
411
|
+
const valid = await mnemonicValidate(mnemonic);
|
|
412
|
+
if (!valid) {
|
|
413
|
+
throw new Error("Invalid mnemonic: words do not form a valid TON seed phrase");
|
|
414
|
+
}
|
|
415
|
+
const keyPair = await mnemonicToPrivateKey(mnemonic);
|
|
416
|
+
const wallet = WalletContractV5R1.create({
|
|
417
|
+
workchain: 0,
|
|
418
|
+
publicKey: keyPair.publicKey
|
|
419
|
+
});
|
|
420
|
+
const address = wallet.address.toString({ bounceable: true, testOnly: false });
|
|
421
|
+
return {
|
|
422
|
+
version: "w5r1",
|
|
423
|
+
address,
|
|
424
|
+
publicKey: keyPair.publicKey.toString("hex"),
|
|
425
|
+
mnemonic,
|
|
426
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function getWalletAddress() {
|
|
430
|
+
const wallet = loadWallet();
|
|
431
|
+
return wallet?.address || null;
|
|
432
|
+
}
|
|
433
|
+
async function getKeyPair() {
|
|
434
|
+
if (_keyPairCache) return _keyPairCache;
|
|
435
|
+
const wallet = loadWallet();
|
|
436
|
+
if (!wallet) return null;
|
|
437
|
+
_keyPairCache = await mnemonicToPrivateKey(wallet.mnemonic);
|
|
438
|
+
return _keyPairCache;
|
|
439
|
+
}
|
|
440
|
+
async function getWalletBalance(address) {
|
|
441
|
+
try {
|
|
442
|
+
const endpoint = await getCachedHttpEndpoint();
|
|
443
|
+
const client = new TonClient({ endpoint });
|
|
444
|
+
const { Address: Address2 } = await import("@ton/core");
|
|
445
|
+
const addressObj = Address2.parse(address);
|
|
446
|
+
const balance = await client.getBalance(addressObj);
|
|
447
|
+
const balanceFormatted = fromNano(balance);
|
|
448
|
+
return {
|
|
449
|
+
balance: balanceFormatted,
|
|
450
|
+
balanceNano: balance.toString()
|
|
451
|
+
};
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error("Failed to get balance:", error);
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
var TON_PRICE_CACHE_TTL_MS = 3e4;
|
|
458
|
+
var _tonPriceCache = null;
|
|
459
|
+
async function getTonPrice() {
|
|
460
|
+
if (_tonPriceCache && Date.now() - _tonPriceCache.timestamp < TON_PRICE_CACHE_TTL_MS) {
|
|
461
|
+
return { ..._tonPriceCache };
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const response = await tonapiFetch(`/rates?tokens=ton¤cies=usd`);
|
|
465
|
+
if (response.ok) {
|
|
466
|
+
const data = await response.json();
|
|
467
|
+
const price = data?.rates?.TON?.prices?.USD;
|
|
468
|
+
if (typeof price === "number" && price > 0) {
|
|
469
|
+
_tonPriceCache = { usd: price, source: "TonAPI", timestamp: Date.now() };
|
|
470
|
+
return _tonPriceCache;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
const response = await fetchWithTimeout(
|
|
477
|
+
`${COINGECKO_API_URL}/simple/price?ids=the-open-network&vs_currencies=usd`
|
|
478
|
+
);
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
throw new Error(`CoinGecko API error: ${response.status}`);
|
|
481
|
+
}
|
|
482
|
+
const data = await response.json();
|
|
483
|
+
const price = data["the-open-network"]?.usd;
|
|
484
|
+
if (typeof price === "number" && price > 0) {
|
|
485
|
+
_tonPriceCache = { usd: price, source: "CoinGecko", timestamp: Date.now() };
|
|
486
|
+
return _tonPriceCache;
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error("Failed to get TON price:", error);
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/agent/tools/plugin-loader.ts
|
|
495
|
+
import { readdirSync, readFileSync as readFileSync4, existsSync as existsSync4, statSync } from "fs";
|
|
496
|
+
import { join as join4 } from "path";
|
|
497
|
+
import { pathToFileURL } from "url";
|
|
498
|
+
import { execFile } from "child_process";
|
|
499
|
+
import { promisify } from "util";
|
|
500
|
+
|
|
501
|
+
// src/agent/tools/plugin-validator.ts
|
|
502
|
+
import { z as z2 } from "zod";
|
|
503
|
+
var ManifestSchema = z2.object({
|
|
504
|
+
name: z2.string().min(1).max(64).regex(
|
|
505
|
+
/^[a-z0-9][a-z0-9-]*$/,
|
|
506
|
+
"Must be lowercase alphanumeric with hyphens, starting with a letter or number"
|
|
507
|
+
),
|
|
508
|
+
version: z2.string().regex(/^\d+\.\d+\.\d+$/, "Must be semver (e.g., 1.0.0)"),
|
|
509
|
+
author: z2.string().max(128).optional(),
|
|
510
|
+
description: z2.string().max(256).optional(),
|
|
511
|
+
dependencies: z2.array(z2.string()).optional(),
|
|
512
|
+
defaultConfig: z2.record(z2.string(), z2.unknown()).optional(),
|
|
513
|
+
sdkVersion: z2.string().max(32).optional(),
|
|
514
|
+
secrets: z2.record(
|
|
515
|
+
z2.string(),
|
|
516
|
+
z2.object({
|
|
517
|
+
required: z2.boolean(),
|
|
518
|
+
description: z2.string().max(256),
|
|
519
|
+
env: z2.string().max(128).optional()
|
|
520
|
+
})
|
|
521
|
+
).optional()
|
|
522
|
+
});
|
|
523
|
+
function validateManifest(raw) {
|
|
524
|
+
return ManifestSchema.parse(raw);
|
|
525
|
+
}
|
|
526
|
+
function validateToolDefs(defs, pluginName) {
|
|
527
|
+
const valid = [];
|
|
528
|
+
for (const def of defs) {
|
|
529
|
+
if (!def || typeof def !== "object") {
|
|
530
|
+
console.warn(`\u26A0\uFE0F [${pluginName}] tool is not an object, skipping`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const t = def;
|
|
534
|
+
if (!t.name || typeof t.name !== "string") {
|
|
535
|
+
console.warn(`\u26A0\uFE0F [${pluginName}] tool missing 'name', skipping`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (!t.description || typeof t.description !== "string") {
|
|
539
|
+
console.warn(`\u26A0\uFE0F [${pluginName}] tool "${t.name}" missing 'description', skipping`);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (!t.execute || typeof t.execute !== "function") {
|
|
543
|
+
console.warn(`\u26A0\uFE0F [${pluginName}] tool "${t.name}" missing 'execute' function, skipping`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
valid.push(t);
|
|
547
|
+
}
|
|
548
|
+
return valid;
|
|
549
|
+
}
|
|
550
|
+
function sanitizeConfigForPlugins(config) {
|
|
551
|
+
return {
|
|
552
|
+
agent: {
|
|
553
|
+
provider: config.agent.provider,
|
|
554
|
+
model: config.agent.model,
|
|
555
|
+
max_tokens: config.agent.max_tokens
|
|
556
|
+
},
|
|
557
|
+
telegram: {
|
|
558
|
+
admin_ids: config.telegram.admin_ids
|
|
559
|
+
},
|
|
560
|
+
deals: { enabled: config.deals.enabled }
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// packages/sdk/dist/index.js
|
|
565
|
+
var PluginSDKError = class extends Error {
|
|
566
|
+
constructor(message, code) {
|
|
567
|
+
super(message);
|
|
568
|
+
this.code = code;
|
|
569
|
+
}
|
|
570
|
+
name = "PluginSDKError";
|
|
571
|
+
};
|
|
572
|
+
var SDK_VERSION = "1.0.0";
|
|
573
|
+
|
|
574
|
+
// src/ton/transfer.ts
|
|
575
|
+
import { WalletContractV5R1 as WalletContractV5R12, TonClient as TonClient2, toNano, internal } from "@ton/ton";
|
|
576
|
+
import { Address, SendMode } from "@ton/core";
|
|
577
|
+
async function sendTon(params) {
|
|
578
|
+
try {
|
|
579
|
+
const { toAddress, amount, comment = "", bounce = false } = params;
|
|
580
|
+
let recipientAddress;
|
|
581
|
+
try {
|
|
582
|
+
recipientAddress = Address.parse(toAddress);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
console.error(`Invalid recipient address: ${toAddress}`, e);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const keyPair = await getKeyPair();
|
|
588
|
+
if (!keyPair) {
|
|
589
|
+
console.error("Wallet not initialized");
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const wallet = WalletContractV5R12.create({
|
|
593
|
+
workchain: 0,
|
|
594
|
+
publicKey: keyPair.publicKey
|
|
595
|
+
});
|
|
596
|
+
const endpoint = await getCachedHttpEndpoint();
|
|
597
|
+
const client = new TonClient2({ endpoint });
|
|
598
|
+
const contract = client.open(wallet);
|
|
599
|
+
const seqno = await contract.getSeqno();
|
|
600
|
+
await contract.sendTransfer({
|
|
601
|
+
seqno,
|
|
602
|
+
secretKey: keyPair.secretKey,
|
|
603
|
+
sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS,
|
|
604
|
+
messages: [
|
|
605
|
+
internal({
|
|
606
|
+
to: recipientAddress,
|
|
607
|
+
value: toNano(amount),
|
|
608
|
+
body: comment,
|
|
609
|
+
bounce
|
|
610
|
+
})
|
|
611
|
+
]
|
|
612
|
+
});
|
|
613
|
+
const pseudoHash = `${seqno}_${Date.now()}_${amount.toFixed(2)}`;
|
|
614
|
+
console.log(`\u{1F4B8} [TON] Sent ${amount} TON to ${toAddress.slice(0, 8)}... - seqno: ${seqno}`);
|
|
615
|
+
return pseudoHash;
|
|
616
|
+
} catch (error) {
|
|
617
|
+
console.error("Error sending TON:", error);
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/utils/retry.ts
|
|
623
|
+
var DEFAULT_OPTIONS = {
|
|
624
|
+
maxAttempts: RETRY_DEFAULT_MAX_ATTEMPTS,
|
|
625
|
+
baseDelayMs: RETRY_DEFAULT_BASE_DELAY_MS,
|
|
626
|
+
maxDelayMs: RETRY_DEFAULT_MAX_DELAY_MS,
|
|
627
|
+
timeout: RETRY_DEFAULT_TIMEOUT_MS
|
|
628
|
+
};
|
|
629
|
+
function sleep(ms) {
|
|
630
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
631
|
+
}
|
|
632
|
+
async function withRetry(fn, options = {}) {
|
|
633
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
634
|
+
let lastError;
|
|
635
|
+
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
636
|
+
try {
|
|
637
|
+
const result = await Promise.race([
|
|
638
|
+
fn(),
|
|
639
|
+
new Promise(
|
|
640
|
+
(_, reject) => setTimeout(() => reject(new Error("Operation timeout")), opts.timeout)
|
|
641
|
+
)
|
|
642
|
+
]);
|
|
643
|
+
return result;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
646
|
+
console.warn(`Retry attempt ${attempt}/${opts.maxAttempts} failed:`, lastError.message);
|
|
647
|
+
if (attempt < opts.maxAttempts) {
|
|
648
|
+
const delay = Math.min(opts.baseDelayMs * Math.pow(2, attempt - 1), opts.maxDelayMs);
|
|
649
|
+
await sleep(delay);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
throw lastError || new Error("All retry attempts failed");
|
|
654
|
+
}
|
|
655
|
+
async function withBlockchainRetry(fn, operation = "blockchain operation") {
|
|
656
|
+
try {
|
|
657
|
+
return await withRetry(fn, {
|
|
658
|
+
maxAttempts: RETRY_DEFAULT_MAX_ATTEMPTS,
|
|
659
|
+
baseDelayMs: RETRY_BLOCKCHAIN_BASE_DELAY_MS,
|
|
660
|
+
maxDelayMs: RETRY_BLOCKCHAIN_MAX_DELAY_MS,
|
|
661
|
+
timeout: RETRY_BLOCKCHAIN_TIMEOUT_MS
|
|
662
|
+
});
|
|
663
|
+
} catch (error) {
|
|
664
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
665
|
+
throw new Error(`${operation} failed after retries: ${message}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/sdk/ton.ts
|
|
670
|
+
var DEFAULT_MAX_AGE_MINUTES = 10;
|
|
671
|
+
var DEFAULT_TX_RETENTION_DAYS = 30;
|
|
672
|
+
var CLEANUP_PROBABILITY = 0.1;
|
|
673
|
+
function cleanupOldTransactions(db, retentionDays, log) {
|
|
674
|
+
if (Math.random() > CLEANUP_PROBABILITY) return;
|
|
675
|
+
try {
|
|
676
|
+
const cutoff = Math.floor(Date.now() / 1e3) - retentionDays * 24 * 60 * 60;
|
|
677
|
+
const result = db.prepare("DELETE FROM used_transactions WHERE used_at < ?").run(cutoff);
|
|
678
|
+
if (result.changes > 0) {
|
|
679
|
+
log.debug(`Cleaned up ${result.changes} old transaction records (>${retentionDays}d)`);
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
log.error("Transaction cleanup failed:", err);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function createTonSDK(log, db) {
|
|
686
|
+
return {
|
|
687
|
+
getAddress() {
|
|
688
|
+
try {
|
|
689
|
+
return getWalletAddress();
|
|
690
|
+
} catch (err) {
|
|
691
|
+
log.error("ton.getAddress() failed:", err);
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
async getBalance(address) {
|
|
696
|
+
try {
|
|
697
|
+
const addr = address ?? getWalletAddress();
|
|
698
|
+
if (!addr) return null;
|
|
699
|
+
return await getWalletBalance(addr);
|
|
700
|
+
} catch (err) {
|
|
701
|
+
log.error("ton.getBalance() failed:", err);
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
async getPrice() {
|
|
706
|
+
try {
|
|
707
|
+
return await getTonPrice();
|
|
708
|
+
} catch (err) {
|
|
709
|
+
log.error("ton.getPrice() failed:", err);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
async sendTON(to, amount, comment) {
|
|
714
|
+
const walletAddr = getWalletAddress();
|
|
715
|
+
if (!walletAddr) {
|
|
716
|
+
throw new PluginSDKError("Wallet not initialized", "WALLET_NOT_INITIALIZED");
|
|
717
|
+
}
|
|
718
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
719
|
+
throw new PluginSDKError("Amount must be a positive number", "OPERATION_FAILED");
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
const { Address: Address2 } = await import("@ton/core");
|
|
723
|
+
Address2.parse(to);
|
|
724
|
+
} catch {
|
|
725
|
+
throw new PluginSDKError("Invalid TON address format", "INVALID_ADDRESS");
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
const txRef = await sendTon({
|
|
729
|
+
toAddress: to,
|
|
730
|
+
amount,
|
|
731
|
+
comment,
|
|
732
|
+
bounce: false
|
|
733
|
+
});
|
|
734
|
+
if (!txRef) {
|
|
735
|
+
throw new PluginSDKError(
|
|
736
|
+
"Transaction failed \u2014 no reference returned",
|
|
737
|
+
"OPERATION_FAILED"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
return { txRef, amount };
|
|
741
|
+
} catch (err) {
|
|
742
|
+
if (err instanceof PluginSDKError) throw err;
|
|
743
|
+
throw new PluginSDKError(
|
|
744
|
+
`Failed to send TON: ${err instanceof Error ? err.message : String(err)}`,
|
|
745
|
+
"OPERATION_FAILED"
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
async getTransactions(address, limit) {
|
|
750
|
+
try {
|
|
751
|
+
const { TonClient: TonClient3 } = await import("@ton/ton");
|
|
752
|
+
const { Address: Address2 } = await import("@ton/core");
|
|
753
|
+
const { getCachedHttpEndpoint: getCachedHttpEndpoint2 } = await import("./endpoint-FLYNEZ2F.js");
|
|
754
|
+
const { formatTransactions } = await import("./format-transactions-FD74HI5N.js");
|
|
755
|
+
const addressObj = Address2.parse(address);
|
|
756
|
+
const endpoint = await getCachedHttpEndpoint2();
|
|
757
|
+
const client = new TonClient3({ endpoint });
|
|
758
|
+
const transactions = await withBlockchainRetry(
|
|
759
|
+
() => client.getTransactions(addressObj, {
|
|
760
|
+
limit: Math.min(limit ?? 10, 50)
|
|
761
|
+
}),
|
|
762
|
+
"sdk.ton.getTransactions"
|
|
763
|
+
);
|
|
764
|
+
return formatTransactions(transactions);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
log.error("ton.getTransactions() failed:", err);
|
|
767
|
+
return [];
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
async verifyPayment(params) {
|
|
771
|
+
if (!db) {
|
|
772
|
+
throw new PluginSDKError(
|
|
773
|
+
"No database available \u2014 verifyPayment requires migrate() with used_transactions table",
|
|
774
|
+
"OPERATION_FAILED"
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const address = getWalletAddress();
|
|
778
|
+
if (!address) {
|
|
779
|
+
throw new PluginSDKError("Wallet not initialized", "WALLET_NOT_INITIALIZED");
|
|
780
|
+
}
|
|
781
|
+
const maxAgeMinutes = params.maxAgeMinutes ?? DEFAULT_MAX_AGE_MINUTES;
|
|
782
|
+
cleanupOldTransactions(db, DEFAULT_TX_RETENTION_DAYS, log);
|
|
783
|
+
try {
|
|
784
|
+
const txs = await this.getTransactions(address, 20);
|
|
785
|
+
for (const tx of txs) {
|
|
786
|
+
if (tx.type !== "ton_received") continue;
|
|
787
|
+
if (!tx.amount || !tx.from) continue;
|
|
788
|
+
const tonAmount = parseFloat(tx.amount.replace(/ TON$/, ""));
|
|
789
|
+
if (isNaN(tonAmount)) continue;
|
|
790
|
+
if (tonAmount < params.amount * PAYMENT_TOLERANCE_RATIO) continue;
|
|
791
|
+
if (tx.secondsAgo > maxAgeMinutes * 60) continue;
|
|
792
|
+
const memo = (tx.comment ?? "").trim().toLowerCase().replace(/^@/, "");
|
|
793
|
+
const expected = params.memo.toLowerCase().replace(/^@/, "");
|
|
794
|
+
if (memo !== expected) continue;
|
|
795
|
+
const txHash = tx.hash;
|
|
796
|
+
const result = db.prepare(
|
|
797
|
+
`INSERT OR IGNORE INTO used_transactions (tx_hash, user_id, amount, game_type, used_at)
|
|
798
|
+
VALUES (?, ?, ?, ?, unixepoch())`
|
|
799
|
+
).run(txHash, params.memo, tonAmount, params.gameType);
|
|
800
|
+
if (result.changes === 0) continue;
|
|
801
|
+
return {
|
|
802
|
+
verified: true,
|
|
803
|
+
txHash,
|
|
804
|
+
amount: tonAmount,
|
|
805
|
+
playerWallet: tx.from,
|
|
806
|
+
date: tx.date,
|
|
807
|
+
secondsAgo: tx.secondsAgo
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
verified: false,
|
|
812
|
+
error: `Payment not found. Send ${params.amount} TON to ${address} with memo: ${params.memo}`
|
|
813
|
+
};
|
|
814
|
+
} catch (err) {
|
|
815
|
+
if (err instanceof PluginSDKError) throw err;
|
|
816
|
+
log.error("ton.verifyPayment() failed:", err);
|
|
817
|
+
return {
|
|
818
|
+
verified: false,
|
|
819
|
+
error: `Verification failed: ${err instanceof Error ? err.message : String(err)}`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
// ─── Jettons ─────────────────────────────────────────────────
|
|
824
|
+
async getJettonBalances(ownerAddress) {
|
|
825
|
+
try {
|
|
826
|
+
const addr = ownerAddress ?? getWalletAddress();
|
|
827
|
+
if (!addr) return [];
|
|
828
|
+
const response = await tonapiFetch(`/accounts/${addr}/jettons`);
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
log.error(`ton.getJettonBalances() TonAPI error: ${response.status}`);
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
const data = await response.json();
|
|
834
|
+
const balances = [];
|
|
835
|
+
for (const item of data.balances || []) {
|
|
836
|
+
const { balance, wallet_address, jetton } = item;
|
|
837
|
+
if (jetton.verification === "blacklist") continue;
|
|
838
|
+
const decimals = jetton.decimals || 9;
|
|
839
|
+
const rawBalance = BigInt(balance);
|
|
840
|
+
const divisor = BigInt(10 ** decimals);
|
|
841
|
+
const wholePart = rawBalance / divisor;
|
|
842
|
+
const fractionalPart = rawBalance % divisor;
|
|
843
|
+
const balanceFormatted = fractionalPart === BigInt(0) ? wholePart.toString() : `${wholePart}.${fractionalPart.toString().padStart(decimals, "0").replace(/0+$/, "")}`;
|
|
844
|
+
balances.push({
|
|
845
|
+
jettonAddress: jetton.address,
|
|
846
|
+
walletAddress: wallet_address.address,
|
|
847
|
+
balance,
|
|
848
|
+
balanceFormatted,
|
|
849
|
+
symbol: jetton.symbol || "UNKNOWN",
|
|
850
|
+
name: jetton.name || "Unknown Token",
|
|
851
|
+
decimals,
|
|
852
|
+
verified: jetton.verification === "whitelist",
|
|
853
|
+
usdPrice: item.price?.prices?.USD ? Number(item.price.prices.USD) : void 0
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
return balances;
|
|
857
|
+
} catch (err) {
|
|
858
|
+
log.error("ton.getJettonBalances() failed:", err);
|
|
859
|
+
return [];
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
async getJettonInfo(jettonAddress) {
|
|
863
|
+
try {
|
|
864
|
+
const response = await tonapiFetch(`/jettons/${jettonAddress}`);
|
|
865
|
+
if (response.status === 404) return null;
|
|
866
|
+
if (!response.ok) {
|
|
867
|
+
log.error(`ton.getJettonInfo() TonAPI error: ${response.status}`);
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
const data = await response.json();
|
|
871
|
+
const metadata = data.metadata || {};
|
|
872
|
+
const decimals = parseInt(metadata.decimals || "9");
|
|
873
|
+
return {
|
|
874
|
+
address: metadata.address || jettonAddress,
|
|
875
|
+
name: metadata.name || "Unknown",
|
|
876
|
+
symbol: metadata.symbol || "UNKNOWN",
|
|
877
|
+
decimals,
|
|
878
|
+
totalSupply: data.total_supply || "0",
|
|
879
|
+
holdersCount: data.holders_count || 0,
|
|
880
|
+
verified: data.verification === "whitelist",
|
|
881
|
+
description: metadata.description || void 0,
|
|
882
|
+
image: data.preview || metadata.image || void 0
|
|
883
|
+
};
|
|
884
|
+
} catch (err) {
|
|
885
|
+
log.error("ton.getJettonInfo() failed:", err);
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
async sendJetton(jettonAddress, to, amount, opts) {
|
|
890
|
+
const { Address: Address2, beginCell, SendMode: SendMode2 } = await import("@ton/core");
|
|
891
|
+
const { WalletContractV5R1: WalletContractV5R13, TonClient: TonClient3, toNano: toNano2, internal: internal2 } = await import("@ton/ton");
|
|
892
|
+
const { getCachedHttpEndpoint: getCachedHttpEndpoint2 } = await import("./endpoint-FLYNEZ2F.js");
|
|
893
|
+
const walletData = loadWallet();
|
|
894
|
+
if (!walletData) {
|
|
895
|
+
throw new PluginSDKError("Wallet not initialized", "WALLET_NOT_INITIALIZED");
|
|
896
|
+
}
|
|
897
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
898
|
+
throw new PluginSDKError("Amount must be a positive number", "OPERATION_FAILED");
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
Address2.parse(to);
|
|
902
|
+
} catch {
|
|
903
|
+
throw new PluginSDKError("Invalid recipient address", "INVALID_ADDRESS");
|
|
904
|
+
}
|
|
905
|
+
const jettonsResponse = await tonapiFetch(`/accounts/${walletData.address}/jettons`);
|
|
906
|
+
if (!jettonsResponse.ok) {
|
|
907
|
+
throw new PluginSDKError(
|
|
908
|
+
`Failed to fetch jetton balances: ${jettonsResponse.status}`,
|
|
909
|
+
"OPERATION_FAILED"
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
const jettonsData = await jettonsResponse.json();
|
|
913
|
+
const jettonBalance = jettonsData.balances?.find(
|
|
914
|
+
(b) => b.jetton.address.toLowerCase() === jettonAddress.toLowerCase() || Address2.parse(b.jetton.address).toString() === Address2.parse(jettonAddress).toString()
|
|
915
|
+
);
|
|
916
|
+
if (!jettonBalance) {
|
|
917
|
+
throw new PluginSDKError(
|
|
918
|
+
`You don't own any of this jetton: ${jettonAddress}`,
|
|
919
|
+
"OPERATION_FAILED"
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
const senderJettonWallet = jettonBalance.wallet_address.address;
|
|
923
|
+
const decimals = jettonBalance.jetton.decimals || 9;
|
|
924
|
+
const currentBalance = BigInt(jettonBalance.balance);
|
|
925
|
+
const amountInUnits = BigInt(Math.floor(amount * 10 ** decimals));
|
|
926
|
+
if (amountInUnits > currentBalance) {
|
|
927
|
+
throw new PluginSDKError(
|
|
928
|
+
`Insufficient balance. Have ${Number(currentBalance) / 10 ** decimals}, need ${amount}`,
|
|
929
|
+
"OPERATION_FAILED"
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
const comment = opts?.comment;
|
|
933
|
+
let forwardPayload = beginCell().endCell();
|
|
934
|
+
if (comment) {
|
|
935
|
+
forwardPayload = beginCell().storeUint(0, 32).storeStringTail(comment).endCell();
|
|
936
|
+
}
|
|
937
|
+
const JETTON_TRANSFER_OP = 260734629;
|
|
938
|
+
const messageBody = beginCell().storeUint(JETTON_TRANSFER_OP, 32).storeUint(0, 64).storeCoins(amountInUnits).storeAddress(Address2.parse(to)).storeAddress(Address2.parse(walletData.address)).storeBit(false).storeCoins(comment ? toNano2("0.01") : BigInt(1)).storeBit(comment ? true : false).storeMaybeRef(comment ? forwardPayload : null).endCell();
|
|
939
|
+
const keyPair = await getKeyPair();
|
|
940
|
+
if (!keyPair) {
|
|
941
|
+
throw new PluginSDKError("Wallet key derivation failed", "OPERATION_FAILED");
|
|
942
|
+
}
|
|
943
|
+
const wallet = WalletContractV5R13.create({
|
|
944
|
+
workchain: 0,
|
|
945
|
+
publicKey: keyPair.publicKey
|
|
946
|
+
});
|
|
947
|
+
const endpoint = await getCachedHttpEndpoint2();
|
|
948
|
+
const client = new TonClient3({ endpoint });
|
|
949
|
+
const walletContract = client.open(wallet);
|
|
950
|
+
const seqno = await walletContract.getSeqno();
|
|
951
|
+
await walletContract.sendTransfer({
|
|
952
|
+
seqno,
|
|
953
|
+
secretKey: keyPair.secretKey,
|
|
954
|
+
sendMode: SendMode2.PAY_GAS_SEPARATELY + SendMode2.IGNORE_ERRORS,
|
|
955
|
+
messages: [
|
|
956
|
+
internal2({
|
|
957
|
+
to: Address2.parse(senderJettonWallet),
|
|
958
|
+
value: toNano2("0.05"),
|
|
959
|
+
body: messageBody,
|
|
960
|
+
bounce: true
|
|
961
|
+
})
|
|
962
|
+
]
|
|
963
|
+
});
|
|
964
|
+
return { success: true, seqno };
|
|
965
|
+
},
|
|
966
|
+
async getJettonWalletAddress(ownerAddress, jettonAddress) {
|
|
967
|
+
try {
|
|
968
|
+
const response = await tonapiFetch(`/accounts/${ownerAddress}/jettons`);
|
|
969
|
+
if (!response.ok) {
|
|
970
|
+
log.error(`ton.getJettonWalletAddress() TonAPI error: ${response.status}`);
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
const { Address: Address2 } = await import("@ton/core");
|
|
974
|
+
const data = await response.json();
|
|
975
|
+
const match = (data.balances || []).find(
|
|
976
|
+
(b) => b.jetton.address.toLowerCase() === jettonAddress.toLowerCase() || Address2.parse(b.jetton.address).toString() === Address2.parse(jettonAddress).toString()
|
|
977
|
+
);
|
|
978
|
+
return match ? match.wallet_address.address : null;
|
|
979
|
+
} catch (err) {
|
|
980
|
+
log.error("ton.getJettonWalletAddress() failed:", err);
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
// ─── NFT ─────────────────────────────────────────────────────
|
|
985
|
+
async getNftItems(ownerAddress) {
|
|
986
|
+
try {
|
|
987
|
+
const addr = ownerAddress ?? getWalletAddress();
|
|
988
|
+
if (!addr) return [];
|
|
989
|
+
const response = await tonapiFetch(
|
|
990
|
+
`/accounts/${encodeURIComponent(addr)}/nfts?limit=100&indirect_ownership=true`
|
|
991
|
+
);
|
|
992
|
+
if (!response.ok) {
|
|
993
|
+
log.error(`ton.getNftItems() TonAPI error: ${response.status}`);
|
|
994
|
+
return [];
|
|
995
|
+
}
|
|
996
|
+
const data = await response.json();
|
|
997
|
+
if (!Array.isArray(data.nft_items)) return [];
|
|
998
|
+
return data.nft_items.filter((item) => item.trust !== "blacklist").map((item) => mapNftItem(item));
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
log.error("ton.getNftItems() failed:", err);
|
|
1001
|
+
return [];
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
async getNftInfo(nftAddress) {
|
|
1005
|
+
try {
|
|
1006
|
+
const response = await tonapiFetch(`/nfts/${nftAddress}`);
|
|
1007
|
+
if (response.status === 404) return null;
|
|
1008
|
+
if (!response.ok) {
|
|
1009
|
+
log.error(`ton.getNftInfo() TonAPI error: ${response.status}`);
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
const item = await response.json();
|
|
1013
|
+
return mapNftItem(item);
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
log.error("ton.getNftInfo() failed:", err);
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
// ─── Utilities ───────────────────────────────────────────────
|
|
1020
|
+
toNano(amount) {
|
|
1021
|
+
try {
|
|
1022
|
+
const { toNano: convert } = __require("@ton/ton");
|
|
1023
|
+
return convert(String(amount));
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
throw new PluginSDKError(
|
|
1026
|
+
`toNano conversion failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1027
|
+
"OPERATION_FAILED"
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
fromNano(nano) {
|
|
1032
|
+
const { fromNano: convert } = __require("@ton/ton");
|
|
1033
|
+
return convert(nano);
|
|
1034
|
+
},
|
|
1035
|
+
validateAddress(address) {
|
|
1036
|
+
try {
|
|
1037
|
+
const { Address: Address2 } = __require("@ton/core");
|
|
1038
|
+
Address2.parse(address);
|
|
1039
|
+
return true;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function mapNftItem(item) {
|
|
1047
|
+
const meta = item.metadata || {};
|
|
1048
|
+
const coll = item.collection || {};
|
|
1049
|
+
const previews = item.previews || [];
|
|
1050
|
+
const preview = previews.length > 1 && previews[1].url || previews.length > 0 && previews[0].url || void 0;
|
|
1051
|
+
return {
|
|
1052
|
+
address: item.address,
|
|
1053
|
+
index: item.index ?? 0,
|
|
1054
|
+
ownerAddress: item.owner?.address || void 0,
|
|
1055
|
+
collectionAddress: coll.address || void 0,
|
|
1056
|
+
collectionName: coll.name || void 0,
|
|
1057
|
+
name: meta.name || void 0,
|
|
1058
|
+
description: meta.description ? meta.description.slice(0, 200) : void 0,
|
|
1059
|
+
image: preview || meta.image || void 0,
|
|
1060
|
+
verified: item.trust === "whitelist"
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/sdk/telegram.ts
|
|
1065
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
1066
|
+
|
|
1067
|
+
// src/sdk/telegram-messages.ts
|
|
1068
|
+
import { randomBytes } from "crypto";
|
|
1069
|
+
function createTelegramMessagesSDK(bridge, log) {
|
|
1070
|
+
function requireBridge() {
|
|
1071
|
+
if (!bridge.isAvailable()) {
|
|
1072
|
+
throw new PluginSDKError(
|
|
1073
|
+
"Telegram bridge not connected. SDK telegram methods can only be called at runtime (inside tool executors or start()), not during plugin loading.",
|
|
1074
|
+
"BRIDGE_NOT_CONNECTED"
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function getClient() {
|
|
1079
|
+
return bridge.getClient().getClient();
|
|
1080
|
+
}
|
|
1081
|
+
function toSimpleMessage(msg) {
|
|
1082
|
+
return {
|
|
1083
|
+
id: msg.id,
|
|
1084
|
+
text: msg.message ?? "",
|
|
1085
|
+
senderId: Number(msg.fromId?.userId ?? msg.fromId?.channelId ?? 0),
|
|
1086
|
+
timestamp: new Date(msg.date * 1e3)
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
return {
|
|
1090
|
+
// ─── Messages ──────────────────────────────────────────────
|
|
1091
|
+
async deleteMessage(chatId, messageId, revoke = true) {
|
|
1092
|
+
requireBridge();
|
|
1093
|
+
try {
|
|
1094
|
+
const gramJsClient = getClient();
|
|
1095
|
+
const { Api } = await import("telegram");
|
|
1096
|
+
const isChannel = chatId.startsWith("-100");
|
|
1097
|
+
if (isChannel) {
|
|
1098
|
+
const channel = await gramJsClient.getEntity(chatId);
|
|
1099
|
+
await gramJsClient.invoke(
|
|
1100
|
+
new Api.channels.DeleteMessages({
|
|
1101
|
+
channel,
|
|
1102
|
+
id: [messageId]
|
|
1103
|
+
})
|
|
1104
|
+
);
|
|
1105
|
+
} else {
|
|
1106
|
+
await gramJsClient.invoke(
|
|
1107
|
+
new Api.messages.DeleteMessages({
|
|
1108
|
+
id: [messageId],
|
|
1109
|
+
revoke
|
|
1110
|
+
})
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1115
|
+
throw new PluginSDKError(
|
|
1116
|
+
`Failed to delete message: ${err instanceof Error ? err.message : String(err)}`,
|
|
1117
|
+
"OPERATION_FAILED"
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
async forwardMessage(fromChatId, toChatId, messageId) {
|
|
1122
|
+
requireBridge();
|
|
1123
|
+
try {
|
|
1124
|
+
const gramJsClient = getClient();
|
|
1125
|
+
const { Api } = await import("telegram");
|
|
1126
|
+
const result = await gramJsClient.invoke(
|
|
1127
|
+
new Api.messages.ForwardMessages({
|
|
1128
|
+
fromPeer: fromChatId,
|
|
1129
|
+
toPeer: toChatId,
|
|
1130
|
+
id: [messageId],
|
|
1131
|
+
randomId: [randomBytes(8).readBigUInt64BE()]
|
|
1132
|
+
})
|
|
1133
|
+
);
|
|
1134
|
+
const updates = result;
|
|
1135
|
+
if (updates.updates) {
|
|
1136
|
+
for (const update of updates.updates) {
|
|
1137
|
+
if (update.className === "UpdateNewMessage" || update.className === "UpdateNewChannelMessage") {
|
|
1138
|
+
return update.message.id;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return 0;
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1145
|
+
throw new PluginSDKError(
|
|
1146
|
+
`Failed to forward message: ${err instanceof Error ? err.message : String(err)}`,
|
|
1147
|
+
"OPERATION_FAILED"
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
async pinMessage(chatId, messageId, opts) {
|
|
1152
|
+
requireBridge();
|
|
1153
|
+
try {
|
|
1154
|
+
const gramJsClient = getClient();
|
|
1155
|
+
const { Api } = await import("telegram");
|
|
1156
|
+
await gramJsClient.invoke(
|
|
1157
|
+
new Api.messages.UpdatePinnedMessage({
|
|
1158
|
+
peer: chatId,
|
|
1159
|
+
id: messageId,
|
|
1160
|
+
silent: opts?.silent,
|
|
1161
|
+
unpin: opts?.unpin
|
|
1162
|
+
})
|
|
1163
|
+
);
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1166
|
+
throw new PluginSDKError(
|
|
1167
|
+
`Failed to ${opts?.unpin ? "unpin" : "pin"} message: ${err instanceof Error ? err.message : String(err)}`,
|
|
1168
|
+
"OPERATION_FAILED"
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
async searchMessages(chatId, query, limit = 20) {
|
|
1173
|
+
requireBridge();
|
|
1174
|
+
try {
|
|
1175
|
+
const gramJsClient = getClient();
|
|
1176
|
+
const { Api } = await import("telegram");
|
|
1177
|
+
const entity = await gramJsClient.getEntity(chatId);
|
|
1178
|
+
const result = await gramJsClient.invoke(
|
|
1179
|
+
new Api.messages.Search({
|
|
1180
|
+
peer: entity,
|
|
1181
|
+
q: query,
|
|
1182
|
+
filter: new Api.InputMessagesFilterEmpty(),
|
|
1183
|
+
limit
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
const resultData = result;
|
|
1187
|
+
return (resultData.messages ?? []).map(toSimpleMessage);
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1190
|
+
log.error("telegram.searchMessages() failed:", err);
|
|
1191
|
+
return [];
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
async scheduleMessage(chatId, text, scheduleDate) {
|
|
1195
|
+
requireBridge();
|
|
1196
|
+
try {
|
|
1197
|
+
const gramJsClient = getClient();
|
|
1198
|
+
const result = await gramJsClient.sendMessage(chatId, {
|
|
1199
|
+
message: text,
|
|
1200
|
+
schedule: scheduleDate
|
|
1201
|
+
});
|
|
1202
|
+
return result.id ?? 0;
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1205
|
+
throw new PluginSDKError(
|
|
1206
|
+
`Failed to schedule message: ${err instanceof Error ? err.message : String(err)}`,
|
|
1207
|
+
"OPERATION_FAILED"
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
async getReplies(chatId, messageId, limit = 50) {
|
|
1212
|
+
requireBridge();
|
|
1213
|
+
try {
|
|
1214
|
+
const gramJsClient = getClient();
|
|
1215
|
+
const { Api } = await import("telegram");
|
|
1216
|
+
const bigInt = (await import("./BigInteger-DQ33LTTE.js")).default;
|
|
1217
|
+
const peer = await gramJsClient.getInputEntity(chatId);
|
|
1218
|
+
const result = await gramJsClient.invoke(
|
|
1219
|
+
new Api.messages.GetReplies({
|
|
1220
|
+
peer,
|
|
1221
|
+
msgId: messageId,
|
|
1222
|
+
offsetId: 0,
|
|
1223
|
+
offsetDate: 0,
|
|
1224
|
+
addOffset: 0,
|
|
1225
|
+
limit,
|
|
1226
|
+
maxId: 0,
|
|
1227
|
+
minId: 0,
|
|
1228
|
+
hash: bigInt(0)
|
|
1229
|
+
})
|
|
1230
|
+
);
|
|
1231
|
+
const messages = [];
|
|
1232
|
+
if ("messages" in result) {
|
|
1233
|
+
for (const msg of result.messages) {
|
|
1234
|
+
if (msg.className === "Message") {
|
|
1235
|
+
messages.push(toSimpleMessage(msg));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
1240
|
+
return messages;
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1243
|
+
throw new PluginSDKError(
|
|
1244
|
+
`Failed to get replies: ${err instanceof Error ? err.message : String(err)}`,
|
|
1245
|
+
"OPERATION_FAILED"
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
// ─── Media ─────────────────────────────────────────────────
|
|
1250
|
+
async sendPhoto(chatId, photo, opts) {
|
|
1251
|
+
requireBridge();
|
|
1252
|
+
try {
|
|
1253
|
+
const gramJsClient = getClient();
|
|
1254
|
+
const result = await gramJsClient.sendFile(chatId, {
|
|
1255
|
+
file: photo,
|
|
1256
|
+
caption: opts?.caption,
|
|
1257
|
+
replyTo: opts?.replyToId
|
|
1258
|
+
});
|
|
1259
|
+
return result.id;
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1262
|
+
throw new PluginSDKError(
|
|
1263
|
+
`Failed to send photo: ${err instanceof Error ? err.message : String(err)}`,
|
|
1264
|
+
"OPERATION_FAILED"
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
async sendVideo(chatId, video, opts) {
|
|
1269
|
+
requireBridge();
|
|
1270
|
+
try {
|
|
1271
|
+
const gramJsClient = getClient();
|
|
1272
|
+
const { Api } = await import("telegram");
|
|
1273
|
+
const result = await gramJsClient.sendFile(chatId, {
|
|
1274
|
+
file: video,
|
|
1275
|
+
caption: opts?.caption,
|
|
1276
|
+
replyTo: opts?.replyToId,
|
|
1277
|
+
forceDocument: false,
|
|
1278
|
+
attributes: [
|
|
1279
|
+
new Api.DocumentAttributeVideo({
|
|
1280
|
+
roundMessage: false,
|
|
1281
|
+
supportsStreaming: true,
|
|
1282
|
+
duration: 0,
|
|
1283
|
+
w: 0,
|
|
1284
|
+
h: 0
|
|
1285
|
+
})
|
|
1286
|
+
]
|
|
1287
|
+
});
|
|
1288
|
+
return result.id;
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1291
|
+
throw new PluginSDKError(
|
|
1292
|
+
`Failed to send video: ${err instanceof Error ? err.message : String(err)}`,
|
|
1293
|
+
"OPERATION_FAILED"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
async sendVoice(chatId, voice, opts) {
|
|
1298
|
+
requireBridge();
|
|
1299
|
+
try {
|
|
1300
|
+
const gramJsClient = getClient();
|
|
1301
|
+
const { Api } = await import("telegram");
|
|
1302
|
+
const result = await gramJsClient.sendFile(chatId, {
|
|
1303
|
+
file: voice,
|
|
1304
|
+
caption: opts?.caption,
|
|
1305
|
+
replyTo: opts?.replyToId,
|
|
1306
|
+
attributes: [new Api.DocumentAttributeAudio({ voice: true, duration: 0 })]
|
|
1307
|
+
});
|
|
1308
|
+
return result.id;
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1311
|
+
throw new PluginSDKError(
|
|
1312
|
+
`Failed to send voice: ${err instanceof Error ? err.message : String(err)}`,
|
|
1313
|
+
"OPERATION_FAILED"
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
async sendFile(chatId, file, opts) {
|
|
1318
|
+
requireBridge();
|
|
1319
|
+
try {
|
|
1320
|
+
const gramJsClient = getClient();
|
|
1321
|
+
const { Api } = await import("telegram");
|
|
1322
|
+
const attributes = [];
|
|
1323
|
+
if (opts?.fileName) {
|
|
1324
|
+
attributes.push(new Api.DocumentAttributeFilename({ fileName: opts.fileName }));
|
|
1325
|
+
}
|
|
1326
|
+
const result = await gramJsClient.sendFile(chatId, {
|
|
1327
|
+
file,
|
|
1328
|
+
caption: opts?.caption,
|
|
1329
|
+
replyTo: opts?.replyToId,
|
|
1330
|
+
forceDocument: true,
|
|
1331
|
+
attributes: attributes.length > 0 ? attributes : void 0
|
|
1332
|
+
});
|
|
1333
|
+
return result.id;
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1336
|
+
throw new PluginSDKError(
|
|
1337
|
+
`Failed to send file: ${err instanceof Error ? err.message : String(err)}`,
|
|
1338
|
+
"OPERATION_FAILED"
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
},
|
|
1342
|
+
async sendGif(chatId, gif, opts) {
|
|
1343
|
+
requireBridge();
|
|
1344
|
+
try {
|
|
1345
|
+
const gramJsClient = getClient();
|
|
1346
|
+
const { Api } = await import("telegram");
|
|
1347
|
+
const result = await gramJsClient.sendFile(chatId, {
|
|
1348
|
+
file: gif,
|
|
1349
|
+
caption: opts?.caption,
|
|
1350
|
+
replyTo: opts?.replyToId,
|
|
1351
|
+
attributes: [new Api.DocumentAttributeAnimated()]
|
|
1352
|
+
});
|
|
1353
|
+
return result.id;
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1356
|
+
throw new PluginSDKError(
|
|
1357
|
+
`Failed to send GIF: ${err instanceof Error ? err.message : String(err)}`,
|
|
1358
|
+
"OPERATION_FAILED"
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
},
|
|
1362
|
+
async sendSticker(chatId, sticker) {
|
|
1363
|
+
requireBridge();
|
|
1364
|
+
try {
|
|
1365
|
+
const gramJsClient = getClient();
|
|
1366
|
+
const result = await gramJsClient.sendFile(chatId, {
|
|
1367
|
+
file: sticker
|
|
1368
|
+
});
|
|
1369
|
+
return result.id;
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1372
|
+
throw new PluginSDKError(
|
|
1373
|
+
`Failed to send sticker: ${err instanceof Error ? err.message : String(err)}`,
|
|
1374
|
+
"OPERATION_FAILED"
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
async downloadMedia(chatId, messageId) {
|
|
1379
|
+
requireBridge();
|
|
1380
|
+
try {
|
|
1381
|
+
const gramJsClient = getClient();
|
|
1382
|
+
const messages = await gramJsClient.getMessages(chatId, {
|
|
1383
|
+
ids: [messageId]
|
|
1384
|
+
});
|
|
1385
|
+
if (!messages || messages.length === 0 || !messages[0].media) {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
const buffer = await gramJsClient.downloadMedia(messages[0], {});
|
|
1389
|
+
return buffer ? Buffer.from(buffer) : null;
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1392
|
+
throw new PluginSDKError(
|
|
1393
|
+
`Failed to download media: ${err instanceof Error ? err.message : String(err)}`,
|
|
1394
|
+
"OPERATION_FAILED"
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
// ─── Advanced ──────────────────────────────────────────────
|
|
1399
|
+
async setTyping(chatId) {
|
|
1400
|
+
requireBridge();
|
|
1401
|
+
try {
|
|
1402
|
+
await bridge.setTyping(chatId);
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1405
|
+
throw new PluginSDKError(
|
|
1406
|
+
`Failed to set typing: ${err instanceof Error ? err.message : String(err)}`,
|
|
1407
|
+
"OPERATION_FAILED"
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/sdk/telegram-social.ts
|
|
1415
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1416
|
+
function createTelegramSocialSDK(bridge, log) {
|
|
1417
|
+
function requireBridge() {
|
|
1418
|
+
if (!bridge.isAvailable()) {
|
|
1419
|
+
throw new PluginSDKError(
|
|
1420
|
+
"Telegram bridge not connected. SDK telegram methods can only be called at runtime (inside tool executors or start()), not during plugin loading.",
|
|
1421
|
+
"BRIDGE_NOT_CONNECTED"
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function getClient() {
|
|
1426
|
+
return bridge.getClient().getClient();
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
// ─── Chat & Users ─────────────────────────────────────────
|
|
1430
|
+
async getChatInfo(chatId) {
|
|
1431
|
+
requireBridge();
|
|
1432
|
+
try {
|
|
1433
|
+
const client = getClient();
|
|
1434
|
+
const { Api } = await import("telegram");
|
|
1435
|
+
let entity;
|
|
1436
|
+
try {
|
|
1437
|
+
entity = await client.getEntity(chatId);
|
|
1438
|
+
} catch {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
const isChannel = entity.className === "Channel" || entity.className === "ChannelForbidden";
|
|
1442
|
+
const isChat = entity.className === "Chat" || entity.className === "ChatForbidden";
|
|
1443
|
+
const isUser = entity.className === "User";
|
|
1444
|
+
if (isUser) {
|
|
1445
|
+
const user = entity;
|
|
1446
|
+
return {
|
|
1447
|
+
id: user.id?.toString() || chatId,
|
|
1448
|
+
title: [user.firstName, user.lastName].filter(Boolean).join(" ") || "Unknown",
|
|
1449
|
+
type: "private",
|
|
1450
|
+
username: user.username || void 0
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
if (isChannel) {
|
|
1454
|
+
const channel = entity;
|
|
1455
|
+
let description;
|
|
1456
|
+
let membersCount;
|
|
1457
|
+
try {
|
|
1458
|
+
const fullChannel = await client.invoke(
|
|
1459
|
+
new Api.channels.GetFullChannel({ channel: entity })
|
|
1460
|
+
);
|
|
1461
|
+
const fullChat = fullChannel.fullChat;
|
|
1462
|
+
description = fullChat.about || void 0;
|
|
1463
|
+
membersCount = fullChat.participantsCount || void 0;
|
|
1464
|
+
} catch {
|
|
1465
|
+
}
|
|
1466
|
+
const type = channel.megagroup ? "supergroup" : channel.broadcast ? "channel" : "group";
|
|
1467
|
+
return {
|
|
1468
|
+
id: channel.id?.toString() || chatId,
|
|
1469
|
+
title: channel.title || "Unknown",
|
|
1470
|
+
type,
|
|
1471
|
+
username: channel.username || void 0,
|
|
1472
|
+
description,
|
|
1473
|
+
membersCount
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
if (isChat) {
|
|
1477
|
+
const chat = entity;
|
|
1478
|
+
let description;
|
|
1479
|
+
try {
|
|
1480
|
+
const fullChatResult = await client.invoke(
|
|
1481
|
+
new Api.messages.GetFullChat({ chatId: chat.id })
|
|
1482
|
+
);
|
|
1483
|
+
const fullChat = fullChatResult.fullChat;
|
|
1484
|
+
description = fullChat.about || void 0;
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
id: chat.id?.toString() || chatId,
|
|
1489
|
+
title: chat.title || "Unknown",
|
|
1490
|
+
type: "group",
|
|
1491
|
+
description,
|
|
1492
|
+
membersCount: chat.participantsCount || void 0
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
return null;
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1498
|
+
log.error("telegram.getChatInfo() failed:", err);
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
async getUserInfo(userId) {
|
|
1503
|
+
requireBridge();
|
|
1504
|
+
try {
|
|
1505
|
+
const client = getClient();
|
|
1506
|
+
const { Api } = await import("telegram");
|
|
1507
|
+
let entity;
|
|
1508
|
+
try {
|
|
1509
|
+
const id = typeof userId === "string" ? userId.replace("@", "") : userId.toString();
|
|
1510
|
+
entity = await client.getEntity(id);
|
|
1511
|
+
} catch {
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
if (entity.className !== "User") return null;
|
|
1515
|
+
const user = entity;
|
|
1516
|
+
return {
|
|
1517
|
+
id: Number(user.id),
|
|
1518
|
+
firstName: user.firstName || "",
|
|
1519
|
+
lastName: user.lastName || void 0,
|
|
1520
|
+
username: user.username || void 0,
|
|
1521
|
+
isBot: user.bot || false
|
|
1522
|
+
};
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1525
|
+
throw new PluginSDKError(
|
|
1526
|
+
`Failed to get user info: ${err instanceof Error ? err.message : String(err)}`,
|
|
1527
|
+
"OPERATION_FAILED"
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
async resolveUsername(username) {
|
|
1532
|
+
requireBridge();
|
|
1533
|
+
try {
|
|
1534
|
+
const client = getClient();
|
|
1535
|
+
const { Api } = await import("telegram");
|
|
1536
|
+
const cleanUsername = username.replace("@", "").toLowerCase();
|
|
1537
|
+
if (!cleanUsername) return null;
|
|
1538
|
+
let result;
|
|
1539
|
+
try {
|
|
1540
|
+
result = await client.invoke(
|
|
1541
|
+
new Api.contacts.ResolveUsername({ username: cleanUsername })
|
|
1542
|
+
);
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
if (err.message?.includes("USERNAME_NOT_OCCUPIED") || err.errorMessage === "USERNAME_NOT_OCCUPIED") {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
throw err;
|
|
1548
|
+
}
|
|
1549
|
+
if (result.users && result.users.length > 0) {
|
|
1550
|
+
const user = result.users[0];
|
|
1551
|
+
return {
|
|
1552
|
+
id: Number(user.id),
|
|
1553
|
+
type: "user",
|
|
1554
|
+
username: user.username || void 0,
|
|
1555
|
+
title: user.firstName || void 0
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
if (result.chats && result.chats.length > 0) {
|
|
1559
|
+
const chat = result.chats[0];
|
|
1560
|
+
const type = chat.className === "Channel" ? "channel" : "chat";
|
|
1561
|
+
return {
|
|
1562
|
+
id: Number(chat.id),
|
|
1563
|
+
type,
|
|
1564
|
+
username: chat.username || void 0,
|
|
1565
|
+
title: chat.title || void 0
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
return null;
|
|
1569
|
+
} catch (err) {
|
|
1570
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1571
|
+
throw new PluginSDKError(
|
|
1572
|
+
`Failed to resolve username: ${err instanceof Error ? err.message : String(err)}`,
|
|
1573
|
+
"OPERATION_FAILED"
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
},
|
|
1577
|
+
async getParticipants(chatId, limit) {
|
|
1578
|
+
requireBridge();
|
|
1579
|
+
try {
|
|
1580
|
+
const client = getClient();
|
|
1581
|
+
const { Api } = await import("telegram");
|
|
1582
|
+
const entity = await client.getEntity(chatId);
|
|
1583
|
+
const result = await client.invoke(
|
|
1584
|
+
new Api.channels.GetParticipants({
|
|
1585
|
+
channel: entity,
|
|
1586
|
+
filter: new Api.ChannelParticipantsRecent(),
|
|
1587
|
+
offset: 0,
|
|
1588
|
+
limit: limit ?? 100,
|
|
1589
|
+
hash: 0
|
|
1590
|
+
})
|
|
1591
|
+
);
|
|
1592
|
+
const resultData = result;
|
|
1593
|
+
return (resultData.users || []).map((user) => ({
|
|
1594
|
+
id: Number(user.id),
|
|
1595
|
+
firstName: user.firstName || "",
|
|
1596
|
+
lastName: user.lastName || void 0,
|
|
1597
|
+
username: user.username || void 0,
|
|
1598
|
+
isBot: user.bot || false
|
|
1599
|
+
}));
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1602
|
+
log.error("telegram.getParticipants() failed:", err);
|
|
1603
|
+
return [];
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
// ─── Interactive ──────────────────────────────────────────
|
|
1607
|
+
async createPoll(chatId, question, answers, opts) {
|
|
1608
|
+
requireBridge();
|
|
1609
|
+
if (!answers || answers.length < 2) {
|
|
1610
|
+
throw new PluginSDKError("Poll must have at least 2 answers", "OPERATION_FAILED");
|
|
1611
|
+
}
|
|
1612
|
+
if (answers.length > 10) {
|
|
1613
|
+
throw new PluginSDKError("Poll cannot have more than 10 answers", "OPERATION_FAILED");
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
const client = getClient();
|
|
1617
|
+
const { Api } = await import("telegram");
|
|
1618
|
+
const anonymous = opts?.isAnonymous ?? true;
|
|
1619
|
+
const multipleChoice = opts?.multipleChoice ?? false;
|
|
1620
|
+
const poll = new Api.Poll({
|
|
1621
|
+
id: randomBytes2(8).readBigUInt64BE(),
|
|
1622
|
+
question: new Api.TextWithEntities({ text: question, entities: [] }),
|
|
1623
|
+
answers: answers.map(
|
|
1624
|
+
(opt, idx) => new Api.PollAnswer({
|
|
1625
|
+
text: new Api.TextWithEntities({ text: opt, entities: [] }),
|
|
1626
|
+
option: Buffer.from([idx])
|
|
1627
|
+
})
|
|
1628
|
+
),
|
|
1629
|
+
publicVoters: !anonymous,
|
|
1630
|
+
multipleChoice
|
|
1631
|
+
});
|
|
1632
|
+
const result = await client.invoke(
|
|
1633
|
+
new Api.messages.SendMedia({
|
|
1634
|
+
peer: chatId,
|
|
1635
|
+
media: new Api.InputMediaPoll({ poll }),
|
|
1636
|
+
message: "",
|
|
1637
|
+
randomId: randomBytes2(8).readBigUInt64BE()
|
|
1638
|
+
})
|
|
1639
|
+
);
|
|
1640
|
+
if (result.className === "Updates" || result.className === "UpdatesCombined") {
|
|
1641
|
+
for (const update of result.updates) {
|
|
1642
|
+
if (update.className === "UpdateNewMessage" || update.className === "UpdateNewChannelMessage") {
|
|
1643
|
+
return update.message?.id ?? 0;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return 0;
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1650
|
+
throw new PluginSDKError(
|
|
1651
|
+
`Failed to create poll: ${err instanceof Error ? err.message : String(err)}`,
|
|
1652
|
+
"OPERATION_FAILED"
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
async createQuiz(chatId, question, answers, correctIndex, explanation) {
|
|
1657
|
+
requireBridge();
|
|
1658
|
+
if (!answers || answers.length < 2) {
|
|
1659
|
+
throw new PluginSDKError("Quiz must have at least 2 answers", "OPERATION_FAILED");
|
|
1660
|
+
}
|
|
1661
|
+
if (answers.length > 10) {
|
|
1662
|
+
throw new PluginSDKError("Quiz cannot have more than 10 answers", "OPERATION_FAILED");
|
|
1663
|
+
}
|
|
1664
|
+
if (correctIndex < 0 || correctIndex >= answers.length) {
|
|
1665
|
+
throw new PluginSDKError(
|
|
1666
|
+
`correctIndex ${correctIndex} is out of bounds (0-${answers.length - 1})`,
|
|
1667
|
+
"OPERATION_FAILED"
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
try {
|
|
1671
|
+
const client = getClient();
|
|
1672
|
+
const { Api } = await import("telegram");
|
|
1673
|
+
const poll = new Api.Poll({
|
|
1674
|
+
id: randomBytes2(8).readBigUInt64BE(),
|
|
1675
|
+
question: new Api.TextWithEntities({ text: question, entities: [] }),
|
|
1676
|
+
answers: answers.map(
|
|
1677
|
+
(opt, idx) => new Api.PollAnswer({
|
|
1678
|
+
text: new Api.TextWithEntities({ text: opt, entities: [] }),
|
|
1679
|
+
option: Buffer.from([idx])
|
|
1680
|
+
})
|
|
1681
|
+
),
|
|
1682
|
+
quiz: true,
|
|
1683
|
+
publicVoters: false,
|
|
1684
|
+
multipleChoice: false
|
|
1685
|
+
});
|
|
1686
|
+
const result = await client.invoke(
|
|
1687
|
+
new Api.messages.SendMedia({
|
|
1688
|
+
peer: chatId,
|
|
1689
|
+
media: new Api.InputMediaPoll({
|
|
1690
|
+
poll,
|
|
1691
|
+
correctAnswers: [Buffer.from([correctIndex])],
|
|
1692
|
+
solution: explanation,
|
|
1693
|
+
solutionEntities: []
|
|
1694
|
+
}),
|
|
1695
|
+
message: "",
|
|
1696
|
+
randomId: randomBytes2(8).readBigUInt64BE()
|
|
1697
|
+
})
|
|
1698
|
+
);
|
|
1699
|
+
if (result.className === "Updates" || result.className === "UpdatesCombined") {
|
|
1700
|
+
for (const update of result.updates) {
|
|
1701
|
+
if (update.className === "UpdateNewMessage" || update.className === "UpdateNewChannelMessage") {
|
|
1702
|
+
return update.message?.id ?? 0;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return 0;
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1709
|
+
throw new PluginSDKError(
|
|
1710
|
+
`Failed to create quiz: ${err instanceof Error ? err.message : String(err)}`,
|
|
1711
|
+
"OPERATION_FAILED"
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
},
|
|
1715
|
+
// ─── Moderation ───────────────────────────────────────────
|
|
1716
|
+
async banUser(chatId, userId) {
|
|
1717
|
+
requireBridge();
|
|
1718
|
+
try {
|
|
1719
|
+
const client = getClient();
|
|
1720
|
+
const { Api } = await import("telegram");
|
|
1721
|
+
await client.invoke(
|
|
1722
|
+
new Api.channels.EditBanned({
|
|
1723
|
+
channel: chatId,
|
|
1724
|
+
participant: userId.toString(),
|
|
1725
|
+
bannedRights: new Api.ChatBannedRights({
|
|
1726
|
+
untilDate: 0,
|
|
1727
|
+
viewMessages: true,
|
|
1728
|
+
sendMessages: true,
|
|
1729
|
+
sendMedia: true,
|
|
1730
|
+
sendStickers: true,
|
|
1731
|
+
sendGifs: true,
|
|
1732
|
+
sendGames: true,
|
|
1733
|
+
sendInline: true,
|
|
1734
|
+
embedLinks: true
|
|
1735
|
+
})
|
|
1736
|
+
})
|
|
1737
|
+
);
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1740
|
+
throw new PluginSDKError(
|
|
1741
|
+
`Failed to ban user: ${err instanceof Error ? err.message : String(err)}`,
|
|
1742
|
+
"OPERATION_FAILED"
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
async unbanUser(chatId, userId) {
|
|
1747
|
+
requireBridge();
|
|
1748
|
+
try {
|
|
1749
|
+
const client = getClient();
|
|
1750
|
+
const { Api } = await import("telegram");
|
|
1751
|
+
await client.invoke(
|
|
1752
|
+
new Api.channels.EditBanned({
|
|
1753
|
+
channel: chatId,
|
|
1754
|
+
participant: userId.toString(),
|
|
1755
|
+
bannedRights: new Api.ChatBannedRights({
|
|
1756
|
+
untilDate: 0
|
|
1757
|
+
})
|
|
1758
|
+
})
|
|
1759
|
+
);
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1762
|
+
throw new PluginSDKError(
|
|
1763
|
+
`Failed to unban user: ${err instanceof Error ? err.message : String(err)}`,
|
|
1764
|
+
"OPERATION_FAILED"
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
},
|
|
1768
|
+
async muteUser(chatId, userId, untilDate) {
|
|
1769
|
+
requireBridge();
|
|
1770
|
+
try {
|
|
1771
|
+
const client = getClient();
|
|
1772
|
+
const { Api } = await import("telegram");
|
|
1773
|
+
await client.invoke(
|
|
1774
|
+
new Api.channels.EditBanned({
|
|
1775
|
+
channel: chatId,
|
|
1776
|
+
participant: userId.toString(),
|
|
1777
|
+
bannedRights: new Api.ChatBannedRights({
|
|
1778
|
+
untilDate: untilDate ?? 0,
|
|
1779
|
+
sendMessages: true
|
|
1780
|
+
})
|
|
1781
|
+
})
|
|
1782
|
+
);
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1785
|
+
throw new PluginSDKError(
|
|
1786
|
+
`Failed to mute user: ${err instanceof Error ? err.message : String(err)}`,
|
|
1787
|
+
"OPERATION_FAILED"
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
// ─── Stars & Gifts ────────────────────────────────────────
|
|
1792
|
+
async getStarsBalance() {
|
|
1793
|
+
requireBridge();
|
|
1794
|
+
try {
|
|
1795
|
+
const client = getClient();
|
|
1796
|
+
const { Api } = await import("telegram");
|
|
1797
|
+
const result = await client.invoke(
|
|
1798
|
+
new Api.payments.GetStarsStatus({
|
|
1799
|
+
peer: new Api.InputPeerSelf()
|
|
1800
|
+
})
|
|
1801
|
+
);
|
|
1802
|
+
return Number(result.balance?.amount?.toString() || "0");
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1805
|
+
throw new PluginSDKError(
|
|
1806
|
+
`Failed to get stars balance: ${err instanceof Error ? err.message : String(err)}`,
|
|
1807
|
+
"OPERATION_FAILED"
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
async sendGift(userId, giftId, opts) {
|
|
1812
|
+
requireBridge();
|
|
1813
|
+
try {
|
|
1814
|
+
const client = getClient();
|
|
1815
|
+
const { Api } = await import("telegram");
|
|
1816
|
+
const user = await client.getEntity(userId.toString());
|
|
1817
|
+
const invoiceData = {
|
|
1818
|
+
peer: user,
|
|
1819
|
+
giftId: BigInt(giftId),
|
|
1820
|
+
hideName: opts?.anonymous ?? false,
|
|
1821
|
+
message: opts?.message ? new Api.TextWithEntities({ text: opts.message, entities: [] }) : void 0
|
|
1822
|
+
};
|
|
1823
|
+
const form = await client.invoke(
|
|
1824
|
+
new Api.payments.GetPaymentForm({
|
|
1825
|
+
invoice: new Api.InputInvoiceStarGift(invoiceData)
|
|
1826
|
+
})
|
|
1827
|
+
);
|
|
1828
|
+
await client.invoke(
|
|
1829
|
+
new Api.payments.SendStarsForm({
|
|
1830
|
+
formId: form.formId,
|
|
1831
|
+
invoice: new Api.InputInvoiceStarGift(invoiceData)
|
|
1832
|
+
})
|
|
1833
|
+
);
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1836
|
+
throw new PluginSDKError(
|
|
1837
|
+
`Failed to send gift: ${err instanceof Error ? err.message : String(err)}`,
|
|
1838
|
+
"OPERATION_FAILED"
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
},
|
|
1842
|
+
async getAvailableGifts() {
|
|
1843
|
+
requireBridge();
|
|
1844
|
+
try {
|
|
1845
|
+
const client = getClient();
|
|
1846
|
+
const { Api } = await import("telegram");
|
|
1847
|
+
const result = await client.invoke(new Api.payments.GetStarGifts({ hash: 0 }));
|
|
1848
|
+
if (result.className === "payments.StarGiftsNotModified") {
|
|
1849
|
+
return [];
|
|
1850
|
+
}
|
|
1851
|
+
return (result.gifts || []).filter((gift) => !gift.soldOut).map((gift) => ({
|
|
1852
|
+
id: gift.id?.toString(),
|
|
1853
|
+
starsAmount: Number(gift.stars?.toString() || "0"),
|
|
1854
|
+
availableAmount: gift.limited ? Number(gift.availabilityRemains?.toString() || "0") : void 0,
|
|
1855
|
+
totalAmount: gift.limited ? Number(gift.availabilityTotal?.toString() || "0") : void 0
|
|
1856
|
+
}));
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1859
|
+
throw new PluginSDKError(
|
|
1860
|
+
`Failed to get available gifts: ${err instanceof Error ? err.message : String(err)}`,
|
|
1861
|
+
"OPERATION_FAILED"
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
},
|
|
1865
|
+
async getMyGifts(limit) {
|
|
1866
|
+
requireBridge();
|
|
1867
|
+
try {
|
|
1868
|
+
const client = getClient();
|
|
1869
|
+
const { Api } = await import("telegram");
|
|
1870
|
+
const result = await client.invoke(
|
|
1871
|
+
new Api.payments.GetSavedStarGifts({
|
|
1872
|
+
peer: new Api.InputPeerSelf(),
|
|
1873
|
+
offset: "",
|
|
1874
|
+
limit: limit ?? 50
|
|
1875
|
+
})
|
|
1876
|
+
);
|
|
1877
|
+
return (result.gifts || []).map((savedGift) => {
|
|
1878
|
+
const gift = savedGift.gift;
|
|
1879
|
+
return {
|
|
1880
|
+
id: gift?.id?.toString() || "",
|
|
1881
|
+
fromId: savedGift.fromId ? Number(savedGift.fromId) : void 0,
|
|
1882
|
+
date: savedGift.date || 0,
|
|
1883
|
+
starsAmount: Number(gift?.stars?.toString() || "0"),
|
|
1884
|
+
saved: savedGift.unsaved !== true,
|
|
1885
|
+
messageId: savedGift.msgId || void 0
|
|
1886
|
+
};
|
|
1887
|
+
});
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1890
|
+
throw new PluginSDKError(
|
|
1891
|
+
`Failed to get my gifts: ${err instanceof Error ? err.message : String(err)}`,
|
|
1892
|
+
"OPERATION_FAILED"
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
},
|
|
1896
|
+
async getResaleGifts(limit) {
|
|
1897
|
+
requireBridge();
|
|
1898
|
+
try {
|
|
1899
|
+
const client = getClient();
|
|
1900
|
+
const { Api } = await import("telegram");
|
|
1901
|
+
if (!Api.payments.GetResaleStarGifts) {
|
|
1902
|
+
throw new PluginSDKError(
|
|
1903
|
+
"Resale gift marketplace is not supported in the current Telegram API layer.",
|
|
1904
|
+
"OPERATION_FAILED"
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const result = await client.invoke(
|
|
1908
|
+
new Api.payments.GetResaleStarGifts({
|
|
1909
|
+
offset: "",
|
|
1910
|
+
limit: limit ?? 50
|
|
1911
|
+
})
|
|
1912
|
+
);
|
|
1913
|
+
return (result.gifts || []).map((listing) => ({
|
|
1914
|
+
id: listing.odayId?.toString() || "",
|
|
1915
|
+
starsAmount: Number(listing.resellStars?.toString() || "0")
|
|
1916
|
+
}));
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1919
|
+
throw new PluginSDKError(
|
|
1920
|
+
`Failed to get resale gifts: ${err instanceof Error ? err.message : String(err)}`,
|
|
1921
|
+
"OPERATION_FAILED"
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
},
|
|
1925
|
+
async buyResaleGift(giftId) {
|
|
1926
|
+
requireBridge();
|
|
1927
|
+
try {
|
|
1928
|
+
const client = getClient();
|
|
1929
|
+
const { Api } = await import("telegram");
|
|
1930
|
+
if (!Api.InputInvoiceStarGiftResale) {
|
|
1931
|
+
throw new PluginSDKError(
|
|
1932
|
+
"Resale gift purchasing is not supported in the current Telegram API layer.",
|
|
1933
|
+
"OPERATION_FAILED"
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
const stargiftInput = new Api.InputSavedStarGiftUser({
|
|
1937
|
+
odayId: BigInt(giftId)
|
|
1938
|
+
});
|
|
1939
|
+
const invoiceData = { stargift: stargiftInput };
|
|
1940
|
+
const form = await client.invoke(
|
|
1941
|
+
new Api.payments.GetPaymentForm({
|
|
1942
|
+
invoice: new Api.InputInvoiceStarGiftResale(invoiceData)
|
|
1943
|
+
})
|
|
1944
|
+
);
|
|
1945
|
+
await client.invoke(
|
|
1946
|
+
new Api.payments.SendStarsForm({
|
|
1947
|
+
formId: form.formId,
|
|
1948
|
+
invoice: new Api.InputInvoiceStarGiftResale(invoiceData)
|
|
1949
|
+
})
|
|
1950
|
+
);
|
|
1951
|
+
} catch (err) {
|
|
1952
|
+
if (err instanceof PluginSDKError) throw err;
|
|
1953
|
+
throw new PluginSDKError(
|
|
1954
|
+
`Failed to buy resale gift: ${err instanceof Error ? err.message : String(err)}`,
|
|
1955
|
+
"OPERATION_FAILED"
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
async sendStory(mediaPath, opts) {
|
|
1960
|
+
requireBridge();
|
|
1961
|
+
try {
|
|
1962
|
+
const client = getClient();
|
|
1963
|
+
const { Api, helpers } = await import("telegram");
|
|
1964
|
+
const { CustomFile } = await import("telegram/client/uploads.js");
|
|
1965
|
+
const { readFileSync: readFileSync5, statSync: statSync2 } = await import("fs");
|
|
1966
|
+
const { basename: basename2 } = await import("path");
|
|
1967
|
+
const filePath = mediaPath;
|
|
1968
|
+
const fileName = basename2(filePath);
|
|
1969
|
+
const fileSize = statSync2(filePath).size;
|
|
1970
|
+
const fileBuffer = readFileSync5(filePath);
|
|
1971
|
+
const isVideo = filePath.toLowerCase().match(/\.(mp4|mov|avi)$/);
|
|
1972
|
+
const customFile = new CustomFile(fileName, fileSize, filePath, fileBuffer);
|
|
1973
|
+
const uploadedFile = await client.uploadFile({
|
|
1974
|
+
file: customFile,
|
|
1975
|
+
workers: 1
|
|
1976
|
+
});
|
|
1977
|
+
let inputMedia;
|
|
1978
|
+
if (isVideo) {
|
|
1979
|
+
inputMedia = new Api.InputMediaUploadedDocument({
|
|
1980
|
+
file: uploadedFile,
|
|
1981
|
+
mimeType: "video/mp4",
|
|
1982
|
+
attributes: [
|
|
1983
|
+
new Api.DocumentAttributeVideo({
|
|
1984
|
+
duration: 0,
|
|
1985
|
+
w: 720,
|
|
1986
|
+
h: 1280,
|
|
1987
|
+
supportsStreaming: true
|
|
1988
|
+
}),
|
|
1989
|
+
new Api.DocumentAttributeFilename({ fileName })
|
|
1990
|
+
]
|
|
1991
|
+
});
|
|
1992
|
+
} else {
|
|
1993
|
+
inputMedia = new Api.InputMediaUploadedPhoto({
|
|
1994
|
+
file: uploadedFile
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
const privacyRules = [new Api.InputPrivacyValueAllowAll()];
|
|
1998
|
+
const result = await client.invoke(
|
|
1999
|
+
new Api.stories.SendStory({
|
|
2000
|
+
peer: "me",
|
|
2001
|
+
media: inputMedia,
|
|
2002
|
+
caption: opts?.caption || "",
|
|
2003
|
+
privacyRules,
|
|
2004
|
+
randomId: helpers.generateRandomBigInt()
|
|
2005
|
+
})
|
|
2006
|
+
);
|
|
2007
|
+
return result.id || 0;
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
if (err instanceof PluginSDKError) throw err;
|
|
2010
|
+
throw new PluginSDKError(
|
|
2011
|
+
`Failed to send story: ${err instanceof Error ? err.message : String(err)}`,
|
|
2012
|
+
"OPERATION_FAILED"
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/sdk/telegram.ts
|
|
2020
|
+
function createTelegramSDK(bridge, log) {
|
|
2021
|
+
function requireBridge() {
|
|
2022
|
+
if (!bridge.isAvailable()) {
|
|
2023
|
+
throw new PluginSDKError(
|
|
2024
|
+
"Telegram bridge not connected. SDK telegram methods can only be called at runtime (inside tool executors or start()), not during plugin loading.",
|
|
2025
|
+
"BRIDGE_NOT_CONNECTED"
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return {
|
|
2030
|
+
async sendMessage(chatId, text, opts) {
|
|
2031
|
+
requireBridge();
|
|
2032
|
+
try {
|
|
2033
|
+
const msg = await bridge.sendMessage({
|
|
2034
|
+
chatId,
|
|
2035
|
+
text,
|
|
2036
|
+
replyToId: opts?.replyToId,
|
|
2037
|
+
inlineKeyboard: opts?.inlineKeyboard
|
|
2038
|
+
});
|
|
2039
|
+
return msg.id;
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
if (err instanceof PluginSDKError) throw err;
|
|
2042
|
+
throw new PluginSDKError(
|
|
2043
|
+
`Failed to send message: ${err instanceof Error ? err.message : String(err)}`,
|
|
2044
|
+
"OPERATION_FAILED"
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
},
|
|
2048
|
+
async editMessage(chatId, messageId, text, opts) {
|
|
2049
|
+
requireBridge();
|
|
2050
|
+
try {
|
|
2051
|
+
const msg = await bridge.editMessage({
|
|
2052
|
+
chatId,
|
|
2053
|
+
messageId,
|
|
2054
|
+
text,
|
|
2055
|
+
inlineKeyboard: opts?.inlineKeyboard
|
|
2056
|
+
});
|
|
2057
|
+
return typeof msg?.id === "number" ? msg.id : messageId;
|
|
2058
|
+
} catch (err) {
|
|
2059
|
+
if (err instanceof PluginSDKError) throw err;
|
|
2060
|
+
throw new PluginSDKError(
|
|
2061
|
+
`Failed to edit message: ${err instanceof Error ? err.message : String(err)}`,
|
|
2062
|
+
"OPERATION_FAILED"
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
async sendDice(chatId, emoticon, replyToId) {
|
|
2067
|
+
requireBridge();
|
|
2068
|
+
try {
|
|
2069
|
+
const gramJsClient = bridge.getClient().getClient();
|
|
2070
|
+
const { Api } = await import("telegram");
|
|
2071
|
+
const result = await gramJsClient.invoke(
|
|
2072
|
+
new Api.messages.SendMedia({
|
|
2073
|
+
peer: chatId,
|
|
2074
|
+
media: new Api.InputMediaDice({ emoticon }),
|
|
2075
|
+
message: "",
|
|
2076
|
+
randomId: randomBytes3(8).readBigUInt64BE(),
|
|
2077
|
+
replyTo: replyToId ? new Api.InputReplyToMessage({ replyToMsgId: replyToId }) : void 0
|
|
2078
|
+
})
|
|
2079
|
+
);
|
|
2080
|
+
let value;
|
|
2081
|
+
let messageId;
|
|
2082
|
+
if (result.className === "Updates" || result.className === "UpdatesCombined") {
|
|
2083
|
+
for (const update of result.updates) {
|
|
2084
|
+
if (update.className === "UpdateNewMessage" || update.className === "UpdateNewChannelMessage") {
|
|
2085
|
+
const msg = update.message;
|
|
2086
|
+
if (msg?.media?.className === "MessageMediaDice") {
|
|
2087
|
+
value = msg.media.value;
|
|
2088
|
+
messageId = msg.id;
|
|
2089
|
+
break;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (value === void 0 || messageId === void 0) {
|
|
2095
|
+
throw new Error("Could not extract dice value from Telegram response");
|
|
2096
|
+
}
|
|
2097
|
+
return { value, messageId };
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
if (err instanceof PluginSDKError) throw err;
|
|
2100
|
+
throw new PluginSDKError(
|
|
2101
|
+
`Failed to send dice: ${err instanceof Error ? err.message : String(err)}`,
|
|
2102
|
+
"OPERATION_FAILED"
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2106
|
+
async sendReaction(chatId, messageId, emoji) {
|
|
2107
|
+
requireBridge();
|
|
2108
|
+
try {
|
|
2109
|
+
await bridge.sendReaction(chatId, messageId, emoji);
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
if (err instanceof PluginSDKError) throw err;
|
|
2112
|
+
throw new PluginSDKError(
|
|
2113
|
+
`Failed to send reaction: ${err instanceof Error ? err.message : String(err)}`,
|
|
2114
|
+
"OPERATION_FAILED"
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
},
|
|
2118
|
+
async getMessages(chatId, limit) {
|
|
2119
|
+
requireBridge();
|
|
2120
|
+
try {
|
|
2121
|
+
const messages = await bridge.getMessages(chatId, limit ?? 50);
|
|
2122
|
+
return messages.map((m) => ({
|
|
2123
|
+
id: m.id,
|
|
2124
|
+
text: m.text,
|
|
2125
|
+
senderId: m.senderId,
|
|
2126
|
+
senderUsername: m.senderUsername,
|
|
2127
|
+
timestamp: m.timestamp
|
|
2128
|
+
}));
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
log.error("telegram.getMessages() failed:", err);
|
|
2131
|
+
return [];
|
|
2132
|
+
}
|
|
2133
|
+
},
|
|
2134
|
+
getMe() {
|
|
2135
|
+
try {
|
|
2136
|
+
const me = bridge.getClient()?.getMe?.();
|
|
2137
|
+
if (!me) return null;
|
|
2138
|
+
return {
|
|
2139
|
+
id: Number(me.id),
|
|
2140
|
+
username: me.username,
|
|
2141
|
+
firstName: me.firstName,
|
|
2142
|
+
isBot: me.isBot
|
|
2143
|
+
};
|
|
2144
|
+
} catch {
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
},
|
|
2148
|
+
isAvailable() {
|
|
2149
|
+
return bridge.isAvailable();
|
|
2150
|
+
},
|
|
2151
|
+
getRawClient() {
|
|
2152
|
+
if (!bridge.isAvailable()) return null;
|
|
2153
|
+
try {
|
|
2154
|
+
return bridge.getClient().getClient();
|
|
2155
|
+
} catch {
|
|
2156
|
+
return null;
|
|
2157
|
+
}
|
|
2158
|
+
},
|
|
2159
|
+
// Spread extended methods from sub-modules
|
|
2160
|
+
...createTelegramMessagesSDK(bridge, log),
|
|
2161
|
+
...createTelegramSocialSDK(bridge, log)
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// src/sdk/secrets.ts
|
|
2166
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
|
|
2167
|
+
import { join as join3 } from "path";
|
|
2168
|
+
var SECRETS_DIR = join3(TELETON_ROOT, "plugins", "data");
|
|
2169
|
+
function getSecretsPath(pluginName) {
|
|
2170
|
+
return join3(SECRETS_DIR, `${pluginName}.secrets.json`);
|
|
2171
|
+
}
|
|
2172
|
+
function readSecretsFile(pluginName) {
|
|
2173
|
+
const filePath = getSecretsPath(pluginName);
|
|
2174
|
+
try {
|
|
2175
|
+
if (!existsSync3(filePath)) return {};
|
|
2176
|
+
const raw = readFileSync3(filePath, "utf-8");
|
|
2177
|
+
const parsed = JSON.parse(raw);
|
|
2178
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
2179
|
+
return parsed;
|
|
2180
|
+
} catch {
|
|
2181
|
+
return {};
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function writePluginSecret(pluginName, key, value) {
|
|
2185
|
+
mkdirSync3(SECRETS_DIR, { recursive: true });
|
|
2186
|
+
const filePath = getSecretsPath(pluginName);
|
|
2187
|
+
const existing = readSecretsFile(pluginName);
|
|
2188
|
+
existing[key] = value;
|
|
2189
|
+
writeFileSync3(filePath, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
2190
|
+
}
|
|
2191
|
+
function deletePluginSecret(pluginName, key) {
|
|
2192
|
+
const existing = readSecretsFile(pluginName);
|
|
2193
|
+
if (!(key in existing)) return false;
|
|
2194
|
+
delete existing[key];
|
|
2195
|
+
const filePath = getSecretsPath(pluginName);
|
|
2196
|
+
writeFileSync3(filePath, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
2197
|
+
return true;
|
|
2198
|
+
}
|
|
2199
|
+
function listPluginSecretKeys(pluginName) {
|
|
2200
|
+
return Object.keys(readSecretsFile(pluginName));
|
|
2201
|
+
}
|
|
2202
|
+
function createSecretsSDK(pluginName, pluginConfig, log) {
|
|
2203
|
+
const envPrefix = pluginName.replace(/-/g, "_").toUpperCase();
|
|
2204
|
+
function get(key) {
|
|
2205
|
+
const envKey = `${envPrefix}_${key.toUpperCase()}`;
|
|
2206
|
+
const envValue = process.env[envKey];
|
|
2207
|
+
if (envValue) {
|
|
2208
|
+
log.debug(`Secret "${key}" resolved from env var ${envKey}`);
|
|
2209
|
+
return envValue;
|
|
2210
|
+
}
|
|
2211
|
+
const stored = readSecretsFile(pluginName);
|
|
2212
|
+
if (key in stored && stored[key]) {
|
|
2213
|
+
log.debug(`Secret "${key}" resolved from secrets store`);
|
|
2214
|
+
return stored[key];
|
|
2215
|
+
}
|
|
2216
|
+
const configValue = pluginConfig[key];
|
|
2217
|
+
if (configValue !== void 0 && configValue !== null) {
|
|
2218
|
+
log.debug(`Secret "${key}" resolved from pluginConfig`);
|
|
2219
|
+
return String(configValue);
|
|
2220
|
+
}
|
|
2221
|
+
return void 0;
|
|
2222
|
+
}
|
|
2223
|
+
return {
|
|
2224
|
+
get,
|
|
2225
|
+
require(key) {
|
|
2226
|
+
const value = get(key);
|
|
2227
|
+
if (!value) {
|
|
2228
|
+
throw new PluginSDKError(
|
|
2229
|
+
`Missing required secret "${key}". Set it via: /plugin set ${pluginName} ${key} <value>`,
|
|
2230
|
+
"SECRET_NOT_FOUND"
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
return value;
|
|
2234
|
+
},
|
|
2235
|
+
has(key) {
|
|
2236
|
+
return get(key) !== void 0;
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/sdk/storage.ts
|
|
2242
|
+
var KV_TABLE = "_kv";
|
|
2243
|
+
var CLEANUP_PROBABILITY2 = 0.05;
|
|
2244
|
+
function ensureTable(db) {
|
|
2245
|
+
db.exec(`
|
|
2246
|
+
CREATE TABLE IF NOT EXISTS ${KV_TABLE} (
|
|
2247
|
+
key TEXT PRIMARY KEY,
|
|
2248
|
+
value TEXT NOT NULL,
|
|
2249
|
+
expires_at INTEGER
|
|
2250
|
+
)
|
|
2251
|
+
`);
|
|
2252
|
+
}
|
|
2253
|
+
function createStorageSDK(db) {
|
|
2254
|
+
ensureTable(db);
|
|
2255
|
+
const stmtGet = db.prepare(`SELECT value, expires_at FROM ${KV_TABLE} WHERE key = ?`);
|
|
2256
|
+
const stmtSet = db.prepare(
|
|
2257
|
+
`INSERT OR REPLACE INTO ${KV_TABLE} (key, value, expires_at) VALUES (?, ?, ?)`
|
|
2258
|
+
);
|
|
2259
|
+
const stmtDel = db.prepare(`DELETE FROM ${KV_TABLE} WHERE key = ?`);
|
|
2260
|
+
const stmtHas = db.prepare(`SELECT 1 FROM ${KV_TABLE} WHERE key = ?`);
|
|
2261
|
+
const stmtClear = db.prepare(`DELETE FROM ${KV_TABLE}`);
|
|
2262
|
+
const stmtCleanup = db.prepare(
|
|
2263
|
+
`DELETE FROM ${KV_TABLE} WHERE expires_at IS NOT NULL AND expires_at < ?`
|
|
2264
|
+
);
|
|
2265
|
+
function maybeCleanup() {
|
|
2266
|
+
if (Math.random() < CLEANUP_PROBABILITY2) {
|
|
2267
|
+
stmtCleanup.run(Date.now());
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return {
|
|
2271
|
+
get(key) {
|
|
2272
|
+
maybeCleanup();
|
|
2273
|
+
const row = stmtGet.get(key);
|
|
2274
|
+
if (!row) return void 0;
|
|
2275
|
+
if (row.expires_at !== null && row.expires_at < Date.now()) {
|
|
2276
|
+
stmtDel.run(key);
|
|
2277
|
+
return void 0;
|
|
2278
|
+
}
|
|
2279
|
+
try {
|
|
2280
|
+
return JSON.parse(row.value);
|
|
2281
|
+
} catch {
|
|
2282
|
+
return row.value;
|
|
2283
|
+
}
|
|
2284
|
+
},
|
|
2285
|
+
set(key, value, opts) {
|
|
2286
|
+
const serialized = JSON.stringify(value);
|
|
2287
|
+
const expiresAt = opts?.ttl ? Date.now() + opts.ttl : null;
|
|
2288
|
+
stmtSet.run(key, serialized, expiresAt);
|
|
2289
|
+
},
|
|
2290
|
+
delete(key) {
|
|
2291
|
+
const result = stmtDel.run(key);
|
|
2292
|
+
return result.changes > 0;
|
|
2293
|
+
},
|
|
2294
|
+
has(key) {
|
|
2295
|
+
const row = stmtGet.get(key);
|
|
2296
|
+
if (!row) return false;
|
|
2297
|
+
if (row.expires_at !== null && row.expires_at < Date.now()) {
|
|
2298
|
+
stmtDel.run(key);
|
|
2299
|
+
return false;
|
|
2300
|
+
}
|
|
2301
|
+
return true;
|
|
2302
|
+
},
|
|
2303
|
+
clear() {
|
|
2304
|
+
stmtClear.run();
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/sdk/index.ts
|
|
2310
|
+
function createPluginSDK(deps, opts) {
|
|
2311
|
+
const log = createLogger(opts.pluginName);
|
|
2312
|
+
const ton = Object.freeze(createTonSDK(log, opts.db));
|
|
2313
|
+
const telegram = Object.freeze(createTelegramSDK(deps.bridge, log));
|
|
2314
|
+
const secrets = Object.freeze(createSecretsSDK(opts.pluginName, opts.pluginConfig, log));
|
|
2315
|
+
const storage = opts.db ? Object.freeze(createStorageSDK(opts.db)) : null;
|
|
2316
|
+
const frozenLog = Object.freeze(log);
|
|
2317
|
+
const frozenConfig = Object.freeze(opts.sanitizedConfig);
|
|
2318
|
+
const frozenPluginConfig = Object.freeze(opts.pluginConfig);
|
|
2319
|
+
return Object.freeze({
|
|
2320
|
+
version: SDK_VERSION,
|
|
2321
|
+
ton,
|
|
2322
|
+
telegram,
|
|
2323
|
+
secrets,
|
|
2324
|
+
storage,
|
|
2325
|
+
db: opts.db,
|
|
2326
|
+
config: frozenConfig,
|
|
2327
|
+
pluginConfig: frozenPluginConfig,
|
|
2328
|
+
log: frozenLog
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
function createLogger(pluginName) {
|
|
2332
|
+
const prefix = `[${pluginName}]`;
|
|
2333
|
+
return {
|
|
2334
|
+
info: (...args) => console.log(prefix, ...args),
|
|
2335
|
+
warn: (...args) => console.warn(`\u26A0\uFE0F ${prefix}`, ...args),
|
|
2336
|
+
error: (...args) => console.error(`\u274C ${prefix}`, ...args),
|
|
2337
|
+
debug: (...args) => {
|
|
2338
|
+
if (process.env.DEBUG || process.env.VERBOSE) {
|
|
2339
|
+
console.log(`\u{1F50D} ${prefix}`, ...args);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
function parseSemver(v) {
|
|
2345
|
+
const match = v.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
2346
|
+
if (!match) return null;
|
|
2347
|
+
return {
|
|
2348
|
+
major: parseInt(match[1]),
|
|
2349
|
+
minor: parseInt(match[2]),
|
|
2350
|
+
patch: parseInt(match[3])
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
function semverGte(a, b) {
|
|
2354
|
+
if (a.major !== b.major) return a.major > b.major;
|
|
2355
|
+
if (a.minor !== b.minor) return a.minor > b.minor;
|
|
2356
|
+
return a.patch >= b.patch;
|
|
2357
|
+
}
|
|
2358
|
+
function semverSatisfies(current, range) {
|
|
2359
|
+
const cur = parseSemver(current);
|
|
2360
|
+
if (!cur) {
|
|
2361
|
+
console.warn(`\u26A0\uFE0F [SDK] Could not parse current version "${current}", skipping check`);
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
if (range.startsWith(">=")) {
|
|
2365
|
+
const req2 = parseSemver(range.slice(2));
|
|
2366
|
+
if (!req2) {
|
|
2367
|
+
console.warn(`\u26A0\uFE0F [SDK] Malformed sdkVersion range "${range}", skipping check`);
|
|
2368
|
+
return true;
|
|
2369
|
+
}
|
|
2370
|
+
return semverGte(cur, req2);
|
|
2371
|
+
}
|
|
2372
|
+
if (range.startsWith("^")) {
|
|
2373
|
+
const req2 = parseSemver(range.slice(1));
|
|
2374
|
+
if (!req2) {
|
|
2375
|
+
console.warn(`\u26A0\uFE0F [SDK] Malformed sdkVersion range "${range}", skipping check`);
|
|
2376
|
+
return true;
|
|
2377
|
+
}
|
|
2378
|
+
if (req2.major === 0) {
|
|
2379
|
+
return cur.major === 0 && cur.minor === req2.minor && semverGte(cur, req2);
|
|
2380
|
+
}
|
|
2381
|
+
return cur.major === req2.major && semverGte(cur, req2);
|
|
2382
|
+
}
|
|
2383
|
+
const req = parseSemver(range);
|
|
2384
|
+
if (!req) {
|
|
2385
|
+
console.warn(`\u26A0\uFE0F [SDK] Malformed sdkVersion "${range}", skipping check`);
|
|
2386
|
+
return true;
|
|
2387
|
+
}
|
|
2388
|
+
return cur.major === req.major && cur.minor === req.minor && cur.patch === req.patch;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// src/agent/tools/plugin-loader.ts
|
|
2392
|
+
var execFileAsync = promisify(execFile);
|
|
2393
|
+
var PLUGIN_DATA_DIR = join4(TELETON_ROOT, "plugins", "data");
|
|
2394
|
+
function adaptPlugin(raw, entryName, config, loadedModuleNames, sdkDeps) {
|
|
2395
|
+
let manifest = null;
|
|
2396
|
+
if (raw.manifest) {
|
|
2397
|
+
try {
|
|
2398
|
+
manifest = validateManifest(raw.manifest);
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
console.warn(
|
|
2401
|
+
`\u26A0\uFE0F [${entryName}] invalid manifest, ignoring:`,
|
|
2402
|
+
err instanceof Error ? err.message : err
|
|
2403
|
+
);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
if (!manifest) {
|
|
2407
|
+
const manifestPath = join4(WORKSPACE_PATHS.PLUGINS_DIR, entryName, "manifest.json");
|
|
2408
|
+
try {
|
|
2409
|
+
if (existsSync4(manifestPath)) {
|
|
2410
|
+
const diskManifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
|
|
2411
|
+
if (diskManifest && typeof diskManifest.version === "string") {
|
|
2412
|
+
manifest = {
|
|
2413
|
+
name: entryName,
|
|
2414
|
+
version: diskManifest.version,
|
|
2415
|
+
description: typeof diskManifest.description === "string" ? diskManifest.description : void 0,
|
|
2416
|
+
author: typeof diskManifest.author === "string" ? diskManifest.author : diskManifest.author?.name ?? void 0
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
} catch {
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
const pluginName = manifest?.name ?? entryName.replace(/\.js$/, "");
|
|
2424
|
+
const pluginVersion = manifest?.version ?? "0.0.0";
|
|
2425
|
+
if (manifest?.dependencies) {
|
|
2426
|
+
for (const dep of manifest.dependencies) {
|
|
2427
|
+
if (!loadedModuleNames.includes(dep)) {
|
|
2428
|
+
throw new Error(`Plugin "${pluginName}" requires module "${dep}" which is not loaded`);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (manifest?.sdkVersion) {
|
|
2433
|
+
if (!semverSatisfies(SDK_VERSION, manifest.sdkVersion)) {
|
|
2434
|
+
throw new Error(
|
|
2435
|
+
`Plugin "${pluginName}" requires SDK ${manifest.sdkVersion} but current SDK is ${SDK_VERSION}`
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
const pluginConfigKey = pluginName.replace(/-/g, "_");
|
|
2440
|
+
const rawPluginConfig = config.plugins?.[pluginConfigKey] ?? {};
|
|
2441
|
+
const pluginConfig = { ...manifest?.defaultConfig, ...rawPluginConfig };
|
|
2442
|
+
const log = (...args) => console.log(`[${pluginName}]`, ...args);
|
|
2443
|
+
if (manifest?.secrets) {
|
|
2444
|
+
const dummyLogger = {
|
|
2445
|
+
info: log,
|
|
2446
|
+
warn: (...a) => console.warn(`\u26A0\uFE0F [${pluginName}]`, ...a),
|
|
2447
|
+
error: (...a) => console.error(`\u274C [${pluginName}]`, ...a),
|
|
2448
|
+
debug: () => {
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
const secretsCheck = createSecretsSDK(pluginName, pluginConfig, dummyLogger);
|
|
2452
|
+
const missing = [];
|
|
2453
|
+
for (const [key, decl] of Object.entries(
|
|
2454
|
+
manifest.secrets
|
|
2455
|
+
)) {
|
|
2456
|
+
if (decl.required && !secretsCheck.has(key)) {
|
|
2457
|
+
missing.push(`${key} \u2014 ${decl.description}`);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
if (missing.length > 0) {
|
|
2461
|
+
console.warn(
|
|
2462
|
+
`\u26A0\uFE0F [${pluginName}] Missing required secrets:
|
|
2463
|
+
` + missing.map((m) => ` \u2022 ${m}`).join("\n") + `
|
|
2464
|
+
Set via: /plugin set ${pluginName} <key> <value>`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const hasMigrate = typeof raw.migrate === "function";
|
|
2469
|
+
let pluginDb = null;
|
|
2470
|
+
const getDb = () => pluginDb;
|
|
2471
|
+
const withPluginDb = createDbWrapper(getDb, pluginName);
|
|
2472
|
+
const sanitizedConfig = sanitizeConfigForPlugins(config);
|
|
2473
|
+
const module = {
|
|
2474
|
+
name: pluginName,
|
|
2475
|
+
version: pluginVersion,
|
|
2476
|
+
// Store event hooks from plugin exports
|
|
2477
|
+
onMessage: typeof raw.onMessage === "function" ? raw.onMessage : void 0,
|
|
2478
|
+
onCallbackQuery: typeof raw.onCallbackQuery === "function" ? raw.onCallbackQuery : void 0,
|
|
2479
|
+
configure() {
|
|
2480
|
+
},
|
|
2481
|
+
migrate() {
|
|
2482
|
+
try {
|
|
2483
|
+
const dbPath = join4(PLUGIN_DATA_DIR, `${pluginName}.db`);
|
|
2484
|
+
pluginDb = openModuleDb(dbPath);
|
|
2485
|
+
if (hasMigrate) {
|
|
2486
|
+
raw.migrate(pluginDb);
|
|
2487
|
+
const pluginTables = pluginDb.prepare(
|
|
2488
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`
|
|
2489
|
+
).all().map((t) => t.name).filter((n) => n !== "_kv");
|
|
2490
|
+
if (pluginTables.length > 0) {
|
|
2491
|
+
migrateFromMainDb(pluginDb, pluginTables);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
console.error(
|
|
2496
|
+
`\u274C [${pluginName}] migrate() failed:`,
|
|
2497
|
+
err instanceof Error ? err.message : err
|
|
2498
|
+
);
|
|
2499
|
+
if (pluginDb) {
|
|
2500
|
+
try {
|
|
2501
|
+
pluginDb.close();
|
|
2502
|
+
} catch {
|
|
2503
|
+
}
|
|
2504
|
+
pluginDb = null;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
},
|
|
2508
|
+
tools() {
|
|
2509
|
+
try {
|
|
2510
|
+
let toolDefs;
|
|
2511
|
+
if (typeof raw.tools === "function") {
|
|
2512
|
+
const sdk = createPluginSDK(sdkDeps, {
|
|
2513
|
+
pluginName,
|
|
2514
|
+
db: pluginDb,
|
|
2515
|
+
sanitizedConfig,
|
|
2516
|
+
pluginConfig
|
|
2517
|
+
});
|
|
2518
|
+
toolDefs = raw.tools(sdk);
|
|
2519
|
+
} else if (Array.isArray(raw.tools)) {
|
|
2520
|
+
toolDefs = raw.tools;
|
|
2521
|
+
} else {
|
|
2522
|
+
return [];
|
|
2523
|
+
}
|
|
2524
|
+
const validDefs = validateToolDefs(toolDefs, pluginName);
|
|
2525
|
+
return validDefs.map((def) => {
|
|
2526
|
+
const rawExecutor = def.execute;
|
|
2527
|
+
const sandboxedExecutor = (params, context) => {
|
|
2528
|
+
const sanitizedContext = {
|
|
2529
|
+
...context,
|
|
2530
|
+
config: context.config ? sanitizeConfigForPlugins(context.config) : void 0
|
|
2531
|
+
};
|
|
2532
|
+
return rawExecutor(params, sanitizedContext);
|
|
2533
|
+
};
|
|
2534
|
+
return {
|
|
2535
|
+
tool: {
|
|
2536
|
+
name: def.name,
|
|
2537
|
+
description: def.description,
|
|
2538
|
+
parameters: def.parameters || {
|
|
2539
|
+
type: "object",
|
|
2540
|
+
properties: {}
|
|
2541
|
+
},
|
|
2542
|
+
...def.category ? { category: def.category } : {}
|
|
2543
|
+
},
|
|
2544
|
+
executor: pluginDb ? withPluginDb(sandboxedExecutor) : sandboxedExecutor,
|
|
2545
|
+
scope: def.scope
|
|
2546
|
+
};
|
|
2547
|
+
});
|
|
2548
|
+
} catch (err) {
|
|
2549
|
+
console.error(
|
|
2550
|
+
`\u274C [${pluginName}] tools() failed:`,
|
|
2551
|
+
err instanceof Error ? err.message : err
|
|
2552
|
+
);
|
|
2553
|
+
return [];
|
|
2554
|
+
}
|
|
2555
|
+
},
|
|
2556
|
+
async start(context) {
|
|
2557
|
+
if (!raw.start) return;
|
|
2558
|
+
try {
|
|
2559
|
+
const enhancedContext = {
|
|
2560
|
+
bridge: context.bridge,
|
|
2561
|
+
db: pluginDb ?? null,
|
|
2562
|
+
config: sanitizedConfig,
|
|
2563
|
+
pluginConfig,
|
|
2564
|
+
log
|
|
2565
|
+
};
|
|
2566
|
+
await raw.start(enhancedContext);
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
console.error(
|
|
2569
|
+
`\u274C [${pluginName}] start() failed:`,
|
|
2570
|
+
err instanceof Error ? err.message : err
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
},
|
|
2574
|
+
async stop() {
|
|
2575
|
+
try {
|
|
2576
|
+
await raw.stop?.();
|
|
2577
|
+
} catch (err) {
|
|
2578
|
+
console.error(
|
|
2579
|
+
`\u274C [${pluginName}] stop() failed:`,
|
|
2580
|
+
err instanceof Error ? err.message : err
|
|
2581
|
+
);
|
|
2582
|
+
} finally {
|
|
2583
|
+
if (pluginDb) {
|
|
2584
|
+
try {
|
|
2585
|
+
pluginDb.close();
|
|
2586
|
+
} catch {
|
|
2587
|
+
}
|
|
2588
|
+
pluginDb = null;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
return module;
|
|
2594
|
+
}
|
|
2595
|
+
async function ensurePluginDeps(pluginDir, pluginEntry) {
|
|
2596
|
+
const pkgJson = join4(pluginDir, "package.json");
|
|
2597
|
+
const lockfile = join4(pluginDir, "package-lock.json");
|
|
2598
|
+
const nodeModules = join4(pluginDir, "node_modules");
|
|
2599
|
+
if (!existsSync4(pkgJson)) return;
|
|
2600
|
+
if (!existsSync4(lockfile)) {
|
|
2601
|
+
console.warn(
|
|
2602
|
+
`\u26A0\uFE0F [${pluginEntry}] package.json without package-lock.json \u2014 skipping (lockfile required)`
|
|
2603
|
+
);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
if (existsSync4(nodeModules)) {
|
|
2607
|
+
const marker = join4(nodeModules, ".package-lock.json");
|
|
2608
|
+
if (existsSync4(marker) && statSync(marker).mtimeMs >= statSync(lockfile).mtimeMs) return;
|
|
2609
|
+
}
|
|
2610
|
+
console.log(`\u{1F4E6} [${pluginEntry}] Installing dependencies...`);
|
|
2611
|
+
try {
|
|
2612
|
+
await execFileAsync("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
2613
|
+
cwd: pluginDir,
|
|
2614
|
+
timeout: 6e4,
|
|
2615
|
+
env: { ...process.env, NODE_ENV: "production" }
|
|
2616
|
+
});
|
|
2617
|
+
console.log(`\u{1F4E6} [${pluginEntry}] Dependencies installed`);
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
console.error(`\u274C [${pluginEntry}] Failed to install deps: ${String(err).slice(0, 300)}`);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
async function loadEnhancedPlugins(config, loadedModuleNames, sdkDeps) {
|
|
2623
|
+
const pluginsDir = WORKSPACE_PATHS.PLUGINS_DIR;
|
|
2624
|
+
if (!existsSync4(pluginsDir)) {
|
|
2625
|
+
return [];
|
|
2626
|
+
}
|
|
2627
|
+
const entries = readdirSync(pluginsDir);
|
|
2628
|
+
const modules = [];
|
|
2629
|
+
const loadedNames = /* @__PURE__ */ new Set();
|
|
2630
|
+
const pluginPaths = [];
|
|
2631
|
+
for (const entry of entries) {
|
|
2632
|
+
if (entry === "data") continue;
|
|
2633
|
+
const entryPath = join4(pluginsDir, entry);
|
|
2634
|
+
let modulePath = null;
|
|
2635
|
+
try {
|
|
2636
|
+
const stat = statSync(entryPath);
|
|
2637
|
+
if (stat.isFile() && entry.endsWith(".js")) {
|
|
2638
|
+
modulePath = entryPath;
|
|
2639
|
+
} else if (stat.isDirectory()) {
|
|
2640
|
+
const indexPath = join4(entryPath, "index.js");
|
|
2641
|
+
if (existsSync4(indexPath)) {
|
|
2642
|
+
modulePath = indexPath;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
} catch {
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
if (modulePath) {
|
|
2649
|
+
pluginPaths.push({ entry, path: modulePath });
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
await Promise.allSettled(
|
|
2653
|
+
pluginPaths.filter(({ path }) => path.endsWith("index.js")).map(({ entry }) => ensurePluginDeps(join4(pluginsDir, entry), entry))
|
|
2654
|
+
);
|
|
2655
|
+
const loadResults = await Promise.allSettled(
|
|
2656
|
+
pluginPaths.map(async ({ entry, path }) => {
|
|
2657
|
+
const moduleUrl = pathToFileURL(path).href;
|
|
2658
|
+
const mod = await import(moduleUrl);
|
|
2659
|
+
return { entry, mod };
|
|
2660
|
+
})
|
|
2661
|
+
);
|
|
2662
|
+
for (const result of loadResults) {
|
|
2663
|
+
if (result.status === "rejected") {
|
|
2664
|
+
console.error(
|
|
2665
|
+
`\u274C Plugin failed to load:`,
|
|
2666
|
+
result.reason instanceof Error ? result.reason.message : result.reason
|
|
2667
|
+
);
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
const { entry, mod } = result.value;
|
|
2671
|
+
try {
|
|
2672
|
+
if (!mod.tools || typeof mod.tools !== "function" && !Array.isArray(mod.tools)) {
|
|
2673
|
+
console.warn(`\u26A0\uFE0F Plugin "${entry}": no 'tools' array or function exported, skipping`);
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
const adapted = adaptPlugin(mod, entry, config, loadedModuleNames, sdkDeps);
|
|
2677
|
+
if (loadedNames.has(adapted.name)) {
|
|
2678
|
+
console.warn(
|
|
2679
|
+
`\u26A0\uFE0F Plugin "${adapted.name}" already loaded, skipping duplicate from "${entry}"`
|
|
2680
|
+
);
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
loadedNames.add(adapted.name);
|
|
2684
|
+
modules.push(adapted);
|
|
2685
|
+
} catch (err) {
|
|
2686
|
+
console.error(
|
|
2687
|
+
`\u274C Plugin "${entry}" failed to adapt:`,
|
|
2688
|
+
err instanceof Error ? err.message : err
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
return modules;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// src/workspace/validator.ts
|
|
2696
|
+
import { existsSync as existsSync5, lstatSync, readdirSync as readdirSync2 } from "fs";
|
|
2697
|
+
import { resolve, normalize, relative, extname, basename } from "path";
|
|
2698
|
+
import { homedir as homedir2 } from "os";
|
|
2699
|
+
var WorkspaceSecurityError = class extends Error {
|
|
2700
|
+
constructor(message, attemptedPath) {
|
|
2701
|
+
super(message);
|
|
2702
|
+
this.attemptedPath = attemptedPath;
|
|
2703
|
+
this.name = "WorkspaceSecurityError";
|
|
2704
|
+
}
|
|
2705
|
+
};
|
|
2706
|
+
function decodeRecursive(str) {
|
|
2707
|
+
let decoded = str;
|
|
2708
|
+
let prev = "";
|
|
2709
|
+
let iterations = 0;
|
|
2710
|
+
const maxIterations = 10;
|
|
2711
|
+
while (decoded !== prev && iterations < maxIterations) {
|
|
2712
|
+
prev = decoded;
|
|
2713
|
+
try {
|
|
2714
|
+
decoded = decodeURIComponent(decoded);
|
|
2715
|
+
} catch {
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
iterations++;
|
|
2719
|
+
}
|
|
2720
|
+
return decoded;
|
|
2721
|
+
}
|
|
2722
|
+
function validatePath(inputPath, allowCreate = false) {
|
|
2723
|
+
if (!inputPath || inputPath.trim() === "") {
|
|
2724
|
+
throw new WorkspaceSecurityError("Path cannot be empty.", inputPath);
|
|
2725
|
+
}
|
|
2726
|
+
const trimmedPath = inputPath.trim().replace(/\\/g, "/");
|
|
2727
|
+
const decodedPath = decodeRecursive(trimmedPath);
|
|
2728
|
+
let absolutePath;
|
|
2729
|
+
if (decodedPath.startsWith("/")) {
|
|
2730
|
+
absolutePath = resolve(normalize(decodedPath));
|
|
2731
|
+
} else if (decodedPath.startsWith("~/")) {
|
|
2732
|
+
const expanded = decodedPath.replace(/^~(?=$|[\\/])/, homedir2());
|
|
2733
|
+
absolutePath = resolve(expanded);
|
|
2734
|
+
} else {
|
|
2735
|
+
absolutePath = resolve(WORKSPACE_ROOT, normalize(decodedPath));
|
|
2736
|
+
}
|
|
2737
|
+
const relativePath = relative(WORKSPACE_ROOT, absolutePath);
|
|
2738
|
+
if (relativePath.startsWith("..") || relativePath.startsWith("/")) {
|
|
2739
|
+
throw new WorkspaceSecurityError(
|
|
2740
|
+
`Access denied: Path '${inputPath}' is outside the workspace. Only files in ~/.teleton/workspace/ are accessible.`,
|
|
2741
|
+
inputPath
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
const exists = existsSync5(absolutePath);
|
|
2745
|
+
if (!exists && !allowCreate) {
|
|
2746
|
+
throw new WorkspaceSecurityError(
|
|
2747
|
+
`File not found: '${inputPath}' does not exist in workspace.`,
|
|
2748
|
+
inputPath
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (exists) {
|
|
2752
|
+
const stats = lstatSync(absolutePath);
|
|
2753
|
+
if (stats.isSymbolicLink()) {
|
|
2754
|
+
throw new WorkspaceSecurityError(
|
|
2755
|
+
`Access denied: Symbolic links are not allowed for security reasons.`,
|
|
2756
|
+
inputPath
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
return {
|
|
2761
|
+
absolutePath,
|
|
2762
|
+
relativePath,
|
|
2763
|
+
exists,
|
|
2764
|
+
isDirectory: exists ? lstatSync(absolutePath).isDirectory() : false,
|
|
2765
|
+
extension: extname(absolutePath).toLowerCase(),
|
|
2766
|
+
filename: basename(absolutePath)
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
function validateReadPath(inputPath) {
|
|
2770
|
+
const validated = validatePath(inputPath, false);
|
|
2771
|
+
if (validated.isDirectory) {
|
|
2772
|
+
throw new WorkspaceSecurityError(`Cannot read directory as file: '${inputPath}'`, inputPath);
|
|
2773
|
+
}
|
|
2774
|
+
return validated;
|
|
2775
|
+
}
|
|
2776
|
+
var IMMUTABLE_FILES = ["SOUL.md", "STRATEGY.md", "SECURITY.md"];
|
|
2777
|
+
function validateWritePath(inputPath, fileType) {
|
|
2778
|
+
const validated = validatePath(inputPath, true);
|
|
2779
|
+
if (IMMUTABLE_FILES.includes(validated.filename)) {
|
|
2780
|
+
throw new WorkspaceSecurityError(
|
|
2781
|
+
`Cannot write to ${validated.filename}. This file is configured by the owner. Use memory_write instead.`,
|
|
2782
|
+
inputPath
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
if (fileType && ALLOWED_EXTENSIONS[fileType]) {
|
|
2786
|
+
const allowedExts = ALLOWED_EXTENSIONS[fileType];
|
|
2787
|
+
if (!allowedExts.includes(validated.extension)) {
|
|
2788
|
+
throw new WorkspaceSecurityError(
|
|
2789
|
+
`Invalid file type: '${validated.extension}' is not allowed for ${fileType}. Allowed: ${allowedExts.join(", ")}`,
|
|
2790
|
+
inputPath
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return validated;
|
|
2795
|
+
}
|
|
2796
|
+
function validateDirectory(inputPath) {
|
|
2797
|
+
const validated = validatePath(inputPath, true);
|
|
2798
|
+
if (validated.exists && !validated.isDirectory) {
|
|
2799
|
+
throw new WorkspaceSecurityError(
|
|
2800
|
+
`Path exists but is not a directory: '${inputPath}'`,
|
|
2801
|
+
inputPath
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
return validated;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
export {
|
|
2808
|
+
DealsConfigSchema,
|
|
2809
|
+
ConfigSchema,
|
|
2810
|
+
getProviderMetadata,
|
|
2811
|
+
getSupportedProviders,
|
|
2812
|
+
validateApiKeyFormat,
|
|
2813
|
+
expandPath,
|
|
2814
|
+
loadConfig,
|
|
2815
|
+
configExists,
|
|
2816
|
+
getDefaultConfigPath,
|
|
2817
|
+
WorkspaceSecurityError,
|
|
2818
|
+
validatePath,
|
|
2819
|
+
validateReadPath,
|
|
2820
|
+
validateWritePath,
|
|
2821
|
+
validateDirectory,
|
|
2822
|
+
generateWallet,
|
|
2823
|
+
saveWallet,
|
|
2824
|
+
loadWallet,
|
|
2825
|
+
walletExists,
|
|
2826
|
+
importWallet,
|
|
2827
|
+
getWalletAddress,
|
|
2828
|
+
getKeyPair,
|
|
2829
|
+
getWalletBalance,
|
|
2830
|
+
getTonPrice,
|
|
2831
|
+
writePluginSecret,
|
|
2832
|
+
deletePluginSecret,
|
|
2833
|
+
listPluginSecretKeys,
|
|
2834
|
+
withBlockchainRetry,
|
|
2835
|
+
sendTon,
|
|
2836
|
+
adaptPlugin,
|
|
2837
|
+
ensurePluginDeps,
|
|
2838
|
+
loadEnhancedPlugins
|
|
2839
|
+
};
|