Начальная реализация транспорта ws для max web и прочие улучшения

This commit is contained in:
Alexey Polyakov
2026-04-07 12:36:30 +03:00
parent 52949602af
commit 0ffc649dd9
19 changed files with 873 additions and 228 deletions

View File

@@ -18,9 +18,11 @@ class BaseProcessor:
self.db_pool = db_pool self.db_pool = db_pool
self.clients = clients self.clients = clients
self.send_event = send_event self.event = send_event
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.type = type
if type == "socket": if type == "socket":
self.proto = MobileProto() self.proto = MobileProto()
elif type == "web": elif type == "web":

View File

@@ -1,4 +1,8 @@
import lz4.block, msgpack, logging, json import logging
import lz4.block
import msgpack
class MobileProto: class MobileProto:
def __init__(self) -> None: def __init__(self) -> None:
@@ -6,9 +10,9 @@ class MobileProto:
# TODO узнать какие должны быть лимиты и поменять, # TODO узнать какие должны быть лимиты и поменять,
# сейчас это больше заглушка # сейчас это больше заглушка
MAX_PAYLOAD_SIZE = 1048576 # 1 MB MAX_PAYLOAD_SIZE = 1048576 # 1 MB
MAX_DECOMPRESSED_SIZE = 1048576 # 1 MB MAX_DECOMPRESSED_SIZE = 1048576 # 1 MB
HEADER_SIZE = 10 # 1+2+1+2+4 HEADER_SIZE = 10 # 1+2+1+2+4
### Работа с протоколом ### Работа с протоколом
def unpack_packet(self, data: bytes) -> dict | None: def unpack_packet(self, data: bytes) -> dict | None:
@@ -32,12 +36,16 @@ class MobileProto:
# Проверяем размер payload # Проверяем размер payload
if payload_length > self.MAX_PAYLOAD_SIZE: if payload_length > self.MAX_PAYLOAD_SIZE:
self.logger.warning(f"Payload слишком большой: {payload_length} B (лимит {self.MAX_PAYLOAD_SIZE})") self.logger.warning(
f"Payload слишком большой: {payload_length} B (лимит {self.MAX_PAYLOAD_SIZE})"
)
return None return None
# Проверяем длину пакета # Проверяем длину пакета
if len(data) < self.HEADER_SIZE + payload_length: if len(data) < self.HEADER_SIZE + payload_length:
self.logger.warning(f"Пакет неполный: требуется {self.HEADER_SIZE + payload_length} B, получено {len(data)}") self.logger.warning(
f"Пакет неполный: требуется {self.HEADER_SIZE + payload_length} B, получено {len(data)}"
)
return None return None
payload_bytes = data[10 : 10 + payload_length] payload_bytes = data[10 : 10 + payload_length]
@@ -60,7 +68,9 @@ class MobileProto:
# Распаковываем msgpack # Распаковываем msgpack
payload = msgpack.unpackb(payload_bytes, raw=False, strict_map_key=False) payload = msgpack.unpackb(payload_bytes, raw=False, strict_map_key=False)
self.logger.debug(f"Распаковал - ver={ver} cmd={cmd} seq={seq} opcode={opcode} payload={payload}") self.logger.debug(
f"Распаковал - ver={ver} cmd={cmd} seq={seq} opcode={opcode} payload={payload}"
)
# Возвращаем # Возвращаем
return { return {
@@ -71,7 +81,14 @@ class MobileProto:
"payload": payload, "payload": payload,
} }
def pack_packet(self, ver: int = 10, cmd: int = 0x100, seq: int = 1, opcode: int = 6, payload: dict = None) -> bytes: def pack_packet(
self,
ver: int = 11,
cmd: int = 0x100,
seq: int = 1,
opcode: int = 6,
payload: dict = {},
) -> bytes:
# Запаковываем заголовок # Запаковываем заголовок
ver_b = ver.to_bytes(1, "big") ver_b = ver.to_bytes(1, "big")
cmd_b = cmd.to_bytes(1, "big") cmd_b = cmd.to_bytes(1, "big")
@@ -83,15 +100,17 @@ class MobileProto:
if payload_bytes is None: if payload_bytes is None:
payload_bytes = b"" payload_bytes = b""
payload_len = len(payload_bytes) & 0xFFFFFF payload_len = len(payload_bytes) & 0xFFFFFF
payload_len_b = payload_len.to_bytes(4, 'big') payload_len_b = payload_len.to_bytes(4, "big")
self.logger.debug(f"Упаковал - ver={ver} cmd={cmd} seq={seq} opcode={opcode} payload={payload}") self.logger.debug(
f"Упаковал - ver={ver} cmd={cmd} seq={seq} opcode={opcode} payload={payload}"
)
# Возвращаем пакет # Возвращаем пакет
return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes
### Констаты протокола ### Констаты протокола
CMD_OK = 1 # 0x100 CMD_OK = 1 # 0x100
CMD_NOF = 2 # 0x200 CMD_NOF = 2 # 0x200
CMD_ERR = 3 # 0x300 CMD_ERR = 3 # 0x300
PROTO_VER = 10 PROTO_VER = 11 # 10 для android клиента

View File

@@ -1,10 +1,12 @@
import time, logging import logging
import time
class RateLimiter: class RateLimiter:
""" """
ip rate limiter using sliding window algorithm ip rate limiter using sliding window algorithm
""" """
def __init__(self, max_attempts=5, window_seconds=60): def __init__(self, max_attempts=5, window_seconds=60):
self.max_attempts = max_attempts self.max_attempts = max_attempts
self.window_seconds = window_seconds self.window_seconds = window_seconds
@@ -21,7 +23,9 @@ class RateLimiter:
self.attempts[ip] = [t for t in self.attempts[ip] if t > cutoff] self.attempts[ip] = [t for t in self.attempts[ip] if t > cutoff]
if len(self.attempts[ip]) >= self.max_attempts: if len(self.attempts[ip]) >= self.max_attempts:
self.logger.warning(f"request limit exceeded for {ip}: {len(self.attempts[ip])}/{self.max_attempts}") self.logger.warning(
f"request limit exceeded for {ip}: {len(self.attempts[ip])}/{self.max_attempts}"
)
return False return False
self.attempts[ip].append(now) self.attempts[ip].append(now)

View File

