Compare commits

..

No commits in common. "dd44bf2598652233e3a2b82f70d4712cea675418" and "b7ac60830025c3895f9ffa78bbc483d438e6d508" have entirely different histories.

6 changed files with 62 additions and 101 deletions

View File

@ -1 +0,0 @@
TODO

View File

@ -1,29 +0,0 @@
# Описание протокола TamTam по Websocket
## Основная информация
В веб версии мессенджера ТамТам используется протокол, работающий поверх Websocket.
Пакеты в этом протоколе являются текстовыми JSON данными.
Структура пакета:
```
{
ver: int,
cmd: int,
seq: int,
opcode: int,
payload: {}
}
```
* ver - версия протокола
* cmd - определяет, от кого отправлен пакет. клиент - 0, сервер - 1
* seq - порядковый номер пакета (сервер дублирует его из запроса клиента)
* opcode - команда
* payload - полезная нагрузка команды
## Команды протокола
### PING (1)
Клиент периодически отправляет пакет с командой PING и пустой нагрузкой серверу.
Сервер отвечает ему тем же.

View File

@ -102,14 +102,20 @@ class Tools:
# Выносим результат в лист # Выносим результат в лист
chats.append( chats.append(
self.generate_chat( {
row.get("id"), "id": row.get("id"),
row.get("owner"), "type": row.get("type"),
row.get("type"), "status": "ACTIVE",
participants, "owner": row.get("owner"),
message, "participants": participants,
messageTime "lastMessage": message,
) "lastEventTime": messageTime,
"lastDelayedUpdateTime": 0,
"lastFireDelayedErrorTime": 0,
"created": 1,
"joinTime": 1,
"modified": messageTime
}
) )
# Получаем последнее сообщение из избранного # Получаем последнее сообщение из избранного
@ -117,19 +123,24 @@ class Tools:
senderId, db_pool senderId, db_pool
) )
# ID избранного
chatId = senderId ^ senderId
# Хардкодим в лист чатов избранное # Хардкодим в лист чатов избранное
chats.append( chats.append(
self.generate_chat( {
chatId, "id": 0,
senderId, "type": "DIALOG",
"DIALOG", "status": "ACTIVE",
[senderId], "owner": senderId,
message, "participants": {
messageTime str(senderId): 0 # if not messageTime else messageTime
) },
"lastMessage": message,
"lastEventTime": messageTime,
"lastDelayedUpdateTime": 0,
"lastFireDelayedErrorTime": 0,
"created": 1,
"joinTime": 1,
"modified": messageTime
}
) )
return chats return chats
@ -174,11 +185,9 @@ class Tools:
"time": int(row.get("time")), "time": int(row.get("time")),
"type": row.get("type"), "type": row.get("type"),
"sender": row.get("sender"), "sender": row.get("sender"),
"cid": int(row.get("cid")),
"text": row.get("text"), "text": row.get("text"),
"attaches": json.loads(row.get("attaches")), "attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")), # "reactionInfo": {}
"reactionInfo": {}
} }
# Возвращаем # Возвращаем

View File

