Compare commits

..

51 Commits

Author SHA1 Message Date
Alexey Polyakov
c0e23840b5 maybe fixes 2026-05-14 17:30:57 +03:00
Alexey Polyakov
c5721f3f9e Вроде как нормальные заглушки под ассеты 2026-05-13 18:56:27 +03:00
Alexey Polyakov
03cffc24aa TT: регистрация через клиент 2026-05-13 18:42:00 +03:00
Alexey Polyakov
87f22a3feb MAX & TT: теперь полноценный чёрный список 2026-05-13 15:58:02 +03:00
zavolo
7d2e070d1f fix(chat history): фикс 2026-05-11 00:47:04 +03:00
zavolo
24b0123185 fix(chat history): фикс 2026-05-11 00:38:04 +03:00
zavolo
31844c7fa2 fix(chat history): фикс 2026-05-11 00:26:31 +03:00
zavolo
9b60b15538 fix(chat history): фикс 2026-05-11 00:11:24 +03:00
zavolo
0d91f6542e fix(chat history): фикс 2026-05-10 23:39:08 +03:00
zavolo
77d6ca8cc0 fix(chat history): фикс 2026-05-10 23:27:13 +03:00
zavolo
3bf8bc5770 fix(chat history): фикс 2026-05-10 23:21:07 +03:00
zavolo
861b75eb1c MAX: bootstrap-история в LOGIN — клиент перестал думать что всё уже синканулось
В ответе LOGIN сервер слал messages: {} и chatMarker: 0. Десктопный
клиент в этом случае считает, что локальная история уже синхронизирована
со старого запуска, и НЕ отправляет CHAT_HISTORY (49) при открытии чата.
В окне видно только lastMessage из chats[], а вся реальная переписка —
ничерта.

- src/common/tools.py: collect_bootstrap_history(chatIds, ...) —
  собирает карту {chatId: [последние N сообщений]}, в т.ч. избранное
  под клиентским id = senderId ^ senderId.
- src/oneme/processors/auth.py: подсовываем эту карту в
  payload.messages, chatMarker = текущее время вместо 0.
2026-05-10 22:27:42 +03:00
zavolo
fa0ed34adc MAX: история таки заработала — cid/link/reactionInfo обязательны в схеме
Десктопный MAX подключается через TCP (mobile-протокол) и парсит
msgpack по фиксированной схеме. Если в сообщении выпадает любое из
полей — клиент молча обрывает соединение. После 87cfc19 как раз
такие условные `if elements: ...` / `if link: ...` (а link и
reaction_info там всегда были `{}`, то есть falsy) вырезали поля
из ответа CHAT_HISTORY и MSG_SEND, чем и сломали историю.

- src/common/tools.py: новый build_message_dict() — единая сборка
  тела сообщения, где все поля (id, cid, time, type, sender, text,
  attaches, elements, reactionInfo, link) присутствуют ВСЕГДА.
  get_last_message переписан через него.
- src/oneme/processors/history.py: chat_history использует
  build_message_dict вместо ручной логики с условными if-ками.
- src/oneme/processors/messages.py: msg_send.bodyMessage теперь
  отдает cid / reactionInfo / link даже пустыми и приводит id
  к int для mobile, str для web.

Цепная польза: auth.py LOGIN bootstrap (через generate_chats →
get_last_message) и search.py тоже теперь шлют корректную схему.
2026-05-10 22:17:18 +03:00
Alexey Polyakov
87cfc1932e Попытка починить историю (спойлер, нихуя не получилось) 2026-05-10 19:04:51 +03:00
Alexey Polyakov
17245f44d0 Фикс сборки избранного 2026-05-09 18:55:49 +03:00
Alexey Polyakov
b1a37bfa24 update sql scheme 2026-05-09 18:54:53 +03:00
Alexey Polyakov
d81eec5532 Генерируем айди пользователей рандомно (опять, да) 2026-05-09 18:16:32 +03:00
Alexey Polyakov
ddb810589f MAX: исправление уязвимости с избранными 2026-05-09 17:55:39 +03:00
Alexey Polyakov
dff6937da8 MAX: добавление контакта по номеру телефона 2026-05-09 15:50:47 +03:00
Alexey Polyakov
ac40cc53c9 MAX && TT: обновление контакта 2026-05-08 16:15:33 +03:00
Alexey Polyakov
756956d8a0 TT: тоже самое что в макс 2026-05-08 16:04:03 +03:00
Alexey Polyakov
00071c80be oops 2026-05-08 15:58:29 +03:00
Alexey Polyakov
a045457128 MAX: блокировка и разблокировка контакта 2026-05-08 15:57:24 +03:00
Alexey Polyakov
4d51c70f8e Вынес sqlite в отдельный модуль 2026-05-08 15:27:05 +03:00
Alexey Polyakov
2d3b9285bf MAX: теперь для избранного не сравниваем айди с нулём 2026-05-08 15:24:49 +03:00
Alexey Polyakov
6bb0d52419 nothing 2026-05-06 21:27:01 +03:00
Alexey Polyakov
911008c0a1 oops 2026-05-06 20:46:22 +03:00
Alexey Polyakov
b8472821eb MAX: web fix 2 2026-05-06 20:42:33 +03:00
Alexey Polyakov
f1c1639d9f MAX: web fix 2026-05-06 20:33:09 +03:00
Alexey Polyakov
7426e83914 nothing 2026-05-06 19:34:14 +03:00
Alexey Polyakov
8dc3ef1731 MAX: Почистил серверный конфиг 2026-05-06 17:40:11 +03:00
Alexey Polyakov
f1ff4fd062 MAX && TT: общение в таме, и корректировки под веб морду 2026-05-06 15:58:27 +03:00
Alexey Polyakov
0b6eda6178 TG Bot: fix username 2026-05-06 15:53:14 +03:00
Alexey Polyakov
02df98cdbd TG Bot: fix 2026-05-06 15:43:30 +03:00
Alexey Polyakov
49d73200b0 fix 2026-05-06 15:39:22 +03:00
Alexey Polyakov
389a08ebce nothing 2026-05-06 15:35:23 +03:00
Alexey Polyakov
613e1b96cd Решение проблемы обработки sigterm 2026-05-06 15:25:07 +03:00
Alexey Polyakov
0f2d946b98 TG Bot: возможность включения белого списка 2026-05-06 00:51:56 +03:00
Alexey Polyakov
1ff974dfce oops 2026-05-05 23:08:02 +03:00
Alexey Polyakov
bcd94b3a57 TT: ну вроде шире поддержка, а вообще обратная совместимость с максом клас 2026-05-05 23:06:50 +03:00
Alexey Polyakov
89f1fefa31 MAX & TT: теперь в качестве страны локации используется настоящая страна пользователя, а также зафиксировал версии библиотек в зависимостях 2026-04-28 18:22:16 +03:00
Alexey Polyakov
c716520ca4 MAX: добавление/удаление контактов, статусы 2026-04-28 06:56:29 +03:00
Alexey Polyakov
ff46e417f4 MAX: oops 2026-04-27 17:41:51 +03:00
Alexey Polyakov
bd95755db4 MAX: создание папок 2026-04-27 17:40:28 +03:00
Alexey Polyakov
cb058f6ab6 Теперь ID пользователей идут по порядку, как и раньше 2026-04-25 12:10:44 +03:00
Alexey Polyakov
cd9ba981ae MAX: отдача черного списка 2026-04-24 23:31:55 +03:00
Alexey Polyakov
810d480dbd MAX: фикс версий < 25.8.0 2026-04-24 21:30:02 +03:00
Alexey Polyakov
227f90c3c3 MAX: Рефактор папок 2026-04-24 20:54:28 +03:00
Alexey Polyakov
56133416e3 MAX: пуши через firebase (особо не тестил, вроде работает) 2026-04-24 19:46:08 +03:00
Alexey Polyakov
35a4101608 MAX: обновление настроек приватности 2026-04-24 17:17:33 +03:00
Alexey Polyakov
9fcba1af86 MAX: Рабочие баннеры 2026-04-24 15:51:01 +03:00
45 changed files with 3972 additions and 859 deletions

View File

@@ -24,6 +24,9 @@ domain = "openmax.su"
avatar_base_url = "http://127.0.0.1/avatar/"
telegram_bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
telegram_bot_enabled = "1"
telegram_whitelist_enabled = "1"
telegram_whitelist_ids = "1,2,3"
origins="http://127.0.0.1,https://web.openmax.su"
sms_gateway_url = "http://127.0.0.1:8100/sms-gateway"
sms_gateway_url = "http://127.0.0.1:8100/sms-gateway"
firebase_credentials_path = ""
geo_db_path = ""

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@ __pycache__
.env
*.pem
*.sqlite
*.crt
*.crt
*-adminsdk-*.json
*.mmdb

View File