@@ -1,18 +1,28 @@
import json
import time
import random
import hashlib import hashlib
import json
import random
import time
class Tools: class Tools:
def __init__(self): def __init__(self):
pass pass
def generate_profile( def generate_profile(
self, id=1, phone=70000000000, avatarUrl=None, self,
photoId=None, updateTime=0, id=1,
firstName="Test", lastName="Account", options=[], phone=70000000000,
description=None, accountStatus=0, profileOptions=[], avatarUrl=None,
includeProfileOptions=True, username=None photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
description=None,
accountStatus=0,
profileOptions=[],
includeProfileOptions=True,
username=None,
): ):
contact = { contact = {
"id": id, "id": id,
@@ -23,14 +33,13 @@ class Tools:
"name": firstName, "name": firstName,
"firstName": firstName, "firstName": firstName,
"lastName": lastName, "lastName": lastName,
"type": "ONEME" "type": "ONEME",
} }
], ],
"options": options, "options": options,
"accountStatus": accountStatus "accountStatus": accountStatus,
} }
if avatarUrl: if avatarUrl:
contact["photoId"] = photoId contact["photoId"] = photoId
contact["baseUrl"] = avatarUrl contact["baseUrl"] = avatarUrl
@@ -42,31 +51,30 @@ class Tools:
if username: if username:
contact["link"] = "https://max.ru/" + username contact["link"] = "https://max.ru/" + username
if includeProfileOptions == True: if includeProfileOptions:
return { return {"contact": contact, "profileOptions": profileOptions}
"contact": contact,
"profileOptions": profileOptions
}
else: else:
return contact return contact
def generate_profile_tt( def generate_profile_tt(
self, id=1, phone=70000000000, avatarUrl=None, self,
photoId=None, updateTime=0, id=1,
firstName="Test", lastName="Account", options=[], phone=70000000000,
description=None, username=None avatarUrl=None,
photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
description=None,
username=None,
): ):
contact = { contact = {
"id": id, "id": id,
"updateTime": updateTime, "updateTime": updateTime,
"phone": phone, "phone": phone,
"names": [ "names": [{"name": f"{firstName} {lastName}", "type": "TT"}],
{ "options": options,
"name": f"{firstName} {lastName}",
"type": "TT"
}
],
"options": options
} }
if avatarUrl: if avatarUrl:
@@ -82,16 +90,16 @@ class Tools:
return contact return contact
def generate_chat(self, id, owner, type, participants, lastMessage, lastEventTime, prevMessageId=0): def generate_chat(
self, id, owner, type, participants, lastMessage, lastEventTime, prevMessageId=0
):
"""Генерация чата""" """Генерация чата"""
# Генерируем список участников # Генерируем список участников
if isinstance(participants, dict): if isinstance(participants, dict):
result_participants = {str(k): v for k, v in participants.items()} result_participants = {str(k): v for k, v in participants.items()}
else: else:
# assume list # assume list
result_participants = { result_participants = {str(participant): 0 for participant in participants}
str(participant): 0 for participant in participants
}
result = None result = None
@@ -112,13 +120,12 @@ class Tools:
"prevMessageId": prevMessageId, "prevMessageId": prevMessageId,
"joinTime": 1, "joinTime": 1,
"modified": lastEventTime, "modified": lastEventTime,
} }
# Возвращаем # Возвращаем
return result return result
async def generate_chats(self, chatIds, db_pool, senderId, include_favourites=True): async def generate_chats(self, chatIds, db_pool, senderId, include_favourites=True, protocol_type='mobile'):
"""Генерирует чаты для отдачи клиенту""" """Генерирует чаты для отдачи клиенту"""
# Готовый список с чатами # Готовый список с чатами
chats = [] chats = []
@@ -128,23 +135,31 @@ class Tools:
async with db_pool.acquire() as db_connection: async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor: async with db_connection.cursor() as cursor:
# Получаем чат по id # Получаем чат по id
await cursor.execute("SELECT * FROM `chats` WHERE id = %s", (chatId,)) await cursor.execute(
"SELECT * FROM `chats` WHERE id = %s", (chatId,)
)
row = await cursor.fetchone() row = await cursor.fetchone()
if row: if row:
# Получаем последнее сообщение из чата # Получаем последнее сообщение из чата
message, messageTime = await self.get_last_message( message, messageTime = await self.get_last_message(
chatId, db_pool chatId, db_pool, protocol_type=protocol_type
) )
# Формируем список участников с временем последней активности # Формируем список участников с временем последней активности
participant_ids = await self.get_chat_participants(chatId, db_pool) participant_ids = await self.get_chat_participants(
chatId, db_pool
)
participants = await self.get_participant_last_activity( participants = await self.get_participant_last_activity(
chatId, participant_ids, db_pool chatId, participant_ids, db_pool
) )
# Получаем ID предыдущего сообщения # Получаем ID предыдущего сообщения
prevMessageId = await self.get_previous_message_id(chatId, db_pool) prevMessageId = await self.get_previous_message_id(
chatId, db_pool, protocol_type=protocol_type
)
# Выносим результат в лист # Выносим результат в лист
chats.append( chats.append(
self.generate_chat( self.generate_chat(
@@ -154,14 +169,14 @@ class Tools:
participants, participants,
message, message,
messageTime, messageTime,
prevMessageId prevMessageId,
) )
) )
if include_favourites == True: if include_favourites:
# Получаем последнее сообщение из избранного # Получаем последнее сообщение из избранного
message, messageTime = await self.get_last_message( message, messageTime = await self.get_last_message(
senderId, db_pool senderId, db_pool, protocol_type=protocol_type
) )
# ID избранного # ID избранного
@@ -173,50 +188,68 @@ class Tools:
) )
# Получаем ID предыдущего сообщения для избранного (чат ID = senderId) # Получаем ID предыдущего сообщения для избранного (чат ID = senderId)
prevMessageId = await self.get_previous_message_id(senderId, db_pool) prevMessageId = await self.get_previous_message_id(senderId, db_pool, protocol_type=protocol_type)
# Хардкодим в лист чатов избранное # Хардкодим в лист чатов избранное
chats.append( chats.append(
self.generate_chat( self.generate_chat(
chatId, chatId if protocol_type == 'mobile' else str(chatId),
senderId, senderId,
"DIALOG", "DIALOG",
participants, participants,
message, message,
messageTime, messageTime,
prevMessageId prevMessageId,
) )
) )
return chats return chats
async def insert_message(self, chatId, senderId, text, attaches, elements, cid, type, db_pool): async def insert_message(
self, chatId, senderId, text, attaches, elements, cid, type, db_pool
):
"""Добавление сообщения в историю""" """Добавление сообщения в историю"""
async with db_pool.acquire() as db_connection: async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor: async with db_connection.cursor() as cursor:
# Получаем id последнего сообщения в чате # Получаем id последнего сообщения в чате
await cursor.execute("SELECT id FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1", (chatId,)) await cursor.execute(
"SELECT id FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1",
(chatId,),
)
row = await cursor.fetchone() or {} row = await cursor.fetchone() or {}
last_message_id = row.get("id") or 0 # последнее id сообщения в чате last_message_id = row.get("id") or 0 # последнее id сообщения в чате
message_id = self.generate_id() message_id = self.generate_id()
message_time = int(time.time() * 1000) # время отправки сообщения message_time = int(time.time() * 1000) # время отправки сообщения
# Вносим новое сообщение в таблицу # Вносим новое сообщение в таблицу
await cursor.execute( await cursor.execute(
"INSERT INTO `messages` (id, chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)", "INSERT INTO `messages` (id, chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(message_id, chatId, senderId, message_time, text, json.dumps(attaches), cid, json.dumps(elements), type) (
message_id,
chatId,
senderId,
message_time,
text,
json.dumps(attaches),
cid,
json.dumps(elements),
type,
),
) )
# Возвращаем айдишки # Возвращаем айдишки
return int(message_id), int(last_message_id), message_time return int(message_id), int(last_message_id), message_time
async def get_last_message(self, chatId, db_pool): async def get_last_message(self, chatId, db_pool, protocol_type='mobile'):
"""Получение последнего сообщения в чате""" """Получение последнего сообщения в чате"""
async with db_pool.acquire() as db_connection: async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor: async with db_connection.cursor() as cursor:
# Получаем id последнего сообщения в чате # Получаем id последнего сообщения в чате
await cursor.execute("SELECT * FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1", (chatId,)) await cursor.execute(
"SELECT * FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1",
(chatId,),
)
row = await cursor.fetchone() row = await cursor.fetchone()
@@ -226,7 +259,7 @@ class Tools:
# Собираем сообщение # Собираем сообщение
message = { message = {
"id": row.get("id"), "id": row.get("id") if protocol_type == 'mobile' else str(row.get('id')),
"time": int(row.get("time")), "time": int(row.get("time")),
"type": row.get("type"), "type": row.get("type"),
"sender": row.get("sender"), "sender": row.get("sender"),
@@ -234,28 +267,28 @@ class Tools:
"text": row.get("text"), "text": row.get("text"),
"attaches": json.loads(row.get("attaches")), "attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")), "elements": json.loads(row.get("elements")),
"reactionInfo": {} "reactionInfo": {},
} }
# Возвращаем # Возвращаем
return message, int(row.get("time")) return message, int(row.get("time"))
async def get_previous_message_id(self, chatId, db_pool): async def get_previous_message_id(self, chatId, db_pool, protocol_type='mobile'):
"""Получение ID предыдущего сообщения (второго с конца) в чате.""" """Получение ID предыдущего сообщения (второго с конца) в чате."""
async with db_pool.acquire() as db_connection: async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor: async with db_connection.cursor() as cursor:
await cursor.execute( await cursor.execute(
"SELECT id FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1 OFFSET 1", "SELECT id FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1 OFFSET 1",
(chatId,) (chatId,),
) )
row = await cursor.fetchone() row = await cursor.fetchone()
# Если результат есть, возвращаем его # Если результат есть, возвращаем его
if row: if row:
return int(row.get("id")) return row.get("id") if protocol_type == 'mobile' else str(row.get('id'))
# В ином случае возвращаем 0 # В ином случае возвращаем 0
return 0 return 0 if protocol_type == 'mobile' else "0"
async def get_participant_last_activity(self, chatId, participant_ids, db_pool): async def get_participant_last_activity(self, chatId, participant_ids, db_pool):
"""Возвращает словарь {participant_id: last_activity_time} для участников чата.""" """Возвращает словарь {participant_id: last_activity_time} для участников чата."""
@@ -265,7 +298,7 @@ class Tools:
async with db_pool.acquire() as db_connection: async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor: async with db_connection.cursor() as cursor:
# Собираем всех участников # Собираем всех участников
placeholders = ','.join(['%s'] * len(participant_ids)) placeholders = ",".join(["%s"] * len(participant_ids))
query = f""" query = f"""
SELECT sender, MAX(time) as last_time SELECT sender, MAX(time) as last_time
FROM messages FROM messages
@@ -294,7 +327,7 @@ class Tools:
async with db_connection.cursor() as cursor: async with db_connection.cursor() as cursor:
await cursor.execute( await cursor.execute(
"SELECT user_id FROM chat_participants WHERE chat_id = %s", "SELECT user_id FROM chat_participants WHERE chat_id = %s",
(chatId,) (chatId,),
) )
rows = await cursor.fetchall() rows = await cursor.fetchall()
return [row["user_id"] for row in rows] return [row["user_id"] for row in rows]

