TT: ну вроде шире поддержка, а вообще обратная совместимость с максом клас

This commit is contained in:
Alexey Polyakov
2026-05-05 23:06:50 +03:00
parent 89f1fefa31
commit bcd94b3a57
16 changed files with 1020 additions and 364 deletions

View File

@@ -419,6 +419,7 @@ class Tools:
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"link": {}
}
# Возвращаем

View File

@@ -64,11 +64,13 @@ class HistoryProcessors(BaseProcessor):
"time": int(row.get("time")),
"type": row.get("type"),
"sender": row.get("sender"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"options": 1,
"link": {},
#"options": 1,
})
if forward > 0:
@@ -81,14 +83,17 @@ class HistoryProcessors(BaseProcessor):
for row in result:
messages.append({
"id": row.get("id"),
"id": row.get("id") if self.type == 'mobile' else str(row.get('id')),
"time": int(row.get("time")),
"type": row.get("type"),
"sender": row.get("sender"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {}
"reactionInfo": {},
"link": {}
#"options": 1,
})
# Сортируем сообщения по времени

View File

@@ -9,319 +9,12 @@ class TTConfig:
"googNoiseSuppression": "true",
"googHighpassFilter": "false",
"googTypingNoiseDetection": "false",
"googAudioNetworkAdaptorConfig": "ChyyARkNCtcjPBUK1yM8GKjDASCw6gEomHUwoJwBCgfKAQQIABAACgvCAQgIqMMBELiRAgosqgEpChEIuBcVzcxMPhjogQIlCtejOxIRCOgHFc3MTD4YsOoBJQrXozsYyAEKC7oBCAiw6gEQoJwB"
"googAudioNetworkAdaptorConfig": ""
},
"a-lte": 24,
"a-wifi": 34,
"account-removal-enabled": False,
"animated-emojis": {
"❤️": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/03.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/04.json"
}
},
"👍": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-29lottie/e/16.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/01_m.json"
}
},
"👎": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/17.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/02.json"
}
},
"🙏": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/04.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-29lottie/e/30_ng.json"
}
},
"😘": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/05.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/03.json"
}
},
"🔥": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/06.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/10.json"
}
},
"😂": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/07.json"
}
},
"👏": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/56.json"
}
},
"😮": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/09.json"
}
},
"💋": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/13_v02.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2023-01-18lottie/r/kissing2.json"
}
},
"🥂": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/20.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/20.json"
}
},
"😳": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/09.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/02.json"
}
},
"😔": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/11.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/05.json"
}
},
"😍": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/07.json"
}
},
"😯": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/08.json"
}
},
"😉": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/09.json"
}
},
"🌺": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2023-03-06lottie/flower.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/11.json"
}
},
"🎂": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/14.json"
}
},
"💩": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/15.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2023-01-18lottie/r/shit_1.json"
}
},
"🐰": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/19.json"
}
},
"🎅": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/21.json"
}
},
"🎄": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/23.json"
}
},
"🎆": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/22.json"
}
},
"❄️": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/25.json"
}
},
"🎉": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/12.json"
}
},
"🥗": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-29lottie/e/28.json"
}
},
"🧡": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/31.json"
}
},
"💔": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/32.json"
}
},
"🎁": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/34.json"
}
},
"🌹": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/35.json"
}
},
"🌸": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/36.json"
}
},
"🍒": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/37.json"
}
},
"🥕": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/39.json"
}
},
"🍑": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/40.json"
}
},
"🍋": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/41.json"
}
},
"🍃": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/42.json"
}
},
"😺": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/43.json"
}
},
"🐶": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/44.json"
}
},
"🐽": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/45.json"
}
},
"💐": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/46.json"
}
},
"🎈": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/47.json"
}
},
"🍾": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/48.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/49.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/50.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/51.json"
}
},
"💃": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/52.json"
}
},
"☀️": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/53.json"
}
},
"👋": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/54.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/57.json"
}
},
"🙂": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/58.json"
}
},
"🤩": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-15animoji/59.json"
}
},
"😇": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/60.json"
}
},
"😎": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/61.json"
}
},
"🍎": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/62.json"
}
}
},
"animated-emojis": {},
"animated-emojis-limits": {
"low": 5,
"average": 10,
@@ -383,10 +76,10 @@ class TTConfig:
"image-quality": 0.800000011920929,
"image-size": 40000000,
"image-width": 1680,
"invite-header": "Приглашение в ТамТам",
"invite-link": "https://tt.me/starwear",
"invite-long": "Я общаюсь в ТамТам, присоединяйся https://tt.me/starwear",
"invite-short": "Привет! Ставь ТамТам! Жду ответа! https://tt.me/starwear",
"invite-header": "",
"invite-link": "",
"invite-long": "",
"invite-short": "",
"keep-connection": 2,
"l10n": False,
"live-location-enabled": True,
@@ -453,25 +146,12 @@ class TTConfig:
"profiling-enabled": False,
"progress-diff-for-notify": 1,
"promo-contact-id": 0,
"promo-recent-contacts": True,
"promo_contact_label": "Белый Маг",
"proxy": "msgproxy.okcdn.ru",
"proxy-domains": [
"okcdn.ru",
"mycdn.me",
"ok.ru",
"odnoklassniki.ru",
"odkl.ru",
"vk.com",
"userapi.com",
"vkuser.net",
"vkusercdn.ru"
],
"proxy-exclude": [
"r.mradx.net",
"ad.mail.ru"
],
"proxy-rotation": True,
"promo-recent-contacts": False,
"promo_contact_label": "",
"proxy": "",
"proxy-domains": [],
"proxy-exclude": [],
"proxy-rotation": False,
"push-alert-timeout": 604800,
"push-tracking-enabled": True,
"quick-forward-cases": [],
@@ -519,24 +199,24 @@ class TTConfig:
"TOP"
],
"stickers-suggestion-keywords-inline": False,
"support-account": "tt.me/support",
"support-account": "",
"support-button-enable": False,
"t-ice-reconnect": 15,
"t-incoming-call": 40,
"t-start-connect": 20,
"tam-emoji-font-url": "https://st.okcdn.ru/static/messages/2022-08-25noto/TamNotoColorEmojiCompat.ttf",
"tam-emoji-font-url": "",
"tcp-candidates": False,
"tracer-crash-report-enabled": True,
"tracer-crash-report-host": "https://api-hprof.odkl.ru",
"tracer-crash-send-asap-enabled": True,
"tracer-crash-send-logs-enabled": True,
"tracer-crash-send-threads-dump-enabled": True,
"tracer-crash-report-enabled": False,
"tracer-crash-report-host": "",
"tracer-crash-send-asap-enabled": False,
"tracer-crash-send-logs-enabled": False,
"tracer-crash-send-threads-dump-enabled": False,
"tracer-disk-overflow-report-threshold": 3000000000,
"tracer-disk-usage-probability": 500,
"tracer-enabled": True,
"tracer-host": "https://api-hprof.odkl.ru",
"tracer-enabled": False,
"tracer-host": "",
"tracer-hprof-probability": -1,
"tracer-sampled-conditions": "tag=app_start_ui_freeze_2k;probability=100000;startEvent=app_first_activity_created;interestingEvent=app_freeze;interestingDuration=2000",
"tracer-sampled-conditions": "",
"tracer-sampled-duration": 20000,
"tracer-systrace-duration": 20000,
"tracer-systrace-interesting-duration": 10000,
@@ -569,4 +249,4 @@ class TTConfig:
"iceServers": [],
"has-phone": True,
"promo-constructors": []
}
}