@@ -22,3 +22,26 @@
2. Открываем консоль в той же директории и производим декомпиляцию: `apktool d <имя apk> -o max`
3. Заходим в папку проекта и заменяем во всех классах "api.oneme.ru" на свой адрес сервера
4. Производим повторную сборку с помощью команды: `apktool b max -o max_modified.apk`
---
# Патчинг Firebase для push-уведомлений
> [!Important]
> Без замены Firebase-конфига пуши от вашего сервера не будут работать.
1. Создайте проект в [Firebase Console](https://console.firebase.google.com/) и добавьте Android-приложение с пакетом `ru.oneme.app`
2. Скачайте `google-services.json`
3. В декомпилированном APK откройте `res/values/strings.xml` и замените следующие строки на значения из вашего `google-services.json`:
| Строка | Оригинал | Откуда взять |
|---|---|---|
| `google_api_key` | `AIzaSyABuDYeeDXIOrKTXLkUj30Ii143ofPe63Q` | `client[0].api_key[0].current_key` |
| `google_app_id` | `1:659634599081:android:9605285443b661167225b8` | `client[0].client_info.mobilesdk_app_id` |
| `gcm_defaultSenderId` | `659634599081` | `project_info.project_number` |
| `project_id` | `max-messenger-app` | `project_info.project_id` |
| `google_crash_reporting_api_key` | `AIzaSyABuDYeeDXIOrKTXLkUj30Ii143ofPe63Q` | `client[0].api_key[0].current_key` |
| `google_storage_bucket` | `max-messenger-app.firebasestorage.app` | `project_info.storage_bucket` |
4. Соберите и подпишите APK
5. В настройках проекта Firebase создайте сервисный аккаунт и укажите путь в `.env`

View File

@@ -1,10 +1,12 @@
aiogram
aiomysql
msgpack
lz4
websockets
pydantic
aiosqlite
aiohttp
python-dotenv
cryptography
aiogram==3.26.0
aiomysql==0.3.2
msgpack==1.1.2
lz4==4.4.5
websockets==16.0
pydantic==2.12.5
aiosqlite==0.22.1
aiohttp==3.13.5
python-dotenv==1.2.2
cryptography==46.0.6
firebase-admin==7.4.0
geoip2==5.2.0

View File

@@ -43,11 +43,18 @@ class ServerConfig:
### 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_bot_enabled = bool(int(os.getenv("telegram_bot_enabled", 0)))
telegram_whitelist_ids = [x.strip() for x in os.getenv("telegram_whitelist_ids", "").split(",") if x.strip()]
telegram_whitelist_enabled = bool(int(os.getenv("telegram_whitelist_enabled", 0)))
### origins
origins = [x.strip() for x in os.getenv("origins", "").split(",") if x.strip()] if os.getenv("origins") else None
### sms шлюз
sms_gateway_url = os.getenv("sms_gateway_url", "")
### Firebase
firebase_credentials_path = os.getenv("firebase_credentials_path", "")
### Путь к гео бд
geo_db_path = os.getenv("geo_db_path", "")

View File

@@ -44,6 +44,7 @@ class Opcodes:
CONTACT_MUTUAL = 38
CONTACT_PHOTOS = 39
CONTACT_SORT = 40
CONTACT_ADD_BY_PHONE = 41
CONTACT_VERIFY = 42
REMOVE_CONTACT_PHOTO = 43
CHAT_INFO = 48

92
src/common/push.py Normal file
View File

@@ -0,0 +1,92 @@
import asyncio
import logging
import time
import firebase_admin
from firebase_admin import credentials, messaging
class PushService:
def __init__(self, credentials_path):
self.logger = logging.getLogger(__name__)
if not credentials_path:
self.logger.warning("Огненная база сегодня не работает, укажите путь к файлу с ключами")
self.enabled = False
return
cred = credentials.Certificate(credentials_path)
firebase_admin.initialize_app(cred)
self.enabled = True
self.logger.info("Огненная база инициализирована")
async def send(self, push_token, data):
"""Отправка пуша"""
if not self.enabled:
return None
str_data = {k: str(v) for k, v in data.items() if v is not None}
message = messaging.Message(
data=str_data,
token=push_token,
android=messaging.AndroidConfig(
priority="high",
),
)
try:
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, messaging.send, message)
self.logger.debug(f"Отправил пуш: {response}")
return response
except messaging.UnregisteredError:
self.logger.warning(f"Пуш-токен не зарегистрирован: {push_token}")
return None
except Exception as e:
self.logger.error(f"Не удалось отправить пуш: {e}")
return None
async def send_to_user(self, db_pool, phone, sender_id=None, msg_id=None,
chat_id=None, text="", is_group=False):
"""Отправка пушей на все устройства пользователя"""
if not self.enabled:
return
# Получаем имя отправителя
user_name = ""
if sender_id:
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT firstname, lastname FROM users WHERE id = %s",
(sender_id,)
)
sender = await cursor.fetchone()
if sender:
firstname = sender.get("firstname", "")
lastname = sender.get("lastname", "")
user_name = f"{firstname} {lastname}".strip()
now_ms = str(int(time.time() * 1000))
msg_type = "ChatMessage" if is_group else "Message"
data = {
"type": msg_type,
"msgid": str(msg_id) if msg_id else "0",
"suid": str(sender_id) if sender_id else None,
"mc": str(chat_id) if chat_id else None,
"msg": text,
"userName": user_name,
"ttime": now_ms,
"ctime": now_ms,
}
# Получаем все пуш-токены пользователя
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT push_token FROM tokens WHERE phone = %s AND push_token IS NOT NULL",
(phone,)
)
rows = await cursor.fetchall()
for row in rows:
await self.send(row["push_token"], data)

View File

@@ -5,14 +5,20 @@ class SQLQueries:
SELECT_USER_BY_TG_ID = "SELECT * FROM users WHERE telegram_id = %s"
INSERT_USER = """
INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
INSERT_USER_DATA = """
INSERT INTO user_data
(phone, folders, user_config, chat_config)
VALUES (%s, %s, %s, %s)
"""
(phone, user_config, chat_config)
VALUES (%s, %s, %s)
"""
INSERT_DEFAULT_FOLDER = """
INSERT INTO user_folders
(id, phone, title, sort_order)
VALUES ('all.chat.folder', %s, 'Все', 0)
"""

63
src/common/sqlite.py Normal file
View File

@@ -0,0 +1,63 @@
class SQLiteCursorCompat:
def __init__(self, connection):
self.connection = connection
self.cursor = None
async def __aenter__(self):
self.cursor = await self.connection.cursor()
return self
async def __aexit__(self, exc_type, exc, tb):
if self.cursor is not None:
await self.cursor.close()
self.cursor = None
@property
def lastrowid(self):
return None if self.cursor is None else self.cursor.lastrowid
def _normalize_query(self, query):
return query.replace("%s", "?").replace(
"UNIX_TIMESTAMP()", "CAST(strftime('%s','now') AS INTEGER)"
)
async def execute(self, query, params=()):
normalized_query = self._normalize_query(query)
if params is None:
params = ()
elif not isinstance(params, (tuple, list, dict)):
params = (params,)
await self.cursor.execute(normalized_query, params)
async def fetchone(self):
row = await self.cursor.fetchone()
if row is None:
return None
return dict(row)
async def fetchall(self):
rows = await self.cursor.fetchall()
return [dict(row) for row in rows]
class SQLiteConnectionCompat:
def __init__(self, connection):
self.connection = connection
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
async def commit(self):
await self.connection.commit()
def cursor(self):
return SQLiteCursorCompat(self.connection)
class SQLitePoolCompat:
def __init__(self, connection):
self.connection = connection
def acquire(self):
return SQLiteConnectionCompat(self.connection)

View File