View File

@@ -1,12 +1,17 @@
# Импортирование библиотек # Импортирование библиотек
import ssl, logging, asyncio import asyncio
import logging
import ssl
from common.config import ServerConfig from common.config import ServerConfig
from oneme.controller import OnemeController from oneme.controller import OnemeController
from telegrambot.controller import TelegramBotController
from tamtam.controller import TTController from tamtam.controller import TTController
from telegrambot.controller import TelegramBotController
# Конфиг сервера # Конфиг сервера
server_config = ServerConfig() server_config = ServerConfig()
async def init_db(): async def init_db():
"""Инициализация базы данных""" """Инициализация базы данных"""
@@ -14,6 +19,7 @@ async def init_db():
if server_config.db_type == "mysql": if server_config.db_type == "mysql":
import aiomysql import aiomysql
db = await aiomysql.create_pool( db = await aiomysql.create_pool(
host=server_config.db_host, host=server_config.db_host,
port=server_config.db_port, port=server_config.db_port,
@@ -21,16 +27,18 @@ async def init_db():
password=server_config.db_password, password=server_config.db_password,
db=server_config.db_name, db=server_config.db_name,
cursorclass=aiomysql.DictCursor, cursorclass=aiomysql.DictCursor,
autocommit=True autocommit=True,
) )
elif server_config.db_type == "sqlite": elif server_config.db_type == "sqlite":
import aiosqlite import aiosqlite
raw_db = await aiosqlite.connect(server_config.db_file) raw_db = await aiosqlite.connect(server_config.db_file)
db["acquire"] = lambda: raw_db db["acquire"] = lambda: raw_db
# Возвращаем # Возвращаем
return db return db
def init_ssl(): def init_ssl():
"""Создание контекста SSL""" """Создание контекста SSL"""
# Создаем контекст SSL # Создаем контекст SSL
@@ -40,6 +48,7 @@ def init_ssl():
# Возвращаем # Возвращаем
return ssl_context return ssl_context
def set_logging(): def set_logging():
"""Настройка уровня логирования""" """Настройка уровня логирования"""
# Настройка уровня логирования # Настройка уровня логирования
@@ -52,10 +61,12 @@ def set_logging():
else: else:
logging.basicConfig(level=None) logging.basicConfig(level=None)
async def main(): async def main():
"""Запуск сервера""" """Запуск сервера"""
async def api_event(target, eventData): async def api_event(target, eventData):
for client in api.get("clients").get(target, {}).get("clients", {}): for client in api.get("clients", {}).get(target, {}).get("clients", {}):
await controllers[client["protocol"]].event(target, client, eventData) await controllers[client["protocol"]].event(target, client, eventData)
set_logging() set_logging()
@@ -68,24 +79,22 @@ async def main():
"ssl": ssl_context, "ssl": ssl_context,
"clients": clients, "clients": clients,
"event": api_event, "event": api_event,
"origins": server_config.origins "origins": server_config.origins,
} }
controllers = { controllers = {
"oneme": OnemeController(), "oneme": OnemeController(),
"tamtam": TTController(), "tamtam": TTController(),
"telegrambot": TelegramBotController() "telegrambot": TelegramBotController(),
} }
api["telegram_bot"] = controllers["telegrambot"] api["telegram_bot"] = controllers["telegrambot"]
tasks = [ tasks = [controller.launch(api) for controller in controllers.values()]
controller.launch(api)
for controller in controllers.values()
]
# Запускаем контроллеры # Запускаем контроллеры
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@@ -3,5 +3,120 @@ class OnemeConfig:
pass pass
SERVER_CONFIG = { SERVER_CONFIG = {
"async-tracer": 0,
"presence-ttl": 300,
"non-contact-sync-time": 86400,
"contact-batching-variant": 0,
"account-nickname-enabled": True,
"web-ad-banner": {
"enabled": False
},
"edit-timeout": 0,
"reactions-menu": [],
"invite-long": "",
"calls-endpoint": "",
"calls-test-domain": "",
"max-readmarks": 100,
"max-cname-length": 200,
"max-description-length": 400,
"new-avatar-gradient-colors-enabled": True,
"max-msg-length": 4000,
"file-upload-unsupported-types": [],
"file-upload-max-size": 4294967296,
"image-quality": 0.8,
"image-width": 1920,
"image-height": 1920,
"image-size": 10000000,
"max-favorite-chats": 5,
"bot-complaint-enabled": True,
"reactions-max": 8,
"welcome-sticker-ids": [],
"edit-chat-type-screen-enabled": True,
"edit-channel-type-screen-enabled": True,
"esia-verify-botId": 0,
"official-org": False,
"esia-enabled": False,
"calls-debug-mode": False,
"channels-suggests-folder": True,
"delete-msg-fys-large-chat-disabled": False,
"calls-web-download-logs": False,
"calls-web-upload-logs": False,
"calls-video-zoom": False,
"calls-fullscreen-mode": False,
"group-call-part-limit": 100,
"call-chat-members-load-config": {},
"cfs": False,
"cse": False,
"calls-hotkeys": True,
"gc-link-pre-settings": False,
"gc-from-p2p": False,
"call-rate": {},
"channels-enabled": True,
"max-participants": 20000,
"max-added-participants": 100,
"saved-messages-aliases": [],
"author-visibility-forward-enabled": False,
"official-bot-naming-enabled": False,
"search-webapps-showcase": {
"items": []
},
"settings-entry-banners": [],
"settings-business": "https://telegram.org/blog/telegram-business",
"appearance-multi-theme-screen-enabled": True,
"moscow-theme-enabled": True,
"creation-2fa-config": {
"enabled": False,
"pass_min_len": 6,
"pass_max_len": 64,
"hint_max_len": 30
},
"lebedev-theme-enabled": True,
"quotes-enabled": True,
"channels-complaint-enabled": True,
"reactions-settings-enabled": True,
"channel-statistics-botid": 0,
"enable-unknown-contact-bottom-sheet": 0,
"informer-enabled": True,
"family-protection-botid": 0,
"new-year-theme-2026": True,
"scheduled-messages-enabled": True,
"scheduled-posts-enabled": True,
"scheduled-faves-enabled": True,
"non-contact-complaints-enabled": True,
"join-requests": True,
"web-persistent-cache": False,
"create-channel-type-screen": True,
"show-warning-links": True,
"white-list-links": [],
"february-23-26-theme": True,
"march-8-26-theme": True,
"audio-play-cmd": False,
"audio-play-opus": False,
"bots-channel-adding": True,
"stickers-botid": 0,
"sticker-set-edit-enabled": True,
"calls-new-history-enabled": True,
"y-map": {
"tile": "",
"geocoder": "",
"static": ""
},
"enable-audio-messages-transcription": True,
"enable-video-messages-transcription": True,
"retry-transcribe-attempt": 5,
"retry-transcribe-timeout": 2000,
"org-profile": False,
"media-not-ready-retry-delay": 2000,
"polls-in-chats": True,
"polls-in-channels": True,
"render-polls": True,
"poll-ttl": {
"chat": 5000,
"bigchat": 15000,
"channel": 25000
},
"new-collage": False,
"channel-profile-invite-link": False,
"rename-profile-to-settings": True,
"live-streams": True
} }

