Files
aiform_prod/backend/app/services/sms_service.py
AI Assistant 2e45786e46 feat: Telegram Mini App integration and UX improvements
- Добавлена полная интеграция с Telegram Mini App (динамическая загрузка SDK)
- Отдельный компактный дизайн для Telegram Mini App
- Добавлен loader при инициализации (предотвращает мелькание SMS-авторизации)
- Улучшена навигация: кнопки "Назад" и "К списку заявок" теперь сохраняют авторизацию
- Telegram Mini App: кнопка "Выход" просто закрывает приложение
- Telegram Mini App: заявки "В работе" скрыты из списка
- Веб-версия: для заявок "В работе" добавлена кнопка "Просмотреть в Telegram" (ссылка на @klientprav_bot)
- Telegram Mini App: кнопки действий в черновиках расположены вертикально
- Веб-версия: убрано отображение номера телефона в приветствии
- Исправлена проблема с возвратом к списку черновиков (не требует повторной SMS-авторизации)
- Заблокировано удаление и редактирование заявок со статусом "В работе"
- Добавлена документация по Telegram Mini App интеграции
2026-01-29 16:12:48 +03:00

197 lines
7.7 KiB
Python
Raw 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.

"""
SMS Service для отправки кодов верификации (SigmaSMS)
"""
import httpx
import random
import logging
from typing import Optional
from ..config import settings
from .redis_service import redis_service
logger = logging.getLogger(__name__)
class SMSService:
"""Сервис для работы с SMS через SigmaSMS API"""
def __init__(self):
self.api_url = settings.sms_api_url
self.login = settings.sms_login
self.password = settings.sms_password
self.token = settings.sms_token
self.sender = settings.sms_sender
self.enabled = settings.sms_enabled
async def _get_token(self) -> Optional[str]:
"""Получить JWT токен для API"""
try:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.api_url}login",
json={
"username": self.login,
"password": self.password
},
timeout=10.0
)
if response.status_code == 200:
data = response.json()
return data.get("token")
else:
logger.error(f"Failed to get SMS token: {response.status_code}")
return self.token # Используем токен из .env как fallback
except Exception as e:
logger.error(f"Error getting SMS token: {e}")
return self.token
def generate_code(self) -> str:
"""Генерировать 6-значный код"""
return str(random.randint(100000, 999999))
async def send_sms(self, phone: str, message: str) -> bool:
"""
Отправить SMS
Args:
phone: Номер телефона (формат: +79001234567)
message: Текст сообщения
Returns:
bool: True если отправлено успешно
"""
if not self.enabled:
logger.warning("SMS отправка отключена в конфигурации")
return False
# 🔧 DEV MODE: Не отправляем реальные SMS в development, экономим бюджет
if settings.debug or settings.app_env == "development":
logger.info(f"🔧 DEV MODE: SMS to {phone} not sent (saving money!)")
logger.info(f"📱 Message would be: {message}")
return True # Возвращаем True чтобы код сохранился в Redis для проверки
try:
# Получаем актуальный токен
token = await self._get_token()
if not token:
logger.error("No SMS token available")
return False
# Очищаем номер телефона
clean_phone = phone.replace("+", "").replace("-", "").replace(" ", "")
# Отправляем SMS
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.api_url}sendings",
headers={
"Authorization": f"Bearer {token}"
},
json={
"recipient": clean_phone,
"type": "sms",
"payload": {
"sender": self.sender,
"text": message
}
},
timeout=15.0
)
if response.status_code in [200, 201]:
logger.info(f"✅ SMS sent to {phone}")
return True
else:
logger.error(f"Failed to send SMS: {response.status_code} - {response.text}")
return False
except Exception as e:
logger.error(f"Error sending SMS: {e}")
return False
async def send_verification_code(self, phone: str) -> Optional[str]:
"""
Отправить код верификации на телефон
Args:
phone: Номер телефона
Returns:
str: Код верификации (для отладки) или None при ошибке
"""
# Нормализуем формат телефона (убираем + если есть)
phone = phone.replace("+", "").replace("-", "").replace(" ", "")
# Проверка rate limiting (не больше 1 SMS в минуту на номер)
# ВРЕМЕННО ОТКЛЮЧЕНО для тестирования
rate_limit_key = f"sms_rate:{phone}"
# if await redis_service.exists(rate_limit_key):
# ttl = await redis_service.client.ttl(f"{settings.redis_prefix}{rate_limit_key}")
# logger.warning(f"Rate limit for {phone}, retry in {ttl} seconds")
# return None
# Генерируем код
code = self.generate_code()
# Сохраняем код в Redis на 10 минут
verification_key = f"sms_verify:{phone}"
await redis_service.set(verification_key, code, expire=600) # 10 минут
# Устанавливаем rate limit на 60 секунд
# ВРЕМЕННО ОТКЛЮЧЕНО для тестирования - убрать задержку
# await redis_service.set(rate_limit_key, "1", expire=60)
# Формируем сообщение
message = f"Ваш код подтверждения: {code}. Действителен 10 минут."
# Отправляем SMS
success = await self.send_sms(phone, message)
if success:
logger.info(f"Verification code sent to {phone}")
return code # Возвращаем для отладки
else:
# Удаляем код если не удалось отправить
await redis_service.delete(verification_key)
return None
async def verify_code(self, phone: str, code: str) -> bool:
"""
Проверить код верификации
Args:
phone: Номер телефона
code: Код для проверки
Returns:
bool: True если код верный
"""
# Нормализуем формат телефона (убираем + если есть)
phone = phone.replace("+", "").replace("-", "").replace(" ", "")
verification_key = f"sms_verify:{phone}"
stored_code = await redis_service.get(verification_key)
if not stored_code:
logger.warning(f"No verification code found for {phone} (key: {verification_key})")
return False
logger.info(f"🔍 Comparing codes: stored='{stored_code}' vs input='{code}' (types: {type(stored_code).__name__} vs {type(code).__name__})")
if stored_code == code:
# Удаляем код после успешной проверки
await redis_service.delete(verification_key)
logger.info(f"✅ Code verified for {phone}")
return True
else:
logger.warning(f"❌ Invalid code for {phone}: expected '{stored_code}', got '{code}'")
return False
# Глобальный экземпляр
sms_service = SMSService()