@@ -13,6 +13,9 @@ class Static:
CHAT_NOT_FOUND = "chat_not_found"
CHAT_NOT_ACCESS = "chat_not_access"
RATE_LIMITED = "rate_limited"
CONTACT_NOT_FOUND = "contact_not_found"
CONTACT_ALREADY_ADDED = "contact_already_added"
CONTACT_BLOCKED = "contact_blocked"
class ChatTypes:
DIALOG = "DIALOG"
@@ -80,7 +83,25 @@ class Static:
"error": "error.rate_limited",
"message": "Too many attempts. Please try again later",
"title": "Слишком много попыток"
}
},
"contact_not_found": {
"localizedMessage": "Контакт не найден",
"error": "contact.not.found",
"message": "Contact not found",
"title": "Контакт не найден"
},
"contact_already_added": {
"localizedMessage": "Контакт уже добавлен",
"error": "contact.already.added",
"message": "Contact already added",
"title": "Контакт уже добавлен"
},
"contact_blocked": {
"localizedMessage": "Вы не можете написать этому пользователю",
"error": "contact.blocked",
"message": "Contact is blocked",
"title": "Вы не можете написать этому пользователю"
},
}
### Сообщения бота
@@ -196,25 +217,6 @@ class Static:
]},
]
### Заглушка для папок
ALL_CHAT_FOLDER = [{
"id": "all.chat.folder",
"title": "Все",
"filters": [],
"updateTime": 0,
"options": [],
"sourceId": 1
}]
ALL_CHAT_FOLDER_ORDER = ["all.chat.folder"]
### Стандартные папки с настройками пользователя
USER_FOLDERS = {
"folders": [],
"foldersOrder": [],
"allFilterExcludeFolders": []
}
USER_SETTINGS = {
"CHATS_PUSH_NOTIFICATION": "ON",
"PUSH_DETAILS": True,

View File

@@ -1,26 +1,55 @@
import hashlib
import json
import random
import secrets
import time
import geoip2.database
class Tools:
def __init__(self):
pass
def build_message_dict(self, row, protocol_type="mobile"):
"""Сборка тела сообщения"""
try:
attaches = json.loads(row.get("attaches") or "[]")
except (TypeError, ValueError):
attaches = []
try:
elements = json.loads(row.get("elements") or "[]")
except (TypeError, ValueError):
elements = []
message = {
"id": row.get("id") if protocol_type == "mobile" else str(row.get("id")),
"cid": int(row.get("cid") or 0),
"chatId": int(row.get("chat_id") or 0),
"time": int(row.get("time")),
"type": row.get("type") or "USER",
"sender": row.get("sender"),
"text": row.get("text") or "",
"attaches": attaches,
"elements": elements,
"reactionInfo": {},
"link": {}
}
return message
def generate_profile(
self,
id=1,
phone=70000000000,
id=None,
phone=None,
avatarUrl=None,
photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
updateTime=None,
firstName=None,
lastName=None,
options=None,
description=None,
accountStatus=0,
profileOptions=[],
accountStatus=None,
profileOptions=None,
includeProfileOptions=True,
username=None,
@@ -44,6 +73,8 @@ class Tools:
],
"options": options,
"accountStatus": accountStatus,
"location": "RU",
"registrationTime": int(time.time() * 1000)
}
if avatarUrl:
@@ -77,22 +108,40 @@ class Tools:
def generate_profile_tt(
self,
id=1,
phone=70000000000,
id=None,
phone=None,
avatarUrl=None,
photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
updateTime=None,
firstName=None,
lastName=None,
options=None,
description=None,
username=None,
custom_firstname=None,
custom_lastname=None,
blocked=None
):
# Так как TT не поддерживает фамилию, и если нам ее не передали в функцию
# то используем только имя, чтобы избежать None в фамилии
if firstName and lastName:
name = f"{firstName} {lastName}"
else:
name = firstName
# Используем такой же костыль, как и выше
if custom_firstname and custom_lastname:
custom_name = f"{custom_firstname} {custom_lastname}"
elif custom_firstname:
custom_name = custom_firstname
else:
custom_name = None
contact = {
"id": id,
"updateTime": updateTime,
"phone": phone,
"names": [{"name": f"{firstName} {lastName}", "type": "TT"}],
"names": [{"name": name, "type": "TT"}],
"options": options,
}
@@ -104,8 +153,19 @@ class Tools:
if description:
contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username:
contact["link"] = "https://tamtam.chat/" + username
contact["link"] = "https://tt.me/" + username
if custom_firstname:
contact["names"].append(
{"name": custom_name, "type": "CUSTOM"}
)
if blocked:
contact["status"] = "BLOCKED"
return contact
@@ -203,27 +263,28 @@ class Tools:
if include_favourites:
# Получаем последнее сообщение из избранного
favouriteChatId = -senderId
message, messageTime = await self.get_last_message(
senderId, db_pool, protocol_type=protocol_type
favouriteChatId, db_pool, protocol_type=protocol_type
)
# ID избранного
# ID избранного для клиента
chatId = senderId ^ senderId
# Получаем последнюю активность участника (отправителя) в избранном
# Получаем последнюю активность в избранном
participants = await self.get_participant_last_activity(
senderId, [senderId], db_pool
favouriteChatId, [senderId], db_pool
)
# Получаем ID предыдущего сообщения для избранного (чат ID = senderId)
# Получаем ID предыдущего сообщения для избранного
prevMessageId = await self.get_previous_message_id(
senderId, db_pool, protocol_type=protocol_type
favouriteChatId, db_pool, protocol_type=protocol_type
)
# Хардкодим в лист чатов избранное
chats.append(
self.generate_chat(
chatId if protocol_type == "mobile" else str(chatId),
chatId,
senderId,
"DIALOG",
participants,
@@ -334,7 +395,7 @@ class Tools:
async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor:
await cursor.execute(
"SELECT * FROM `contacts` WHERE owner_id = %s",
"SELECT * FROM `contacts` WHERE owner_id = %s AND is_blocked = FALSE",
(owner_id,),
)
rows = await cursor.fetchall()
@@ -367,9 +428,11 @@ class Tools:
row = await cursor.fetchone() or {}
last_message_id = row.get("id") or 0 # последнее id сообщения в чате
message_id = self.generate_id()
message_time = int(time.time() * 1000) # время отправки сообщения
# Генерируем ID сообщения
message_id = int(time.time() * 1000000) * 1000 + random.randint(100, 999)
# Вносим новое сообщение в таблицу
await cursor.execute(
"INSERT INTO `messages` (id, chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
@@ -405,23 +468,8 @@ class Tools:
if not row:
return None, None
# Собираем сообщение
message = {
"id": row.get("id")
if protocol_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": {},
}
# Возвращаем
return message, int(row.get("time"))
return self.build_message_dict(row, protocol_type), int(row.get("time"))
async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
"""Получение ID предыдущего сообщения (второго с конца) в чате."""
@@ -490,16 +538,103 @@ class Tools:
if userPhone:
await coro(*args)
def generate_id(self):
# Получаем время в юниксе
timestamp = int(time.time())
async def update_user_config(self, cursor, phone, user_settings, default_settings):
"""Функция для обновления юзер конфига из бд в случае его изменения"""
# Генерируем дополнительно рандомное число
random_number = random.randint(0, 9999)
user_config = json.loads(user_settings)
updated_config = {**default_settings, **user_config}
# Собираем их вместе и вычисляем хеш
combined = f"{timestamp}{random_number}".encode()
unique_id = int(hashlib.md5(combined).hexdigest(), 16) % 1000000000
if updated_config != user_config:
await cursor.execute(
"UPDATE user_data SET user_config = %s WHERE phone = %s",
(json.dumps(updated_config), phone),
)
# Возвращаем
return unique_id
return updated_config
async def collect_presence(self, contact_ids, clients, db_pool):
"""Собирает статусы пользователей"""
now = int(time.time())
presence = {}
# Список тех, кого нужно поискать в базе данных
db_lookup_ids = []
# Проходимся по всем айдишникам,
# которые передал нам клиент
for contact_id in contact_ids:
contact_id = int(contact_id)
client = clients.get(contact_id)
# Если пользователь онлайн
if client and client.get("status") == 2:
presence[str(contact_id)] = {"seen": now, "status": 2}
# Если пользователь подключен,
# но не взаимодействует с клиентом
elif client and client.get("last_seen"):
presence[str(contact_id)] = {"seen": client.get("last_seen")}
# А если никакое условие не подошло, то добавляем его в лист,
# а позже посмотрим в базе данных
else:
db_lookup_ids.append(contact_id)
# Проходимся по листу и добавляем недостающих,
# если такие существуют конечно
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
for contact_id in db_lookup_ids:
await cursor.execute(
"SELECT lastseen FROM users WHERE id = %s",
(contact_id,)
)
row = await cursor.fetchone()
if row:
lastseen = row.get("lastseen")
presence[int(contact_id)] = {"seen": int(lastseen)}
return presence
def get_geo(self, ip, db_path):
"""
Получение страны пользователя по его айпи адресу
Используется во время запуска сессии
"""
try:
with geoip2.database.Reader(db_path) as reader:
response = reader.country(ip)
return response.country.name or "Localhost Federation"
except Exception:
return "Localhost Federation"
async def generate_user_id(self, db_pool):
"""Генерация id пользователя"""
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
while True:
user_id = secrets.randbelow(2_147_383_647) + 100_000
await cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
if not await cursor.fetchone():
return user_id
async def contact_is_blocked(self, owner_id, contact_id, db_pool):
"""
По изначальной задумке, данная функция должна проверять, заблокирован ли контакт
На сервере долгое время не был доделан черный список, хотя управление им было реализовано
(на деле, это я поленился)
Вернёт вам true, если контакт заблокирован, иначе false
"""
# Проверяем наличие контакта
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s AND is_blocked = %s", (owner_id, contact_id, True))
row = await cursor.fetchone()
# Есди контакт существует и заблокирован, возвращаем true,
if row:
return True
else: # в ином случае false
return False

View File

@@ -1,9 +1,14 @@
# Импортирование библиотек
import asyncio
import logging
import signal
import ssl
import sys
import traceback
from common.config import ServerConfig
from common.push import PushService
from common.sqlite import SQLitePoolCompat
from oneme.controller import OnemeController
from tamtam.controller import TTController
from telegrambot.controller import TelegramBotController
@@ -11,71 +16,6 @@ from telegrambot.controller import TelegramBotController
# Конфиг сервера
server_config = ServerConfig()
class SQLiteCursorCompat:
def __init__(self, connection):
self.connection = connection
self.cursor = None
async def __aenter__(self):
self.cursor = await self.connection.cursor()
return self
async def __aexit__(self, exc_type, exc, tb):
if self.cursor is not None:
await self.cursor.close()
self.cursor = None
@property
def lastrowid(self):
return None if self.cursor is None else self.cursor.lastrowid
def _normalize_query(self, query):
return query.replace("%s", "?").replace(
"UNIX_TIMESTAMP()", "CAST(strftime('%s','now') AS INTEGER)"
)
async def execute(self, query, params=()):
normalized_query = self._normalize_query(query)
if params is None:
params = ()
elif not isinstance(params, (tuple, list, dict)):
params = (params,)
await self.cursor.execute(normalized_query, params)
async def fetchone(self):
row = await self.cursor.fetchone()
if row is None:
return None
return dict(row)
async def fetchall(self):
rows = await self.cursor.fetchall()
return [dict(row) for row in rows]
class SQLiteConnectionCompat:
def __init__(self, connection):
self.connection = connection
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
def cursor(self):
return SQLiteCursorCompat(self.connection)
class SQLitePoolCompat:
def __init__(self, connection):
self.connection = connection
def acquire(self):
return SQLiteConnectionCompat(self.connection)
async def init_db():
"""Инициализация базы данных"""
@@ -113,7 +53,6 @@ def init_ssl():
# Возвращаем
return ssl_context
def set_logging():
"""Настройка уровня логирования"""
# Настройка уровня логирования
@@ -130,14 +69,33 @@ def set_logging():
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 = {}
push_service = PushService(server_config.firebase_credentials_path)
async def api_event(target, eventData):
target_clients = api.get("clients", {}).get(target, {}).get("clients", [])
for client in target_clients:
await controllers[client["protocol"]].event(target, client, eventData)
# Если у пользователя нет активных подключений
# и это новое сообщение - отсылаем пуш
if not target_clients and eventData.get("eventType") == "new_msg":
message = eventData.get("message", {})
sender_id = message.get("sender")
text = message.get("text", "")
chat_id = eventData.get("chatId", "")
msg_id = message.get("id", 0)
await push_service.send_to_user(
db, target,
sender_id=sender_id,
msg_id=msg_id,
chat_id=chat_id,
text=text,
)
api = {
"db": db,
@@ -155,11 +113,39 @@ async def main():
api["telegram_bot"] = controllers["telegrambot"]
tasks = [controller.launch(api) for controller in controllers.values()]
loop = asyncio.get_running_loop()
running_tasks = []
def _shutdown(sig):
logging.info(f"Получен сигнал {sig}, завершаем все задачи...")
for task in running_tasks:
task.cancel()
if sys.platform != "win32":
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, _shutdown, sig)
coros = [controller.launch(api) for controller in controllers.values()]
running_tasks.extend(asyncio.create_task(coro) for coro in coros)
# Запускаем контроллеры
await asyncio.gather(*tasks)
try:
await asyncio.gather(*running_tasks)
except asyncio.CancelledError:
logging.info("Все задачи завершены")
except Exception as e:
logging.error(
f"Произошла неизвестная ошибка: {e}"
)
traceback.print_exc()
finally:
if hasattr(db, 'close'):
db.close()
await db.wait_closed()
elif hasattr(db, 'connection') and hasattr(db.connection, 'close'):
await db.connection.close()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -3,120 +3,334 @@ class OnemeConfig:
pass
SERVER_CONFIG = {
"async-tracer": 0,
"presence-ttl": 300,
"non-contact-sync-time": 86400,
"contact-batching-variant": 0,
"account-nickname-enabled": True,
"web-ad-banner": {
"enabled": False
"account-nickname-enabled": False,
"account-removal-enabled": False,
"anr-config": {
"enabled": True,
"timeout": {
"low": 5000,
"avg": 5000,
"high": 5000
}
},
"edit-timeout": 0,
"reactions-menu": [],
"invite-long": "",
"calls-endpoint": "",
"calls-test-domain": "",
"max-readmarks": 100,
"max-cname-length": 200,
"max-description-length": 400,
"new-avatar-gradient-colors-enabled": True,
"max-msg-length": 4000,
"file-upload-unsupported-types": [],
"file-upload-max-size": 4294967296,
"image-quality": 0.8,
"image-width": 1920,
"image-height": 1920,
"image-size": 10000000,
"max-favorite-chats": 5,
"bot-complaint-enabled": True,
"reactions-max": 8,
"welcome-sticker-ids": [],
"edit-chat-type-screen-enabled": True,
"edit-channel-type-screen-enabled": True,
"esia-verify-botId": 0,
"official-org": False,
"esia-enabled": False,
"calls-debug-mode": False,
"channels-suggests-folder": True,
"delete-msg-fys-large-chat-disabled": False,
"calls-web-download-logs": False,
"calls-web-upload-logs": False,
"calls-video-zoom": False,
"calls-fullscreen-mode": False,
"group-call-part-limit": 100,
"call-chat-members-load-config": {},
"cfs": False,
"cse": False,
"calls-hotkeys": True,
"gc-link-pre-settings": False,
"gc-from-p2p": False,
"call-rate": {},
"channels-enabled": True,
"max-participants": 20000,
"max-added-participants": 100,
"saved-messages-aliases": [],
"author-visibility-forward-enabled": False,
"official-bot-naming-enabled": False,
"search-webapps-showcase": {
"items": []
},
"settings-entry-banners": [],
"settings-business": "https://telegram.org/blog/telegram-business",
"appearance-multi-theme-screen-enabled": True,
"moscow-theme-enabled": True,
"creation-2fa-config": {
"enabled": False,
"pass_min_len": 6,
"pass_max_len": 64,
"hint_max_len": 30
"audio-transcription-locales": [],
"available-complaints": [
"FAKE",
"SPAM",
"PORNO",
"EXTREMISM",
"THREAT",
"OTHER"
],
"avatars-screen-enabled": True,
"bad-networ-indicator-config": {
"signalingConfig": {
"dcReportNetworkStatEnabled": False
}
},
"lebedev-theme-enabled": True,
"quotes-enabled": True,
"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": "",
"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,
"reactions-settings-enabled": True,
"channel-statistics-botid": 0,
"enable-unknown-contact-bottom-sheet": 0,
"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,
"family-protection-botid": 0,
"new-year-theme-2026": True,
"inline-ev-player": True,
"invalidate-db-msg-exception": True,
"invite-friends-sheet-frequency": [
2,
7
],
"invite-link": "",
"invite-long": "",
"invite-short": "",
"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,
"scheduled-faves-enabled": True,
"non-contact-complaints-enabled": True,
"join-requests": True,
"web-persistent-cache": False,
"create-channel-type-screen": True,
"search-webapps-showcase": {
"items": []
},
"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": [],
"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": [],
"webapp-push-open": True,
"webview-cache-enabled": False,
"welcome-sticker-ids": [],
"white-list-links": [],
"february-23-26-theme": True,
"march-8-26-theme": True,
"audio-play-cmd": False,
"audio-play-opus": False,
"bots-channel-adding": True,
"stickers-botid": 0,
"sticker-set-edit-enabled": True,
"calls-new-history-enabled": True,
"wm-analytics-enabled": True,
"wm-workers-limit": 80,
"wud": False,
"y-map": {
"tile": "",
"geocoder": "",
"static": ""
"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"
},
"enable-audio-messages-transcription": True,
"enable-video-messages-transcription": True,
"retry-transcribe-attempt": 5,
"retry-transcribe-timeout": 2000,
"org-profile": False,
"media-not-ready-retry-delay": 2000,
"polls-in-chats": True,
"polls-in-channels": True,
"render-polls": True,
"poll-ttl": {
"chat": 5000,
"bigchat": 15000,
"channel": 25000
},
"new-collage": False,
"channel-profile-invite-link": False,
"rename-profile-to-settings": True,
"live-streams": True
}
"has-phone": True
}

View File

@@ -10,13 +10,19 @@ from common.opcodes import Opcodes
class OnemeController(ControllerBase):
def __init__(self):
self.config = ServerConfig()
self.proto = MobileProto()
self.proto_tcp = MobileProto()
self.proto_web = WebProto()
self.opcodes = Opcodes()
async def event(self, target, client, eventData):
# Извлекаем тип события и врайтер
eventType = eventData.get("eventType")
writer = client.get("writer")
is_web = client.get("type") == "web"
# Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе
if writer == eventData.get("writer"):
@@ -41,7 +47,7 @@ class OnemeController(ControllerBase):
}
# Создаем пакет
packet = self.proto.pack_packet(
packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_MESSAGE, payload=payload
)
elif eventType == "typing":
@@ -58,7 +64,7 @@ class OnemeController(ControllerBase):
}
# Создаем пакет
packet = self.proto.pack_packet(
packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_TYPING, payload=payload
)
elif eventType == "profile_updated":
@@ -71,13 +77,32 @@ class OnemeController(ControllerBase):
}
# Создаем пакет
packet = self.proto.pack_packet(
packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PROFILE, payload=payload
)
elif eventType == "presence":
userId = eventData.get("userId")
presence = eventData.get("presence")
event_time = eventData.get("time")
# Отправляем пакет
writer.write(packet)
await writer.drain()
payload = {
"userId": userId,
"presence": presence,
"time": event_time
}
packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
)
if not packet:
return
if is_web:
await writer.send(packet)
else:
writer.write(packet)
await writer.drain()
def launch(self, api):
async def _start_all():
@@ -94,10 +119,11 @@ class OnemeController(ControllerBase):
OnemeWS(
host=self.config.host,
port=self.config.oneme_ws_port,
clients=api['clients'],
ssl_context=api['ssl'],
db_pool=api['db'],
send_event=api['event']
clients=api['clients'],
send_event=api['event'],
telegram_bot=api.get('telegram_bot'),
).start()
)

