telegram-opencode-bridge-bot 0.1.0__py3-none-any.whl
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.
- handlers/__init__.py +1 -0
- handlers/commands.py +943 -0
- handlers/messages.py +482 -0
- opencode/__init__.py +8 -0
- opencode/client.py +443 -0
- opencode/server.py +144 -0
- sessions/__init__.py +1 -0
- sessions/manager.py +342 -0
- telegram_opencode_bridge_bot-0.1.0.dist-info/METADATA +156 -0
- telegram_opencode_bridge_bot-0.1.0.dist-info/RECORD +16 -0
- telegram_opencode_bridge_bot-0.1.0.dist-info/WHEEL +5 -0
- telegram_opencode_bridge_bot-0.1.0.dist-info/entry_points.txt +2 -0
- telegram_opencode_bridge_bot-0.1.0.dist-info/top_level.txt +4 -0
- utils/__init__.py +1 -0
- utils/formatting.py +218 -0
- utils/security.py +115 -0
sessions/manager.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import aiosqlite
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SessionManager:
|
|
10
|
+
"""Manages OpenCode sessions per Telegram user with SQLite persistence."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, db_path: str = "sessions.db"):
|
|
13
|
+
self.db_path = db_path
|
|
14
|
+
self._db: Optional[aiosqlite.Connection] = None
|
|
15
|
+
# In-memory cache for fast lookups
|
|
16
|
+
self._active_sessions: Dict[int, Dict[str, Any]] = {}
|
|
17
|
+
|
|
18
|
+
async def initialize(self) -> None:
|
|
19
|
+
"""Initialize the database and create tables if needed."""
|
|
20
|
+
self._db = await aiosqlite.connect(self.db_path)
|
|
21
|
+
|
|
22
|
+
# 1. Create sessions table with work_dir support
|
|
23
|
+
await self._db.execute("""
|
|
24
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
user_id INTEGER NOT NULL,
|
|
27
|
+
opencode_session_id TEXT NOT NULL,
|
|
28
|
+
model TEXT DEFAULT '',
|
|
29
|
+
mode TEXT DEFAULT 'build',
|
|
30
|
+
message_count INTEGER DEFAULT 0,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
last_active TEXT NOT NULL,
|
|
33
|
+
is_active INTEGER DEFAULT 1,
|
|
34
|
+
name TEXT DEFAULT '',
|
|
35
|
+
work_dir TEXT DEFAULT ''
|
|
36
|
+
)
|
|
37
|
+
""")
|
|
38
|
+
|
|
39
|
+
# 2. Create user settings table for preferred workspace directories and scan depth
|
|
40
|
+
await self._db.execute("""
|
|
41
|
+
CREATE TABLE IF NOT EXISTS user_settings (
|
|
42
|
+
user_id INTEGER PRIMARY KEY,
|
|
43
|
+
work_dir TEXT NOT NULL,
|
|
44
|
+
scan_depth INTEGER DEFAULT 2
|
|
45
|
+
)
|
|
46
|
+
""")
|
|
47
|
+
|
|
48
|
+
# 3. Safely migrate existing databases to add columns if missing
|
|
49
|
+
try:
|
|
50
|
+
await self._db.execute("ALTER TABLE sessions ADD COLUMN name TEXT DEFAULT ''")
|
|
51
|
+
except aiosqlite.OperationalError:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
await self._db.execute("ALTER TABLE sessions ADD COLUMN work_dir TEXT DEFAULT ''")
|
|
56
|
+
except aiosqlite.OperationalError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
await self._db.execute("ALTER TABLE user_settings ADD COLUMN scan_depth INTEGER DEFAULT 2")
|
|
61
|
+
except aiosqlite.OperationalError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
await self._db.execute("ALTER TABLE user_settings ADD COLUMN streaming INTEGER DEFAULT 0")
|
|
66
|
+
except aiosqlite.OperationalError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
await self._db.execute("""
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_user_active
|
|
71
|
+
ON sessions(user_id, is_active)
|
|
72
|
+
""")
|
|
73
|
+
await self._db.commit()
|
|
74
|
+
|
|
75
|
+
# 4. Load active sessions into memory
|
|
76
|
+
async with self._db.execute(
|
|
77
|
+
"SELECT user_id, opencode_session_id, model, mode, message_count, created_at, last_active, name, work_dir "
|
|
78
|
+
"FROM sessions WHERE is_active = 1"
|
|
79
|
+
) as cursor:
|
|
80
|
+
async for row in cursor:
|
|
81
|
+
self._active_sessions[row[0]] = {
|
|
82
|
+
"session_id": row[1],
|
|
83
|
+
"model": row[2],
|
|
84
|
+
"mode": row[3],
|
|
85
|
+
"message_count": row[4],
|
|
86
|
+
"created_at": row[5],
|
|
87
|
+
"last_active": row[6],
|
|
88
|
+
"name": row[7] if len(row) > 7 else "",
|
|
89
|
+
"work_dir": row[8] if len(row) > 8 else "",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logger.info(f"Session manager initialized. {len(self._active_sessions)} active sessions loaded.")
|
|
93
|
+
|
|
94
|
+
async def get_active_session(self, user_id: int) -> Optional[str]:
|
|
95
|
+
"""Get the active OpenCode session ID for a user, or None."""
|
|
96
|
+
session = self._active_sessions.get(user_id)
|
|
97
|
+
return session["session_id"] if session else None
|
|
98
|
+
|
|
99
|
+
async def get_session_info(self, user_id: int) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""Get full session info for a user."""
|
|
101
|
+
return self._active_sessions.get(user_id)
|
|
102
|
+
|
|
103
|
+
async def set_active_session(
|
|
104
|
+
self, user_id: int, opencode_session_id: str, model: str = "", work_dir: str = ""
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Set or create an active session for a user."""
|
|
107
|
+
now = datetime.utcnow().isoformat()
|
|
108
|
+
|
|
109
|
+
# Deactivate any existing active session
|
|
110
|
+
if user_id in self._active_sessions:
|
|
111
|
+
await self._db.execute(
|
|
112
|
+
"UPDATE sessions SET is_active = 0, last_active = ? WHERE user_id = ? AND is_active = 1",
|
|
113
|
+
(now, user_id)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Insert new active session
|
|
117
|
+
await self._db.execute(
|
|
118
|
+
"INSERT INTO sessions (user_id, opencode_session_id, model, created_at, last_active, name, work_dir) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
119
|
+
(user_id, opencode_session_id, model, now, now, "", work_dir)
|
|
120
|
+
)
|
|
121
|
+
await self._db.commit()
|
|
122
|
+
|
|
123
|
+
# Update cache
|
|
124
|
+
self._active_sessions[user_id] = {
|
|
125
|
+
"session_id": opencode_session_id,
|
|
126
|
+
"model": model,
|
|
127
|
+
"mode": "build",
|
|
128
|
+
"message_count": 0,
|
|
129
|
+
"created_at": now,
|
|
130
|
+
"last_active": now,
|
|
131
|
+
"name": "",
|
|
132
|
+
"work_dir": work_dir,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logger.info(f"New session for user {user_id}: {opencode_session_id}")
|
|
136
|
+
|
|
137
|
+
async def increment_message_count(self, user_id: int, prompt: Optional[str] = None) -> None:
|
|
138
|
+
"""Increment the message count for a user's active session and optionally set name."""
|
|
139
|
+
if user_id in self._active_sessions:
|
|
140
|
+
self._active_sessions[user_id]["message_count"] += 1
|
|
141
|
+
now = datetime.utcnow().isoformat()
|
|
142
|
+
self._active_sessions[user_id]["last_active"] = now
|
|
143
|
+
|
|
144
|
+
name_to_set = None
|
|
145
|
+
if prompt and (not self._active_sessions[user_id].get("name") or self._active_sessions[user_id]["message_count"] == 1):
|
|
146
|
+
# Clean up prompt for a nice title (strip newlines/spaces)
|
|
147
|
+
clean_prompt = " ".join(prompt.split())
|
|
148
|
+
short_name = clean_prompt[:35] + "..." if len(clean_prompt) > 35 else clean_prompt
|
|
149
|
+
self._active_sessions[user_id]["name"] = short_name
|
|
150
|
+
name_to_set = short_name
|
|
151
|
+
|
|
152
|
+
if name_to_set:
|
|
153
|
+
await self._db.execute(
|
|
154
|
+
"UPDATE sessions SET message_count = message_count + 1, last_active = ?, name = ? "
|
|
155
|
+
"WHERE user_id = ? AND is_active = 1",
|
|
156
|
+
(now, name_to_set, user_id)
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
await self._db.execute(
|
|
160
|
+
"UPDATE sessions SET message_count = message_count + 1, last_active = ? "
|
|
161
|
+
"WHERE user_id = ? AND is_active = 1",
|
|
162
|
+
(now, user_id)
|
|
163
|
+
)
|
|
164
|
+
await self._db.commit()
|
|
165
|
+
|
|
166
|
+
async def set_mode(self, user_id: int, mode: str) -> None:
|
|
167
|
+
"""Set the mode (build/plan) for a user's active session."""
|
|
168
|
+
if user_id in self._active_sessions:
|
|
169
|
+
self._active_sessions[user_id]["mode"] = mode
|
|
170
|
+
await self._db.execute(
|
|
171
|
+
"UPDATE sessions SET mode = ? WHERE user_id = ? AND is_active = 1",
|
|
172
|
+
(mode, user_id)
|
|
173
|
+
)
|
|
174
|
+
await self._db.commit()
|
|
175
|
+
|
|
176
|
+
async def set_model(self, user_id: int, model: str) -> None:
|
|
177
|
+
"""Set the model for a user's active session."""
|
|
178
|
+
if user_id in self._active_sessions:
|
|
179
|
+
self._active_sessions[user_id]["model"] = model
|
|
180
|
+
await self._db.execute(
|
|
181
|
+
"UPDATE sessions SET model = ? WHERE user_id = ? AND is_active = 1",
|
|
182
|
+
(model, user_id)
|
|
183
|
+
)
|
|
184
|
+
await self._db.commit()
|
|
185
|
+
|
|
186
|
+
async def clear_session(self, user_id: int) -> None:
|
|
187
|
+
"""Deactivate the current session for a user (they'll get a new one on next message)."""
|
|
188
|
+
if user_id in self._active_sessions:
|
|
189
|
+
now = datetime.utcnow().isoformat()
|
|
190
|
+
await self._db.execute(
|
|
191
|
+
"UPDATE sessions SET is_active = 0, last_active = ? WHERE user_id = ? AND is_active = 1",
|
|
192
|
+
(now, user_id)
|
|
193
|
+
)
|
|
194
|
+
await self._db.commit()
|
|
195
|
+
del self._active_sessions[user_id]
|
|
196
|
+
logger.info(f"Session cleared for user {user_id}")
|
|
197
|
+
|
|
198
|
+
async def list_user_sessions(self, user_id: int) -> List[Dict[str, Any]]:
|
|
199
|
+
"""List all sessions (active and archived) for a user."""
|
|
200
|
+
sessions = []
|
|
201
|
+
async with self._db.execute(
|
|
202
|
+
"SELECT opencode_session_id, model, mode, message_count, created_at, last_active, is_active, name, work_dir "
|
|
203
|
+
"FROM sessions WHERE user_id = ? ORDER BY created_at DESC LIMIT 20",
|
|
204
|
+
(user_id,)
|
|
205
|
+
) as cursor:
|
|
206
|
+
async for row in cursor:
|
|
207
|
+
sessions.append({
|
|
208
|
+
"session_id": row[0],
|
|
209
|
+
"model": row[1],
|
|
210
|
+
"mode": row[2],
|
|
211
|
+
"message_count": row[3],
|
|
212
|
+
"created_at": row[4],
|
|
213
|
+
"last_active": row[5],
|
|
214
|
+
"is_active": bool(row[6]),
|
|
215
|
+
"name": row[7] if len(row) > 7 else "",
|
|
216
|
+
"work_dir": row[8] if len(row) > 8 else "",
|
|
217
|
+
})
|
|
218
|
+
return sessions
|
|
219
|
+
|
|
220
|
+
async def switch_session(self, user_id: int, session_id: str) -> bool:
|
|
221
|
+
"""Switch a user to a different existing session."""
|
|
222
|
+
# Check if session exists for this user
|
|
223
|
+
async with self._db.execute(
|
|
224
|
+
"SELECT opencode_session_id, model, mode, message_count, created_at, name, work_dir "
|
|
225
|
+
"FROM sessions WHERE user_id = ? AND opencode_session_id = ?",
|
|
226
|
+
(user_id, session_id)
|
|
227
|
+
) as cursor:
|
|
228
|
+
row = await cursor.fetchone()
|
|
229
|
+
if not row:
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
now = datetime.utcnow().isoformat()
|
|
233
|
+
|
|
234
|
+
# Deactivate current
|
|
235
|
+
await self._db.execute(
|
|
236
|
+
"UPDATE sessions SET is_active = 0 WHERE user_id = ? AND is_active = 1",
|
|
237
|
+
(user_id,)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Activate target
|
|
241
|
+
await self._db.execute(
|
|
242
|
+
"UPDATE sessions SET is_active = 1, last_active = ? WHERE user_id = ? AND opencode_session_id = ?",
|
|
243
|
+
(now, user_id, session_id)
|
|
244
|
+
)
|
|
245
|
+
await self._db.commit()
|
|
246
|
+
|
|
247
|
+
# Update cache
|
|
248
|
+
self._active_sessions[user_id] = {
|
|
249
|
+
"session_id": row[0],
|
|
250
|
+
"model": row[1],
|
|
251
|
+
"mode": row[2],
|
|
252
|
+
"message_count": row[3],
|
|
253
|
+
"created_at": row[4],
|
|
254
|
+
"last_active": now,
|
|
255
|
+
"name": row[5] or "",
|
|
256
|
+
"work_dir": row[6] or "",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
logger.info(f"User {user_id} switched to session {session_id}")
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
async def get_user_work_dir(self, user_id: int, default_dir: str) -> str:
|
|
263
|
+
"""Get the active workspace/project directory for a specific user, falling back to default."""
|
|
264
|
+
async with self._db.execute(
|
|
265
|
+
"SELECT work_dir FROM user_settings WHERE user_id = ?",
|
|
266
|
+
(user_id,)
|
|
267
|
+
) as cursor:
|
|
268
|
+
row = await cursor.fetchone()
|
|
269
|
+
if row and row[0]:
|
|
270
|
+
return row[0]
|
|
271
|
+
return default_dir
|
|
272
|
+
|
|
273
|
+
async def set_user_work_dir(self, user_id: int, work_dir: str) -> None:
|
|
274
|
+
"""Save or update the preferred workspace directory for a user, preserving scan_depth."""
|
|
275
|
+
await self._db.execute(
|
|
276
|
+
"INSERT INTO user_settings (user_id, work_dir, scan_depth) VALUES (?, ?, 2) "
|
|
277
|
+
"ON CONFLICT(user_id) DO UPDATE SET work_dir = excluded.work_dir",
|
|
278
|
+
(user_id, work_dir)
|
|
279
|
+
)
|
|
280
|
+
await self._db.commit()
|
|
281
|
+
logger.info(f"Set preferred project directory for user {user_id}: {work_dir}")
|
|
282
|
+
|
|
283
|
+
async def get_user_scan_depth(self, user_id: int, default_depth: int = 2) -> int:
|
|
284
|
+
"""Get the preferred recursive scan depth for a specific user, falling back to default."""
|
|
285
|
+
async with self._db.execute(
|
|
286
|
+
"SELECT scan_depth FROM user_settings WHERE user_id = ?",
|
|
287
|
+
(user_id,)
|
|
288
|
+
) as cursor:
|
|
289
|
+
row = await cursor.fetchone()
|
|
290
|
+
if row and row[0] is not None:
|
|
291
|
+
return int(row[0])
|
|
292
|
+
return default_depth
|
|
293
|
+
|
|
294
|
+
async def set_user_scan_depth(self, user_id: int, depth: int) -> None:
|
|
295
|
+
"""Save or update the preferred recursive scan depth for a user, preserving work_dir."""
|
|
296
|
+
await self._db.execute(
|
|
297
|
+
"INSERT INTO user_settings (user_id, work_dir, scan_depth) VALUES (?, '', ?) "
|
|
298
|
+
"ON CONFLICT(user_id) DO UPDATE SET scan_depth = excluded.scan_depth",
|
|
299
|
+
(user_id, depth)
|
|
300
|
+
)
|
|
301
|
+
await self._db.commit()
|
|
302
|
+
logger.info(f"Set preferred project scan depth for user {user_id}: {depth}")
|
|
303
|
+
|
|
304
|
+
async def get_user_streaming(self, user_id: int, default_val: int = 0) -> int:
|
|
305
|
+
"""Get the user's streaming preference (0 = disabled, 1 = enabled)."""
|
|
306
|
+
async with self._db.execute(
|
|
307
|
+
"SELECT streaming FROM user_settings WHERE user_id = ?",
|
|
308
|
+
(user_id,)
|
|
309
|
+
) as cursor:
|
|
310
|
+
row = await cursor.fetchone()
|
|
311
|
+
if row and row[0] is not None:
|
|
312
|
+
return int(row[0])
|
|
313
|
+
return default_val
|
|
314
|
+
|
|
315
|
+
async def set_user_streaming(self, user_id: int, enabled: int) -> None:
|
|
316
|
+
"""Save or update the user's streaming preference."""
|
|
317
|
+
await self._db.execute(
|
|
318
|
+
"INSERT INTO user_settings (user_id, work_dir, scan_depth, streaming) VALUES (?, '', 2, ?) "
|
|
319
|
+
"ON CONFLICT(user_id) DO UPDATE SET streaming = excluded.streaming",
|
|
320
|
+
(user_id, enabled)
|
|
321
|
+
)
|
|
322
|
+
await self._db.commit()
|
|
323
|
+
logger.info(f"Set preferred streaming for user {user_id}: {enabled}")
|
|
324
|
+
|
|
325
|
+
async def get_last_session_in_workspace(self, user_id: int, work_dir: str) -> Optional[str]:
|
|
326
|
+
"""Find the most recently active session ID for a user in a specific workspace."""
|
|
327
|
+
async with self._db.execute(
|
|
328
|
+
"SELECT opencode_session_id FROM sessions "
|
|
329
|
+
"WHERE user_id = ? AND work_dir = ? "
|
|
330
|
+
"ORDER BY last_active DESC LIMIT 1",
|
|
331
|
+
(user_id, work_dir)
|
|
332
|
+
) as cursor:
|
|
333
|
+
row = await cursor.fetchone()
|
|
334
|
+
if row:
|
|
335
|
+
return row[0]
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
async def close(self) -> None:
|
|
339
|
+
"""Close the database connection."""
|
|
340
|
+
if self._db:
|
|
341
|
+
await self._db.close()
|
|
342
|
+
logger.info("Session manager closed.")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: telegram-opencode-bridge-bot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Telegram bot that bridges messages directly to OpenCode — an AI coding agent running on your machine
|
|
5
|
+
Author-email: MaheshNagabhairava <your@email.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/MaheshNagabhairava/telegram-opencode-bridge-bot
|
|
8
|
+
Keywords: telegram,bot,opencode,ai,coding,bridge
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: python-telegram-bot[ext]>=21.0
|
|
14
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
16
|
+
Requires-Dist: aiosqlite>=0.19.0
|
|
17
|
+
|
|
18
|
+
# 🤖 Telegram → OpenCode Bridge Bot
|
|
19
|
+
|
|
20
|
+
A lightweight Python bot that bridges your Telegram messages directly to [OpenCode](https://opencode.ai) — an AI coding agent running on your machine. Think of it as having Claude/GPT-powered coding assistance right in your pocket via Telegram.
|
|
21
|
+
|
|
22
|
+
## ✨ Features
|
|
23
|
+
|
|
24
|
+
- **Direct OpenCode integration** — routes your messages to OpenCode's HTTP API
|
|
25
|
+
- **Persistent sessions** — conversations maintain context across messages
|
|
26
|
+
- **Session management** — create, switch, list, and share sessions
|
|
27
|
+
- **Model switching** — change AI models on the fly (`/model`)
|
|
28
|
+
- **Plan/Build modes** — toggle between read-only analysis and full execution
|
|
29
|
+
- **Smart formatting** — code blocks with syntax highlighting in Telegram
|
|
30
|
+
- **Auto message splitting** — handles responses longer than Telegram's 4096 char limit
|
|
31
|
+
- **Security** — whitelist-based access control + rate limiting
|
|
32
|
+
- **Workspace Switching** — Switching from one workspace to another
|
|
33
|
+
|
|
34
|
+
## 📋 Prerequisites
|
|
35
|
+
|
|
36
|
+
1. **Python 3.10+**
|
|
37
|
+
2. **OpenCode CLI** — install via:
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g opencode-ai
|
|
40
|
+
# or
|
|
41
|
+
curl -fsSL https://opencode.ai/install | bash
|
|
42
|
+
```
|
|
43
|
+
3. **Telegram Bot Token** — get one from [@BotFather](https://t.me/BotFather)
|
|
44
|
+
4. **Your Telegram User ID** — send `/id` to the bot after setup, or use [@userinfobot](https://t.me/userinfobot)
|
|
45
|
+
|
|
46
|
+
## 🚀 Quick Start
|
|
47
|
+
|
|
48
|
+
### 1. Clone & Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd telegram-opencode-bot
|
|
52
|
+
pip install -r requirements.txt
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Start OpenCode Server
|
|
56
|
+
|
|
57
|
+
In a separate terminal:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
opencode serve --port 4096 --hostname 127.0.0.1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This starts the OpenCode HTTP API on `localhost:4096`.
|
|
64
|
+
|
|
65
|
+
### 3. Run the Bot
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python bot.py
|
|
69
|
+
python bot.py --env (if u want to setup ur configuration later)
|
|
70
|
+
```
|
|
71
|
+
> **💡 Tip:** Don't know your Telegram user ID? Start the bot with `AUTHORIZED_USERS=0` temporarily, send `/id` to the bot, then update the `.env` with your real ID.
|
|
72
|
+
|
|
73
|
+
### 4. Chat!
|
|
74
|
+
|
|
75
|
+
Open Telegram, find your bot, and start asking! 🎉
|
|
76
|
+
|
|
77
|
+
## 📱 Commands
|
|
78
|
+
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `/start` | Welcome message & connection check |
|
|
82
|
+
| `/help` | Show all commands |
|
|
83
|
+
| `/new` | Start a fresh conversation |
|
|
84
|
+
| `/sessions` | List your recent sessions |
|
|
85
|
+
| `/switch <id>` | Switch to a different session |
|
|
86
|
+
| `/model <name>` | Change AI model(ex: /model opencode/deepseek-v4-flash-free) |
|
|
87
|
+
| `/mode <plan\|build>` | Toggle plan/build mode |
|
|
88
|
+
| `/share` | Share current session (public URL) |
|
|
89
|
+
| `/status` | Check connection & session details |
|
|
90
|
+
| `/id` | Show your Telegram user ID |
|
|
91
|
+
| `/stop` | Abort active model processing |
|
|
92
|
+
| `/models` | List all available models |
|
|
93
|
+
| `/project` | To view the current workspace, sub folder workspaces and to change the workspace (ex: /project 2)|
|
|
94
|
+
| `/project depth <1to5>` | Depth level recursive check for subfolders (ex: if u give depth 2, only 2 sub folders it will show from root) |
|
|
95
|
+
| `/enable` | To enable the streaming |
|
|
96
|
+
| `/disable` | To disable the streaming |
|
|
97
|
+
|
|
98
|
+
## 🏗️ Architecture
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Telegram User
|
|
102
|
+
│
|
|
103
|
+
▼
|
|
104
|
+
Telegram Bot (Python)
|
|
105
|
+
│
|
|
106
|
+
├──► OpenCode HTTP API (localhost:4096) ← primary
|
|
107
|
+
│
|
|
108
|
+
├──► LLM Provider (Claude/GPT/Gemini)
|
|
109
|
+
└──► Local Filesystem & Shell
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## ⚙️ Configuration
|
|
113
|
+
|
|
114
|
+
| Variable | Description | Default |
|
|
115
|
+
|----------|-------------|---------|
|
|
116
|
+
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | **Required** |
|
|
117
|
+
| `AUTHORIZED_USERS` | Comma-separated Telegram user IDs | **Required** |
|
|
118
|
+
| `OPENCODE_SERVER_URL` | OpenCode HTTP API URL | `http://localhost:4096` |
|
|
119
|
+
| `OPENCODE_SERVER_USERNAME` | OpenCode auth username | *(empty)* |
|
|
120
|
+
| `OPENCODE_SERVER_PASSWORD` | OpenCode auth password | *(empty)* |
|
|
121
|
+
| `OPENCODE_MODEL` | Default AI model | `anthropic/claude-sonnet-4` |
|
|
122
|
+
| `OPENCODE_WORK_DIR` | Working directory for OpenCode | `.` |
|
|
123
|
+
| `MAX_MESSAGE_LENGTH` | Max Telegram message length | `4000` |
|
|
124
|
+
| `RESPONSE_TIMEOUT` | Max wait for response (seconds) | `0` |
|
|
125
|
+
| `DB_PATH` | SQLite database path | `sessions.db` |
|
|
126
|
+
|
|
127
|
+
## 🔒 Security
|
|
128
|
+
|
|
129
|
+
- **User whitelist** — only Telegram user IDs in `AUTHORIZED_USERS` can use the bot
|
|
130
|
+
- **Rate limiting** — 20 requests per minute per user (configurable)
|
|
131
|
+
- **No public exposure** — designed to run on your local machine
|
|
132
|
+
|
|
133
|
+
> ⚠️ **Warning:** This bot executes AI-driven code on your machine. Only authorize trusted users.
|
|
134
|
+
|
|
135
|
+
## 📁 Project Structure
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
telegram-opencode-bot/
|
|
139
|
+
├── bot.py # Main entry point
|
|
140
|
+
├── config.py # Environment configuration
|
|
141
|
+
├── handlers/
|
|
142
|
+
│ ├── commands.py # Slash command handlers
|
|
143
|
+
│ └── messages.py # Text message → OpenCode bridge
|
|
144
|
+
├── opencode/
|
|
145
|
+
│ ├── client.py # OpenCode HTTP API client
|
|
146
|
+
│
|
|
147
|
+
├── sessions/
|
|
148
|
+
│ └── manager.py # Per-user session tracking (SQLite)
|
|
149
|
+
├── utils/
|
|
150
|
+
│ ├── formatting.py # Telegram message formatting
|
|
151
|
+
│ └── security.py # Auth, rate limiting, sanitization
|
|
152
|
+
├── .env.example # Environment template
|
|
153
|
+
├── requirements.txt # Python dependencies
|
|
154
|
+
└── README.md # This file
|
|
155
|
+
```
|
|
156
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
handlers/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
+
handlers/commands.py,sha256=maetTTDVF9L-5OwdkA-ntXKYrGWJisFKPiy3yX8oo7A,43432
|
|
3
|
+
handlers/messages.py,sha256=KHfCdUXjH5LY14bPD0oX2U1qdPNIhaJOFhVpssNpbxk,21361
|
|
4
|
+
opencode/__init__.py,sha256=N8XuBa44IAcAx8JFoAK-Sho1xczreGDaG9pj0_wwaAw,224
|
|
5
|
+
opencode/client.py,sha256=o15SXko9KOnHP_kgMExSwqAp515XsR2xMrDulqpJ8jw,16729
|
|
6
|
+
opencode/server.py,sha256=VP_BCaVPwm9X_Z7qZ_6m5LLIacMHEnlrTrwDithdyHc,5362
|
|
7
|
+
sessions/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
sessions/manager.py,sha256=7PHHTVRAjDhOOfU24ezrmq6uCwv-HIiRsQ6m_s-kknA,14833
|
|
9
|
+
utils/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
10
|
+
utils/formatting.py,sha256=UD7zvl6TjoBVP-rYtup4G73v3lTKpjE8pJHSPYIxBsA,7137
|
|
11
|
+
utils/security.py,sha256=QDa4mIaFL7mUQ8OmCfFXBq-tUzZvdraQ1lumjWfUIdY,3931
|
|
12
|
+
telegram_opencode_bridge_bot-0.1.0.dist-info/METADATA,sha256=E6t5hIGpeGjeKPO7AYWTagYP3O9ApjbFZdVXJ1Y3dsg,6061
|
|
13
|
+
telegram_opencode_bridge_bot-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
telegram_opencode_bridge_bot-0.1.0.dist-info/entry_points.txt,sha256=y4R7q1zs1w1VqiGXaF71kU_2p4lLbJzfTvnlBcvnQ10,51
|
|
15
|
+
telegram_opencode_bridge_bot-0.1.0.dist-info/top_level.txt,sha256=k31OhktT-XbhuLxDtdPxwn96ACQNqs9o3dj-I-1BfgE,33
|
|
16
|
+
telegram_opencode_bridge_bot-0.1.0.dist-info/RECORD,,
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|