montana/Русский/Бот/montana_real_phone.py

326 lines
13 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""
Montana Protocol Real Phone Number Binding
Привязка реальных телефонных номеров к Montana адресам.
Модель:
1. Пользователь вводит свой реальный номер (+7-921-123-4567)
2. Montana отправляет SMS с кодом верификации
3. Пользователь вводит код
4. Номер привязывается к Montana адресу
5. Теперь номер = кошелек (может звонить за 1 Ɉ/сек)
"""
import json
import threading
import fcntl
import time
import random
import hashlib
from pathlib import Path
from typing import Dict, Optional, List
from datetime import datetime, timedelta
import logging
log = logging.getLogger("montana_real_phone")
# ═══════════════════════════════════════════════════════════════════════════════
# REAL PHONE NUMBER BINDING SERVICE
# ═══════════════════════════════════════════════════════════════════════════════
class RealPhoneService:
"""
Сервис привязки реальных телефонных номеров к Montana адресам.
Workflow:
1. request_verification(phone, montana_address) отправляет SMS с кодом
2. verify_code(phone, code) проверяет код и привязывает номер
3. lookup(phone) возвращает Montana адрес владельца
"""
def __init__(self, data_dir: Path):
self.data_dir = data_dir
self.data_dir.mkdir(parents=True, exist_ok=True)
self.bindings_file = self.data_dir / "real_phone_bindings.json"
self.pending_file = self.data_dir / "real_phone_pending.json"
self._lock = threading.Lock()
self._ensure_files()
def _ensure_files(self):
"""Создать файлы если не существуют"""
if not self.bindings_file.exists():
self._save_bindings({})
if not self.pending_file.exists():
self._save_pending({})
def _load_bindings(self) -> Dict[str, Dict]:
"""Загрузить привязанные номера"""
with open(self.bindings_file, 'r') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
try:
data = json.load(f)
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
return data
def _save_bindings(self, bindings: Dict[str, Dict]):
"""Сохранить привязанные номера"""
with open(self.bindings_file, 'w') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
json.dump(bindings, f, indent=2)
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
def _load_pending(self) -> Dict[str, Dict]:
"""Загрузить pending верификации"""
with open(self.pending_file, 'r') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_SH)
try:
data = json.load(f)
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
return data
def _save_pending(self, pending: Dict[str, Dict]):
"""Сохранить pending верификации"""
with open(self.pending_file, 'w') as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
json.dump(pending, f, indent=2)
finally:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
def _normalize_phone(self, phone: str) -> str:
"""
Нормализовать телефонный номер.
Examples:
+7-921-123-4567 +79211234567
8 (921) 123-45-67 +79211234567
+1 (555) 123-4567 +15551234567
"""
# Удалить все кроме цифр и +
normalized = ''.join(c for c in phone if c.isdigit() or c == '+')
# Если начинается с 8 (Россия) → +7
if normalized.startswith('8') and len(normalized) == 11:
normalized = '+7' + normalized[1:]
# Добавить + если нет
if not normalized.startswith('+'):
# Определить страну по длине (упрощенно)
if len(normalized) == 10: # USA
normalized = '+1' + normalized
elif len(normalized) == 11: # Russia
normalized = '+' + normalized
return normalized
def _generate_code(self) -> str:
"""Сгенерировать 6-значный код верификации"""
return f"{random.randint(100000, 999999)}"
def _send_sms(self, phone: str, code: str) -> bool:
"""
Отправить SMS с кодом верификации.
В production это интегрируется с SMS провайдером (Twilio, etc.)
Сейчас просто логируем код.
"""
# TODO: Интеграция с SMS API (Twilio, MessageBird, etc.)
log.info(f"SMS → {phone}: Your Montana verification code: {code}")
# В dev режиме просто возвращаем True
# В production проверяем результат SMS API
return True
def request_verification(
self,
phone: str,
montana_address: str
) -> Dict:
"""
Запросить верификацию номера.
Args:
phone: Телефонный номер (любой формат)
montana_address: Montana адрес для привязки
Returns:
Dict с результатом запроса
"""
phone_normalized = self._normalize_phone(phone)
with self._lock:
bindings = self._load_bindings()
pending = self._load_pending()
# Проверить что номер не привязан к другому адресу
if phone_normalized in bindings:
existing_owner = bindings[phone_normalized]['montana_address']
if existing_owner != montana_address:
raise ValueError(
f"Phone number already bound to different address: {existing_owner[:10]}..."
)
return {
"status": "already_verified",
"phone": phone_normalized,
"montana_address": montana_address
}
# Сгенерировать код
code = self._generate_code()
# Сохранить в pending (expires in 10 minutes)
expires = (datetime.utcnow() + timedelta(minutes=10)).isoformat() + "Z"
pending[phone_normalized] = {
"code": code,
"montana_address": montana_address,
"expires": expires,
"created": datetime.utcnow().isoformat() + "Z",
"attempts": 0
}
self._save_pending(pending)
# Отправить SMS
sms_sent = self._send_sms(phone_normalized, code)
if not sms_sent:
raise Exception("Failed to send SMS")
log.info(f"Verification requested: {phone_normalized}{montana_address[:10]}...")
return {
"status": "code_sent",
"phone": phone_normalized,
"montana_address": montana_address,
"expires": expires,
"dev_code": code if __debug__ else None # Only in debug mode
}
def verify_code(
self,
phone: str,
code: str
) -> Dict:
"""
Проверить код верификации и привязать номер.
Args:
phone: Телефонный номер
code: 6-значный код из SMS
Returns:
Dict с результатом верификации
"""
phone_normalized = self._normalize_phone(phone)
with self._lock:
pending = self._load_pending()
if phone_normalized not in pending:
raise ValueError("No pending verification for this phone number")
verification = pending[phone_normalized]
# Проверить expiration
expires = datetime.fromisoformat(verification['expires'].replace('Z', '+00:00'))
if datetime.utcnow() > expires.replace(tzinfo=None):
del pending[phone_normalized]
self._save_pending(pending)
raise ValueError("Verification code expired")
# Проверить код
if verification['code'] != code:
verification['attempts'] += 1
if verification['attempts'] >= 3:
del pending[phone_normalized]
self._save_pending(pending)
raise ValueError("Too many failed attempts")
else:
self._save_pending(pending)
raise ValueError(f"Invalid code ({verification['attempts']}/3 attempts)")
# Код правильный — привязать номер
montana_address = verification['montana_address']
bindings = self._load_bindings()
bindings[phone_normalized] = {
"montana_address": montana_address,
"verified": datetime.utcnow().isoformat() + "Z",
"phone": phone_normalized,
"audio_seconds_used": 0,
"video_seconds_used": 0
}
self._save_bindings(bindings)
# Удалить из pending
del pending[phone_normalized]
self._save_pending(pending)
log.info(f"Phone verified: {phone_normalized}{montana_address[:10]}...")
return {
"status": "verified",
"phone": phone_normalized,
"montana_address": montana_address,
"verified": bindings[phone_normalized]['verified']
}
def lookup(self, phone: str) -> Optional[Dict]:
"""
Найти Montana адрес по телефонному номеру.
Args:
phone: Телефонный номер
Returns:
Dict с информацией о привязке или None
"""
phone_normalized = self._normalize_phone(phone)
with self._lock:
bindings = self._load_bindings()
if phone_normalized in bindings:
return bindings[phone_normalized].copy()
return None
def is_verified(self, phone: str) -> bool:
"""Проверить привязан ли номер"""
return self.lookup(phone) is not None
def get_by_address(self, montana_address: str) -> List[str]:
"""
Найти все телефонные номера привязанные к Montana адресу.
Args:
montana_address: Montana адрес
Returns:
Список телефонных номеров
"""
with self._lock:
bindings = self._load_bindings()
phones = []
for phone, info in bindings.items():
if info['montana_address'] == montana_address:
phones.append(phone)
return phones
# ═══════════════════════════════════════════════════════════════════════════════
# GLOBAL INSTANCE
# ═══════════════════════════════════════════════════════════════════════════════
_real_phone_service: Optional[RealPhoneService] = None
def get_real_phone_service(data_dir: Path = None) -> RealPhoneService:
"""Получить глобальный экземпляр RealPhoneService (singleton)"""
global _real_phone_service
if _real_phone_service is None:
if data_dir is None:
data_dir = Path(__file__).parent / "data"
_real_phone_service = RealPhoneService(data_dir)
return _real_phone_service