View File

@@ -54,6 +54,32 @@ class AssetsPayloadModel(pydantic.BaseModel):
sync: int
type: str
class AssetsGetPayloadModel(pydantic.BaseModel):
type: str
count: int = 100
query: str = None
class AssetsGetByIdsPayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsAddPayloadModel(pydantic.BaseModel):
type: str
id: int = None
class AssetsRemovePayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsMovePayloadModel(pydantic.BaseModel):
type: str
id: int
position: int
class AssetsListModifyPayloadModel(pydantic.BaseModel):
type: str
ids: list
class GetCallHistoryPayloadModel(pydantic.BaseModel):
forward: bool
count: int
@@ -75,6 +101,12 @@ class SendMessagePayloadModel(pydantic.BaseModel):
class SyncFoldersPayloadModel(pydantic.BaseModel):
folderSync: int
class CreateFolderPayloadModel(pydantic.BaseModel):
id: str
title: str
filters: list = []
include: list = []
class SearchChatsPayloadModel(pydantic.BaseModel):
chatIds: list
@@ -130,4 +162,21 @@ class ChatHistoryPayloadModel(pydantic.BaseModel):
class ChatSubscribePayloadModel(pydantic.BaseModel):
chatId: int
subscribe: bool
subscribe: bool
class ContactListPayloadModel(pydantic.BaseModel):
status: str
count: int = None
class ContactPresencePayloadModel(pydantic.BaseModel):
contactIds: list
class ContactAddByPhonePayloadModel(pydantic.BaseModel):
phone: str
firstName: str
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int
firstName: str = None
lastName: str = None

View File

@@ -2,7 +2,8 @@ from .assets import AssetsProcessors
from .auth import AuthProcessors
from .calls import CallsProcessors
from .chats import ChatsProcessors
from .complains import ComplainsProcessors
from .complaints import ComplaintsProcessors
from .contacts import ContactsProcessors
from .folders import FoldersProcessors
from .history import HistoryProcessors
from .main import MainProcessors
@@ -15,7 +16,8 @@ class Processors(
AuthProcessors,
CallsProcessors,
ChatsProcessors,
ComplainsProcessors,
ComplaintsProcessors,
ContactsProcessors,
FoldersProcessors,
HistoryProcessors,
MainProcessors,

View File

@@ -1,31 +1,167 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import AssetsPayloadModel
from oneme.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
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 = {
"sections": [],
"sync": int(time.time() * 1000)
response = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"stickersOrder": [
"RECENT",
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
)
await self._send(writer, packet)
# Отправляем
await self._send(writer, packet)
async def assets_get(self, payload, seq, writer):
try:
data = AssetsGetPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": [], "marker": None}
else:
response = {"stickers": [], "marker": None}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET, payload=response
)
await self._send(writer, packet)
async def assets_get_by_ids(self, payload, seq, writer):
try:
data = AssetsGetByIdsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET_BY_IDS, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": []}
else:
response = {"stickers": []}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET_BY_IDS, payload=response
)
await self._send(writer, packet)
async def assets_add(self, payload, seq, writer):
try:
AssetsAddPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_ADD, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_ADD, payload={}
)
await self._send(writer, packet)
async def assets_remove(self, payload, seq, writer):
try:
AssetsRemovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_REMOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_REMOVE, payload={}
)
await self._send(writer, packet)
async def assets_move(self, payload, seq, writer):
try:
AssetsMovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_MOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_MOVE, payload={}
)
await self._send(writer, packet)
async def assets_list_modify(self, payload, seq, writer):
try:
AssetsListModifyPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_LIST_MODIFY, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_LIST_MODIFY, payload={}
)
await self._send(writer, packet)

View File

