@tormentalabs/opencode-telegram-plugin 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +222 -0
- package/package.json +34 -0
- package/src/bot.ts +143 -0
- package/src/config.ts +218 -0
- package/src/handlers/callbacks.ts +209 -0
- package/src/handlers/commands.ts +562 -0
- package/src/handlers/messages.ts +163 -0
- package/src/hooks/message.ts +448 -0
- package/src/hooks/permission.ts +81 -0
- package/src/hooks/session.ts +126 -0
- package/src/hooks/tool.ts +99 -0
- package/src/index.ts +395 -0
- package/src/state/mapping.ts +112 -0
- package/src/state/mode.ts +40 -0
- package/src/state/store.ts +167 -0
- package/src/utils/chunk.ts +186 -0
- package/src/utils/format.ts +120 -0
- package/src/utils/safeSend.ts +99 -0
- package/src/utils/throttle.ts +128 -0
- package/src/utils/typing.ts +30 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Context } from "grammy";
|
|
2
|
+
import { resolveCallback, getChatState } from "../state/store.js";
|
|
3
|
+
import { attachSession } from "../state/mode.js";
|
|
4
|
+
import { safeSend } from "../utils/safeSend.js";
|
|
5
|
+
import { escapeHtml } from "../utils/format.js";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Client interface — only the methods used in this file
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
interface OpenCodeClient {
|
|
12
|
+
postSessionByIdPermissionsByPermissionId(params: {
|
|
13
|
+
path: { id: string; permissionId: string };
|
|
14
|
+
body: unknown;
|
|
15
|
+
}): Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let _client: OpenCodeClient | null = null;
|
|
19
|
+
|
|
20
|
+
export function setClient(client: unknown): void {
|
|
21
|
+
_client = client as OpenCodeClient;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getClient(): OpenCodeClient {
|
|
25
|
+
if (!_client) throw new Error("OpenCode client not initialized");
|
|
26
|
+
return _client;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Safely edits the text of the message that contained the tapped button.
|
|
35
|
+
* Swallows errors (the message may already be edited or deleted).
|
|
36
|
+
*/
|
|
37
|
+
async function safeEditText(ctx: Context, text: string): Promise<void> {
|
|
38
|
+
await safeSend(() =>
|
|
39
|
+
ctx.editMessageText(text, { parse_mode: "HTML" }),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the original text of the callback message, or an empty string.
|
|
45
|
+
*/
|
|
46
|
+
function originalMessageText(ctx: Context): string {
|
|
47
|
+
return ctx.callbackQuery?.message?.text ?? "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Handler
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handles all inline-button callback queries.
|
|
56
|
+
*
|
|
57
|
+
* We always call `ctx.answerCallbackQuery()` — even on the error paths —
|
|
58
|
+
* so Telegram dismisses the loading spinner on the client side.
|
|
59
|
+
*/
|
|
60
|
+
export async function handleCallback(ctx: Context): Promise<void> {
|
|
61
|
+
const callbackData = ctx.callbackQuery?.data;
|
|
62
|
+
|
|
63
|
+
// Malformed query — nothing we can do except dismiss the spinner
|
|
64
|
+
if (!callbackData) {
|
|
65
|
+
await safeSend(() =>
|
|
66
|
+
ctx.answerCallbackQuery({ text: "Invalid callback." }),
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const chatId = ctx.chat?.id;
|
|
72
|
+
if (!chatId) {
|
|
73
|
+
await safeSend(() =>
|
|
74
|
+
ctx.answerCallbackQuery({ text: "Unknown chat." }),
|
|
75
|
+
);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolve the opaque key back to its action + data payload
|
|
80
|
+
const entry = resolveCallback(callbackData);
|
|
81
|
+
if (!entry) {
|
|
82
|
+
await safeSend(() =>
|
|
83
|
+
ctx.answerCallbackQuery({
|
|
84
|
+
text: "⏰ This button has expired. Please repeat the command.",
|
|
85
|
+
show_alert: true,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
switch (entry.action) {
|
|
93
|
+
// ----------------------------------------------------------------
|
|
94
|
+
// Permission approval
|
|
95
|
+
// ----------------------------------------------------------------
|
|
96
|
+
case "perm_approve": {
|
|
97
|
+
const { sessionId, permissionId } = entry.data;
|
|
98
|
+
if (!sessionId || !permissionId) {
|
|
99
|
+
await safeSend(() =>
|
|
100
|
+
ctx.answerCallbackQuery({ text: "Invalid callback data." }),
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await getClient().postSessionByIdPermissionsByPermissionId({
|
|
106
|
+
path: { id: sessionId, permissionId },
|
|
107
|
+
body: {},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
getChatState(chatId).pendingPermissions.delete(permissionId);
|
|
111
|
+
await safeSend(() => ctx.answerCallbackQuery({ text: "✅ Approved" }));
|
|
112
|
+
await safeEditText(
|
|
113
|
+
ctx,
|
|
114
|
+
`${escapeHtml(originalMessageText(ctx))}\n\n✅ <b>Approved</b>`,
|
|
115
|
+
);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ----------------------------------------------------------------
|
|
120
|
+
// Permission denial — notify SDK so the session doesn't hang
|
|
121
|
+
// ----------------------------------------------------------------
|
|
122
|
+
case "perm_deny": {
|
|
123
|
+
const { sessionId, permissionId } = entry.data;
|
|
124
|
+
if (!sessionId || !permissionId) {
|
|
125
|
+
await safeSend(() =>
|
|
126
|
+
ctx.answerCallbackQuery({ text: "Invalid callback data." }),
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Best-effort: call the permission endpoint to unblock the session.
|
|
132
|
+
// If the SDK has no explicit deny mechanism, this call may fail —
|
|
133
|
+
// the session will eventually time out on its own.
|
|
134
|
+
try {
|
|
135
|
+
await getClient().postSessionByIdPermissionsByPermissionId({
|
|
136
|
+
path: { id: sessionId, permissionId },
|
|
137
|
+
body: { deny: true },
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// SDK may not support explicit deny — fall through silently
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getChatState(chatId).pendingPermissions.delete(permissionId);
|
|
144
|
+
await safeSend(() => ctx.answerCallbackQuery({ text: "❌ Denied" }));
|
|
145
|
+
await safeEditText(
|
|
146
|
+
ctx,
|
|
147
|
+
`${escapeHtml(originalMessageText(ctx))}\n\n❌ <b>Denied</b>`,
|
|
148
|
+
);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ----------------------------------------------------------------
|
|
153
|
+
// Session attachment (from /attach or /switch picker)
|
|
154
|
+
// ----------------------------------------------------------------
|
|
155
|
+
case "attach_session": {
|
|
156
|
+
const { sessionId } = entry.data;
|
|
157
|
+
if (!sessionId) {
|
|
158
|
+
await safeSend(() =>
|
|
159
|
+
ctx.answerCallbackQuery({ text: "Invalid session ID." }),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
attachSession(chatId, sessionId);
|
|
165
|
+
|
|
166
|
+
await safeSend(() =>
|
|
167
|
+
ctx.answerCallbackQuery({ text: "✅ Attached!" }),
|
|
168
|
+
);
|
|
169
|
+
await safeEditText(
|
|
170
|
+
ctx,
|
|
171
|
+
`✅ Attached to session:\n<code>${escapeHtml(sessionId)}</code>`,
|
|
172
|
+
);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ----------------------------------------------------------------
|
|
177
|
+
// Model selection — reserved for future use
|
|
178
|
+
// ----------------------------------------------------------------
|
|
179
|
+
case "model_select": {
|
|
180
|
+
const { modelId } = entry.data;
|
|
181
|
+
await safeSend(() =>
|
|
182
|
+
ctx.answerCallbackQuery({
|
|
183
|
+
text: modelId ? `Selected: ${modelId}` : "Model selection coming soon.",
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ----------------------------------------------------------------
|
|
190
|
+
// Unknown action
|
|
191
|
+
// ----------------------------------------------------------------
|
|
192
|
+
default: {
|
|
193
|
+
await safeSend(() =>
|
|
194
|
+
ctx.answerCallbackQuery({ text: "Unknown action." }),
|
|
195
|
+
);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
201
|
+
// Best-effort: surface the error to the user via the callback answer
|
|
202
|
+
await safeSend(() =>
|
|
203
|
+
ctx.answerCallbackQuery({
|
|
204
|
+
text: `❌ ${msg.slice(0, 190)}`,
|
|
205
|
+
show_alert: true,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|