#!/usr/bin/env python3 """Minimal Botovod worker: only runtime credentials, bounded retry and one command result.""" import json import os from pathlib import Path import random import sys import time import httpx def main(): config_path = os.environ.get('BOT_CONFIG', '') config = json.loads(Path(config_path).read_text(encoding='utf-8')) if config_path else {} base = os.environ.get('BOT_API_URL', config.get('api_url', 'https://botovod.srgkslv.ru')).rstrip('/') token = os.environ.get('BOT_TOKEN', config.get('token', '')) if not token: sys.exit('Set BOT_TOKEN or BOT_CONFIG (path to the private provisioning JSON).') last_heartbeat = 0.0 backoff = 3.0 with httpx.Client(base_url=base, headers={'Authorization': f'Bearer {token}'}, timeout=30) as client: try: while True: try: if time.monotonic() - last_heartbeat >= 60: heartbeat = client.post('/api/bot/heartbeat', json={'status': 'online'}) heartbeat.raise_for_status() last_heartbeat = time.monotonic() pending = client.get('/api/bot/commands/pending') pending.raise_for_status() for command in pending.json()['commands']: text = command['text'].strip() if text == 'status': result = {'status': 'done', 'result': 'Bot is online'} elif text == 'help': result = {'status': 'done', 'result': 'Commands: status, help, echo '} elif text.startswith('echo '): result = {'status': 'done', 'result': text[5:] or '(empty)'} else: result = {'status': 'failed', 'error': 'Unknown command. Send help.'} # Retry the completion itself after network/5xx errors, not command execution. complete_command(client, command['id'], result) backoff = 3.0 time.sleep(3) except httpx.HTTPStatusError as error: status = error.response.status_code if status < 500 and status != 429: sys.exit(f'API rejected request (HTTP {status}); check credentials and payload.') print(f'API temporarily unavailable (HTTP {status}); retrying.', file=sys.stderr) time.sleep(backoff + random.random()) backoff = min(60, backoff * 2) except httpx.TransportError: print('Network unavailable; retrying.', file=sys.stderr) time.sleep(backoff + random.random()) backoff = min(60, backoff * 2) except KeyboardInterrupt: try: client.post('/api/bot/heartbeat', json={'status': 'offline'}) except httpx.HTTPError: pass def complete_command(client, command_id, result): for attempt in range(5): try: response = client.post(f'/api/bot/commands/{command_id}/complete', json=result) if response.status_code < 500 and response.status_code != 429: response.raise_for_status() return response.raise_for_status() except (httpx.TransportError, httpx.HTTPStatusError) as error: if isinstance(error, httpx.HTTPStatusError) and error.response.status_code < 500 and error.response.status_code != 429: raise if attempt == 4: raise time.sleep(min(30, 2 ** attempt) + random.random()) if __name__ == '__main__': main()