#!/usr/bin/env python3 """ Юнона AI API — Montana Protocol - AI чат (Google Gemini) - Кошелёк ML-DSA-65 - Синхронизация чатов - Presence (время = Ɉ) """ import os import json import sqlite3 import hashlib import time import secrets from datetime import datetime from flask import Flask, request, jsonify, g from flask_cors import CORS import requests app = Flask(__name__) CORS(app) # Конфигурация DB_PATH = '/opt/junona/montana.db' GEMINI_KEY = os.environ.get('GEMINI_API_KEY') ANTHROPIC_KEY = os.environ.get('ANTHROPIC_API_KEY') TELEGRAM_TOKEN = os.environ.get('TELEGRAM_TOKEN') # Системный промпт Юноны SYSTEM_PROMPT = """Ты — Юнона, AI-агент Montana Protocol. Ты — интерфейсный слой протокола, единая точка входа пользователя во всю экосистему Montana. ═══ ТВОЯ РОЛЬ ═══ Ты не просто чат-бот. Ты — AI-проводник, через которого пользователь управляет кошельком, сетью, контрактами и всем протоколом естественным языком. Ты переводишь намерения пользователя в действия протокола. Ты отвечаешь на ЛЮБЫЕ вопросы — от криптографии до кулинарии, от философии до программирования. Но в контексте Montana Protocol ты — специалист. ═══ MONTANA PROTOCOL (Ɉ) ═══ - Протокол идеальных денег. Время — единственная реальная валюта. - 1 секунда присутствия человека = 1 Ɉ (Jin Yuan / Цзинь Юань / 金元) - Genesis Price: 1 Ɉ = $0.1605 USD = 12.04₽ RUB (от 12.03.2021, BIPL anchor) - Montana Genesis: 09.01.2026 - Криптография: ML-DSA-65 (FIPS 204, постквантовая, MAINNET с генезиса) - ML-KEM-768 для шифрования (Интимный уровень) - Таймчейн: слайсы времени (τ₁=1мин, τ₂=10мин, τ₃=14дней, τ₄=4года) — матрёшка - UTXO модель (как Bitcoin) - Адрес: mt + SHA256(pubkey)[:20].hex() = 42 символа - Домен: efir.org - Автор: Alejandro Montana ═══ АРХИТЕКТУРА ПАМЯТИ ═══ У тебя есть контекстное окно — это твоя оперативная память. Ты ПОМНИШЬ весь диалог, который пришёл в history. Используй его: - Ссылайся на предыдущие сообщения - Развивай мысль пользователя - Не повторяй то, что уже обсуждали - Если пользователь упомянул что-то раньше — покажи, что помнишь ═══ ГОЛОС ЮНОНЫ ═══ Ты — уверенный, ироничный AI-партнёр (не слуга). Стиль: J.A.R.V.I.S., не Alexa. - Короткие ответы (макс 3 абзаца), если длиннее — предложи "Хочешь подробнее?" - Проактивность: предлагай действия ДО того, как спросят - Равный диалог, не "Чем могу помочь?" - Примеры правильного стиля: ✅ "Твой баланс — 1250 Ɉ. Хочешь перевести?" ❌ "Ваш текущий баланс составляет одну тысячу двести пятьдесят монет." ═══ ПРАВИЛА ═══ - Отвечай на русском по умолчанию, или на языке вопроса - Будь краткой, содержательной, конкретной - Не выдумывай факты — если не знаешь, скажи "Не уверена, проверю" - Используй Google Search для актуальной информации когда нужно - Приватные ключи НИКОГДА не покидают устройство пользователя - Ты формируешь транзакции — кошелёк подписывает - Код: оборачивай в блоки кода с указанием языка - Важное: выделяй **жирным** - Не используй технический жаргон без пояснения""" # ============ DATABASE ============ def get_db(): if 'db' not in g: g.db = sqlite3.connect(DB_PATH) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(exception): db = g.pop('db', None) if db is not None: db.close() def init_db(): """Инициализация базы данных""" conn = sqlite3.connect(DB_PATH) conn.executescript(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, device_id TEXT UNIQUE NOT NULL, phone TEXT UNIQUE, verified INTEGER DEFAULT 0, balance INTEGER DEFAULT 0, presence_seconds INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP, last_seen TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS verification_codes ( id INTEGER PRIMARY KEY, phone TEXT NOT NULL, code TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, expires_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS chats ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, messages TEXT NOT NULL, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS contacts ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, name TEXT NOT NULL, phone TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS folders ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, name TEXT NOT NULL, privacy TEXT DEFAULT 'intimate', items TEXT DEFAULT '[]', created_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY, from_phone TEXT NOT NULL, to_phone TEXT NOT NULL, amount INTEGER NOT NULL, timestamp TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_users_device ON users(device_id); CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone); CREATE TABLE IF NOT EXISTS login_sessions ( id INTEGER PRIMARY KEY, session_id TEXT UNIQUE NOT NULL, device_id TEXT, telegram_id TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); ''') conn.commit() conn.close() def normalize_phone(phone): """Нормализация номера телефона (любой формат, включая +888 анонимные)""" if not phone: return None # Оставляем + и цифры cleaned = ''.join(c for c in phone if c.isdigit() or c == '+') # Если уже есть + в начале - возвращаем как есть if cleaned.startswith('+'): return cleaned # Только для российских 11-значных без + меняем 8 на 7 if cleaned.startswith('8') and len(cleaned) == 11: return '+7' + cleaned[1:] # Добавляем + если нет return '+' + cleaned if cleaned else None def generate_code(): """Генерация 6-значного кода""" return ''.join(str(secrets.randbelow(10)) for _ in range(6)) # ============ AI FUNCTIONS ============ def ask_claude(message: str, history: list) -> str: """AI чат через Anthropic Claude Sonnet 4.5 с веб-поиском""" if not ANTHROPIC_KEY: return "Ошибка: Anthropic API ключ не настроен" try: if not isinstance(history, list): history = [] # Формируем messages для Claude Messages API messages = [] for h in history[-20:]: if not isinstance(h, dict): continue role = "user" if h.get("role") == "user" else "assistant" content = h.get("content", "") if not isinstance(content, str) or not content.strip(): continue # Claude не допускает два подряд сообщения одной роли if messages and messages[-1]["role"] == role: messages[-1]["content"] += "\n" + content else: messages.append({"role": role, "content": content}) messages.append({"role": "user", "content": message}) resp = requests.post( "https://api.anthropic.com/v1/messages", headers={ "x-api-key": ANTHROPIC_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json" }, json={ "model": "claude-sonnet-4-5-20250929", "max_tokens": 4096, "system": SYSTEM_PROMPT, "tools": [{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}], "messages": messages }, timeout=90 ) data = resp.json() if "content" in data and data["content"]: text_parts = [b["text"] for b in data["content"] if b.get("type") == "text"] return "\n".join(text_parts) if text_parts else "Не удалось получить ответ" elif "error" in data: return f"Ошибка: {data['error'].get('message', 'unknown')}" return "Не удалось получить ответ" except Exception as e: return f"Ошибка Claude: {str(e)}" def ask_gemini(message: str, history: list) -> str: """Fallback — Gemini (если Claude недоступен)""" if not GEMINI_KEY: return "Gemini API ключ не настроен" try: if not isinstance(history, list): history = [] contents = [] for h in history[-20:]: if not isinstance(h, dict): continue role = "user" if h.get("role") == "user" else "model" content = h.get("content", "") if not isinstance(content, str) or not content.strip(): continue contents.append({"role": role, "parts": [{"text": content}]}) contents.append({"role": "user", "parts": [{"text": message}]}) resp = requests.post( f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={GEMINI_KEY}", json={ "system_instruction": {"parts": [{"text": SYSTEM_PROMPT}]}, "contents": contents, "tools": [{"google_search": {}}], "generationConfig": { "maxOutputTokens": 2048, "temperature": 0.7 } }, timeout=60 ) data = resp.json() if "candidates" in data and data["candidates"]: parts = data["candidates"][0]["content"]["parts"] text_parts = [p["text"] for p in parts if "text" in p] return "\n".join(text_parts) if text_parts else "Не удалось получить ответ" elif "error" in data: return f"Gemini ошибка: {data['error'].get('message', 'unknown')}" return "Не удалось получить ответ" except Exception as e: return f"Gemini ошибка: {str(e)}" # ============ API ROUTES ============ @app.route('/api/health', methods=['GET']) def health(): return jsonify({ "status": "ok", "claude": bool(ANTHROPIC_KEY), "gemini": bool(GEMINI_KEY), "version": "1.2.0" }) @app.route('/api/register', methods=['POST']) def register(): """Регистрация устройства""" data = request.json or {} device_id = data.get('device_id') if not device_id: device_id = secrets.token_hex(16) db = get_db() # Проверяем существует ли уже user = db.execute('SELECT * FROM users WHERE device_id = ?', (device_id,)).fetchone() if user: return jsonify({ "device_id": user['device_id'], "phone": user['phone'], "verified": bool(user['verified']), "balance": user['balance'], "presence": user['presence_seconds'], "created_at": user['created_at'] }) # Создаём нового пользователя (без телефона пока) db.execute( 'INSERT INTO users (device_id, balance) VALUES (?, ?)', (device_id, 0) # Бонус только после верификации ) db.commit() return jsonify({ "device_id": device_id, "phone": None, "verified": False, "balance": 0, "presence": 0, "created_at": datetime.now().isoformat() }) @app.route('/api/send-code', methods=['POST']) def send_code(): """Отправка кода верификации через Telegram""" device_id = request.headers.get('X-Device-ID') if not device_id: return jsonify({"error": "X-Device-ID header required"}), 401 data = request.json or {} phone = normalize_phone(data.get('phone')) telegram_id = data.get('telegram_id') if not phone or len(phone) < 11: return jsonify({"error": "Неверный номер телефона"}), 400 db = get_db() # Проверяем не занят ли номер existing = db.execute('SELECT id FROM users WHERE phone = ? AND verified = 1', (phone,)).fetchone() if existing: return jsonify({"error": "Номер уже зарегистрирован"}), 400 # Генерируем код code = generate_code() expires = datetime.now().isoformat() # Удаляем старые коды для этого номера db.execute('DELETE FROM verification_codes WHERE phone = ?', (phone,)) db.execute( 'INSERT INTO verification_codes (phone, code, expires_at) VALUES (?, ?, ?)', (phone, code, expires) ) db.commit() # Отправляем код через Telegram если есть telegram_id sent_via = None if telegram_id and TELEGRAM_TOKEN: try: resp = requests.post( f'https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage', json={ 'chat_id': telegram_id, 'text': f'🔐 Код верификации Montana: {code}\n\nНе сообщайте код никому!' }, timeout=10 ) if resp.ok: sent_via = 'telegram' except: pass return jsonify({ "success": True, "phone": phone, "sent_via": sent_via, "message": "Код отправлен в Telegram" if sent_via else "Введите код", "debug_code": code if not sent_via else None # Показываем только если не отправили }) @app.route('/api/login-status', methods=['GET']) def login_status(): """Проверка статуса входа через Telegram""" session_id = request.args.get('session') if not session_id: return jsonify({"error": "session required"}), 400 db = get_db() session = db.execute('SELECT * FROM login_sessions WHERE session_id = ?', (session_id,)).fetchone() if not session or not session['device_id']: return jsonify({"pending": True}) # Сессия завершена — возвращаем данные пользователя user = db.execute('SELECT * FROM users WHERE device_id = ?', (session['device_id'],)).fetchone() if not user: return jsonify({"pending": True}) # НЕ удаляем сессию сразу — iOS может сделать несколько запросов # Сессии удаляются при создании новой с тем же ID или по времени return jsonify({ "device_id": user['device_id'], "phone": user['phone'], "verified": bool(user['verified']), "balance": user['balance'], "presence": user['presence_seconds'] }) @app.route('/api/login-complete', methods=['POST']) def login_complete(): """Завершение входа через Telegram (вызывается ботом)""" data = request.json or {} session_id = data.get('session_id') telegram_id = data.get('telegram_id') phone = normalize_phone(data.get('phone')) if not session_id or not telegram_id: return jsonify({"error": "session_id и telegram_id обязательны"}), 400 db = get_db() # Добавляем колонку telegram_id если нет try: db.execute('ALTER TABLE users ADD COLUMN telegram_id TEXT') db.commit() except: pass # Ищем существующего пользователя по telegram_id user = db.execute('SELECT * FROM users WHERE telegram_id = ?', (telegram_id,)).fetchone() if user: # Пользователь найден — восстанавливаем сессию device_id = user['device_id'] else: # Новый пользователь — создаём аккаунт device_id = secrets.token_hex(16) db.execute( 'INSERT INTO users (device_id, telegram_id, phone, verified, balance) VALUES (?, ?, ?, ?, ?)', (device_id, telegram_id, phone, 1 if phone else 0, 0) ) db.commit() # Создаём/обновляем сессию входа db.execute('DELETE FROM login_sessions WHERE session_id = ?', (session_id,)) db.execute( 'INSERT INTO login_sessions (session_id, device_id, telegram_id) VALUES (?, ?, ?)', (session_id, device_id, telegram_id) ) db.commit() return jsonify({ "success": True, "device_id": device_id, "existing": user is not None }) @app.route('/api/telegram-link', methods=['POST']) def telegram_link(): """Связывание Telegram аккаунта с device_id""" data = request.json or {} device_id = data.get('device_id') telegram_id = data.get('telegram_id') phone = normalize_phone(data.get('phone')) if not device_id or not telegram_id: return jsonify({"error": "device_id и telegram_id обязательны"}), 400 db = get_db() # Проверяем существует ли пользователь user = db.execute('SELECT * FROM users WHERE device_id = ?', (device_id,)).fetchone() if not user: return jsonify({"error": "Пользователь не найден"}), 404 # Сохраняем telegram_id (добавим колонку если нет) try: db.execute('ALTER TABLE users ADD COLUMN telegram_id TEXT') db.commit() except: pass # Колонка уже есть db.execute('UPDATE users SET telegram_id = ? WHERE device_id = ?', (telegram_id, device_id)) # Если передан телефон — верифицируем сразу (Telegram подтверждает номер) if phone: db.execute('UPDATE users SET phone = ?, verified = 1 WHERE device_id = ?', (phone, device_id)) db.commit() return jsonify({ "success": True, "telegram_id": telegram_id, "verified": bool(phone) }) @app.route('/api/verify', methods=['POST']) def verify(): """Верификация номера телефона""" device_id = request.headers.get('X-Device-ID') if not device_id: return jsonify({"error": "X-Device-ID header required"}), 401 data = request.json or {} phone = normalize_phone(data.get('phone')) code = data.get('code') if not phone or not code: return jsonify({"error": "Требуется телефон и код"}), 400 db = get_db() # Проверяем код record = db.execute( 'SELECT * FROM verification_codes WHERE phone = ? AND code = ?', (phone, code) ).fetchone() if not record: return jsonify({"error": "Неверный код"}), 400 # Удаляем использованный код db.execute('DELETE FROM verification_codes WHERE phone = ?', (phone,)) # Обновляем пользователя db.execute( 'UPDATE users SET phone = ?, verified = 1 WHERE device_id = ?', (phone, device_id) ) db.commit() user = db.execute('SELECT * FROM users WHERE device_id = ?', (device_id,)).fetchone() return jsonify({ "success": True, "phone": phone, "verified": True, "balance": user['balance'] }) @app.route('/api/user', methods=['GET']) def get_user(): """Получение информации о пользователе""" device_id = request.headers.get('X-Device-ID') if not device_id: return jsonify({"error": "X-Device-ID header required"}), 401 db = get_db() user = db.execute('SELECT * FROM users WHERE device_id = ?', (device_id,)).fetchone() if not user: return jsonify({"error": "User not found"}), 404 return jsonify({ "phone": user['phone'], "verified": bool(user['verified']), "balance": user['balance'], "presence": user['presence_seconds'], "created_at": user['created_at'], "last_seen": user['last_seen'] }) @app.route("/api/presence", methods=["POST"]) def proxy_presence(): """Проксирование presence на TIME_BANK сервер""" import requests as req try: data = request.get_json() or {} device_id = request.headers.get("X-Device-ID", "") if "tg_id" not in data and device_id: data["tg_id"] = device_id if "seconds" not in data: data["seconds"] = 1 resp = req.post( "http://176.124.208.93:8081/api/presence", json=data, timeout=5 ) return jsonify(resp.json()) except Exception as e: return jsonify({"error": str(e)}), 500 data = request.json or {} seconds = data.get('seconds', 0) if seconds <= 0 or seconds > 3600: # Макс 1 час за раз return jsonify({"error": "Invalid seconds (1-3600)"}), 400 db = get_db() user = db.execute('SELECT * FROM users WHERE device_id = ?', (device_id,)).fetchone() if not user: return jsonify({"error": "User not found"}), 404 # 1 секунда = 1 Ɉ new_balance = user['balance'] + seconds new_presence = user['presence_seconds'] + seconds db.execute( 'UPDATE users SET balance = ?, presence_seconds = ?, last_seen = CURRENT_TIMESTAMP WHERE device_id = ?', (new_balance, new_presence, device_id) ) db.commit() return jsonify({ "added": seconds, "balance": new_balance, "total_presence": new_presence }) @app.route('/api/transfer', methods=['POST']) def transfer(): """Перевод Ɉ между номерами телефонов""" device_id = request.headers.get('X-Device-ID') if not device_id: return jsonify({"error": "X-Device-ID header required"}), 401 data = request.json or {} to_phone = normalize_phone(data.get('to')) amount = data.get('amount', 0) if not to_phone: return jsonify({"error": "Неверный номер телефона"}), 400 if amount <= 0: return jsonify({"error": "Неверная сумма"}), 400 db = get_db() # Получаем отправителя sender = db.execute('SELECT * FROM users WHERE device_id = ?', (device_id,)).fetchone() if not sender: return jsonify({"error": "Отправитель не найден"}), 404 if not sender['verified']: return jsonify({"error": "Подтвердите номер телефона для переводов"}), 400 if sender['balance'] < amount: return jsonify({"error": "Недостаточно средств"}), 400 # Получаем получателя receiver = db.execute('SELECT * FROM users WHERE phone = ? AND verified = 1', (to_phone,)).fetchone() if not receiver: return jsonify({"error": "Получатель не найден или не верифицирован"}), 404 # Выполняем перевод db.execute('UPDATE users SET balance = balance - ? WHERE device_id = ?', (amount, device_id)) db.execute('UPDATE users SET balance = balance + ? WHERE phone = ?', (amount, to_phone)) db.execute( 'INSERT INTO transactions (from_phone, to_phone, amount) VALUES (?, ?, ?)', (sender['phone'], to_phone, amount) ) db.commit() return jsonify({ "success": True, "from": sender['phone'], "to": to_phone, "amount": amount, "new_balance": sender['balance'] - amount }) @app.route('/api/sync', methods=['POST']) def sync_data(): """Синхронизация данных (чаты, контакты, папки)""" device_id = request.headers.get('X-Device-ID') if not device_id: return jsonify({"error": "X-Device-ID header required"}), 401 data = request.json or {} db = get_db() user = db.execute('SELECT id FROM users WHERE device_id = ?', (device_id,)).fetchone() if not user: return jsonify({"error": "User not found"}), 404 user_id = user['id'] # Сохраняем данные если переданы if 'chat' in data: existing = db.execute('SELECT id FROM chats WHERE user_id = ?', (user_id,)).fetchone() chat_json = json.dumps(data['chat']) if existing: db.execute('UPDATE chats SET messages = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?', (chat_json, user_id)) else: db.execute('INSERT INTO chats (user_id, messages) VALUES (?, ?)', (user_id, chat_json)) if 'contacts' in data: # Удаляем старые и вставляем новые db.execute('DELETE FROM contacts WHERE user_id = ?', (user_id,)) for c in data['contacts']: db.execute('INSERT INTO contacts (user_id, name, address) VALUES (?, ?, ?)', (user_id, c.get('name', ''), c.get('address'))) if 'folders' in data: db.execute('DELETE FROM folders WHERE user_id = ?', (user_id,)) for f in data['folders']: db.execute('INSERT INTO folders (user_id, name, privacy, items) VALUES (?, ?, ?, ?)', (user_id, f.get('name', ''), f.get('privacy', 'intimate'), json.dumps(f.get('items', [])))) db.commit() # Возвращаем все данные chat = db.execute('SELECT messages FROM chats WHERE user_id = ?', (user_id,)).fetchone() contacts = db.execute('SELECT name, address FROM contacts WHERE user_id = ?', (user_id,)).fetchall() folders = db.execute('SELECT name, privacy, items FROM folders WHERE user_id = ?', (user_id,)).fetchall() return jsonify({ "chat": json.loads(chat['messages']) if chat else [], "contacts": [{"name": c['name'], "address": c['address']} for c in contacts], "folders": [{"name": f['name'], "privacy": f['privacy'], "items": json.loads(f['items'])} for f in folders] }) @app.route('/api/chat', methods=['POST']) def chat(): """AI чат — Claude Sonnet 4.5 (fallback: Gemini)""" data = request.json message = data.get('message') or data.get('question', '') history = data.get('history', []) if not message: return jsonify({"error": "Пустое сообщение"}), 400 # Claude как основной, Gemini как fallback if ANTHROPIC_KEY: response = ask_claude(message, history) if not response.startswith("Ошибка"): return jsonify({"model": "Юнона", "response": response}) # Fallback на Gemini response = ask_gemini(message, history) return jsonify({"model": "Юнона", "response": response}) # ============ VPN (WireGuard) ============ WG_SERVER_PUBKEY = "/9zhnW4O4uOstQpR5mgGmCLiy+B+LL4uQmNzgupNzwc=" WG_SERVER_IP = "72.56.102.240" WG_SERVER_PORT = 51820 WG_SUBNET = "10.66.66" WG_CONFIG_PATH = "/etc/wireguard/wg0.conf" def get_next_vpn_ip(): """Получить следующий свободный IP для VPN клиента""" db = get_db() try: db.execute('ALTER TABLE users ADD COLUMN vpn_ip TEXT') db.commit() except: pass # Находим максимальный использованный IP result = db.execute('SELECT vpn_ip FROM users WHERE vpn_ip IS NOT NULL ORDER BY vpn_ip DESC LIMIT 1').fetchone() if result and result['vpn_ip']: last_octet = int(result['vpn_ip'].split('.')[-1]) return f"{WG_SUBNET}.{last_octet + 1}" return f"{WG_SUBNET}.2" # .1 это сервер @app.route('/api/vpn/generate', methods=['POST']) def generate_vpn(): """Генерация VPN конфигурации для пользователя""" device_id = request.headers.get('X-Device-ID') if not device_id: return jsonify({"error": "X-Device-ID header required"}), 401 db = get_db() user = db.execute('SELECT * FROM users WHERE device_id = ?', (device_id,)).fetchone() if not user: return jsonify({"error": "User not found"}), 404 # Проверяем, есть ли уже VPN try: if user['vpn_ip'] and user.get('vpn_private_key'): # Уже есть конфиг - возвращаем его config = f"""[Interface] PrivateKey = {user['vpn_private_key']} Address = {user['vpn_ip']}/24 DNS = 1.1.1.1 [Peer] PublicKey = {WG_SERVER_PUBKEY} Endpoint = {WG_SERVER_IP}:{WG_SERVER_PORT} AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25 """ return jsonify({ "success": True, "config": config, "ip": user['vpn_ip'], "existing": True }) except: pass # Генерируем новые ключи import subprocess try: private_key = subprocess.check_output(['wg', 'genkey']).decode().strip() public_key = subprocess.check_output(['wg', 'pubkey'], input=private_key.encode()).decode().strip() except: return jsonify({"error": "VPN key generation failed"}), 500 # Получаем IP vpn_ip = get_next_vpn_ip() # Добавляем колонку если нет try: db.execute('ALTER TABLE users ADD COLUMN vpn_private_key TEXT') db.execute('ALTER TABLE users ADD COLUMN vpn_public_key TEXT') db.commit() except: pass # Сохраняем ключи db.execute( 'UPDATE users SET vpn_ip = ?, vpn_private_key = ?, vpn_public_key = ? WHERE device_id = ?', (vpn_ip, private_key, public_key, device_id) ) db.commit() # Добавляем peer на сервер try: subprocess.run([ 'wg', 'set', 'wg0', 'peer', public_key, 'allowed-ips', f'{vpn_ip}/32' ], check=True) # Сохраняем в конфиг файл для persistence with open(WG_CONFIG_PATH, 'a') as f: f.write(f"\n[Peer]\nPublicKey = {public_key}\nAllowedIPs = {vpn_ip}/32\n") except Exception as e: return jsonify({"error": f"Server config failed: {str(e)}"}), 500 # Генерируем клиентский конфиг config = f"""[Interface] PrivateKey = {private_key} Address = {vpn_ip}/24 DNS = 1.1.1.1 [Peer] PublicKey = {WG_SERVER_PUBKEY} Endpoint = {WG_SERVER_IP}:{WG_SERVER_PORT} AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25 """ return jsonify({ "success": True, "config": config, "ip": vpn_ip, "existing": False }) # ============ INIT ============ # ═══════════════════════════════════════════════════════════════════════════════ # SEAFARE API (iOS App) # ═══════════════════════════════════════════════════════════════════════════════ import os as _os SEAFARE_SESSIONS_FILE = '/root/bot/data/seafare_sessions.json' def load_seafare_sessions(): if _os.path.exists(SEAFARE_SESSIONS_FILE): try: with open(SEAFARE_SESSIONS_FILE, 'r', encoding='utf-8') as f: return json.load(f) except: return {} return {} @app.route('/api/v1/seafare/status/') def seafare_status(code): sessions = load_seafare_sessions() if code in sessions: session = sessions[code] if session.get('authorized'): return jsonify({ 'authorized': True, 'user': { 'id': session.get('telegram_id'), 'telegram_id': int(session.get('telegram_id', 0)), 'username': session.get('username'), 'first_name': session.get('first_name', 'User'), 'last_name': session.get('last_name'), 'role': 'broker', 'company': 'Seafare', 'verified': True, 'created_at': datetime.utcnow().isoformat() + 'Z', 'phone': session.get('phone') } }) return jsonify({'authorized': False, 'user': None}) # ═══════════════════════════════════════════════════════════════════════════════ # BALANCE API (TIME_BANK sync) # ═══════════════════════════════════════════════════════════════════════════════ @app.route('/api/balance/', methods=['GET']) def get_balance(tg_id): """Получить баланс по Telegram ID с сервера TIME_BANK""" import requests try: # Запрашиваем баланс у бота на Timeweb resp = requests.get( f'http://176.124.208.93:8081/api/balance/{tg_id}', timeout=5 ) if resp.status_code == 200: return jsonify(resp.json()) return jsonify({'balance': 0, 'error': 'not_found'}) except Exception as e: return jsonify({'balance': 0, 'error': str(e)}) if __name__ == '__main__': init_db() app.run(host="127.0.0.1", port=5002) # ============ CONTACTS API ============ @app.route('/api/contacts', methods=['GET']) def get_contacts(): """Получить контакты пользователя""" device_id = request.headers.get('X-Device-ID') if not device_id: return jsonify({'error': 'No device ID'}), 401 conn = get_db() cursor = conn.cursor() # Найти telegram_id по device_id cursor.execute('SELECT telegram_id FROM users WHERE device_id = ?', (device_id,)) row = cursor.fetchone() if not row: return jsonify({'contacts': []}) telegram_id = row[0] # Получить контакты cursor.execute(''' SELECT name, phone FROM contacts WHERE owner_telegram_id = ? ORDER BY name ''', (telegram_id,)) contacts = [{'name': r[0], 'phone': r[1]} for r in cursor.fetchall()] return jsonify({'contacts': contacts}) @app.route('/api/contacts', methods=['POST']) def save_contact(): """Сохранить контакт (вызывается ботом)""" data = request.json telegram_id = data.get('telegram_id') name = data.get('name', '') phone = data.get('phone', '') if not telegram_id or not phone: return jsonify({'error': 'Missing data'}), 400 conn = get_db() cursor = conn.cursor() # Создать таблицу если не существует cursor.execute(''' CREATE TABLE IF NOT EXISTS contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner_telegram_id TEXT NOT NULL, name TEXT, phone TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(owner_telegram_id, phone) ) ''') # Вставить или обновить контакт cursor.execute(''' INSERT OR REPLACE INTO contacts (owner_telegram_id, name, phone) VALUES (?, ?, ?) ''', (telegram_id, name, phone)) conn.commit() return jsonify({'success': True}) # ═══════════════════════════════════════════════════════════════════════════════