@@ -16,7 +16,6 @@ from oneme.models import (
VerifyCodePayloadModel,
)
class AuthProcessors(BaseProcessor):
def __init__(
self,
@@ -30,17 +29,60 @@ class AuthProcessors(BaseProcessor):
self.server_config = OnemeConfig().SERVER_CONFIG
self.telegram_bot = telegram_bot
def _check_legacy_version(self, app_version):
"""
Функция определения легаси версий клиентов,
для которых потребуются некоторые корректировки ответов сервера
Сейчас данная функция используется для форматирования ответов
под версии ниже 25.8.0
Функция вернет True, если версия слишком старая,
или False в противном случае
[fun fact] С 25.8.0, похоже, начали корректировать протокол, поскольку это
самая старая версия, которая работала без всяких корректировок сервера
"""
return tuple(int(v) for v in app_version.split(".")) < (25, 8, 0)
async def _send_banners(self, writer):
"""Функция отправки баннеров клиенту"""
# Итоговый список баннеров для отдачи клиенту
banners = []
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Собираем все баннеры, которые есть в бд
await cursor.execute(
"SELECT * FROM banners WHERE enabled = TRUE"
)
rows = await cursor.fetchall()
# Добавляем каждый баннер в лист
for row in rows:
banner = {
"description": row.get("description"),
"title": row.get("title"),
"priority": row.get("priority"),
"type": row.get("type"),
"hideCloseButton": bool(row.get("hide_close_button")),
"rerun": row.get("rerun"),
"url": row.get("url"),
"animojiId": row.get("animoji_id"),
"repeat": row.get("repeat"),
"hideOnClick": bool(row.get("hide_on_click")),
"id": row.get("id"),
"isTitleAnimated": bool(row.get("is_title_animated")),
}
banners.append(banner)
# Собираем данные пакета
payload = {
"showTime": 86400000, # Сколько будет показываться баннер, тут сутки в миллисекундах
# можно в будущем переделать, и сделать выбор в конфигурации
# думаю, было бы прикольно
"showTime": 86400000,
"updateTime": int(time.time() * 1000),
"banners": [
# TODO: разобраться как работают баннеры и их реализовать
# думаю админам инстансов было бы прикольно, и нам
]
"banners": banners
}
# Собираем пакет
@@ -155,7 +197,7 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
async def auth(self, payload, seq, writer, deviceType, deviceName):
async def auth(self, payload, seq, writer, deviceType, deviceName, appVersion, ip):
"""Обработчик проверки кода"""
try:
VerifyCodePayloadModel.model_validate(payload)
@@ -243,8 +285,10 @@ class AuthProcessors(BaseProcessor):
hashed_login,
deviceType,
deviceName,
"Little Saint James Island",
int(time.time()),
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000),
), # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть
)
@@ -253,11 +297,16 @@ class AuthProcessors(BaseProcessor):
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
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = (
None if not account.get("description") else account.get("description")
)
if self._check_legacy_version(appVersion):
include_profile_options = False
else:
include_profile_options = True
# Собираем данные пакета
payload = {
"tokenAttrs": {"LOGIN": {"token": login}},
@@ -273,7 +322,7 @@ class AuthProcessors(BaseProcessor):
description=description,
accountStatus=int(account.get("accountstatus")),
profileOptions=json.loads(account.get("profileoptions")),
includeProfileOptions=True,
includeProfileOptions=include_profile_options,
username=account.get("username"),
),
}
@@ -286,7 +335,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName):
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, appVersion, ip):
"""Обработчик подтверждения регистрации нового пользователя"""
# Валидируем данные пакета
try:
@@ -348,6 +397,9 @@ class AuthProcessors(BaseProcessor):
now_ms = int(time.time() * 1000)
now_s = int(time.time())
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем пользователя
await cursor.execute(
"""
@@ -357,7 +409,7 @@ class AuthProcessors(BaseProcessor):
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
self.tools.generate_id(),
user_id,
phone,
None,
first_name,
@@ -371,23 +423,30 @@ class AuthProcessors(BaseProcessor):
),
)
user_id = cursor.lastrowid
# Добавляем данные аккаунта
await cursor.execute(
"""
INSERT INTO user_data
(phone, folders, user_config, chat_config)
VALUES (%s, %s, %s, %s)
(phone, user_config, chat_config)
VALUES (%s, %s, %s)
""",
(
phone,
json.dumps(self.static.USER_FOLDERS),
json.dumps(self.static.USER_SETTINGS),
json.dumps({}),
),
)
# Добавляем дефолтную папку
await cursor.execute(
"""
INSERT INTO user_folders
(id, phone, title, sort_order)
VALUES ('all.chat.folder', %s, 'Все', 0)
""",
(phone,),
)
# Удаляем токен
await cursor.execute(
"DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,)
@@ -401,11 +460,18 @@ class AuthProcessors(BaseProcessor):
hashed_login,
deviceType or "ANDROID",
deviceName or "Unknown",
"Little Saint James Island",
now_s,
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
now_ms,
),
)
if self._check_legacy_version(appVersion):
include_profile_options = False
else:
include_profile_options = True
# Генерируем профиль
profile = self.tools.generate_profile(
id=user_id,
@@ -419,7 +485,7 @@ class AuthProcessors(BaseProcessor):
description=None,
accountStatus=0,
profileOptions=[],
includeProfileOptions=True,
includeProfileOptions=include_profile_options,
username=None,
)
@@ -445,7 +511,7 @@ class AuthProcessors(BaseProcessor):
f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={first_name} {last_name}"
)
async def login(self, payload, seq, writer):
async def login(self, payload, seq, writer, appVersion):
"""Обработчик авторизации клиента на сервере"""
# Валидируем данные пакета
try:
@@ -455,7 +521,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer
)
return
return None, None, None
# Чаты, где состоит пользователь
chats = []
@@ -479,7 +545,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer
)
return
return None, None, None
# Ищем аккаунт пользователя в бд
await cursor.execute(
@@ -497,18 +563,29 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь
await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get("id")),
(user.get("id"),),
)
user_chats = await cursor.fetchall()
for chat in user_chats:
chats.append(chat.get("chat_id"))
# Обновляем юзер конфиг
updated_user_config = await self.tools.update_user_config(
cursor, token_data.get("phone"),
user_data.get("user_config"), self.static.USER_SETTINGS
)
# Аватарка с биографией
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
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion):
include_profile_options = False
else:
include_profile_options = True
# Генерируем профиль
profile = self.tools.generate_profile(
id=user.get("id"),
@@ -522,7 +599,7 @@ class AuthProcessors(BaseProcessor):
description=description,
accountStatus=int(user.get("accountstatus")),
profileOptions=json.loads(user.get("profileoptions")),
includeProfileOptions=True,
includeProfileOptions=include_profile_options,
username=user.get("username"),
)
@@ -536,6 +613,10 @@ class AuthProcessors(BaseProcessor):
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,
@@ -543,25 +624,22 @@ class AuthProcessors(BaseProcessor):
"chatMarker": 0,
"messages": {},
"contacts": contacts,
"presence": {},
"presence": presence,
"config": {
"hash": "0",
"server": self.server_config,
"user": json.loads(user_data.get("user_config")),
"user": updated_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.opcodes.LOGIN, payload=payload
)
# print(
# json.dumps(payload, indent=4)
# )
# Отправляем
await self._send(writer, packet)

View File

@@ -3,7 +3,7 @@ import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ComplainReasonsGetPayloadModel
class ComplainsProcessors(BaseProcessor):
class ComplaintsProcessors(BaseProcessor):
async def complain_reasons_get(self, payload, seq, writer):
"""Обработчик получения причин жалоб"""
# Валидируем данные пакета

View File

@@ -0,0 +1,412 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ContactAddByPhonePayloadModel, 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.CONTACT_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)
)
# Создаем диалог, если его нет
chatId = userId ^ contactId
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) VALUES (%s, %s, %s)",
(chatId, userId, "DIALOG")
)
for uid in [int(userId), int(contactId)]:
await cursor.execute(
"INSERT INTO chat_participants (chat_id, user_id) VALUES (%s, %s)",
(chatId, uid)
)
# а если уже существует, отправляем ошибку
else:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_ADDED, 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)
elif action == "BLOCK":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем существующий контакт, если такой есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = TRUE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае добавляем новую запись в бд
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(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, TRUE)",
(userId, contactId, firstName, lastName)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UNBLOCK":
# Разблокируем контакт
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем контакт, если он есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = FALSE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае отправляем ошибку
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UPDATE":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Если контакта нет, отдаем ошибку
if not row:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
# Обновляем контакт
await cursor.execute(
"UPDATE contacts SET custom_firstname = %s, custom_lastname = %s WHERE owner_id = %s AND contact_id = %s",
(firstName, lastName, userId, contactId)
)
# Получаем данные пользователя
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = 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 + 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")),
description=user.get("description"),
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)
async def contact_add_by_phone(self, payload, seq, writer, userId):
"""Добавление контакта по номеру телефона"""
# Валидируем данные пакета
try:
ContactAddByPhonePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_ADD_BY_PHONE, self.error_types.INVALID_PAYLOAD, writer)
return
phone = payload.get("phone")
firstName = payload.get("firstName")
lastName = payload.get("lastName")
# Ищем пользователя по номеру телефона
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (int(phone),))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_ADD_BY_PHONE, self.error_types.CONTACT_NOT_FOUND, writer)
return
contactId = user.get("id")
# Проверяем, не добавлен ли уже контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
existing_contact = await cursor.fetchone()
is_new = existing_contact is None
if is_new:
# Добавляем контакт
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname) VALUES (%s, %s, %s, %s)",
(userId, contactId, firstName, lastName)
)
# Создаем диалог, если его нет
chatId = userId ^ contactId
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) VALUES (%s, %s, %s)",
(chatId, userId, "DIALOG")
)
for uid in [int(userId), int(contactId)]:
await cursor.execute(
"INSERT INTO chat_participants (chat_id, user_id) VALUES (%s, %s)",
(chatId, uid)
)
# Генерируем профиль
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")),
description=user.get("description"),
includeProfileOptions=False,
custom_firstname=firstName,
custom_lastname=lastName,
username=user.get("username"),
)
response_payload = {
"new": is_new,
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_ADD_BY_PHONE, payload=response_payload
)
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

@@ -2,7 +2,7 @@ import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import SyncFoldersPayloadModel
from oneme.models import SyncFoldersPayloadModel, CreateFolderPayloadModel
class FoldersProcessors(BaseProcessor):
async def folders_get(self, payload, seq, writer, senderPhone):
@@ -18,16 +18,32 @@ class FoldersProcessors(BaseProcessor):
# Ищем папки в бд
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"))
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": 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")
"folders": folders,
"foldersOrder": [folder["id"] for folder in result_folders],
"allFilterExcludeFolders": []
}
# Собираем пакет
@@ -36,4 +52,82 @@ class FoldersProcessors(BaseProcessor):
)
# Отправляем
await self._send(writer, packet)
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,5 +1,6 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ChatHistoryPayloadModel
@@ -20,19 +21,21 @@ class HistoryProcessors(BaseProcessor):
backward = payload.get("backward", 0)
from_time = payload.get("from", 0)
getMessages = payload.get("getMessages", True)
getChat = payload.get("getChat", False)
messages = []
# Если пользователь хочет получить историю из избранного,
# то выставляем в качестве ID чата его ID
if chatId == 0:
chatId = senderId
# то выставляем в качестве ID чата отрицательный ID отправителя
isFavourite = chatId == (senderId ^ senderId)
if isFavourite:
chatId = -senderId
# Проверяем, существует ли чат
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем состоит ли пользователь в чате,
# только в случае того, если это не избранное
if chatId != senderId:
if not isFavourite:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
@@ -58,19 +61,8 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию
messages.append({
"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"),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"options": 1,
})
messages.append(self.tools.build_message_dict(row, self.type))
backward_count = len(result)
if forward > 0:
await cursor.execute(
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s",
@@ -80,25 +72,19 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
messages.append({
"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")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {}
})
messages.append(self.tools.build_message_dict(row, self.type))
forward_count = len(result)
# Сортируем сообщения по времени
messages.sort(key=lambda x: x["time"])
# Формируем ответ
payload = {
"messages": messages
}
if getChat:
payload["chat"] = {}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_HISTORY, payload=payload

View File

@@ -1,9 +1,10 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import (
HelloPayloadModel,
PingPayloadModel,
HelloPayloadModel,
PingPayloadModel,
UpdateProfilePayloadModel
)
@@ -20,14 +21,17 @@ class MainProcessors(BaseProcessor):
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.SESSION_INIT, self.error_types.INVALID_PAYLOAD, writer)
return None, None
return None, None, None
# Получаем данные из пакета
deviceType = payload.get("userAgent").get("deviceType")
deviceName = payload.get("userAgent").get("deviceName")
userAgent = payload.get("userAgent")
deviceType = userAgent.get("deviceType")
deviceName = userAgent.get("deviceName")
appVersion = userAgent.get("appVersion")
# Данные пакета
payload = {
"callsSeed": int(time.time() * 1000),
"location": "RU",
"app-update-type": 0, # 1 = принудительное обновление
"reg-country-code": self.static.REG_COUNTRY_CODES,
@@ -43,9 +47,9 @@ class MainProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
return deviceType, deviceName
return deviceType, deviceName, appVersion
async def ping(self, payload, seq, writer):
async def ping(self, payload, seq, writer, userId=None):
"""Обработчик пинга"""
# Валидируем данные пакета
try:
@@ -55,6 +59,58 @@ class MainProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.PING, self.error_types.INVALID_PAYLOAD, writer)
return
# Обновляем статус пользователя, если он авторизован
# и в пакете отправлен интерактив
interactive = payload.get("interactive") if payload else None
if userId and interactive is not None:
now = int(time.time())
user = self.clients.get(userId)
if user:
if interactive:
user["status"] = 2
user["last_seen"] = now
else:
user["status"] = 0
user["last_seen"] = now
# Сохраняем последнее время посещения
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE users SET lastseen = %s WHERE id = %s",
(str(now), userId)
)
# Рассылаем статус контактам пользователя
now_ms = int(time.time() * 1000)
if interactive:
presence_data = {"on": "ON", "seen": now, "status": 1}
else:
presence_data = {"seen": now}
# Находим всех, у кого этот пользователь в контактах
async with self.db_pool.acquire() as conn2:
async with conn2.cursor() as cursor2:
await cursor2.execute(
"SELECT owner_id FROM contacts WHERE contact_id = %s",
(userId,)
)
contact_owners = await cursor2.fetchall()
# Рассылаем
for row in contact_owners:
owner_id = int(row.get("owner_id"))
if owner_id in self.clients:
await self.event(
owner_id,
{
"eventType": "presence",
"userId": userId,
"presence": presence_data,
"time": now_ms,
}
)
# Собираем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.PING, payload=None
@@ -97,7 +153,7 @@ class MainProcessors(BaseProcessor):
# Аватарка с биографией
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
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Генерируем профиль
@@ -127,5 +183,62 @@ class MainProcessors(BaseProcessor):
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):
"""
Обработчик 22 опкода (config)
Он отвечает за обновление настроек приватности
и пуш токена для пушей
"""
# Пейлоад, который отдадим клиенту
# а отдавать его нужно только при изменении настроек приватности
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)

View File

@@ -88,10 +88,9 @@ class MessagesProcessors(BaseProcessor):
chatId = userId ^ senderId
# Если клиент хочет отправить сообщение в избранное,
# то выставляем в качестве ID чата ID отправителя
# (А ещё используем это, если клиент вообще ничего не указал)
if chatId == 0 or not chatId:
chatId = senderId
# то выставляем в качестве ID чата отрицательный ID отправителя
if chatId == (senderId ^ senderId):
chatId = -senderId
participants = [senderId]
else:
# Если все таки клиент хочет отправить сообщение в нормальный чат,
@@ -114,6 +113,14 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId,
@@ -126,16 +133,21 @@ class MessagesProcessors(BaseProcessor):
db_pool=self.db_pool
)
# Готовое тело сообщения
# Готовое тело сообщения. Поля cid / elements / reactionInfo / link
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
# ожидает фиксированную msgpack-схему и обрывает соединение
# при отсутствии любого из них (см. регрессию из 87cfc19).
bodyMessage = {
"id": messageId,
"id": messageId if self.type == "mobile" else str(messageId),
"cid": int(cid or 0),
"time": messageTime,
"type": "USER",
"sender": senderId,
"cid": cid,
"text": text,
"attaches": attaches,
"elements": elements
"attaches": attaches if isinstance(attaches, list) else [],
"elements": elements if isinstance(elements, list) else [],
"reactionInfo": {},
"link": {},
}
# Отправляем событие всем участникам чата
@@ -144,7 +156,7 @@ class MessagesProcessors(BaseProcessor):
participant,
{
"eventType": "new_msg",
"chatId": 0 if chatId == senderId else chatId,
"chatId": 0 if chatId == -senderId else chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
@@ -154,7 +166,7 @@ class MessagesProcessors(BaseProcessor):
# Данные пакета
payload = {
"chatId": 0 if chatId == senderId else chatId,
"chatId": 0 if chatId == -senderId else chatId,
"message": bodyMessage,
"unread": 0,
"mark": messageTime

View File

@@ -36,7 +36,7 @@ class SearchProcessors(BaseProcessor):
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
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Получаем данные контакта
@@ -129,7 +129,7 @@ class SearchProcessors(BaseProcessor):
# Аватарка с биографией
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
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Получаем данные контакта
@@ -223,12 +223,12 @@ class SearchProcessors(BaseProcessor):
)
)
else:
# Получаем последнее сообщение из чата
# Получаем последнее сообщение из избранного
message, messageTime = await self.tools.get_last_message(
senderId, self.db_pool, protocol_type=self.type
-senderId, self.db_pool, protocol_type=self.type
)
# ID избранного
# ID избранного для клиента
chatId = senderId ^ senderId
# Добавляем чат в список

View File

@@ -1,5 +1,6 @@
import asyncio
import logging
import time
import traceback
from common.opcodes import Opcodes
@@ -32,7 +33,7 @@ class OnemeMobile:
self.opcodes = Opcodes()
# rate limiter anti ddos brute force protection
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 # Максимальный размер данных из сокета
@@ -45,6 +46,7 @@ class OnemeMobile:
deviceType = None
deviceName = None
appVersion = None
userPhone = None
userId = None
@@ -89,7 +91,7 @@ class OnemeMobile:
match opcode:
case self.opcodes.SESSION_INIT:
deviceType, deviceName = await self.processors.session_init(
deviceType, deviceName, appVersion = await self.processors.session_init(
payload, seq, writer
)
case self.opcodes.AUTH_REQUEST:
@@ -112,7 +114,7 @@ class OnemeMobile:
)
else:
await self.processors.auth(
payload, seq, writer, deviceType, deviceName
payload, seq, writer, deviceType, deviceName, appVersion, address[0]
)
case self.opcodes.AUTH_CONFIRM:
if not self.auth_rate_limiter.is_allowed(address[0]):
@@ -124,7 +126,7 @@ class OnemeMobile:
)
elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm(
payload, seq, writer, deviceType, deviceName
payload, seq, writer, deviceType, deviceName, appVersion, address[0]
)
else:
self.logger.warning(
@@ -143,7 +145,7 @@ class OnemeMobile:
userPhone,
userId,
hashedToken,
) = await self.processors.login(payload, seq, writer)
) = await self.processors.login(payload, seq, writer, appVersion)
if userPhone:
await self._finish_auth(
@@ -155,7 +157,7 @@ class OnemeMobile:
)
break
case self.opcodes.PING:
await self.processors.ping(payload, seq, writer)
await self.processors.ping(payload, seq, writer, userId)
case self.opcodes.LOG:
await self.processors.log(payload, seq, writer)
case self.opcodes.ASSETS_UPDATE:
@@ -166,6 +168,54 @@ class OnemeMobile:
seq,
writer,
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone,
self.processors.assets_get,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone,
self.processors.assets_get_by_ids,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone,
self.processors.assets_add,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone,
self.processors.assets_remove,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone,
self.processors.assets_move,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone,
self.processors.assets_list_modify,
payload,
seq,
writer,
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone,
@@ -193,6 +243,15 @@ class OnemeMobile:
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,
@@ -252,6 +311,15 @@ class OnemeMobile:
writer,
userId,
)
case self.opcodes.CONTACT_LIST:
await self.auth_required(
userPhone,
self.processors.contact_list,
payload,
seq,
writer,
userId,
)
case self.opcodes.COMPLAIN_REASONS_GET:
await self.auth_required(
userPhone,
@@ -272,6 +340,42 @@ class OnemeMobile:
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_ADD_BY_PHONE:
await self.auth_required(
userPhone,
self.processors.contact_add_by_phone,
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:
@@ -303,6 +407,8 @@ class OnemeMobile:
self.clients[id] = {
"phone": phone,
"id": id,
"status": 2,
"last_seen": 0,
"clients": [
{
"writer": writer,
@@ -313,6 +419,38 @@ class OnemeMobile:
],
}
await self._broadcast_presence(id, True)
async def _broadcast_presence(self, userId, online):
now = int(time.time())
now_ms = int(time.time() * 1000)
if online:
presence_data = {"on": "ON", "seen": now, "status": 1}
else:
presence_data = {"seen": now}
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT owner_id FROM contacts WHERE contact_id = %s",
(userId,)
)
contact_owners = await cursor.fetchall()
for row in contact_owners:
owner_id = int(row.get("owner_id"))
if owner_id in self.clients:
await self.processors.event(
owner_id,
{
"eventType": "presence",
"userId": userId,
"presence": presence_data,
"time": now_ms,
}
)
async def _end_session(self, id, ip, port):
"""Завершение сессии"""
# Получаем пользователя в списке
@@ -328,6 +466,20 @@ class OnemeMobile:
if (client.get("ip"), client.get("port")) == (ip, port):
clients.pop(i)
if not clients:
now = int(time.time())
user["status"] = 0
user["last_seen"] = now
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE users SET lastseen = %s WHERE id = %s",
(str(now), id)
)
await self._broadcast_presence(id, False)
async def start(self):
"""Функция для запуска сервера"""
self.server = await asyncio.start_server(
@@ -336,5 +488,9 @@ class OnemeMobile:
self.logger.info(f"Сокет запущен на порту {self.port}")
async with self.server:
await self.server.serve_forever()
try:
async with self.server:
await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()

View File

@@ -1,6 +1,8 @@
import logging
import time
import traceback
import websockets
import asyncio
from common.proto_web import WebProto
from oneme.processors import Processors
from common.rate_limiter import RateLimiter
@@ -8,7 +10,9 @@ from common.opcodes import Opcodes
from common.tools import Tools
class OnemeWS:
def __init__(self, host, port, clients, ssl_context, db_pool, send_event):
def __init__(
self, host, port, ssl_context, db_pool, clients, send_event, telegram_bot
):
self.host = host
self.port = port
self.ssl_context = ssl_context
@@ -20,7 +24,13 @@ class OnemeWS:
self.opcodes = Opcodes()
self.proto = WebProto()
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, type="web")
self.processors = Processors(
db_pool=db_pool,
clients=clients,
send_event=send_event,
telegram_bot=telegram_bot,
type="web"
)
self.auth_required = Tools().auth_required
# rate limiter
@@ -37,6 +47,7 @@ class OnemeWS:
deviceType = None
deviceName = None
appVersion = None
userPhone = None
userId = None
@@ -63,7 +74,7 @@ class OnemeWS:
match opcode:
case self.opcodes.SESSION_INIT:
deviceType, deviceName = await self.processors.session_init(
deviceType, deviceName, appVersion = await self.processors.session_init(
payload, seq, websocket
)
case self.opcodes.AUTH_REQUEST:
@@ -86,7 +97,7 @@ class OnemeWS:
)
else:
await self.processors.auth(
payload, seq, websocket, deviceType, deviceName
payload, seq, websocket, deviceType, deviceName, appVersion, address[0]
)
case self.opcodes.AUTH_CONFIRM:
if not self.auth_rate_limiter.is_allowed(address[0]):
@@ -98,7 +109,7 @@ class OnemeWS:
)
elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm(
payload, seq, websocket, deviceType, deviceName
payload, seq, websocket, deviceType, deviceName, appVersion, address[0]
)
else:
self.logger.warning(
@@ -117,7 +128,7 @@ class OnemeWS:
userPhone,
userId,
hashedToken,
) = await self.processors.login(payload, seq, websocket)
) = await self.processors.login(payload, seq, websocket, appVersion)
if userPhone:
await self._finish_auth(
@@ -129,7 +140,7 @@ class OnemeWS:
)
break
case self.opcodes.PING:
await self.processors.ping(payload, seq, websocket)
await self.processors.ping(payload, seq, websocket, userId)
case self.opcodes.LOG:
await self.processors.log(payload, seq, websocket)
case self.opcodes.ASSETS_UPDATE:
@@ -140,6 +151,54 @@ class OnemeWS:
seq,
websocket,
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone,
self.processors.assets_get,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone,
self.processors.assets_get_by_ids,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone,
self.processors.assets_add,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone,
self.processors.assets_remove,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone,
self.processors.assets_move,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone,
self.processors.assets_list_modify,
payload,
seq,
websocket,
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone,
@@ -167,6 +226,15 @@ class OnemeWS:
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,
@@ -226,6 +294,15 @@ class OnemeWS:
websocket,
userId,
)
case self.opcodes.CONTACT_LIST:
await self.auth_required(
userPhone,
self.processors.contact_list,
payload,
seq,
websocket,
userId,
)
case self.opcodes.COMPLAIN_REASONS_GET:
await self.auth_required(
userPhone,
@@ -246,6 +323,42 @@ class OnemeWS:
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_ADD_BY_PHONE:
await self.auth_required(
userPhone,
self.processors.contact_add_by_phone,
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:
@@ -272,23 +385,59 @@ class OnemeWS:
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "oneme"
"protocol": "oneme",
"type": "web"
}
)
else:
self.clients[id] = {
"phone": phone,
"id": id,
"status": 2,
"last_seen": 0,
"clients": [
{
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "oneme"
"protocol": "oneme",
"type": "web"
}
]
}
await self._broadcast_presence(id, True)
async def _broadcast_presence(self, userId, online):
now = int(time.time())
now_ms = int(time.time() * 1000)
if online:
presence_data = {"on": "ON", "seen": now, "status": 1}
else:
presence_data = {"seen": now}
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT owner_id FROM contacts WHERE contact_id = %s",
(userId,)
)
contact_owners = await cursor.fetchall()
for row in contact_owners:
owner_id = int(row.get("owner_id"))
if owner_id in self.clients:
await self.processors.event(
owner_id,
{
"eventType": "presence",
"userId": userId,
"presence": presence_data,
"time": now_ms,
}
)
async def _end_session(self, id, ip, port):
"""Завершение сессии"""
# Получаем пользователя в списке
@@ -304,6 +453,20 @@ class OnemeWS:
if (client.get("ip"), client.get("port")) == (ip, port):
clients.pop(i)
if not clients:
now = int(time.time())
user["status"] = 0
user["last_seen"] = now
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE users SET lastseen = %s WHERE id = %s",
(str(now), id)
)
await self._broadcast_presence(id, False)
async def start(self):
"""Функция для запуска WebSocket сервера"""
self.server = await websockets.serve(
@@ -315,4 +478,9 @@ class OnemeWS:
self.logger.info(f"WebSocket запущен на порту {self.port}")
await self.server.wait_closed()
try:
await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise

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,10 +3,106 @@ from tamtam.socket import TamTamMobile
from tamtam.websocket import TamTamWS
from classes.controllerbase import ControllerBase
from common.config import ServerConfig
from common.proto_tcp import MobileProto
from common.proto_web import WebProto
from common.opcodes import Opcodes
class TTController(ControllerBase):
def __init__(self):
self.config = ServerConfig()
self.proto_tcp = MobileProto()
self.proto_web = WebProto()
self.opcodes = Opcodes()
async def event(self, target, client, eventData):
# Извлекаем тип события и врайтер
eventType = eventData.get("eventType")
writer = client.get("writer")
is_web = client.get("type") == "web"
# Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе
if writer == eventData.get("writer"):
return
# Обрабатываем событие
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 = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.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 = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_TYPING, payload=payload
)
elif eventType == "profile_updated":
# Данные события
profile = eventData.get("profile")
# Данные пакета
payload = {
"profile": profile
}
# Создаем пакет
packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PROFILE, payload=payload
)
elif eventType == "presence":
userId = eventData.get("userId")
presence = eventData.get("presence")
event_time = eventData.get("time")
payload = {
"userId": userId,
"presence": presence,
"time": event_time
}
packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
)
if not packet:
return
if is_web:
await writer.send(packet)
else:
writer.write(packet)
await writer.drain()
def launch(self, api):
async def _start_all():

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,99 @@ 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 AssetsGetPayloadModel(pydantic.BaseModel):
type: str
count: int = 100
query: str = None
class AssetsGetByIdsPayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsAddPayloadModel(pydantic.BaseModel):
type: str
id: int = None
class AssetsRemovePayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsMovePayloadModel(pydantic.BaseModel):
type: str
id: int
position: int
class AssetsListModifyPayloadModel(pydantic.BaseModel):
type: str
ids: list
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 = None
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
class AuthConfirmRegisterPayloadModel(pydantic.BaseModel):
token: str
name: str
tokenType: str
deviceType: str
deviceId: str = None
@pydantic.field_validator('name')
def validate_name(cls, v):
v = v.strip()
if not v:
raise ValueError('name must not be empty')
if len(v) > 59:
raise ValueError('name too long')
return v

View File

@@ -2,9 +2,19 @@ from .main import MainProcessors
from .auth import AuthProcessors
from .search import SearchProcessors
from .history import HistoryProcessors
from .assets import AssetsProcessors
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,
ChatsProcessors,
ContactsProcessors,
MessagesProcessors,
SessionsProcessors):
pass

View File

@@ -0,0 +1,167 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
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
response = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"stickersOrder": [
"RECENT",
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
)
await self._send(writer, packet)
async def assets_get(self, payload, seq, writer):
try:
data = AssetsGetPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": [], "marker": None}
else:
response = {"stickers": [], "marker": None}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET, payload=response
)
await self._send(writer, packet)
async def assets_get_by_ids(self, payload, seq, writer):
try:
data = AssetsGetByIdsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET_BY_IDS, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": []}
else:
response = {"stickers": []}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET_BY_IDS, payload=response
)
await self._send(writer, packet)
async def assets_add(self, payload, seq, writer):
try:
AssetsAddPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_ADD, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_ADD, payload={}
)
await self._send(writer, packet)
async def assets_remove(self, payload, seq, writer):
try:
AssetsRemovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_REMOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_REMOVE, payload={}
)
await self._send(writer, packet)
async def assets_move(self, payload, seq, writer):
try:
AssetsMovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_MOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_MOVE, payload={}
)
await self._send(writer, packet)
async def assets_list_modify(self, payload, seq, writer):
try:
AssetsListModifyPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_LIST_MODIFY, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_LIST_MODIFY, payload={}
)
await self._send(writer, packet)

View File

@@ -4,10 +4,12 @@ import time
import json
import re
from classes.baseprocessor import BaseProcessor
from common.sms import send_sms_code
from tamtam.models import (
RequestCodePayloadModel,
VerifyCodePayloadModel,
FinalAuthPayloadModel,
AuthConfirmRegisterPayloadModel,
LoginPayloadModel,
)
from tamtam.config import TTConfig
@@ -17,6 +19,172 @@ class AuthProcessors(BaseProcessor):
super().__init__(db_pool, clients, send_event, type)
self.server_config = TTConfig().SERVER_CONFIG
async def _finish_auth(self, payload, seq, writer, cursor, phone, hashed_token, hashed_login, account, deviceType, deviceName, ip, login):
"""Завершение существующего пользователя"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return None
# Удаляем токен
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)",
(
phone,
hashed_login,
deviceType,
deviceName,
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000)
)
)
# Аватарка с биографией
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 + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
return {
"userToken": str(account.get("id")),
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
async def _finish_reg(self, payload, seq, writer, cursor, phone, hashed_token, hashed_login, deviceType, deviceName, ip, login):
"""Регистрация пользователя во время авторизации"""
# Валидируем данные пакета
try:
AuthConfirmRegisterPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return None
name = payload.get("name", "").strip()
now_ms = int(time.time() * 1000)
now_s = int(time.time())
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем пользователя
# NOTE: На бумаге у нас как бы полная поддержка ТТ (ну, все функции, в которые может макс),
# а клиенты тамтама не знают, что такое фамилия в аккаунтах тамтама (оно предназначено только для ОК)
# по этому просто не писать указывать фамилию в бд, ее клиент и так не отдаст
await cursor.execute(
"""
INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
user_id,
phone,
None,
name,
None,
None,
json.dumps([]),
json.dumps(["TT", "ONEME"]),
0,
str(now_ms),
str(now_s),
),
)
# Добавляем данные аккаунта
await cursor.execute(
"""
INSERT INTO user_data
(phone, user_config, chat_config)
VALUES (%s, %s, %s)
""",
(
phone,
json.dumps(self.static.USER_SETTINGS),
json.dumps({}),
),
)
# Добавляем дефолтную папку
await cursor.execute(
"""
INSERT INTO user_folders
(id, phone, title, sort_order)
VALUES ('all.chat.folder', %s, 'Все', 0)
""",
(phone,),
)
# Удаляем токен
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)",
(
phone,
hashed_login,
deviceType or "ANDROID",
deviceName or "Unknown",
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
now_ms,
),
)
# Генерируем профиль
profile = self.tools.generate_profile_tt(
id=user_id,
phone=int(phone),
avatarUrl=None,
photoId=None,
updateTime=now_ms,
firstName=name,
lastName="",
options=["TT", "ONEME"],
description=None,
username=None,
)
self.logger.info(
f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={name}"
)
# Собираем данные пакета
return {
"userToken": "0",
"profile": profile,
"tokenType": "LOGIN",
"token": login,
}
async def auth_request(self, payload, seq, writer):
"""Обработчик запроса кода"""
# Валидируем данные пакета
@@ -30,29 +198,51 @@ class AuthProcessors(BaseProcessor):
# Извлекаем телефон из пакета
phone = re.sub(r'\D', '', payload.get("phone", ""))
# Генерируем токен с кодом
code = f"{secrets.randbelow(1_000_000):06d}"
# Генерируем токен
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
# Ищем пользователя, и если он существует, сохраняем токен
user_exists = False
# Ищем пользователя
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()
# Если пользователь существует, сохраняем токен
# Получаем код через SMS шлюз или генерируем локально
local_fallback_code = False
if self.config.sms_gateway_url:
code = await send_sms_code(self.config.sms_gateway_url, phone)
if code is None:
code = f"{secrets.randbelow(1_000_000):06d}"
local_fallback_code = True
else:
code = f"{secrets.randbelow(1_000_000):06d}"
local_fallback_code = True
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
# Сохраняем токен
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
if user:
user_exists = True
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")
)
else:
# Пользователь не найден - сохраняем токен в register
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, "register")
)
# Данные пакета
payload = {
@@ -71,7 +261,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code}")
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
async def auth(self, payload, seq, writer):
"""Обработчик проверки кода"""
@@ -112,13 +302,32 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_CODE, writer)
return
# Если это новый пользователь - переводим токен в verified
# и отдаём клиенту NEW токен, чтобы он показал экран ввода имени
if stored_token.get("state") == "register":
await cursor.execute(
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s",
("verified", hashed_token)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK,
seq=seq,
opcode=self.opcodes.AUTH,
payload={
"tokenAttrs": {"NEW": {"token": token}},
"tokenTypes": {"NEW": token},
},
)
await self._send(writer, packet)
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",
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s",
("verified", hashed_token)
)
@@ -158,22 +367,17 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName):
"""Обработчик финальной аутентификации"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, ip):
"""Обработчик финальной аутентификации / регистрации"""
# Извлекаем данные из пакета
token = payload.get("token")
if not deviceType:
deviceType = payload.get("deviceType")
if not deviceName:
deviceName = "Unknown device"
# Хешируем токен
hashed_token = hashlib.sha256(token.encode()).hexdigest()
@@ -181,10 +385,9 @@ class AuthProcessors(BaseProcessor):
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,)
@@ -196,53 +399,36 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer)
return
# Если авторизация только началась - отдаем ошибку
if stored_token.get("state") == "started":
# Если авторизация только началась (код ещё не проверен) - отдаем ошибку
if stored_token.get("state") == "started" or stored_token.get("state") == "register":
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer)
return
# Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
phone = stored_token.get("phone")
# Проверяем, существует ли пользователь
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
account = await cursor.fetchone()
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
# Если пользователь есть, производим создание сессии
if account:
resp_payload = await self._finish_auth(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, account, deviceType, deviceName, ip, login
)
else: # в ином случае производим регистрацию
resp_payload = await self._finish_reg(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, deviceType, deviceName, ip, login
)
# Создаем сессию
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()))
)
# Аватарка с биографией
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 + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
payload = {
"userToken": "0", # Пока как заглушка
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
if resp_payload is None:
return
# Создаем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=resp_payload
)
# Отправляем
@@ -257,7 +443,7 @@ class AuthProcessors(BaseProcessor):
self.logger.error(f"Возникли ошибки при валидации пакета: {e}")
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_PAYLOAD, writer)
return
return None, None, None
# Чаты, где состоит пользователь
chats = []
@@ -278,7 +464,7 @@ class AuthProcessors(BaseProcessor):
if token_data is None:
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_TOKEN, writer)
return
return None, None, None
# Ищем аккаунт пользователя в бд
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
@@ -290,8 +476,8 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь
await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get('id'))
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get('id'),)
)
user_chats = await cursor.fetchall()
@@ -300,6 +486,12 @@ class AuthProcessors(BaseProcessor):
chat.get("chat_id")
)
# Обновляем юзер конфиг
updated_user_config = await self.tools.update_user_config(
cursor, token_data.get("phone"),
user_data.get("user_config"), self.static.USER_SETTINGS
)
# Аватарка с биографией
photo_id = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
@@ -324,18 +516,27 @@ 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": json.loads(user_data.get("user_config")),
"user": updated_user_config,
"chatFolders": {
"FOLDERS": [],
"ALL_FILTER_EXCLUDE": []
@@ -357,6 +558,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
@@ -364,4 +569,39 @@ 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,301 @@
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_ADDED, 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)
elif action == "BLOCK":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем существующий контакт, если такой есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = TRUE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае добавляем новую запись в бд
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(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, TRUE)",
(userId, contactId, firstName, lastName)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UNBLOCK":
# Разблокируем контакт
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем контакт, если он есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = FALSE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае отправляем ошибку
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UPDATE":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Если контакта нет, отдаем ошибку
if not row:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
# Обновляем контакт
await cursor.execute(
"UPDATE contacts SET custom_firstname = %s, custom_lastname = %s WHERE owner_id = %s AND contact_id = %s",
(firstName, lastName, userId, contactId)
)
# Получаем данные пользователя
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
# Генерируем профиль
photo_id = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
contact = 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=user.get("description"),
username=user.get("username")
),
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)
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,168 @@
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
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, 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": chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
"writer": writer
}
)
# Данные пакета
payload = {
"chatId": 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 # Максимальный размер данных из сокета
@@ -96,7 +96,7 @@ class TamTamMobile:
if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.AUTH_CONFIRM, self.processors.error_types.RATE_LIMITED, writer)
else:
await self.processors.auth_confirm(payload, seq, writer, deviceType, deviceName)
await self.processors.auth_confirm(payload, seq, writer, deviceType, deviceName, address[0])
case self.opcodes.LOGIN:
if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.LOGIN, self.processors.error_types.RATE_LIMITED, writer)
@@ -105,18 +105,111 @@ 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.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, writer
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, writer
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, writer
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, writer
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, writer
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, 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.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):
"""Завершение открытия сессии"""
@@ -130,7 +223,8 @@ class TamTamMobile:
"writer": writer,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam"
"protocol": "tamtam",
"type": "tcp"
}
)
else:
@@ -142,7 +236,8 @@ class TamTamMobile:
"writer": writer,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam"
"protocol": "tamtam",
"type": "tcp"
}
]
}
@@ -170,5 +265,9 @@ class TamTamMobile:
self.logger.info(f"Сокет запущен на порту {self.port}")
async with self.server:
await self.server.serve_forever()
try:
async with self.server:
await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()

