1018 lines
38 KiB
Python
1018 lines
38 KiB
Python
#!/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})
|
||
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|