View File

@@ -3,24 +3,24 @@ import pydantic
class UserAgentModel(pydantic.BaseModel):
deviceType: str
appVersion: str
osVersion: str
timezone: str
screen: str
osVersion: str = None
timezone: str = None
screen: str = None
pushDeviceType: str = None
locale: str
locale: str = None
deviceName: str
deviceLocale: str
deviceLocale: str = None
class HelloPayloadModel(pydantic.BaseModel):
userAgent: UserAgentModel
deviceId: str
deviceId: str = None
class RequestCodePayloadModel(pydantic.BaseModel):
phone: str
class VerifyCodePayloadModel(pydantic.BaseModel):
verifyCode: str
authTokenType: str
authTokenType: str = None
token: str
class FinalAuthPayloadModel(pydantic.BaseModel):
@@ -30,7 +30,7 @@ class FinalAuthPayloadModel(pydantic.BaseModel):
token: str
class LoginPayloadModel(pydantic.BaseModel):
interactive: bool
interactive: bool = None
token: str
class SearchUsersPayloadModel(pydantic.BaseModel):
@@ -41,4 +41,57 @@ class PingPayloadModel(pydantic.BaseModel):
class ChatHistoryPayloadModel(pydantic.BaseModel):
chatId: int
backward: int
backward: int
class UpdateProfilePayloadModel(pydantic.BaseModel):
pass
class SearchChatsPayloadModel(pydantic.BaseModel):
chatIds: list
class AssetsPayloadModel(pydantic.BaseModel):
sync: int
type: str = None
userId: int = None
class GetCallTokenPayloadModel(pydantic.BaseModel):
userId: int
value: str
class GetCallHistoryPayloadModel(pydantic.BaseModel):
forward: bool
count: int
class ChatSubscribePayloadModel(pydantic.BaseModel):
chatId: int
subscribe: bool
class ContactListPayloadModel(pydantic.BaseModel):
status: str
count: int = None
class ContactPresencePayloadModel(pydantic.BaseModel):
contactIds: list
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int
firstName: str
lastName: str = None
class TypingPayloadModel(pydantic.BaseModel):
chatId: int
type: str = None
class MessageModel(pydantic.BaseModel):
isLive: bool = None
detectShare: bool = None
elements: list = None
attaches: list = None
cid: int = None
text: str = None
class SendMessagePayloadModel(pydantic.BaseModel):
userId: int = None
chatId: int = None
message: MessageModel

