mirror of
https://github.com/openmax-server/server.git
synced 2026-07-08 13:00:16 +03:00
fc6ec5eec5
* Различные фиксы (#14)
* Исключаем только ошибку валидации
* Небольшие правки вебсокета тамтама
* Теперь номера брутить чутка сложнее
* Авторизация теперь проверяется для некоторых команд
* Теперь проверяется orign у вебсокета тамтама
* Дополнил пример env
* Починил немного сокет тамтама
* Теперь все должно работать исправно
* Документация: описал про SESSION_INIT
* Update deviceId description in tamtam_ws.md
* feat: 16 опкод для обновления профиля
* feat: 23 опкод для регистрации, смс шлюз, докер
* SQLite: Ошибка
И вообще не юзайте sqlite пожалуйста, он баганный
* Серверный конфиг снова сломался
* Починил изменение профиля
* История сообщений (вроде как даже работает)
* Дополнил немного 48 опкод, и починил отдачу истории в избранном (избранное все равно не работает)
* Доделал авторизацию в ТамТаме
* Швырнул архитектуру, чтобы позже объединить контроллеры веба и сокета в одно, а также разделить процессоры
* Удалил tamtam.proto, поскольку нигде больше не используется
* Поделил процессоры в таме
* Коды стран
* Генерируем айди, за вместо того, чтобы писать его попорядку
* Вынес список участников чата в отдельную таблицу
* Теперь мы не должны отправлять 128 опкод самому себе. вроде
* ТамТам: реализация 32 опкода. Надеюсь, будет работать
* ТамТам: обработчик 5 и 1 опкода
* ТамТам: починил 32 опкод и добавил серверный конфиг. Избранное больше не добавляется в список чатов
MAX: починил 32 опкод
* TT: oops
* MAX: заглушка в 158 опкоде
* Common: поправил 1 прикол
* TamTam: websocket transport for web version
* TamTam && MAX: история (в мохе она вроде теперь получше работает)
* Правка контроллеров небольшая
* MAX: Разделил процессоры
* TG Bot: переписал на aiogram
* Common: фикс года
* Начальная реализация транспорта ws для max web и прочие улучшения
* Common: добавил сжатие lz4
* Исправление конфликтов
* TT: фикс вебсокета
* 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>
* MAX: заглушка для баннеров, правка пакета со списком жалоб, отдача контактов и прочие улучшения
* oops
* MAX: Рабочие баннеры
* MAX: обновление настроек приватности
* MAX: пуши через firebase (особо не тестил, вроде работает)
* MAX: Рефактор папок
* MAX: фикс версий < 25.8.0
* MAX: отдача черного списка
* Теперь ID пользователей идут по порядку, как и раньше
* MAX: создание папок
* MAX: oops
* MAX: добавление/удаление контактов, статусы
* MAX & TT: теперь в качестве страны локации используется настоящая страна пользователя, а также зафиксировал версии библиотек в зависимостях
* TT: ну вроде шире поддержка, а вообще обратная совместимость с максом клас
* oops
* TG Bot: возможность включения белого списка
* Решение проблемы обработки sigterm
* nothing
* fix
* TG Bot: fix
* TG Bot: fix username
* MAX && TT: общение в таме, и корректировки под веб морду
* MAX: Почистил серверный конфиг
* nothing
* MAX: web fix
* MAX: web fix 2
* oops
* nothing
* MAX: теперь для избранного не сравниваем айди с нулём
* Вынес sqlite в отдельный модуль
* MAX: блокировка и разблокировка контакта
* oops
* TT: тоже самое что в макс
* MAX && TT: обновление контакта
* MAX: добавление контакта по номеру телефона
* MAX: исправление уязвимости с избранными
* Генерируем айди пользователей рандомно (опять, да)
* update sql scheme
* Фикс сборки избранного
* Попытка починить историю (спойлер, нихуя не получилось)
* MAX: история таки заработала — cid/link/reactionInfo обязательны в схеме
Десктопный MAX подключается через TCP (mobile-протокол) и парсит
msgpack по фиксированной схеме. Если в сообщении выпадает любое из
полей — клиент молча обрывает соединение. После 87cfc19 как раз
такие условные `if elements: ...` / `if link: ...` (а link и
reaction_info там всегда были `{}`, то есть falsy) вырезали поля
из ответа CHAT_HISTORY и MSG_SEND, чем и сломали историю.
- src/common/tools.py: новый build_message_dict() — единая сборка
тела сообщения, где все поля (id, cid, time, type, sender, text,
attaches, elements, reactionInfo, link) присутствуют ВСЕГДА.
get_last_message переписан через него.
- src/oneme/processors/history.py: chat_history использует
build_message_dict вместо ручной логики с условными if-ками.
- src/oneme/processors/messages.py: msg_send.bodyMessage теперь
отдает cid / reactionInfo / link даже пустыми и приводит id
к int для mobile, str для web.
Цепная польза: auth.py LOGIN bootstrap (через generate_chats →
get_last_message) и search.py тоже теперь шлют корректную схему.
* MAX: bootstrap-история в LOGIN — клиент перестал думать что всё уже синканулось
В ответе LOGIN сервер слал messages: {} и chatMarker: 0. Десктопный
клиент в этом случае считает, что локальная история уже синхронизирована
со старого запуска, и НЕ отправляет CHAT_HISTORY (49) при открытии чата.
В окне видно только lastMessage из chats[], а вся реальная переписка —
ничерта.
- src/common/tools.py: collect_bootstrap_history(chatIds, ...) —
собирает карту {chatId: [последние N сообщений]}, в т.ч. избранное
под клиентским id = senderId ^ senderId.
- src/oneme/processors/auth.py: подсовываем эту карту в
payload.messages, chatMarker = текущее время вместо 0.
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* MAX & TT: теперь полноценный чёрный список
* TT: регистрация через клиент
* Вроде как нормальные заглушки под ассеты
* maybe fixes
---------
Co-authored-by: WowInceptionGood <143893762+WowInceptionGood@users.noreply.github.com>
Co-authored-by: zavolo <me@kirillka.ru>
Co-authored-by: Aleksandr Kosachev <38947781+gohryt@users.noreply.github.com>
264 lines
12 KiB
Python
264 lines
12 KiB
Python
import logging
|
||
import traceback
|
||
import websockets
|
||
import asyncio
|
||
from common.proto_web import WebProto
|
||
from tamtam.processors import Processors
|
||
from common.rate_limiter import RateLimiter
|
||
from common.opcodes import Opcodes
|
||
from common.tools import Tools
|
||
|
||
class TamTamWS:
|
||
def __init__(self, host, port, clients, ssl_context, db_pool, send_event):
|
||
self.host = host
|
||
self.port = port
|
||
self.ssl_context = ssl_context
|
||
self.server = None
|
||
self.logger = logging.getLogger(__name__)
|
||
self.db_pool = db_pool
|
||
self.clients = clients
|
||
|
||
self.opcodes = Opcodes()
|
||
|
||
self.proto = WebProto()
|
||
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, type="web")
|
||
self.auth_required = Tools().auth_required
|
||
|
||
# rate limiter
|
||
self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
|
||
|
||
self.read_timeout = 300 # Таймаут чтения из websocket (секунды)
|
||
self.max_read_size = 65536 # Максимальный размер данных
|
||
|
||
async def handle_client(self, websocket):
|
||
"""Функция для обработки WebSocket подключений"""
|
||
# IP-адрес клиента
|
||
address = websocket.remote_address
|
||
self.logger.info(f"Работаю с клиентом {address[0]}:{address[1]}")
|
||
|
||
deviceType = None
|
||
deviceName = None
|
||
|
||
userPhone = None
|
||
userId = None
|
||
hashedToken = None
|
||
|
||
try:
|
||
async for message in websocket:
|
||
# Проверяем размер данных
|
||
if len(message) > self.max_read_size:
|
||
self.logger.warning(f"Пакет от {address[0]}:{address[1]} превышает лимит ({len(message)} байт)")
|
||
break
|
||
|
||
# Распаковываем данные
|
||
packet = self.proto.unpack_packet(message)
|
||
|
||
# Если пакет невалидный — пропускаем
|
||
if not packet:
|
||
self.logger.warning(f"Невалидный пакет от {address[0]}:{address[1]}")
|
||
continue
|
||
|
||
opcode = packet.get("opcode")
|
||
seq = packet.get("seq")
|
||
payload = packet.get("payload")
|
||
|
||
match opcode:
|
||
case self.opcodes.SESSION_INIT:
|
||
deviceType, deviceName = await self.processors.session_init(payload, seq, websocket)
|
||
case self.opcodes.PING:
|
||
await self.processors.ping(payload, seq, websocket)
|
||
case self.opcodes.LOG:
|
||
await self.processors.log(payload, seq, websocket)
|
||
case self.opcodes.AUTH_REQUEST:
|
||
if not self.auth_rate_limiter.is_allowed(address[0]):
|
||
await self.processors._send_error(seq, self.opcodes.AUTH_REQUEST, self.processors.error_types.RATE_LIMITED, websocket)
|
||
else:
|
||
await self.processors.auth_request(payload, seq, websocket)
|
||
case self.opcodes.AUTH:
|
||
if not self.auth_rate_limiter.is_allowed(address[0]):
|
||
await self.processors._send_error(seq, self.opcodes.AUTH, self.processors.error_types.RATE_LIMITED, websocket)
|
||
else:
|
||
await self.processors.auth(payload, seq, websocket)
|
||
case self.opcodes.AUTH_CONFIRM:
|
||
if not self.auth_rate_limiter.is_allowed(address[0]):
|
||
await self.processors._send_error(seq, self.opcodes.AUTH_CONFIRM, self.processors.error_types.RATE_LIMITED, websocket)
|
||
else:
|
||
await self.processors.auth_confirm(payload, seq, websocket, deviceType, deviceName, address[0])
|
||
case self.opcodes.LOGIN:
|
||
if not self.auth_rate_limiter.is_allowed(address[0]):
|
||
await self.processors._send_error(seq, self.opcodes.LOGIN, self.processors.error_types.RATE_LIMITED, websocket)
|
||
else:
|
||
userPhone, userId, hashedToken = await self.processors.login(payload, seq, websocket)
|
||
|
||
if userPhone:
|
||
await self._finish_auth(websocket, address, userPhone, userId)
|
||
case self.opcodes.LOGOUT:
|
||
await self.processors.logout(
|
||
seq, websocket, hashedToken=hashedToken
|
||
)
|
||
break
|
||
case self.opcodes.CONTACT_INFO:
|
||
await self.auth_required(
|
||
userPhone, self.processors.contact_info, payload, seq, websocket
|
||
)
|
||
case self.opcodes.CHAT_HISTORY:
|
||
await self.auth_required(
|
||
userPhone, self.processors.chat_history, payload, seq, websocket, userId
|
||
)
|
||
case self.opcodes.ASSETS_UPDATE:
|
||
await self.auth_required(
|
||
userPhone, self.processors.assets_update, payload, seq, websocket
|
||
)
|
||
case self.opcodes.ASSETS_GET:
|
||
await self.auth_required(
|
||
userPhone, self.processors.assets_get, payload, seq, websocket
|
||
)
|
||
case self.opcodes.ASSETS_GET_BY_IDS:
|
||
await self.auth_required(
|
||
userPhone, self.processors.assets_get_by_ids, payload, seq, websocket
|
||
)
|
||
case self.opcodes.ASSETS_ADD:
|
||
await self.auth_required(
|
||
userPhone, self.processors.assets_add, payload, seq, websocket
|
||
)
|
||
case self.opcodes.ASSETS_REMOVE:
|
||
await self.auth_required(
|
||
userPhone, self.processors.assets_remove, payload, seq, websocket
|
||
)
|
||
case self.opcodes.ASSETS_MOVE:
|
||
await self.auth_required(
|
||
userPhone, self.processors.assets_move, payload, seq, websocket
|
||
)
|
||
case self.opcodes.ASSETS_LIST_MODIFY:
|
||
await self.auth_required(
|
||
userPhone, self.processors.assets_list_modify, payload, seq, websocket
|
||
)
|
||
case self.opcodes.VIDEO_CHAT_HISTORY:
|
||
await self.auth_required(
|
||
userPhone, self.processors.video_chat_history, payload, seq, websocket
|
||
)
|
||
case self.opcodes.MSG_SEND:
|
||
await self.auth_required(
|
||
userPhone, self.processors.msg_send, payload, seq, websocket, userId, self.db_pool
|
||
)
|
||
case self.opcodes.MSG_TYPING:
|
||
await self.auth_required(
|
||
userPhone, self.processors.msg_typing, payload, seq, websocket, userId
|
||
)
|
||
case self.opcodes.FOLDERS_GET:
|
||
await self.auth_required(
|
||
userPhone, self.processors.folders_get, payload, seq, websocket, userPhone
|
||
)
|
||
case self.opcodes.FOLDERS_UPDATE:
|
||
await self.auth_required(
|
||
userPhone, self.processors.folders_update, payload, seq, websocket, userPhone
|
||
)
|
||
case self.opcodes.SESSIONS_INFO:
|
||
await self.auth_required(
|
||
userPhone, self.processors.sessions_info, payload, seq, websocket, userPhone, hashedToken
|
||
)
|
||
case self.opcodes.CHAT_INFO:
|
||
await self.auth_required(
|
||
userPhone, self.processors.chat_info, payload, seq, websocket, userId
|
||
)
|
||
case self.opcodes.CONTACT_LIST:
|
||
await self.auth_required(
|
||
userPhone, self.processors.contact_list, payload, seq, websocket, userId
|
||
)
|
||
case self.opcodes.PROFILE:
|
||
await self.processors.profile(
|
||
payload, seq, websocket, userId=userId
|
||
)
|
||
case self.opcodes.CHAT_SUBSCRIBE:
|
||
await self.auth_required(
|
||
userPhone, self.processors.chat_subscribe, payload, seq, websocket
|
||
)
|
||
case self.opcodes.CONFIG:
|
||
await self.auth_required(
|
||
userPhone, self.processors.update_config, payload, seq, websocket, userPhone, hashedToken
|
||
)
|
||
case self.opcodes.CONTACT_UPDATE:
|
||
await self.auth_required(
|
||
userPhone, self.processors.contact_update, payload, seq, websocket, userId
|
||
)
|
||
case self.opcodes.CONTACT_PRESENCE:
|
||
await self.auth_required(
|
||
userPhone, self.processors.contact_presence, payload, seq, websocket
|
||
)
|
||
case _:
|
||
self.logger.warning(f"Неизвестный опкод {opcode}")
|
||
except websockets.exceptions.ConnectionClosed:
|
||
self.logger.info(f"Прекратил работать с клиентом {address[0]}:{address[1]}")
|
||
except Exception as e:
|
||
self.logger.error(f" Произошла ошибка при работе с клиентом {address[0]}:{address[1]}: {e}")
|
||
traceback.print_exc()
|
||
|
||
# Удаляем клиента из словаря при отключении
|
||
if userId:
|
||
await self._end_session(userId, address[0], address[1])
|
||
|
||
self.logger.info(f"Прекратил работать с клиентом {address[0]}:{address[1]}")
|
||
|
||
async def _finish_auth(self, websocket, addr, phone, id):
|
||
"""Завершение открытия сессии"""
|
||
# Ищем пользователя в словаре
|
||
user = self.clients.get(id)
|
||
|
||
# Добавляем новое подключение в словарь
|
||
if user:
|
||
user["clients"].append(
|
||
{
|
||
"writer": websocket,
|
||
"ip": addr[0],
|
||
"port": addr[1],
|
||
"protocol": "tamtam",
|
||
"type": "web"
|
||
}
|
||
)
|
||
else:
|
||
self.clients[id] = {
|
||
"phone": phone,
|
||
"id": id,
|
||
"clients": [
|
||
{
|
||
"writer": websocket,
|
||
"ip": addr[0],
|
||
"port": addr[1],
|
||
"protocol": "tamtam",
|
||
"type": "web"
|
||
}
|
||
]
|
||
}
|
||
|
||
async def _end_session(self, id, ip, port):
|
||
"""Завершение сессии"""
|
||
# Получаем пользователя в списке
|
||
user = self.clients.get(id)
|
||
if not user:
|
||
return
|
||
|
||
# Получаем подключения пользователя
|
||
clients = user.get("clients", [])
|
||
|
||
# Удаляем нужное подключение из словаря
|
||
for i, client in enumerate(clients):
|
||
if (client.get("ip"), client.get("port")) == (ip, port):
|
||
clients.pop(i)
|
||
|
||
async def start(self):
|
||
"""Функция для запуска WebSocket сервера"""
|
||
self.server = await websockets.serve(
|
||
self.handle_client,
|
||
self.host,
|
||
self.port,
|
||
ssl=self.ssl_context
|
||
)
|
||
|
||
self.logger.info(f"TT WebSocket запущен на порту {self.port}")
|
||
|
||
try:
|
||
await self.server.wait_closed()
|
||
except asyncio.CancelledError:
|
||
self.server.close()
|
||
await self.server.wait_closed()
|
||
raise |