Fix OpenMAX mobile compatibility and Telegram auth fallback (#30)

* Fix OpenMAX mobile compatibility and Telegram auth fallback

* Common: Убрал скобку в конфиге

---------

Co-authored-by: Alexey Polyakov <starwear3000@mail.ru>
This commit is contained in:
Aleksandr Kosachev
2026-04-14 20:05:34 +03:00
committed by GitHub
parent d9cbafc4e3
commit d9798a6fc6
6 changed files with 290 additions and 103 deletions

View File

@@ -1,22 +1,31 @@
import json
import secrets
import hashlib
import time
import json
import logging
import secrets
import time
import pydantic
from classes.baseprocessor import BaseProcessor
from common.sms import send_sms_code
from oneme.config import OnemeConfig
from oneme.models import (
RequestCodePayloadModel,
VerifyCodePayloadModel,
AuthConfirmRegisterPayloadModel,
LoginPayloadModel,
RequestCodePayloadModel,
VerifyCodePayloadModel,
)
from oneme.config import OnemeConfig
from common.sms import send_sms_code
class AuthProcessors(BaseProcessor):
def __init__(self, db_pool=None, clients=None, send_event=None, telegram_bot=None, type="socket"):
def __init__(
self,
db_pool=None,
clients=None,
send_event=None,
telegram_bot=None,
type="socket",
):
super().__init__(db_pool, clients, send_event, type)
self.server_config = OnemeConfig().SERVER_CONFIG
self.telegram_bot = telegram_bot
@@ -27,7 +36,9 @@ class AuthProcessors(BaseProcessor):
RequestCodePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
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
# Извлекаем телефон из пакета
@@ -49,13 +60,16 @@ class AuthProcessors(BaseProcessor):
user = await cursor.fetchone()
# Получаем код через SMS шлюз или генерируем локально (безопасность прежде всего)
local_fallback_code = False
if self.config.sms_gateway_url:
code = await send_sms_code(self.config.sms_gateway_url, phone)
if code is None:
code = str(secrets.randbelow(900000) + 100000)
local_fallback_code = True
else:
code = str(secrets.randbelow(900000) + 100000)
local_fallback_code = True
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
@@ -68,18 +82,35 @@ class AuthProcessors(BaseProcessor):
# Сохраняем токен
await cursor.execute(
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires) VALUES (%s, %s, %s, %s)",
(phone, token_hash, code_hash, expires,)
(
phone,
token_hash,
code_hash,
expires,
),
)
# Если тг бот включен, и тг привязан к аккаунту - отправляем туда сообщение
if not self.config.sms_gateway_url and self.telegram_bot and user.get("telegram_id"):
await self.telegram_bot.send_code(chat_id=int(user.get("telegram_id")), phone=phone, code=code)
# Если код был сгенерирован локально, а тг привязан к аккаунту - отправляем туда сообщение
if (
local_fallback_code
and self.telegram_bot
and user.get("telegram_id")
):
await self.telegram_bot.send_code(
chat_id=int(user.get("telegram_id")), phone=phone, code=code
)
else:
# Пользователь не найден - сохраняем токен со state='register'
# чтобы после верификации кода направить на экран регистрации
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, "register",)
(
phone,
token_hash,
code_hash,
expires,
"register",
),
)
# Данные пакета
@@ -88,12 +119,15 @@ class AuthProcessors(BaseProcessor):
"codeLength": 6,
"requestMaxDuration": 60000,
"requestCountLeft": 10,
"altActionDuration": 60000
"altActionDuration": 60000,
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_REQUEST, payload=payload
cmd=self.proto.CMD_OK,
seq=seq,
opcode=self.opcodes.AUTH_REQUEST,
payload=payload,
)
# Отправляем
@@ -106,7 +140,9 @@ class AuthProcessors(BaseProcessor):
VerifyCodePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
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
# Извлекаем данные из пакета
@@ -127,18 +163,22 @@ class AuthProcessors(BaseProcessor):
# Ищем токен
await cursor.execute(
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
(hashed_token,)
(hashed_token,),
)
stored_token = await cursor.fetchone()
# Если токен просрочен, или его нет - отправляем ошибку
if stored_token is None:
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
# Проверяем код
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
# Если это новый пользователь - переводим токен в state='verified'
@@ -146,48 +186,60 @@ class AuthProcessors(BaseProcessor):
if stored_token.get("state") == "register":
await cursor.execute(
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s",
("verified", hashed_token,)
(
"verified",
hashed_token,
),
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH,
cmd=self.proto.CMD_OK,
seq=seq,
opcode=self.opcodes.AUTH,
payload={
"tokenAttrs": {
"REGISTER": {
"token": token
}
},
"presetAvatars": []
}
"tokenAttrs": {"REGISTER": {"token": token}},
"presetAvatars": [],
},
)
await self._send(writer, packet)
return
# Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
await cursor.execute(
"SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),)
)
account = await cursor.fetchone()
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
await cursor.execute(
"DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,)
)
# Создаем сессию
await cursor.execute(
"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, "Little Saint James Island", int(time.time()),) # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть
(
stored_token.get("phone"),
hashed_login,
deviceType,
deviceName,
"Little Saint James Island",
int(time.time()),
), # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть
)
# Генерируем профиль
# Аватарка с биографией
photoId = None if not account.get("avatar_id") else int(account.get("avatar_id"))
photoId = (
None if not account.get("avatar_id") else int(account.get("avatar_id"))
)
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not account.get("description") else account.get("description")
description = (
None if not account.get("description") else account.get("description")
)
# Собираем данные пакета
payload = {
"tokenAttrs": {
"LOGIN": {
"token": login
}
},
"tokenAttrs": {"LOGIN": {"token": login}},
"profile": self.tools.generate_profile(
id=account.get("id"),
phone=int(account.get("phone")),
@@ -201,8 +253,8 @@ class AuthProcessors(BaseProcessor):
accountStatus=int(account.get("accountstatus")),
profileOptions=json.loads(account.get("profileoptions")),
includeProfileOptions=True,
username=account.get("username")
)
username=account.get("username"),
),
}
# Создаем пакет
@@ -220,7 +272,9 @@ class AuthProcessors(BaseProcessor):
AuthConfirmRegisterPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
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
# Извлекаем данные из пакета
@@ -240,13 +294,21 @@ class AuthProcessors(BaseProcessor):
# Ищем токен - он должен быть в state='verified'
await cursor.execute(
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP() AND state = %s",
(hashed_token, "verified",)
(
hashed_token,
"verified",
),
)
stored_token = await cursor.fetchone()
# Если токен не найден или просрочен - отправляем ошибку
if stored_token is None:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, self.error_types.CODE_EXPIRED, writer)
await self._send_error(
seq,
self.opcodes.AUTH_CONFIRM,
self.error_types.CODE_EXPIRED,
writer,
)
return
phone = stored_token.get("phone")
@@ -254,7 +316,12 @@ class AuthProcessors(BaseProcessor):
# Проверяем что пользователь с таким телефоном ещё не существует
await cursor.execute("SELECT id FROM users WHERE phone = %s", (phone,))
if await cursor.fetchone():
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
now_ms = int(time.time() * 1000)
@@ -269,10 +336,18 @@ class AuthProcessors(BaseProcessor):
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
self.tools.generate_id(), phone, None, first_name, last_name, None,
json.dumps([]), json.dumps(["TT", "ONEME"]),
0, str(now_ms), str(now_s),
)
self.tools.generate_id(),
phone,
None,
first_name,
last_name,
None,
json.dumps([]),
json.dumps(["TT", "ONEME"]),
0,
str(now_ms),
str(now_s),
),
)
user_id = cursor.lastrowid
@@ -290,16 +365,25 @@ class AuthProcessors(BaseProcessor):
json.dumps(self.static.USER_FOLDERS),
json.dumps(self.static.USER_SETTINGS),
json.dumps({}),
)
),
)
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
await cursor.execute(
"DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,)
)
# Создаем сессию
await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(phone, hashed_login, deviceType or "ANDROID", deviceName or "Unknown", "Little Saint James Island", now_s,)
(
phone,
hashed_login,
deviceType or "ANDROID",
deviceName or "Unknown",
"Little Saint James Island",
now_s,
),
)
# Генерируем профиль
@@ -316,7 +400,7 @@ class AuthProcessors(BaseProcessor):
accountStatus=0,
profileOptions=[],
includeProfileOptions=True,
username=None
username=None,
)
# Собираем данные пакета
@@ -324,17 +408,22 @@ class AuthProcessors(BaseProcessor):
"userToken": "0",
"profile": profile,
"tokenType": "LOGIN",
"token": login
"token": login,
}
# Создаем пакет
packet = self.proto.pack_packet(
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)
self.logger.info(f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={first_name} {last_name}")
self.logger.info(
f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={first_name} {last_name}"
)
async def login(self, payload, seq, writer):
"""Обработчик авторизации клиента на сервере"""
@@ -343,9 +432,11 @@ class AuthProcessors(BaseProcessor):
LoginPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
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
# Чаты, где состоит пользователь
chats = []
@@ -358,33 +449,40 @@ class AuthProcessors(BaseProcessor):
# Ищем токен в бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM tokens WHERE token_hash = %s", (hashed_token,))
await cursor.execute(
"SELECT * FROM tokens WHERE token_hash = %s", (hashed_token,)
)
token_data = await cursor.fetchone()
# Если токен не найден, отправляем ошибку
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
# Ищем аккаунт пользователя в бд
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
await cursor.execute(
"SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),)
)
user = await cursor.fetchone()
# Ищем данные пользователя в бд
await cursor.execute("SELECT * FROM user_data WHERE phone = %s", (token_data.get("phone"),))
await cursor.execute(
"SELECT * FROM user_data WHERE phone = %s",
(token_data.get("phone"),),
)
user_data = await cursor.fetchone()
# Ищем все чаты, где состоит пользователь
await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get('id'))
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get("id")),
)
user_chats = await cursor.fetchall()
for chat in user_chats:
chats.append(
chat.get("chat_id")
)
chats.append(chat.get("chat_id"))
# Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
@@ -405,7 +503,7 @@ class AuthProcessors(BaseProcessor):
accountStatus=int(user.get("accountstatus")),
profileOptions=json.loads(user.get("profileoptions")),
includeProfileOptions=True,
username=user.get("username")
username=user.get("username"),
)
chats = await self.tools.generate_chats(
@@ -422,11 +520,11 @@ class AuthProcessors(BaseProcessor):
"presence": {},
"config": {
"server": self.server_config,
"user": json.loads(user_data.get("user_config"))
"user": json.loads(user_data.get("user_config")),
},
"token": token,
"videoChatHistory": False,
"time": int(time.time() * 1000)
"time": int(time.time() * 1000),
}
# Собираем пакет
@@ -443,12 +541,14 @@ class AuthProcessors(BaseProcessor):
# Удаляем токен из бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("DELETE FROM tokens WHERE token_hash = %s", (hashedToken,))
await cursor.execute(
"DELETE FROM tokens WHERE token_hash = %s", (hashedToken,)
)
# Создаем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGOUT, payload=None
)
# Отправляем
await self._send(writer, response)
await self._send(writer, response)