diff --git a/docs/proto/tamtam_ws.md b/docs/proto/tamtam_ws.md new file mode 100644 index 0000000..bd4e712 --- /dev/null +++ b/docs/proto/tamtam_ws.md @@ -0,0 +1,29 @@ +# Описание протокола TamTam по Websocket + +## Основная информация +В веб версии мессенджера ТамТам используется протокол, работающий поверх Websocket. + +Пакеты в этом протоколе являются текстовыми JSON данными. + +Структура пакета: +``` +{ + ver: int, + cmd: int, + seq: int, + opcode: int, + payload: {} +} +``` + +* ver - версия протокола +* cmd - определяет, от кого отправлен пакет. клиент - 0, сервер - 1 +* seq - порядковый номер пакета (сервер дублирует его из запроса клиента) +* opcode - команда +* payload - полезная нагрузка команды + +## Команды протокола + +### PING (1) +Клиент периодически отправляет пакет с командой PING и пустой нагрузкой серверу. +Сервер отвечает ему тем же. \ No newline at end of file diff --git a/faq/install.md b/faq/install.md new file mode 100644 index 0000000..32e30ac --- /dev/null +++ b/faq/install.md @@ -0,0 +1,18 @@ +# Установка + +1. Склонируйте репозиторий +2. Установите зависимости + +```bash +pip install -r requirements.txt +``` +3. Настройте сервер (пример в `.env.example`) +4. Импортируйте схему таблиц в свою базу данных из `tables.sql` +5. Запустите сервер + +```bash +python3 main.py +``` + +6. Создайте пользователя +7. Зайдите со своего любимого клиента diff --git a/faq/patch_apk.md b/faq/patch_apk.md new file mode 100644 index 0000000..1b8a367 --- /dev/null +++ b/faq/patch_apk.md @@ -0,0 +1,24 @@ +# Смена сервера в мобильном клиенте +> [!Caution] +> Инструкция может быть недостаточной, если вы используете самоподписанный сертификат или сертификат, которому система не доверяет. Вам, возможно, потребуется выполнить дополнительные действия в модификации клиента для успешного входа. + +# MT Manager +1. Открываем apk файл клиента, который желаете пропатчить +2. Нажимаем на любой dex файл +3. Выбираем в качестве редактора "Редактор dex+" +4. Выбираем все dex файлы при появлении окна выбора "MultiDex" +5. В поиске выбираем тип Smali, а в поле поиска пишем "api.oneme.ru" +6. Проходимся по каждому результату и заменяем сервер на свой + +# ApkTool M +1. Декомпилируем приложение, обязательно поставьте галочку у пункта "Декомпилировать classes*.dex" +2. В папке проекта нажимаем на "лупу" +3. Ставим поиск по содержимому с заменой +4. В поле поиска пишем "api.oneme.ru", а в поле замены ваш адрес сервера +5. После замены нажимаем на "Собрать проект" + +# ApkTool +1. Помещаем apk в рабочую директорию +2. Открываем консоль в той же директории и производим декомпиляцию: `apktool d <имя apk> -o max` +3. Заходим в папку проекта и заменяем во всех классах "api.oneme.ru" на свой адрес сервера +4. Производим повторную сборку с помощью команды: `apktool b max -o max_modified.apk` diff --git a/faq/readme.md b/faq/readme.md new file mode 100644 index 0000000..c2379e3 --- /dev/null +++ b/faq/readme.md @@ -0,0 +1,7 @@ +# Навигация по faq + +## Работа с сервером +[Установка сервера](install.md) + +## Патчинг клиентов +[Патч apk](patch_apk.md) diff --git a/readme.md b/readme.md index d0e3d30..a916879 100644 --- a/readme.md +++ b/readme.md @@ -21,23 +21,5 @@ https://t.me/openmax_alerts Клиент может быть практически любым, главное условие - чтобы он был совместим с официальным сервером (`api.oneme.ru` / `api.tamtam.chat`). -На данный момент с сервером может работать последняя версия MAX (26.7.1), однако все тесты проходят на версии 26.5.0. - -# Установка - -1. Склонируйте репозиторий -2. Установите зависимости - -```bash -pip install -r requirements.txt -``` -3. Настройте сервер (пример в `.env.example`) -4. Импортируйте схему таблиц в свою базу данных из `tables.sql` -5. Запустите сервер - -```bash -python3 main.py -``` - -6. Создайте пользователя -7. Зайдите со своего любимого клиента +# Дополнительная информация +[Faq](faq/readme.md) diff --git a/src/oneme_tcp/config.py b/src/oneme_tcp/config.py index 39195db..c9ff193 100644 --- a/src/oneme_tcp/config.py +++ b/src/oneme_tcp/config.py @@ -179,7 +179,7 @@ class OnemeConfig: "moscow-theme-enabled": True, "msg-get-reactions-page-size": 40, "music-files-enabled": False, - "mytracker-enabled": True, + "mytracker-enabled": False, "net-client-dns-enabled": True, "net-session-suppress-bad-disconnected-state": True, "net-stat-config": [ diff --git a/src/tamtam_tcp/processors.py b/src/tamtam_tcp/processors.py index 849ac1d..1d8fe6f 100644 --- a/src/tamtam_tcp/processors.py +++ b/src/tamtam_tcp/processors.py @@ -1,11 +1,19 @@ -import hashlib, secrets, random, time, logging, json +import hashlib +import secrets +import time +import logging +import json +import re 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): + def __init__(self, db_pool=None, clients=None, send_event=None): + if clients is None: + clients = {} # Более правильная логика self.static = Static() self.proto = Proto() self.tools = Tools() @@ -27,11 +35,11 @@ class Processors: "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): @@ -42,10 +50,10 @@ class Processors: 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") + device_type = payload.get("userAgent").get("deviceType") + device_name = payload.get("userAgent").get("deviceName") # Данные пакета payload = { @@ -64,8 +72,8 @@ class Processors: # Отправляем await self._send(writer, packet) - return deviceType, deviceName - + return device_type, device_name + async def process_request_code(self, payload, seq, writer): """Обработчик запроса кода""" # Валидируем данные пакета @@ -76,17 +84,17 @@ class Processors: return # Извлекаем телефон из пакета - phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "") + phone = re.sub(r'\D', '', payload.get("phone", "")) # Не хардкодим, через регулярки # Генерируем токен с кодом - code = str(random.randint(000000, 999999)) + code = f"{secrets.randbelow(1_000_000):06d}" # Старая версия ненадежна, могла отбросить ведущие нули или вообще интерпритировать как систему счисления с основанием 8 token = secrets.token_urlsafe(128) # Хешируем code_hash = hashlib.sha256(code.encode()).hexdigest() token_hash = hashlib.sha256(token.encode()).hexdigest() - # Время истечения токена + # Срок жизни токена (5 минут) expires = int(time.time()) + 300 # Ищем пользователя, и если он существует, сохраняем токен @@ -141,10 +149,11 @@ class Processors: 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,)) + 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: + if not stored_token: await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.CODE_EXPIRED, writer) return @@ -152,7 +161,7 @@ class Processors: 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() @@ -162,9 +171,9 @@ class Processors: # Генерируем профиль # Аватарка с биографией - 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") + photo_id = int(account["avatar_id"]) if account.get("avatar_id") else None + avatar_url = f"{self.config.avatar_base_url}{photo_id}" if photo_id else None + description = account.get("description") # Собираем данные пакета payload = { @@ -172,7 +181,7 @@ class Processors: id=account.get("id"), phone=int(account.get("phone")), avatarUrl=avatar_url, - photoId=photoId, + photoId=photo_id, updateTime=int(account.get("updatetime")), firstName=account.get("firstname"), lastName=account.get("lastname"), @@ -222,7 +231,8 @@ class Processors: 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,)) + 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: @@ -244,12 +254,13 @@ class Processors: # Создаем сессию 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()),) + (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 + photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id")) + avatar_url = None if not photo_id else self.config.avatar_base_url + photo_id description = None if not account.get("description") else account.get("description") # Собираем данные пакета @@ -259,7 +270,7 @@ class Processors: id=account.get("id"), phone=int(account.get("phone")), avatarUrl=avatar_url, - photoId=photoId, + photoId=photo_id, updateTime=int(account.get("updatetime")), firstName=account.get("firstname"), lastName=account.get("lastname"), @@ -277,4 +288,4 @@ class Processors: ) # Отправялем - await self._send(writer, packet) \ No newline at end of file + await self._send(writer, packet)