View File

@@ -1,6 +1,7 @@
import logging
import traceback
import websockets
import asyncio
from common.proto_web import WebProto
from tamtam.processors import Processors
from common.rate_limiter import RateLimiter
@@ -82,7 +83,7 @@ class TamTamWS:
if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.AUTH_CONFIRM, self.processors.error_types.RATE_LIMITED, websocket)
else:
await self.processors.auth_confirm(payload, seq, websocket, deviceType, deviceName)
await self.processors.auth_confirm(payload, seq, websocket, deviceType, deviceName, address[0])
case self.opcodes.LOGIN:
if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.LOGIN, self.processors.error_types.RATE_LIMITED, websocket)
@@ -91,6 +92,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 +105,86 @@ 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.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, websocket
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, websocket
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, websocket
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, websocket
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, websocket
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, 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.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:
@@ -125,7 +211,8 @@ class TamTamWS:
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam"
"protocol": "tamtam",
"type": "web"
}
)
else:
@@ -137,7 +224,8 @@ class TamTamWS:
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam"
"protocol": "tamtam",
"type": "web"
}
]
}
@@ -168,4 +256,9 @@ class TamTamWS:
self.logger.info(f"TT WebSocket запущен на порту {self.port}")
await self.server.wait_closed()
try:
await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise

