mirror of
https://github.com/openmax-server/server.git
synced 2026-03-14 15:57:40 +00:00
first commit
This commit is contained in:
22
.env.example
Normal file
22
.env.example
Normal file
@@ -0,0 +1,22 @@
|
||||
host = "0.0.0.0"
|
||||
oneme_tcp_port = "443"
|
||||
tamtam_tcp_port = "4433"
|
||||
|
||||
oneme_ws_port = "81"
|
||||
tamtam_ws_port = "82"
|
||||
|
||||
log_level = "debug"
|
||||
|
||||
db_host = "localhost"
|
||||
db_port = "3306"
|
||||
db_user = "root"
|
||||
db_password = "password"
|
||||
db_name = "openmax"
|
||||
|
||||
certfile = "cert.pem"
|
||||
keyfile = "key.pem"
|
||||
|
||||
avatar_base_url = "http://127.0.0.1/avatar/"
|
||||
telegram_bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
telegram_bot_enabled = "1"
|
||||
telegram_whitelist_ids = "1,2,3"
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
.env
|
||||
cert.pem
|
||||
key.pem
|
||||
11
LICENSE
Normal file
11
LICENSE
Normal file
@@ -0,0 +1,11 @@
|
||||
Copyright 2025-2026 Alexey Polyakov
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
33
readme.md
Normal file
33
readme.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# OpenMAX
|
||||
|
||||
Эмулятор сервера MAX и ТамТам
|
||||
|
||||
# Требования
|
||||
|
||||
- Python 3.12+ (поддержка версий ниже не гарантирована)
|
||||
- MariaDB или MySQL
|
||||
- Уметь патчить клиент MAX или собирать Komet из исходного кода (естественно с заменой сервера)
|
||||
|
||||
# Требования к клиенту
|
||||
|
||||
Клиент может быть практически любым, главное условие - чтобы он был совместим с официальным сервером (`api.oneme.ru` / `api.tamtam.chat`).
|
||||
|
||||
# Установка
|
||||
|
||||
1. Склонируйте репозиторий
|
||||
2. Установите зависимости
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Настройте сервер (пример в `.env.example`)
|
||||
4. Импортируйте схему таблиц в свою базу данных из `tables.sql`
|
||||
5. Запустите сервер
|
||||
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
6. Создайте пользователя
|
||||
7. Зайдите со своего любимого клиента
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
pyTelegramBotAPI
|
||||
aiomysql
|
||||
python-dotenv
|
||||
msgpack
|
||||
lz4
|
||||
websockets
|
||||
0
src/classes/__init__.py
Normal file
0
src/classes/__init__.py
Normal file
9
src/classes/controllerbase.py
Normal file
9
src/classes/controllerbase.py
Normal file
@@ -0,0 +1,9 @@
|
||||
class ControllerBase():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def event(self, target, client, eventData):
|
||||
pass
|
||||
|
||||
def launch(self, api):
|
||||
pass
|
||||
0
src/common/__init__.py
Normal file
0
src/common/__init__.py
Normal file
40
src/common/config.py
Normal file
40
src/common/config.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
class ServerConfig:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
### Адрес сервера
|
||||
host = os.getenv("host") or "0.0.0.0"
|
||||
|
||||
### Для мобилок
|
||||
oneme_tcp_port = int(os.getenv("oneme_tcp_port") or 443)
|
||||
tamtam_tcp_port = int(os.getenv("tamtam_tcp_port") or 4433)
|
||||
|
||||
### Шлюзы для веба
|
||||
oneme_ws_port = int(os.getenv("oneme_ws_port") or 81)
|
||||
tamtam_ws_port = int(os.getenv("tamtam_ws_port") or 82)
|
||||
|
||||
### Уровень отладки
|
||||
log_level = os.getenv("log_level") or "debug"
|
||||
|
||||
### MySQL
|
||||
db_host = os.getenv("db_host") or "127.0.0.1"
|
||||
db_port = int(os.getenv("db_port") or 3306)
|
||||
db_user = os.getenv("db_user") or "root"
|
||||
db_password = os.getenv("db_password") or "qwerty"
|
||||
db_name = os.getenv("db_name") or "openmax"
|
||||
|
||||
### SSL
|
||||
certfile = os.getenv("certfile") or "cert.pem"
|
||||
keyfile = os.getenv("keyfile") or "key.pem"
|
||||
|
||||
### Avatar base url
|
||||
avatar_base_url = os.getenv("avatar_base_url") or "http://127.0.0.1/avatar/"
|
||||
|
||||
### Telegram bot
|
||||
telegram_bot_token = os.getenv("telegram_bot_token") or "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
telegram_bot_enabled = bool(os.getenv("telegram_bot_enabled")) or True
|
||||
telegram_whitelist_ids = [x.strip() for x in os.getenv("telegram_whitelist_ids", "").split(",") if x.strip()]
|
||||
84
src/common/static.py
Normal file
84
src/common/static.py
Normal file
@@ -0,0 +1,84 @@
|
||||
class Static:
|
||||
"""Тут просто статические константы для их дальнейшего использования"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
class ErrorTypes:
|
||||
NOT_IMPLEMENTED = "not_implemented"
|
||||
INVALID_PAYLOAD = "invalid_payload"
|
||||
USER_NOT_FOUND = "user_not_found"
|
||||
CODE_EXPIRED = "code_expired"
|
||||
INVALID_CODE = "invalid_code"
|
||||
INVALID_TOKEN = "invalid_token"
|
||||
CHAT_NOT_FOUND = "chat_not_found"
|
||||
CHAT_NOT_ACCESS = "chat_not_access"
|
||||
|
||||
class ChatTypes:
|
||||
DIALOG = "DIALOG"
|
||||
|
||||
ERROR_TYPES = {
|
||||
"not_implemented": {
|
||||
"localizedMessage": "Не реализовано",
|
||||
"error": "proto.opcode",
|
||||
"message": "Not implemented",
|
||||
"title": "Не реализовано"
|
||||
},
|
||||
"invalid_payload": {
|
||||
"localizedMessage": "Ошибка валидации",
|
||||
"error": "proto.payload",
|
||||
"message": "Invalid payload",
|
||||
"title": "Ошибка валидации"
|
||||
},
|
||||
"user_not_found": {
|
||||
"localizedMessage": "Не нашли этот номер, проверьте цифры",
|
||||
"error": "error.phone.wrong",
|
||||
"message": "User not found",
|
||||
"title": "Не нашли этот номер, проверьте цифры"
|
||||
},
|
||||
"code_expired": {
|
||||
"localizedMessage": "Этот код устарел, запросите новый",
|
||||
"error": "error.code.expired",
|
||||
"message": "Code expired",
|
||||
"title": "Этот код устарел, запросите новый"
|
||||
},
|
||||
"invalid_code": {
|
||||
"localizedMessage": "Неверный код",
|
||||
"error": "error.code.wrong",
|
||||
"message": "Invalid code",
|
||||
"title": "Неверный код"
|
||||
},
|
||||
"invalid_token": {
|
||||
"localizedMessage": "Ошибка входа. Пожалуйста, авторизируйтесь снова",
|
||||
"error": "login.token",
|
||||
"message": "Invalid token",
|
||||
"title": "Ошибка входа. Пожалуйста, авторизируйтесь снова"
|
||||
},
|
||||
"chat_not_found": {
|
||||
"localizedMessage": "Чат не найден",
|
||||
"error": "chat.not.found",
|
||||
"message": "Chat not found",
|
||||
"title": "Чат не найден"
|
||||
},
|
||||
"chat_not_access": {
|
||||
"localizedMessage": "Нет доступа к чату",
|
||||
"error": "chat.not.access",
|
||||
"message": "Chat not access",
|
||||
"title": "Нет доступа к чату"
|
||||
}
|
||||
}
|
||||
|
||||
COMPLAIN_REASONS = [
|
||||
# TODO: Было бы очень замечательно заполнить этот лист причинами для жалоб
|
||||
]
|
||||
|
||||
### Заглушка для папок
|
||||
ALL_CHAT_FOLDER = [{
|
||||
"id": "all.chat.folder",
|
||||
"title": "Все",
|
||||
"filters": [],
|
||||
"updateTime": 0,
|
||||
"options": [],
|
||||
"sourceId": 1
|
||||
}]
|
||||
|
||||
ALL_CHAT_FOLDER_ORDER = ["all.chat.folder"]
|
||||
194
src/common/tools.py
Normal file
194
src/common/tools.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import json, time
|
||||
|
||||
class Tools:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def generate_profile(
|
||||
self, id=1, phone=70000000000, avatarUrl=None,
|
||||
photoId=None, updateTime=0,
|
||||
firstName="Test", lastName="Account", options=[],
|
||||
description=None, accountStatus=0, profileOptions=[],
|
||||
includeProfileOptions=True, username=None
|
||||
):
|
||||
contact = {
|
||||
"id": id,
|
||||
"updateTime": updateTime,
|
||||
"phone": phone,
|
||||
"names": [
|
||||
{
|
||||
"name": firstName,
|
||||
"firstName": firstName,
|
||||
"lastName": lastName,
|
||||
"type": "ONEME"
|
||||
}
|
||||
],
|
||||
"options": options,
|
||||
"accountStatus": accountStatus
|
||||
}
|
||||
|
||||
|
||||
if avatarUrl:
|
||||
contact["photoId"] = photoId
|
||||
contact["baseUrl"] = avatarUrl
|
||||
contact["baseRawUrl"] = avatarUrl
|
||||
|
||||
if description:
|
||||
contact["description"] = description
|
||||
|
||||
if username:
|
||||
contact["link"] = "https://max.ru/" + username
|
||||
|
||||
if includeProfileOptions == True:
|
||||
return {
|
||||
"contact": contact,
|
||||
"profileOptions": profileOptions
|
||||
}
|
||||
else:
|
||||
return contact
|
||||
|
||||
def generate_chat(self, id, owner, type, participants, lastMessage, lastEventTime):
|
||||
"""Генерация чата"""
|
||||
# Генерируем список участников
|
||||
result_participants = {
|
||||
str(participant): 0 for participant in participants
|
||||
}
|
||||
|
||||
result = None
|
||||
|
||||
# Генерируем нужный список в зависимости от типа чата
|
||||
if type == "DIALOG":
|
||||
result = {
|
||||
"id": id,
|
||||
"type": type,
|
||||
"status": "ACTIVE",
|
||||
"owner": owner,
|
||||
"participants": result_participants,
|
||||
"lastMessage": lastMessage,
|
||||
"lastEventTime": lastEventTime,
|
||||
"lastDelayedUpdateTime": 0,
|
||||
"lastFireDelayedErrorTime": 0,
|
||||
"created": 1,
|
||||
"joinTime": 1,
|
||||
"modified": lastEventTime
|
||||
}
|
||||
|
||||
# Возвращаем
|
||||
return result
|
||||
|
||||
async def generate_chats(self, chatIds, db_pool, senderId):
|
||||
"""Генерирует чаты для отдачи клиенту"""
|
||||
# Готовый список с чатами
|
||||
chats = []
|
||||
|
||||
# Формируем список чатов
|
||||
for chatId in chatIds:
|
||||
async with db_pool.acquire() as db_connection:
|
||||
async with db_connection.cursor() as cursor:
|
||||
# Получаем чат по id
|
||||
await cursor.execute("SELECT * FROM `chats` WHERE id = %s", (chatId,))
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if row:
|
||||
# Получаем последнее сообщение из чата
|
||||
message, messageTime = await self.get_last_message(
|
||||
chatId, db_pool
|
||||
)
|
||||
|
||||
# Формируем список участников
|
||||
participants = {
|
||||
str(participant): 0 for participant in row.get("participants")
|
||||
}
|
||||
|
||||
# Выносим результат в лист
|
||||
chats.append(
|
||||
{
|
||||
"id": row.get("id"),
|
||||
"type": row.get("type"),
|
||||
"status": "ACTIVE",
|
||||
"owner": row.get("owner"),
|
||||
"participants": participants,
|
||||
"lastMessage": message,
|
||||
"lastEventTime": messageTime,
|
||||
"lastDelayedUpdateTime": 0,
|
||||
"lastFireDelayedErrorTime": 0,
|
||||
"created": 1,
|
||||
"joinTime": 1,
|
||||
"modified": messageTime
|
||||
}
|
||||
)
|
||||
|
||||
# Получаем последнее сообщение из избранного
|
||||
message, messageTime = await self.get_last_message(
|
||||
senderId, db_pool
|
||||
)
|
||||
|
||||
# Хардкодим в лист чатов избранное
|
||||
chats.append(
|
||||
{
|
||||
"id": 0,
|
||||
"type": "DIALOG",
|
||||
"status": "ACTIVE",
|
||||
"owner": senderId,
|
||||
"participants": {
|
||||
str(senderId): 0 # if not messageTime else messageTime
|
||||
},
|
||||
"lastMessage": message,
|
||||
"lastEventTime": messageTime,
|
||||
"lastDelayedUpdateTime": 0,
|
||||
"lastFireDelayedErrorTime": 0,
|
||||
"created": 1,
|
||||
"joinTime": 1,
|
||||
"modified": messageTime
|
||||
}
|
||||
)
|
||||
|
||||
return chats
|
||||
|
||||
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_connection.cursor() as cursor:
|
||||
# Получаем id последнего сообщения в чате
|
||||
await cursor.execute("SELECT id FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1", (chatId,))
|
||||
|
||||
row = await cursor.fetchone() or {}
|
||||
last_message_id = row.get("id") or 0 # последнее id сообщения в чате
|
||||
|
||||
# Вносим новое сообщение в таблицу
|
||||
await cursor.execute(
|
||||
"INSERT INTO `messages` (chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
|
||||
(chatId, senderId, int(time.time() * 1000), text, json.dumps(attaches), cid, json.dumps(elements), type)
|
||||
)
|
||||
|
||||
message_id = cursor.lastrowid # id сообщения
|
||||
|
||||
# Возвращаем айдишки
|
||||
return int(message_id), int(last_message_id)
|
||||
|
||||
async def get_last_message(self, chatId, db_pool):
|
||||
"""Получение последнего сообщения в чате"""
|
||||
async with db_pool.acquire() as db_connection:
|
||||
async with db_connection.cursor() as cursor:
|
||||
# Получаем id последнего сообщения в чате
|
||||
await cursor.execute("SELECT * FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1", (chatId,))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
|
||||
# Если нет результатов - возвращаем None
|
||||
if not row:
|
||||
return None, None
|
||||
|
||||
# Собираем сообщение
|
||||
message = {
|
||||
"id": row.get("id"),
|
||||
"time": int(row.get("time")),
|
||||
"type": row.get("type"),
|
||||
"sender": row.get("sender"),
|
||||
"text": row.get("text"),
|
||||
"attaches": json.loads(row.get("attaches")),
|
||||
# "reactionInfo": {}
|
||||
}
|
||||
|
||||
# Возвращаем
|
||||
return message, int(row.get("time"))
|
||||
85
src/main.py
Normal file
85
src/main.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# Импортирование библиотек
|
||||
import aiomysql, ssl, logging, asyncio
|
||||
from common.config import ServerConfig
|
||||
from oneme_tcp.controller import OnemeMobileController
|
||||
from telegrambot.controller import TelegramBotController
|
||||
from tamtam_tcp.controller import TTMobileController
|
||||
from tamtam_ws.controller import TTWSController
|
||||
# Конфиг сервера
|
||||
server_config = ServerConfig()
|
||||
|
||||
async def init_db():
|
||||
"""Инициализация базы данных"""
|
||||
# Создаем пул
|
||||
db = await aiomysql.create_pool(
|
||||
host=server_config.db_host,
|
||||
port=server_config.db_port,
|
||||
user=server_config.db_user,
|
||||
password=server_config.db_password,
|
||||
db=server_config.db_name,
|
||||
cursorclass=aiomysql.DictCursor,
|
||||
autocommit=True
|
||||
)
|
||||
|
||||
# Возвращаем
|
||||
return db
|
||||
|
||||
def init_ssl():
|
||||
"""Создание контекста SSL"""
|
||||
# Создаем контекст SSL
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context.load_cert_chain(server_config.certfile, server_config.keyfile)
|
||||
|
||||
# Возвращаем
|
||||
return ssl_context
|
||||
|
||||
def set_logging():
|
||||
"""Настройка уровня логирования"""
|
||||
# Настройка уровня логирования
|
||||
log_level = server_config.log_level
|
||||
|
||||
if log_level == "debug":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
elif log_level == "info":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
else:
|
||||
logging.basicConfig(level=None)
|
||||
|
||||
async def main():
|
||||
"""Запуск сервера"""
|
||||
async def api_event(target, eventData):
|
||||
for client in api.get("clients").get(target, {}).get("clients", {}):
|
||||
await controllers[client["protocol"]].event(target, client, eventData)
|
||||
|
||||
set_logging()
|
||||
db = await init_db()
|
||||
ssl_context = init_ssl()
|
||||
clients = {}
|
||||
|
||||
api = {
|
||||
"db": db,
|
||||
"ssl": ssl_context,
|
||||
"clients": clients,
|
||||
"event": api_event
|
||||
}
|
||||
|
||||
controllers = {
|
||||
"oneme_mobile": OnemeMobileController(),
|
||||
"tamtam_mobile": TTMobileController(),
|
||||
"tamtam_ws": TTWSController(),
|
||||
"telegrambot": TelegramBotController()
|
||||
}
|
||||
|
||||
# Add telegram bot controller to api for use by processors
|
||||
api["telegram_bot"] = controllers["telegrambot"]
|
||||
|
||||
tasks = [
|
||||
controller.launch(api)
|
||||
for controller in controllers.values()
|
||||
]
|
||||
|
||||
# Запускаем контроллеры
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
0
src/oneme_tcp/__init__.py
Normal file
0
src/oneme_tcp/__init__.py
Normal file
383
src/oneme_tcp/config.py
Normal file
383
src/oneme_tcp/config.py
Normal file
@@ -0,0 +1,383 @@
|
||||
class OnemeConfig:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
# TODO: почистить вообще надо, и настройки потыкать
|
||||
SERVER_CONFIG = {
|
||||
"account-nickname-enabled": False,
|
||||
"account-removal-enabled": False,
|
||||
"anr-config": {
|
||||
"enabled": True,
|
||||
"timeout": {
|
||||
"low": 5000,
|
||||
"avg": 5000,
|
||||
"high": 5000
|
||||
}
|
||||
},
|
||||
"appearance-multi-theme-screen-enabled": True,
|
||||
"audio-transcription-locales": [],
|
||||
"available-complaints": [
|
||||
"FAKE",
|
||||
"SPAM",
|
||||
"PORNO",
|
||||
"EXTREMISM",
|
||||
"THREAT",
|
||||
"OTHER"
|
||||
],
|
||||
"avatars-screen-enabled": True,
|
||||
"bad-networ-indicator-config": {
|
||||
"signalingConfig": {
|
||||
"dcReportNetworkStatEnabled": False
|
||||
}
|
||||
},
|
||||
"bots-channel-adding": True,
|
||||
"cache-msg-preprocess": True,
|
||||
"call-incoming-ab": 2,
|
||||
"call-permissions-interval": 259200,
|
||||
"call-pinch-to-zoom": True,
|
||||
"call-rate": {
|
||||
"limit": 3,
|
||||
"sdk-limit": 2,
|
||||
"duration": 10,
|
||||
"delay": 86400
|
||||
},
|
||||
"callDontUseVpnForRtp": False,
|
||||
"callEnableIceRenomination": False,
|
||||
"calls-endpoint": "https://calls.okcdn.ru/",
|
||||
"calls-sdk-am-speaker-fix": True,
|
||||
"calls-sdk-audio-dynamic-redundancy": {
|
||||
"mab": 16,
|
||||
"dsb": 64,
|
||||
"nl": True,
|
||||
"df": True,
|
||||
"dlb": True
|
||||
},
|
||||
"calls-sdk-enable-nohost": True,
|
||||
"calls-sdk-incall-stat": False,
|
||||
"calls-sdk-linear-opus-bwe": True,
|
||||
"calls-sdk-mapping": {
|
||||
"off": True
|
||||
},
|
||||
"calls-sdk-remove-nonopus-audiocodecs": True,
|
||||
"calls-use-call-end-reason-fix": True,
|
||||
"calls-use-ws-url-validation": True,
|
||||
"cfs": True,
|
||||
"channels-complaint-enabled": True,
|
||||
"channels-enabled": True,
|
||||
"channels-search-subscribers-visible": True,
|
||||
"chat-complaint-enabled": False,
|
||||
"chat-gif-autoplay-enabled": True,
|
||||
"chat-history-notif-msg-strategy": 1,
|
||||
"chat-history-persist": False,
|
||||
"chat-history-warm-opts": 0,
|
||||
"chat-invite-link-permissions-enabled": True,
|
||||
"chat-media-scrollable-caption-enabled": True,
|
||||
"chat-video-autoplay-enabled": True,
|
||||
"chat-video-call-button": True,
|
||||
"chatlist-subtitle-ver": 1,
|
||||
"chats-folder-enabled": True,
|
||||
"chats-page-size": 50,
|
||||
"chats-preload-period": 15,
|
||||
"cis-enabled": True,
|
||||
"contact-add-bottom-sheet": True,
|
||||
"creation-2fa-config": {
|
||||
"pass_min_len": 6,
|
||||
"pass_max_len": 64,
|
||||
"hint_max_len": 30,
|
||||
"enabled": True
|
||||
},
|
||||
"debug-profile-info": False,
|
||||
"default-reactions-settings": {
|
||||
"isActive": True,
|
||||
"count": 8,
|
||||
"included": False,
|
||||
"reactionIds": []
|
||||
},
|
||||
"delete-msg-fys-large-chat-disabled": True,
|
||||
"devnull": {
|
||||
"opcode": True,
|
||||
"upload_hang": True
|
||||
},
|
||||
"disconnect-timeout": 300,
|
||||
"double-tap-reaction": "👍",
|
||||
"double-tap-reaction-enabled": True,
|
||||
"drafts-sync-enabled": False,
|
||||
"edit-chat-type-screen-enabled": False,
|
||||
"edit-timeout": 604800,
|
||||
"enable-filters-for-folders": True,
|
||||
"enable-unknown-contact-bottom-sheet": 2,
|
||||
"fake-chats": True,
|
||||
"family-protection-botid": 67804175,
|
||||
"february-23-26-theme": True,
|
||||
"file-preview": True,
|
||||
"file-upload-enabled": True,
|
||||
"file-upload-max-size": 4294967296,
|
||||
"file-upload-unsupported-types": [
|
||||
"exe"
|
||||
],
|
||||
"force-play-embed": True,
|
||||
"gc-from-p2p": True,
|
||||
"gce": False,
|
||||
"group-call-part-limit": 100,
|
||||
"grse": False,
|
||||
"gsse": True,
|
||||
"hide-incoming-call-notif": True,
|
||||
"host-reachability": True,
|
||||
"image-height": 1920,
|
||||
"image-quality": 0.800000011920929,
|
||||
"image-size": 40000000,
|
||||
"image-width": 1920,
|
||||
"in-app-review-triggers": 255,
|
||||
"informer-enabled": True,
|
||||
"inline-ev-player": True,
|
||||
"invalidate-db-msg-exception": True,
|
||||
"invite-friends-sheet-frequency": [
|
||||
2,
|
||||
7
|
||||
],
|
||||
"invite-link": "https://t.me/openmax_alerts",
|
||||
"invite-long": "Я пользуюсь OpenMAX. Присоединяйся! https://t.me/openmax_alerts",
|
||||
"invite-short": "Я пользуюсь OpenMAX. Присоединяйся! https://t.me/openmax_alerts",
|
||||
"join-requests": True,
|
||||
"js-download-delegate": False,
|
||||
"keep-connection": 2,
|
||||
"lebedev-theme-enabled": True,
|
||||
"lgce": True,
|
||||
"markdown-enabled": True,
|
||||
"markdown-menu": 0,
|
||||
"max-audio-length": 3600,
|
||||
"max-description-length": 400,
|
||||
"max-favorite-chats": 5,
|
||||
"max-favorite-sticker-sets": 100,
|
||||
"max-favorite-stickers": 100,
|
||||
"max-msg-length": 4000,
|
||||
"max-participants": 20000,
|
||||
"max-readmarks": 100,
|
||||
"max-theme-length": 200,
|
||||
"max-video-duration-download": 1200,
|
||||
"max-video-message-length": 60,
|
||||
"media-order": 1,
|
||||
"media-playlist-enabled": True,
|
||||
"media-transform": {
|
||||
"enabled": True,
|
||||
"hdr_enabled": False,
|
||||
"hevc_enabled": True,
|
||||
"max_enc_frames": {
|
||||
"low": 1,
|
||||
"avg": 1,
|
||||
"high": 2
|
||||
}
|
||||
},
|
||||
"media-viewer-rotation-enabled": True,
|
||||
"media-viewer-video-collage-enabled": True,
|
||||
"mentions-enabled": True,
|
||||
"mentions_entity_names_limit": 3,
|
||||
"migrate-unsafe-warn": True,
|
||||
"min-image-side-size": 64,
|
||||
"miui-menu-enabled": True,
|
||||
"money-transfer-botid": 1134691,
|
||||
"moscow-theme-enabled": True,
|
||||
"msg-get-reactions-page-size": 40,
|
||||
"music-files-enabled": False,
|
||||
"mytracker-enabled": True,
|
||||
"net-client-dns-enabled": True,
|
||||
"net-session-suppress-bad-disconnected-state": True,
|
||||
"net-stat-config": [
|
||||
64,
|
||||
48,
|
||||
128,
|
||||
135
|
||||
],
|
||||
"new-admin-permissions": True,
|
||||
"new-logout-logic": False,
|
||||
"new-media-upload-ui": True,
|
||||
"new-media-viewer-enabled": True,
|
||||
"new-settings-storage-screen-enabled": False,
|
||||
"new-width-text-bubbles-mob": True,
|
||||
"new-year-theme-2026": False,
|
||||
"nick-max-length": 60,
|
||||
"nick-min-length": 7,
|
||||
"official-org": True,
|
||||
"one-video-failover": True,
|
||||
"one-video-player": True,
|
||||
"one-video-uploader": True,
|
||||
"one-video-uploader-audio": True,
|
||||
"one-video-uploader-progress-fix": True,
|
||||
"perf-events": {
|
||||
"startup_report": 2,
|
||||
"web_app": 2
|
||||
},
|
||||
"player-load-control": {
|
||||
"mp_autoplay_enabled": False,
|
||||
"time_over_size": False,
|
||||
"buffer_after_rebuffer_ms": 3000,
|
||||
"buffer_ms": 500,
|
||||
"max_buffer_ms": 13000,
|
||||
"min_buffer_ms": 5000,
|
||||
"use_min_size_lc": True,
|
||||
"min_size_lc_fmt_mis_sf": 4
|
||||
},
|
||||
"progress-diff-for-notify": 1,
|
||||
"push-delivery": True,
|
||||
"qr-auth-enabled": True,
|
||||
"quotes-enabled": True,
|
||||
"react-errors": [
|
||||
"error.comment.chat.access",
|
||||
"error.comment.invalid",
|
||||
"error.message.invalid",
|
||||
"error.message.chat.access",
|
||||
"error.message.like.unknown.like",
|
||||
"error.message.like.unknown.reaction",
|
||||
"error.too-many-unlikes-dialog",
|
||||
"error.too-many-unlikes-chat",
|
||||
"error.too-many-likes",
|
||||
"error.reactions.not.allowed"
|
||||
],
|
||||
"react-permission": 2,
|
||||
"reactions-enabled": True,
|
||||
"reactions-max": 8,
|
||||
"reactions-menu": [
|
||||
"👍",
|
||||
"❤️",
|
||||
"🤣",
|
||||
"🔥",
|
||||
"😭",
|
||||
"💯",
|
||||
"💩",
|
||||
"😡"
|
||||
],
|
||||
"reactions-settings-enabled": True,
|
||||
"reconnect-call-ringtone": True,
|
||||
"ringtone-am-mode": True,
|
||||
"saved-messages-aliases": [
|
||||
"избранное",
|
||||
"saved",
|
||||
"favourite",
|
||||
"favorite",
|
||||
"личное",
|
||||
"моё",
|
||||
"мои",
|
||||
"мой",
|
||||
"моя",
|
||||
"любимое",
|
||||
"сохраненные",
|
||||
"сохраненное",
|
||||
"заметки",
|
||||
"закладки"
|
||||
],
|
||||
"scheduled-messages-enabled": True,
|
||||
"scheduled-posts-enabled": True,
|
||||
"search-webapps-showcase": {
|
||||
"items": [
|
||||
{
|
||||
"id": 4479862,
|
||||
"icon": "https://st.max.ru/icons/icon_channel_square.webp",
|
||||
"title": "Каналы"
|
||||
}
|
||||
]
|
||||
},
|
||||
"send-location-enabled": True,
|
||||
"send-logs-interval-sec": 900,
|
||||
"server-side-complains-enabled": True,
|
||||
"set-audio-device": False,
|
||||
"set-unread-timeout": 31536000,
|
||||
"settings-entry-banners": [
|
||||
{
|
||||
"id": 1,
|
||||
"logo": "https://st.max.ru/icons/epgu_white_111125.png",
|
||||
"align": 2,
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://st.max.ru/icons/digital_id_new_40_3x.png",
|
||||
"title": "Цифровой ID",
|
||||
"appid": 8250447
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"items": [
|
||||
{
|
||||
"icon": "https://st.max.ru/icons/sferum_with_padding_120.png",
|
||||
"title": "Войти в Cферум",
|
||||
"appid": 2340831
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"show-reactions-on-multiselect": True,
|
||||
"show-warning-links": True,
|
||||
"speedy-upload": True,
|
||||
"speedy-voice-messages": True,
|
||||
"sse": True,
|
||||
"stat-session-background-threshold": 60000,
|
||||
"sticker-suggestion": [
|
||||
"RECENT",
|
||||
"NEW",
|
||||
"TOP"
|
||||
],
|
||||
"stickers-controller-suspend": True,
|
||||
"stickers-db-batch": True,
|
||||
"streamable-mp4": True,
|
||||
"stub": "stub2",
|
||||
"suspend-video-converter": True,
|
||||
"system-default-ringtone-opt": True,
|
||||
"transfer-botid": 1134691,
|
||||
"typing-enabled-FILE": True,
|
||||
"unique-favorites": True,
|
||||
"unsafe-files-alert": True,
|
||||
"upload-reusability": True,
|
||||
"upload-rx-no-blocking": True,
|
||||
"user-debug-report": 2340932,
|
||||
"video-msg-channels-enabled": True,
|
||||
"video-msg-config": {
|
||||
"duration": 60,
|
||||
"quality": 480,
|
||||
"min_frame_rate": 30,
|
||||
"max_frame_rate": 30
|
||||
},
|
||||
"video-msg-enabled": True,
|
||||
"video-transcoding-class": [
|
||||
2,
|
||||
3
|
||||
],
|
||||
"views-count-enabled": True,
|
||||
"watchdog-config": {
|
||||
"enabled": True,
|
||||
"stuck": 10,
|
||||
"hang": 60
|
||||
},
|
||||
"webapp-exc": [
|
||||
63602953,
|
||||
8250447
|
||||
],
|
||||
"webapp-push-open": True,
|
||||
"webview-cache-enabled": False,
|
||||
"welcome-sticker-ids": [
|
||||
272821,
|
||||
295349,
|
||||
13571,
|
||||
546741,
|
||||
476341
|
||||
],
|
||||
"white-list-links": [
|
||||
"max.ru",
|
||||
"vk.com",
|
||||
"vk.ru",
|
||||
"gosuslugi.ru",
|
||||
"mail.ru",
|
||||
"vk.ru",
|
||||
"vkvideo.ru"
|
||||
],
|
||||
"wm-analytics-enabled": True,
|
||||
"wm-workers-limit": 80,
|
||||
"wud": False,
|
||||
"y-map": {
|
||||
"tile": "34c7fd82-723d-4b23-8abb-33376729a893",
|
||||
"geocoder": "34c7fd82-723d-4b23-8abb-33376729a893",
|
||||
"static": "34c7fd82-723d-4b23-8abb-33376729a893",
|
||||
"logoLight": "https://st.max.ru/icons/ya_maps_logo_light.webp",
|
||||
"logoDark": "https://st.max.ru/icons/ya_maps_logo_dark.webp"
|
||||
},
|
||||
"has-phone": True
|
||||
}
|
||||
75
src/oneme_tcp/controller.py
Normal file
75
src/oneme_tcp/controller.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import asyncio
|
||||
from oneme_tcp.server import OnemeMobileServer
|
||||
from oneme_tcp.proto import Proto
|
||||
from classes.controllerbase import ControllerBase
|
||||
from common.config import ServerConfig
|
||||
|
||||
class OnemeMobileController(ControllerBase):
|
||||
def __init__(self):
|
||||
self.config = ServerConfig()
|
||||
self.proto = Proto()
|
||||
|
||||
async def event(self, target, client, eventData):
|
||||
# Извлекаем тип события и врайтер
|
||||
eventType = eventData.get("eventType")
|
||||
writer = client.get("writer")
|
||||
|
||||
# Обрабатываем событие
|
||||
if eventType == "new_msg":
|
||||
# Данные сообщения
|
||||
chatId = eventData.get("chatId")
|
||||
message = eventData.get("message")
|
||||
prevMessageId = eventData.get("prevMessageId")
|
||||
time = eventData.get("time")
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"chatId": chatId,
|
||||
"message": message,
|
||||
"prevMessageId": prevMessageId,
|
||||
"ttl": False,
|
||||
"unread": 0,
|
||||
"mark": time
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=0, seq=1, opcode=self.proto.NOTIF_MESSAGE, payload=payload
|
||||
)
|
||||
elif eventType == "typing":
|
||||
# Данные события
|
||||
chatId = eventData.get("chatId")
|
||||
userId = eventData.get("userId")
|
||||
type = eventData.get("type")
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"chatId": chatId,
|
||||
"userId": userId,
|
||||
"type": type
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=0, seq=1, opcode=self.proto.NOTIF_TYPING, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем пакет
|
||||
writer.write(packet)
|
||||
await writer.drain()
|
||||
|
||||
def launch(self, api):
|
||||
async def _start_all():
|
||||
await asyncio.gather(
|
||||
OnemeMobileServer(
|
||||
host=self.config.host,
|
||||
port=self.config.oneme_tcp_port,
|
||||
ssl_context=api['ssl'],
|
||||
db_pool=api['db'],
|
||||
clients=api['clients'],
|
||||
send_event=api['event'],
|
||||
telegram_bot=api.get('telegram_bot'),
|
||||
).start()
|
||||
)
|
||||
|
||||
return _start_all()
|
||||
96
src/oneme_tcp/models.py
Normal file
96
src/oneme_tcp/models.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import pydantic
|
||||
|
||||
class UserAgentModel(pydantic.BaseModel):
|
||||
deviceType: str
|
||||
appVersion: str
|
||||
osVersion: str
|
||||
timezone: str
|
||||
release: int = None
|
||||
screen: str
|
||||
pushDeviceType: str
|
||||
arch: str = None
|
||||
locale: str
|
||||
buildNumber: int
|
||||
deviceName: str
|
||||
deviceLocale: str
|
||||
|
||||
class HelloPayloadModel(pydantic.BaseModel):
|
||||
clientSessionId: int
|
||||
mt_instanceid: str = None
|
||||
userAgent: UserAgentModel
|
||||
deviceId: str
|
||||
|
||||
class RequestCodePayloadModel(pydantic.BaseModel):
|
||||
phone: str
|
||||
type: str
|
||||
|
||||
@pydantic.field_validator('phone')
|
||||
def validate_phone(cls, v):
|
||||
"""Валидация номера телефона"""
|
||||
if not v.replace("+", "").replace(" ", "").replace("-", "").isdigit():
|
||||
raise ValueError('phone must be digits')
|
||||
return v
|
||||
|
||||
@pydantic.field_validator('type')
|
||||
def validate_type(cls, v):
|
||||
"""Валидация типа запроса"""
|
||||
if not v in ("START_AUTH", "RESEND"):
|
||||
raise ValueError('type must be valid')
|
||||
return v
|
||||
|
||||
class VerifyCodePayloadModel(pydantic.BaseModel):
|
||||
verifyCode: str
|
||||
authTokenType: str
|
||||
token: str
|
||||
|
||||
class LoginPayloadModel(pydantic.BaseModel):
|
||||
interactive: bool
|
||||
token: str
|
||||
|
||||
class PingPayloadModel(pydantic.BaseModel):
|
||||
interactive: bool
|
||||
|
||||
class AssetsPayloadModel(pydantic.BaseModel):
|
||||
sync: int
|
||||
type: str
|
||||
|
||||
class GetCallHistoryPayloadModel(pydantic.BaseModel):
|
||||
forward: bool
|
||||
count: int
|
||||
|
||||
class MessageModel(pydantic.BaseModel):
|
||||
isLive: bool
|
||||
detectShare: bool
|
||||
elements: list
|
||||
attaches: list = None
|
||||
cid: int
|
||||
text: str = None
|
||||
|
||||
class SendMessagePayloadModel(pydantic.BaseModel):
|
||||
# TODO: пишем сервер макса в 2 ночи и не понимаем как это валидировать (блять)
|
||||
userId: int = None
|
||||
chatId: int = None
|
||||
message: MessageModel
|
||||
|
||||
class SyncFoldersPayloadModel(pydantic.BaseModel):
|
||||
folderSync: int
|
||||
|
||||
class SearchChatsPayloadModel(pydantic.BaseModel):
|
||||
chatIds: list
|
||||
|
||||
class SearchByPhonePayloadModel(pydantic.BaseModel):
|
||||
phone: str
|
||||
|
||||
class GetCallTokenPayloadModel(pydantic.BaseModel):
|
||||
userId: int
|
||||
value: str
|
||||
|
||||
class TypingPayloadModel(pydantic.BaseModel):
|
||||
chatId: int
|
||||
type: str = None
|
||||
|
||||
class SearchUsersPayloadModel(pydantic.BaseModel):
|
||||
contactIds: list
|
||||
|
||||
class ComplainReasonsGetPayloadModel(pydantic.BaseModel):
|
||||
complainSync: int
|
||||
882
src/oneme_tcp/processors.py
Normal file
882
src/oneme_tcp/processors.py
Normal file
@@ -0,0 +1,882 @@
|
||||
import json, random, secrets, hashlib, time, logging
|
||||
from oneme_tcp.models import *
|
||||
from oneme_tcp.proto import Proto
|
||||
from oneme_tcp.config import OnemeConfig
|
||||
from common.tools import Tools
|
||||
from common.config import ServerConfig
|
||||
from common.static import Static
|
||||
|
||||
class Processors:
|
||||
def __init__(self, db_pool=None, clients={}, send_event=None, telegram_bot=None):
|
||||
self.proto = Proto()
|
||||
self.tools = Tools()
|
||||
self.config = ServerConfig()
|
||||
self.static = Static()
|
||||
self.server_config = OnemeConfig().SERVER_CONFIG
|
||||
self.error_types = self.static.ErrorTypes()
|
||||
self.chat_types = self.static.ChatTypes()
|
||||
|
||||
self.db_pool = db_pool
|
||||
self.event = send_event
|
||||
self.clients = clients
|
||||
self.telegram_bot = telegram_bot
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def _send(self, writer, packet):
|
||||
try:
|
||||
writer.write(packet)
|
||||
await writer.drain()
|
||||
except Exception as error:
|
||||
self.logger.error(f"Ошибка при отправке пакета - {error}")
|
||||
|
||||
async def _send_error(self, seq, opcode, type, writer):
|
||||
payload = self.static.ERROR_TYPES.get(type, {
|
||||
"localizedMessage": "Неизвестная ошибка",
|
||||
"error": "unknown.error",
|
||||
"message": "Unknown error",
|
||||
"title": "Неизвестная ошибка"
|
||||
})
|
||||
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_ERR, seq=seq, opcode=opcode, payload=payload
|
||||
)
|
||||
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_hello(self, payload, seq, writer):
|
||||
"""Обработчик приветствия"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
HelloPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.HELLO, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return None, None
|
||||
|
||||
# Получаем данные из пакета
|
||||
deviceType = payload.get("userAgent").get("deviceType")
|
||||
deviceName = payload.get("userAgent").get("deviceName")
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"location": "RU",
|
||||
"app-update-type": 0, # 1 = принудительное обновление
|
||||
"reg-country-code": [
|
||||
# Список стран, который отдает официальный сервер
|
||||
"AZ", "AM", "KZ", "KG", "MD", "TJ", "UZ", "GE", "TH", "TR",
|
||||
"TM", "AE", "LA", "MY", "ID", "CU", "KH", "VN",
|
||||
|
||||
# Список стран, который приделали уже мы
|
||||
"US", "CA", "UA"
|
||||
],
|
||||
"phone-auto-complete-enabled": False,
|
||||
"lang": True
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.SESSION_INIT, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
return deviceType, deviceName
|
||||
|
||||
async def process_ping(self, payload, seq, writer):
|
||||
"""Обработчик пинга"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
PingPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.PING, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Собираем пакет
|
||||
response = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.PING, payload=None
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
|
||||
async def process_telemetry(self, payload, seq, writer):
|
||||
"""Обработчик телеметрии"""
|
||||
# TODO: можно было бы реализовать валидацию телеметрии, но сейчас это не особо важно
|
||||
|
||||
# Собираем пакет
|
||||
response = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.LOG, payload=None
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
|
||||
async def process_request_code(self, payload, seq, writer):
|
||||
"""Обработчик запроса кода"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
RequestCodePayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.AUTH_REQUEST, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем телефон из пакета
|
||||
phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "")
|
||||
|
||||
# Генерируем токен с кодом
|
||||
code = str(random.randint(100000, 999999))
|
||||
token = secrets.token_urlsafe(128)
|
||||
|
||||
# Хешируем
|
||||
code_hash = hashlib.sha256(code.encode()).hexdigest()
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Время истечения токена
|
||||
expires = int(time.time()) + 300
|
||||
|
||||
# Ищем пользователя, и если он существует, сохраняем токен
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
|
||||
user = await cursor.fetchone()
|
||||
|
||||
# Если пользователя нет - отдаем ошибку
|
||||
if user is None:
|
||||
await self._send_error(seq, self.proto.AUTH_REQUEST, self.error_types.USER_NOT_FOUND, writer)
|
||||
return
|
||||
|
||||
# Сохраняем токен
|
||||
await cursor.execute("INSERT INTO auth_tokens (phone, token_hash, code_hash, expires) VALUES (%s, %s, %s, %s)", (phone, token_hash, code_hash, expires,))
|
||||
|
||||
# Если тг бот включен, и тг привязан к аккаунту - отправляем туда сообщение
|
||||
if self.telegram_bot and user.get("telegram_id"):
|
||||
await self.telegram_bot.send_code(chat_id=int(user.get("telegram_id")), phone=phone, code=code)
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"requestMaxDuration": 60000,
|
||||
"requestCountLeft": 10,
|
||||
"altActionDuration": 60000,
|
||||
"codeLength": 6,
|
||||
"token": token
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.AUTH_REQUEST, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
self.logger.debug(f"Код для {phone}: {code}")
|
||||
|
||||
async def process_verify_code(self, payload, seq, writer, deviceType, deviceName):
|
||||
"""Обработчик проверки кода"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
VerifyCodePayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.AUTH, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
code = payload.get("verifyCode")
|
||||
token = payload.get("token")
|
||||
|
||||
# Хешируем токен с кодом
|
||||
hashed_code = hashlib.sha256(code.encode()).hexdigest()
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Генерируем постоянный токен
|
||||
login = secrets.token_urlsafe(128)
|
||||
hashed_login = hashlib.sha256(login.encode()).hexdigest()
|
||||
|
||||
# Ищем токен с кодом
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# Ищем токен
|
||||
await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", (hashed_token,))
|
||||
stored_token = await cursor.fetchone()
|
||||
|
||||
# Если токен просрочен, или его нет - отправляем ошибку
|
||||
if stored_token is None:
|
||||
await self._send_error(seq, self.proto.AUTH, self.error_types.CODE_EXPIRED, writer)
|
||||
return
|
||||
|
||||
# Проверяем код
|
||||
if stored_token.get("code_hash") != hashed_code:
|
||||
await self._send_error(seq, self.proto.AUTH, self.error_types.INVALID_CODE, writer)
|
||||
return
|
||||
|
||||
# Ищем аккаунт
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
|
||||
account = await cursor.fetchone()
|
||||
|
||||
# Удаляем токен
|
||||
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
|
||||
|
||||
# Создаем сессию
|
||||
await cursor.execute(
|
||||
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||
(stored_token.get("phone"), hashed_login, deviceType, deviceName, "Epstein Island", int(time.time()),)
|
||||
)
|
||||
|
||||
# Генерируем профиль
|
||||
# Аватарка с биографией
|
||||
photoId = None if not account.get("avatar_id") else int(account.get("avatar_id"))
|
||||
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
|
||||
description = None if not account.get("description") else account.get("description")
|
||||
|
||||
# Собираем данные пакета
|
||||
payload = {
|
||||
"tokenAttrs": {
|
||||
"LOGIN": {
|
||||
"token": login
|
||||
}
|
||||
},
|
||||
"profile": self.tools.generate_profile(
|
||||
id=account.get("id"),
|
||||
phone=int(account.get("phone")),
|
||||
avatarUrl=avatar_url,
|
||||
photoId=photoId,
|
||||
updateTime=int(account.get("updatetime")),
|
||||
firstName=account.get("firstname"),
|
||||
lastName=account.get("lastname"),
|
||||
options=json.loads(account.get("options")),
|
||||
description=description,
|
||||
accountStatus=int(account.get("accountstatus")),
|
||||
profileOptions=json.loads(account.get("profileoptions")),
|
||||
includeProfileOptions=True,
|
||||
username=account.get("username")
|
||||
)
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.AUTH, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_login(self, payload, seq, writer):
|
||||
"""Обработчик авторизации клиента на сервере"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
LoginPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.LOGIN, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Получаем данные из пакета
|
||||
token = payload.get("token")
|
||||
|
||||
# Хешируем токен
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Ищем токен в бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM tokens WHERE token_hash = %s", (hashed_token,))
|
||||
token_data = await cursor.fetchone()
|
||||
|
||||
# Если токен не найден, отправляем ошибку
|
||||
if token_data is None:
|
||||
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_TOKEN, writer)
|
||||
return
|
||||
|
||||
# Ищем аккаунт пользователя в бд
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
|
||||
user = await cursor.fetchone()
|
||||
|
||||
# Ищем данные пользователя в бд
|
||||
await cursor.execute("SELECT * FROM user_data WHERE phone = %s", (token_data.get("phone"),))
|
||||
user_data = await cursor.fetchone()
|
||||
|
||||
# Аватарка с биографией
|
||||
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=True,
|
||||
username=user.get("username")
|
||||
)
|
||||
|
||||
chats = await self.tools.generate_chats(
|
||||
json.loads(user_data.get("chats")),
|
||||
self.db_pool, user.get("id")
|
||||
)
|
||||
|
||||
# Формируем данные пакета
|
||||
payload = {
|
||||
"profile": profile,
|
||||
"chats": chats,
|
||||
"chatMarker": 0,
|
||||
"messages": {},
|
||||
"contacts": [],
|
||||
"presence": {},
|
||||
"config": {
|
||||
"server": self.server_config,
|
||||
"user": json.loads(user_data.get("user_config"))
|
||||
},
|
||||
"token": token,
|
||||
"videoChatHistory": False,
|
||||
"time": int(time.time() * 1000)
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.LOGIN, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
return int(user.get("phone")), int(user.get("id")), hashed_token
|
||||
|
||||
async def process_logout(self, seq, writer, hashedToken):
|
||||
"""Обработчик завершения сессии"""
|
||||
# Удаляем токен из бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("DELETE FROM tokens WHERE token_hash = %s", (hashedToken,))
|
||||
|
||||
# Создаем пакет
|
||||
response = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.LOGOUT, payload=None
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
|
||||
async def process_get_assets(self, payload, seq, writer):
|
||||
"""Обработчик запроса ассетов клиента на сервере"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
AssetsPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"sections": [],
|
||||
"sync": int(time.time() * 1000)
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.ASSETS_UPDATE, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_get_call_history(self, payload, seq, writer):
|
||||
"""Обработчик получения истории звонков"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
GetCallHistoryPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.VIDEO_CHAT_HISTORY, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"hasMore": False,
|
||||
"history": [],
|
||||
"backwardMarker": 0,
|
||||
"forwardMarker": 0
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.VIDEO_CHAT_HISTORY, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_send_message(self, payload, seq, writer, senderId, db_pool):
|
||||
"""Функция отправки сообщения"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
SendMessagePayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.MSG_SEND, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
userId = payload.get("userId")
|
||||
chatId = payload.get("chatId")
|
||||
message = payload.get("message")
|
||||
|
||||
elements = message.get("elements") or []
|
||||
attaches = message.get("attaches") or []
|
||||
cid = message.get("cid") or 0
|
||||
text = message.get("text") or ""
|
||||
|
||||
# Если клиент вообще ничего не указал в пакете, то выбрасываем ошибку
|
||||
if not all([userId, chatId, elements, attaches, cid, text]):
|
||||
await self._send_error(seq, self.proto.MSG_SEND, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Время отправки сообщения
|
||||
messageTime = int(time.time() * 1000)
|
||||
|
||||
# Вычисляем ID чата по ID пользователя и ID отправителя,
|
||||
# в случае отсутствия ID чата
|
||||
if not chatId:
|
||||
chatId = userId ^ senderId
|
||||
|
||||
# Если клиент хочет отправить сообщение в избранное,
|
||||
# то выставляем ID чата 0
|
||||
# (А ещё используем это, если клиент вообще ничего не указал)
|
||||
if chatId == 0 or not chatId:
|
||||
chatId = senderId
|
||||
participants = [senderId]
|
||||
else:
|
||||
# Если все таки клиент хочет отправить сообщение в нормальный чат,
|
||||
# то ищем его в базе данных (извлекать список участников все таки тоже надо)
|
||||
async with db_pool.acquire() as db_connection:
|
||||
async with db_connection.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
|
||||
chat = await cursor.fetchone()
|
||||
|
||||
# Если нет такого чата - выбрасываем ошибку
|
||||
if not chat:
|
||||
await self._send_error(seq, self.proto.MSG_SEND, self.error_types.CHAT_NOT_FOUND, writer)
|
||||
return
|
||||
|
||||
# Список участников
|
||||
participants = json.loads(chat.get("participants"))
|
||||
|
||||
# Проверяем, является ли отправитель участником чата
|
||||
if int(senderId) not in participants:
|
||||
await self._send_error(seq, self.proto.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
|
||||
return
|
||||
|
||||
# Добавляем сообщение в историю
|
||||
messageId, lastMessageId = await self.tools.insert_message(
|
||||
chatId=chatId,
|
||||
senderId=senderId,
|
||||
text=text,
|
||||
attaches=attaches,
|
||||
elements=elements,
|
||||
cid=cid,
|
||||
type="USER",
|
||||
db_pool=self.db_pool
|
||||
)
|
||||
|
||||
# Готовое тело сообщения
|
||||
bodyMessage = {
|
||||
"id": messageId,
|
||||
"time": messageTime,
|
||||
"type": "USER",
|
||||
"sender": senderId,
|
||||
"cid": cid,
|
||||
"text": text,
|
||||
"attaches": attaches
|
||||
}
|
||||
|
||||
# Отправляем событие всем участникам чата
|
||||
for participant in participants:
|
||||
await self.event(
|
||||
participant,
|
||||
{
|
||||
"eventType": "new_msg",
|
||||
"chatId": 0 if chatId == senderId else chatId,
|
||||
"message": bodyMessage,
|
||||
"prevMessageId": lastMessageId,
|
||||
"time": messageTime
|
||||
}
|
||||
)
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"chatId": 0 if chatId == senderId else chatId,
|
||||
"message": bodyMessage,
|
||||
"unread": 0,
|
||||
"mark": messageTime
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.MSG_SEND, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_get_folders(self, payload, seq, writer, senderPhone):
|
||||
"""Синхронизация папок с сервером"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
SyncFoldersPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.FOLDERS_GET, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Ищем папки в бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT folders FROM user_data WHERE phone = %s", (int(senderPhone),))
|
||||
result_folders = await cursor.fetchone()
|
||||
user_folders = json.loads(result_folders.get("folders"))
|
||||
|
||||
# Создаем данные пакета
|
||||
payload = {
|
||||
"folderSync": int(time.time() * 1000),
|
||||
"folders": self.static.ALL_CHAT_FOLDER + user_folders.get("folders"),
|
||||
"foldersOrder": self.static.ALL_CHAT_FOLDER_ORDER + user_folders.get("foldersOrder"),
|
||||
"allFilterExcludeFolders": user_folders.get("allFilterExcludeFolders")
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.FOLDERS_GET, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_get_sessions(self, payload, seq, writer, senderPhone, hashedToken):
|
||||
"""Получение активных сессий на аккаунте"""
|
||||
# Готовый список сессий
|
||||
sessions = []
|
||||
|
||||
# Ищем сессии в бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM tokens WHERE phone = %s", (str(senderPhone),))
|
||||
user_sessions = await cursor.fetchall()
|
||||
|
||||
# Собираем сессии в список
|
||||
for session in user_sessions:
|
||||
sessions.append(
|
||||
{
|
||||
"time": int(session.get("time")),
|
||||
"client": f"MAX {session.get('device_type')}",
|
||||
"info": session.get("device_name"),
|
||||
"location": session.get("location"),
|
||||
"current": True if session.get("token_hash") == hashedToken else False
|
||||
}
|
||||
)
|
||||
|
||||
# Создаем данные пакета
|
||||
payload = {
|
||||
"sessions": sessions
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
response = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.SESSIONS_INFO, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
|
||||
async def process_search_users(self, payload, seq, writer):
|
||||
"""Поиск пользователей по ID"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
SearchUsersPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.CONTACT_INFO, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Итоговый список пользователей
|
||||
users = []
|
||||
|
||||
# ID пользователей, которые нам предстоит найти
|
||||
contactIds = payload.get("contactIds")
|
||||
|
||||
# Ищем пользователей в бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
for contactId in contactIds:
|
||||
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
|
||||
user = await cursor.fetchone()
|
||||
|
||||
# Если такой пользователь есть, добавляем его в список
|
||||
if user:
|
||||
# Аватарка с биографией
|
||||
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")
|
||||
|
||||
# Генерируем профиль
|
||||
users.append(
|
||||
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 = {
|
||||
"contacts": users
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
response = self.proto.pack_packet(
|
||||
seq=seq, opcode=self.proto.CONTACT_INFO, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
|
||||
async def process_search_chats(self, payload, seq, writer, senderId):
|
||||
"""Поиск чатов по ID"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
SearchChatsPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.CHAT_INFO, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Итоговый список чатов
|
||||
chats = []
|
||||
|
||||
# ID чатов, которые нам предстоит найти
|
||||
chatIds = payload.get("chatIds")
|
||||
|
||||
# Ищем чаты в бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
for chatId in chatIds:
|
||||
if chatId != 0:
|
||||
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
|
||||
chat = await cursor.fetchone()
|
||||
|
||||
if chat:
|
||||
# Если чат - диалог, и пользователь в нем не состоит,
|
||||
# то продолжаем без добавления результата
|
||||
if chat.get("type") == self.chat_types.DIALOG and senderId not in json.loads(chat.get("participants")):
|
||||
continue
|
||||
|
||||
# Получаем последнее сообщение из чата
|
||||
message, messageTime = await self.tools.get_last_message(
|
||||
chatId, self.db_pool
|
||||
)
|
||||
|
||||
# Добавляем чат в список
|
||||
chats.append(
|
||||
self.tools.generate_chat(
|
||||
chatId, chat.get("owner"),
|
||||
chat.get("type"), json.loads(chat.get("participants")),
|
||||
message, messageTime
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Получаем последнее сообщение из чата
|
||||
message, messageTime = await self.tools.get_last_message(
|
||||
senderId, self.db_pool
|
||||
)
|
||||
|
||||
# Добавляем чат в список
|
||||
chats.append(
|
||||
self.tools.generate_chat(
|
||||
chatId, senderId,
|
||||
"DIALOG", [senderId],
|
||||
message, messageTime
|
||||
)
|
||||
)
|
||||
|
||||
# Создаем данные пакета
|
||||
payload = {
|
||||
"chats": chats
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
response = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.CHAT_INFO, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
|
||||
async def process_search_by_phone(self, payload, seq, writer, senderId):
|
||||
"""Поиск по номеру телефона"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
SearchByPhonePayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.CONTACT_INFO_BY_PHONE, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Ищем пользователя в бд
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (int(payload.get("phone")),))
|
||||
user = await cursor.fetchone()
|
||||
|
||||
# Если пользователь не найден, отправляем ошибку
|
||||
if not user:
|
||||
await self._send_error(seq, self.proto.CONTACT_INFO_BY_PHONE, self.error_types.USER_NOT_FOUND, writer)
|
||||
return
|
||||
|
||||
# ID чата
|
||||
chatId = senderId ^ user.get("id")
|
||||
|
||||
# Ищем диалог в бд
|
||||
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
|
||||
chat = await cursor.fetchone()
|
||||
|
||||
# Если диалога нет - создаем
|
||||
if not chat:
|
||||
await cursor.execute(
|
||||
"INSERT INTO chats (id, owner, type, participants) VALUES (%s, %s, %s, %s)",
|
||||
(chatId, senderId, "DIALOG", json.dumps([int(senderId), int(user.get("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 = {
|
||||
"contact": profile
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
response = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.CONTACT_INFO_BY_PHONE, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, response)
|
||||
|
||||
async def process_get_call_token(self, payload, seq, writer):
|
||||
"""Получение токена для звонка"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
GetCallTokenPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.OK_TOKEN, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# TODO: когда-то взяться за звонки
|
||||
|
||||
await self._send_error(seq, self.proto.OK_TOKEN, self.error_types.NOT_IMPLEMENTED, writer)
|
||||
|
||||
async def process_typing(self, payload, seq, writer, senderId):
|
||||
"""Обработчик события печатания"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
TypingPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.MSG_TYPING, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
chatId = payload.get("chatId")
|
||||
type = payload.get("type") or "TYPING"
|
||||
|
||||
# Ищем чат в базе данных
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
|
||||
chat = await cursor.fetchone()
|
||||
|
||||
# Если чат не найден, отправляем ошибку
|
||||
if not chat:
|
||||
await self._send_error(seq, self.proto.MSG_TYPING, self.error_types.CHAT_NOT_FOUND, writer)
|
||||
return
|
||||
|
||||
# Участники чата
|
||||
participants = json.loads(chat.get("participants"))
|
||||
|
||||
# Проверяем, является ли отправитель участником чата
|
||||
if int(senderId) not in participants:
|
||||
await self._send_error(seq, self.proto.MSG_TYPING, self.error_types.CHAT_NOT_ACCESS, writer)
|
||||
return
|
||||
|
||||
# Рассылаем событие участникам чата
|
||||
for participant in participants:
|
||||
if participant != senderId:
|
||||
# Если участник не является отправителем, отправляем
|
||||
await self.event(
|
||||
participant,
|
||||
{
|
||||
"eventType": "typing",
|
||||
"chatId": chatId,
|
||||
"type": type,
|
||||
"userId": senderId
|
||||
}
|
||||
)
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
seq=seq, opcode=self.proto.MSG_TYPING
|
||||
)
|
||||
|
||||
# Отправляем пакет
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_complain_reasons_get(self, payload, seq, writer):
|
||||
"""Обработчик получения причин жалоб"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
ComplainReasonsGetPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.COMPLAIN_REASONS_GET, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Собираем данные пакета
|
||||
payload = {
|
||||
"complains": self.static.COMPLAIN_REASONS,
|
||||
"complainSync": int(time.time())
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
seq=seq, opcode=self.proto.COMPLAIN_REASONS_GET, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем пакет
|
||||
await self._send(writer, packet)
|
||||
229
src/oneme_tcp/proto.py
Normal file
229
src/oneme_tcp/proto.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import lz4.block, msgpack, logging, json
|
||||
|
||||
class Proto:
|
||||
def __init__(self) -> None:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
### Работа с протоколом
|
||||
def unpack_packet(self, data: bytes) -> dict | None:
|
||||
# Распаковываем заголовок
|
||||
ver = int.from_bytes(data[0:1], "big")
|
||||
cmd = int.from_bytes(data[1:3], "big")
|
||||
seq = int.from_bytes(data[3:4], "big")
|
||||
opcode = int.from_bytes(data[4:6], "big")
|
||||
packed_len = int.from_bytes(data[6:10], "big")
|
||||
|
||||
# Флаг упаковки
|
||||
comp_flag = packed_len >> 24
|
||||
|
||||
# Парсим данные пакета
|
||||
payload_length = packed_len & 0xFFFFFF
|
||||
payload_bytes = data[10 : 10 + payload_length]
|
||||
payload = None
|
||||
|
||||
# Декодируем данные пакета
|
||||
if payload_bytes:
|
||||
# Разжимаем данные пакета, если требуется
|
||||
if comp_flag != 0:
|
||||
compressed_data = payload_bytes
|
||||
try:
|
||||
|
||||
payload_bytes = lz4.block.decompress(
|
||||
compressed_data,
|
||||
uncompressed_size=99999,
|
||||
)
|
||||
except lz4.block.LZ4BlockError:
|
||||
return None
|
||||
|
||||
# Распаковываем msgpack
|
||||
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}")
|
||||
|
||||
# Возвращаем
|
||||
return {
|
||||
"ver": ver,
|
||||
"cmd": cmd,
|
||||
"seq": seq,
|
||||
"opcode": opcode,
|
||||
"payload": payload,
|
||||
}
|
||||
|
||||
def pack_packet(self, ver: int = 10, cmd: int = 1, seq: int = 1, opcode: int = 6, payload: dict = None) -> bytes:
|
||||
# Запаковываем заголовок
|
||||
ver_b = ver.to_bytes(1, "big")
|
||||
cmd_b = cmd.to_bytes(2, "big")
|
||||
seq_b = seq.to_bytes(1, "big")
|
||||
opcode_b = opcode.to_bytes(2, "big")
|
||||
|
||||
# Запаковываем данные пакета
|
||||
payload_bytes: bytes | None = msgpack.packb(payload)
|
||||
if payload_bytes is None:
|
||||
payload_bytes = b""
|
||||
payload_len = len(payload_bytes) & 0xFFFFFF
|
||||
payload_len_b = payload_len.to_bytes(4, 'big')
|
||||
|
||||
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
|
||||
|
||||
### Констаты протокола
|
||||
CMD_OK = 0x100
|
||||
CMD_NOF = 0x200
|
||||
CMD_ERR = 0x300
|
||||
PROTO_VER = 10
|
||||
|
||||
### Команды
|
||||
PING = 1
|
||||
DEBUG = 2
|
||||
RECONNECT = 3
|
||||
LOG = 5
|
||||
SESSION_INIT = 6
|
||||
PROFILE = 16
|
||||
AUTH_REQUEST = 17
|
||||
AUTH = 18
|
||||
LOGIN = 19
|
||||
LOGOUT = 20
|
||||
SYNC = 21
|
||||
CONFIG = 22
|
||||
AUTH_CONFIRM = 23
|
||||
AUTH_CREATE_TRACK = 112
|
||||
AUTH_CHECK_PASSWORD = 113
|
||||
AUTH_LOGIN_CHECK_PASSWORD = 115
|
||||
AUTH_LOGIN_PROFILE_DELETE = 116
|
||||
AUTH_LOGIN_RESTORE_PASSWORD = 101
|
||||
AUTH_VALIDATE_PASSWORD = 107
|
||||
AUTH_VALIDATE_HINT = 108
|
||||
AUTH_VERIFY_EMAIL = 109
|
||||
AUTH_CHECK_EMAIL = 110
|
||||
AUTH_SET_2FA = 111
|
||||
AUTH_2FA_DETAILS = 104
|
||||
ASSETS_GET = 26
|
||||
ASSETS_UPDATE = 27
|
||||
ASSETS_GET_BY_IDS = 28
|
||||
ASSETS_LIST_MODIFY = 261
|
||||
ASSETS_REMOVE = 259
|
||||
ASSETS_MOVE = 260
|
||||
ASSETS_ADD = 29
|
||||
PRESET_AVATARS = 25
|
||||
CONTACT_INFO = 32
|
||||
CONTACT_INFO_BY_PHONE = 46
|
||||
CONTACT_ADD = 33
|
||||
CONTACT_UPDATE = 34
|
||||
CONTACT_PRESENCE = 35
|
||||
CONTACT_LIST = 36
|
||||
CONTACT_SEARCH = 37
|
||||
CONTACT_MUTUAL = 38
|
||||
CONTACT_PHOTOS = 39
|
||||
CONTACT_SORT = 40
|
||||
CONTACT_VERIFY = 42
|
||||
REMOVE_CONTACT_PHOTO = 43
|
||||
CHAT_INFO = 48
|
||||
CHAT_HISTORY = 49
|
||||
CHAT_MARK = 50
|
||||
CHAT_MEDIA = 51
|
||||
CHAT_DELETE = 52
|
||||
CHATS_LIST = 53
|
||||
CHAT_CLEAR = 54
|
||||
CHAT_UPDATE = 55
|
||||
CHAT_CHECK_LINK = 56
|
||||
CHAT_JOIN = 57
|
||||
CHAT_LEAVE = 58
|
||||
CHAT_MEMBERS = 59
|
||||
PUBLIC_SEARCH = 60
|
||||
CHAT_PERSONAL_CONFIG = 61
|
||||
CHAT_CREATE = 63
|
||||
REACTIONS_SETTINGS_GET_BY_CHAT_ID = 258
|
||||
CHAT_REACTIONS_SETTINGS_SET = 257
|
||||
MSG_SEND = 64
|
||||
MSG_TYPING = 65
|
||||
MSG_DELETE = 66
|
||||
MSG_EDIT = 67
|
||||
MSG_DELETE_RANGE = 92
|
||||
MSG_REACTION = 178
|
||||
MSG_CANCEL_REACTION = 179
|
||||
MSG_GET_REACTIONS = 180
|
||||
MSG_GET_DETAILED_REACTIONS = 181
|
||||
CHAT_SEARCH = 68
|
||||
MSG_SHARE_PREVIEW = 70
|
||||
MSG_GET = 71
|
||||
MSG_SEARCH_TOUCH = 72
|
||||
MSG_SEARCH = 73
|
||||
MSG_GET_STAT = 74
|
||||
CHAT_SUBSCRIBE = 75
|
||||
VIDEO_CHAT_START = 76
|
||||
VIDEO_CHAT_START_ACTIVE = 78
|
||||
CHAT_MEMBERS_UPDATE = 77
|
||||
VIDEO_CHAT_HISTORY = 79
|
||||
PHOTO_UPLOAD = 80
|
||||
STICKER_UPLOAD = 81
|
||||
VIDEO_UPLOAD = 82
|
||||
VIDEO_PLAY = 83
|
||||
VIDEO_CHAT_CREATE_JOIN_LINK = 84
|
||||
CHAT_PIN_SET_VISIBILITY = 86
|
||||
FILE_UPLOAD = 87
|
||||
FILE_DOWNLOAD = 88
|
||||
LINK_INFO = 89
|
||||
SESSIONS_INFO = 96
|
||||
SESSIONS_CLOSE = 97
|
||||
PHONE_BIND_REQUEST = 98
|
||||
PHONE_BIND_CONFIRM = 99
|
||||
GET_INBOUND_CALLS = 103
|
||||
EXTERNAL_CALLBACK = 105
|
||||
OK_TOKEN = 158
|
||||
CHAT_COMPLAIN = 117
|
||||
MSG_SEND_CALLBACK = 118
|
||||
SUSPEND_BOT = 119
|
||||
LOCATION_STOP = 124
|
||||
GET_LAST_MENTIONS = 127
|
||||
STICKER_CREATE = 193
|
||||
STICKER_SUGGEST = 194
|
||||
VIDEO_CHAT_MEMBERS = 195
|
||||
NOTIF_MESSAGE = 128
|
||||
NOTIF_TYPING = 129
|
||||
NOTIF_MARK = 130
|
||||
NOTIF_CONTACT = 131
|
||||
NOTIF_PRESENCE = 132
|
||||
NOTIF_CONFIG = 134
|
||||
NOTIF_CHAT = 135
|
||||
NOTIF_ATTACH = 136
|
||||
NOTIF_CALL_START = 137
|
||||
NOTIF_CONTACT_SORT = 139
|
||||
NOTIF_MSG_DELETE_RANGE = 140
|
||||
NOTIF_MSG_DELETE = 142
|
||||
NOTIF_MSG_REACTIONS_CHANGED = 155
|
||||
NOTIF_MSG_YOU_REACTED = 156
|
||||
NOTIF_CALLBACK_ANSWER = 143
|
||||
CHAT_BOT_COMMANDS = 144
|
||||
BOT_INFO = 145
|
||||
NOTIF_LOCATION = 147
|
||||
NOTIF_LOCATION_REQUEST = 148
|
||||
NOTIF_ASSETS_UPDATE = 150
|
||||
NOTIF_DRAFT = 152
|
||||
NOTIF_DRAFT_DISCARD = 153
|
||||
DRAFT_SAVE = 176
|
||||
DRAFT_DISCARD = 177
|
||||
CHAT_HIDE = 196
|
||||
CHAT_SEARCH_COMMON_PARTICIPANTS = 198
|
||||
NOTIF_MSG_DELAYED = 154
|
||||
NOTIF_PROFILE = 159
|
||||
PROFILE_DELETE = 199
|
||||
PROFILE_DELETE_TIME = 200
|
||||
WEB_APP_INIT_DATA = 160
|
||||
COMPLAIN = 161
|
||||
COMPLAIN_REASONS_GET = 162
|
||||
FOLDERS_GET = 272
|
||||
FOLDERS_GET_BY_ID = 273
|
||||
FOLDERS_UPDATE = 274
|
||||
FOLDERS_REORDER = 275
|
||||
FOLDERS_DELETE = 276
|
||||
NOTIF_FOLDERS = 277
|
||||
|
||||
AUTH_QR_APPROVE = 290
|
||||
NOTIF_BANNERS = 292
|
||||
CHAT_SUGGEST = 300
|
||||
AUDIO_PLAY = 301
|
||||
SEND_VOTE = 304
|
||||
VOTERS_LIST_BY_ANSWER = 305
|
||||
GET_POLL_UPDATES = 306
|
||||
154
src/oneme_tcp/server.py
Normal file
154
src/oneme_tcp/server.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import asyncio, logging, traceback
|
||||
from oneme_tcp.proto import Proto
|
||||
from oneme_tcp.processors import Processors
|
||||
|
||||
class OnemeMobileServer:
|
||||
def __init__(self, host="0.0.0.0", port=443, ssl_context=None, db_pool=None, clients={}, send_event=None, telegram_bot=None):
|
||||
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.proto = Proto()
|
||||
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, telegram_bot=telegram_bot)
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
"""Функция для обработки подключений"""
|
||||
# IP-адрес клиента
|
||||
address = writer.get_extra_info("peername")
|
||||
self.logger.info(f"Работаю с клиентом {address[0]}:{address[1]}")
|
||||
|
||||
deviceType = None
|
||||
deviceName = None
|
||||
|
||||
userPhone = None
|
||||
userId = None
|
||||
hashedToken = None
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Читаем новые данные из сокета
|
||||
data = await reader.read(4098)
|
||||
|
||||
# Если сокет закрыт - выходим из цикла
|
||||
if not data:
|
||||
break
|
||||
|
||||
# Распаковываем данные
|
||||
packet = self.proto.unpack_packet(data)
|
||||
|
||||
opcode = packet.get("opcode")
|
||||
seq = packet.get("seq")
|
||||
payload = packet.get("payload")
|
||||
|
||||
match opcode:
|
||||
case self.proto.SESSION_INIT:
|
||||
deviceType, deviceName = await self.processors.process_hello(payload, seq, writer)
|
||||
case self.proto.AUTH_REQUEST:
|
||||
await self.processors.process_request_code(payload, seq, writer)
|
||||
case self.proto.AUTH:
|
||||
await self.processors.process_verify_code(payload, seq, writer, deviceType, deviceName)
|
||||
case self.proto.LOGIN:
|
||||
userPhone, userId, hashedToken = await self.processors.process_login(payload, seq, writer)
|
||||
|
||||
if userPhone:
|
||||
await self._finish_auth(writer, address, userPhone, userId)
|
||||
case self.proto.LOGOUT:
|
||||
await self.processors.process_logout(seq, writer, hashedToken=hashedToken)
|
||||
break
|
||||
case self.proto.PING:
|
||||
await self.processors.process_ping(payload, seq, writer)
|
||||
case self.proto.LOG:
|
||||
await self.processors.process_telemetry(payload, seq, writer)
|
||||
case self.proto.ASSETS_UPDATE:
|
||||
await self.processors.process_get_assets(payload, seq, writer)
|
||||
case self.proto.VIDEO_CHAT_HISTORY:
|
||||
await self.processors.process_get_call_history(payload, seq, writer)
|
||||
case self.proto.MSG_SEND:
|
||||
await self.processors.process_send_message(payload, seq, writer, senderId=userId, db_pool=self.db_pool)
|
||||
case self.proto.FOLDERS_GET:
|
||||
await self.processors.process_get_folders(payload, seq, writer, senderPhone=userPhone)
|
||||
case self.proto.SESSIONS_INFO:
|
||||
await self.processors.process_get_sessions(payload, seq, writer, senderPhone=userPhone, hashedToken=hashedToken)
|
||||
case self.proto.CHAT_INFO:
|
||||
await self.processors.process_search_chats(payload, seq, writer, senderId=userId)
|
||||
case self.proto.CONTACT_INFO_BY_PHONE:
|
||||
await self.processors.process_search_by_phone(payload, seq, writer, senderId=userId)
|
||||
case self.proto.OK_TOKEN:
|
||||
await self.processors.process_get_call_token(payload, seq, writer)
|
||||
case self.proto.MSG_TYPING:
|
||||
await self.processors.process_typing(payload, seq, writer, senderId=userId)
|
||||
case self.proto.CONTACT_INFO:
|
||||
await self.processors.process_search_users(payload, seq, writer)
|
||||
case self.proto.COMPLAIN_REASONS_GET:
|
||||
await self.processors.process_complain_reasons_get(payload, seq, writer)
|
||||
case _:
|
||||
self.logger.warning(f"Неизвестный опкод {opcode}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Произошла ошибка при работе с клиентом {address[0]}:{address[1]}: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
# Удаляем клиента из словаря
|
||||
if userPhone:
|
||||
await self._end_session(userId, address[0], address[1])
|
||||
|
||||
writer.close()
|
||||
self.logger.info(f"Прекратил работать работать с клиентом {address[0]}:{address[1]}")
|
||||
|
||||
async def _finish_auth(self, writer, addr, phone, id):
|
||||
"""Завершение открытия сессии"""
|
||||
# Ищем пользователя в словаре
|
||||
user = self.clients.get(id)
|
||||
|
||||
# Добавляем новое подключение в словарь
|
||||
if user:
|
||||
user["clients"].append(
|
||||
{
|
||||
"writer": writer,
|
||||
"ip": addr[0],
|
||||
"port": addr[1],
|
||||
"protocol": "oneme_mobile"
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.clients[id] = {
|
||||
"phone": phone,
|
||||
"id": id,
|
||||
"clients": [
|
||||
{
|
||||
"writer": writer,
|
||||
"ip": addr[0],
|
||||
"port": addr[1],
|
||||
"protocol": "oneme_mobile"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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):
|
||||
"""Функция для запуска сервера"""
|
||||
self.server = await asyncio.start_server(
|
||||
self.handle_client, self.host, self.port, ssl=self.ssl_context
|
||||
)
|
||||
|
||||
self.logger.info(f"Сокет запущен на порту {self.port}")
|
||||
|
||||
async with self.server:
|
||||
await self.server.serve_forever()
|
||||
0
src/tamtam_tcp/__init__.py
Normal file
0
src/tamtam_tcp/__init__.py
Normal file
23
src/tamtam_tcp/controller.py
Normal file
23
src/tamtam_tcp/controller.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import asyncio
|
||||
from tamtam_tcp.server import TTMobileServer
|
||||
from classes.controllerbase import ControllerBase
|
||||
from common.config import ServerConfig
|
||||
|
||||
class TTMobileController(ControllerBase):
|
||||
def __init__(self):
|
||||
self.config = ServerConfig()
|
||||
|
||||
def launch(self, api):
|
||||
async def _start_all():
|
||||
await asyncio.gather(
|
||||
TTMobileServer(
|
||||
host=self.config.host,
|
||||
port=self.config.tamtam_tcp_port,
|
||||
ssl_context=api['ssl'],
|
||||
db_pool=api['db'],
|
||||
clients=api['clients'],
|
||||
send_event=api['event']
|
||||
).start()
|
||||
)
|
||||
|
||||
return _start_all()
|
||||
30
src/tamtam_tcp/models.py
Normal file
30
src/tamtam_tcp/models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pydantic
|
||||
|
||||
class UserAgentModel(pydantic.BaseModel):
|
||||
deviceType: str
|
||||
appVersion: str
|
||||
osVersion: str
|
||||
timezone: str
|
||||
screen: str
|
||||
pushDeviceType: str
|
||||
locale: str
|
||||
deviceName: str
|
||||
deviceLocale: str
|
||||
|
||||
class HelloPayloadModel(pydantic.BaseModel):
|
||||
userAgent: UserAgentModel
|
||||
deviceId: str
|
||||
|
||||
class RequestCodePayloadModel(pydantic.BaseModel):
|
||||
phone: str
|
||||
|
||||
class VerifyCodePayloadModel(pydantic.BaseModel):
|
||||
verifyCode: str
|
||||
authTokenType: str
|
||||
token: str
|
||||
|
||||
class FinalAuthPayloadModel(pydantic.BaseModel):
|
||||
deviceType: str
|
||||
tokenType: str
|
||||
deviceId: str
|
||||
token: str
|
||||
293
src/tamtam_tcp/processors.py
Normal file
293
src/tamtam_tcp/processors.py
Normal file
@@ -0,0 +1,293 @@
|
||||
import hashlib, secrets, random, time, logging, json
|
||||
from common.static import Static
|
||||
from common.tools import Tools
|
||||
from tamtam_tcp.proto import Proto
|
||||
from tamtam_tcp.models import *
|
||||
|
||||
class Processors:
|
||||
def __init__(self, db_pool=None, clients={}, send_event=None):
|
||||
self.static = Static()
|
||||
self.proto = Proto()
|
||||
self.tools = Tools()
|
||||
self.error_types = self.static.ErrorTypes()
|
||||
self.db_pool = db_pool
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def _send(self, writer, packet):
|
||||
try:
|
||||
writer.write(packet)
|
||||
await writer.drain()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def _send_error(self, seq, opcode, type, writer):
|
||||
payload = self.static.ERROR_TYPES.get(type, {
|
||||
"localizedMessage": "Неизвестная ошибка",
|
||||
"error": "unknown.error",
|
||||
"message": "Unknown error",
|
||||
"title": "Неизвестная ошибка"
|
||||
})
|
||||
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_ERR, seq=seq, opcode=opcode, payload=payload
|
||||
)
|
||||
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_hello(self, payload, seq, writer):
|
||||
"""Обработчик приветствия"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
HelloPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.HELLO, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return None, None
|
||||
|
||||
# Получаем данные из пакета
|
||||
deviceType = payload.get("userAgent").get("deviceType")
|
||||
deviceName = payload.get("userAgent").get("deviceName")
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"proxy": "",
|
||||
"logs-enabled": False,
|
||||
"proxy-domains": [],
|
||||
"location": "RU",
|
||||
"libh-enabled": False,
|
||||
"phone-auto-complete-enabled": False
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.HELLO, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
return deviceType, deviceName
|
||||
|
||||
async def process_request_code(self, payload, seq, writer):
|
||||
"""Обработчик запроса кода"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
RequestCodePayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.REQUEST_CODE, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем телефон из пакета
|
||||
phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "")
|
||||
|
||||
# Генерируем токен с кодом
|
||||
code = str(random.randint(000000, 999999))
|
||||
token = secrets.token_urlsafe(128)
|
||||
|
||||
# Хешируем
|
||||
code_hash = hashlib.sha256(code.encode()).hexdigest()
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Время истечения токена
|
||||
expires = int(time.time()) + 300
|
||||
|
||||
# Ищем пользователя, и если он существует, сохраняем токен
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
|
||||
user = await cursor.fetchone()
|
||||
|
||||
if user is None:
|
||||
await self._send_error(seq, self.proto.REQUEST_CODE, self.error_types.USER_NOT_FOUND, writer)
|
||||
return
|
||||
|
||||
# Сохраняем токен
|
||||
await cursor.execute("INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)", (phone, token_hash, code_hash, expires, "started",))
|
||||
|
||||
# Данные пакета
|
||||
payload = {
|
||||
"verifyToken": token,
|
||||
"retries": 5,
|
||||
"codeDelay": 60,
|
||||
"codeLength": 6,
|
||||
"callDelay": 0,
|
||||
"requestType": "SMS"
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.REQUEST_CODE, payload=payload
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
self.logger.debug(f"Код для {phone}: {code}")
|
||||
|
||||
async def process_verify_code(self, payload, seq, writer):
|
||||
"""Обработчик проверки кода"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
VerifyCodePayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
code = payload.get("verifyCode")
|
||||
token = payload.get("token")
|
||||
|
||||
# Хешируем токен с кодом
|
||||
hashed_code = hashlib.sha256(code.encode()).hexdigest()
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Ищем токен с кодом
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# Ищем токен
|
||||
await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", (hashed_token,))
|
||||
stored_token = await cursor.fetchone()
|
||||
|
||||
if stored_token is None:
|
||||
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.CODE_EXPIRED, writer)
|
||||
return
|
||||
|
||||
# Проверяем код
|
||||
if stored_token.get("code_hash") != hashed_code:
|
||||
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_CODE, writer)
|
||||
return
|
||||
|
||||
# Ищем аккаунт
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
|
||||
account = await cursor.fetchone()
|
||||
|
||||
# Обновляем состояние токена
|
||||
await cursor.execute("UPDATE auth_tokens set state = %s WHERE token_hash = %s", ("verified", hashed_token,))
|
||||
|
||||
# # Создаем сессию
|
||||
# await cursor.execute(
|
||||
# "INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||
# (stored_token.get("phone"), hashed_login, deviceType, deviceName, "Epstein Island", int(time.time()),)
|
||||
# )
|
||||
|
||||
# Генерируем профиль
|
||||
# Аватарка с биографией
|
||||
photoId = None if not account.get("avatar_id") else int(account.get("avatar_id"))
|
||||
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
|
||||
description = None if not account.get("description") else account.get("description")
|
||||
|
||||
# Собираем данные пакета
|
||||
payload = {
|
||||
"profile": self.tools.generate_profile(
|
||||
id=account.get("id"),
|
||||
phone=int(account.get("phone")),
|
||||
avatarUrl=avatar_url,
|
||||
photoId=photoId,
|
||||
updateTime=int(account.get("updatetime")),
|
||||
firstName=account.get("firstname"),
|
||||
lastName=account.get("lastname"),
|
||||
options=json.loads(account.get("options")),
|
||||
description=description,
|
||||
accountStatus=int(account.get("accountstatus")),
|
||||
profileOptions=json.loads(account.get("profileoptions")),
|
||||
includeProfileOptions=False,
|
||||
username=account.get("username"),
|
||||
type="TT"
|
||||
).get("contact"),
|
||||
"tokenAttrs": {
|
||||
"AUTH": {
|
||||
"token": token
|
||||
}
|
||||
},
|
||||
"tokenTypes": {
|
||||
"AUTH": token
|
||||
}
|
||||
}
|
||||
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.VERIFY_CODE, payload=payload
|
||||
)
|
||||
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_final_auth(self, payload, seq, writer, deviceType, deviceName):
|
||||
"""Обработчик финальной аутентификации"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
FinalAuthPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.FINAL_AUTH, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
token = payload.get("token")
|
||||
|
||||
if not deviceType:
|
||||
deviceType = payload.get("deviceType")
|
||||
|
||||
# Хешируем токен
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Генерируем постоянный токен
|
||||
login = secrets.token_urlsafe(128)
|
||||
hashed_login = hashlib.sha256(login.encode()).hexdigest()
|
||||
|
||||
# Ищем токен с кодом
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# Ищем токен
|
||||
await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", (hashed_token,))
|
||||
stored_token = await cursor.fetchone()
|
||||
|
||||
if stored_token is None:
|
||||
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_TOKEN, writer)
|
||||
return
|
||||
|
||||
if stored_token.get("state") == "started":
|
||||
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_TOKEN, writer)
|
||||
return
|
||||
|
||||
# Ищем аккаунт
|
||||
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
|
||||
account = await cursor.fetchone()
|
||||
|
||||
# Обновляем состояние токена
|
||||
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
|
||||
|
||||
# Создаем сессию
|
||||
await cursor.execute(
|
||||
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||
(stored_token.get("phone"), hashed_login, deviceType, deviceName, "Epstein Island", int(time.time()),)
|
||||
)
|
||||
|
||||
# Аватарка с биографией
|
||||
photoId = None if not account.get("avatar_id") else int(account.get("avatar_id"))
|
||||
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
|
||||
description = None if not account.get("description") else account.get("description")
|
||||
|
||||
# Собираем данные пакета
|
||||
payload = {
|
||||
"userToken": "0",
|
||||
"profile": self.tools.generate_profile(
|
||||
id=account.get("id"),
|
||||
phone=int(account.get("phone")),
|
||||
avatarUrl=avatar_url,
|
||||
photoId=photoId,
|
||||
updateTime=int(account.get("updatetime")),
|
||||
firstName=account.get("firstname"),
|
||||
lastName=account.get("lastname"),
|
||||
options=json.loads(account.get("options")),
|
||||
description=description,
|
||||
accountStatus=int(account.get("accountstatus")),
|
||||
profileOptions=json.loads(account.get("profileoptions")),
|
||||
includeProfileOptions=False,
|
||||
username=account.get("username"),
|
||||
type="TT"
|
||||
).get("contact"),
|
||||
"tokenType": "LOGIN",
|
||||
"token": login
|
||||
}
|
||||
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.FINAL_AUTH, payload=payload
|
||||
)
|
||||
|
||||
await self._send(writer, packet)
|
||||
91
src/tamtam_tcp/proto.py
Normal file
91
src/tamtam_tcp/proto.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import lz4.block, msgpack, logging, json
|
||||
|
||||
class Proto:
|
||||
def __init__(self) -> None:
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
### Работа с протоколом
|
||||
def unpack_packet(self, data: bytes) -> dict | None:
|
||||
# Распаковываем заголовок
|
||||
ver = int.from_bytes(data[0:1], "big")
|
||||
cmd = int.from_bytes(data[1:3], "big")
|
||||
seq = int.from_bytes(data[3:4], "big")
|
||||
opcode = int.from_bytes(data[4:6], "big")
|
||||
packed_len = int.from_bytes(data[6:10], "big")
|
||||
|
||||
# Флаг упаковки
|
||||
comp_flag = packed_len >> 24
|
||||
|
||||
# Парсим данные пакета
|
||||
payload_length = packed_len & 0xFFFFFF
|
||||
payload_bytes = data[10 : 10 + payload_length]
|
||||
payload = None
|
||||
|
||||
# Декодируем данные пакета
|
||||
if payload_bytes:
|
||||
# Разжимаем данные пакета, если требуется
|
||||
if comp_flag != 0:
|
||||
compressed_data = payload_bytes
|
||||
try:
|
||||
|
||||
payload_bytes = lz4.block.decompress(
|
||||
compressed_data,
|
||||
uncompressed_size=99999,
|
||||
)
|
||||
except lz4.block.LZ4BlockError:
|
||||
return None
|
||||
|
||||
# Распаковываем msgpack
|
||||
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}")
|
||||
|
||||
# Возвращаем
|
||||
return {
|
||||
"ver": ver,
|
||||
"cmd": cmd,
|
||||
"seq": seq,
|
||||
"opcode": opcode,
|
||||
"payload": payload,
|
||||
}
|
||||
|
||||
def pack_packet(self, ver: int = 10, cmd: int = 1, seq: int = 1, opcode: int = 6, payload: dict = None) -> bytes:
|
||||
# Запаковываем заголовок
|
||||
ver_b = ver.to_bytes(1, "big")
|
||||
cmd_b = cmd.to_bytes(2, "big")
|
||||
seq_b = seq.to_bytes(1, "big")
|
||||
opcode_b = opcode.to_bytes(2, "big")
|
||||
|
||||
# Запаковываем данные пакета
|
||||
payload_bytes: bytes | None = msgpack.packb(payload)
|
||||
if payload_bytes is None:
|
||||
payload_bytes = b""
|
||||
payload_len = len(payload_bytes) & 0xFFFFFF
|
||||
payload_len_b = payload_len.to_bytes(4, 'big')
|
||||
|
||||
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
|
||||
|
||||
### Констаты протокола
|
||||
CMD_OK = 0x100
|
||||
CMD_NOF = 0x200
|
||||
CMD_ERR = 0x300
|
||||
PROTO_VER = 10
|
||||
|
||||
HELLO = 6
|
||||
REQUEST_CODE = 17
|
||||
VERIFY_CODE = 18
|
||||
FINAL_AUTH = 23
|
||||
LOGIN = 19
|
||||
PING = 1
|
||||
TELEMETRY = 5
|
||||
GET_ASSETS = 27
|
||||
GET_CALL_HISTORY = 79
|
||||
SEND_MESSAGE = 64
|
||||
GET_FOLDERS = 272
|
||||
GET_SESSIONS = 96
|
||||
LOGOUT = 20
|
||||
SEARCH_CHATS = 48
|
||||
SEARCH_BY_PHONE = 46
|
||||
74
src/tamtam_tcp/server.py
Normal file
74
src/tamtam_tcp/server.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import asyncio, logging, traceback
|
||||
from tamtam_tcp.proto import Proto
|
||||
from tamtam_tcp.processors import Processors
|
||||
|
||||
class TTMobileServer:
|
||||
def __init__(self, host="0.0.0.0", port=443, ssl_context=None, db_pool=None, clients={}, send_event=None):
|
||||
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.proto = Proto()
|
||||
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event)
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
"""Функция для обработки подключений"""
|
||||
# IP-адрес клиента
|
||||
address = writer.get_extra_info("peername")
|
||||
self.logger.info(f"Работаю с клиентом {address[0]}:{address[1]}")
|
||||
|
||||
deviceType = None
|
||||
deviceName = None
|
||||
|
||||
userPhone = None
|
||||
userId = None
|
||||
hashedToken = None
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Читаем новые данные из сокета
|
||||
data = await reader.read(4098)
|
||||
|
||||
# Если сокет закрыт - выходим из цикла
|
||||
if not data:
|
||||
break
|
||||
|
||||
# Распаковываем данные
|
||||
packet = self.proto.unpack_packet(data)
|
||||
|
||||
opcode = packet.get("opcode")
|
||||
seq = packet.get("seq")
|
||||
payload = packet.get("payload")
|
||||
|
||||
match opcode:
|
||||
case self.proto.HELLO:
|
||||
deviceType, deviceName = await self.processors.process_hello(payload, seq, writer)
|
||||
case self.proto.REQUEST_CODE:
|
||||
await self.processors.process_request_code(payload, seq, writer)
|
||||
case self.proto.VERIFY_CODE:
|
||||
await self.processors.process_verify_code(payload, seq, writer)
|
||||
case self.proto.FINAL_AUTH:
|
||||
await self.processors.process_final_auth(payload, seq, writer, deviceType, deviceName)
|
||||
case _:
|
||||
self.logger.warning(f"Неизвестный опкод {opcode}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Произошла ошибка при работе с клиентом {address[0]}:{address[1]}: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
writer.close()
|
||||
self.logger.info(f"Прекратил работать работать с клиентом {address[0]}:{address[1]}")
|
||||
|
||||
async def start(self):
|
||||
"""Функция для запуска сервера"""
|
||||
self.server = await asyncio.start_server(
|
||||
self.handle_client, self.host, self.port, ssl=self.ssl_context
|
||||
)
|
||||
|
||||
self.logger.info(f"Сокет запущен на порту {self.port}")
|
||||
|
||||
async with self.server:
|
||||
await self.server.serve_forever()
|
||||
0
src/tamtam_ws/__init__.py
Normal file
0
src/tamtam_ws/__init__.py
Normal file
22
src/tamtam_ws/controller.py
Normal file
22
src/tamtam_ws/controller.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import asyncio
|
||||
from classes.controllerbase import ControllerBase
|
||||
from common.config import ServerConfig
|
||||
from tamtam_ws.server import TTWSServer
|
||||
|
||||
class TTWSController(ControllerBase):
|
||||
def __init__(self):
|
||||
self.config = ServerConfig()
|
||||
|
||||
def launch(self, api):
|
||||
async def _start_all():
|
||||
await asyncio.gather(
|
||||
TTWSServer(
|
||||
host=self.config.host,
|
||||
port=self.config.tamtam_ws_port,
|
||||
db_pool=api['db'],
|
||||
clients=api['clients'],
|
||||
send_event=api['event']
|
||||
).start()
|
||||
)
|
||||
|
||||
return _start_all()
|
||||
27
src/tamtam_ws/models.py
Normal file
27
src/tamtam_ws/models.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import pydantic
|
||||
|
||||
class MessageModel(pydantic.BaseModel):
|
||||
ver: int
|
||||
cmd: int
|
||||
seq: int
|
||||
opcode: int
|
||||
payload: dict = None
|
||||
|
||||
class UserAgentModel(pydantic.BaseModel):
|
||||
deviceType: str
|
||||
appVersion: str
|
||||
osVersion: str
|
||||
locale: str
|
||||
deviceLocale: str
|
||||
deviceName: str
|
||||
screen: str
|
||||
headerUserAgent: str
|
||||
timezone: str
|
||||
|
||||
class HelloPayloadModel(pydantic.BaseModel):
|
||||
userAgent: UserAgentModel
|
||||
deviceId: str
|
||||
|
||||
class RequestCodePayloadModel(pydantic.BaseModel):
|
||||
phone: str
|
||||
requestType: str
|
||||
76
src/tamtam_ws/processors.py
Normal file
76
src/tamtam_ws/processors.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import hashlib, secrets, random, time, logging, json
|
||||
from common.static import Static
|
||||
from common.tools import Tools
|
||||
from tamtam_ws.proto import Proto
|
||||
from tamtam_ws.models import *
|
||||
|
||||
class Processors:
|
||||
def __init__(self, db_pool=None, clients={}, send_event=None):
|
||||
self.static = Static()
|
||||
self.tools = Tools()
|
||||
self.proto = Proto()
|
||||
self.error_types = self.static.ErrorTypes()
|
||||
self.db_pool = db_pool
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def _send(self, writer, packet):
|
||||
"""Отправка пакета"""
|
||||
await writer.send(packet)
|
||||
|
||||
async def _send_error(self, seq, opcode, type, writer):
|
||||
payload = self.static.ERROR_TYPES.get(type, {
|
||||
"localizedMessage": "Неизвестная ошибка",
|
||||
"error": "unknown.error",
|
||||
"message": "Unknown error",
|
||||
"title": "Неизвестная ошибка"
|
||||
})
|
||||
|
||||
packet = self.proto.pack_packet(
|
||||
seq=seq, opcode=opcode, payload=payload
|
||||
)
|
||||
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_hello(self, payload, seq, writer):
|
||||
"""Обработчик приветствия"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
HelloPayloadModel.model_validate(payload)
|
||||
except Exception as e:
|
||||
await self._send_error(seq, self.proto.SESSION_INIT, self.error_types.INVALID_PAYLOAD, writer)
|
||||
return None, None
|
||||
|
||||
# Получаем данные из пакета
|
||||
deviceType = payload.get("userAgent").get("deviceType")
|
||||
deviceName = payload.get("userAgent").get("deviceName")
|
||||
|
||||
# Собираем данные ответа
|
||||
payload = {
|
||||
"proxy": "",
|
||||
"logs-enabled": False,
|
||||
"proxy-domains": [],
|
||||
"location": "RU"
|
||||
}
|
||||
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(seq=seq, opcode=self.proto.SESSION_INIT, payload=payload)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
return deviceType, deviceName
|
||||
|
||||
async def process_ping(self, payload, seq, writer):
|
||||
"""Обработчик пинга"""
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(seq=seq, opcode=self.proto.PING)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def process_telemetry(self, payload, seq, writer):
|
||||
"""Обработчик телеметрии"""
|
||||
# Создаем пакет
|
||||
packet = self.proto.pack_packet(seq=seq, opcode=self.proto.LOG)
|
||||
|
||||
# Отправляем
|
||||
await self._send(writer, packet)
|
||||
153
src/tamtam_ws/proto.py
Normal file
153
src/tamtam_ws/proto.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import json
|
||||
|
||||
class Proto:
|
||||
def pack_packet(self, ver=10, cmd=1, seq=0, opcode=1, payload=None):
|
||||
# а разве не надо в жсон запаковывать ещё
|
||||
# о всё
|
||||
return json.dumps({
|
||||
"ver": ver,
|
||||
"cmd": cmd,
|
||||
"seq": seq,
|
||||
"opcode": opcode,
|
||||
"payload": payload
|
||||
})
|
||||
|
||||
def unpack_packet(self, packet):
|
||||
# нужно try catch сделать
|
||||
# чтобы не сыпалось всё при неверных пакетах
|
||||
try:
|
||||
parsed_packet = json.loads(packet)
|
||||
except:
|
||||
return {}
|
||||
|
||||
return parsed_packet
|
||||
# мне кажется долго вручную всё писать
|
||||
# а как еще
|
||||
# ну вставить сюда целиком и потом через multiline cursor удалить лишнее
|
||||
# ну ты удалишь тогда. я на тачпаде
|
||||
# ладно щас другим способом удалю
|
||||
# всё нахуй
|
||||
# TAMTAM SOURCE LEAK 2026
|
||||
# так ну че делать будем
|
||||
# так ну
|
||||
|
||||
# 19 опкод сделан?
|
||||
# нет сэр пошли библиотеку тамы смотреть
|
||||
# мб найдем че. она без обфускации
|
||||
# а ты ее видишь?
|
||||
# пошли
|
||||
PING = 1
|
||||
LOG = 5
|
||||
SESSION_INIT = 6
|
||||
PROFILE = 16
|
||||
AUTH_REQUEST = 17
|
||||
AUTH_CHECK_SCENARIO = 263
|
||||
AUTH = 18
|
||||
LOGIN = 19
|
||||
LOGOUT = 20
|
||||
SYNC = 21
|
||||
CONFIG = 22
|
||||
AUTH_CONFIRM = 23
|
||||
ASSETS_GET = 26
|
||||
ASSETS_UPDATE = 27
|
||||
ASSETS_GET_BY_IDS = 28
|
||||
ASSETS_ADD = 29
|
||||
ASSETS_REMOVE = 259
|
||||
ASSETS_MOVE = 260
|
||||
ASSETS_LIST_MODIFY = 261
|
||||
CONTACT_INFO = 32
|
||||
CONTACT_UPDATE = 34
|
||||
CONTACT_PRESENCE = 35
|
||||
CONTACT_LIST = 36
|
||||
CONTACT_PHOTOS = 39
|
||||
CONTACT_CREATE = 41
|
||||
REMOVE_CONTACT_PHOTO = 43
|
||||
OWN_CONTACT_SEARCH = 44
|
||||
CHAT_INFO = 48
|
||||
CHAT_HISTORY = 49
|
||||
CHAT_MARK = 50
|
||||
CHAT_MEDIA = 51
|
||||
CHAT_DELETE = 52
|
||||
CHAT_LIST = 53
|
||||
CHAT_CLEAR = 54
|
||||
CHAT_UPDATE = 55
|
||||
CHAT_CHECK_LINK = 56
|
||||
CHAT_JOIN = 57
|
||||
CHAT_LEAVE = 58
|
||||
CHAT_MEMBERS = 59
|
||||
CHAT_CLOSE = 61
|
||||
CHAT_BOT_COMMANDS = 144
|
||||
CHAT_SUBSCRIBE = 75
|
||||
PUBLIC_SEARCH = 60
|
||||
CHAT_CREATE = 63
|
||||
MSG_SEND = 64
|
||||
MSG_TYPING = 65
|
||||
MSG_DELETE = 66
|
||||
MSG_EDIT = 67
|
||||
CHAT_SEARCH = 68
|
||||
MSG_SHARE_PREVIEW = 70
|
||||
MSG_SEARCH_TOUCH = 72
|
||||
MSG_SEARCH = 73
|
||||
MSG_GET_STAT = 74
|
||||
MSG_GET = 71
|
||||
VIDEO_CHAT_START = 76
|
||||
VIDEO_CHAT_JOIN = 102
|
||||
VIDEO_CHAT_COMMAND = 78
|
||||
VIDEO_CHAT_MEMBERS = 195
|
||||
CHAT_MEMBERS_UPDATE = 77
|
||||
PHOTO_UPLOAD = 80
|
||||
STICKER_UPLOAD = 81
|
||||
VIDEO_UPLOAD = 82
|
||||
VIDEO_PLAY = 83
|
||||
MUSIC_PLAY = 84
|
||||
MUSIC_PLAY30 = 85
|
||||
FILE_UPLOAD = 87
|
||||
FILE_DOWNLOAD = 88
|
||||
CHAT_PIN_SET_VISIBILITY = 86
|
||||
LINK_INFO = 89
|
||||
MESSAGE_LINK = 90
|
||||
MSG_CONSTRUCT = 94
|
||||
SESSIONS_INFO = 96
|
||||
SESSIONS_CLOSE = 97
|
||||
PHONE_BIND_REQUEST = 98
|
||||
PHONE_BIND_CONFIRM = 99
|
||||
UNBIND_OK_PROFILE = 100
|
||||
CHAT_COMPLAIN = 117
|
||||
MSG_SEND_CALLBACK = 118
|
||||
SUSPEND_BOT = 119
|
||||
MSG_REACT = 178
|
||||
MSG_CANCEL_REACTION = 179
|
||||
MSG_GET_REACTIONS = 180
|
||||
MSG_GET_DETAILED_REACTIONS = 181
|
||||
LOCATION_STOP = 124
|
||||
LOCATION_SEND = 125
|
||||
LOCATION_REQUEST = 126
|
||||
NOTIF_MESSAGE = 128
|
||||
NOTIF_TYPING = 129
|
||||
NOTIF_MARK = 130
|
||||
NOTIF_CONTACT = 131
|
||||
NOTIF_PRESENCE = 132
|
||||
NOTIF_CONFIG = 134
|
||||
NOTIF_CHAT = 135
|
||||
NOTIF_ATTACH = 136
|
||||
NOTIF_VIDEO_CHAT_START = 137
|
||||
NOTIF_VIDEO_CHAT_COMMAND = 138
|
||||
NOTIF_CALLBACK_ANSWER = 143
|
||||
NOTIF_MSG_CONSTRUCT = 146
|
||||
NOTIF_LOCATION = 147
|
||||
NOTIF_LOCATION_REQUEST = 148
|
||||
NOTIF_ASSETS_UPDATE = 150
|
||||
NOTIF_MSG_REACTIONS_CHANGED = 155
|
||||
NOTIF_MSG_YOU_REACTED = 156
|
||||
NOTIF_DRAFT = 152
|
||||
NOTIF_DRAFT_DISCARD = 153
|
||||
NOTIF_MSG_DELAYED = 154
|
||||
AUTH_CALL_INFO = 256
|
||||
CONTACT_INFO_EXTERNAL = 45
|
||||
DRAFT_SAVE = 176
|
||||
DRAFT_DISCARD = 177
|
||||
STICKER_CREATE = 193
|
||||
STICKER_SUGGEST = 194
|
||||
CHAT_SEARCH_COUNT_MSG = 197
|
||||
CHAT_SEARCH_COMMON_PARTICIPANTS = 198
|
||||
GET_USER_SCORE = 201
|
||||
61
src/tamtam_ws/server.py
Normal file
61
src/tamtam_ws/server.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import asyncio, logging, json
|
||||
from websockets.asyncio.server import serve
|
||||
from tamtam_ws.models import *
|
||||
from pydantic import ValidationError
|
||||
from tamtam_ws.proto import Proto
|
||||
from tamtam_ws.processors import Processors
|
||||
|
||||
class TTWSServer:
|
||||
def __init__(self, host, port, db_pool=None, clients={}, send_event=None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.proto = Proto()
|
||||
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def handle_client(self, websocket):
|
||||
deviceType = None
|
||||
deviceName = None
|
||||
|
||||
async for message in websocket:
|
||||
# Распаковываем пакет
|
||||
packet = self.proto.unpack_packet(message)
|
||||
|
||||
# Валидируем структуру пакета
|
||||
try:
|
||||
MessageModel.model_validate(packet)
|
||||
except ValidationError as e:
|
||||
self.logger.error(e)
|
||||
|
||||
# Извлекаем данные из пакета
|
||||
seq = packet['seq']
|
||||
opcode = packet['opcode']
|
||||
payload = packet['payload']
|
||||
|
||||
match opcode:
|
||||
case self.proto.SESSION_INIT:
|
||||
# ПРИВЕТ АНДРЕЙ МАЛАХОВ
|
||||
# не не удаляй этот коммент. пусть останется на релизе аххахаха
|
||||
deviceType, deviceType = await self.processors.process_hello(payload, seq, websocket)
|
||||
case self.proto.PING:
|
||||
await self.processors.process_ping(payload, seq, websocket)
|
||||
case self.proto.LOG:
|
||||
# телеметрия аааа слежка цру фсб фбр
|
||||
# УДАЛЯЕМ MYTRACKER ИЗ TAMTAM ТАМ ВИРУС
|
||||
# майтрекер отправляет все ваши сообщения на сервер барака обамы. немедленно удаляем!!!
|
||||
await self.processors.process_telemetry(payload, seq, websocket)
|
||||
# case self.proto.AUTH_REQUEST:
|
||||
# await self.processors.process_auth_request(payload, seq, websocket)
|
||||
# case self.proto.VERIFY_CODE:
|
||||
# await self.processors.process_verify_code(payload, seq, websocket)
|
||||
# case self.proto.FINAL_AUTH:
|
||||
# await self.processors.process_final_auth(payload, seq, websocket, deviceType, deviceName)
|
||||
|
||||
# лан я пойду. пока
|
||||
# а ок
|
||||
|
||||
async def start(self):
|
||||
self.logger.info(f"Вебсокет запущен на порту {self.port}")
|
||||
|
||||
async with serve(self.handle_client, self.host, self.port):
|
||||
await asyncio.Future()
|
||||
0
src/telegrambot/__init__.py
Normal file
0
src/telegrambot/__init__.py
Normal file
163
src/telegrambot/bot.py
Normal file
163
src/telegrambot/bot.py
Normal file
@@ -0,0 +1,163 @@
|
||||
import logging
|
||||
import random
|
||||
import json
|
||||
import time
|
||||
from telebot.async_telebot import AsyncTeleBot
|
||||
|
||||
class TelegramBot:
|
||||
def __init__(self, token, enabled, db_pool, whitelist_ids=None):
|
||||
self.bot = AsyncTeleBot(token)
|
||||
self.enabled = enabled
|
||||
self.db_pool = db_pool
|
||||
self.whitelist_ids = whitelist_ids if whitelist_ids is not None else []
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
@self.bot.message_handler(commands=['start'])
|
||||
async def handle_start(message):
|
||||
tg_id = str(message.from_user.id)
|
||||
|
||||
# Ищем привязанный аккаунт пользователя
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE telegram_id = %s", (tg_id,))
|
||||
account = await cursor.fetchone()
|
||||
|
||||
if account:
|
||||
# Извлекаем id аккаунта с телефоном
|
||||
phone = account.get('phone')
|
||||
|
||||
await self.bot.send_message(
|
||||
message.chat.id,
|
||||
f"👋 С возвращением в OpenMAX!\nВаш номер, если забыли: {phone}"
|
||||
)
|
||||
await self.send_auth_code(message.chat.id, phone)
|
||||
else:
|
||||
await self.bot.send_message(
|
||||
message.chat.id,
|
||||
"👋 Добро пожаловать на этот инстанс OpenMAX!\nУ вас ещё нет аккаунта. Используйте /register для создания."
|
||||
)
|
||||
|
||||
@self.bot.message_handler(commands=['register'])
|
||||
async def handle_register(message):
|
||||
tg_id = str(message.from_user.id)
|
||||
|
||||
# Проверка ID на наличие в белом списке
|
||||
if self.whitelist_ids and tg_id not in self.whitelist_ids:
|
||||
await self.bot.send_message(message.chat.id, "❌ Ваш ID не находится в белом списке.")
|
||||
return
|
||||
|
||||
async with self.db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
# Проверка на существование
|
||||
await cursor.execute("SELECT id FROM users WHERE telegram_id = %s", (tg_id,))
|
||||
if await cursor.fetchone():
|
||||
await self.bot.send_message(message.chat.id, "⚠️ У вас уже есть аккаунт! Существующий аккаунт можно удалить через клиент или обратившись к администратору инстанса.")
|
||||
return
|
||||
|
||||
# Подготовка данных согласно схеме
|
||||
new_phone = f"7900{random.randint(1000000, 9999999)}"
|
||||
updatetime = str(int(time.time() * 1000))
|
||||
lastseen = str(int(time.time()))
|
||||
|
||||
folders = {
|
||||
"folders": [],
|
||||
"foldersOrder": [],
|
||||
"allFilterExcludeFolders": []
|
||||
}
|
||||
|
||||
user_settings = {
|
||||
"CHATS_PUSH_NOTIFICATION": "ON",
|
||||
"PUSH_DETAILS": True,
|
||||
"PUSH_SOUND": "DEFAULT",
|
||||
"INACTIVE_TTL": "6M",
|
||||
"CHATS_QUICK_REPLY": False,
|
||||
"SHOW_READ_MARK": True,
|
||||
"AUDIO_TRANSCRIPTION_ENABLED": True,
|
||||
"CHATS_LED": 65535,
|
||||
"SEARCH_BY_PHONE": "ALL",
|
||||
"INCOMING_CALL": "ALL",
|
||||
"DOUBLE_TAP_REACTION_DISABLED": False,
|
||||
"SAFE_MODE_NO_PIN": False,
|
||||
"CHATS_PUSH_SOUND": "DEFAULT",
|
||||
"DOUBLE_TAP_REACTION_VALUE": None,
|
||||
"FAMILY_PROTECTION": "OFF",
|
||||
"LED": 65535,
|
||||
"HIDDEN": False,
|
||||
"VIBR": True,
|
||||
"CHATS_INVITE": "ALL",
|
||||
"PUSH_NEW_CONTACTS": False,
|
||||
"UNSAFE_FILES": True,
|
||||
"DONT_DISTURB_UNTIL": 0,
|
||||
"CHATS_VIBR": True,
|
||||
"CONTENT_LEVEL_ACCESS": False,
|
||||
"STICKERS_SUGGEST": "ON",
|
||||
"SAFE_MODE": False,
|
||||
"M_CALL_PUSH_NOTIFICATION": "ON",
|
||||
"QUICK_REPLY": False
|
||||
}
|
||||
|
||||
try:
|
||||
# Создаем юзера
|
||||
await cursor.execute(
|
||||
"""
|
||||
INSERT INTO users
|
||||
(phone, telegram_id, firstname, lastname, username,
|
||||
profileoptions, options, accountstatus, updatetime, lastseen)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
new_phone, # phone
|
||||
tg_id, # telegram_id
|
||||
message.from_user.first_name[:59], # firstname
|
||||
(message.from_user.last_name or "")[:59], # lastname
|
||||
(message.from_user.username or "")[:60], # username
|
||||
json.dumps([]), # profileoptions
|
||||
json.dumps(["TT", "ONEME"]), # options
|
||||
0, # accountstatus
|
||||
updatetime,
|
||||
lastseen,
|
||||
)
|
||||
)
|
||||
|
||||
# Добавляем данные о аккаунте
|
||||
await cursor.execute(
|
||||
"""
|
||||
INSERT INTO user_data
|
||||
(phone, chats, contacts, folders, user_config, chat_config)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
new_phone, # phone
|
||||
json.dumps([]), # chats
|
||||
json.dumps([]), # contacts
|
||||
json.dumps(folders), # folders
|
||||
json.dumps(user_settings), # user_config
|
||||
json.dumps({}), # chat_config
|
||||
)
|
||||
)
|
||||
|
||||
await self.bot.send_message(
|
||||
message.chat.id,
|
||||
f"✅ Регистрация завершена!\nВаш новый номер: {new_phone}\nВсе коды для авторизации будут приходить сюда."
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при регистрации: {e}")
|
||||
await self.bot.send_message(message.chat.id, "❌ Ошибка при регистрации аккаунта. Обратитесь к администратору инстанса за помощью.")
|
||||
|
||||
async def start(self):
|
||||
if self.enabled == True:
|
||||
try:
|
||||
await self.bot.polling()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка запуска Telegram бота: {e}")
|
||||
else:
|
||||
self.logger.warning("Запуск Telegram бота отключен")
|
||||
|
||||
async def send_auth_code(self, chat_id, phone, code):
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
chat_id,
|
||||
f"Новая попытка входа в OpenMAX с вашим номером {phone}\nКод: {code}\n❗️ Никому не сообщайте его, иначе можете потерять свой аккаунт!"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки кода в Telegram: {e}")
|
||||
28
src/telegrambot/controller.py
Normal file
28
src/telegrambot/controller.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import asyncio
|
||||
from telegrambot.bot import TelegramBot
|
||||
from classes.controllerbase import ControllerBase
|
||||
from common.config import ServerConfig
|
||||
|
||||
class TelegramBotController(ControllerBase):
|
||||
def __init__(self):
|
||||
self.config = ServerConfig()
|
||||
self.bot = None
|
||||
|
||||
def launch(self, api):
|
||||
async def _start_all():
|
||||
await asyncio.gather(
|
||||
self.bot.start()
|
||||
)
|
||||
|
||||
# Инициализируем бота
|
||||
self.bot = TelegramBot(
|
||||
token=self.config.telegram_bot_token,
|
||||
enabled=self.config.telegram_bot_enabled,
|
||||
db_pool=api['db'],
|
||||
whitelist_ids=self.config.telegram_whitelist_ids
|
||||
)
|
||||
|
||||
return _start_all()
|
||||
|
||||
async def send_code(self, chat_id, phone, code):
|
||||
await self.bot.send_auth_code(chat_id, phone, code)
|
||||
61
tables.sql
Normal file
61
tables.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
CREATE TABLE `users` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`phone` VARCHAR(20) UNIQUE,
|
||||
`telegram_id` VARCHAR(64) UNIQUE,
|
||||
`firstname` VARCHAR(59) NOT NULL,
|
||||
`lastname` VARCHAR(59),
|
||||
`description` VARCHAR(400),
|
||||
`avatar_id` VARCHAR(16),
|
||||
`updatetime` VARCHAR(24),
|
||||
`lastseen` VARCHAR(24),
|
||||
`profileoptions` JSON NOT NULL,
|
||||
`options` JSON NOT NULL,
|
||||
`accountstatus` VARCHAR(16) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`username` VARCHAR(60) UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE `tokens` (
|
||||
`phone` VARCHAR(20) NOT NULL,
|
||||
`token_hash` VARCHAR(64) NOT NULL,
|
||||
`device_type` VARCHAR(256) NOT NULL,
|
||||
`device_name` VARCHAR(256) NOT NULL,
|
||||
`location` VARCHAR(256) NOT NULL,
|
||||
`time` VARCHAR(16) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `auth_tokens` (
|
||||
`phone` VARCHAR(20) NOT NULL,
|
||||
`token_hash` VARCHAR(64) NOT NULL,
|
||||
`code_hash` VARCHAR(64) NOT NULL,
|
||||
`expires` VARCHAR(16) NOT NULL,
|
||||
`state` VARCHAR(16)
|
||||
);
|
||||
|
||||
CREATE TABLE `user_data` (
|
||||
`phone` VARCHAR(20) NOT NULL UNIQUE,
|
||||
`chats` JSON NOT NULL,
|
||||
`contacts` JSON NOT NULL,
|
||||
`folders` JSON NOT NULL,
|
||||
`user_config` JSON NOT NULL,
|
||||
`chat_config` JSON NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `chats` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`owner` INT NOT NULL,
|
||||
`type` VARCHAR(16) NOT NULL,
|
||||
`participants` JSON NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `messages` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`chat_id` INT NOT NULL,
|
||||
`sender` INT NOT NULL,
|
||||
`time` VARCHAR(32) NOT NULL,
|
||||
`text` VARCHAR(4000) NOT NULL,
|
||||
`attaches` JSON NOT NULL,
|
||||
`cid` VARCHAR(32) NOT NULL,
|
||||
`elements` JSON NOT NULL,
|
||||
`type` VARCHAR(16) NOT NULL
|
||||
);
|
||||
Reference in New Issue
Block a user