Compare commits

...

2 Commits

Author SHA1 Message Date
Alexey Polyakov bbee49d2d8 Коды стран 2026-03-19 16:48:59 +03:00
Alexey Polyakov 9bc6c15d82 Поделил процессоры в таме 2026-03-19 16:21:48 +03:00
6 changed files with 170 additions and 122 deletions

View File

@ -0,0 +1,47 @@
import logging
from common.config import ServerConfig
from common.static import Static
from common.tools import Tools
from common.proto_tcp import MobileProto
from common.proto_web import WebProto
from common.opcodes import Opcodes
class BaseProcessor:
def __init__(self, db_pool=None, clients=None, send_event=None, type="socket"):
if clients is None:
clients = {}
self.config = ServerConfig()
self.static = Static()
self.tools = Tools()
self.opcodes = Opcodes()
self.error_types = self.static.ErrorTypes()
self.db_pool = db_pool
self.clients = clients
self.send_event = send_event
self.logger = logging.getLogger(__name__)
if type == "socket":
self.proto = MobileProto()
elif type == "web":
self.proto = WebProto()
async def _send(self, writer, packet):
try:
writer.write(packet)
await writer.drain()
except Exception:
pass
async def _send_error(self, seq, opcode, error_type, writer):
payload = self.static.ERROR_TYPES.get(error_type, {
"localizedMessage": "Неизвестная ошибка",
"error": "unknown.error",
"message": "Unknown error",
"title": "Неизвестная ошибка"
})
packet = self.proto.pack_packet(
cmd=self.proto.CMD_ERR, seq=seq, opcode=opcode, payload=payload
)
await self._send(writer, packet)

View File

@ -176,3 +176,23 @@ class Static:
"M_CALL_PUSH_NOTIFICATION": "ON", "M_CALL_PUSH_NOTIFICATION": "ON",
"QUICK_REPLY": False "QUICK_REPLY": False
} }
### Коды стран, которым разрешён вход
REG_COUNTRY_CODES = ['AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW',
'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN',
'BO', 'BR', 'BS', 'BT', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG', 'CH', 'CI',
'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DE', 'DJ',
'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'ER', 'ES', 'ET', 'FI', 'FJ', 'FK', 'FM',
'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL', 'GM', 'GN', 'GP',
'GQ', 'GR', 'GT', 'GU', 'GW', 'GY', 'HK', 'HN', 'HR', 'HT', 'HU', 'ID', 'IE', 'IL',
'IM', 'IS', 'IN', 'IO', 'IQ', 'IR', 'IT', 'JE', 'JM', 'JO', 'JP', 'KE', 'KG', 'KH',
'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LI', 'LK', 'LR',
'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MK', 'ML',
'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ',
'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP', 'NR', 'NU', 'NZ', 'OM', 'PA',
'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PS', 'PT', 'PW', 'PY', 'QA',
'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD', 'SE', 'SG', 'SH', 'SI', 'SK',
'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SX', 'SY', 'SZ', 'TC', 'TD', 'TG',
'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG',
'US', 'UY', 'UZ', 'VA', 'VC', 'VE', 'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'XK', 'YE',
'YT', 'ZA', 'ZM', 'ZW']

View File

@ -73,14 +73,7 @@ class Processors:
payload = { payload = {
"location": "RU", "location": "RU",
"app-update-type": 0, # 1 = принудительное обновление "app-update-type": 0, # 1 = принудительное обновление
"reg-country-code": [ "reg-country-code": self.static.REG_COUNTRY_CODES,
# Список стран, который отдает официальный сервер
"AZ", "AM", "KZ", "KG", "MD", "TJ", "UZ", "GE", "TH", "TR",
"TM", "AE", "LA", "MY", "ID", "CU", "KH", "VN",
# Список стран, который приделали уже мы
"US", "CA", "UA"
],
"phone-auto-complete-enabled": False, "phone-auto-complete-enabled": False,
"lang": True "lang": True
} }

View File

@ -0,0 +1,5 @@
from .main import MainProcessors
from .auth import AuthProcessors
class Processors(MainProcessors, AuthProcessors):
pass

View File