View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
from oneme.socket import OnemeMobileServer from oneme.socket import OnemeMobile
from oneme.websocket import OnemeWS
from common.proto_tcp import MobileProto from common.proto_tcp import MobileProto
from common.proto_web import WebProto from common.proto_web import WebProto
from classes.controllerbase import ControllerBase from classes.controllerbase import ControllerBase
@@ -78,7 +79,7 @@ class OnemeController(ControllerBase):
def launch(self, api): def launch(self, api):
async def _start_all(): async def _start_all():
await asyncio.gather( await asyncio.gather(
OnemeMobileServer( OnemeMobile(
host=self.config.host, host=self.config.host,
port=self.config.oneme_tcp_port, port=self.config.oneme_tcp_port,
ssl_context=api['ssl'], ssl_context=api['ssl'],
@@ -86,6 +87,14 @@ class OnemeController(ControllerBase):
clients=api['clients'], clients=api['clients'],
send_event=api['event'], send_event=api['event'],
telegram_bot=api.get('telegram_bot'), telegram_bot=api.get('telegram_bot'),
).start(),
OnemeWS(
host=self.config.host,
port=self.config.oneme_ws_port,
clients=api['clients'],
ssl_context=api['ssl'],
db_pool=api['db'],
send_event=api['event']
).start() ).start()
) )

View File