View File

@@ -2,9 +2,21 @@ from .main import MainProcessors
from .auth import AuthProcessors
from .search import SearchProcessors
from .history import HistoryProcessors
from .assets import AssetsProcessors
from .calls import CallsProcessors
from .chats import ChatsProcessors
from .contacts import ContactsProcessors
from .messages import MessagesProcessors
from .sessions import SessionsProcessors
class Processors(MainProcessors,
AuthProcessors,
SearchProcessors,
HistoryProcessors):
pass
HistoryProcessors,
AssetsProcessors,
CallsProcessors,
ChatsProcessors,
ContactsProcessors,
MessagesProcessors,
SessionsProcessors):
pass

View File

@@ -0,0 +1,34 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import AssetsPayloadModel
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета
payload = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"sections": [],
"stickersOrder": []
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
)
# Отправляем
await self._send(writer, packet)

View File

@@ -174,6 +174,9 @@ class AuthProcessors(BaseProcessor):
if not deviceType:
deviceType = payload.get("deviceType")
if not deviceName:
deviceName = "Unknown device"
# Хешируем токен
hashed_token = hashlib.sha256(token.encode()).hexdigest()
@@ -231,7 +234,9 @@ class AuthProcessors(BaseProcessor):
# Собираем данные пакета
payload = {
"userToken": "0", # Пока как заглушка
# Я хз че сюда вставлять)
# ребята из одноклассников, может быть вы подскажете?
"userToken": str(account.get("id")),
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
@@ -338,16 +343,25 @@ class AuthProcessors(BaseProcessor):
include_favourites=False
)
# Генерируем список контактов
contacts = await self.tools.collect_user_contacts(
user.get("id"), self.db_pool, self.config.avatar_base_url
)
# Собираем статусы контактов
contact_ids = [c.get("id") for c in contacts if c.get("id") is not None]
presence = await self.tools.collect_presence(contact_ids, self.clients, self.db_pool)
# Формируем данные пакета
payload = {
"profile": profile,
"chats": chats,
"chatMarker": 0,
"messages": {},
"contacts": [],
"presence": {},
"contacts": contacts,
"presence": presence,
"config": {
"hash": "e5903aa8-0000000000000000-80000106-0000000000000001-00000001-0000000000000000-00000000-2-00000001-0000019c9559d057",
"hash": "0",
"server": self.server_config,
"user": updated_user_config,
"chatFolders": {
@@ -371,6 +385,10 @@ class AuthProcessors(BaseProcessor):
"time": int(time.time() * 1000)
}
# print(
# json.dumps(payload, indent=4)
# )
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGIN, payload=payload
@@ -378,4 +396,21 @@ class AuthProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
return int(user.get("phone")), int(user.get("id")), hashed_token
return int(user.get("phone")), int(user.get("id")), hashed_token
async def 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.opcodes.LOGOUT, payload=None
)
# Отправляем
await self._send(writer, response)

View File

@@ -0,0 +1,20 @@
import pydantic
from classes.baseprocessor import BaseProcessor
from tamtam.models import ChatSubscribePayloadModel
class ChatsProcessors(BaseProcessor):
async def chat_subscribe(self, payload, seq, writer):
# Валидируем входные данные
try:
ChatSubscribePayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.CHAT_SUBSCRIBE, self.error_types.INVALID_PAYLOAD, writer)
return
# Созадаем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_SUBSCRIBE, payload=None
)
# Отправялем
await self._send(writer, packet)