@ -179,7 +179,7 @@ class OnemeConfig:
"moscow-theme-enabled": True, "moscow-theme-enabled": True,
"msg-get-reactions-page-size": 40, "msg-get-reactions-page-size": 40,
"music-files-enabled": False, "music-files-enabled": False,
"mytracker-enabled": False, "mytracker-enabled": True,
"net-client-dns-enabled": True, "net-client-dns-enabled": True,
"net-session-suppress-bad-disconnected-state": True, "net-session-suppress-bad-disconnected-state": True,
"net-stat-config": [ "net-stat-config": [

View File

@ -445,7 +445,7 @@ class Processors:
chatId = userId ^ senderId chatId = userId ^ senderId
# Если клиент хочет отправить сообщение в избранное, # Если клиент хочет отправить сообщение в избранное,
# то выставляем в качестве ID чата ID отправителя # то выставляем ID чата 0
# (А ещё используем это, если клиент вообще ничего не указал) # (А ещё используем это, если клиент вообще ничего не указал)
if chatId == 0 or not chatId: if chatId == 0 or not chatId:
chatId = senderId chatId = senderId
@ -491,8 +491,7 @@ class Processors:
"sender": senderId, "sender": senderId,
"cid": cid, "cid": cid,
"text": text, "text": text,
"attaches": attaches, "attaches": attaches
"elements": elements
} }
# Отправляем событие всем участникам чата # Отправляем событие всем участникам чата
@ -701,9 +700,6 @@ class Processors:
senderId, self.db_pool senderId, self.db_pool
) )
# ID избранного
chatId = senderId ^ senderId
# Добавляем чат в список # Добавляем чат в список
chats.append( chats.append(
self.tools.generate_chat( self.tools.generate_chat(

View File

@ -1,19 +1,11 @@
import hashlib import hashlib, secrets, random, time, logging, json
import secrets
import time
import logging
import json
import re
from common.static import Static from common.static import Static
from common.tools import Tools from common.tools import Tools
from tamtam_tcp.proto import Proto from tamtam_tcp.proto import Proto
from tamtam_tcp.models import * from tamtam_tcp.models import *
class Processors: class Processors:
def __init__(self, db_pool=None, clients=None, send_event=None): def __init__(self, db_pool=None, clients={}, send_event=None):
if clients is None:
clients = {} # Более правильная логика
self.static = Static() self.static = Static()
self.proto = Proto() self.proto = Proto()
self.tools = Tools() self.tools = Tools()
@ -35,11 +27,11 @@ class Processors:
"message": "Unknown error", "message": "Unknown error",
"title": "Неизвестная ошибка" "title": "Неизвестная ошибка"
}) })
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_ERR, seq=seq, opcode=opcode, payload=payload cmd=self.proto.CMD_ERR, seq=seq, opcode=opcode, payload=payload
) )
await self._send(writer, packet) await self._send(writer, packet)
async def process_hello(self, payload, seq, writer): async def process_hello(self, payload, seq, writer):
@ -50,10 +42,10 @@ class Processors:
except Exception as e: except Exception as e:
await self._send_error(seq, self.proto.HELLO, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.proto.HELLO, self.error_types.INVALID_PAYLOAD, writer)
return None, None return None, None
# Получаем данные из пакета # Получаем данные из пакета
device_type = payload.get("userAgent").get("deviceType") deviceType = payload.get("userAgent").get("deviceType")
device_name = payload.get("userAgent").get("deviceName") deviceName = payload.get("userAgent").get("deviceName")
# Данные пакета # Данные пакета
payload = { payload = {
@ -72,8 +64,8 @@ class Processors:
# Отправляем # Отправляем
await self._send(writer, packet) await self._send(writer, packet)
return device_type, device_name return deviceType, deviceName
async def process_request_code(self, payload, seq, writer): async def process_request_code(self, payload, seq, writer):
"""Обработчик запроса кода""" """Обработчик запроса кода"""
# Валидируем данные пакета # Валидируем данные пакета
@ -84,17 +76,17 @@ class Processors:
return return
# Извлекаем телефон из пакета # Извлекаем телефон из пакета
phone = re.sub(r'\D', '', payload.get("phone", "")) # Не хардкодим, через регулярки phone = payload.get("phone").replace("+", "").replace(" ", "").replace("-", "")
# Генерируем токен с кодом # Генерируем токен с кодом
code = f"{secrets.randbelow(1_000_000):06d}" # Старая версия ненадежна, могла отбросить ведущие нули или вообще интерпритировать как систему счисления с основанием 8 code = str(random.randint(000000, 999999))
token = secrets.token_urlsafe(128) token = secrets.token_urlsafe(128)
# Хешируем # Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest() code_hash = hashlib.sha256(code.encode()).hexdigest()
token_hash = hashlib.sha256(token.encode()).hexdigest() token_hash = hashlib.sha256(token.encode()).hexdigest()
# Срок жизни токена (5 минут) # Время истечения токена
expires = int(time.time()) + 300 expires = int(time.time()) + 300
# Ищем пользователя, и если он существует, сохраняем токен # Ищем пользователя, и если он существует, сохраняем токен
@ -103,14 +95,12 @@ class Processors:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
user = await cursor.fetchone() user = await cursor.fetchone()
if not user: if user is None:
await self._send_error(seq, self.proto.REQUEST_CODE, self.error_types.USER_NOT_FOUND, writer) await self._send_error(seq, self.proto.REQUEST_CODE, self.error_types.USER_NOT_FOUND, writer)
return return
# Сохраняем токен # Сохраняем токен
await cursor.execute( 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",))
"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 = { payload = {
@ -153,11 +143,10 @@ class Processors:
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
# Ищем токен # Ищем токен
await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", (hashed_token,))
(hashed_token,))
stored_token = await cursor.fetchone() stored_token = await cursor.fetchone()
if not stored_token: if stored_token is None:
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.CODE_EXPIRED, writer) await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.CODE_EXPIRED, writer)
return return
@ -165,14 +154,13 @@ class Processors:
if stored_token.get("code_hash") != hashed_code: if stored_token.get("code_hash") != hashed_code:
await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_CODE, writer) await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_CODE, writer)
return return
# Ищем аккаунт # Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
account = await cursor.fetchone() account = await cursor.fetchone()
# Обновляем состояние токена # Обновляем состояние токена
await cursor.execute("UPDATE auth_tokens set state = %s WHERE token_hash = %s", await cursor.execute("UPDATE auth_tokens set state = %s WHERE token_hash = %s", ("verified", hashed_token,))
("verified", hashed_token,))
# # Создаем сессию # # Создаем сессию
# await cursor.execute( # await cursor.execute(
@ -182,9 +170,9 @@ class Processors:
# Генерируем профиль # Генерируем профиль
# Аватарка с биографией # Аватарка с биографией
photo_id = int(account["avatar_id"]) if account.get("avatar_id") else None photoId = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = f"{self.config.avatar_base_url}{photo_id}" if photo_id else None avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = account.get("description") description = None if not account.get("description") else account.get("description")
# Собираем данные пакета # Собираем данные пакета
payload = { payload = {
@ -192,7 +180,7 @@ class Processors:
id=account.get("id"), id=account.get("id"),
phone=int(account.get("phone")), phone=int(account.get("phone")),
avatarUrl=avatar_url, avatarUrl=avatar_url,
photoId=photo_id, photoId=photoId,
updateTime=int(account.get("updatetime")), updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"), firstName=account.get("firstname"),
lastName=account.get("lastname"), lastName=account.get("lastname"),
@ -246,8 +234,7 @@ class Processors:
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
# Ищем токен # Ищем токен
await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", await cursor.execute("SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", (hashed_token,))
(hashed_token,))
stored_token = await cursor.fetchone() stored_token = await cursor.fetchone()
if stored_token is None: if stored_token is None:
@ -268,13 +255,12 @@ class Processors:
# Создаем сессию # Создаем сессию
await cursor.execute( await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)", "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", (stored_token.get("phone"), hashed_login, deviceType, deviceName, "Epstein Island", int(time.time()),)
int(time.time()),)
) )
# Аватарка с биографией # Аватарка с биографией
photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id")) photoId = 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 avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not account.get("description") else account.get("description") description = None if not account.get("description") else account.get("description")
# Собираем данные пакета # Собираем данные пакета
@ -284,7 +270,7 @@ class Processors:
id=account.get("id"), id=account.get("id"),
phone=int(account.get("phone")), phone=int(account.get("phone")),
avatarUrl=avatar_url, avatarUrl=avatar_url,
photoId=photo_id, photoId=photoId,
updateTime=int(account.get("updatetime")), updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"), firstName=account.get("firstname"),
lastName=account.get("lastname"), lastName=account.get("lastname"),
@ -304,4 +290,4 @@ class Processors:
cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.FINAL_AUTH, payload=payload cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.FINAL_AUTH, payload=payload
) )
await self._send(writer, packet) await self._send(writer, packet)