@@ -48,7 +48,7 @@ class LoginPayloadModel(pydantic.BaseModel):
token: str token: str
class PingPayloadModel(pydantic.BaseModel): class PingPayloadModel(pydantic.BaseModel):
interactive: bool interactive: bool = None
class AssetsPayloadModel(pydantic.BaseModel): class AssetsPayloadModel(pydantic.BaseModel):
sync: int sync: int
@@ -59,11 +59,11 @@ class GetCallHistoryPayloadModel(pydantic.BaseModel):
count: int count: int
class MessageModel(pydantic.BaseModel): class MessageModel(pydantic.BaseModel):
isLive: bool isLive: bool = None
detectShare: bool detectShare: bool = None
elements: list elements: list = None
attaches: list = None attaches: list = None
cid: int cid: int = None
text: str = None text: str = None
class SendMessagePayloadModel(pydantic.BaseModel): class SendMessagePayloadModel(pydantic.BaseModel):

View File

@@ -34,7 +34,7 @@ class AuthProcessors(BaseProcessor):
phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "") phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "")
# Генерируем токен # Генерируем токен
token = secrets.token_urlsafe(128) token = secrets.token_urlsafe(102)
token_hash = hashlib.sha256(token.encode()).hexdigest() token_hash = hashlib.sha256(token.encode()).hexdigest()
# Время истечения токена # Время истечения токена
@@ -282,7 +282,7 @@ class AuthProcessors(BaseProcessor):
""" """
INSERT INTO user_data INSERT INTO user_data
(phone, contacts, folders, user_config, chat_config) (phone, contacts, folders, user_config, chat_config)
VALUES (%s %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s)
""", """,
( (
phone, phone,
@@ -409,7 +409,7 @@ class AuthProcessors(BaseProcessor):
) )
chats = await self.tools.generate_chats( chats = await self.tools.generate_chats(
chats, self.db_pool, user.get("id") chats, self.db_pool, user.get("id"), protocol_type=self.type
) )
# Формируем данные пакета # Формируем данные пакета

View File

@@ -58,8 +58,9 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall() result = await cursor.fetchall()
for row in result: for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию
messages.append({ messages.append({
"id": row.get("id"), "id": row.get("id") if self.type == 'mobile' else str(row.get('id')),
"time": int(row.get("time")), "time": int(row.get("time")),
"type": row.get("type"), "type": row.get("type"),
"sender": row.get("sender"), "sender": row.get("sender"),

View File

@@ -32,6 +32,7 @@ class MainProcessors(BaseProcessor):
"app-update-type": 0, # 1 = принудительное обновление "app-update-type": 0, # 1 = принудительное обновление
"reg-country-code": self.static.REG_COUNTRY_CODES, "reg-country-code": self.static.REG_COUNTRY_CODES,
"phone-auto-complete-enabled": False, "phone-auto-complete-enabled": False,
"qr-auth-enabled": False,
"lang": True "lang": True
} }

View File

@@ -83,7 +83,7 @@ class MessagesProcessors(BaseProcessor):
# Вычисляем ID чата по ID пользователя и ID отправителя, # Вычисляем ID чата по ID пользователя и ID отправителя,
# в случае отсутствия ID чата # в случае отсутствия ID чата
if not chatId: if chatId is None:
chatId = userId ^ senderId chatId = userId ^ senderId
# Если клиент хочет отправить сообщение в избранное, # Если клиент хочет отправить сообщение в избранное,

View File

@@ -76,48 +76,69 @@ class SearchProcessors(BaseProcessor):
# Валидируем данные пакета # Валидируем данные пакета
try: try:
SearchByPhonePayloadModel.model_validate(payload) SearchByPhonePayloadModel.model_validate(payload)
except pydantic.ValidationError as error: except Exception as e:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_INFO_BY_PHONE, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.CONTACT_INFO_BY_PHONE, self.error_types.INVALID_PAYLOAD, writer)
return return
# Ищем пользователя в бд # Ищем пользователя в бд
phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "")
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 users WHERE phone = %s", (phone,)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (int(payload.get("phone")),))
user = await cursor.fetchone() user = await cursor.fetchone()
# Если пользователь найден # Если пользователь не найден, отправляем ошибку
if user: if not user:
# Аватарка с биографией await self._send_error(seq, self.opcodes.CONTACT_INFO_BY_PHONE, self.error_types.USER_NOT_FOUND, writer)
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) return
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description")
# Генерируем профиль # ID чата
profile = self.tools.generate_profile( chatId = senderId ^ user.get("id")
id=user.get("id"),
phone=int(user.get("phone")), # Ищем диалог в бд
avatarUrl=avatar_url, await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
photoId=photoId, chat = await cursor.fetchone()
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"), # Если диалога нет - создаем
lastName=user.get("lastname"), if not chat:
options=json.loads(user.get("options")), await cursor.execute(
description=description, "INSERT INTO chats (id, owner, type) VALUES (%s, %s, %s)",
accountStatus=int(user.get("accountstatus")), (chatId, senderId, "DIALOG")
profileOptions=json.loads(user.get("profileoptions")),
includeProfileOptions=False,
username=user.get("username")
) )
else:
profile = None # Добавляем участников в таблицу chat_participants
participants = [int(senderId), int(user.get("id"))]
for user_id in participants:
await cursor.execute(
"INSERT INTO chat_participants (chat_id, user_id) VALUES (%s, %s)",
(chatId, user_id)
)
# Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description")
# Генерируем профиль
profile = self.tools.generate_profile(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photoId,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
description=description,
accountStatus=int(user.get("accountstatus")),
profileOptions=json.loads(user.get("profileoptions")),
includeProfileOptions=False,
username=user.get("username")
)
# Создаем данные пакета # Создаем данные пакета
payload = { payload = {
"profile": profile "contact": profile
} }
# Создаем пакет # Создаем пакет
@@ -162,7 +183,7 @@ class SearchProcessors(BaseProcessor):
# Получаем последнее сообщение из чата # Получаем последнее сообщение из чата
message, messageTime = await self.tools.get_last_message( message, messageTime = await self.tools.get_last_message(
chatId, self.db_pool chatId, self.db_pool, protocol_type=self.type
) )
# Добавляем чат в список # Добавляем чат в список
@@ -176,7 +197,7 @@ class SearchProcessors(BaseProcessor):
else: else:
# Получаем последнее сообщение из чата # Получаем последнее сообщение из чата
message, messageTime = await self.tools.get_last_message( message, messageTime = await self.tools.get_last_message(
senderId, self.db_pool senderId, self.db_pool, protocol_type=self.type
) )
# ID избранного # ID избранного

View File