View File

@@ -0,0 +1,188 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel
class ContactsProcessors(BaseProcessor):
async def contact_list(self, payload, seq, writer, userId):
"""Обработчик получения контактов"""
# Валидируем данные пакета
try:
ContactListPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_LIST, self.error_types.INVALID_PAYLOAD, writer)
return
status = payload.get("status")
count = payload.get("count")
# Итоговый контакт-лист
contact_list = []
if status == "BLOCKED":
# Собираем контакты, которые в черном списке
blocked = []
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
if count:
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND is_blocked = TRUE LIMIT %s",
(userId, count),
)
else:
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND is_blocked = TRUE",
(userId,),
)
rows = await cursor.fetchall()
for row in rows:
blocked.append(
{
"id": int(row.get("contact_id")),
"firstname": row.get("custom_firstname"),
"lastname": row.get("custom_lastname"),
"blocked": True,
}
)
# Генерируем контакт-лист
contact_list = await self.tools.generate_contacts(
blocked, self.db_pool, avatar_base_url=self.config.avatar_base_url
)
# Собираем данные пакета
response_payload = {
"contacts": contact_list
}
# Создаем пакет
packet = self.proto.pack_packet(
seq=seq, opcode=self.opcodes.CONTACT_LIST, payload=response_payload
)
# Отправляем пакет
await self._send(writer, packet)
async def contact_update(self, payload, seq, writer, userId):
"""
Обработчик опкода какого-то там
(их хуй запомнишь, даже в мриме команды помню, бля)
Отвечает за добавку, удаление, блокировку и разблокировку контакта
"""
# Валидируем данные пакета
try:
ContactUpdatePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
action = payload.get("action")
contactId = payload.get("contactId")
firstName = payload.get("firstName")
lastName = payload.get("lastName", "")
if action == "ADD":
# Проверяем, существует ли пользователь с таким ID
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.USER_NOT_FOUND, writer)
return
# Проверяем, не добавлен ли уже контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Если контакта не существует, то можем продолжать,
if not row:
# Добавляем контакт
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, FALSE)",
(userId, contactId, firstName, lastName)
)
# а если уже существует, отправляем ошибку
else:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_EXISTS, writer)
return
# Генерируем профиль
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 + str(photoId)
contact = 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")),
accountStatus=int(user.get("accountstatus")),
includeProfileOptions=False,
custom_firstname=firstName,
custom_lastname=lastName,
)
response_payload = {
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=response_payload
)
await self._send(writer, packet)
elif action == "REMOVE":
# Удаляем контакт
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"DELETE FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
async def contact_presence(self, payload, seq, writer):
"""Обработчик получения статуса контактов"""
# Валидируем данные пакета
try:
ContactPresencePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_PRESENCE, self.error_types.INVALID_PAYLOAD, writer)
return
contact_ids = payload.get("contactIds", [])
now_ms = int(time.time() * 1000)
presence = await self.tools.collect_presence(contact_ids, self.clients, self.db_pool)
response_payload = {
"presence": presence,
"time": now_ms
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_PRESENCE, payload=response_payload
)
await self._send(writer, packet)

