mirror of
https://github.com/openmax-server/server.git
synced 2026-05-23 12:01:43 +03:00
Начальная реализация транспорта ws для max web и прочие улучшения
This commit is contained in:
@@ -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":
|
||||||
|
|||||||
@@ -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 клиента
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
29
src/main.py
29
src/main.py
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
# Формируем данные пакета
|
# Формируем данные пакета
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
# Если клиент хочет отправить сообщение в избранное,
|
# Если клиент хочет отправить сообщение в избранное,
|
||||||
|
|||||||
@@ -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 избранного
|
||||||
|
|||||||
@@ -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
317
src/oneme/websocket.py
Normal 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()
|
||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
Reference in New Issue
Block a user