diff --git a/.env.example b/.env.example index 5ee2c2f..fe08fca 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Telegram Bot Token +# Telegram Bot Token (required) # Get this from @BotFather on Telegram # 1. Open Telegram and search for @BotFather # 2. Send /newbot @@ -6,16 +6,16 @@ # 4. Copy the token it gives you TELEGRAM_BOT_TOKEN=your_bot_token_here +# Restrict bot to your Telegram chat (required) +# Get your chat ID by messaging @userinfobot on Telegram +# The bot will refuse to start without this value. +TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here + # opencode API URL # Must match the --port flag you use when starting opencode serve -# Example: opencode serve --port 5050 -OPENCODE_API_URL=http://127.0.0.1:5050 +# Example: opencode serve --port 4096 +OPENCODE_API_URL=http://127.0.0.1:4096 # Optional: password protect the opencode server -# If set, you must also run: OPENCODE_SERVER_PASSWORD=your-secret opencode serve --port 5050 +# If set, you must also run: OPENCODE_SERVER_PASSWORD=your-secret opencode serve --port 4096 # OPENCODE_SERVER_PASSWORD=your-secret - -# Optional: restrict bot to a single Telegram user -# Get your chat ID by messaging @userinfobot on Telegram -# If not set, the bot responds to anyone -# TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here diff --git a/.gitignore b/.gitignore index 82831d5..65efa74 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .env # Dependencies -node_modules/ __pycache__/ *.pyc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..709ebcc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,98 @@ +# AGENTS.md + +Instructions for AI agents working on this repository. + +## Project overview + +`opencode-dispatch` is a Telegram bot that acts as a secure remote bridge to a locally-running `opencode` instance. It forwards messages from a single authorized Telegram chat to the opencode REST API and relays the responses back. + +Single entry point: `bot.py`. Python only. + +## Architecture + +``` +Telegram (user) ──► bot.py ──► opencode REST API (opencode serve) + ◄────────────────────────────── +``` + +**Concurrency model:** +- `python-telegram-bot` runs an asyncio event loop +- `send_to_opencode()` is synchronous (blocking HTTP, up to 1200s timeout) and is always called via `loop.run_in_executor()` to avoid blocking the event loop +- A `queue.Queue` serializes requests when a message arrives while one is already processing +- A `threading.Lock` (`processing_lock`) guards `is_processing` and `current_task` + +**opencode API endpoints used:** + +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/global/health` | Health check (`/status` command) | +| `GET` | `/session` | List existing sessions | +| `POST` | `/session` | Create a new session | +| `POST` | `/session/{id}/message` | Send a message and get the response | + +## Environment variables + +Defined in `.env` (copy from `.env.example`). + +| Variable | Required | Description | +|----------|----------|-------------| +| `TELEGRAM_BOT_TOKEN` | **Yes** | Bot token from @BotFather | +| `TELEGRAM_ALLOWED_CHAT_ID` | **Yes** | Telegram chat ID to allow. Bot refuses to start if unset. | +| `OPENCODE_API_URL` | No | Default: `http://127.0.0.1:5050` | +| `OPENCODE_SERVER_PASSWORD` | No | If set on the opencode server, must match | + +> **Note:** `OPENCODE_SERVER_PASSWORD` is read from the env but not yet forwarded in HTTP requests. If you implement password support, add it as a header or query param to `get_session()` and `send_to_opencode()`. + +## Security invariants — do not break + +1. **`ALLOWED_CHAT_ID` is enforced.** `main()` exits if `TELEGRAM_ALLOWED_CHAT_ID` is not set. +2. **`is_authorized()` is called at the top of every handler** — including all command handlers (`/start`, `/help`, `/status`, `/working`, `/clear`) and all message handlers. Unauthorized requests are silently dropped (no reply). +3. **No internal info is leaked to Telegram.** Error messages sent to the user are generic strings. Detailed errors go to `logger` only. +4. **No user-visible stack traces.** Use `logger.exception()` server-side, return a static string to the user. + +## Adding a new bot command + +1. Write an `async def my_command(update, context)` function. +2. Call `if not is_authorized(update): return` as the first line. +3. Register it in `main()` with `app.add_handler(CommandHandler("my_command", my_command))`. +4. Add it to the help text in `help_command()`. + +## Adding a new message type handler (e.g. video) + +Same pattern as `handle_voice`, `handle_document`, `handle_photo`: +1. Implement the handler with an `is_authorized` guard. +2. Register with `app.add_handler(MessageHandler(filters.VIDEO, handle_video))` in `main()`. + +## Running locally + +```bash +# Install dependencies +pip install -r requirements.txt + +# Configure +cp .env.example .env +# Edit .env: fill TELEGRAM_BOT_TOKEN and TELEGRAM_ALLOWED_CHAT_ID + +# Start opencode in your target project folder +cd ~/your-project +opencode serve --port 5050 + +# Run the bot +python bot.py +``` + +## Dependencies + +``` +python-telegram-bot==21.6 # async Telegram bot framework +requests==2.32.3 # synchronous HTTP client for opencode API +python-dotenv==1.0.1 # .env file loading +``` + +No linting or test runner is currently configured. The `.gitignore` includes `.ruff_cache/` and `.pytest_cache/` anticipating future setup. + +## What is not implemented yet + +- `OPENCODE_SERVER_PASSWORD` forwarding in HTTP requests +- Voice, photo, and document handling (handlers exist but reply "not supported") +- Queued message processing: messages added to the queue while the bot is busy are stored but never drained — the queue worker (`process_queue`) was removed. Queued items are currently orphaned. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..65e61df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.14-slim + +WORKDIR /app + +# Install dependencies first (layer caching) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy bot +COPY bot.py . + +# Non-root user +RUN useradd --no-create-home --shell /bin/false botuser +USER botuser + +CMD ["python", "bot.py"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..c9c65ab --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,38 @@ +pipeline { + environment { + registry = 'https://registry.hub.docker.com' + registryCredential = 'dockerhub_jcabillot' + dockerImage = 'jcabillot/opencode-dispatch' + } + + agent any + + triggers { + cron('@midnight') + } + + stages { + stage('Clone repository') { + steps{ + checkout scm + } + } + + stage('Build image') { + steps{ + sh 'docker build --force-rm=true --no-cache=true --pull -t ${dockerImage} .' + } + } + + stage('Deploy Image') { + steps{ + script { + withCredentials([usernamePassword(credentialsId: 'dockerhub_jcabillot', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) { + sh 'docker login --username ${DOCKER_USER} --password ${DOCKER_PASS}' + sh 'docker push ${dockerImage}' + } + } + } + } + } +} diff --git a/README.md b/README.md index cdcceb5..4fdc42f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +> **Fork** of [alexanxin/opencode-dispatch](https://github.com/alexanxin/opencode-dispatch) — security hardening and Python-only rewrite. + # Beyond the Terminal: The Road to a Truly Autonomous AI Agent **opencode-dispatch** is a secure Telegram bridge for the [opencode CLI](https://opencode.ai). It brings the power of a 120K-star, self-hosted AI agent to your pocket—giving you "Dispatch-style" remote access without the corporate subscription or vendor lock-in. @@ -37,7 +39,7 @@ This repository is more than a bridge; it's a foundational layer for a persisten - **[opencode CLI](https://opencode.ai)** installed — verify with `opencode --version` - A Telegram account -- Python 3.10+ or Node.js +- Python 3.10+ - A Telegram bot token (free from [@BotFather](https://t.me/BotFather)) ## Quick Setup @@ -49,31 +51,26 @@ This repository is more than a bridge; it's a foundational layer for a persisten ### Step 2: Install Dependencies -Choose Python or Node.js (both work the same): - -**Python:** ```bash pip install -r requirements.txt ``` -**Node.js:** -```bash -npm install -``` - ### Step 3: Configure ```bash cp .env.example .env ``` -Edit `.env` and add your Telegram bot token: +Edit `.env` and set your Telegram bot token and chat ID: ```env TELEGRAM_BOT_TOKEN=your_bot_token_here +TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here OPENCODE_API_URL=http://127.0.0.1:5050 ``` +> Get your chat ID by messaging [@userinfobot](https://t.me/userinfobot) on Telegram. + ### Step 4: Start opencode ```bash @@ -85,8 +82,9 @@ opencode serve --port 5050 ### Step 5: Run the Bot -- **Python:** `python bot.py` -- **Node.js:** `npm start` +```bash +python bot.py +``` ### Bot Commands @@ -97,13 +95,17 @@ opencode serve --port 5050 | `/working` | Check what task is currently being processed | | `/clear` | Clear pending messages from queue | -## Security Recommendations +## Security -### 1. Limit workspace access +### 1. Chat ID locking (Required) + +The bot **requires** `TELEGRAM_ALLOWED_CHAT_ID` to be set and will refuse to start without it. This ensures only your Telegram account can interact with the bot. + +### 2. Limit workspace access Never run from your home directory (`~`) or root (`/`). opencode can access all files in the directory it's started from. -### 2. Password protect (Recommended) +### 3. Password protect (Recommended) Set a password to prevent unauthorized local access: @@ -117,17 +119,6 @@ Then add to `.env`: OPENCODE_SERVER_PASSWORD=your-secret ``` -### 3. Restrict to your Telegram account (Highly Recommended) - -To lock the bot so only YOU can use it: - -1. Get your chat ID from [@userinfobot](https://t.me/userinfobot) -2. Add to `.env`: - -```env -TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here -``` - ## Troubleshooting **"Can't connect to opencode"** @@ -136,7 +127,7 @@ TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here **"Bot isn't responding"** - Check your Telegram bot token in `.env` -- Make sure the bot is running (`python bot.py` or `npm start`) +- Make sure the bot is running (`python bot.py`) **"Port already in use"** - Pick a different port: `opencode serve --port 5051` diff --git a/bot.js b/bot.js deleted file mode 100644 index 6825cdc..0000000 --- a/bot.js +++ /dev/null @@ -1,214 +0,0 @@ -import { Telegraf } from 'telegraf'; -import { config } from 'dotenv'; -import axios from 'axios'; - -config(); - -const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; -const OPENCODE_API_URL = process.env.OPENCODE_API_URL || 'http://127.0.0.1:5050'; -const ALLOWED_CHAT_ID = process.env.TELEGRAM_ALLOWED_CHAT_ID ? String(process.env.TELEGRAM_ALLOWED_CHAT_ID) : null; -const messageQueue = []; -let isProcessing = false; -let currentTask = 'Idle'; - -if (!BOT_TOKEN) { - console.error('Error: TELEGRAM_BOT_TOKEN not set in .env file'); - process.exit(1); -} - -async function getSession() { - try { - const r = await axios.get(`${OPENCODE_API_URL}/session`, { timeout: 10000 }); - if (r.data && r.data.length > 0) { - return r.data[0].id; - } - } catch (e) {} - try { - const r = await axios.post(`${OPENCODE_API_URL}/session`, {}, { timeout: 10000 }); - if (r.data && r.data.id) { - return r.data.id; - } - } catch (e) {} - return null; -} - -async function sendToOpencode(message) { - const sessionId = await getSession(); - if (!sessionId) { - return "Error: Could not connect to opencode session."; - } - - try { - const r = await axios.post( - `${OPENCODE_API_URL}/session/${sessionId}/message`, - { parts: [{ type: "text", text: `[Telegram] ${message}` }] }, - { timeout: 1200000 } - ); - if (r.data && r.data.parts) { - const textParts = r.data.parts - .filter(p => p.type === "text" && p.text) - .map(p => p.text); - return textParts.join("\n") || "Message sent, no text response."; - } - return "Message sent."; - } catch (error) { - if (error.code === 'ECONNREFUSED') { - return "Can't connect to opencode. Is it running?"; - } else if (error.code === 'ETIMEDOUT') { - return "opencode took too long to respond. Please try again."; - } - return `Error: ${error.message}`; - } -} - -async function processQueue() { - while (messageQueue.length > 0) { - const item = messageQueue.shift(); - isProcessing = true; - try { - const reply = await sendToOpencode(item.message); - await item.ctx.telegram.editMessageText( - item.chatId, - item.messageId, - null, - `🔄 Processed from queue\n\n${reply.substring(0, 4000)}` - ); - } catch (e) { - await item.ctx.telegram.editMessageText( - item.chatId, - item.messageId, - null, - `Error: ${e.message}` - ); - } - isProcessing = false; - } -} - -const bot = new Telegraf(BOT_TOKEN); - -bot.start((ctx) => { - ctx.reply( - 'opencode-dispatch bot\n\n' + - 'Send any message and opencode will process it.\n' + - `Server: ${OPENCODE_API_URL}` - ); -}); - -bot.help((ctx) => { - ctx.reply( - 'How to use:\n\n' + - '1. Make sure opencode is running\n' + - '2. Send me any message\n' + - '3. I\'ll forward it to opencode and relay the response\n\n' + - 'Commands: /start, /help, /status, /working, /clear' - ); -}); - -bot.command('status', async (ctx) => { - const sessionId = await getSession(); - let healthy = false; - try { - const r = await axios.get(`${OPENCODE_API_URL}/global/health`, { timeout: 5000 }); - healthy = r.ok; - } catch (e) {} - ctx.reply( - `Server: ${OPENCODE_API_URL}\n` + - `opencode: ${healthy ? '✅' : '❌'}\n` + - `Session: ${sessionId || 'none'}\n` + - `Queue: ${messageQueue.length} messages` - ); -}); - -bot.command('clear', (ctx) => { - if (isProcessing) { - ctx.reply('❌ Can\'t clear queue while processing. Wait for current task to finish.'); - } else { - messageQueue.length = 0; - ctx.reply('✅ Queue cleared.'); - } -}); - -bot.command('working', (ctx) => { - if (isProcessing) { - ctx.reply(`🔄 Currently working on:\n"${currentTask}"\n\nQueue: ${messageQueue.length} messages`); - } else { - ctx.reply('✅ Currently idle. No task in progress.'); - } -}); - -bot.on('text', async (ctx) => { - const chatId = String(ctx.chat.id); - if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) { - ctx.reply('❌ This bot is not authorized to respond to you.'); - return; - } - - const userMessage = ctx.message.text; - - if (isProcessing) { - const sent = await ctx.reply( - '⏳ opencode is busy. Your message has been added to the queue.\n' + - 'I\'ll respond when ready. Use /status to check queue position.' - ); - messageQueue.push({ - message: userMessage, - chatId: ctx.chat.id, - messageId: sent.message_id, - ctx: ctx - }); - } else { - currentTask = userMessage.length > 50 ? userMessage.substring(0, 50) + '...' : userMessage; - const sent = await ctx.reply('🔄 Processing...'); - isProcessing = true; - try { - const reply = await sendToOpencode(userMessage); - await ctx.telegram.editMessageText( - ctx.chat.id, - sent.message_id, - null, - reply.substring(0, 4000) - ); - } catch (e) { - await ctx.telegram.editMessageText( - ctx.chat.id, - sent.message_id, - null, - `Error: ${e.message}` - ); - } - isProcessing = false; - currentTask = 'Idle'; - - if (messageQueue.length > 0) { - processQueue(); - } - } -}); - -bot.on('voice', (ctx) => { - const chatId = String(ctx.chat.id); - if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return; - ctx.reply('Voice messages not yet supported. Send text.'); -}); - -bot.on('document', (ctx) => { - const chatId = String(ctx.chat.id); - if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return; - ctx.reply('File handling not yet supported. Send text.'); -}); - -bot.on('photo', (ctx) => { - const chatId = String(ctx.chat.id); - if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return; - ctx.reply('Image handling not yet supported. Send text.'); -}); - -console.log('opencode-dispatch bot starting...'); -console.log(`Connecting to opencode at: ${OPENCODE_API_URL}`); -console.log('Press Ctrl+C to stop'); - -bot.launch(); - -process.once('SIGINT', () => bot.stop('SIGINT')); -process.once('SIGTERM', () => bot.stop('SIGTERM')); diff --git a/bot.py b/bot.py index 69ba722..cd30f9a 100644 --- a/bot.py +++ b/bot.py @@ -1,23 +1,33 @@ #!/usr/bin/env python3 +import asyncio +import logging import os -import threading import queue +import threading + import requests from dotenv import load_dotenv from telegram import Update from telegram.ext import ( Application, CommandHandler, + ContextTypes, MessageHandler, filters, - ContextTypes, ) load_dotenv() -OPENCODE_API_URL = os.getenv("OPENCODE_API_URL", "http://127.0.0.1:5050") +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", +) +logger = logging.getLogger(__name__) + +OPENCODE_API_URL = os.getenv("OPENCODE_API_URL", "http://127.0.0.1:4096") BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") ALLOWED_CHAT_ID = os.getenv("TELEGRAM_ALLOWED_CHAT_ID") + SESSION_ID = None message_queue = queue.Queue() is_processing = False @@ -25,6 +35,12 @@ current_task = "Idle" processing_lock = threading.Lock() +def is_authorized(update: Update) -> bool: + """Check if the update comes from the allowed chat.""" + chat_id = str(update.message.chat.id) + return chat_id == ALLOWED_CHAT_ID + + def get_session(): """Get or create a session.""" global SESSION_ID @@ -38,14 +54,14 @@ def get_session(): SESSION_ID = sessions[0]["id"] return SESSION_ID except Exception: - pass + logger.exception("Failed to fetch existing sessions") try: r = requests.post(f"{OPENCODE_API_URL}/session", json={}, timeout=10) if r.ok: SESSION_ID = r.json()["id"] return SESSION_ID except Exception: - pass + logger.exception("Failed to create new session") return None @@ -53,7 +69,7 @@ def send_to_opencode(message): """Send message to opencode and return response.""" session_id = get_session() if not session_id: - return "Error: Could not connect to opencode session." + return "Could not connect to opencode session." try: r = requests.post( @@ -73,56 +89,32 @@ def send_to_opencode(message): else "Message sent, no text response." ) else: - return f"opencode returned {r.status_code}: {r.text[:200]}" + logger.error("opencode returned %d: %s", r.status_code, r.text[:500]) + return "opencode returned an error. Check server logs." except requests.exceptions.ConnectionError: + logger.error("Connection error to opencode at %s", OPENCODE_API_URL) return "Can't connect to opencode. Is it running?" except requests.exceptions.Timeout: + logger.warning("opencode request timed out") return "opencode took too long to respond. Please try again." - except Exception as e: - return f"Error: {str(e)}" - - -def process_queue(bot, chat_id): - """Process messages in the queue one at a time.""" - global is_processing - while True: - try: - item = message_queue.get(timeout=1) - if item is None: - break - user_id, message_id, user_message = item - with processing_lock: - is_processing = True - try: - reply = send_to_opencode(user_message) - - async def send_reply(): - await bot.edit_message_text( - chat_id=chat_id, - message_id=message_id, - text=f"🔄 Processing...\n\n{reply[:4000]}", - ) - - import asyncio - - asyncio.run(send_reply()) - finally: - with processing_lock: - is_processing = False - message_queue.task_done() - except queue.Empty: - continue + except Exception: + logger.exception("Unexpected error sending message to opencode") + return "An unexpected error occurred. Check server logs." async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not is_authorized(update): + return await update.message.reply_text( "opencode-dispatch bot\n\n" "Send any message and opencode will process it.\n" - f"Server: {OPENCODE_API_URL}" + "Commands: /start, /help, /status, /working, /clear" ) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not is_authorized(update): + return await update.message.reply_text( "How to use:\n\n" "1. Make sure opencode is running\n" @@ -133,7 +125,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): - session_id = get_session() + if not is_authorized(update): + return try: r = requests.get(f"{OPENCODE_API_URL}/global/health", timeout=5) healthy = r.ok @@ -141,18 +134,19 @@ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): healthy = False queue_size = message_queue.qsize() await update.message.reply_text( - f"Server: {OPENCODE_API_URL}\n" - f"opencode: {'✅' if healthy else '❌'}\n" - f"Session: {session_id or 'none'}\n" + f"opencode: {'connected' if healthy else 'unreachable'}\n" + f"Session: {'active' if SESSION_ID else 'none'}\n" f"Queue: {queue_size} messages" ) async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not is_authorized(update): + return with processing_lock: if is_processing: await update.message.reply_text( - "❌ Can't clear queue while processing. Wait for current task to finish." + "Can't clear queue while processing. Wait for current task to finish." ) else: while not message_queue.empty(): @@ -160,26 +154,23 @@ async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE): message_queue.get_nowait() except queue.Empty: break - await update.message.reply_text("✅ Queue cleared.") + await update.message.reply_text("Queue cleared.") async def working_command(update: Update, context: ContextTypes.DEFAULT_TYPE): - global current_task + if not is_authorized(update): + return if is_processing: await update.message.reply_text( - f'🔄 Currently working on:\n"{current_task}"\n\nQueue: {message_queue.qsize()} messages' + f'Currently working on:\n"{current_task}"\n\nQueue: {message_queue.qsize()} messages' ) else: - await update.message.reply_text("✅ Currently idle. No task in progress.") + await update.message.reply_text("Currently idle. No task in progress.") async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): global is_processing, current_task - chat_id = str(update.message.chat.id) - if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: - await update.message.reply_text( - "❌ This bot is not authorized to respond to you." - ) + if not is_authorized(update): return user_message = update.message.text user_id = update.effective_user.id if update.effective_user else "unknown" @@ -189,7 +180,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): if currently_processing: sent = await update.message.reply_text( - "⏳ opencode is busy. Your message has been added to the queue.\n" + "opencode is busy. Your message has been added to the queue.\n" "I'll respond when ready. Use /status to check queue position." ) message_queue.put((user_id, sent.message_id, user_message)) @@ -198,14 +189,16 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): user_message[:50] + "..." if len(user_message) > 50 else user_message ) await update.message.chat.send_action("typing") - sent = await update.message.reply_text("🔄 Processing...") + sent = await update.message.reply_text("Processing...") with processing_lock: is_processing = True try: - reply = send_to_opencode(user_message) + loop = asyncio.get_event_loop() + reply = await loop.run_in_executor(None, send_to_opencode, user_message) await sent.edit_text(reply[:4000]) - except Exception as e: - await sent.edit_text(f"Error: {str(e)}") + except Exception: + logger.exception("Error processing message") + await sent.edit_text("An error occurred. Check server logs.") finally: with processing_lock: is_processing = False @@ -213,29 +206,33 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): - chat_id = str(update.message.chat.id) - if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: + if not is_authorized(update): return await update.message.reply_text("Voice messages not yet supported. Send text.") async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE): - chat_id = str(update.message.chat.id) - if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: + if not is_authorized(update): return await update.message.reply_text("File handling not yet supported. Send text.") async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): - chat_id = str(update.message.chat.id) - if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: + if not is_authorized(update): return await update.message.reply_text("Image handling not yet supported. Send text.") def main(): if not BOT_TOKEN: - print("Error: TELEGRAM_BOT_TOKEN not set in .env file") + logger.error("TELEGRAM_BOT_TOKEN not set in .env file") + return + + if not ALLOWED_CHAT_ID: + logger.error( + "TELEGRAM_ALLOWED_CHAT_ID not set in .env file. " + "Refusing to start without access control." + ) return app = Application.builder().token(BOT_TOKEN).build() @@ -250,9 +247,8 @@ def main(): app.add_handler(MessageHandler(filters.Document.ALL, handle_document)) app.add_handler(MessageHandler(filters.PHOTO, handle_photo)) - print(f"opencode-dispatch bot starting...") - print(f"Connecting to opencode at: {OPENCODE_API_URL}") - print("Press Ctrl+C to stop") + logger.info("opencode-dispatch bot starting...") + logger.info("Press Ctrl+C to stop") app.run_polling(allowed_updates=Update.ALL_TYPES) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fb9da7c..0000000 --- a/package-lock.json +++ /dev/null @@ -1,481 +0,0 @@ -{ - "name": "opencode-dispatch", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "opencode-dispatch", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "axios": "^1.7.9", - "dotenv": "^16.4.5", - "telegraf": "^4.16.3" - } - }, - "node_modules/@telegraf/types": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz", - "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==", - "license": "MIT" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "license": "MIT" - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "license": "MIT" - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/p-timeout": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", - "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/safe-compare": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", - "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==", - "license": "MIT", - "dependencies": { - "buffer-alloc": "^1.2.0" - } - }, - "node_modules/sandwich-stream": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", - "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/telegraf": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", - "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", - "license": "MIT", - "dependencies": { - "@telegraf/types": "^7.1.0", - "abort-controller": "^3.0.0", - "debug": "^4.3.4", - "mri": "^1.2.0", - "node-fetch": "^2.7.0", - "p-timeout": "^4.1.0", - "safe-compare": "^1.1.4", - "sandwich-stream": "^2.0.2" - }, - "bin": { - "telegraf": "lib/cli.mjs" - }, - "engines": { - "node": "^12.20.0 || >=14.13.1" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index ebafdc5..0000000 --- a/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "opencode-dispatch", - "version": "1.0.0", - "description": "Control opencode from Telegram - like Claude users do with Dispatch", - "main": "bot.js", - "type": "module", - "scripts": { - "start": "node bot.js" - }, - "dependencies": { - "telegraf": "^4.16.3", - "dotenv": "^16.4.5", - "axios": "^1.7.9" - }, - "keywords": [ - "opencode", - "telegram", - "bot", - "ai", - "cli" - ], - "author": "", - "license": "MIT" -} diff --git a/requirements.txt b/requirements.txt index 8943bcc..742c9a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -python-telegram-bot==21.6 -requests==2.32.3 -python-dotenv==1.0.1 +python-telegram-bot==22.7 +requests==2.33.0 +python-dotenv==1.2.2