View File

@@ -0,0 +1,133 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import SyncFoldersPayloadModel, CreateFolderPayloadModel
class FoldersProcessors(BaseProcessor):
async def folders_get(self, payload, seq, writer, senderPhone):
"""Синхронизация папок с сервером"""
# Валидируем данные пакета
try:
SyncFoldersPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.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 id, title, filters, `include`, options, update_time, source_id "
"FROM user_folders WHERE phone = %s ORDER BY sort_order",
(int(senderPhone),)
)
result_folders = await cursor.fetchall()
folders = [
{
"id": folder["id"],
"title": folder["title"],
"filters": json.loads(folder["filters"]),
"include": json.loads(folder["include"]),
"updateTime": folder["update_time"],
"options": json.loads(folder["options"]),
"sourceId": folder["source_id"]
}
for folder in result_folders
]
# Создаем данные пакета
payload = {
"folderSync": int(time.time() * 1000),
"folders": folders,
"foldersOrder": [folder["id"] for folder in result_folders],
"allFilterExcludeFolders": []
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.FOLDERS_GET, payload=payload
)
# Отправляем
await self._send(writer, packet)
async def folders_update(self, payload, seq, writer, senderPhone):
"""Создание папки"""
# Валидируем данные пакета
try:
CreateFolderPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.FOLDERS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
update_time = int(time.time() * 1000)
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT COALESCE(MAX(sort_order), -1) as max_order FROM user_folders WHERE phone = %s",
(int(senderPhone),)
)
row = await cursor.fetchone()
next_order = row["max_order"] + 1
# Создаем новую папку
await cursor.execute(
"INSERT INTO user_folders (id, phone, title, filters, `include`, options, source_id, update_time, sort_order) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(
payload.get("id"),
int(senderPhone),
payload.get("title"),
json.dumps(payload.get("filters")),
json.dumps(payload.get("include", [])),
json.dumps([]),
1,
update_time,
next_order,
)
)
await conn.commit()
# Получаем обновленный порядок папок
await cursor.execute(
"SELECT id FROM user_folders WHERE phone = %s ORDER BY sort_order",
(int(senderPhone),)
)
all_folders = await cursor.fetchall()
folders_order = [f["id"] for f in all_folders]
# Формируем данные пакета
response_payload = {
"folder": {
"id": payload.get("id"),
"title": payload.get("title"),
"include": payload.get("include"),
"filters": payload.get("filters"),
"updateTime": update_time,
"options": [],
"sourceId": 1,
},
"folderSync": update_time,
"foldersOrder": folders_order,
}
# Формируем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.FOLDERS_UPDATE, payload=response_payload
)
await self._send(writer, packet)
# Разработчики протокола, объяснитесь, что за хеш !!! а еще подарите нам способ его формирования
notify_about_hash = self.proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_CONFIG,
payload={"config": {"hash": "0"}}
)
await self._send(writer, notify_about_hash)