View File

@@ -14,7 +14,7 @@ from common.tools import Tools
class TelegramBot:
def __init__(self, token, enabled, db_pool, whitelist_ids=None):
def __init__(self, token, enabled, db_pool, whitelist_ids=None, whitelist_enabled=False):
self.bot = Bot(token=token)
self.dp = Dispatcher()
self.router = Router()
@@ -23,6 +23,7 @@ class TelegramBot:
self.enabled = enabled
self.db_pool = db_pool
self.whitelist_ids = whitelist_ids if whitelist_ids is not None else []
self.whitelist_enabled = whitelist_enabled
self.logger = logging.getLogger(__name__)
self.msg_types = Static().BotMessageTypes()
@@ -59,12 +60,13 @@ class TelegramBot:
async def handle_register(self, message: Message):
tg_id = str(message.from_user.id)
# Проверка ID на наличие в белом списке
if self.whitelist_ids and tg_id not in self.whitelist_ids:
await message.answer(
self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED)
)
return
# Проверка ID на наличие в белом списке (если он включен, конечно)
if self.whitelist_enabled:
if self.whitelist_ids and tg_id not in self.whitelist_ids:
await message.answer(
self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED)
)
return
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
@@ -81,17 +83,24 @@ class TelegramBot:
updatetime = str(int(time.time() * 1000))
lastseen = str(int(time.time()))
firstname = message.from_user.first_name[:59]
lastname = (message.from_user.last_name or "")[:59]
username = (message.from_user.username or f"user{int(time.time() * 1000)}")[:60]
try:
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем юзера
await cursor.execute(
self.sql_queries.INSERT_USER,
(
self.tools.generate_id(),
user_id, # id
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
firstname, # firstname
lastname, # lastname
username, # username
json.dumps([]), # profileoptions
json.dumps(["TT", "ONEME"]), # options
0, # accountstatus
@@ -105,12 +114,17 @@ class TelegramBot:
self.sql_queries.INSERT_USER_DATA,
(
new_phone, # phone
json.dumps(self.static.USER_FOLDERS), # folders
json.dumps(self.static.USER_SETTINGS), # user settings
json.dumps({}), # chat_config
),
)
# Добавляем дефолтную папку
await cursor.execute(
self.sql_queries.INSERT_DEFAULT_FOLDER,
(new_phone,),
)
await message.answer(
self.get_bot_message(
self.msg_types.REGISTRATION_SUCCESS
@@ -125,7 +139,7 @@ class TelegramBot:
async def start(self):
if self.enabled:
try:
await self.dp.start_polling(self.bot)
await self.dp.start_polling(self.bot, handle_signals=False)
except Exception as e:
self.logger.error(f"Ошибка запуска Telegram бота: {e}")
else:

View File

@@ -19,7 +19,8 @@ class TelegramBotController(ControllerBase):
token=self.config.telegram_bot_token,
enabled=self.config.telegram_bot_enabled,
db_pool=api['db'],
whitelist_ids=self.config.telegram_whitelist_ids
whitelist_ids=self.config.telegram_whitelist_ids,
whitelist_enabled=self.config.telegram_whitelist_enabled,
)
return _start_all()

View File

@@ -1,7 +1,7 @@
CREATE TABLE `users` (
`id` INT NOT NULL,
`phone` VARCHAR(20) UNIQUE,
`telegram_id` VARCHAR(64) UNIQUE,
`telegram_id` VARCHAR(64),
`firstname` VARCHAR(59) NOT NULL,
`lastname` VARCHAR(59),
`description` VARCHAR(400),
@@ -23,6 +23,7 @@ CREATE TABLE `tokens` (
`device_name` VARCHAR(256) NOT NULL,
`location` VARCHAR(256) NOT NULL,
`time` VARCHAR(16) NOT NULL,
`push_token` VARCHAR(512) DEFAULT NULL,
PRIMARY KEY (`phone`, `token_hash`)
);
@@ -37,7 +38,6 @@ CREATE TABLE `auth_tokens` (
CREATE TABLE `user_data` (
`phone` VARCHAR(20) NOT NULL UNIQUE,
`folders` JSON NOT NULL,
`user_config` JSON NOT NULL,
`chat_config` JSON NOT NULL,
PRIMARY KEY (`phone`)
@@ -51,7 +51,7 @@ CREATE TABLE `chats` (
);
CREATE TABLE `messages` (
`id` INT NOT NULL,
`id` BIGINT NOT NULL,
`chat_id` INT NOT NULL,
`sender` INT NOT NULL,
`time` VARCHAR(32) NOT NULL,
@@ -78,3 +78,34 @@ CREATE TABLE `contacts` (
`is_blocked` BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (`owner_id`, `contact_id`)
);
CREATE TABLE `banners` (
`id` VARCHAR(64) NOT NULL,
`title` VARCHAR(256) NOT NULL,
`description` VARCHAR(512) NOT NULL,
`url` VARCHAR(512) NOT NULL,
`type` INT NOT NULL DEFAULT 1,
`priority` INT NOT NULL DEFAULT 0,
`animoji_id` INT NOT NULL DEFAULT 0,
`repeat` INT NOT NULL DEFAULT 1,
`rerun` BIGINT NOT NULL DEFAULT 0,
`hide_close_button` BOOLEAN NOT NULL DEFAULT FALSE,
`hide_on_click` BOOLEAN NOT NULL DEFAULT FALSE,
`is_title_animated` BOOLEAN NOT NULL DEFAULT FALSE,
`enabled` BOOLEAN NOT NULL DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
CREATE TABLE `user_folders` (
`id` VARCHAR(64) NOT NULL,
`phone` VARCHAR(20) NOT NULL,
`title` VARCHAR(128) NOT NULL,
`filters` JSON NOT NULL DEFAULT ('[]'),
`include` JSON NOT NULL DEFAULT ('[]'),
`options` JSON NOT NULL DEFAULT ('[]'),
`source_id` INT NOT NULL DEFAULT 1,
`update_time` BIGINT NOT NULL DEFAULT 0,
`sort_order` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`, `phone`)
);