@@ -1,12 +1,18 @@
import asyncio, logging, traceback import asyncio
import logging
import traceback
from common.opcodes import Opcodes
from common.proto_tcp import MobileProto from common.proto_tcp import MobileProto
from oneme.processors import Processors
from common.rate_limiter import RateLimiter from common.rate_limiter import RateLimiter
from common.tools import Tools from common.tools import Tools
from common.opcodes import Opcodes from oneme.processors import Processors
class OnemeMobileServer:
def __init__(self, host, port, ssl_context, db_pool, clients, send_event, telegram_bot): class OnemeMobile:
def __init__(
self, host, port, ssl_context, db_pool, clients, send_event, telegram_bot
):
self.host = host self.host = host
self.port = port self.port = port
self.ssl_context = ssl_context self.ssl_context = ssl_context
@@ -17,14 +23,19 @@ class OnemeMobileServer:
self.proto = MobileProto() self.proto = MobileProto()
self.auth_required = Tools().auth_required self.auth_required = Tools().auth_required
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, telegram_bot=telegram_bot) self.processors = Processors(
db_pool=db_pool,
clients=clients,
send_event=send_event,
telegram_bot=telegram_bot,
)
self.opcodes = Opcodes() self.opcodes = Opcodes()
# rate limiter anti ddos brute force protection # rate limiter anti ddos brute force protection
self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60) self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
self.read_timeout = 300 # Таймаут чтения из сокета (секунды) self.read_timeout = 300 # Таймаут чтения из сокета (секунды)
self.max_read_size = 65536 # Максимальный размер данных из сокета self.max_read_size = 65536 # Максимальный размер данных из сокета
async def handle_client(self, reader, writer): async def handle_client(self, reader, writer):
"""Функция для обработки подключений""" """Функция для обработки подключений"""
@@ -44,20 +55,22 @@ class OnemeMobileServer:
# Читаем новые данные из сокета с таймаутом # Читаем новые данные из сокета с таймаутом
try: try:
data = await asyncio.wait_for( data = await asyncio.wait_for(
reader.read(self.max_read_size), reader.read(self.max_read_size), timeout=self.read_timeout
timeout=self.read_timeout
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.logger.info(f"Таймаут соединения для {address[0]}:{address[1]}") self.logger.info(
f"Таймаут соединения для {address[0]}:{address[1]}"
)
break break
# Если сокет закрыт - выходим из цикла # Если сокет закрыт - выходим из цикла
if not data: if not data:
break break
if len(data) > self.max_read_size: if len(data) > self.max_read_size:
self.logger.warning(f"Пакет от {address[0]}:{address[1]} превышает лимит ({len(data)} байт)") self.logger.warning(
f"Пакет от {address[0]}:{address[1]} превышает лимит ({len(data)} байт)"
)
break break
# Распаковываем данные # Распаковываем данные
@@ -65,7 +78,9 @@ class OnemeMobileServer:
# Скип если пакет невалидный # Скип если пакет невалидный
if packet is None: if packet is None:
self.logger.warning(f"Невалидный пакет от {address[0]}:{address[1]}") self.logger.warning(
f"Невалидный пакет от {address[0]}:{address[1]}"
)
continue continue
opcode = packet.get("opcode") opcode = packet.get("opcode")
@@ -74,34 +89,70 @@ class OnemeMobileServer:
match opcode: match opcode:
case self.opcodes.SESSION_INIT: case self.opcodes.SESSION_INIT:
deviceType, deviceName = await self.processors.session_init(payload, seq, writer) deviceType, deviceName = await self.processors.session_init(
payload, seq, writer
)
case self.opcodes.AUTH_REQUEST: case self.opcodes.AUTH_REQUEST:
if not self.auth_rate_limiter.is_allowed(address[0]): 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, writer) await self.processors._send_error(
seq,
self.opcodes.AUTH_REQUEST,
self.processors.error_types.RATE_LIMITED,
writer,
)
else: else:
await self.processors.auth_request(payload, seq, writer) await self.processors.auth_request(payload, seq, writer)
case self.opcodes.AUTH: case self.opcodes.AUTH:
if not self.auth_rate_limiter.is_allowed(address[0]): 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, writer) await self.processors._send_error(
seq,
self.opcodes.AUTH,
self.processors.error_types.RATE_LIMITED,
writer,
)
else: else:
await self.processors.auth(payload, seq, writer, deviceType, deviceName) await self.processors.auth(
payload, seq, writer, deviceType, deviceName
)
case self.opcodes.AUTH_CONFIRM: case self.opcodes.AUTH_CONFIRM:
if not self.auth_rate_limiter.is_allowed(address[0]): 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, writer) await self.processors._send_error(
seq,
self.opcodes.AUTH_CONFIRM,
self.processors.error_types.RATE_LIMITED,
writer,
)
elif payload and payload.get("tokenType") == "REGISTER": elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm(payload, seq, writer, deviceType, deviceName) await self.processors.auth_confirm(
payload, seq, writer, deviceType, deviceName
)
else: else:
self.logger.warning(f"AUTH_CONFIRM с неизвестным tokenType: {payload}") self.logger.warning(
f"AUTH_CONFIRM с неизвестным tokenType: {payload}"
)
case self.opcodes.LOGIN: case self.opcodes.LOGIN:
if not self.auth_rate_limiter.is_allowed(address[0]): 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, writer) await self.processors._send_error(
seq,
self.opcodes.LOGIN,
self.processors.error_types.RATE_LIMITED,
writer,
)
else: else:
userPhone, userId, hashedToken = await self.processors.login(payload, seq, writer) (
userPhone,
userId,
hashedToken,
) = await self.processors.login(payload, seq, writer)
if userPhone: if userPhone:
await self._finish_auth(writer, address, userPhone, userId) await self._finish_auth(
writer, address, userPhone, userId
)
case self.opcodes.LOGOUT: case self.opcodes.LOGOUT:
await self.processors.logout(seq, writer, hashedToken=hashedToken) await self.processors.logout(
seq, writer, hashedToken=hashedToken
)
break break
case self.opcodes.PING: case self.opcodes.PING:
await self.processors.ping(payload, seq, writer) await self.processors.ping(payload, seq, writer)
@@ -109,35 +160,75 @@ class OnemeMobileServer:
await self.processors.log(payload, seq, writer) await self.processors.log(payload, seq, writer)
case self.opcodes.ASSETS_UPDATE: case self.opcodes.ASSETS_UPDATE:
await self.auth_required( await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, writer userPhone,
self.processors.assets_update,
payload,
seq,
writer,
) )
case self.opcodes.VIDEO_CHAT_HISTORY: case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, writer userPhone,
self.processors.video_chat_history,
payload,
seq,
writer,
) )
case self.opcodes.MSG_SEND: case self.opcodes.MSG_SEND:
await self.auth_required( await self.auth_required(
userPhone, self.processors.msg_send, payload, seq, writer, userId, self.db_pool userPhone,
self.processors.msg_send,
payload,
seq,
writer,
userId,
self.db_pool,
) )
case self.opcodes.FOLDERS_GET: case self.opcodes.FOLDERS_GET:
await self.auth_required( await self.auth_required(
userPhone, self.processors.folders_get, payload, seq, writer, userPhone userPhone,
self.processors.folders_get,
payload,
seq,
writer,
userPhone,
) )
case self.opcodes.SESSIONS_INFO: case self.opcodes.SESSIONS_INFO:
await self.auth_required( await self.auth_required(
userPhone, self.processors.sessions_info, payload, seq, writer, userPhone, hashedToken userPhone,
self.processors.sessions_info,
payload,
seq,
writer,
userPhone,
hashedToken,
) )
case self.opcodes.CHAT_INFO: case self.opcodes.CHAT_INFO:
await self.auth_required( await self.auth_required(
userPhone, self.processors.chat_info, payload, seq, writer, userId userPhone,
self.processors.chat_info,
payload,
seq,
writer,
userId,
) )
case self.opcodes.CHAT_HISTORY: case self.opcodes.CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, self.processors.chat_history, payload, seq, writer, userId userPhone,
self.processors.chat_history,
payload,
seq,
writer,
userId,
) )
case self.opcodes.CONTACT_INFO_BY_PHONE: case self.opcodes.CONTACT_INFO_BY_PHONE:
await self.auth_required( await self.auth_required(
userPhone, self.processors.contact_info_by_phone, payload, seq, writer, userId userPhone,
self.processors.contact_info_by_phone,
payload,
seq,
writer,
userId,
) )
case self.opcodes.OK_TOKEN: case self.opcodes.OK_TOKEN:
await self.auth_required( await self.auth_required(
@@ -145,15 +236,28 @@ class OnemeMobileServer:
) )
case self.opcodes.MSG_TYPING: case self.opcodes.MSG_TYPING:
await self.auth_required( await self.auth_required(
userPhone, self.processors.msg_typing, payload, seq, writer, userId userPhone,
self.processors.msg_typing,
payload,
seq,
writer,
userId,
) )
case self.opcodes.CONTACT_INFO: case self.opcodes.CONTACT_INFO:
await self.auth_required( await self.auth_required(
userPhone, self.processors.contact_info, payload, seq, writer userPhone,
self.processors.contact_info,
payload,
seq,
writer,
) )
case self.opcodes.COMPLAIN_REASONS_GET: case self.opcodes.COMPLAIN_REASONS_GET:
await self.auth_required( await self.auth_required(
userPhone, self.processors.complain_reasons_get, payload, seq, writer userPhone,
self.processors.complain_reasons_get,
payload,
seq,
writer,
) )
case self.opcodes.PROFILE: case self.opcodes.PROFILE:
await self.processors.profile( await self.processors.profile(
@@ -161,12 +265,18 @@ class OnemeMobileServer:
) )
case self.opcodes.CHAT_SUBSCRIBE: case self.opcodes.CHAT_SUBSCRIBE:
await self.auth_required( await self.auth_required(
userPhone, self.processors.chat_subscribe, payload, seq, writer userPhone,
self.processors.chat_subscribe,
payload,
seq,
writer,
) )
case _: case _:
self.logger.warning(f"Неизвестный опкод {opcode}") self.logger.warning(f"Неизвестный опкод {opcode}")
except Exception as e: except Exception as e:
self.logger.error(f"Произошла ошибка при работе с клиентом {address[0]}:{address[1]}: {e}") self.logger.error(
f"Произошла ошибка при работе с клиентом {address[0]}:{address[1]}: {e}"
)
traceback.print_exc() traceback.print_exc()
# Удаляем клиента из словаря # Удаляем клиента из словаря
@@ -174,7 +284,9 @@ class OnemeMobileServer:
await self._end_session(userId, address[0], address[1]) await self._end_session(userId, address[0], address[1])
writer.close() writer.close()
self.logger.info(f"Прекратил работать работать с клиентом {address[0]}:{address[1]}") self.logger.info(
f"Прекратил работать работать с клиентом {address[0]}:{address[1]}"
)
async def _finish_auth(self, writer, addr, phone, id): async def _finish_auth(self, writer, addr, phone, id):
"""Завершение открытия сессии""" """Завершение открытия сессии"""
@@ -184,12 +296,7 @@ class OnemeMobileServer:
# Добавляем новое подключение в словарь # Добавляем новое подключение в словарь
if user: if user:
user["clients"].append( user["clients"].append(
{ {"writer": writer, "ip": addr[0], "port": addr[1], "protocol": "oneme"}
"writer": writer,
"ip": addr[0],
"port": addr[1],
"protocol": "oneme"
}
) )
else: else:
self.clients[id] = { self.clients[id] = {
@@ -200,9 +307,9 @@ class OnemeMobileServer:
"writer": writer, "writer": writer,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "oneme" "protocol": "oneme",
} }
] ],
} }
async def _end_session(self, id, ip, port): async def _end_session(self, id, ip, port):