View File

@@ -1,6 +1,8 @@
import json
import pydantic
from classes.baseprocessor import BaseProcessor
from tamtam.models import HelloPayloadModel, PingPayloadModel
from tamtam.models import UpdateProfilePayloadModel
class MainProcessors(BaseProcessor):
async def session_init(self, payload, seq, writer):
@@ -35,6 +37,106 @@ class MainProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
return device_type, device_name
async def profile(self, payload, seq, writer, userId):
"""Обработчик получения/обновления профиля"""
# Валидируем входные данные
try:
UpdateProfilePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.PROFILE, 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 id = %s", (userId,))
user = await cursor.fetchone()
# Если пользователь не найден
if not user:
await self._send_error(seq, self.opcodes.PROFILE, self.error_types.USER_NOT_FOUND, writer)
return
# Аватарка с биографией
photo_id = int(user["avatar_id"]) if user.get("avatar_id") else None
avatar_url = f"{self.config.avatar_base_url}{photo_id}" if photo_id else None
description = user.get("description")
# Генерируем профиль
profile = self.tools.generate_profile_tt(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
description=description,
username=user.get("username")
)
# Создаем данные пакета
payload = {
"profile": profile
}
# Собираем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.PROFILE, payload=payload
)
# Отправляем
await self._send(writer, response)
async def update_config(self, payload, seq, writer, userPhone, hashedToken=None):
"""Обработчик обновления настроек и пуш-токена"""
result_payload = None
if payload.get("pushToken"):
push_token = payload.get("pushToken")
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE tokens SET push_token = %s WHERE phone = %s AND token_hash = %s",
(push_token, str(userPhone), hashedToken)
)
elif payload.get("settings") and payload.get("settings").get("user"):
new_settings = payload.get("settings").get("user")
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT user_config FROM user_data WHERE phone = %s", (userPhone,)
)
row = await cursor.fetchone()
if row:
current_config = json.loads(row.get("user_config"))
for key, value in new_settings.items():
if key in current_config:
current_config[key] = value
await cursor.execute(
"UPDATE user_data SET user_config = %s WHERE phone = %s",
(json.dumps(current_config), userPhone)
)
result_payload = {
"user": current_config,
"hash": "0"
}
# Собираем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONFIG, payload=result_payload
)
# Отправляем
await self._send(writer, response)
async def ping(self, payload, seq, writer):
"""Обработчик пинга"""

View File

@@ -0,0 +1,160 @@
import pydantic
from classes.baseprocessor import BaseProcessor
from tamtam.models import (
TypingPayloadModel,
SendMessagePayloadModel
)
class MessagesProcessors(BaseProcessor):
async def msg_typing(self, payload, seq, writer, senderId):
"""Обработчик события печатания"""
# Валидируем данные пакета
try:
TypingPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.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.opcodes.MSG_TYPING, self.error_types.CHAT_NOT_FOUND, writer)
return
# Участники чата
participants = await self.tools.get_chat_participants(chatId, self.db_pool)
# Проверяем, является ли отправитель участником чата
if int(senderId) not in participants:
await self._send_error(seq, self.opcodes.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,
"writer": writer,
}
)
# Создаем пакет
packet = self.proto.pack_packet(
seq=seq, opcode=self.opcodes.MSG_TYPING
)
# Отправляем пакет
await self._send(writer, packet)
async def msg_send(self, payload, seq, writer, senderId, db_pool):
"""Функция отправки сообщения"""
# Валидируем данные пакета
try:
SendMessagePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.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 ""
# Вычисляем ID чата по ID пользователя и ID отправителя,
# в случае отсутствия ID чата
if chatId is None:
chatId = userId ^ senderId
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.opcodes.MSG_SEND, self.error_types.CHAT_NOT_FOUND, writer)
return
# Список участников
participants = await self.tools.get_chat_participants(chatId, db_pool)
# Проверяем, является ли отправитель участником чата
if int(senderId) not in participants:
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Добавляем сообщение в историю
messageId, lastMessageId, messageTime = 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,
"elements": elements
}
# Отправляем событие всем участникам чата
for participant in participants:
await self.event(
participant,
{
"eventType": "new_msg",
"chatId": 0 if chatId == senderId else chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
"writer": writer
}
)
# Данные пакета
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.opcodes.MSG_SEND, payload=payload
)
# Отправляем
await self._send(writer, packet)