@ -1,102 +1,32 @@
import hashlib import hashlib
import secrets import secrets
import time import time
import logging
import json import json
import re import re
from common.static import Static from classes.baseprocessor import BaseProcessor
from common.tools import Tools from tamtam.models import (
RequestCodePayloadModel,
from common.proto_tcp import MobileProto VerifyCodePayloadModel,
from common.proto_web import WebProto FinalAuthPayloadModel,
from common.opcodes import Opcodes LoginPayloadModel,
)
from tamtam.models import *
class Processors:
def __init__(self, db_pool=None, clients=None, send_event=None, type="socket"):
if clients is None:
clients = {} # Более правильная логика
self.static = Static()
self.tools = Tools()
self.opcodes = Opcodes()
self.error_types = self.static.ErrorTypes()
self.db_pool = db_pool
self.logger = logging.getLogger(__name__)
if type == "socket":
self.proto = MobileProto()
elif type == "web":
self.proto = WebProto()
async def _send(self, writer, packet):
try:
writer.write(packet)
await writer.drain()
except:
pass
async def _send_error(self, seq, opcode, type, writer):
payload = self.static.ERROR_TYPES.get(type, {
"localizedMessage": "Неизвестная ошибка",
"error": "unknown.error",
"message": "Unknown error",
"title": "Неизвестная ошибка"
})
packet = self.proto.pack_packet(
cmd=self.proto.CMD_ERR, seq=seq, opcode=opcode, payload=payload
)
await self._send(writer, packet)
async def session_init(self, payload, seq, writer):
"""Обработчик приветствия"""
# Валидируем данные пакета
try:
HelloPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.SESSION_INIT, self.error_types.INVALID_PAYLOAD, writer)
return None, None
# Получаем данные из пакета
device_type = payload.get("userAgent").get("deviceType")
device_name = payload.get("userAgent").get("deviceName")
# Данные пакета
payload = {
"proxy": "",
"logs-enabled": False,
"proxy-domains": [],
"location": "RU",
"libh-enabled": False,
"phone-auto-complete-enabled": False
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.SESSION_INIT, payload=payload
)
# Отправляем
await self._send(writer, packet)
return device_type, device_name
class AuthProcessors(BaseProcessor):
async def auth_request(self, payload, seq, writer): async def auth_request(self, payload, seq, writer):
"""Обработчик запроса кода""" """Обработчик запроса кода"""
# Валидируем данные пакета # Валидируем данные пакета
try: try:
RequestCodePayloadModel.model_validate(payload) RequestCodePayloadModel.model_validate(payload)
except Exception as e: except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_REQUEST, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.AUTH_REQUEST,
self.error_types.INVALID_PAYLOAD, writer)
return return
# Извлекаем телефон из пакета # Извлекаем телефон из пакета
phone = re.sub(r'\D', '', payload.get("phone", "")) # Не хардкодим, через регулярки phone = re.sub(r'\D', '', payload.get("phone", ""))
# Генерируем токен с кодом # Генерируем токен с кодом
code = f"{secrets.randbelow(1_000_000):06d}" # Старая версия ненадежна, могла отбросить ведущие нули или вообще интерпритировать как систему счисления с основанием 8 code = f"{secrets.randbelow(1_000_000):06d}"
token = secrets.token_urlsafe(128) token = secrets.token_urlsafe(128)
# Хешируем # Хешируем
@ -114,8 +44,10 @@ class Processors:
# Если пользователь существует, сохраняем токен # Если пользователь существует, сохраняем токен
if user: if user:
# Сохраняем токен await cursor.execute(
await cursor.execute("INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)", (phone, token_hash, code_hash, expires, "started",)) "INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)",
(phone, token_hash, code_hash, expires, "started")
)
# Данные пакета # Данные пакета
payload = { payload = {
@ -142,7 +74,8 @@ class Processors:
try: try:
VerifyCodePayloadModel.model_validate(payload) VerifyCodePayloadModel.model_validate(payload)
except Exception as e: except Exception as e:
await self._send_error(seq, self.opcodes.AUTH, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.AUTH,
self.error_types.INVALID_PAYLOAD, writer)
return return
# Извлекаем данные из пакета # Извлекаем данные из пакета
@ -157,17 +90,21 @@ class Processors:
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
# Ищем токен # Ищем токен
await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", await cursor.execute(
(hashed_token,)) "SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
(hashed_token,)
)
stored_token = await cursor.fetchone() stored_token = await cursor.fetchone()
if not stored_token: if not stored_token:
await self._send_error(seq, self.opcodes.AUTH, self.error_types.CODE_EXPIRED, writer) await self._send_error(seq, self.opcodes.AUTH,
self.error_types.CODE_EXPIRED, writer)
return return
# Проверяем код # Проверяем код
if stored_token.get("code_hash") != hashed_code: if stored_token.get("code_hash") != hashed_code:
await self._send_error(seq, self.opcodes.AUTH, self.error_types.INVALID_CODE, writer) await self._send_error(seq, self.opcodes.AUTH,
self.error_types.INVALID_CODE, writer)
return return
# Ищем аккаунт # Ищем аккаунт
@ -175,7 +112,10 @@ class Processors:
account = await cursor.fetchone() account = await cursor.fetchone()
# Обновляем состояние токена # Обновляем состояние токена
await cursor.execute("UPDATE auth_tokens set state = %s WHERE token_hash = %s", ("verified", hashed_token,)) await cursor.execute(
"UPDATE auth_tokens set state = %s WHERE token_hash = %s",
("verified", hashed_token)
)
# Генерируем профиль # Генерируем профиль
# Аватарка с биографией # Аватарка с биографией
@ -219,7 +159,8 @@ class Processors:
try: try:
FinalAuthPayloadModel.model_validate(payload) FinalAuthPayloadModel.model_validate(payload)
except Exception as e: except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return return
# Извлекаем данные из пакета # Извлекаем данные из пакета
@ -239,17 +180,21 @@ class Processors:
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
# Ищем токен # Ищем токен
await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", await cursor.execute(
(hashed_token,)) "SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
(hashed_token,)
)
stored_token = await cursor.fetchone() stored_token = await cursor.fetchone()
if stored_token is None: if stored_token is None:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, self.error_types.INVALID_TOKEN, writer) await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer)
return return
# Если авторизация только началась - отдаем ошибку # Если авторизация только началась - отдаем ошибку
if stored_token.get("state") == "started": if stored_token.get("state") == "started":
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, self.error_types.INVALID_TOKEN, writer) await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer)
return return
# Ищем аккаунт # Ищем аккаунт
@ -262,13 +207,13 @@ class Processors:
# Создаем сессию # Создаем сессию
await cursor.execute( await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)", "INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(stored_token.get("phone"), hashed_login, deviceType, deviceName, "Epstein Island", (stored_token.get("phone"), hashed_login, deviceType, deviceName,
int(time.time()),) "Epstein Island", int(time.time()))
) )
# Аватарка с биографией # Аватарка с биографией
photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id")) photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + photo_id avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not account.get("description") else account.get("description") description = None if not account.get("description") else account.get("description")
# Собираем данные пакета # Собираем данные пакета
@ -295,7 +240,7 @@ class Processors:
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload
) )
# Отправялем # Отправляем
await self._send(writer, packet) await self._send(writer, packet)
async def login(self, payload, seq, writer): async def login(self, payload, seq, writer):
@ -303,9 +248,10 @@ class Processors:
# Валидируем данные пакета # Валидируем данные пакета
try: try:
LoginPayloadModel.model_validate(payload) LoginPayloadModel.model_validate(payload)
except pydantic.ValidationError as error: except Exception as e:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}") self.logger.error(f"Возникли ошибки при валидации пакета: {e}")
await self._send_error(seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_PAYLOAD, writer)
return return
# Получаем данные из пакета # Получаем данные из пакета
@ -322,7 +268,8 @@ class Processors:
# Если токен не найден, отправляем ошибку # Если токен не найден, отправляем ошибку
if token_data is None: if token_data is None:
await self._send_error(seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer) await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_TOKEN, writer)
return return
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
@ -335,7 +282,7 @@ class Processors:
# Аватарка с биографией # Аватарка с биографией
photo_id = None if not user.get("avatar_id") else int(user.get("avatar_id")) photo_id = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + photo_id avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not user.get("description") else user.get("description") description = None if not user.get("description") else user.get("description")
# Генерируем профиль # Генерируем профиль

View File

@ -0,0 +1,36 @@
from classes.baseprocessor import BaseProcessor
from tamtam.models import HelloPayloadModel
class MainProcessors(BaseProcessor):
async def session_init(self, payload, seq, writer):
"""Обработчик приветствия"""
# Валидируем данные пакета
try:
HelloPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.SESSION_INIT,
self.error_types.INVALID_PAYLOAD, writer)
return None, None
# Получаем данные из пакета
device_type = payload.get("userAgent").get("deviceType")
device_name = payload.get("userAgent").get("deviceName")
# Данные пакета
payload = {
"proxy": "",
"logs-enabled": False,
"proxy-domains": [],
"location": "RU",
"libh-enabled": False,
"phone-auto-complete-enabled": False
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.SESSION_INIT, payload=payload
)
# Отправляем
await self._send(writer, packet)
return device_type, device_name