Compare commits

..

49 Commits

Author SHA1 Message Date
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
48 changed files with 798 additions and 3514 deletions
-2
View File
@@ -24,9 +24,7 @@ domain = "openmax.su"
avatar_base_url = "http://127.0.0.1/avatar/" avatar_base_url = "http://127.0.0.1/avatar/"
telegram_bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ" telegram_bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
telegram_bot_enabled = "1" telegram_bot_enabled = "1"
telegram_whitelist_enabled = "1"
telegram_whitelist_ids = "1,2,3" telegram_whitelist_ids = "1,2,3"
origins="http://127.0.0.1,https://web.openmax.su" origins="http://127.0.0.1,https://web.openmax.su"
sms_gateway_url = "http://127.0.0.1:8100/sms-gateway" sms_gateway_url = "http://127.0.0.1:8100/sms-gateway"
firebase_credentials_path = "" firebase_credentials_path = ""
geo_db_path = ""
-1
View File
@@ -4,4 +4,3 @@ __pycache__
*.sqlite *.sqlite
*.crt *.crt
*-adminsdk-*.json *-adminsdk-*.json
*.mmdb
-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 # Навигация по faq
## Работа с сервером ## Работа с сервером
- [Установка сервера](install.md) [Установка сервера](install.md)
## Патчинг клиентов ## Патчинг клиентов
- [Патч мобильного клиента для Android](patch_apk.md) [Патч apk](patch_apk.md)
- [Патч мобильного клиента для IOS](patch_ipa.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] > [!Caution]
> >
> Проект находится на ранней стадии разработки и, вероятно, полон багов. > Проект находится на ранней стадии разработки и вероятно полон багов.
> >
> Использование в профессиональных средах не рекомендовано. > Использование в профессиональных средах не рекомендовано.
@@ -13,7 +13,7 @@ https://t.me/openmax_alerts
# Требования # Требования
- Python 3.12+ (поддержка версий ниже не гарантирована) - Python 3.12+ (поддержка версий ниже не гарантирована)
- MariaDB, MySQL или SQLite (использование последнего не рекомендуется, так как поддержка ещё в разработке) - MariaDB, MySQL или SQLite
- Уметь патчить клиент MAX или собирать Komet из исходного кода (естественно с заменой сервера) - Уметь патчить клиент MAX или собирать Komet из исходного кода (естественно с заменой сервера)
- Сертификат и приватный ключ X.509 (для тестирования сервера можно сгенерировать самоподписанный: ```openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365```) - Сертификат и приватный ключ 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`). Клиент может быть практически любым, главное условие - чтобы он был совместим с официальным сервером (`api.oneme.ru` / `api.tamtam.chat`).
# Дополнительная информация # Дополнительная информация
- [FAQ](faq/readme.md) [Faq](faq/readme.md)
- [Документация проекта](https://github.com/openmax-server/docs)
- [Публичные сервера](faq/servers.md)
+11 -12
View File
@@ -1,12 +1,11 @@
aiogram==3.26.0 aiogram
aiomysql==0.3.2 aiomysql
msgpack==1.1.2 msgpack
lz4==4.4.5 lz4
websockets==16.0 websockets
pydantic==2.12.5 pydantic
aiosqlite==0.22.1 aiosqlite
aiohttp==3.13.5 aiohttp
python-dotenv==1.2.2 python-dotenv
cryptography==46.0.6 cryptography
firebase-admin==7.4.0 firebase-admin
geoip2==5.2.0
+1 -5
View File
@@ -43,9 +43,8 @@ class ServerConfig:
### Telegram bot ### Telegram bot
telegram_bot_token = os.getenv("telegram_bot_token") or "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ" 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_ids = [x.strip() for x in os.getenv("telegram_whitelist_ids", "").split(",") if x.strip()]
telegram_whitelist_enabled = bool(int(os.getenv("telegram_whitelist_enabled", 0)))
### origins ### origins
origins = [x.strip() for x in os.getenv("origins", "").split(",") if x.strip()] if os.getenv("origins") else None origins = [x.strip() for x in os.getenv("origins", "").split(",") if x.strip()] if os.getenv("origins") else None
@@ -55,6 +54,3 @@ class ServerConfig:
### Firebase ### Firebase
firebase_credentials_path = os.getenv("firebase_credentials_path", "") firebase_credentials_path = os.getenv("firebase_credentials_path", "")
### Путь к гео бд
geo_db_path = os.getenv("geo_db_path", "")
-1
View File
@@ -44,7 +44,6 @@ class Opcodes:
CONTACT_MUTUAL = 38 CONTACT_MUTUAL = 38
CONTACT_PHOTOS = 39 CONTACT_PHOTOS = 39
CONTACT_SORT = 40 CONTACT_SORT = 40
CONTACT_ADD_BY_PHONE = 41
CONTACT_VERIFY = 42 CONTACT_VERIFY = 42
REMOVE_CONTACT_PHOTO = 43 REMOVE_CONTACT_PHOTO = 43
CHAT_INFO = 48 CHAT_INFO = 48
+2 -2
View File
@@ -6,9 +6,9 @@ class SQLQueries:
INSERT_USER = """ INSERT_USER = """
INSERT INTO users INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username, (phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen) 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 = """ 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_FOUND = "chat_not_found"
CHAT_NOT_ACCESS = "chat_not_access" CHAT_NOT_ACCESS = "chat_not_access"
RATE_LIMITED = "rate_limited" RATE_LIMITED = "rate_limited"
CONTACT_NOT_FOUND = "contact_not_found"
CONTACT_ALREADY_ADDED = "contact_already_added"
CONTACT_BLOCKED = "contact_blocked"
class ChatTypes: class ChatTypes:
DIALOG = "DIALOG" DIALOG = "DIALOG"
@@ -83,25 +80,7 @@ class Static:
"error": "error.rate_limited", "error": "error.rate_limited",
"message": "Too many attempts. Please try again later", "message": "Too many attempts. Please try again later",
"title": "Слишком много попыток" "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": "Вы не можете написать этому пользователю"
},
} }
### Сообщения бота ### Сообщения бота
+43 -180
View File
@@ -1,55 +1,24 @@
import json import json
import random
import secrets
import time import time
import geoip2.database
class Tools: class Tools:
def __init__(self): def __init__(self):
pass 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( def generate_profile(
self, self,
id=None, id=1,
phone=None, phone=70000000000,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=None, updateTime=0,
firstName=None, firstName="Test",
lastName=None, lastName="Account",
options=None, options=[],
description=None, description=None,
accountStatus=None, accountStatus=0,
profileOptions=None, profileOptions=[],
includeProfileOptions=True, includeProfileOptions=True,
username=None, username=None,
@@ -73,8 +42,6 @@ class Tools:
], ],
"options": options, "options": options,
"accountStatus": accountStatus, "accountStatus": accountStatus,
"location": "RU",
"registrationTime": int(time.time() * 1000)
} }
if avatarUrl: if avatarUrl:
@@ -108,40 +75,22 @@ class Tools:
def generate_profile_tt( def generate_profile_tt(
self, self,
id=None, id=1,
phone=None, phone=70000000000,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=None, updateTime=0,
firstName=None, firstName="Test",
lastName=None, lastName="Account",
options=None, options=[],
description=None, description=None,
username=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 = { contact = {
"id": id, "id": id,
"updateTime": updateTime, "updateTime": updateTime,
"phone": phone, "phone": phone,
"names": [{"name": name, "type": "TT"}], "names": [{"name": f"{firstName} {lastName}", "type": "TT"}],
"options": options, "options": options,
} }
@@ -153,19 +102,8 @@ class Tools:
if description: if description:
contact["description"] = description contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username: if username:
contact["link"] = "https://tt.me/" + username contact["link"] = "https://tamtam.chat/" + username
if custom_firstname:
contact["names"].append(
{"name": custom_name, "type": "CUSTOM"}
)
if blocked:
contact["status"] = "BLOCKED"
return contact return contact
@@ -263,28 +201,27 @@ class Tools:
if include_favourites: if include_favourites:
# Получаем последнее сообщение из избранного # Получаем последнее сообщение из избранного
favouriteChatId = -senderId
message, messageTime = await self.get_last_message( 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 chatId = senderId ^ senderId
# Получаем последнюю активность в избранном # Получаем последнюю активность участника (отправителя) в избранном
participants = await self.get_participant_last_activity( 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( prevMessageId = await self.get_previous_message_id(
favouriteChatId, db_pool, protocol_type=protocol_type senderId, db_pool, protocol_type=protocol_type
) )
# Хардкодим в лист чатов избранное # Хардкодим в лист чатов избранное
chats.append( chats.append(
self.generate_chat( self.generate_chat(
chatId, chatId if protocol_type == "mobile" else str(chatId),
senderId, senderId,
"DIALOG", "DIALOG",
participants, participants,
@@ -395,7 +332,7 @@ class Tools:
async with db_pool.acquire() as db_connection: async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor: async with db_connection.cursor() as cursor:
await cursor.execute( await cursor.execute(
"SELECT * FROM `contacts` WHERE owner_id = %s AND is_blocked = FALSE", "SELECT * FROM `contacts` WHERE owner_id = %s",
(owner_id,), (owner_id,),
) )
rows = await cursor.fetchall() rows = await cursor.fetchall()
@@ -430,14 +367,10 @@ class Tools:
last_message_id = row.get("id") or 0 # последнее id сообщения в чате last_message_id = row.get("id") or 0 # последнее id сообщения в чате
message_time = int(time.time() * 1000) # время отправки сообщения message_time = int(time.time() * 1000) # время отправки сообщения
# Генерируем ID сообщения
message_id = int(time.time() * 1000000) * 1000 + random.randint(100, 999)
# Вносим новое сообщение в таблицу # Вносим новое сообщение в таблицу
await cursor.execute( 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, chatId,
senderId, senderId,
message_time, message_time,
@@ -449,6 +382,8 @@ class Tools:
), ),
) )
message_id = cursor.lastrowid
# Возвращаем айдишки # Возвращаем айдишки
return int(message_id), int(last_message_id), message_time return int(message_id), int(last_message_id), message_time
@@ -468,8 +403,23 @@ class Tools:
if not row: if not row:
return None, None return None, None
# Собираем сообщение
message = {
"id": row.get("id")
if protocol_type == "mobile"
else str(row.get("id")),
"time": int(row.get("time")),
"type": row.get("type"),
"sender": row.get("sender"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
}
# Возвращаем # Возвращаем
return 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"): async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
"""Получение ID предыдущего сообщения (второго с конца) в чате.""" """Получение ID предыдущего сообщения (второго с конца) в чате."""
@@ -551,90 +501,3 @@ class Tools:
) )
return updated_config return updated_config
async def collect_presence(self, contact_ids, clients, db_pool):
"""Собирает статусы пользователей"""
now = int(time.time())
presence = {}
# Список тех, кого нужно поискать в базе данных
db_lookup_ids = []
# Проходимся по всем айдишникам,
# которые передал нам клиент
for contact_id in contact_ids:
contact_id = int(contact_id)
client = clients.get(contact_id)
# Если пользователь онлайн
if client and client.get("status") == 2:
presence[str(contact_id)] = {"seen": now, "status": 2}
# Если пользователь подключен,
# но не взаимодействует с клиентом
elif client and client.get("last_seen"):
presence[str(contact_id)] = {"seen": client.get("last_seen")}
# А если никакое условие не подошло, то добавляем его в лист,
# а позже посмотрим в базе данных
else:
db_lookup_ids.append(contact_id)
# Проходимся по листу и добавляем недостающих,
# если такие существуют конечно
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
for contact_id in db_lookup_ids:
await cursor.execute(
"SELECT lastseen FROM users WHERE id = %s",
(contact_id,)
)
row = await cursor.fetchone()
if row:
lastseen = row.get("lastseen")
presence[int(contact_id)] = {"seen": int(lastseen)}
return presence
def get_geo(self, ip, db_path):
"""
Получение страны пользователя по его айпи адресу
Используется во время запуска сессии
"""
try:
with geoip2.database.Reader(db_path) as reader:
response = reader.country(ip)
return response.country.name or "Localhost Federation"
except Exception:
return "Localhost Federation"
async def generate_user_id(self, db_pool):
"""Генерация id пользователя"""
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
while True:
user_id = secrets.randbelow(2_147_383_647) + 100_000
await cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
if not await cursor.fetchone():
return user_id
async def contact_is_blocked(self, owner_id, contact_id, db_pool):
"""
По изначальной задумке, данная функция должна проверять, заблокирован ли контакт
На сервере долгое время не был доделан черный список, хотя управление им было реализовано
(на деле, это я поленился)
Вернёт вам true, если контакт заблокирован, иначе false
"""
# Проверяем наличие контакта
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s AND is_blocked = %s", (owner_id, contact_id, True))
row = await cursor.fetchone()
# Есди контакт существует и заблокирован, возвращаем true,
if row:
return True
else: # в ином случае false
return False
+68 -34
View File
@@ -1,14 +1,10 @@
# Импортирование библиотек # Импортирование библиотек
import asyncio import asyncio
import logging import logging
import signal
import ssl import ssl
import sys
import traceback
from common.config import ServerConfig from common.config import ServerConfig
from common.push import PushService from common.push import PushService
from common.sqlite import SQLitePoolCompat
from oneme.controller import OnemeController from oneme.controller import OnemeController
from tamtam.controller import TTController from tamtam.controller import TTController
from telegrambot.controller import TelegramBotController from telegrambot.controller import TelegramBotController
@@ -16,6 +12,71 @@ from telegrambot.controller import TelegramBotController
# Конфиг сервера # Конфиг сервера
server_config = ServerConfig() 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(): async def init_db():
"""Инициализация базы данных""" """Инициализация базы данных"""
@@ -53,6 +114,7 @@ def init_ssl():
# Возвращаем # Возвращаем
return ssl_context return ssl_context
def set_logging(): def set_logging():
"""Настройка уровня логирования""" """Настройка уровня логирования"""
# Настройка уровня логирования # Настройка уровня логирования
@@ -113,39 +175,11 @@ async def main():
api["telegram_bot"] = controllers["telegrambot"] api["telegram_bot"] = controllers["telegrambot"]
loop = asyncio.get_running_loop() tasks = [controller.launch(api) for controller in controllers.values()]
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)
# Запускаем контроллеры # Запускаем контроллеры
try: await asyncio.gather(*tasks)
await asyncio.gather(*running_tasks)
except asyncio.CancelledError:
logging.info("Все задачи завершены")
except Exception as e:
logging.error(
f"Произошла неизвестная ошибка: {e}"
)
traceback.print_exc()
finally:
if hasattr(db, 'close'):
db.close()
await db.wait_closed()
elif hasattr(db, 'connection') and hasattr(db.connection, 'close'):
await db.connection.close()
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
+104 -318
View File
@@ -3,334 +3,120 @@ class OnemeConfig:
pass pass
SERVER_CONFIG = { SERVER_CONFIG = {
"account-nickname-enabled": False, "async-tracer": 0,
"account-removal-enabled": False, "presence-ttl": 300,
"anr-config": { "non-contact-sync-time": 86400,
"enabled": True, "contact-batching-variant": 0,
"timeout": { "account-nickname-enabled": True,
"low": 5000, "web-ad-banner": {
"avg": 5000, "enabled": False
"high": 5000
}
}, },
"appearance-multi-theme-screen-enabled": True, "edit-timeout": 0,
"audio-transcription-locales": [], "reactions-menu": [],
"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": "",
"invite-long": "", "invite-long": "",
"invite-short": "", "calls-endpoint": "",
"join-requests": True, "calls-test-domain": "",
"js-download-delegate": False,
"keep-connection": 2,
"lebedev-theme-enabled": True,
"lgce": True,
"markdown-enabled": True,
"markdown-menu": 0,
"max-audio-length": 3600,
"max-description-length": 400,
"max-favorite-chats": 5,
"max-favorite-sticker-sets": 100,
"max-favorite-stickers": 100,
"max-msg-length": 4000,
"max-participants": 20000,
"max-readmarks": 100, "max-readmarks": 100,
"max-theme-length": 200, "max-cname-length": 200,
"max-video-duration-download": 1200, "max-description-length": 400,
"max-video-message-length": 60, "new-avatar-gradient-colors-enabled": True,
"media-order": 1, "max-msg-length": 4000,
"media-playlist-enabled": True, "file-upload-unsupported-types": [],
"media-transform": { "file-upload-max-size": 4294967296,
"enabled": True, "image-quality": 0.8,
"hdr_enabled": False, "image-width": 1920,
"hevc_enabled": True, "image-height": 1920,
"max_enc_frames": { "image-size": 10000000,
"low": 1, "max-favorite-chats": 5,
"avg": 1, "bot-complaint-enabled": True,
"high": 2
}
},
"media-viewer-rotation-enabled": True,
"media-viewer-video-collage-enabled": True,
"mentions-enabled": True,
"mentions_entity_names_limit": 3,
"migrate-unsafe-warn": True,
"min-image-side-size": 64,
"miui-menu-enabled": True,
"money-transfer-botid": 1134691,
"moscow-theme-enabled": True,
"msg-get-reactions-page-size": 40,
"music-files-enabled": False,
"mytracker-enabled": True,
"net-client-dns-enabled": True,
"net-session-suppress-bad-disconnected-state": True,
"net-stat-config": [
64,
48,
128,
135
],
"new-admin-permissions": True,
"new-logout-logic": False,
"new-media-upload-ui": True,
"new-media-viewer-enabled": True,
"new-settings-storage-screen-enabled": False,
"new-width-text-bubbles-mob": True,
"new-year-theme-2026": False,
"nick-max-length": 60,
"nick-min-length": 7,
"official-org": True,
"one-video-failover": True,
"one-video-player": True,
"one-video-uploader": True,
"one-video-uploader-audio": True,
"one-video-uploader-progress-fix": True,
"perf-events": {
"startup_report": 2,
"web_app": 2
},
"player-load-control": {
"mp_autoplay_enabled": False,
"time_over_size": False,
"buffer_after_rebuffer_ms": 3000,
"buffer_ms": 500,
"max_buffer_ms": 13000,
"min_buffer_ms": 5000,
"use_min_size_lc": True,
"min_size_lc_fmt_mis_sf": 4
},
"progress-diff-for-notify": 1,
"push-delivery": True,
"qr-auth-enabled": True,
"quotes-enabled": True,
"react-errors": [
"error.comment.chat.access",
"error.comment.invalid",
"error.message.invalid",
"error.message.chat.access",
"error.message.like.unknown.like",
"error.message.like.unknown.reaction",
"error.too-many-unlikes-dialog",
"error.too-many-unlikes-chat",
"error.too-many-likes",
"error.reactions.not.allowed"
],
"react-permission": 2,
"reactions-enabled": True,
"reactions-max": 8, "reactions-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,
"reactions-settings-enabled": True, "calls-web-upload-logs": False,
"reconnect-call-ringtone": True, "calls-video-zoom": False,
"ringtone-am-mode": True, "calls-fullscreen-mode": False,
"saved-messages-aliases": [ "group-call-part-limit": 100,
"избранное", "call-chat-members-load-config": {},
"saved", "cfs": False,
"favourite", "cse": False,
"favorite", "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,
"закладки"
],
"scheduled-messages-enabled": True,
"scheduled-posts-enabled": True,
"search-webapps-showcase": { "search-webapps-showcase": {
"items": [] "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": [], "settings-entry-banners": [],
"show-reactions-on-multiselect": True, "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,
"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,
"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, "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": [], "white-list-links": [],
"wm-analytics-enabled": True, "february-23-26-theme": True,
"wm-workers-limit": 80, "march-8-26-theme": True,
"wud": False, "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": { "y-map": {
"tile": "34c7fd82-723d-4b23-8abb-33376729a893", "tile": "",
"geocoder": "34c7fd82-723d-4b23-8abb-33376729a893", "geocoder": "",
"static": "34c7fd82-723d-4b23-8abb-33376729a893", "static": ""
"logoLight": "https://st.max.ru/icons/ya_maps_logo_light.webp",
"logoDark": "https://st.max.ru/icons/ya_maps_logo_dark.webp"
}, },
"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
} }
+7 -33
View File
@@ -10,19 +10,13 @@ from common.opcodes import Opcodes
class OnemeController(ControllerBase): class OnemeController(ControllerBase):
def __init__(self): def __init__(self):
self.config = ServerConfig() self.config = ServerConfig()
self.proto_tcp = MobileProto() self.proto = MobileProto()
self.proto_web = WebProto()
self.opcodes = Opcodes() self.opcodes = Opcodes()
async def event(self, target, client, eventData): async def event(self, target, client, eventData):
# Извлекаем тип события и врайтер # Извлекаем тип события и врайтер
eventType = eventData.get("eventType") eventType = eventData.get("eventType")
writer = client.get("writer") 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"): 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 cmd=0, seq=1, opcode=self.opcodes.NOTIF_MESSAGE, payload=payload
) )
elif eventType == "typing": 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 cmd=0, seq=1, opcode=self.opcodes.NOTIF_TYPING, payload=payload
) )
elif eventType == "profile_updated": elif eventType == "profile_updated":
@@ -77,30 +71,11 @@ class OnemeController(ControllerBase):
} }
# Создаем пакет # Создаем пакет
packet = proto.pack_packet( packet = self.proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PROFILE, payload=payload 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) writer.write(packet)
await writer.drain() await writer.drain()
@@ -119,11 +94,10 @@ class OnemeController(ControllerBase):
OnemeWS( OnemeWS(
host=self.config.host, host=self.config.host,
port=self.config.oneme_ws_port, port=self.config.oneme_ws_port,
clients=api['clients'],
ssl_context=api['ssl'], ssl_context=api['ssl'],
db_pool=api['db'], db_pool=api['db'],
clients=api['clients'], send_event=api['event']
send_event=api['event'],
telegram_bot=api.get('telegram_bot'),
).start() ).start()
) )
-45
View File
@@ -54,32 +54,6 @@ class AssetsPayloadModel(pydantic.BaseModel):
sync: int sync: int
type: str 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): class GetCallHistoryPayloadModel(pydantic.BaseModel):
forward: bool forward: bool
count: int count: int
@@ -101,12 +75,6 @@ class SendMessagePayloadModel(pydantic.BaseModel):
class SyncFoldersPayloadModel(pydantic.BaseModel): class SyncFoldersPayloadModel(pydantic.BaseModel):
folderSync: int folderSync: int
class CreateFolderPayloadModel(pydantic.BaseModel):
id: str
title: str
filters: list = []
include: list = []
class SearchChatsPayloadModel(pydantic.BaseModel): class SearchChatsPayloadModel(pydantic.BaseModel):
chatIds: list chatIds: list
@@ -167,16 +135,3 @@ class ChatSubscribePayloadModel(pydantic.BaseModel):
class ContactListPayloadModel(pydantic.BaseModel): class ContactListPayloadModel(pydantic.BaseModel):
status: str status: str
count: int = None count: int = None
class ContactPresencePayloadModel(pydantic.BaseModel):
contactIds: list
class ContactAddByPhonePayloadModel(pydantic.BaseModel):
phone: str
firstName: str
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int
firstName: str = None
lastName: str = None
+2 -2
View File
@@ -2,7 +2,7 @@ from .assets import AssetsProcessors
from .auth import AuthProcessors from .auth import AuthProcessors
from .calls import CallsProcessors from .calls import CallsProcessors
from .chats import ChatsProcessors from .chats import ChatsProcessors
from .complaints import ComplaintsProcessors from .complains import ComplainsProcessors
from .contacts import ContactsProcessors from .contacts import ContactsProcessors
from .folders import FoldersProcessors from .folders import FoldersProcessors
from .history import HistoryProcessors from .history import HistoryProcessors
@@ -16,7 +16,7 @@ class Processors(
AuthProcessors, AuthProcessors,
CallsProcessors, CallsProcessors,
ChatsProcessors, ChatsProcessors,
ComplaintsProcessors, ComplainsProcessors,
ContactsProcessors, ContactsProcessors,
FoldersProcessors, FoldersProcessors,
HistoryProcessors, HistoryProcessors,
+13 -149
View File
@@ -1,18 +1,12 @@
import pydantic import pydantic
import time import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ( from oneme.models import AssetsPayloadModel
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor): class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer): async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try: try:
AssetsPayloadModel.model_validate(payload) AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error: except pydantic.ValidationError as error:
@@ -20,148 +14,18 @@ class AssetsProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return return
response = { # TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {}, # Данные пакета
"stickersUpdates": {}, payload = {
"stickersOrder": [ "sections": [],
"RECENT", "sync": int(time.time() * 1000)
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
} }
# Собираем пакет
packet = self.proto.pack_packet( 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)
+16 -27
View File
@@ -197,7 +197,7 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet) await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})") self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
async def auth(self, payload, seq, writer, deviceType, deviceName, appVersion, ip): async def auth(self, payload, seq, writer, deviceType, deviceName, appVersion):
"""Обработчик проверки кода""" """Обработчик проверки кода"""
try: try:
VerifyCodePayloadModel.model_validate(payload) VerifyCodePayloadModel.model_validate(payload)
@@ -285,10 +285,8 @@ class AuthProcessors(BaseProcessor):
hashed_login, hashed_login,
deviceType, deviceType,
deviceName, deviceName,
self.tools.get_geo( "Little Saint James Island",
ip=ip, db_path=self.config.geo_db_path int(time.time()),
),
int(time.time() * 1000),
), # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть ), # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть
) )
@@ -297,7 +295,7 @@ class AuthProcessors(BaseProcessor):
photoId = ( photoId = (
None if not account.get("avatar_id") else int(account.get("avatar_id")) 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 = ( description = (
None if not account.get("description") else account.get("description") None if not account.get("description") else account.get("description")
) )
@@ -335,7 +333,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, packet) await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, appVersion, ip): async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, appVersion):
"""Обработчик подтверждения регистрации нового пользователя""" """Обработчик подтверждения регистрации нового пользователя"""
# Валидируем данные пакета # Валидируем данные пакета
try: try:
@@ -397,19 +395,15 @@ class AuthProcessors(BaseProcessor):
now_ms = int(time.time() * 1000) now_ms = int(time.time() * 1000)
now_s = int(time.time()) now_s = int(time.time())
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем пользователя # Создаем пользователя
await cursor.execute( await cursor.execute(
""" """
INSERT INTO users INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username, (phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen) 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, phone,
None, None,
first_name, first_name,
@@ -423,6 +417,8 @@ class AuthProcessors(BaseProcessor):
), ),
) )
user_id = cursor.lastrowid
# Добавляем данные аккаунта # Добавляем данные аккаунта
await cursor.execute( await cursor.execute(
""" """
@@ -460,10 +456,8 @@ class AuthProcessors(BaseProcessor):
hashed_login, hashed_login,
deviceType or "ANDROID", deviceType or "ANDROID",
deviceName or "Unknown", deviceName or "Unknown",
self.tools.get_geo( "Little Saint James Island",
ip=ip, db_path=self.config.geo_db_path now_s,
),
now_ms,
), ),
) )
@@ -521,7 +515,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error( await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer
) )
return None, None, None return
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
chats = [] chats = []
@@ -545,7 +539,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error( await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer
) )
return None, None, None return
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
await cursor.execute( await cursor.execute(
@@ -563,7 +557,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь # Ищем все чаты, где состоит пользователь
await cursor.execute( await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s", "SELECT * FROM chat_participants WHERE user_id = %s",
(user.get("id"),), (user.get("id")),
) )
user_chats = await cursor.fetchall() user_chats = await cursor.fetchall()
@@ -578,7 +572,7 @@ class AuthProcessors(BaseProcessor):
# Аватарка с биографией # Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) 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") description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion): if self._check_legacy_version(appVersion):
@@ -613,10 +607,6 @@ class AuthProcessors(BaseProcessor):
user.get("id"), self.db_pool, self.config.avatar_base_url user.get("id"), self.db_pool, self.config.avatar_base_url
) )
# Собираем статусы контактов
contact_ids = [c.get("id") for c in contacts if c.get("id") is not None]
presence = await self.tools.collect_presence(contact_ids, self.clients, self.db_pool)
# Формируем данные пакета # Формируем данные пакета
payload = { payload = {
"profile": profile, "profile": profile,
@@ -624,9 +614,8 @@ class AuthProcessors(BaseProcessor):
"chatMarker": 0, "chatMarker": 0,
"messages": {}, "messages": {},
"contacts": contacts, "contacts": contacts,
"presence": presence, "presence": {},
"config": { "config": {
"hash": "0",
"server": self.server_config, "server": self.server_config,
"user": updated_user_config, "user": updated_user_config,
}, },
@@ -3,7 +3,7 @@ import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ComplainReasonsGetPayloadModel from oneme.models import ComplainReasonsGetPayloadModel
class ComplaintsProcessors(BaseProcessor): class ComplainsProcessors(BaseProcessor):
async def complain_reasons_get(self, payload, seq, writer): async def complain_reasons_get(self, payload, seq, writer):
"""Обработчик получения причин жалоб""" """Обработчик получения причин жалоб"""
# Валидируем данные пакета # Валидируем данные пакета
+1 -347
View File
@@ -1,8 +1,6 @@
import pydantic import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ContactAddByPhonePayloadModel, ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel from oneme.models import ContactListPayloadModel
class ContactsProcessors(BaseProcessor): class ContactsProcessors(BaseProcessor):
async def contact_list(self, payload, seq, writer, userId): async def contact_list(self, payload, seq, writer, userId):
@@ -66,347 +64,3 @@ class ContactsProcessors(BaseProcessor):
# Отправляем пакет # Отправляем пакет
await self._send(writer, packet) await self._send(writer, packet)
async def contact_update(self, payload, seq, writer, userId):
"""
Обработчик опкода какого-то там
(их хуй запомнишь, даже в мриме команды помню, бля)
Отвечает за добавку, удаление, блокировку и разблокировку контакта
"""
# Валидируем данные пакета
try:
ContactUpdatePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
action = payload.get("action")
contactId = payload.get("contactId")
firstName = payload.get("firstName")
lastName = payload.get("lastName", "")
if action == "ADD":
# Проверяем, существует ли пользователь с таким ID
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
# Проверяем, не добавлен ли уже контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Если контакта не существует, то можем продолжать,
if not row:
# Добавляем контакт
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, FALSE)",
(userId, contactId, firstName, lastName)
)
# Создаем диалог, если его нет
chatId = userId ^ contactId
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
if not chat:
await cursor.execute(
"INSERT INTO chats (id, owner, type) VALUES (%s, %s, %s)",
(chatId, userId, "DIALOG")
)
for uid in [int(userId), int(contactId)]:
await cursor.execute(
"INSERT INTO chat_participants (chat_id, user_id) VALUES (%s, %s)",
(chatId, uid)
)
# а если уже существует, отправляем ошибку
else:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_ADDED, writer)
return
# Генерируем профиль
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
contact = self.tools.generate_profile(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photoId,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
accountStatus=int(user.get("accountstatus")),
includeProfileOptions=False,
custom_firstname=firstName,
custom_lastname=lastName,
)
response_payload = {
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=response_payload
)
await self._send(writer, packet)
elif action == "REMOVE":
# Удаляем контакт
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"DELETE FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "BLOCK":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем существующий контакт, если такой есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = TRUE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае добавляем новую запись в бд
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.USER_NOT_FOUND, writer)
return
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, TRUE)",
(userId, contactId, firstName, lastName)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UNBLOCK":
# Разблокируем контакт
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем контакт, если он есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = FALSE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае отправляем ошибку
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UPDATE":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Если контакта нет, отдаем ошибку
if not row:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
# Обновляем контакт
await cursor.execute(
"UPDATE contacts SET custom_firstname = %s, custom_lastname = %s WHERE owner_id = %s AND contact_id = %s",
(firstName, lastName, userId, contactId)
)
# Получаем данные пользователя
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
# Генерируем профиль
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
contact = self.tools.generate_profile(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photoId,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
accountStatus=int(user.get("accountstatus")),
description=user.get("description"),
includeProfileOptions=False,
custom_firstname=firstName,
custom_lastname=lastName,
)
response_payload = {
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=response_payload
)
await self._send(writer, packet)
async def contact_add_by_phone(self, payload, seq, writer, userId):
"""Добавление контакта по номеру телефона"""
# Валидируем данные пакета
try:
ContactAddByPhonePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_ADD_BY_PHONE, self.error_types.INVALID_PAYLOAD, writer)
return
phone = payload.get("phone")
firstName = payload.get("firstName")
lastName = payload.get("lastName")
# Ищем пользователя по номеру телефона
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (int(phone),))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_ADD_BY_PHONE, self.error_types.CONTACT_NOT_FOUND, writer)
return
contactId = user.get("id")
# Проверяем, не добавлен ли уже контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
existing_contact = await cursor.fetchone()
is_new = existing_contact is None
if is_new:
# Добавляем контакт
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname) VALUES (%s, %s, %s, %s)",
(userId, contactId, firstName, lastName)
)
# Создаем диалог, если его нет
chatId = userId ^ contactId
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
if not chat:
await cursor.execute(
"INSERT INTO chats (id, owner, type) VALUES (%s, %s, %s)",
(chatId, userId, "DIALOG")
)
for uid in [int(userId), int(contactId)]:
await cursor.execute(
"INSERT INTO chat_participants (chat_id, user_id) VALUES (%s, %s)",
(chatId, uid)
)
# Генерируем профиль
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
contact = self.tools.generate_profile(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photoId,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
accountStatus=int(user.get("accountstatus")),
description=user.get("description"),
includeProfileOptions=False,
custom_firstname=firstName,
custom_lastname=lastName,
username=user.get("username"),
)
response_payload = {
"new": is_new,
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_ADD_BY_PHONE, payload=response_payload
)
await self._send(writer, packet)
async def contact_presence(self, payload, seq, writer):
"""Обработчик получения статуса контактов"""
# Валидируем данные пакета
try:
ContactPresencePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_PRESENCE, self.error_types.INVALID_PAYLOAD, writer)
return
contact_ids = payload.get("contactIds", [])
now_ms = int(time.time() * 1000)
presence = await self.tools.collect_presence(contact_ids, self.clients, self.db_pool)
response_payload = {
"presence": presence,
"time": now_ms
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_PRESENCE, payload=response_payload
)
await self._send(writer, packet)
+3 -82
View File
@@ -2,7 +2,7 @@ import pydantic
import json import json
import time import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import SyncFoldersPayloadModel, CreateFolderPayloadModel from oneme.models import SyncFoldersPayloadModel
class FoldersProcessors(BaseProcessor): class FoldersProcessors(BaseProcessor):
async def folders_get(self, payload, seq, writer, senderPhone): async def folders_get(self, payload, seq, writer, senderPhone):
@@ -19,7 +19,7 @@ class FoldersProcessors(BaseProcessor):
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute( await cursor.execute(
"SELECT id, title, filters, `include`, options, update_time, source_id " "SELECT id, title, filters, options, update_time, source_id "
"FROM user_folders WHERE phone = %s ORDER BY sort_order", "FROM user_folders WHERE phone = %s ORDER BY sort_order",
(int(senderPhone),) (int(senderPhone),)
) )
@@ -30,10 +30,9 @@ class FoldersProcessors(BaseProcessor):
"id": folder["id"], "id": folder["id"],
"title": folder["title"], "title": folder["title"],
"filters": json.loads(folder["filters"]), "filters": json.loads(folder["filters"]),
"include": json.loads(folder["include"]),
"updateTime": folder["update_time"], "updateTime": folder["update_time"],
"options": json.loads(folder["options"]), "options": json.loads(folder["options"]),
"sourceId": folder["source_id"] "sourceId": folder["source_id"],
} }
for folder in result_folders for folder in result_folders
] ]
@@ -53,81 +52,3 @@ class FoldersProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, packet) await self._send(writer, packet)
async def folders_update(self, payload, seq, writer, senderPhone):
"""Создание папки"""
# Валидируем данные пакета
try:
CreateFolderPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.FOLDERS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
update_time = int(time.time() * 1000)
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT COALESCE(MAX(sort_order), -1) as max_order FROM user_folders WHERE phone = %s",
(int(senderPhone),)
)
row = await cursor.fetchone()
next_order = row["max_order"] + 1
# Создаем новую папку
await cursor.execute(
"INSERT INTO user_folders (id, phone, title, filters, `include`, options, source_id, update_time, sort_order) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(
payload.get("id"),
int(senderPhone),
payload.get("title"),
json.dumps(payload.get("filters")),
json.dumps(payload.get("include", [])),
json.dumps([]),
1,
update_time,
next_order,
)
)
await conn.commit()
# Получаем обновленный порядок папок
await cursor.execute(
"SELECT id FROM user_folders WHERE phone = %s ORDER BY sort_order",
(int(senderPhone),)
)
all_folders = await cursor.fetchall()
folders_order = [f["id"] for f in all_folders]
# Формируем данные пакета
response_payload = {
"folder": {
"id": payload.get("id"),
"title": payload.get("title"),
"include": payload.get("include"),
"filters": payload.get("filters"),
"updateTime": update_time,
"options": [],
"sourceId": 1,
},
"folderSync": update_time,
"foldersOrder": folders_order,
}
# Формируем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.FOLDERS_UPDATE, payload=response_payload
)
await self._send(writer, packet)
# Разработчики протокола, объяснитесь, что за хеш !!! а еще подарите нам способ его формирования
notify_about_hash = self.proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_CONFIG,
payload={"config": {"hash": "0"}}
)
await self._send(writer, notify_about_hash)
+28 -14
View File
@@ -1,6 +1,5 @@
import pydantic import pydantic
import json import json
import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ChatHistoryPayloadModel from oneme.models import ChatHistoryPayloadModel
@@ -21,21 +20,19 @@ class HistoryProcessors(BaseProcessor):
backward = payload.get("backward", 0) backward = payload.get("backward", 0)
from_time = payload.get("from", 0) from_time = payload.get("from", 0)
getMessages = payload.get("getMessages", True) getMessages = payload.get("getMessages", True)
getChat = payload.get("getChat", False)
messages = [] messages = []
# Если пользователь хочет получить историю из избранного, # Если пользователь хочет получить историю из избранного,
# то выставляем в качестве ID чата отрицательный ID отправителя # то выставляем в качестве ID чата его ID
isFavourite = chatId == (senderId ^ senderId) if chatId == 0:
if isFavourite: chatId = senderId
chatId = -senderId
# Проверяем, существует ли чат # Проверяем, существует ли чат
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
# Проверяем состоит ли пользователь в чате, # Проверяем состоит ли пользователь в чате,
# только в случае того, если это не избранное # только в случае того, если это не избранное
if not isFavourite: if chatId != senderId:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,)) await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone() chat = await cursor.fetchone()
@@ -61,8 +58,19 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall() result = await cursor.fetchall()
for row in result: for row in result:
messages.append(self.tools.build_message_dict(row, self.type)) # TODO: Сборку тела сообщения нужно вынести в отдельную функцию
backward_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"),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"options": 1,
})
if forward > 0: if forward > 0:
await cursor.execute( await cursor.execute(
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s", "SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s",
@@ -72,19 +80,25 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall() result = await cursor.fetchall()
for row in result: for row in result:
messages.append(self.tools.build_message_dict(row, self.type)) messages.append({
forward_count = len(result) "id": row.get("id"),
"time": int(row.get("time")),
"type": row.get("type"),
"sender": row.get("sender"),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {}
})
# Сортируем сообщения по времени # Сортируем сообщения по времени
messages.sort(key=lambda x: x["time"]) messages.sort(key=lambda x: x["time"])
# Формируем ответ
payload = { payload = {
"messages": messages "messages": messages
} }
if getChat:
payload["chat"] = {}
# Собираем пакет # Собираем пакет
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_HISTORY, payload=payload cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_HISTORY, payload=payload
+2 -56
View File
@@ -1,6 +1,5 @@
import pydantic import pydantic
import json import json
import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ( from oneme.models import (
HelloPayloadModel, HelloPayloadModel,
@@ -31,7 +30,6 @@ class MainProcessors(BaseProcessor):
# Данные пакета # Данные пакета
payload = { payload = {
"callsSeed": int(time.time() * 1000),
"location": "RU", "location": "RU",
"app-update-type": 0, # 1 = принудительное обновление "app-update-type": 0, # 1 = принудительное обновление
"reg-country-code": self.static.REG_COUNTRY_CODES, "reg-country-code": self.static.REG_COUNTRY_CODES,
@@ -49,7 +47,7 @@ class MainProcessors(BaseProcessor):
await self._send(writer, packet) await self._send(writer, packet)
return deviceType, deviceName, appVersion return deviceType, deviceName, appVersion
async def ping(self, payload, seq, writer, userId=None): async def ping(self, payload, seq, writer):
"""Обработчик пинга""" """Обработчик пинга"""
# Валидируем данные пакета # Валидируем данные пакета
try: try:
@@ -59,58 +57,6 @@ class MainProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.PING, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.PING, self.error_types.INVALID_PAYLOAD, writer)
return return
# Обновляем статус пользователя, если он авторизован
# и в пакете отправлен интерактив
interactive = payload.get("interactive") if payload else None
if userId and interactive is not None:
now = int(time.time())
user = self.clients.get(userId)
if user:
if interactive:
user["status"] = 2
user["last_seen"] = now
else:
user["status"] = 0
user["last_seen"] = now
# Сохраняем последнее время посещения
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE users SET lastseen = %s WHERE id = %s",
(str(now), userId)
)
# Рассылаем статус контактам пользователя
now_ms = int(time.time() * 1000)
if interactive:
presence_data = {"on": "ON", "seen": now, "status": 1}
else:
presence_data = {"seen": now}
# Находим всех, у кого этот пользователь в контактах
async with self.db_pool.acquire() as conn2:
async with conn2.cursor() as cursor2:
await cursor2.execute(
"SELECT owner_id FROM contacts WHERE contact_id = %s",
(userId,)
)
contact_owners = await cursor2.fetchall()
# Рассылаем
for row in contact_owners:
owner_id = int(row.get("owner_id"))
if owner_id in self.clients:
await self.event(
owner_id,
{
"eventType": "presence",
"userId": userId,
"presence": presence_data,
"time": now_ms,
}
)
# Собираем пакет # Собираем пакет
response = self.proto.pack_packet( response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.PING, payload=None cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.PING, payload=None
@@ -153,7 +99,7 @@ class MainProcessors(BaseProcessor):
# Аватарка с биографией # Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) 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") 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 chatId = userId ^ senderId
# Если клиент хочет отправить сообщение в избранное, # Если клиент хочет отправить сообщение в избранное,
# то выставляем в качестве ID чата отрицательный ID отправителя # то выставляем в качестве ID чата ID отправителя
if chatId == (senderId ^ senderId): # (А ещё используем это, если клиент вообще ничего не указал)
chatId = -senderId if chatId == 0 or not chatId:
chatId = senderId
participants = [senderId] participants = [senderId]
else: else:
# Если все таки клиент хочет отправить сообщение в нормальный чат, # Если все таки клиент хочет отправить сообщение в нормальный чат,
@@ -113,14 +114,6 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer) await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return 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( messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId, chatId=chatId,
@@ -133,21 +126,16 @@ class MessagesProcessors(BaseProcessor):
db_pool=self.db_pool db_pool=self.db_pool
) )
# Готовое тело сообщения. Поля cid / elements / reactionInfo / link # Готовое тело сообщения
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
# ожидает фиксированную msgpack-схему и обрывает соединение
# при отсутствии любого из них (см. регрессию из 87cfc19).
bodyMessage = { bodyMessage = {
"id": messageId if self.type == "mobile" else str(messageId), "id": messageId,
"cid": int(cid or 0),
"time": messageTime, "time": messageTime,
"type": "USER", "type": "USER",
"sender": senderId, "sender": senderId,
"cid": cid,
"text": text, "text": text,
"attaches": attaches if isinstance(attaches, list) else [], "attaches": attaches,
"elements": elements if isinstance(elements, list) else [], "elements": elements
"reactionInfo": {},
"link": {},
} }
# Отправляем событие всем участникам чата # Отправляем событие всем участникам чата
@@ -156,7 +144,7 @@ class MessagesProcessors(BaseProcessor):
participant, participant,
{ {
"eventType": "new_msg", "eventType": "new_msg",
"chatId": 0 if chatId == -senderId else chatId, "chatId": 0 if chatId == senderId else chatId,
"message": bodyMessage, "message": bodyMessage,
"prevMessageId": lastMessageId, "prevMessageId": lastMessageId,
"time": messageTime, "time": messageTime,
@@ -166,7 +154,7 @@ class MessagesProcessors(BaseProcessor):
# Данные пакета # Данные пакета
payload = { payload = {
"chatId": 0 if chatId == -senderId else chatId, "chatId": 0 if chatId == senderId else chatId,
"message": bodyMessage, "message": bodyMessage,
"unread": 0, "unread": 0,
"mark": messageTime "mark": messageTime
+5 -5
View File
@@ -36,7 +36,7 @@ class SearchProcessors(BaseProcessor):
if user: if user:
# Аватарка с биографией # Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) 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") 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")) 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") description = None if not user.get("description") else user.get("description")
# Получаем данные контакта # Получаем данные контакта
@@ -223,12 +223,12 @@ class SearchProcessors(BaseProcessor):
) )
) )
else: else:
# Получаем последнее сообщение из избранного # Получаем последнее сообщение из чата
message, messageTime = await self.tools.get_last_message( 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 chatId = senderId ^ senderId
# Добавляем чат в список # Добавляем чат в список
+4 -140
View File
@@ -1,6 +1,5 @@
import asyncio import asyncio
import logging import logging
import time
import traceback import traceback
from common.opcodes import Opcodes from common.opcodes import Opcodes
@@ -33,7 +32,7 @@ class OnemeMobile:
self.opcodes = Opcodes() self.opcodes = Opcodes()
# rate limiter anti ddos brute force protection # rate limiter anti ddos brute force protection
self.auth_rate_limiter = RateLimiter(max_attempts=15, window_seconds=60) self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
self.read_timeout = 300 # Таймаут чтения из сокета (секунды) self.read_timeout = 300 # Таймаут чтения из сокета (секунды)
self.max_read_size = 65536 # Максимальный размер данных из сокета self.max_read_size = 65536 # Максимальный размер данных из сокета
@@ -114,7 +113,7 @@ class OnemeMobile:
) )
else: else:
await self.processors.auth( await self.processors.auth(
payload, seq, writer, deviceType, deviceName, appVersion, address[0] payload, seq, writer, deviceType, deviceName, appVersion
) )
case self.opcodes.AUTH_CONFIRM: case self.opcodes.AUTH_CONFIRM:
if not self.auth_rate_limiter.is_allowed(address[0]): if not self.auth_rate_limiter.is_allowed(address[0]):
@@ -126,7 +125,7 @@ class OnemeMobile:
) )
elif payload and payload.get("tokenType") == "REGISTER": elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm( await self.processors.auth_confirm(
payload, seq, writer, deviceType, deviceName, appVersion, address[0] payload, seq, writer, deviceType, deviceName, appVersion
) )
else: else:
self.logger.warning( self.logger.warning(
@@ -157,7 +156,7 @@ class OnemeMobile:
) )
break break
case self.opcodes.PING: case self.opcodes.PING:
await self.processors.ping(payload, seq, writer, userId) await self.processors.ping(payload, seq, writer)
case self.opcodes.LOG: case self.opcodes.LOG:
await self.processors.log(payload, seq, writer) await self.processors.log(payload, seq, writer)
case self.opcodes.ASSETS_UPDATE: case self.opcodes.ASSETS_UPDATE:
@@ -168,54 +167,6 @@ class OnemeMobile:
seq, seq,
writer, 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: case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, userPhone,
@@ -243,15 +194,6 @@ class OnemeMobile:
writer, writer,
userPhone, userPhone,
) )
case self.opcodes.FOLDERS_UPDATE:
await self.auth_required(
userPhone,
self.processors.folders_update,
payload,
seq,
writer,
userPhone,
)
case self.opcodes.SESSIONS_INFO: case self.opcodes.SESSIONS_INFO:
await self.auth_required( await self.auth_required(
userPhone, userPhone,
@@ -350,32 +292,6 @@ class OnemeMobile:
userPhone, userPhone,
hashedToken, hashedToken,
) )
case self.opcodes.CONTACT_UPDATE:
await self.auth_required(
userPhone,
self.processors.contact_update,
payload,
seq,
writer,
userId,
)
case self.opcodes.CONTACT_ADD_BY_PHONE:
await self.auth_required(
userPhone,
self.processors.contact_add_by_phone,
payload,
seq,
writer,
userId,
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone,
self.processors.contact_presence,
payload,
seq,
writer
)
case _: case _:
self.logger.warning(f"Неизвестный опкод {opcode}") self.logger.warning(f"Неизвестный опкод {opcode}")
except Exception as e: except Exception as e:
@@ -407,8 +323,6 @@ class OnemeMobile:
self.clients[id] = { self.clients[id] = {
"phone": phone, "phone": phone,
"id": id, "id": id,
"status": 2,
"last_seen": 0,
"clients": [ "clients": [
{ {
"writer": writer, "writer": writer,
@@ -419,38 +333,6 @@ class OnemeMobile:
], ],
} }
await self._broadcast_presence(id, True)
async def _broadcast_presence(self, userId, online):
now = int(time.time())
now_ms = int(time.time() * 1000)
if online:
presence_data = {"on": "ON", "seen": now, "status": 1}
else:
presence_data = {"seen": now}
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT owner_id FROM contacts WHERE contact_id = %s",
(userId,)
)
contact_owners = await cursor.fetchall()
for row in contact_owners:
owner_id = int(row.get("owner_id"))
if owner_id in self.clients:
await self.processors.event(
owner_id,
{
"eventType": "presence",
"userId": userId,
"presence": presence_data,
"time": now_ms,
}
)
async def _end_session(self, id, ip, port): async def _end_session(self, id, ip, port):
"""Завершение сессии""" """Завершение сессии"""
# Получаем пользователя в списке # Получаем пользователя в списке
@@ -466,20 +348,6 @@ class OnemeMobile:
if (client.get("ip"), client.get("port")) == (ip, port): if (client.get("ip"), client.get("port")) == (ip, port):
clients.pop(i) clients.pop(i)
if not clients:
now = int(time.time())
user["status"] = 0
user["last_seen"] = now
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE users SET lastseen = %s WHERE id = %s",
(str(now), id)
)
await self._broadcast_presence(id, False)
async def start(self): async def start(self):
"""Функция для запуска сервера""" """Функция для запуска сервера"""
self.server = await asyncio.start_server( self.server = await asyncio.start_server(
@@ -488,9 +356,5 @@ class OnemeMobile:
self.logger.info(f"Сокет запущен на порту {self.port}") self.logger.info(f"Сокет запущен на порту {self.port}")
try:
async with self.server: async with self.server:
await self.server.serve_forever() await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
+8 -156
View File
@@ -1,8 +1,6 @@
import logging import logging
import time
import traceback import traceback
import websockets import websockets
import asyncio
from common.proto_web import WebProto from common.proto_web import WebProto
from oneme.processors import Processors from oneme.processors import Processors
from common.rate_limiter import RateLimiter from common.rate_limiter import RateLimiter
@@ -10,9 +8,7 @@ from common.opcodes import Opcodes
from common.tools import Tools from common.tools import Tools
class OnemeWS: class OnemeWS:
def __init__( def __init__(self, host, port, clients, ssl_context, db_pool, send_event):
self, host, port, ssl_context, db_pool, clients, send_event, telegram_bot
):
self.host = host self.host = host
self.port = port self.port = port
self.ssl_context = ssl_context self.ssl_context = ssl_context
@@ -24,13 +20,7 @@ class OnemeWS:
self.opcodes = Opcodes() self.opcodes = Opcodes()
self.proto = WebProto() self.proto = WebProto()
self.processors = Processors( self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, type="web")
db_pool=db_pool,
clients=clients,
send_event=send_event,
telegram_bot=telegram_bot,
type="web"
)
self.auth_required = Tools().auth_required self.auth_required = Tools().auth_required
# rate limiter # rate limiter
@@ -97,7 +87,7 @@ class OnemeWS:
) )
else: else:
await self.processors.auth( await self.processors.auth(
payload, seq, websocket, deviceType, deviceName, appVersion, address[0] payload, seq, websocket, deviceType, deviceName, appVersion
) )
case self.opcodes.AUTH_CONFIRM: case self.opcodes.AUTH_CONFIRM:
if not self.auth_rate_limiter.is_allowed(address[0]): if not self.auth_rate_limiter.is_allowed(address[0]):
@@ -109,7 +99,7 @@ class OnemeWS:
) )
elif payload and payload.get("tokenType") == "REGISTER": elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm( await self.processors.auth_confirm(
payload, seq, websocket, deviceType, deviceName, appVersion, address[0] payload, seq, websocket, deviceType, deviceName, appVersion
) )
else: else:
self.logger.warning( self.logger.warning(
@@ -128,7 +118,7 @@ class OnemeWS:
userPhone, userPhone,
userId, userId,
hashedToken, hashedToken,
) = await self.processors.login(payload, seq, websocket, appVersion) ) = await self.processors.login(payload, seq, websocket, deviceType, appVersion)
if userPhone: if userPhone:
await self._finish_auth( await self._finish_auth(
@@ -140,7 +130,7 @@ class OnemeWS:
) )
break break
case self.opcodes.PING: case self.opcodes.PING:
await self.processors.ping(payload, seq, websocket, userId) await self.processors.ping(payload, seq, websocket)
case self.opcodes.LOG: case self.opcodes.LOG:
await self.processors.log(payload, seq, websocket) await self.processors.log(payload, seq, websocket)
case self.opcodes.ASSETS_UPDATE: case self.opcodes.ASSETS_UPDATE:
@@ -151,54 +141,6 @@ class OnemeWS:
seq, seq,
websocket, 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: case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, userPhone,
@@ -226,15 +168,6 @@ class OnemeWS:
websocket, websocket,
userPhone, userPhone,
) )
case self.opcodes.FOLDERS_UPDATE:
await self.auth_required(
userPhone,
self.processors.folders_update,
payload,
seq,
websocket,
userPhone,
)
case self.opcodes.SESSIONS_INFO: case self.opcodes.SESSIONS_INFO:
await self.auth_required( await self.auth_required(
userPhone, userPhone,
@@ -333,32 +266,6 @@ class OnemeWS:
userPhone, userPhone,
hashedToken, hashedToken,
) )
case self.opcodes.CONTACT_UPDATE:
await self.auth_required(
userPhone,
self.processors.contact_update,
payload,
seq,
websocket,
userId,
)
case self.opcodes.CONTACT_ADD_BY_PHONE:
await self.auth_required(
userPhone,
self.processors.contact_add_by_phone,
payload,
seq,
websocket,
userId,
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone,
self.processors.contact_presence,
payload,
seq,
websocket
)
case _: case _:
self.logger.warning(f"Неизвестный опкод {opcode}") self.logger.warning(f"Неизвестный опкод {opcode}")
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
@@ -385,59 +292,23 @@ class OnemeWS:
"writer": websocket, "writer": websocket,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "oneme", "protocol": "oneme"
"type": "web"
} }
) )
else: else:
self.clients[id] = { self.clients[id] = {
"phone": phone, "phone": phone,
"id": id, "id": id,
"status": 2,
"last_seen": 0,
"clients": [ "clients": [
{ {
"writer": websocket, "writer": websocket,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "oneme", "protocol": "oneme"
"type": "web"
} }
] ]
} }
await self._broadcast_presence(id, True)
async def _broadcast_presence(self, userId, online):
now = int(time.time())
now_ms = int(time.time() * 1000)
if online:
presence_data = {"on": "ON", "seen": now, "status": 1}
else:
presence_data = {"seen": now}
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT owner_id FROM contacts WHERE contact_id = %s",
(userId,)
)
contact_owners = await cursor.fetchall()
for row in contact_owners:
owner_id = int(row.get("owner_id"))
if owner_id in self.clients:
await self.processors.event(
owner_id,
{
"eventType": "presence",
"userId": userId,
"presence": presence_data,
"time": now_ms,
}
)
async def _end_session(self, id, ip, port): async def _end_session(self, id, ip, port):
"""Завершение сессии""" """Завершение сессии"""
# Получаем пользователя в списке # Получаем пользователя в списке
@@ -453,20 +324,6 @@ class OnemeWS:
if (client.get("ip"), client.get("port")) == (ip, port): if (client.get("ip"), client.get("port")) == (ip, port):
clients.pop(i) clients.pop(i)
if not clients:
now = int(time.time())
user["status"] = 0
user["last_seen"] = now
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE users SET lastseen = %s WHERE id = %s",
(str(now), id)
)
await self._broadcast_presence(id, False)
async def start(self): async def start(self):
"""Функция для запуска WebSocket сервера""" """Функция для запуска WebSocket сервера"""
self.server = await websockets.serve( self.server = await websockets.serve(
@@ -478,9 +335,4 @@ class OnemeWS:
self.logger.info(f"WebSocket запущен на порту {self.port}") self.logger.info(f"WebSocket запущен на порту {self.port}")
try:
await self.server.wait_closed() await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise
+342 -22
View File
@@ -9,12 +9,319 @@ class TTConfig:
"googNoiseSuppression": "true", "googNoiseSuppression": "true",
"googHighpassFilter": "false", "googHighpassFilter": "false",
"googTypingNoiseDetection": "false", "googTypingNoiseDetection": "false",
"googAudioNetworkAdaptorConfig": "" "googAudioNetworkAdaptorConfig": "ChyyARkNCtcjPBUK1yM8GKjDASCw6gEomHUwoJwBCgfKAQQIABAACgvCAQgIqMMBELiRAgosqgEpChEIuBcVzcxMPhjogQIlCtejOxIRCOgHFc3MTD4YsOoBJQrXozsYyAEKC7oBCAiw6gEQoJwB"
}, },
"a-lte": 24, "a-lte": 24,
"a-wifi": 34, "a-wifi": 34,
"account-removal-enabled": False, "account-removal-enabled": False,
"animated-emojis": {}, "animated-emojis": {
"❤️": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/03.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/04.json"
}
},
"👍": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-29lottie/e/16.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/01_m.json"
}
},
"👎": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/17.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/02.json"
}
},
"🙏": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/04.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-29lottie/e/30_ng.json"
}
},
"😘": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/05.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/03.json"
}
},
"🔥": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/06.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/10.json"
}
},
"😂": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/07.json"
}
},
"👏": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/56.json"
}
},
"😮": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/09.json"
}
},
"💋": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/13_v02.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2023-01-18lottie/r/kissing2.json"
}
},
"🥂": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/20.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/20.json"
}
},
"😳": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/09.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/02.json"
}
},
"😔": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/r/11.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/05.json"
}
},
"😍": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/07.json"
}
},
"😯": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/08.json"
}
},
"😉": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/09.json"
}
},
"🌺": {
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2023-03-06lottie/flower.json"
},
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/11.json"
}
},
"🎂": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/14.json"
}
},
"💩": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/15.json"
},
"reactionAction": {
"url": "https://st.okcdn.ru/static/messages/2023-01-18lottie/r/shit_1.json"
}
},
"🐰": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/19.json"
}
},
"🎅": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/21.json"
}
},
"🎄": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/23.json"
}
},
"🎆": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/22.json"
}
},
"❄️": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/25.json"
}
},
"🎉": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-28lottie/e/12.json"
}
},
"🥗": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2022-12-29lottie/e/28.json"
}
},
"🧡": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/31.json"
}
},
"💔": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/32.json"
}
},
"🎁": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/34.json"
}
},
"🌹": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/35.json"
}
},
"🌸": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/36.json"
}
},
"🍒": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/37.json"
}
},
"🥕": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/39.json"
}
},
"🍑": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/40.json"
}
},
"🍋": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/41.json"
}
},
"🍃": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/42.json"
}
},
"😺": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/43.json"
}
},
"🐶": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/44.json"
}
},
"🐽": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-07-20animojie/45.json"
}
},
"💐": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/46.json"
}
},
"🎈": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/47.json"
}
},
"🍾": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/48.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/49.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/50.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/51.json"
}
},
"💃": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/52.json"
}
},
"☀️": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/53.json"
}
},
"👋": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/54.json"
}
},
"": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/57.json"
}
},
"🙂": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/58.json"
}
},
"🤩": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-15animoji/59.json"
}
},
"😇": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/60.json"
}
},
"😎": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/61.json"
}
},
"🍎": {
"emoji": {
"url": "https://st.okcdn.ru/static/messages/2023-08-14animoji/62.json"
}
}
},
"animated-emojis-limits": { "animated-emojis-limits": {
"low": 5, "low": 5,
"average": 10, "average": 10,
@@ -76,10 +383,10 @@ class TTConfig:
"image-quality": 0.800000011920929, "image-quality": 0.800000011920929,
"image-size": 40000000, "image-size": 40000000,
"image-width": 1680, "image-width": 1680,
"invite-header": "", "invite-header": "Приглашение в ТамТам",
"invite-link": "", "invite-link": "https://tt.me/starwear",
"invite-long": "", "invite-long": "Я общаюсь в ТамТам, присоединяйся https://tt.me/starwear",
"invite-short": "", "invite-short": "Привет! Ставь ТамТам! Жду ответа! https://tt.me/starwear",
"keep-connection": 2, "keep-connection": 2,
"l10n": False, "l10n": False,
"live-location-enabled": True, "live-location-enabled": True,
@@ -146,12 +453,25 @@ class TTConfig:
"profiling-enabled": False, "profiling-enabled": False,
"progress-diff-for-notify": 1, "progress-diff-for-notify": 1,
"promo-contact-id": 0, "promo-contact-id": 0,
"promo-recent-contacts": False, "promo-recent-contacts": True,
"promo_contact_label": "", "promo_contact_label": "Белый Маг",
"proxy": "", "proxy": "msgproxy.okcdn.ru",
"proxy-domains": [], "proxy-domains": [
"proxy-exclude": [], "okcdn.ru",
"proxy-rotation": False, "mycdn.me",
"ok.ru",
"odnoklassniki.ru",
"odkl.ru",
"vk.com",
"userapi.com",
"vkuser.net",
"vkusercdn.ru"
],
"proxy-exclude": [
"r.mradx.net",
"ad.mail.ru"
],
"proxy-rotation": True,
"push-alert-timeout": 604800, "push-alert-timeout": 604800,
"push-tracking-enabled": True, "push-tracking-enabled": True,
"quick-forward-cases": [], "quick-forward-cases": [],
@@ -199,24 +519,24 @@ class TTConfig:
"TOP" "TOP"
], ],
"stickers-suggestion-keywords-inline": False, "stickers-suggestion-keywords-inline": False,
"support-account": "", "support-account": "tt.me/support",
"support-button-enable": False, "support-button-enable": False,
"t-ice-reconnect": 15, "t-ice-reconnect": 15,
"t-incoming-call": 40, "t-incoming-call": 40,
"t-start-connect": 20, "t-start-connect": 20,
"tam-emoji-font-url": "", "tam-emoji-font-url": "https://st.okcdn.ru/static/messages/2022-08-25noto/TamNotoColorEmojiCompat.ttf",
"tcp-candidates": False, "tcp-candidates": False,
"tracer-crash-report-enabled": False, "tracer-crash-report-enabled": True,
"tracer-crash-report-host": "", "tracer-crash-report-host": "https://api-hprof.odkl.ru",
"tracer-crash-send-asap-enabled": False, "tracer-crash-send-asap-enabled": True,
"tracer-crash-send-logs-enabled": False, "tracer-crash-send-logs-enabled": True,
"tracer-crash-send-threads-dump-enabled": False, "tracer-crash-send-threads-dump-enabled": True,
"tracer-disk-overflow-report-threshold": 3000000000, "tracer-disk-overflow-report-threshold": 3000000000,
"tracer-disk-usage-probability": 500, "tracer-disk-usage-probability": 500,
"tracer-enabled": False, "tracer-enabled": True,
"tracer-host": "", "tracer-host": "https://api-hprof.odkl.ru",
"tracer-hprof-probability": -1, "tracer-hprof-probability": -1,
"tracer-sampled-conditions": "", "tracer-sampled-conditions": "tag=app_start_ui_freeze_2k;probability=100000;startEvent=app_first_activity_created;interestingEvent=app_freeze;interestingDuration=2000",
"tracer-sampled-duration": 20000, "tracer-sampled-duration": 20000,
"tracer-systrace-duration": 20000, "tracer-systrace-duration": 20000,
"tracer-systrace-interesting-duration": 10000, "tracer-systrace-interesting-duration": 10000,
-96
View File
@@ -3,106 +3,10 @@ from tamtam.socket import TamTamMobile
from tamtam.websocket import TamTamWS from tamtam.websocket import TamTamWS
from classes.controllerbase import ControllerBase from classes.controllerbase import ControllerBase
from common.config import ServerConfig 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): class TTController(ControllerBase):
def __init__(self): def __init__(self):
self.config = ServerConfig() 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): def launch(self, api):
async def _start_all(): async def _start_all():
+8 -103
View File
@@ -3,24 +3,24 @@ import pydantic
class UserAgentModel(pydantic.BaseModel): class UserAgentModel(pydantic.BaseModel):
deviceType: str deviceType: str
appVersion: str appVersion: str
osVersion: str = None osVersion: str
timezone: str = None timezone: str
screen: str = None screen: str
pushDeviceType: str = None pushDeviceType: str = None
locale: str = None locale: str
deviceName: str deviceName: str
deviceLocale: str = None deviceLocale: str
class HelloPayloadModel(pydantic.BaseModel): class HelloPayloadModel(pydantic.BaseModel):
userAgent: UserAgentModel userAgent: UserAgentModel
deviceId: str = None deviceId: str
class RequestCodePayloadModel(pydantic.BaseModel): class RequestCodePayloadModel(pydantic.BaseModel):
phone: str phone: str
class VerifyCodePayloadModel(pydantic.BaseModel): class VerifyCodePayloadModel(pydantic.BaseModel):
verifyCode: str verifyCode: str
authTokenType: str = None authTokenType: str
token: str token: str
class FinalAuthPayloadModel(pydantic.BaseModel): class FinalAuthPayloadModel(pydantic.BaseModel):
@@ -30,7 +30,7 @@ class FinalAuthPayloadModel(pydantic.BaseModel):
token: str token: str
class LoginPayloadModel(pydantic.BaseModel): class LoginPayloadModel(pydantic.BaseModel):
interactive: bool = None interactive: bool
token: str token: str
class SearchUsersPayloadModel(pydantic.BaseModel): class SearchUsersPayloadModel(pydantic.BaseModel):
@@ -42,98 +42,3 @@ class PingPayloadModel(pydantic.BaseModel):
class ChatHistoryPayloadModel(pydantic.BaseModel): class ChatHistoryPayloadModel(pydantic.BaseModel):
chatId: int chatId: int
backward: int backward: int
class UpdateProfilePayloadModel(pydantic.BaseModel):
pass
class SearchChatsPayloadModel(pydantic.BaseModel):
chatIds: list
class AssetsPayloadModel(pydantic.BaseModel):
sync: int
type: str = None
userId: int = None
class AssetsGetPayloadModel(pydantic.BaseModel):
type: str
count: int = 100
query: str = None
class AssetsGetByIdsPayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsAddPayloadModel(pydantic.BaseModel):
type: str
id: int = None
class AssetsRemovePayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsMovePayloadModel(pydantic.BaseModel):
type: str
id: int
position: int
class AssetsListModifyPayloadModel(pydantic.BaseModel):
type: str
ids: list
class GetCallTokenPayloadModel(pydantic.BaseModel):
userId: int
value: str
class GetCallHistoryPayloadModel(pydantic.BaseModel):
forward: bool
count: int
class ChatSubscribePayloadModel(pydantic.BaseModel):
chatId: int
subscribe: bool
class ContactListPayloadModel(pydantic.BaseModel):
status: str
count: int = None
class ContactPresencePayloadModel(pydantic.BaseModel):
contactIds: list
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int
firstName: str = None
lastName: str = None
class TypingPayloadModel(pydantic.BaseModel):
chatId: int
type: str = None
class MessageModel(pydantic.BaseModel):
isLive: bool = None
detectShare: bool = None
elements: list = None
attaches: list = None
cid: int = None
text: str = None
class SendMessagePayloadModel(pydantic.BaseModel):
userId: int = None
chatId: int = None
message: MessageModel
class AuthConfirmRegisterPayloadModel(pydantic.BaseModel):
token: str
name: str
tokenType: str
deviceType: str
deviceId: str = None
@pydantic.field_validator('name')
def validate_name(cls, v):
v = v.strip()
if not v:
raise ValueError('name must not be empty')
if len(v) > 59:
raise ValueError('name too long')
return v
+1 -11
View File
@@ -2,19 +2,9 @@ from .main import MainProcessors
from .auth import AuthProcessors from .auth import AuthProcessors
from .search import SearchProcessors from .search import SearchProcessors
from .history import HistoryProcessors from .history import HistoryProcessors
from .assets import AssetsProcessors
from .chats import ChatsProcessors
from .contacts import ContactsProcessors
from .messages import MessagesProcessors
from .sessions import SessionsProcessors
class Processors(MainProcessors, class Processors(MainProcessors,
AuthProcessors, AuthProcessors,
SearchProcessors, SearchProcessors,
HistoryProcessors, HistoryProcessors):
AssetsProcessors,
ChatsProcessors,
ContactsProcessors,
MessagesProcessors,
SessionsProcessors):
pass pass
-167
View File
@@ -1,167 +0,0 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
response = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"stickersOrder": [
"RECENT",
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
)
await self._send(writer, packet)
async def assets_get(self, payload, seq, writer):
try:
data = AssetsGetPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": [], "marker": None}
else:
response = {"stickers": [], "marker": None}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET, payload=response
)
await self._send(writer, packet)
async def assets_get_by_ids(self, payload, seq, writer):
try:
data = AssetsGetByIdsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET_BY_IDS, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": []}
else:
response = {"stickers": []}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET_BY_IDS, payload=response
)
await self._send(writer, packet)
async def assets_add(self, payload, seq, writer):
try:
AssetsAddPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_ADD, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_ADD, payload={}
)
await self._send(writer, packet)
async def assets_remove(self, payload, seq, writer):
try:
AssetsRemovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_REMOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_REMOVE, payload={}
)
await self._send(writer, packet)
async def assets_move(self, payload, seq, writer):
try:
AssetsMovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_MOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_MOVE, payload={}
)
await self._send(writer, packet)
async def assets_list_modify(self, payload, seq, writer):
try:
AssetsListModifyPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_LIST_MODIFY, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_LIST_MODIFY, payload={}
)
await self._send(writer, packet)
+63 -297
View File
@@ -4,12 +4,10 @@ import time
import json import json
import re import re
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from common.sms import send_sms_code
from tamtam.models import ( from tamtam.models import (
RequestCodePayloadModel, RequestCodePayloadModel,
VerifyCodePayloadModel, VerifyCodePayloadModel,
FinalAuthPayloadModel, FinalAuthPayloadModel,
AuthConfirmRegisterPayloadModel,
LoginPayloadModel, LoginPayloadModel,
) )
from tamtam.config import TTConfig from tamtam.config import TTConfig
@@ -19,172 +17,6 @@ class AuthProcessors(BaseProcessor):
super().__init__(db_pool, clients, send_event, type) super().__init__(db_pool, clients, send_event, type)
self.server_config = TTConfig().SERVER_CONFIG 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): async def auth_request(self, payload, seq, writer):
"""Обработчик запроса кода""" """Обработчик запроса кода"""
# Валидируем данные пакета # Валидируем данные пакета
@@ -198,51 +30,29 @@ class AuthProcessors(BaseProcessor):
# Извлекаем телефон из пакета # Извлекаем телефон из пакета
phone = re.sub(r'\D', '', payload.get("phone", "")) phone = re.sub(r'\D', '', payload.get("phone", ""))
# Генерируем токен # Генерируем токен с кодом
code = f"{secrets.randbelow(1_000_000):06d}"
token = secrets.token_urlsafe(128) token = secrets.token_urlsafe(128)
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
token_hash = hashlib.sha256(token.encode()).hexdigest() token_hash = hashlib.sha256(token.encode()).hexdigest()
# Срок жизни токена (5 минут) # Срок жизни токена (5 минут)
expires = int(time.time()) + 300 expires = int(time.time()) + 300
user_exists = False # Ищем пользователя, и если он существует, сохраняем токен
# Ищем пользователя
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
user = await cursor.fetchone() 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: if user:
user_exists = True
await cursor.execute( await cursor.execute(
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)", "INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)",
(phone, token_hash, code_hash, expires, "started") (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 = { payload = {
@@ -261,7 +71,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, packet) 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): async def auth(self, payload, seq, writer):
"""Обработчик проверки кода""" """Обработчик проверки кода"""
@@ -302,32 +112,13 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_CODE, writer) self.error_types.INVALID_CODE, writer)
return 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"),)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
account = await cursor.fetchone() account = await cursor.fetchone()
# Обновляем состояние токена # Обновляем состояние токена
await cursor.execute( 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) ("verified", hashed_token)
) )
@@ -367,17 +158,22 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet) await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, ip): async def auth_confirm(self, payload, seq, writer, deviceType, deviceName):
"""Обработчик финальной аутентификации / регистрации""" """Обработчик финальной аутентификации"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return
# Извлекаем данные из пакета # Извлекаем данные из пакета
token = payload.get("token") token = payload.get("token")
if not deviceType: if not deviceType:
deviceType = payload.get("deviceType") deviceType = payload.get("deviceType")
if not deviceName:
deviceName = "Unknown device"
# Хешируем токен # Хешируем токен
hashed_token = hashlib.sha256(token.encode()).hexdigest() hashed_token = hashlib.sha256(token.encode()).hexdigest()
@@ -385,9 +181,10 @@ class AuthProcessors(BaseProcessor):
login = secrets.token_urlsafe(128) login = secrets.token_urlsafe(128)
hashed_login = hashlib.sha256(login.encode()).hexdigest() hashed_login = hashlib.sha256(login.encode()).hexdigest()
# Ищем токен # Ищем токен с кодом
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
# Ищем токен
await cursor.execute( await cursor.execute(
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", "SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
(hashed_token,) (hashed_token,)
@@ -399,36 +196,53 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return 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, await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return return
phone = stored_token.get("phone") # Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
# Проверяем, существует ли пользователь
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
account = await cursor.fetchone() account = await cursor.fetchone()
# Если пользователь есть, производим создание сессии # Удаляем токен
if account: await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
resp_payload = await self._finish_auth(
payload, seq, writer, cursor, phone, hashed_token, # Создаем сессию
hashed_login, account, deviceType, deviceName, ip, login await cursor.execute(
) "INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
else: # в ином случае производим регистрацию (stored_token.get("phone"), hashed_login, deviceType, deviceName,
resp_payload = await self._finish_reg( "Epstein Island", int(time.time()))
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, deviceType, deviceName, ip, login
) )
if resp_payload is None: # Аватарка с биографией
return photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
payload = {
"userToken": "0", # Пока как заглушка
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
# Создаем пакет # Создаем пакет
packet = self.proto.pack_packet( 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 +257,7 @@ class AuthProcessors(BaseProcessor):
self.logger.error(f"Возникли ошибки при валидации пакета: {e}") self.logger.error(f"Возникли ошибки при валидации пакета: {e}")
await self._send_error(seq, self.opcodes.LOGIN, await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_PAYLOAD, writer) self.error_types.INVALID_PAYLOAD, writer)
return None, None, None return
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
chats = [] chats = []
@@ -464,7 +278,7 @@ class AuthProcessors(BaseProcessor):
if token_data is None: if token_data is None:
await self._send_error(seq, self.opcodes.LOGIN, await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return None, None, None return
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
@@ -477,7 +291,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь # Ищем все чаты, где состоит пользователь
await cursor.execute( await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s", "SELECT * FROM chat_participants WHERE user_id = %s",
(user.get('id'),) (user.get('id'))
) )
user_chats = await cursor.fetchall() user_chats = await cursor.fetchall()
@@ -516,25 +330,16 @@ class AuthProcessors(BaseProcessor):
include_favourites=False include_favourites=False
) )
# Генерируем список контактов
contacts = await self.tools.collect_user_contacts(
user.get("id"), self.db_pool, self.config.avatar_base_url
)
# Собираем статусы контактов
contact_ids = [c.get("id") for c in contacts if c.get("id") is not None]
presence = await self.tools.collect_presence(contact_ids, self.clients, self.db_pool)
# Формируем данные пакета # Формируем данные пакета
payload = { payload = {
"profile": profile, "profile": profile,
"chats": chats, "chats": chats,
"chatMarker": 0, "chatMarker": 0,
"messages": {}, "messages": {},
"contacts": contacts, "contacts": [],
"presence": presence, "presence": {},
"config": { "config": {
"hash": "0", "hash": "e5903aa8-0000000000000000-80000106-0000000000000001-00000001-0000000000000000-00000000-2-00000001-0000019c9559d057",
"server": self.server_config, "server": self.server_config,
"user": updated_user_config, "user": updated_user_config,
"chatFolders": { "chatFolders": {
@@ -558,10 +363,6 @@ class AuthProcessors(BaseProcessor):
"time": int(time.time() * 1000) "time": int(time.time() * 1000)
} }
# print(
# json.dumps(payload, indent=4)
# )
# Собираем пакет # Собираем пакет
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGIN, payload=payload cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGIN, payload=payload
@@ -570,38 +371,3 @@ class AuthProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, packet) await self._send(writer, packet)
return int(user.get("phone")), int(user.get("id")), hashed_token return int(user.get("phone")), int(user.get("id")), hashed_token
async def logout(self, seq, writer, hashedToken):
"""Обработчик завершения сессии"""
# Удаляем токен из бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"DELETE FROM tokens WHERE token_hash = %s", (hashedToken,)
)
# ⣿⡇⣽⣿⣿⣿⣧⠘⣿⣿⠠⣤⣍⡛⢿⣿⣿⠏⣰⣿⣿⣿⣿⣿⡆⢿
# ⣿⢀⣿⣿⣿⣿⣿⣷⡈⢿⡄⢿⣿⣿⣦⡙⠏⣰⣿⣿⣿⣿⣿⣿⡇⣿
# ⣿⢸⣿⣿⣿⣿⣿⣿⡿⠄⣡⣤⣿⣿⣿⣿⣄⣿⣿⣿⣿⣿⣿⣿⡇⣿
# ⣿⢸⣿⣿⣿⣿⣿⣿⣤⣬⣭⣬⣬⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣿
# ⣿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⣿
# ⣿⡀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠿⠿⠿⣿⣿⡿⢠⣿
# ⣿⣧⠸⣿⣧⠀⣴⡆⣤⠀⢸⣿⣿⣿⣿⠀⠀⢸⣿⡌⣶⣿⠟⢁⣾⣿
# ⠙⠛⠂⠹⡿⢸⣿⡇⢸⠁⢸⣿⣿⣿⣿⠀⠀⢈⣿⡇⢸⣯⣤⣤⠀⣿
# ⣆⠙⣿⣿⣇⢸⣿⣇⠀⢀⣾⡿⢿⣿⣿⣀⣀⣼⣿⡇⣸⣿⡿⢁⣾⣿
# ⣿⣷⢀⡟⡉⠞⢻⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⣿⠋⠘⣹⣿⡄⢻⣿⣿
# ⣿⡇⣼⣿⣧⣶⣿⣿⣿⣟⠻⢋⣍⣉⣋⣼⣿⣿⣿⣶⢿⣿⣿⡄⢻⣿
# ⣿⣧⣭⣭⣄⡙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⣡⣤⣭⣤⣴⣾⣿
# ⣿⣿⣿⣿⣿⣿⣇⠠⣬⣭⣽⣿⣿⣿⣿⣿⣷⡈⢿⣿⣿⣿⣿⣿⣿⣿
# ⣿⣿⣿⣿⣿⣿⣿⣦⠙⣿⣿⣿⣿⣿⣿⣿⣿⣷⡈⣿⣿⣿⣿⣿⣿⣿
# ⣿⣿⣿⣿⣿⣿⣿⠃⠼⢿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠹⣿⣿⣿⣿⣿⣿
# ⣿⣿⣿⣿⣿⣿⣿⣶⠆⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⣿⣿⣿⣿⣿⣿
# ⣿⣿⣿⣿⣿⣿⣿⣿⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⢹⣿⣿⣿⣿⣿
# Создаем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGOUT, payload=None
)
# Отправляем
await self._send(writer, response)
-20
View File
@@ -1,20 +0,0 @@
import pydantic
from classes.baseprocessor import BaseProcessor
from tamtam.models import ChatSubscribePayloadModel
class ChatsProcessors(BaseProcessor):
async def chat_subscribe(self, payload, seq, writer):
# Валидируем входные данные
try:
ChatSubscribePayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.CHAT_SUBSCRIBE, self.error_types.INVALID_PAYLOAD, writer)
return
# Созадаем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_SUBSCRIBE, payload=None
)
# Отправялем
await self._send(writer, packet)
-301
View File
@@ -1,301 +0,0 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel
class ContactsProcessors(BaseProcessor):
async def contact_list(self, payload, seq, writer, userId):
"""Обработчик получения контактов"""
# Валидируем данные пакета
try:
ContactListPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_LIST, self.error_types.INVALID_PAYLOAD, writer)
return
status = payload.get("status")
count = payload.get("count")
# Итоговый контакт-лист
contact_list = []
if status == "BLOCKED":
# Собираем контакты, которые в черном списке
blocked = []
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
if count:
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND is_blocked = TRUE LIMIT %s",
(userId, count),
)
else:
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND is_blocked = TRUE",
(userId,),
)
rows = await cursor.fetchall()
for row in rows:
blocked.append(
{
"id": int(row.get("contact_id")),
"firstname": row.get("custom_firstname"),
"lastname": row.get("custom_lastname"),
"blocked": True,
}
)
# Генерируем контакт-лист
contact_list = await self.tools.generate_contacts(
blocked, self.db_pool, avatar_base_url=self.config.avatar_base_url
)
# Собираем данные пакета
response_payload = {
"contacts": contact_list
}
# Создаем пакет
packet = self.proto.pack_packet(
seq=seq, opcode=self.opcodes.CONTACT_LIST, payload=response_payload
)
# Отправляем пакет
await self._send(writer, packet)
async def contact_update(self, payload, seq, writer, userId):
"""
Обработчик опкода какого-то там
(их хуй запомнишь, даже в мриме команды помню, бля)
Отвечает за добавку, удаление, блокировку и разблокировку контакта
"""
# Валидируем данные пакета
try:
ContactUpdatePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
action = payload.get("action")
contactId = payload.get("contactId")
firstName = payload.get("firstName")
lastName = payload.get("lastName", "")
if action == "ADD":
# Проверяем, существует ли пользователь с таким ID
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.USER_NOT_FOUND, writer)
return
# Проверяем, не добавлен ли уже контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Если контакта не существует, то можем продолжать,
if not row:
# Добавляем контакт
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, FALSE)",
(userId, contactId, firstName, lastName)
)
# а если уже существует, отправляем ошибку
else:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_ADDED, writer)
return
# Генерируем профиль
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
contact = self.tools.generate_profile(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photoId,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
accountStatus=int(user.get("accountstatus")),
includeProfileOptions=False,
custom_firstname=firstName,
custom_lastname=lastName,
)
response_payload = {
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=response_payload
)
await self._send(writer, packet)
elif action == "REMOVE":
# Удаляем контакт
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"DELETE FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "BLOCK":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем существующий контакт, если такой есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = TRUE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае добавляем новую запись в бд
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.USER_NOT_FOUND, writer)
return
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, TRUE)",
(userId, contactId, firstName, lastName)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UNBLOCK":
# Разблокируем контакт
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Обновляем контакт, если он есть
if row:
await cursor.execute(
"UPDATE contacts SET is_blocked = FALSE WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
else: # В ином случае отправляем ошибку
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=None
)
await self._send(writer, packet)
elif action == "UPDATE":
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем, существует ли контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
row = await cursor.fetchone()
# Если контакта нет, отдаем ошибку
if not row:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_NOT_FOUND, writer)
return
# Обновляем контакт
await cursor.execute(
"UPDATE contacts SET custom_firstname = %s, custom_lastname = %s WHERE owner_id = %s AND contact_id = %s",
(firstName, lastName, userId, contactId)
)
# Получаем данные пользователя
await cursor.execute("SELECT * FROM users WHERE id = %s", (contactId,))
user = await cursor.fetchone()
# Генерируем профиль
photo_id = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
contact = self.tools.generate_profile_tt(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
description=user.get("description"),
username=user.get("username")
),
response_payload = {
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_UPDATE, payload=response_payload
)
await self._send(writer, packet)
async def contact_presence(self, payload, seq, writer):
"""Обработчик получения статуса контактов"""
# Валидируем данные пакета
try:
ContactPresencePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_PRESENCE, self.error_types.INVALID_PAYLOAD, writer)
return
contact_ids = payload.get("contactIds", [])
now_ms = int(time.time() * 1000)
presence = await self.tools.collect_presence(contact_ids, self.clients, self.db_pool)
response_payload = {
"presence": presence,
"time": now_ms
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_PRESENCE, payload=response_payload
)
await self._send(writer, packet)
-133
View File
@@ -1,133 +0,0 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import SyncFoldersPayloadModel, CreateFolderPayloadModel
class FoldersProcessors(BaseProcessor):
async def folders_get(self, payload, seq, writer, senderPhone):
"""Синхронизация папок с сервером"""
# Валидируем данные пакета
try:
SyncFoldersPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.FOLDERS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
# Ищем папки в бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT id, title, filters, `include`, options, update_time, source_id "
"FROM user_folders WHERE phone = %s ORDER BY sort_order",
(int(senderPhone),)
)
result_folders = await cursor.fetchall()
folders = [
{
"id": folder["id"],
"title": folder["title"],
"filters": json.loads(folder["filters"]),
"include": json.loads(folder["include"]),
"updateTime": folder["update_time"],
"options": json.loads(folder["options"]),
"sourceId": folder["source_id"]
}
for folder in result_folders
]
# Создаем данные пакета
payload = {
"folderSync": int(time.time() * 1000),
"folders": folders,
"foldersOrder": [folder["id"] for folder in result_folders],
"allFilterExcludeFolders": []
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.FOLDERS_GET, payload=payload
)
# Отправляем
await self._send(writer, packet)
async def folders_update(self, payload, seq, writer, senderPhone):
"""Создание папки"""
# Валидируем данные пакета
try:
CreateFolderPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.FOLDERS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
update_time = int(time.time() * 1000)
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT COALESCE(MAX(sort_order), -1) as max_order FROM user_folders WHERE phone = %s",
(int(senderPhone),)
)
row = await cursor.fetchone()
next_order = row["max_order"] + 1
# Создаем новую папку
await cursor.execute(
"INSERT INTO user_folders (id, phone, title, filters, `include`, options, source_id, update_time, sort_order) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(
payload.get("id"),
int(senderPhone),
payload.get("title"),
json.dumps(payload.get("filters")),
json.dumps(payload.get("include", [])),
json.dumps([]),
1,
update_time,
next_order,
)
)
await conn.commit()
# Получаем обновленный порядок папок
await cursor.execute(
"SELECT id FROM user_folders WHERE phone = %s ORDER BY sort_order",
(int(senderPhone),)
)
all_folders = await cursor.fetchall()
folders_order = [f["id"] for f in all_folders]
# Формируем данные пакета
response_payload = {
"folder": {
"id": payload.get("id"),
"title": payload.get("title"),
"include": payload.get("include"),
"filters": payload.get("filters"),
"updateTime": update_time,
"options": [],
"sourceId": 1,
},
"folderSync": update_time,
"foldersOrder": folders_order,
}
# Формируем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.FOLDERS_UPDATE, payload=response_payload
)
await self._send(writer, packet)
# Разработчики протокола, объяснитесь, что за хеш !!! а еще подарите нам способ его формирования
notify_about_hash = self.proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_CONFIG,
payload={"config": {"hash": "0"}}
)
await self._send(writer, notify_about_hash)
-102
View File
@@ -1,8 +1,6 @@
import json
import pydantic import pydantic
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from tamtam.models import HelloPayloadModel, PingPayloadModel from tamtam.models import HelloPayloadModel, PingPayloadModel
from tamtam.models import UpdateProfilePayloadModel
class MainProcessors(BaseProcessor): class MainProcessors(BaseProcessor):
async def session_init(self, payload, seq, writer): async def session_init(self, payload, seq, writer):
@@ -38,106 +36,6 @@ class MainProcessors(BaseProcessor):
await self._send(writer, packet) await self._send(writer, packet)
return device_type, device_name return device_type, device_name
async def profile(self, payload, seq, writer, userId):
"""Обработчик получения/обновления профиля"""
# Валидируем входные данные
try:
UpdateProfilePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.PROFILE, self.error_types.INVALID_PAYLOAD, writer)
return
# Ищем пользователя в бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE id = %s", (userId,))
user = await cursor.fetchone()
# Если пользователь не найден
if not user:
await self._send_error(seq, self.opcodes.PROFILE, self.error_types.USER_NOT_FOUND, writer)
return
# Аватарка с биографией
photo_id = int(user["avatar_id"]) if user.get("avatar_id") else None
avatar_url = f"{self.config.avatar_base_url}{photo_id}" if photo_id else None
description = user.get("description")
# Генерируем профиль
profile = self.tools.generate_profile_tt(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
description=description,
username=user.get("username")
)
# Создаем данные пакета
payload = {
"profile": profile
}
# Собираем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.PROFILE, payload=payload
)
# Отправляем
await self._send(writer, response)
async def update_config(self, payload, seq, writer, userPhone, hashedToken=None):
"""Обработчик обновления настроек и пуш-токена"""
result_payload = None
if payload.get("pushToken"):
push_token = payload.get("pushToken")
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"UPDATE tokens SET push_token = %s WHERE phone = %s AND token_hash = %s",
(push_token, str(userPhone), hashedToken)
)
elif payload.get("settings") and payload.get("settings").get("user"):
new_settings = payload.get("settings").get("user")
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT user_config FROM user_data WHERE phone = %s", (userPhone,)
)
row = await cursor.fetchone()
if row:
current_config = json.loads(row.get("user_config"))
for key, value in new_settings.items():
if key in current_config:
current_config[key] = value
await cursor.execute(
"UPDATE user_data SET user_config = %s WHERE phone = %s",
(json.dumps(current_config), userPhone)
)
result_payload = {
"user": current_config,
"hash": "0"
}
# Собираем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONFIG, payload=result_payload
)
# Отправляем
await self._send(writer, response)
async def ping(self, payload, seq, writer): async def ping(self, payload, seq, writer):
"""Обработчик пинга""" """Обработчик пинга"""
# Валидируем данные пакета # Валидируем данные пакета
-168
View File
@@ -1,168 +0,0 @@
import pydantic
from classes.baseprocessor import BaseProcessor
from tamtam.models import (
TypingPayloadModel,
SendMessagePayloadModel
)
class MessagesProcessors(BaseProcessor):
async def msg_typing(self, payload, seq, writer, senderId):
"""Обработчик события печатания"""
# Валидируем данные пакета
try:
TypingPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.MSG_TYPING, self.error_types.INVALID_PAYLOAD, writer)
return
# Извлекаем данные из пакета
chatId = payload.get("chatId")
type = payload.get("type") or "TYPING"
# Ищем чат в базе данных
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
# Если чат не найден, отправляем ошибку
if not chat:
await self._send_error(seq, self.opcodes.MSG_TYPING, self.error_types.CHAT_NOT_FOUND, writer)
return
# Участники чата
participants = await self.tools.get_chat_participants(chatId, self.db_pool)
# Проверяем, является ли отправитель участником чата
if int(senderId) not in participants:
await self._send_error(seq, self.opcodes.MSG_TYPING, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Рассылаем событие участникам чата
for participant in participants:
if participant != senderId:
# Если участник не является отправителем, отправляем
await self.event(
participant,
{
"eventType": "typing",
"chatId": chatId,
"type": type,
"userId": senderId,
"writer": writer,
}
)
# Создаем пакет
packet = self.proto.pack_packet(
seq=seq, opcode=self.opcodes.MSG_TYPING
)
# Отправляем пакет
await self._send(writer, packet)
async def msg_send(self, payload, seq, writer, senderId, db_pool):
"""Функция отправки сообщения"""
# Валидируем данные пакета
try:
SendMessagePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.INVALID_PAYLOAD, writer)
return
# Извлекаем данные из пакета
userId = payload.get("userId")
chatId = payload.get("chatId")
message = payload.get("message")
elements = message.get("elements") or []
attaches = message.get("attaches") or []
cid = message.get("cid") or 0
text = message.get("text") or ""
# Вычисляем ID чата по ID пользователя и ID отправителя,
# в случае отсутствия ID чата
if chatId is None:
chatId = userId ^ senderId
async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
# Если нет такого чата - выбрасываем ошибку
if not chat:
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_FOUND, writer)
return
# Список участников
participants = await self.tools.get_chat_participants(chatId, db_pool)
# Проверяем, является ли отправитель участником чата
if int(senderId) not in participants:
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId,
senderId=senderId,
text=text,
attaches=attaches,
elements=elements,
cid=cid,
type="USER",
db_pool=self.db_pool
)
# Готовое тело сообщения
bodyMessage = {
"id": messageId,
"time": messageTime,
"type": "USER",
"sender": senderId,
"cid": cid,
"text": text,
"attaches": attaches,
"elements": elements
}
# Отправляем событие всем участникам чата
for participant in participants:
await self.event(
participant,
{
"eventType": "new_msg",
"chatId": chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
"writer": writer
}
)
# Данные пакета
payload = {
"chatId": chatId,
"message": bodyMessage,
"unread": 0,
"mark": messageTime
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.MSG_SEND, payload=payload
)
# Отправляем
await self._send(writer, packet)
-57
View File
@@ -1,7 +1,6 @@
import json, pydantic import json, pydantic
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from tamtam.models import SearchUsersPayloadModel from tamtam.models import SearchUsersPayloadModel
from tamtam.models import SearchChatsPayloadModel
class SearchProcessors(BaseProcessor): class SearchProcessors(BaseProcessor):
async def contact_info(self, payload, seq, writer): async def contact_info(self, payload, seq, writer):
@@ -62,59 +61,3 @@ class SearchProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, response) await self._send(writer, response)
async def chat_info(self, payload, seq, writer, senderId):
"""Поиск чатов по ID"""
# Валидируем данные пакета
try:
SearchChatsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CHAT_INFO, self.error_types.INVALID_PAYLOAD, writer)
return
# Итоговый список чатов
chats = []
# ID чатов, которые нам предстоит найти
chatIds = payload.get("chatIds")
# Ищем чаты в бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
for chatId in chatIds:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
if chat:
# Проверяем, является ли пользователь участником чата
participants = await self.tools.get_chat_participants(chatId, self.db_pool)
if int(senderId) not in participants:
continue
# Получаем последнее сообщение из чата
message, messageTime = await self.tools.get_last_message(
chatId, self.db_pool, protocol_type=self.type
)
# Добавляем чат в список
chats.append(
self.tools.generate_chat(
chatId, chat.get("owner"),
chat.get("type"), participants,
message, messageTime
)
)
# Создаем данные пакета
payload = {
"chats": chats
}
# Собираем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_INFO, payload=payload
)
# Отправляем
await self._send(writer, response)
-38
View File
@@ -1,38 +0,0 @@
from classes.baseprocessor import BaseProcessor
class SessionsProcessors(BaseProcessor):
async def sessions_info(self, payload, seq, writer, senderPhone, hashedToken):
"""Получение активных сессий на аккаунте"""
# Готовый список сессий
sessions = []
# Ищем сессии в бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM tokens WHERE phone = %s", (str(senderPhone),))
user_sessions = await cursor.fetchall()
# Собираем сессии в список
for session in user_sessions:
sessions.append(
{
"time": int(session.get("time")),
"client": f"TamTam {session.get('device_type')}",
"info": session.get("device_name"),
"location": session.get("location"),
"current": True if session.get("token_hash") == hashedToken else False
}
)
# Создаем данные пакета
payload = {
"sessions": sessions
}
# Создаем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.SESSIONS_INFO, payload=payload
)
# Отправляем
await self._send(writer, response)
+5 -104
View File
@@ -24,7 +24,7 @@ class TamTamMobile:
self.auth_required = Tools().auth_required self.auth_required = Tools().auth_required
# rate limiter # rate limiter
self.auth_rate_limiter = RateLimiter(max_attempts=15, window_seconds=60) self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
self.read_timeout = 300 # Таймаут чтения из сокета (секунды) self.read_timeout = 300 # Таймаут чтения из сокета (секунды)
self.max_read_size = 65536 # Максимальный размер данных из сокета self.max_read_size = 65536 # Максимальный размер данных из сокета
@@ -96,7 +96,7 @@ class TamTamMobile:
if not self.auth_rate_limiter.is_allowed(address[0]): if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.AUTH_CONFIRM, self.processors.error_types.RATE_LIMITED, writer) await self.processors._send_error(seq, self.opcodes.AUTH_CONFIRM, self.processors.error_types.RATE_LIMITED, writer)
else: else:
await self.processors.auth_confirm(payload, seq, writer, deviceType, deviceName, address[0]) await self.processors.auth_confirm(payload, seq, writer, deviceType, deviceName)
case self.opcodes.LOGIN: case self.opcodes.LOGIN:
if not self.auth_rate_limiter.is_allowed(address[0]): if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.LOGIN, self.processors.error_types.RATE_LIMITED, writer) await self.processors._send_error(seq, self.opcodes.LOGIN, self.processors.error_types.RATE_LIMITED, writer)
@@ -105,111 +105,18 @@ class TamTamMobile:
if userPhone: if userPhone:
await self._finish_auth(writer, address, userPhone, userId) await self._finish_auth(writer, address, userPhone, userId)
case self.opcodes.LOGOUT:
await self.processors.logout(
seq, writer, hashedToken=hashedToken
)
break
case self.opcodes.CONTACT_INFO: case self.opcodes.CONTACT_INFO:
await self.auth_required( await self.auth_required(
userPhone, self.processors.contact_info, payload, seq, writer userPhone, self.processors.contact_info, payload, seq, writer
) )
case self.opcodes.CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.chat_history, payload, seq, writer, userId
)
case self.opcodes.ASSETS_UPDATE:
await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, writer
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, writer
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, writer
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, writer
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, writer
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, writer
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, payload, seq, writer
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, writer
)
case self.opcodes.MSG_SEND:
await self.auth_required(
userPhone, self.processors.msg_send, payload, seq, writer, userId, self.db_pool
)
case self.opcodes.MSG_TYPING:
await self.auth_required(
userPhone, self.processors.msg_typing, payload, seq, writer, userId
)
case self.opcodes.FOLDERS_GET:
await self.auth_required(
userPhone, self.processors.folders_get, payload, seq, writer, userPhone
)
case self.opcodes.FOLDERS_UPDATE:
await self.auth_required(
userPhone, self.processors.folders_update, payload, seq, writer, userPhone
)
case self.opcodes.SESSIONS_INFO:
await self.auth_required(
userPhone, self.processors.sessions_info, payload, seq, writer, userPhone, hashedToken
)
case self.opcodes.CHAT_INFO:
await self.auth_required(
userPhone, self.processors.chat_info, payload, seq, writer, userId
)
case self.opcodes.CONTACT_LIST:
await self.auth_required(
userPhone, self.processors.contact_list, payload, seq, writer, userId
)
case self.opcodes.PROFILE:
await self.processors.profile(
payload, seq, writer, userId=userId
)
case self.opcodes.CHAT_SUBSCRIBE:
await self.auth_required(
userPhone, self.processors.chat_subscribe, payload, seq, writer
)
case self.opcodes.CONFIG:
await self.auth_required(
userPhone, self.processors.update_config, payload, seq, writer, userPhone, hashedToken
)
case self.opcodes.CONTACT_UPDATE:
await self.auth_required(
userPhone, self.processors.contact_update, payload, seq, writer, userId
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone, self.processors.contact_presence, payload, seq, writer
)
case _: case _:
self.logger.warning(f"Неизвестный опкод {opcode}") self.logger.warning(f"Неизвестный опкод {opcode}")
except Exception as e: except Exception as e:
self.logger.error(f"Произошла ошибка при работе с клиентом {address[0]}:{address[1]}: {e}") self.logger.error(f"Произошла ошибка при работе с клиентом {address[0]}:{address[1]}: {e}")
traceback.print_exc() traceback.print_exc()
# Удаляем клиента из словаря при отключении
if userId:
await self._end_session(userId, address[0], address[1])
writer.close() writer.close()
self.logger.info(f"Прекратил работать с клиентом {address[0]}:{address[1]}") self.logger.info(f"Прекратил работать работать с клиентом {address[0]}:{address[1]}")
async def _finish_auth(self, writer, addr, phone, id): async def _finish_auth(self, writer, addr, phone, id):
"""Завершение открытия сессии""" """Завершение открытия сессии"""
@@ -223,8 +130,7 @@ class TamTamMobile:
"writer": writer, "writer": writer,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "tamtam", "protocol": "tamtam"
"type": "tcp"
} }
) )
else: else:
@@ -236,8 +142,7 @@ class TamTamMobile:
"writer": writer, "writer": writer,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "tamtam", "protocol": "tamtam"
"type": "tcp"
} }
] ]
} }
@@ -265,9 +170,5 @@ class TamTamMobile:
self.logger.info(f"Сокет запущен на порту {self.port}") self.logger.info(f"Сокет запущен на порту {self.port}")
try:
async with self.server: async with self.server:
await self.server.serve_forever() await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
+3 -96
View File
@@ -1,7 +1,6 @@
import logging import logging
import traceback import traceback
import websockets import websockets
import asyncio
from common.proto_web import WebProto from common.proto_web import WebProto
from tamtam.processors import Processors from tamtam.processors import Processors
from common.rate_limiter import RateLimiter from common.rate_limiter import RateLimiter
@@ -83,7 +82,7 @@ class TamTamWS:
if not self.auth_rate_limiter.is_allowed(address[0]): if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.AUTH_CONFIRM, self.processors.error_types.RATE_LIMITED, websocket) await self.processors._send_error(seq, self.opcodes.AUTH_CONFIRM, self.processors.error_types.RATE_LIMITED, websocket)
else: else:
await self.processors.auth_confirm(payload, seq, websocket, deviceType, deviceName, address[0]) await self.processors.auth_confirm(payload, seq, websocket, deviceType, deviceName)
case self.opcodes.LOGIN: case self.opcodes.LOGIN:
if not self.auth_rate_limiter.is_allowed(address[0]): if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.opcodes.LOGIN, self.processors.error_types.RATE_LIMITED, websocket) await self.processors._send_error(seq, self.opcodes.LOGIN, self.processors.error_types.RATE_LIMITED, websocket)
@@ -92,11 +91,6 @@ class TamTamWS:
if userPhone: if userPhone:
await self._finish_auth(websocket, address, userPhone, userId) await self._finish_auth(websocket, address, userPhone, userId)
case self.opcodes.LOGOUT:
await self.processors.logout(
seq, websocket, hashedToken=hashedToken
)
break
case self.opcodes.CONTACT_INFO: case self.opcodes.CONTACT_INFO:
await self.auth_required( await self.auth_required(
userPhone, self.processors.contact_info, payload, seq, websocket userPhone, self.processors.contact_info, payload, seq, websocket
@@ -105,86 +99,6 @@ class TamTamWS:
await self.auth_required( await self.auth_required(
userPhone, self.processors.chat_history, payload, seq, websocket, userId userPhone, self.processors.chat_history, payload, seq, websocket, userId
) )
case self.opcodes.ASSETS_UPDATE:
await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, websocket
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, websocket
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, websocket
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, websocket
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, websocket
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, websocket
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, payload, seq, websocket
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, websocket
)
case self.opcodes.MSG_SEND:
await self.auth_required(
userPhone, self.processors.msg_send, payload, seq, websocket, userId, self.db_pool
)
case self.opcodes.MSG_TYPING:
await self.auth_required(
userPhone, self.processors.msg_typing, payload, seq, websocket, userId
)
case self.opcodes.FOLDERS_GET:
await self.auth_required(
userPhone, self.processors.folders_get, payload, seq, websocket, userPhone
)
case self.opcodes.FOLDERS_UPDATE:
await self.auth_required(
userPhone, self.processors.folders_update, payload, seq, websocket, userPhone
)
case self.opcodes.SESSIONS_INFO:
await self.auth_required(
userPhone, self.processors.sessions_info, payload, seq, websocket, userPhone, hashedToken
)
case self.opcodes.CHAT_INFO:
await self.auth_required(
userPhone, self.processors.chat_info, payload, seq, websocket, userId
)
case self.opcodes.CONTACT_LIST:
await self.auth_required(
userPhone, self.processors.contact_list, payload, seq, websocket, userId
)
case self.opcodes.PROFILE:
await self.processors.profile(
payload, seq, websocket, userId=userId
)
case self.opcodes.CHAT_SUBSCRIBE:
await self.auth_required(
userPhone, self.processors.chat_subscribe, payload, seq, websocket
)
case self.opcodes.CONFIG:
await self.auth_required(
userPhone, self.processors.update_config, payload, seq, websocket, userPhone, hashedToken
)
case self.opcodes.CONTACT_UPDATE:
await self.auth_required(
userPhone, self.processors.contact_update, payload, seq, websocket, userId
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone, self.processors.contact_presence, payload, seq, websocket
)
case _: case _:
self.logger.warning(f"Неизвестный опкод {opcode}") self.logger.warning(f"Неизвестный опкод {opcode}")
except websockets.exceptions.ConnectionClosed: except websockets.exceptions.ConnectionClosed:
@@ -211,8 +125,7 @@ class TamTamWS:
"writer": websocket, "writer": websocket,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "tamtam", "protocol": "tamtam"
"type": "web"
} }
) )
else: else:
@@ -224,8 +137,7 @@ class TamTamWS:
"writer": websocket, "writer": websocket,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "tamtam", "protocol": "tamtam"
"type": "web"
} }
] ]
} }
@@ -256,9 +168,4 @@ class TamTamWS:
self.logger.info(f"TT WebSocket запущен на порту {self.port}") self.logger.info(f"TT WebSocket запущен на порту {self.port}")
try:
await self.server.wait_closed() await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise
+6 -16
View File
@@ -14,7 +14,7 @@ from common.tools import Tools
class TelegramBot: class TelegramBot:
def __init__(self, token, enabled, db_pool, whitelist_ids=None, whitelist_enabled=False): def __init__(self, token, enabled, db_pool, whitelist_ids=None):
self.bot = Bot(token=token) self.bot = Bot(token=token)
self.dp = Dispatcher() self.dp = Dispatcher()
self.router = Router() self.router = Router()
@@ -23,7 +23,6 @@ class TelegramBot:
self.enabled = enabled self.enabled = enabled
self.db_pool = db_pool self.db_pool = db_pool
self.whitelist_ids = whitelist_ids if whitelist_ids is not None else [] self.whitelist_ids = whitelist_ids if whitelist_ids is not None else []
self.whitelist_enabled = whitelist_enabled
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.msg_types = Static().BotMessageTypes() self.msg_types = Static().BotMessageTypes()
@@ -60,8 +59,7 @@ class TelegramBot:
async def handle_register(self, message: Message): async def handle_register(self, message: Message):
tg_id = str(message.from_user.id) tg_id = str(message.from_user.id)
# Проверка ID на наличие в белом списке (если он включен, конечно) # Проверка ID на наличие в белом списке
if self.whitelist_enabled:
if self.whitelist_ids and tg_id not in self.whitelist_ids: if self.whitelist_ids and tg_id not in self.whitelist_ids:
await message.answer( await message.answer(
self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED) self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED)
@@ -83,24 +81,16 @@ class TelegramBot:
updatetime = str(int(time.time() * 1000)) updatetime = str(int(time.time() * 1000))
lastseen = str(int(time.time())) 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: try:
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем юзера # Создаем юзера
await cursor.execute( await cursor.execute(
self.sql_queries.INSERT_USER, self.sql_queries.INSERT_USER,
( (
user_id, # id
new_phone, # phone new_phone, # phone
tg_id, # telegram_id tg_id, # telegram_id
firstname, # firstname message.from_user.first_name[:59], # firstname
lastname, # lastname (message.from_user.last_name or "")[:59], # lastname
username, # username (message.from_user.username or "")[:60], # username
json.dumps([]), # profileoptions json.dumps([]), # profileoptions
json.dumps(["TT", "ONEME"]), # options json.dumps(["TT", "ONEME"]), # options
0, # accountstatus 0, # accountstatus
@@ -139,7 +129,7 @@ class TelegramBot:
async def start(self): async def start(self):
if self.enabled: if self.enabled:
try: try:
await self.dp.start_polling(self.bot, handle_signals=False) await self.dp.start_polling(self.bot)
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка запуска Telegram бота: {e}") self.logger.error(f"Ошибка запуска Telegram бота: {e}")
else: else:
+1 -2
View File
@@ -19,8 +19,7 @@ class TelegramBotController(ControllerBase):
token=self.config.telegram_bot_token, token=self.config.telegram_bot_token,
enabled=self.config.telegram_bot_enabled, enabled=self.config.telegram_bot_enabled,
db_pool=api['db'], db_pool=api['db'],
whitelist_ids=self.config.telegram_whitelist_ids, whitelist_ids=self.config.telegram_whitelist_ids
whitelist_enabled=self.config.telegram_whitelist_enabled,
) )
return _start_all() return _start_all()
+3 -4
View File
@@ -1,7 +1,7 @@
CREATE TABLE `users` ( CREATE TABLE `users` (
`id` INT NOT NULL, `id` INT NOT NULL AUTO_INCREMENT,
`phone` VARCHAR(20) UNIQUE, `phone` VARCHAR(20) UNIQUE,
`telegram_id` VARCHAR(64), `telegram_id` VARCHAR(64) UNIQUE,
`firstname` VARCHAR(59) NOT NULL, `firstname` VARCHAR(59) NOT NULL,
`lastname` VARCHAR(59), `lastname` VARCHAR(59),
`description` VARCHAR(400), `description` VARCHAR(400),
@@ -51,7 +51,7 @@ CREATE TABLE `chats` (
); );
CREATE TABLE `messages` ( CREATE TABLE `messages` (
`id` BIGINT NOT NULL, `id` INT NOT NULL AUTO_INCREMENT,
`chat_id` INT NOT NULL, `chat_id` INT NOT NULL,
`sender` INT NOT NULL, `sender` INT NOT NULL,
`time` VARCHAR(32) NOT NULL, `time` VARCHAR(32) NOT NULL,
@@ -102,7 +102,6 @@ CREATE TABLE `user_folders` (
`phone` VARCHAR(20) NOT NULL, `phone` VARCHAR(20) NOT NULL,
`title` VARCHAR(128) NOT NULL, `title` VARCHAR(128) NOT NULL,
`filters` JSON NOT NULL DEFAULT ('[]'), `filters` JSON NOT NULL DEFAULT ('[]'),
`include` JSON NOT NULL DEFAULT ('[]'),
`options` JSON NOT NULL DEFAULT ('[]'), `options` JSON NOT NULL DEFAULT ('[]'),
`source_id` INT NOT NULL DEFAULT 1, `source_id` INT NOT NULL DEFAULT 1,
`update_time` BIGINT NOT NULL DEFAULT 0, `update_time` BIGINT NOT NULL DEFAULT 0,