View File

@@ -1,6 +1,7 @@
import json, pydantic
from classes.baseprocessor import BaseProcessor
from tamtam.models import SearchUsersPayloadModel
from tamtam.models import SearchChatsPayloadModel
class SearchProcessors(BaseProcessor):
async def contact_info(self, payload, seq, writer):
@@ -59,5 +60,61 @@ class SearchProcessors(BaseProcessor):
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_INFO, payload=payload
)
# Отправляем
await self._send(writer, response)
async def chat_info(self, payload, seq, writer, senderId):
"""Поиск чатов по ID"""
# Валидируем данные пакета
try:
SearchChatsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.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:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
if chat:
# Проверяем, является ли пользователь участником чата
participants = await self.tools.get_chat_participants(chatId, self.db_pool)
if int(senderId) not in participants:
continue
# Получаем последнее сообщение из чата
message, messageTime = await self.tools.get_last_message(
chatId, self.db_pool, protocol_type=self.type
)
# Добавляем чат в список
chats.append(
self.tools.generate_chat(
chatId, chat.get("owner"),
chat.get("type"), participants,
message, messageTime
)
)
# Создаем данные пакета
payload = {
"chats": chats
}
# Собираем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_INFO, payload=payload
)
# Отправляем
await self._send(writer, response)

View File

@@ -0,0 +1,38 @@
from classes.baseprocessor import BaseProcessor
class SessionsProcessors(BaseProcessor):
async def sessions_info(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"TamTam {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.opcodes.SESSIONS_INFO, payload=payload
)
# Отправляем
await self._send(writer, response)

View File

