Compare commits

..

56 Commits

Author SHA1 Message Date
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
Alexey Polyakov 4abe6de885 oops 2026-04-20 22:25:38 +03:00
Alexey Polyakov 4121bd0e1d MAX: заглушка для баннеров, правка пакета со списком жалоб, отдача контактов и прочие улучшения 2026-04-20 22:22:02 +03:00
Aleksandr Kosachev d9798a6fc6 Fix OpenMAX mobile compatibility and Telegram auth fallback (#30)
* Fix OpenMAX mobile compatibility and Telegram auth fallback

* Common: Убрал скобку в конфиге

---------

Co-authored-by: Alexey Polyakov <starwear3000@mail.ru>
2026-04-14 20:05:34 +03:00
Alexey Polyakov d9cbafc4e3 TT: фикс вебсокета 2026-04-12 07:15:09 +03:00
WowInceptionGood 448b854bdb Исправление конфликтов 2026-04-10 18:27:42 +03:00
Alexey Polyakov dcbcb0a600 Common: добавил сжатие lz4 2026-04-10 17:43:35 +03:00
Alexey Polyakov 0ffc649dd9 Начальная реализация транспорта ws для max web и прочие улучшения 2026-04-07 12:36:30 +03:00
Alexey Polyakov 52949602af Common: фикс года 2026-04-03 23:02:49 +03:00
Alexey Polyakov 0f5c06f80c TG Bot: переписал на aiogram 2026-03-30 22:34:09 +03:00
Alexey Polyakov e5607adb9b MAX: Разделил процессоры 2026-03-29 11:04:45 +03:00
Alexey Polyakov 4014bab5c9 Правка контроллеров небольшая 2026-03-27 19:35:59 +03:00
Alexey Polyakov 0b7282b284 TamTam && MAX: история (в мохе она вроде теперь получше работает) 2026-03-27 19:26:20 +03:00
Alexey Polyakov 7a2e5a20d6 TamTam: websocket transport for web version 2026-03-27 19:00:14 +03:00
Alexey Polyakov ac76015d08 Common: поправил 1 прикол 2026-03-27 17:41:09 +03:00
Alexey Polyakov f4ea147a5f MAX: заглушка в 158 опкоде 2026-03-22 19:28:52 +03:00
Alexey Polyakov 5fcc56950f TT: oops 2026-03-22 18:56:50 +03:00
Alexey Polyakov a8732b5d7f ТамТам: починил 32 опкод и добавил серверный конфиг. Избранное больше не добавляется в список чатов
MAX: починил 32 опкод
2026-03-22 18:26:04 +03:00
Alexey Polyakov aa6411b75d ТамТам: обработчик 5 и 1 опкода 2026-03-22 17:09:20 +03:00
Alexey Polyakov f2e7e37592 ТамТам: реализация 32 опкода. Надеюсь, будет работать 2026-03-22 16:59:13 +03:00
Alexey Polyakov 8d7a1dacb5 Теперь мы не должны отправлять 128 опкод самому себе. вроде 2026-03-22 16:51:31 +03:00
Alexey Polyakov ef512b060f Вынес список участников чата в отдельную таблицу 2026-03-21 15:05:38 +03:00
Alexey Polyakov 2cf18b878a Генерируем айди, за вместо того, чтобы писать его попорядку 2026-03-19 23:13:27 +03:00
Alexey Polyakov bbee49d2d8 Коды стран 2026-03-19 16:48:59 +03:00
Alexey Polyakov 9bc6c15d82 Поделил процессоры в таме 2026-03-19 16:21:48 +03:00
Alexey Polyakov 11b2e2748d Удалил tamtam.proto, поскольку нигде больше не используется 2026-03-19 01:16:35 +03:00
Alexey Polyakov 6c05b5f1b5 Швырнул архитектуру, чтобы позже объединить контроллеры веба и сокета в одно, а также разделить процессоры 2026-03-19 01:13:12 +03:00
Alexey Polyakov 2dab853569 Доделал авторизацию в ТамТаме 2026-03-19 00:10:21 +03:00
Alexey Polyakov c7eace4648 Дополнил немного 48 опкод, и починил отдачу истории в избранном (избранное все равно не работает) 2026-03-18 22:16:31 +03:00
Alexey Polyakov 09c1f8b6f6 История сообщений (вроде как даже работает) 2026-03-18 21:55:24 +03:00
Alexey Polyakov 9c3038bc84 Починил изменение профиля 2026-03-18 21:06:33 +03:00
Alexey Polyakov 91453d7173 Серверный конфиг снова сломался 2026-03-18 20:54:59 +03:00
WowInceptionGood d5ea45cb96 SQLite: Ошибка
И вообще не юзайте sqlite пожалуйста, он баганный
2026-03-16 00:11:50 +03:00
zavolo 2d09f52c2e feat: 23 опкод для регистрации, смс шлюз, докер 2026-03-15 13:25:40 -04:00
zavolo d4d5dd5530 feat: 16 опкод для обновления профиля 2026-03-15 11:39:32 +03:00
Alexey Polyakov 81f5fb762f Update deviceId description in tamtam_ws.md 2026-03-12 19:52:38 +03:00
WowInceptionGood 301e55be05 Документация: описал про SESSION_INIT 2026-03-12 16:49:52 +00:00
WowInceptionGood db3b7323d9 Merge branch 'dev/0.1.0' of https://github.com/openmax-server/server into dev/0.1.0 2026-03-12 16:34:45 +00:00
WowInceptionGood 66fb40a1fd Merge branch 'master' into dev/0.1.0 2026-03-12 16:33:29 +00:00
Alexey Polyakov 9004566652 Merge branch 'master' into dev/0.1.0 2026-03-12 19:30:37 +03:00
Alexey Polyakov 07dd71b0ad Теперь все должно работать исправно 2026-03-12 19:25:09 +03:00
Alexey Polyakov 1ec1d49424 Merge branch 'master' into dev/0.1.0 2026-03-11 20:44:19 +03:00
Alexey Polyakov 9034485408 Различные фиксы (#14)
* Исключаем только ошибку валидации

* Небольшие правки вебсокета тамтама

* Теперь номера брутить чутка сложнее

* Авторизация теперь проверяется для некоторых команд

* Теперь проверяется orign у вебсокета тамтама

* Дополнил пример env

* Починил немного сокет тамтама
2026-03-11 15:21:49 +03:00
37 changed files with 433 additions and 1980 deletions
+1
View File
@@ -0,0 +1 @@
Проект OpenMAX не принимает правки, написанные с использованием искусственного интеллекта и нейросетевых технологий
+1 -1
View File
@@ -44,4 +44,4 @@
| `google_storage_bucket` | `max-messenger-app.firebasestorage.app` | `project_info.storage_bucket` |
4. Соберите и подпишите APK
5. В настройках проекта Firebase создайте сервисный аккаунт и укажите путь в `.env`
5. В настройках проекта Firebase создайте сервисный аккаунт и укажите путь в `.env`
-8
View File
@@ -1,8 +0,0 @@
# Патч MAX для IOS
1) Распаковываете IPA
2) Находите файл MAX в Payload/Max.app
3) Открываете hex-редактор и находите api.oneme.ru и меняете на свой
При желании можете поменять другие URL
URL не должен привышать количество символов которое было у изначального URL!
4) Открываете IPA как архив и добавляете патченный файл MAX в Payload/Max.app
+2 -3
View File
@@ -1,8 +1,7 @@
# Навигация по faq
## Работа с сервером
- [Установка сервера](install.md)
[Установка сервера](install.md)
## Патчинг клиентов
- [Патч мобильного клиента для Android](patch_apk.md)
- [Патч мобильного клиента для IOS](patch_ipa.md)
[Патч apk](patch_apk.md)
-11
View File
@@ -1,11 +0,0 @@
> [!Caution]
>
> На данной странице представлены инстансы от сторонних разработчиков.
>
> Используйте на свой страх и риск
>
# Открытые сервера
* [JustMAX](https://t.me/justmax_official)
* [mox.nyako.tk](https://t.me/opengrame/296), порт 443 (регистрация по реальному номеру)
+3 -5
View File
@@ -1,6 +1,6 @@
> [!Caution]
>
> Проект находится на ранней стадии разработки и, вероятно, полон багов.
> Проект находится на ранней стадии разработки и вероятно полон багов.
>
> Использование в профессиональных средах не рекомендовано.
@@ -13,7 +13,7 @@ https://t.me/openmax_alerts
# Требования
- Python 3.12+ (поддержка версий ниже не гарантирована)
- MariaDB, MySQL или SQLite (использование последнего не рекомендуется, так как поддержка ещё в разработке)
- MariaDB, MySQL или SQLite
- Уметь патчить клиент MAX или собирать Komet из исходного кода (естественно с заменой сервера)
- Сертификат и приватный ключ X.509 (для тестирования сервера можно сгенерировать самоподписанный: ```openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365```)
@@ -22,6 +22,4 @@ https://t.me/openmax_alerts
Клиент может быть практически любым, главное условие - чтобы он был совместим с официальным сервером (`api.oneme.ru` / `api.tamtam.chat`).
# Дополнительная информация
- [FAQ](faq/readme.md)
- [Документация проекта](https://github.com/openmax-server/docs)
- [Публичные сервера](faq/servers.md)
[Faq](faq/readme.md)
+2 -2
View File
@@ -43,9 +43,9 @@ class ServerConfig:
### Telegram bot
telegram_bot_token = os.getenv("telegram_bot_token") or "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
telegram_bot_enabled = bool(int(os.getenv("telegram_bot_enabled", 0)))
telegram_bot_enabled = bool(os.getenv("telegram_bot_enabled")) or True
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)))
telegram_whitelist_enabled = bool(os.getenv("telegram_whitelist_enabled")) or True
### origins
origins = [x.strip() for x in os.getenv("origins", "").split(",") if x.strip()] if os.getenv("origins") else None
-1
View File
@@ -44,7 +44,6 @@ 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
+2 -2
View File
@@ -6,9 +6,9 @@ class SQLQueries:
INSERT_USER = """
INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username,
(phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
INSERT_USER_DATA = """
-63
View File
@@ -1,63 +0,0 @@
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)
+1 -22
View File
@@ -13,9 +13,6 @@ 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"
@@ -83,25 +80,7 @@ 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": "Вы не можете написать этому пользователю"
},
}
}
### Сообщения бота
+45 -121
View File
@@ -1,7 +1,6 @@
import json
import random
import secrets
import time
import os
import geoip2.database
@@ -10,46 +9,19 @@ 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=None,
phone=None,
id=1,
phone=70000000000,
avatarUrl=None,
photoId=None,
updateTime=None,
firstName=None,
lastName=None,
options=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
description=None,
accountStatus=None,
profileOptions=None,
accountStatus=0,
profileOptions=[],
includeProfileOptions=True,
username=None,
@@ -73,8 +45,6 @@ class Tools:
],
"options": options,
"accountStatus": accountStatus,
"location": "RU",
"registrationTime": int(time.time() * 1000)
}
if avatarUrl:
@@ -108,40 +78,22 @@ class Tools:
def generate_profile_tt(
self,
id=None,
phone=None,
id=1,
phone=70000000000,
avatarUrl=None,
photoId=None,
updateTime=None,
firstName=None,
lastName=None,
options=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
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": name, "type": "TT"}],
"names": [{"name": f"{firstName} {lastName}", "type": "TT"}],
"options": options,
}
@@ -153,19 +105,8 @@ class Tools:
if description:
contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username:
contact["link"] = "https://tt.me/" + username
if custom_firstname:
contact["names"].append(
{"name": custom_name, "type": "CUSTOM"}
)
if blocked:
contact["status"] = "BLOCKED"
contact["link"] = "https://tamtam.chat/" + username
return contact
@@ -263,28 +204,27 @@ class Tools:
if include_favourites:
# Получаем последнее сообщение из избранного
favouriteChatId = -senderId
message, messageTime = await self.get_last_message(
favouriteChatId, db_pool, protocol_type=protocol_type
senderId, db_pool, protocol_type=protocol_type
)
# ID избранного для клиента
# ID избранного
chatId = senderId ^ senderId
# Получаем последнюю активность в избранном
# Получаем последнюю активность участника (отправителя) в избранном
participants = await self.get_participant_last_activity(
favouriteChatId, [senderId], db_pool
senderId, [senderId], db_pool
)
# Получаем ID предыдущего сообщения для избранного
# Получаем ID предыдущего сообщения для избранного (чат ID = senderId)
prevMessageId = await self.get_previous_message_id(
favouriteChatId, db_pool, protocol_type=protocol_type
senderId, db_pool, protocol_type=protocol_type
)
# Хардкодим в лист чатов избранное
chats.append(
self.generate_chat(
chatId,
chatId if protocol_type == "mobile" else str(chatId),
senderId,
"DIALOG",
participants,
@@ -430,14 +370,10 @@ class Tools:
last_message_id = row.get("id") or 0 # последнее 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)",
"INSERT INTO `messages` (chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
(
message_id,
chatId,
senderId,
message_time,
@@ -449,6 +385,8 @@ class Tools:
),
)
message_id = cursor.lastrowid
# Возвращаем айдишки
return int(message_id), int(last_message_id), message_time
@@ -468,8 +406,24 @@ 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": {},
"link": {}
}
# Возвращаем
return self.build_message_dict(row, protocol_type), int(row.get("time"))
return message, int(row.get("time"))
async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
"""Получение ID предыдущего сообщения (второго с конца) в чате."""
@@ -607,34 +561,4 @@ class Tools:
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
return "Localhost Federation"
+68 -34
View File
@@ -1,14 +1,10 @@
# Импортирование библиотек
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
@@ -16,6 +12,71 @@ 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():
"""Инициализация базы данных"""
@@ -53,6 +114,7 @@ def init_ssl():
# Возвращаем
return ssl_context
def set_logging():
"""Настройка уровня логирования"""
# Настройка уровня логирования
@@ -113,39 +175,11 @@ async def main():
api["telegram_bot"] = controllers["telegrambot"]
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)
tasks = [controller.launch(api) for controller in controllers.values()]
# Запускаем контроллеры
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()
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
+106 -320
View File
@@ -3,334 +3,120 @@ class OnemeConfig:
pass
SERVER_CONFIG = {
"account-nickname-enabled": False,
"account-removal-enabled": False,
"anr-config": {
"enabled": True,
"timeout": {
"low": 5000,
"avg": 5000,
"high": 5000
}
"async-tracer": 0,
"presence-ttl": 300,
"non-contact-sync-time": 86400,
"contact-batching-variant": 0,
"account-nickname-enabled": True,
"web-ad-banner": {
"enabled": False
},
"appearance-multi-theme-screen-enabled": True,
"audio-transcription-locales": [],
"available-complaints": [
"FAKE",
"SPAM",
"PORNO",
"EXTREMISM",
"THREAT",
"OTHER"
],
"avatars-screen-enabled": True,
"bad-networ-indicator-config": {
"signalingConfig": {
"dcReportNetworkStatEnabled": False
}
},
"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,
"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,
"inline-ev-player": True,
"invalidate-db-msg-exception": True,
"invite-friends-sheet-frequency": [
2,
7
],
"invite-link": "",
"edit-timeout": 0,
"reactions-menu": [],
"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,
"calls-endpoint": "",
"calls-test-domain": "",
"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,
"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,
"reactions-menu": [
"👍",
"❤️",
"🤣",
"🔥",
"😭",
"💯",
"💩",
"😡"
],
"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
},
"lebedev-theme-enabled": True,
"quotes-enabled": True,
"channels-complaint-enabled": True,
"reactions-settings-enabled": True,
"reconnect-call-ringtone": True,
"ringtone-am-mode": True,
"saved-messages-aliases": [
"избранное",
"saved",
"favourite",
"favorite",
"личное",
"моё",
"мои",
"мой",
"моя",
"любимое",
"сохраненные",
"сохраненное",
"заметки",
"закладки"
],
"channel-statistics-botid": 0,
"enable-unknown-contact-bottom-sheet": 0,
"informer-enabled": True,
"family-protection-botid": 0,
"new-year-theme-2026": True,
"scheduled-messages-enabled": True,
"scheduled-posts-enabled": 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,
"scheduled-faves-enabled": True,
"non-contact-complaints-enabled": True,
"join-requests": True,
"web-persistent-cache": False,
"create-channel-type-screen": 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": [],
"wm-analytics-enabled": True,
"wm-workers-limit": 80,
"wud": False,
"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,
"y-map": {
"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"
"tile": "",
"geocoder": "",
"static": ""
},
"has-phone": True
}
"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
}
+10 -22
View File
@@ -10,19 +10,13 @@ from common.opcodes import Opcodes
class OnemeController(ControllerBase):
def __init__(self):
self.config = ServerConfig()
self.proto_tcp = MobileProto()
self.proto_web = WebProto()
self.proto = MobileProto()
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"):
@@ -47,7 +41,7 @@ class OnemeController(ControllerBase):
}
# Создаем пакет
packet = proto.pack_packet(
packet = self.proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_MESSAGE, payload=payload
)
elif eventType == "typing":
@@ -64,7 +58,7 @@ class OnemeController(ControllerBase):
}
# Создаем пакет
packet = proto.pack_packet(
packet = self.proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_TYPING, payload=payload
)
elif eventType == "profile_updated":
@@ -77,7 +71,7 @@ class OnemeController(ControllerBase):
}
# Создаем пакет
packet = proto.pack_packet(
packet = self.proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PROFILE, payload=payload
)
elif eventType == "presence":
@@ -91,18 +85,13 @@ class OnemeController(ControllerBase):
"time": event_time
}
packet = proto.pack_packet(
packet = self.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()
# Отправляем пакет
writer.write(packet)
await writer.drain()
def launch(self, api):
async def _start_all():
@@ -119,11 +108,10 @@ 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'],
clients=api['clients'],
send_event=api['event'],
telegram_bot=api.get('telegram_bot'),
send_event=api['event']
).start()
)
+1 -31
View File
@@ -54,32 +54,6 @@ 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
@@ -171,12 +145,8 @@ class ContactListPayloadModel(pydantic.BaseModel):
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
firstName: str
lastName: str = None
+2 -2
View File
@@ -2,7 +2,7 @@ from .assets import AssetsProcessors
from .auth import AuthProcessors
from .calls import CallsProcessors
from .chats import ChatsProcessors
from .complaints import ComplaintsProcessors
from .complains import ComplainsProcessors
from .contacts import ContactsProcessors
from .folders import FoldersProcessors
from .history import HistoryProcessors
@@ -16,7 +16,7 @@ class Processors(
AuthProcessors,
CallsProcessors,
ChatsProcessors,
ComplaintsProcessors,
ComplainsProcessors,
ContactsProcessors,
FoldersProcessors,
HistoryProcessors,
+13 -149
View File
@@ -1,167 +1,31 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
from oneme.models import AssetsPayloadModel
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
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,
},
],
# Данные пакета
payload = {
"sections": [],
"sync": int(time.time() * 1000)
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
)
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)
# Отправляем
await self._send(writer, packet)
+11 -14
View File
@@ -297,7 +297,7 @@ 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 + str(photoId)
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = (
None if not account.get("description") else account.get("description")
)
@@ -397,19 +397,15 @@ 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(
"""
INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username,
(phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
user_id,
phone,
None,
first_name,
@@ -423,6 +419,8 @@ class AuthProcessors(BaseProcessor):
),
)
user_id = cursor.lastrowid
# Добавляем данные аккаунта
await cursor.execute(
"""
@@ -521,7 +519,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer
)
return None, None, None
return
# Чаты, где состоит пользователь
chats = []
@@ -545,7 +543,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer
)
return None, None, None
return
# Ищем аккаунт пользователя в бд
await cursor.execute(
@@ -563,7 +561,7 @@ 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()
@@ -578,7 +576,7 @@ class AuthProcessors(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 + str(photoId)
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion):
@@ -633,9 +631,8 @@ class AuthProcessors(BaseProcessor):
"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
)
@@ -3,7 +3,7 @@ import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ComplainReasonsGetPayloadModel
class ComplaintsProcessors(BaseProcessor):
class ComplainsProcessors(BaseProcessor):
async def complain_reasons_get(self, payload, seq, writer):
"""Обработчик получения причин жалоб"""
# Валидируем данные пакета
+3 -227
View File
@@ -2,7 +2,7 @@ import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ContactAddByPhonePayloadModel, ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel
from oneme.models import ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel
class ContactsProcessors(BaseProcessor):
async def contact_list(self, payload, seq, writer, userId):
@@ -95,7 +95,7 @@ class ContactsProcessors(BaseProcessor):
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.USER_NOT_FOUND, writer)
return
# Проверяем, не добавлен ли уже контакт
@@ -112,26 +112,9 @@ class ContactsProcessors(BaseProcessor):
"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)
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_EXISTS, writer)
return
# Генерируем профиль
@@ -178,213 +161,6 @@ class ContactsProcessors(BaseProcessor):
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):
"""Обработчик получения статуса контактов"""
# Валидируем данные пакета
+33 -14
View File
@@ -1,6 +1,5 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ChatHistoryPayloadModel
@@ -21,21 +20,19 @@ 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 отправителя
isFavourite = chatId == (senderId ^ senderId)
if isFavourite:
chatId = -senderId
# то выставляем в качестве ID чата его ID
if chatId == 0:
chatId = senderId
# Проверяем, существует ли чат
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем состоит ли пользователь в чате,
# только в случае того, если это не избранное
if not isFavourite:
if chatId != senderId:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
@@ -61,8 +58,21 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
messages.append(self.tools.build_message_dict(row, self.type))
backward_count = len(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"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"link": {},
#"options": 1,
})
if forward > 0:
await cursor.execute(
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s",
@@ -72,19 +82,28 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
messages.append(self.tools.build_message_dict(row, self.type))
forward_count = len(result)
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"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"link": {}
#"options": 1,
})
# Сортируем сообщения по времени
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
+1 -1
View File
@@ -153,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 + str(photoId)
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description")
# Генерируем профиль
+11 -23
View File
@@ -88,9 +88,10 @@ class MessagesProcessors(BaseProcessor):
chatId = userId ^ senderId
# Если клиент хочет отправить сообщение в избранное,
# то выставляем в качестве ID чата отрицательный ID отправителя
if chatId == (senderId ^ senderId):
chatId = -senderId
# то выставляем в качестве ID чата ID отправителя
# (А ещё используем это, если клиент вообще ничего не указал)
if chatId == 0 or not chatId:
chatId = senderId
participants = [senderId]
else:
# Если все таки клиент хочет отправить сообщение в нормальный чат,
@@ -113,14 +114,6 @@ 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,
@@ -133,21 +126,16 @@ class MessagesProcessors(BaseProcessor):
db_pool=self.db_pool
)
# Готовое тело сообщения. Поля cid / elements / reactionInfo / link
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
# ожидает фиксированную msgpack-схему и обрывает соединение
# при отсутствии любого из них (см. регрессию из 87cfc19).
# Готовое тело сообщения
bodyMessage = {
"id": messageId if self.type == "mobile" else str(messageId),
"cid": int(cid or 0),
"id": messageId,
"time": messageTime,
"type": "USER",
"sender": senderId,
"cid": cid,
"text": text,
"attaches": attaches if isinstance(attaches, list) else [],
"elements": elements if isinstance(elements, list) else [],
"reactionInfo": {},
"link": {},
"attaches": attaches,
"elements": elements
}
# Отправляем событие всем участникам чата
@@ -156,7 +144,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,
@@ -166,7 +154,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
+5 -5
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 + str(photoId)
avatar_url = None if not photoId else self.config.avatar_base_url + 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 + str(photoId)
avatar_url = None if not photoId else self.config.avatar_base_url + 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
# Добавляем чат в список
+2 -63
View File
@@ -168,54 +168,6 @@ 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,
@@ -359,15 +311,6 @@ class OnemeMobile:
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,
@@ -488,9 +431,5 @@ class OnemeMobile:
self.logger.info(f"Сокет запущен на порту {self.port}")
try:
async with self.server:
await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
async with self.server:
await self.server.serve_forever()
+6 -79
View File
@@ -2,7 +2,6 @@ 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
@@ -10,9 +9,7 @@ from common.opcodes import Opcodes
from common.tools import Tools
class OnemeWS:
def __init__(
self, host, port, ssl_context, db_pool, clients, send_event, telegram_bot
):
def __init__(self, host, port, clients, ssl_context, db_pool, send_event):
self.host = host
self.port = port
self.ssl_context = ssl_context
@@ -24,13 +21,7 @@ class OnemeWS:
self.opcodes = Opcodes()
self.proto = WebProto()
self.processors = Processors(
db_pool=db_pool,
clients=clients,
send_event=send_event,
telegram_bot=telegram_bot,
type="web"
)
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, type="web")
self.auth_required = Tools().auth_required
# rate limiter
@@ -128,7 +119,7 @@ class OnemeWS:
userPhone,
userId,
hashedToken,
) = await self.processors.login(payload, seq, websocket, appVersion)
) = await self.processors.login(payload, seq, websocket, deviceType, appVersion)
if userPhone:
await self._finish_auth(
@@ -151,54 +142,6 @@ 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,
@@ -342,15 +285,6 @@ class OnemeWS:
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,
@@ -385,8 +319,7 @@ class OnemeWS:
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "oneme",
"type": "web"
"protocol": "oneme"
}
)
else:
@@ -400,8 +333,7 @@ class OnemeWS:
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "oneme",
"type": "web"
"protocol": "oneme"
}
]
}
@@ -478,9 +410,4 @@ class OnemeWS:
self.logger.info(f"WebSocket запущен на порту {self.port}")
try:
await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise
await self.server.wait_closed()
-96
View File
@@ -3,106 +3,10 @@ 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():
+2 -44
View File
@@ -54,32 +54,6 @@ class AssetsPayloadModel(pydantic.BaseModel):
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
@@ -102,7 +76,7 @@ class ContactPresencePayloadModel(pydantic.BaseModel):
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int
firstName: str = None
firstName: str
lastName: str = None
class TypingPayloadModel(pydantic.BaseModel):
@@ -120,20 +94,4 @@ class MessageModel(pydantic.BaseModel):
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
message: MessageModel
+13 -146
View File
@@ -1,167 +1,34 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
from tamtam.models import AssetsPayloadModel
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
response = {
# Данные пакета
payload = {
"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,
},
],
"sections": [],
"stickersOrder": []
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
)
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)
# Отправляем
await self._send(writer, packet)
+71 -262
View File
@@ -4,12 +4,10 @@ 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
@@ -19,172 +17,6 @@ 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):
"""Обработчик запроса кода"""
# Валидируем данные пакета
@@ -198,51 +30,29 @@ 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 = {
@@ -261,7 +71,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
self.logger.debug(f"Код для {phone}: {code}")
async def auth(self, payload, seq, writer):
"""Обработчик проверки кода"""
@@ -302,32 +112,13 @@ 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)
)
@@ -368,7 +159,15 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, ip):
"""Обработчик финальной аутентификации / регистрации"""
"""Обработчик финальной аутентификации"""
# Валидируем данные пакета
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
# Извлекаем данные из пакета
token = payload.get("token")
@@ -385,9 +184,10 @@ 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,)
@@ -399,36 +199,63 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer)
return
# Если авторизация только началась (код ещё не проверен) - отдаем ошибку
if stored_token.get("state") == "started" or stored_token.get("state") == "register":
# Если авторизация только началась - отдаем ошибку
if stored_token.get("state") == "started":
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer)
return
phone = stored_token.get("phone")
# Проверяем, существует ли пользователь
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
# Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
account = await cursor.fetchone()
# Если пользователь есть, производим создание сессии
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("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
if resp_payload is None:
return
# Создаем сессию
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,
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")
# Собираем данные пакета
payload = {
# Я хз че сюда вставлять)
# ребята из одноклассников, может быть вы подскажете?
"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
}
# Создаем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=resp_payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload
)
# Отправляем
@@ -443,7 +270,7 @@ class AuthProcessors(BaseProcessor):
self.logger.error(f"Возникли ошибки при валидации пакета: {e}")
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_PAYLOAD, writer)
return None, None, None
return
# Чаты, где состоит пользователь
chats = []
@@ -464,7 +291,7 @@ class AuthProcessors(BaseProcessor):
if token_data is None:
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_TOKEN, writer)
return None, None, None
return
# Ищем аккаунт пользователя в бд
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
@@ -476,8 +303,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()
@@ -580,24 +407,6 @@ class AuthProcessors(BaseProcessor):
"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
+1 -114
View File
@@ -114,7 +114,7 @@ class ContactsProcessors(BaseProcessor):
)
# а если уже существует, отправляем ошибку
else:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_ADDED, writer)
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_EXISTS, writer)
return
# Генерируем профиль
@@ -161,119 +161,6 @@ class ContactsProcessors(BaseProcessor):
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):
"""Обработчик получения статуса контактов"""
# Валидируем данные пакета
+2 -10
View File
@@ -105,14 +105,6 @@ 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,
@@ -143,7 +135,7 @@ class MessagesProcessors(BaseProcessor):
participant,
{
"eventType": "new_msg",
"chatId": chatId,
"chatId": 0 if chatId == senderId else chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
@@ -153,7 +145,7 @@ class MessagesProcessors(BaseProcessor):
# Данные пакета
payload = {
"chatId": chatId,
"chatId": 0 if chatId == senderId else chatId,
"message": bodyMessage,
"unread": 0,
"mark": messageTime
+4 -34
View File
@@ -122,30 +122,6 @@ class TamTamMobile:
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
@@ -223,8 +199,7 @@ class TamTamMobile:
"writer": writer,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam",
"type": "tcp"
"protocol": "tamtam"
}
)
else:
@@ -236,8 +211,7 @@ class TamTamMobile:
"writer": writer,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam",
"type": "tcp"
"protocol": "tamtam"
}
]
}
@@ -265,9 +239,5 @@ class TamTamMobile:
self.logger.info(f"Сокет запущен на порту {self.port}")
try:
async with self.server:
await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
async with self.server:
await self.server.serve_forever()
+3 -35
View File
@@ -1,7 +1,6 @@
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
@@ -109,30 +108,6 @@ class TamTamWS:
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
@@ -211,8 +186,7 @@ class TamTamWS:
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam",
"type": "web"
"protocol": "tamtam"
}
)
else:
@@ -224,8 +198,7 @@ class TamTamWS:
"writer": websocket,
"ip": addr[0],
"port": addr[1],
"protocol": "tamtam",
"type": "web"
"protocol": "tamtam"
}
]
}
@@ -256,9 +229,4 @@ class TamTamWS:
self.logger.info(f"TT WebSocket запущен на порту {self.port}")
try:
await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise
await self.server.wait_closed()
+4 -12
View File
@@ -83,24 +83,16 @@ 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,
(
user_id, # id
new_phone, # phone
tg_id, # telegram_id
firstname, # firstname
lastname, # lastname
username, # username
message.from_user.first_name[:59], # firstname
(message.from_user.last_name or "")[:59], # lastname
(message.from_user.username or "")[:60], # username
json.dumps([]), # profileoptions
json.dumps(["TT", "ONEME"]), # options
0, # accountstatus
@@ -139,7 +131,7 @@ class TelegramBot:
async def start(self):
if self.enabled:
try:
await self.dp.start_polling(self.bot, handle_signals=False)
await self.dp.start_polling(self.bot)
except Exception as e:
self.logger.error(f"Ошибка запуска Telegram бота: {e}")
else:
+3 -3
View File
@@ -1,7 +1,7 @@
CREATE TABLE `users` (
`id` INT NOT NULL,
`id` INT NOT NULL AUTO_INCREMENT,
`phone` VARCHAR(20) UNIQUE,
`telegram_id` VARCHAR(64),
`telegram_id` VARCHAR(64) UNIQUE,
`firstname` VARCHAR(59) NOT NULL,
`lastname` VARCHAR(59),
`description` VARCHAR(400),
@@ -51,7 +51,7 @@ CREATE TABLE `chats` (
);
CREATE TABLE `messages` (
`id` BIGINT NOT NULL,
`id` INT NOT NULL AUTO_INCREMENT,
`chat_id` INT NOT NULL,
`sender` INT NOT NULL,
`time` VARCHAR(32) NOT NULL,