montana/Русский/Сайт/junona_api.py

1018 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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/<code>')
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/<tg_id>', 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})
# ═══════════════════════════════════════════════════════════════════════════════