@@ -24,7 +24,7 @@ class TamTamMobile:
self.auth_required = Tools().auth_required
# rate limiter
self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
self.auth_rate_limiter = RateLimiter(max_attempts=15, window_seconds=60)
self.read_timeout = 300 # Таймаут чтения из сокета (секунды)
self.max_read_size = 65536 # Максимальный размер данных из сокета
@@ -105,18 +105,91 @@ class TamTamMobile:
if userPhone:
await self._finish_auth(writer, address, userPhone, userId)
case self.opcodes.LOGOUT:
await self.processors.logout(
seq, writer, hashedToken=hashedToken
)
break
case self.opcodes.CONTACT_INFO:
await self.auth_required(
userPhone, self.processors.contact_info, payload, seq, writer
)
case self.opcodes.CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.chat_history, payload, seq, writer, userId
)
case self.opcodes.ASSETS_UPDATE:
await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, writer
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, writer
)
case self.opcodes.MSG_SEND:
await self.auth_required(
userPhone, self.processors.msg_send, payload, seq, writer, userId, self.db_pool
)
case self.opcodes.MSG_TYPING:
await self.auth_required(
userPhone, self.processors.msg_typing, payload, seq, writer, userId
)
case self.opcodes.FOLDERS_GET:
await self.auth_required(
userPhone, self.processors.folders_get, payload, seq, writer, userPhone
)
case self.opcodes.FOLDERS_UPDATE:
await self.auth_required(
userPhone, self.processors.folders_update, payload, seq, writer, userPhone
)
case self.opcodes.SESSIONS_INFO:
await self.auth_required(
userPhone, self.processors.sessions_info, payload, seq, writer, userPhone, hashedToken
)
case self.opcodes.CHAT_INFO:
await self.auth_required(
userPhone, self.processors.chat_info, payload, seq, writer, userId
)
case self.opcodes.OK_TOKEN:
await self.auth_required(
userPhone, self.processors.ok_token, payload, seq, writer
)
case self.opcodes.CONTACT_LIST:
await self.auth_required(
userPhone, self.processors.contact_list, payload, seq, writer, userId
)
case self.opcodes.PROFILE:
await self.processors.profile(
payload, seq, writer, userId=userId
)
case self.opcodes.CHAT_SUBSCRIBE:
await self.auth_required(
userPhone, self.processors.chat_subscribe, payload, seq, writer
)
case self.opcodes.CONFIG:
await self.auth_required(
userPhone, self.processors.update_config, payload, seq, writer, userPhone, hashedToken
)
case self.opcodes.CONTACT_UPDATE:
await self.auth_required(
userPhone, self.processors.contact_update, payload, seq, writer, userId
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone, self.processors.contact_presence, 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 userId:
await self._end_session(userId, address[0], address[1])
writer.close()
self.logger.info(f"Прекратил работать работать с клиентом {address[0]}:{address[1]}")
self.logger.info(f"Прекратил работать с клиентом {address[0]}:{address[1]}")
async def _finish_auth(self, writer, addr, phone, id):
"""Завершение открытия сессии"""

View File

@@ -91,6 +91,11 @@ class TamTamWS:
if userPhone:
await self._finish_auth(websocket, address, userPhone, userId)
case self.opcodes.LOGOUT:
await self.processors.logout(
seq, websocket, hashedToken=hashedToken
)
break
case self.opcodes.CONTACT_INFO:
await self.auth_required(
userPhone, self.processors.contact_info, payload, seq, websocket
@@ -99,6 +104,66 @@ class TamTamWS:
await self.auth_required(
userPhone, self.processors.chat_history, payload, seq, websocket, userId
)
case self.opcodes.ASSETS_UPDATE:
await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, websocket
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, websocket
)
case self.opcodes.MSG_SEND:
await self.auth_required(
userPhone, self.processors.msg_send, payload, seq, websocket, userId, self.db_pool
)
case self.opcodes.MSG_TYPING:
await self.auth_required(
userPhone, self.processors.msg_typing, payload, seq, websocket, userId
)
case self.opcodes.FOLDERS_GET:
await self.auth_required(
userPhone, self.processors.folders_get, payload, seq, websocket, userPhone
)
case self.opcodes.FOLDERS_UPDATE:
await self.auth_required(
userPhone, self.processors.folders_update, payload, seq, websocket, userPhone
)
case self.opcodes.SESSIONS_INFO:
await self.auth_required(
userPhone, self.processors.sessions_info, payload, seq, websocket, userPhone, hashedToken
)
case self.opcodes.CHAT_INFO:
await self.auth_required(
userPhone, self.processors.chat_info, payload, seq, websocket, userId
)
case self.opcodes.OK_TOKEN:
await self.auth_required(
userPhone, self.processors.ok_token, payload, seq, websocket
)
case self.opcodes.CONTACT_LIST:
await self.auth_required(
userPhone, self.processors.contact_list, payload, seq, websocket, userId
)
case self.opcodes.PROFILE:
await self.processors.profile(
payload, seq, websocket, userId=userId
)
case self.opcodes.CHAT_SUBSCRIBE:
await self.auth_required(
userPhone, self.processors.chat_subscribe, payload, seq, websocket
)
case self.opcodes.CONFIG:
await self.auth_required(
userPhone, self.processors.update_config, payload, seq, websocket, userPhone, hashedToken
)
case self.opcodes.CONTACT_UPDATE:
await self.auth_required(
userPhone, self.processors.contact_update, payload, seq, websocket, userId
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone, self.processors.contact_presence, payload, seq, websocket
)
case _:
self.logger.warning(f"Неизвестный опкод {opcode}")
except websockets.exceptions.ConnectionClosed: