# Установщик Telegram-бота для обмена файлами с Claude (v2 · robust) ## Как этим пользоваться (прочитай это сам, это НЕ нужно вставлять) 1. Открой Claude Code (десктоп / VS Code / openvscode-server в браузере). 2. Создай бота: в Telegram напиши **@BotFather** → `/newbot` → дай имя → получишь **токен** вида `12345:AAxxxx`. 3. Открой эту ссылку в чате с Claude и напиши «настрой бота по этой методике». 4. Claude сам спросит у тебя токен от @BotFather — пришлёшь ему в чат. 5. Claude поставит Python (если нужно), напишет код, запустит бота. 6. Когда Claude скажет — открой своего бота в Telegram, нажми **Start**, отправь `/id` и пришли любой файл. Claude дочитает твой `chat_id`, привяжет бота к тебе и проверит обмен в обе стороны. Дальше просто говоришь Claude: «отправь мне в бота» — он шлёт файл тебе в Telegram; «я отправила в бота / что-то пришло» — он читает присланный файл. --- === НАЧАЛО ПРОМПТА === Помоги настроить Telegram-бота для двустороннего обмена файлами между мной и тобой (Claude) на этом компьютере. Делай всё сам, по шагам, не переспрашивай по мелочам. ## 🛡 ЗАЩИТА ОТ ОШИБОК СРЕДЫ (читай ОБЯЗАТЕЛЬНО) Если у тебя в любой момент: - Ошибка `Subprocess initialization did not complete within 60000ms` - Ошибка `Stream closed` при запросе разрешения - Команды `python3`, `cp`, Edit или Write неожиданно падают, а `grep`/`ls`/`echo` ещё работают **НЕ пытайся обходить.** Это сломанный канал подтверждений в расширении VS Code, не твой код. Сразу остановись и скажи студенту ровно эту фразу: > ⚠️ Среда временно потеряла связь. Сделай, пожалуйста: **Cmd+Shift+P → Developer: Reload Window** (или Cmd+R в браузере, если openvscode-server). После перезагрузки напиши мне «продолжи настройку бота» — я возобновлю с того места, где остановились. Не выполняй больше команд после такого сообщения. Жди когда студент перезагрузит окно. ## 📋 РАБОЧИЙ ПЛАН ### Первый шаг Спроси у меня токен бота от @BotFather (формат: `12345:AAxxxx`). Если я ещё не создал бота — объясни как создать через @BotFather (3 шага: открыть BotFather → `/newbot` → имя и юзернейм → получить токен). После того как я пришлю токен, продолжай настройку. ### Логика, которую нужно получить - Бот — это маленькая Python-программа, которая крутится на ЭТОМ компьютере и общается с Telegram. - Я с любого устройства пишу боту в Telegram и кидаю файл → бот скачивает его в папку `inbox/` на этом компе → ты (Claude) читаешь файлы оттуда. - Ты кладёшь файл в папку `outbox/` → бот каждые несколько секунд проверяет её и сам отправляет новые файлы мне в Telegram, затем переносит отправленное в `outbox/sent/` (чтобы не слать повторно). - При ошибке отправки файл **остаётся в очереди** и пробуется снова, а после 5 неудачных попыток уходит в `outbox/failed/` (чтобы не зависать вечно). - Бот привязывается к моему `chat_id` (узнаём его автоматически из логов после первого сообщения), чтобы принимал файлы только от меня и знал, кому слать из outbox. ### Шаг 0. Определи среду Выполни: ``` uname -a 2>/dev/null || echo "windows" ``` Запомни: - **Linux** (содержит `Linux` в выводе) → облачная среда (openvscode-server / jupyter / контейнер). Путь проекта: `$HOME/tg-bot/`. Никаких systemd, нет sudo. Автозапуск через `nohup` + хук в `~/.bashrc`. - **macOS** (содержит `Darwin`) → десктоп. Путь: `$HOME/tg-bot/`. Автозапуск через `launchd` (`~/Library/LaunchAgents/com.mybot.tgbot.plist`). - **Windows** (вывод `windows`) → десктоп. Путь: `%USERPROFILE%\tg-bot\`. Автозапуск через папку Startup или Планировщик. Дальше веди настройку **под определённую ОС**, не пытайся применять Windows-команды на Linux и наоборот. ### Шаг 1. Проверь Python - Linux/macOS: `python3 --version` - Windows: `python --version` и `py --version` Если Python нет — установи (Linux: `apt install python3 python3-pip` если есть sudo, иначе скажи студенту что в облаке Лагеря Python должен быть из коробки и попроси сделать Reload Window; macOS: подскажи `brew install python3`; Windows: см. примечание ниже). **Только для Windows**, если обычный установщик с python.org блокируется групповой политикой (код выхода 1625), используй portable-сборку: скачай embeddable-zip Python 3.12 (`https://www.python.org/ftp/python/3.12.7/python-3.12.7-embed-amd64.zip`), распакуй в `%USERPROFILE%\.local\python312`, в файле `python312._pth` раскомментируй `import site` и добавь строку `Lib\site-packages`, затем поставь pip через `get-pip.py` (`https://bootstrap.pypa.io/get-pip.py`). Дальше используй именно этот `python.exe`. ### Шаг 2. Установи библиотеку ``` python3 -m pip install --user "python-telegram-bot[job-queue]==21.9" ``` (на Windows — `python` вместо `python3`) ### Шаг 3. Создай папки Linux/macOS: ```bash mkdir -p "$HOME/tg-bot/inbox" "$HOME/tg-bot/outbox/sent" "$HOME/tg-bot/outbox/failed" ``` Windows (PowerShell): ```powershell New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\tg-bot\inbox","$env:USERPROFILE\tg-bot\outbox\sent","$env:USERPROFILE\tg-bot\outbox\failed" | Out-Null ``` ### Шаг 4. Создай config.json через heredoc (НЕ через Edit/Write) ⚠️ ВАЖНО: создавай файлы **через `cat << 'EOF' > file`** (Bash) или через `Set-Content` (PowerShell), а **не через Edit/Write**. Эти tool-вызовы не требуют подтверждения и работают даже когда канал подтверждений сломан. Linux/macOS — определи путь и создай конфиг: ```bash BOT_DIR="$HOME/tg-bot" TOKEN="ТОТ_ТОКЕН_ЧТО_СТУДЕНТ_ПРИСЛАЛ" cat > "$BOT_DIR/config.json" << EOF { "token": "$TOKEN", "inbox_dir": "$BOT_DIR/inbox", "outbox_dir": "$BOT_DIR/outbox", "allowed_chat_ids": [], "poll_outbox_seconds": 5, "max_send_attempts": 5 } EOF chmod 600 "$BOT_DIR/config.json" ``` Windows (PowerShell): ```powershell $BotDir = "$env:USERPROFILE\tg-bot" $Token = "ТОТ_ТОКЕН_ЧТО_СТУДЕНТ_ПРИСЛАЛ" $Config = @{ token = $Token; inbox_dir = "$BotDir\inbox"; outbox_dir = "$BotDir\outbox"; allowed_chat_ids = @(); poll_outbox_seconds = 5; max_send_attempts = 5 } | ConvertTo-Json Set-Content -Path "$BotDir\config.json" -Value $Config -Encoding UTF8 ``` ### Шаг 5. Создай bot.py через heredoc ⚠️ Тот же принцип — `cat << 'EOF' > bot.py` через Bash, не Edit/Write. Код ниже (это рабочая v2 с обработкой ошибок отправки): Linux/macOS: ```bash cat > "$HOME/tg-bot/bot.py" << 'PYEOF' # -*- coding: utf-8 -*- """Telegram-бот для двустороннего обмена файлами с Claude. v2 (robust).""" import json import logging import re import shutil import sys from pathlib import Path from datetime import datetime # Для Windows-консоли — UTF-8 for _stream in (sys.stdout, sys.stderr): try: _stream.reconfigure(encoding="utf-8", errors="replace") except Exception: pass from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CommandHandler, MessageHandler, ContextTypes, filters, ) BASE_DIR = Path(__file__).resolve().parent CONFIG_PATH = BASE_DIR / "config.json" with CONFIG_PATH.open("r", encoding="utf-8") as f: CONFIG = json.load(f) TOKEN = CONFIG["token"] INBOX_DIR = Path(CONFIG.get("inbox_dir", BASE_DIR / "inbox")) OUTBOX_DIR = Path(CONFIG.get("outbox_dir", BASE_DIR / "outbox")) SENT_DIR = OUTBOX_DIR / "sent" FAILED_DIR = OUTBOX_DIR / "failed" ALLOWED_CHAT_IDS = set(CONFIG.get("allowed_chat_ids", [])) POLL_SECONDS = int(CONFIG.get("poll_outbox_seconds", 5)) MAX_SEND_ATTEMPTS = int(CONFIG.get("max_send_attempts", 5)) # Счётчик неудачных попыток отправки по имени файла _send_attempts = {} for d in (INBOX_DIR, OUTBOX_DIR, SENT_DIR, FAILED_DIR): d.mkdir(parents=True, exist_ok=True) logging.basicConfig( format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s", level=logging.INFO, ) logging.getLogger("httpx").setLevel(logging.WARNING) log = logging.getLogger("tg-bot") def is_allowed(chat_id: int) -> bool: if not ALLOWED_CHAT_IDS: return True return chat_id in ALLOWED_CHAT_IDS def safe_filename(name: str) -> str: name = name.replace("\\", "_").replace("/", "_") name = re.sub(r'[<>:"|?*\x00-\x1f]', "_", name) return name.strip(" .") or "file" def unique_path(directory: Path, filename: str) -> Path: candidate = directory / filename if not candidate.exists(): return candidate stem, suffix = candidate.stem, candidate.suffix i = 1 while True: candidate = directory / f"{stem}_{i}{suffix}" if not candidate.exists(): return candidate i += 1 async def cmd_start(update, context): chat_id = update.effective_chat.id text = ( "👋 Бот для обмена файлами запущен.\n\n" f"🆔 Твой chat_id: `{chat_id}`\n\n" "📥 Пришли мне любой файл — я сохраню его на компьютере.\n" "📤 Всё, что появится в папке outbox, я отправлю тебе обратно.\n\n" "Команды: /start /id /list /help" ) await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN) async def cmd_id(update, context): await update.message.reply_text( f"chat_id: `{update.effective_chat.id}`", parse_mode=ParseMode.MARKDOWN ) async def cmd_help(update, context): await update.message.reply_text( "Пришли файл — сохраню в inbox.\n" "Файлы из папки outbox я автоматически отправлю тебе.\n" "⚠️ Через Telegram Bot API скачиваются файлы до 20 МБ." ) async def cmd_list(update, context): files = sorted(INBOX_DIR.glob("*"), key=lambda p: p.stat().st_mtime, reverse=True) files = [p for p in files if p.is_file()][:15] if not files: await update.message.reply_text("Папка inbox пока пуста.") return lines = ["📂 Последние файлы в inbox:"] for p in files: lines.append(f"• {p.name} ({p.stat().st_size/1024:.0f} КБ)") await update.message.reply_text("\n".join(lines)) async def handle_file(update, context): chat_id = update.effective_chat.id if not is_allowed(chat_id): await update.message.reply_text("⛔ Доступ запрещён.") log.warning("Отклонён файл от неразрешённого chat_id=%s", chat_id) return msg = update.message tg_file = None suggested_name = None if msg.document: tg_file = msg.document suggested_name = msg.document.file_name elif msg.photo: tg_file = msg.photo[-1] suggested_name = f"photo_{tg_file.file_unique_id}.jpg" elif msg.video: tg_file = msg.video suggested_name = msg.video.file_name or f"video_{tg_file.file_unique_id}.mp4" elif msg.audio: tg_file = msg.audio suggested_name = msg.audio.file_name or f"audio_{tg_file.file_unique_id}.mp3" elif msg.voice: tg_file = msg.voice suggested_name = f"voice_{tg_file.file_unique_id}.ogg" elif msg.video_note: tg_file = msg.video_note suggested_name = f"videonote_{tg_file.file_unique_id}.mp4" else: return if not suggested_name: suggested_name = f"file_{datetime.now().strftime('%Y%m%d_%H%M%S')}" suggested_name = safe_filename(suggested_name) dest = unique_path(INBOX_DIR, suggested_name) try: file_obj = await tg_file.get_file() await file_obj.download_to_drive(custom_path=str(dest)) except Exception as exc: log.exception("Ошибка скачивания файла") hint = "" if "too big" in str(exc).lower(): hint = "\n(Telegram Bot API не отдаёт файлы больше 20 МБ.)" await msg.reply_text(f"❌ Не удалось скачать файл: {exc}{hint}") return size_kb = dest.stat().st_size / 1024 log.info("Сохранён файл: %s (%.0f КБ) от chat_id=%s", dest.name, size_kb, chat_id) await msg.reply_text(f"✅ Сохранено: {dest.name} ({size_kb:.0f} КБ)") async def poll_outbox(context): """Опрашивает outbox/. При ошибке оставляет в очереди (а не уходит в sent сразу). После MAX_SEND_ATTEMPTS неудач — двигает в failed/.""" targets = list(ALLOWED_CHAT_IDS) if not targets: last = context.application.bot_data.get("last_chat_id") if last is not None: targets = [last] if not targets: return chat_id = targets[0] for path in sorted(OUTBOX_DIR.glob("*")): if not path.is_file() or path.name.startswith('.'): continue try: with path.open("rb") as fh: await context.bot.send_document( chat_id=chat_id, document=fh, filename=path.name ) shutil.move(str(path), str(unique_path(SENT_DIR, path.name))) _send_attempts.pop(path.name, None) log.info("Отправлен файл %s -> chat_id=%s", path.name, chat_id) except Exception: _send_attempts[path.name] = _send_attempts.get(path.name, 0) + 1 n = _send_attempts[path.name] log.exception("Не удалось отправить %s (попытка %d/%d)", path.name, n, MAX_SEND_ATTEMPTS) if n >= MAX_SEND_ATTEMPTS: shutil.move(str(path), str(unique_path(FAILED_DIR, path.name))) _send_attempts.pop(path.name, None) log.error("Файл %s перенесён в failed/ после %d неудачных попыток", path.name, n) async def remember_chat(update, context): if update.effective_chat: context.application.bot_data["last_chat_id"] = update.effective_chat.id def main(): app = Application.builder().token(TOKEN).build() app.add_handler(CommandHandler("start", cmd_start)) app.add_handler(CommandHandler("id", cmd_id)) app.add_handler(CommandHandler("help", cmd_help)) app.add_handler(CommandHandler("list", cmd_list)) file_filter = ( filters.Document.ALL | filters.PHOTO | filters.VIDEO | filters.AUDIO | filters.VOICE | filters.VIDEO_NOTE ) app.add_handler(MessageHandler(file_filter, handle_file)) app.add_handler(MessageHandler(filters.ALL, remember_chat), group=1) app.job_queue.run_repeating(poll_outbox, interval=POLL_SECONDS, first=POLL_SECONDS) if not ALLOWED_CHAT_IDS: log.warning("allowed_chat_ids пуст — бот принимает файлы от ЛЮБОГО. " "Узнай chat_id командой /id и впиши в config.json.") log.info("Бот запущен. inbox=%s outbox=%s", INBOX_DIR, OUTBOX_DIR) app.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": main() PYEOF ``` (на Windows этот код запиши через `Set-Content -Path "$env:USERPROFILE\tg-bot\bot.py" -Value @'...'@ -Encoding UTF8`) ### Шаг 6. Проверь токен (getMe) Через curl: ```bash TOKEN=$(python3 -c "import json;print(json.load(open('$HOME/tg-bot/config.json'))['token'])") curl -s "https://api.telegram.org/bot${TOKEN}/getMe" | python3 -m json.tool ``` Если в ответе `"ok": true` и есть `"username"` — токен валиден. Сохрани @username бота, скажи студенту. Если `"ok": false` — токен неправильный, попроси прислать заново. ### Шаг 7. Запусти бота в фоне **Linux/macOS** — через `nohup`: ```bash cd "$HOME/tg-bot" nohup python3 bot.py > run.log 2> err.log < /dev/null & echo $! > bot.pid sleep 7 ps -p $(cat bot.pid) > /dev/null && echo "✅ Бот запущен (PID $(cat bot.pid))" || echo "❌ Бот не стартовал, см. err.log" tail -5 err.log ``` **Windows** — через `Start-Process`: ```powershell cd "$env:USERPROFILE\tg-bot" $env:PYTHONUTF8 = "1" $proc = Start-Process python -ArgumentList "bot.py" -WindowStyle Hidden -RedirectStandardOutput "run.log" -RedirectStandardError "err.log" -PassThru $proc.Id | Out-File "bot.pid" Start-Sleep 7 Get-Process -Id $proc.Id -ErrorAction SilentlyContinue Get-Content "err.log" -Tail 5 ``` Убедись по логу что есть `Application started` и процесс жив. ### Шаг 8. Попроси студента проверить обмен > Открой своего бота в Telegram, нажми **/start**, отправь `/id` и пришли любой файл. Скажи мне когда сделаешь. Когда студент сделал: - найди `chat_id` в `err.log` (строка `chat_id=...`), впиши его в `config.json` в `allowed_chat_ids` (теперь бот принимает файлы только от него и знает кому слать из outbox); - перезапусти бота: убей текущий процесс, запусти заново тем же способом из Шага 7; - прочитай присланный файл из `inbox/` и подтверди студенту что видишь его (название + размер); - проверь обратное направление: положи тестовый файл в `outbox/` и убедись по логу что есть строка `Отправлен файл ... -> chat_id=...`. Только это значит **реально доставлено**. Если в логе `Не удалось отправить` — отправка провалилась, файл остался в очереди. ### Шаг 9. Автозапуск **Linux (openvscode-server / jupyter / контейнер)** — самый сложный случай, нет systemd/cron, контейнер может пересоздаваться. Решение через guardian + хук в `~/.bashrc`: ```bash cat > "$HOME/tg-bot/guardian.sh" << 'EOF' #!/usr/bin/env bash while true; do if ! pgrep -f "python3 .*bot.py" >/dev/null; then cd "$HOME/tg-bot" nohup python3 bot.py >> run.log 2>> err.log < /dev/null & echo $! > bot.pid fi sleep 30 done EOF chmod +x "$HOME/tg-bot/guardian.sh" # Добавить в ~/.bashrc автозапуск guardian if ! grep -q "tg-bot/guardian.sh" "$HOME/.bashrc" 2>/dev/null; then cat >> "$HOME/.bashrc" << 'EOF' # --- tg-bot автозапуск --- if [ -f "$HOME/tg-bot/guardian.sh" ] && ! pgrep -f "bash $HOME/tg-bot/guardian.sh" >/dev/null 2>&1; then nohup setsid bash "$HOME/tg-bot/guardian.sh" >> "$HOME/tg-bot/guardian.log" 2>&1 < /dev/null & fi # --- /tg-bot автозапуск --- EOF fi # Запустить сразу nohup setsid bash "$HOME/tg-bot/guardian.sh" >> "$HOME/tg-bot/guardian.log" 2>&1 < /dev/null & ``` **macOS** — через launchd. Сначала останови вручную запущенный процесс (`pkill -f bot.py`), потом создай `~/Library/LaunchAgents/com.mybot.tgbot.plist` через heredoc, в нём `ProgramArguments` (путь к `python3` и `bot.py`), `WorkingDirectory`, `RunAtLoad=true`, `KeepAlive=true`, лог в `run.log`/`err.log`, переменная `PYTHONUTF8=1`. Загрузи: `launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.mybot.tgbot.plist`. Проверь: `launchctl print gui/$(id -u)/com.mybot.tgbot | grep state` должно показать `running`. **Windows** — сделай отдельный файл `start-bot-silent.bat` без `pause`, создай ярлык на него и положи в `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup` (открыть командой `shell:startup`). ### Шаг 10. Запиши в память (memory) Добавь файл в свою память про этот бот: путь проекта, токен где (в config.json — не показывать в чате), команды «отправь в бота / я отправил в бота», лимит 20 МБ, как перезапустить, что есть `failed/` для неудачных файлов. ### Финальное сообщение студенту Скажи: - бот работает, его @username, обмен проверен в обе стороны; - автозапуск настроен (под определённой ОС — guardian.sh / launchd / папка автозагрузки); - лимит Telegram Bot API — 20 МБ на скачивание файла; - если файлы не доходят — смотри `err.log`, ищи строки `Не удалось отправить`; после 5 попыток файл уйдёт в `outbox/failed/`; - если случайно засветил токен → @BotFather → `/revoke`, замени в `config.json`, перезапусти. === КОНЕЦ ПРОМПТА ===