From cab75a58f877ac778597f6d065189d974f12666d Mon Sep 17 00:00:00 2001 From: relyay Date: Tue, 10 Mar 2026 21:19:54 +0300 Subject: [PATCH 1/7] Merge pull request #9 from relyay/fix Some fixes --- src/tamtam_tcp/processors.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/tamtam_tcp/processors.py b/src/tamtam_tcp/processors.py index 5faa5e7..c13aba1 100644 --- a/src/tamtam_tcp/processors.py +++ b/src/tamtam_tcp/processors.py @@ -1,4 +1,5 @@ -import hashlib, secrets, random, time, logging, json +import hashlib, secrets, random, time, logging, json # PEP-8 по приколу сделан >_< +import re from common.static import Static from common.tools import Tools from tamtam_tcp.proto import Proto @@ -76,17 +77,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 # Ищем пользователя, и если он существует, сохраняем токен From 917db804605750679af0f2a3b5e9dabfaf179589 Mon Sep 17 00:00:00 2001 From: devreal95 Date: Tue, 10 Mar 2026 21:42:11 +0300 Subject: [PATCH 2/7] Update config.py (#10) --- src/oneme_tcp/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": [ From 4d82f55b798857073f155d376b1b876ea2e47a05 Mon Sep 17 00:00:00 2001 From: relyay Date: Tue, 10 Mar 2026 21:59:44 +0300 Subject: [PATCH 3/7] Fix (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Улучшена генерация кода, пояснения в некоторых участках, очистка номера телефона через регулярные выражения :> * Именовать переменные snake_case стоит везде, даже если ты достаешь заголовки в такомСтиле if not object использовать предпочтительнее, т.к. он обрабатывает более широкие случаи, когда достать данные не получилось --- src/tamtam_tcp/processors.py | 67 +++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/tamtam_tcp/processors.py b/src/tamtam_tcp/processors.py index c13aba1..0f7977b 100644 --- a/src/tamtam_tcp/processors.py +++ b/src/tamtam_tcp/processors.py @@ -1,12 +1,19 @@ -import hashlib, secrets, random, time, logging, json # PEP-8 по приколу сделан >_< +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() @@ -28,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): @@ -43,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 = { @@ -65,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): """Обработчик запроса кода""" # Валидируем данные пакета @@ -77,10 +84,10 @@ class Processors: return # Извлекаем телефон из пакета - phone = re.sub(r'\D', '', payload.get("phone", "")) # Не хардкодим, через регулярки + phone = re.sub(r'\D', '', payload.get("phone", "")) # Не хардкодим, через регулярки # Генерируем токен с кодом - code = f"{secrets.randbelow(1_000_000):06d}" # Старая версия ненадежна, могла отбросить ведущие нули или вообще интерпритировать как систему счисления с основанием 8 + code = f"{secrets.randbelow(1_000_000):06d}" # Старая версия ненадежна, могла отбросить ведущие нули или вообще интерпритировать как систему счисления с основанием 8 token = secrets.token_urlsafe(128) # Хешируем @@ -96,12 +103,14 @@ class Processors: await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,)) user = await cursor.fetchone() - if user is None: + if not user: 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",)) + 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 = { @@ -144,10 +153,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 @@ -155,13 +165,14 @@ 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() # Обновляем состояние токена - await cursor.execute("UPDATE auth_tokens set state = %s WHERE token_hash = %s", ("verified", hashed_token,)) + await cursor.execute("UPDATE auth_tokens set state = %s WHERE token_hash = %s", + ("verified", hashed_token,)) # # Создаем сессию # await cursor.execute( @@ -171,9 +182,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 = { @@ -181,7 +192,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"), @@ -235,7 +246,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: @@ -256,12 +268,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") # Собираем данные пакета @@ -271,7 +284,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"), @@ -291,4 +304,4 @@ class Processors: cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.FINAL_AUTH, payload=payload ) - await self._send(writer, packet) \ No newline at end of file + await self._send(writer, packet) From 573825e195bd71f7090aa09c773608cc80614661 Mon Sep 17 00:00:00 2001 From: WowInceptionGood <143893762+WowInceptionGood@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:40:49 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=D0=9D=D0=B0=D1=87=D0=B0=D0=BB=20=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B0=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=20=D0=A2=D0=B0=D0=BC=D0=A2=D0=B0=D0=BC=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/proto/tamtam_ws.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/proto/tamtam_ws.md diff --git a/docs/proto/tamtam_ws.md b/docs/proto/tamtam_ws.md new file mode 100644 index 0000000..42910e6 --- /dev/null +++ b/docs/proto/tamtam_ws.md @@ -0,0 +1,29 @@ +# Описание протокола TamTam по Websocket + +## Основная информация +В веб версии мессенджера ТамТам используется протокол, работающий поверх Websocket. + +Пакеты в этом протоколе являются JSON данными, закодированными в текст (UTF-8). + +Структура пакета: +```json +{ + ver: int, + cmd: int, + seq: int, + opcode: int, + payload: {} +} +``` + +ver - версия протокола +cmd - определяет, от кого отправлен пакет. клиент - 0, сервер - 1 +seq - порядковый номер пакета (сервер дублирует его из запроса клиента) +opcode - команда +payload - полезная нагрузка команды + +## Команды протокола + +### PING (1) +Клиент отправляет пакет с командой PING и пустой нагрузкой серверу раз в x секунд. +Сервер отвечает ему тем же. \ No newline at end of file From fbb451cd39db807677da6d1f5c9abd3307685c21 Mon Sep 17 00:00:00 2001 From: WowInceptionGood <143893762+WowInceptionGood@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:41:57 +0000 Subject: [PATCH 5/7] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/proto/tamtam_ws.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/proto/tamtam_ws.md b/docs/proto/tamtam_ws.md index 42910e6..8f62dfe 100644 --- a/docs/proto/tamtam_ws.md +++ b/docs/proto/tamtam_ws.md @@ -3,10 +3,10 @@ ## Основная информация В веб версии мессенджера ТамТам используется протокол, работающий поверх Websocket. -Пакеты в этом протоколе являются JSON данными, закодированными в текст (UTF-8). +Пакеты в этом протоколе являются JSON данными, закодированными в текст. Структура пакета: -```json +``` { ver: int, cmd: int, From fb46d06aabadb96fc4a4ec31c7b6252dd0108a81 Mon Sep 17 00:00:00 2001 From: WowInceptionGood <143893762+WowInceptionGood@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:43:25 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=D0=9B=D0=B0=D0=B4=D0=BD=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B5=D0=B5=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/proto/tamtam_ws.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/proto/tamtam_ws.md b/docs/proto/tamtam_ws.md index 8f62dfe..bd4e712 100644 --- a/docs/proto/tamtam_ws.md +++ b/docs/proto/tamtam_ws.md @@ -3,7 +3,7 @@ ## Основная информация В веб версии мессенджера ТамТам используется протокол, работающий поверх Websocket. -Пакеты в этом протоколе являются JSON данными, закодированными в текст. +Пакеты в этом протоколе являются текстовыми JSON данными. Структура пакета: ``` @@ -16,14 +16,14 @@ } ``` -ver - версия протокола -cmd - определяет, от кого отправлен пакет. клиент - 0, сервер - 1 -seq - порядковый номер пакета (сервер дублирует его из запроса клиента) -opcode - команда -payload - полезная нагрузка команды +* ver - версия протокола +* cmd - определяет, от кого отправлен пакет. клиент - 0, сервер - 1 +* seq - порядковый номер пакета (сервер дублирует его из запроса клиента) +* opcode - команда +* payload - полезная нагрузка команды ## Команды протокола ### PING (1) -Клиент отправляет пакет с командой PING и пустой нагрузкой серверу раз в x секунд. +Клиент периодически отправляет пакет с командой PING и пустой нагрузкой серверу. Сервер отвечает ему тем же. \ No newline at end of file From 582c0f571c7d3e8a00f3c395db69aef0e50d7785 Mon Sep 17 00:00:00 2001 From: BetaAcccc <146182519+BetaAcccc@users.noreply.github.com> Date: Wed, 11 Mar 2026 05:00:25 +0700 Subject: [PATCH 7/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20faq=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create patch_apk.md * Create install.md * Update patch_apk.md * remove space * Create readme.md * Update readme.md * Update readme.md * Update patch_apk.md * Подправил инструкцию по патчу Сделал чище и постарался сделать грамотнее --------- Co-authored-by: Alexey Polyakov Co-authored-by: WowInceptionGood <143893762+WowInceptionGood@users.noreply.github.com> --- faq/install.md | 18 ++++++++++++++++++ faq/patch_apk.md | 24 ++++++++++++++++++++++++ faq/readme.md | 7 +++++++ readme.md | 25 +++---------------------- 4 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 faq/install.md create mode 100644 faq/patch_apk.md create mode 100644 faq/readme.md 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 5165f10..1faf673 100644 --- a/readme.md +++ b/readme.md @@ -3,8 +3,7 @@ > Проект находится на ранней стадии разработки и вероятно полон багов. > > Использование в профессиональных средах не рекомендовано. - - +> # OpenMAX Эмулятор сервера MAX и ТамТам @@ -22,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)