317
src/oneme/websocket.py Normal file
View File

@@ -0,0 +1,317 @@
import logging
import traceback
import websockets
from common.proto_web import WebProto
from oneme.processors import Processors
from common.rate_limiter import RateLimiter
from common.opcodes import Opcodes
from common.tools import Tools
class OnemeWS:
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.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, deviceType, deviceName
)
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,
)
elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm(
payload, seq, websocket, deviceType, deviceName
)
else:
self.logger.warning(
f"AUTH_CONFIRM с неизвестным tokenType: {payload}"
)
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.PING:
await self.processors.ping(payload, seq, websocket)
case self.opcodes.LOG:
await self.processors.log(payload, seq, websocket)
case self.opcodes.ASSETS_UPDATE:
await self.auth_required(
userPhone,
self.processors.assets_update,
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.FOLDERS_GET:
await self.auth_required(
userPhone,
self.processors.folders_get,
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.CHAT_HISTORY:
await self.auth_required(
userPhone,
self.processors.chat_history,
payload,
seq,
websocket,
userId,
)
case self.opcodes.CONTACT_INFO_BY_PHONE:
await self.auth_required(
userPhone,
self.processors.contact_info_by_phone,
payload,
seq,
websocket,
userId,
)
case self.opcodes.OK_TOKEN:
await self.auth_required(
userPhone, self.processors.ok_token, payload, seq, websocket
)
case self.opcodes.MSG_TYPING:
await self.auth_required(
userPhone,
self.processors.msg_typing,
payload,
seq,
websocket,
userId,
)
case self.opcodes.CONTACT_INFO:
await self.auth_required(
userPhone,
self.processors.contact_info,
payload,
seq,
websocket,
)
case self.opcodes.COMPLAIN_REASONS_GET:
await self.auth_required(
userPhone,
self.processors.complain_reasons_get,
payload,
seq,
websocket,
)
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.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": "oneme"
}
)
else:
self.clients[id] = {
"phone": phone,
"id": id,
"clients": [
{
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "oneme"
}
]
}
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"WebSocket запущен на порту {self.port}")
await self.server.wait_closed()

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
from tamtam.socket import TTMobileServer from tamtam.socket import TamTamMobile
from tamtam.websocket import TTWebSocketServer from tamtam.websocket import TamTamWS
from classes.controllerbase import ControllerBase from classes.controllerbase import ControllerBase
from common.config import ServerConfig from common.config import ServerConfig
@@ -11,7 +11,7 @@ class TTController(ControllerBase):
def launch(self, api): def launch(self, api):
async def _start_all(): async def _start_all():
await asyncio.gather( await asyncio.gather(
TTMobileServer( TamTamMobile(
host=self.config.host, host=self.config.host,
port=self.config.tamtam_tcp_port, port=self.config.tamtam_tcp_port,
ssl_context=api['ssl'], ssl_context=api['ssl'],
@@ -19,7 +19,7 @@ class TTController(ControllerBase):
clients=api['clients'], clients=api['clients'],
send_event=api['event'] send_event=api['event']
).start(), ).start(),
TTWebSocketServer( TamTamWS(
host=self.config.host, host=self.config.host,
port=self.config.tamtam_ws_port, port=self.config.tamtam_ws_port,
ssl_context=api['ssl'], ssl_context=api['ssl'],

View File

@@ -7,7 +7,7 @@ from common.rate_limiter import RateLimiter
from common.opcodes import Opcodes from common.opcodes import Opcodes
from common.tools import Tools from common.tools import Tools
class TTMobileServer: class TamTamMobile:
def __init__(self, host, port, ssl_context, db_pool, clients, send_event): def __init__(self, host, port, ssl_context, db_pool, clients, send_event):
self.host = host self.host = host
self.port = port self.port = port

View File

@@ -7,7 +7,7 @@ from common.rate_limiter import RateLimiter
from common.opcodes import Opcodes from common.opcodes import Opcodes
from common.tools import Tools from common.tools import Tools
class TTWebSocketServer: class TamTamWS:
def __init__(self, host, port, clients, ssl_context, db_pool, send_event): def __init__(self, host, port, clients, ssl_context, db_pool, send_event):
self.host = host self.host = host
self.port = port self.port = port

View File

@@ -1,6 +1,6 @@
import json
import logging import logging
import random import random
import json
import time import time
from textwrap import dedent from textwrap import dedent
@@ -8,10 +8,11 @@ from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.types import Message from aiogram.types import Message
from common.static import Static
from common.sql_queries import SQLQueries from common.sql_queries import SQLQueries
from common.static import Static
from common.tools import Tools from common.tools import Tools
class TelegramBot: class TelegramBot:
def __init__(self, token, enabled, db_pool, whitelist_ids=None): def __init__(self, token, enabled, db_pool, whitelist_ids=None):
self.bot = Bot(token=token) self.bot = Bot(token=token)
@@ -45,22 +46,24 @@ class TelegramBot:
if account: if account:
# Извлекаем id аккаунта с телефоном # Извлекаем id аккаунта с телефоном
phone = account.get('phone') phone = account.get("phone")
await message.answer( await message.answer(
self.get_bot_message(self.msg_types.WELCOME_ALREADY_REGISTERED).format(phone=phone) self.get_bot_message(self.msg_types.WELCOME_ALREADY_REGISTERED).format(
phone=phone
)
) )
else: else:
await message.answer( await message.answer(self.get_bot_message(self.msg_types.WELCOME_NEW_USER))
self.get_bot_message(self.msg_types.WELCOME_NEW_USER)
)
async def handle_register(self, message: Message): async def handle_register(self, message: Message):
tg_id = str(message.from_user.id) tg_id = str(message.from_user.id)
# Проверка ID на наличие в белом списке # Проверка ID на наличие в белом списке
if tg_id not in self.whitelist_ids: if tg_id not in self.whitelist_ids:
await message.answer(self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED)) await message.answer(
self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED)
)
return return
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
@@ -84,33 +87,35 @@ class TelegramBot:
self.sql_queries.INSERT_USER, self.sql_queries.INSERT_USER,
( (
self.tools.generate_id(), self.tools.generate_id(),
new_phone, # phone new_phone, # phone
tg_id, # telegram_id tg_id, # telegram_id
message.from_user.first_name[:59], # firstname message.from_user.first_name[:59], # firstname
(message.from_user.last_name or "")[:59], # lastname (message.from_user.last_name or "")[:59], # lastname
(message.from_user.username or "")[:60], # username (message.from_user.username or "")[:60], # username
json.dumps([]), # profileoptions json.dumps([]), # profileoptions
json.dumps(["TT", "ONEME"]), # options json.dumps(["TT", "ONEME"]), # options
0, # accountstatus 0, # accountstatus
updatetime, updatetime,
lastseen, lastseen,
) ),
) )
# Добавляем данные о аккаунте # Добавляем данные о аккаунте
await cursor.execute( await cursor.execute(
self.sql_queries.INSERT_USER_DATA, self.sql_queries.INSERT_USER_DATA,
( (
new_phone, # phone new_phone, # phone
json.dumps([]), # contacts json.dumps([]), # contacts
json.dumps(self.static.USER_FOLDERS), # folders json.dumps(self.static.USER_FOLDERS), # folders
json.dumps(self.static.USER_SETTINGS), # user settings json.dumps(self.static.USER_SETTINGS), # user settings
json.dumps({}), # chat_config json.dumps({}), # chat_config
) ),
) )
await message.answer( await message.answer(
self.get_bot_message(self.msg_types.REGISTRATION_SUCCESS).format(new_phone=new_phone) self.get_bot_message(
self.msg_types.REGISTRATION_SUCCESS
).format(new_phone=new_phone)
) )
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка при регистрации: {e}") self.logger.error(f"Ошибка при регистрации: {e}")
@@ -119,7 +124,7 @@ class TelegramBot:
) )
async def start(self): async def start(self):
if self.enabled == True: if self.enabled:
try: try:
await self.dp.start_polling(self.bot) await self.dp.start_polling(self.bot)
except Exception as e: except Exception as e:
@@ -131,7 +136,9 @@ class TelegramBot:
try: try:
await self.bot.send_message( await self.bot.send_message(
chat_id, chat_id,
self.get_bot_message(self.msg_types.INCOMING_CODE).format(phone=phone, code=code) self.get_bot_message(self.msg_types.INCOMING_CODE).format(
phone=phone, code=code
),
) )
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка отправки кода в Telegram: {e}") self.logger.error(f"Ошибка отправки кода в Telegram: {e}")