Compare commits

...

44 Commits

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

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

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

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

View File

@@ -24,7 +24,9 @@ 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 = ""

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ __pycache__
*.pem *.pem
*.sqlite *.sqlite
*.crt *.crt
*-adminsdk-*.json *-adminsdk-*.json
*.mmdb

View File

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

View File

@@ -43,8 +43,9 @@ 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(os.getenv("telegram_bot_enabled")) or True telegram_bot_enabled = bool(int(os.getenv("telegram_bot_enabled", 0)))
telegram_whitelist_ids = [x.strip() for x in os.getenv("telegram_whitelist_ids", "").split(",") if x.strip()] telegram_whitelist_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
@@ -54,3 +55,6 @@ 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", "")

View File

@@ -44,6 +44,7 @@ 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

View File

@@ -6,9 +6,9 @@ class SQLQueries:
INSERT_USER = """ INSERT_USER = """
INSERT INTO users INSERT INTO users
(phone, telegram_id, firstname, lastname, username, (id, 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) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""" """
INSERT_USER_DATA = """ INSERT_USER_DATA = """

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

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

View File

@@ -13,6 +13,9 @@ class Static:
CHAT_NOT_FOUND = "chat_not_found" CHAT_NOT_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"
@@ -80,7 +83,25 @@ 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": "Вы не можете написать этому пользователю"
},
} }
### Сообщения бота ### Сообщения бота

View File

@@ -1,24 +1,55 @@
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=1, id=None,
phone=70000000000, phone=None,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=0, updateTime=None,
firstName="Test", firstName=None,
lastName="Account", lastName=None,
options=[], options=None,
description=None, description=None,
accountStatus=0, accountStatus=None,
profileOptions=[], profileOptions=None,
includeProfileOptions=True, includeProfileOptions=True,
username=None, username=None,
@@ -42,6 +73,8 @@ class Tools:
], ],
"options": options, "options": options,
"accountStatus": accountStatus, "accountStatus": accountStatus,
"location": "RU",
"registrationTime": int(time.time() * 1000)
} }
if avatarUrl: if avatarUrl:
@@ -75,22 +108,40 @@ class Tools:
def generate_profile_tt( def generate_profile_tt(
self, self,
id=1, id=None,
phone=70000000000, phone=None,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=0, updateTime=None,
firstName="Test", firstName=None,
lastName="Account", lastName=None,
options=[], options=None,
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": f"{firstName} {lastName}", "type": "TT"}], "names": [{"name": name, "type": "TT"}],
"options": options, "options": options,
} }
@@ -102,8 +153,19 @@ class Tools:
if description: if description:
contact["description"] = description contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username: if username:
contact["link"] = "https://tamtam.chat/" + username contact["link"] = "https://tt.me/" + username
if custom_firstname:
contact["names"].append(
{"name": custom_name, "type": "CUSTOM"}
)
if blocked:
contact["status"] = "BLOCKED"
return contact return contact
@@ -201,27 +263,28 @@ class Tools:
if include_favourites: if include_favourites:
# Получаем последнее сообщение из избранного # Получаем последнее сообщение из избранного
favouriteChatId = -senderId
message, messageTime = await self.get_last_message( message, messageTime = await self.get_last_message(
senderId, db_pool, protocol_type=protocol_type favouriteChatId, db_pool, protocol_type=protocol_type
) )
# ID избранного # ID избранного для клиента
chatId = senderId ^ senderId chatId = senderId ^ senderId
# Получаем последнюю активность участника (отправителя) в избранном # Получаем последнюю активность в избранном
participants = await self.get_participant_last_activity( participants = await self.get_participant_last_activity(
senderId, [senderId], db_pool favouriteChatId, [senderId], db_pool
) )
# Получаем ID предыдущего сообщения для избранного (чат ID = senderId) # Получаем ID предыдущего сообщения для избранного
prevMessageId = await self.get_previous_message_id( prevMessageId = await self.get_previous_message_id(
senderId, db_pool, protocol_type=protocol_type favouriteChatId, db_pool, protocol_type=protocol_type
) )
# Хардкодим в лист чатов избранное # Хардкодим в лист чатов избранное
chats.append( chats.append(
self.generate_chat( self.generate_chat(
chatId if protocol_type == "mobile" else str(chatId), chatId,
senderId, senderId,
"DIALOG", "DIALOG",
participants, participants,
@@ -332,7 +395,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", "SELECT * FROM `contacts` WHERE owner_id = %s AND is_blocked = FALSE",
(owner_id,), (owner_id,),
) )
rows = await cursor.fetchall() rows = await cursor.fetchall()
@@ -367,10 +430,14 @@ 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` (chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)", "INSERT INTO `messages` (id, chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
( (
message_id,
chatId, chatId,
senderId, senderId,
message_time, message_time,
@@ -382,8 +449,6 @@ 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
@@ -403,23 +468,8 @@ 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 message, int(row.get("time")) return self.build_message_dict(row, protocol_type), int(row.get("time"))
async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"): async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
"""Получение ID предыдущего сообщения (второго с конца) в чате.""" """Получение ID предыдущего сообщения (второго с конца) в чате."""
@@ -501,3 +551,90 @@ 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

View File

@@ -1,10 +1,14 @@
# Импортирование библиотек # Импортирование библиотек
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
@@ -12,71 +16,6 @@ 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():
"""Инициализация базы данных""" """Инициализация базы данных"""
@@ -114,7 +53,6 @@ def init_ssl():
# Возвращаем # Возвращаем
return ssl_context return ssl_context
def set_logging(): def set_logging():
"""Настройка уровня логирования""" """Настройка уровня логирования"""
# Настройка уровня логирования # Настройка уровня логирования
@@ -175,11 +113,39 @@ async def main():
api["telegram_bot"] = controllers["telegrambot"] api["telegram_bot"] = controllers["telegrambot"]
tasks = [controller.launch(api) for controller in controllers.values()] loop = asyncio.get_running_loop()
running_tasks = []
def _shutdown(sig):
logging.info(f"Получен сигнал {sig}, завершаем все задачи...")
for task in running_tasks:
task.cancel()
if sys.platform != "win32":
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, _shutdown, sig)
coros = [controller.launch(api) for controller in controllers.values()]
running_tasks.extend(asyncio.create_task(coro) for coro in coros)
# Запускаем контроллеры # Запускаем контроллеры
await asyncio.gather(*tasks) try:
await asyncio.gather(*running_tasks)
except asyncio.CancelledError:
logging.info("Все задачи завершены")
except Exception as e:
logging.error(
f"Произошла неизвестная ошибка: {e}"
)
traceback.print_exc()
finally:
if hasattr(db, 'close'):
db.close()
await db.wait_closed()
elif hasattr(db, 'connection') and hasattr(db.connection, 'close'):
await db.connection.close()
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@@ -3,120 +3,334 @@ class OnemeConfig:
pass pass
SERVER_CONFIG = { SERVER_CONFIG = {
"async-tracer": 0, "account-nickname-enabled": False,
"presence-ttl": 300, "account-removal-enabled": False,
"non-contact-sync-time": 86400, "anr-config": {
"contact-batching-variant": 0, "enabled": True,
"account-nickname-enabled": True, "timeout": {
"web-ad-banner": { "low": 5000,
"enabled": False "avg": 5000,
"high": 5000
}
}, },
"edit-timeout": 0,
"reactions-menu": [],
"invite-long": "",
"calls-endpoint": "",
"calls-test-domain": "",
"max-readmarks": 100,
"max-cname-length": 200,
"max-description-length": 400,
"new-avatar-gradient-colors-enabled": True,
"max-msg-length": 4000,
"file-upload-unsupported-types": [],
"file-upload-max-size": 4294967296,
"image-quality": 0.8,
"image-width": 1920,
"image-height": 1920,
"image-size": 10000000,
"max-favorite-chats": 5,
"bot-complaint-enabled": True,
"reactions-max": 8,
"welcome-sticker-ids": [],
"edit-chat-type-screen-enabled": True,
"edit-channel-type-screen-enabled": True,
"esia-verify-botId": 0,
"official-org": False,
"esia-enabled": False,
"calls-debug-mode": False,
"channels-suggests-folder": True,
"delete-msg-fys-large-chat-disabled": False,
"calls-web-download-logs": False,
"calls-web-upload-logs": False,
"calls-video-zoom": False,
"calls-fullscreen-mode": False,
"group-call-part-limit": 100,
"call-chat-members-load-config": {},
"cfs": False,
"cse": False,
"calls-hotkeys": True,
"gc-link-pre-settings": False,
"gc-from-p2p": False,
"call-rate": {},
"channels-enabled": True,
"max-participants": 20000,
"max-added-participants": 100,
"saved-messages-aliases": [],
"author-visibility-forward-enabled": False,
"official-bot-naming-enabled": False,
"search-webapps-showcase": {
"items": []
},
"settings-entry-banners": [],
"settings-business": "https://telegram.org/blog/telegram-business",
"appearance-multi-theme-screen-enabled": True, "appearance-multi-theme-screen-enabled": True,
"moscow-theme-enabled": True, "audio-transcription-locales": [],
"creation-2fa-config": { "available-complaints": [
"enabled": False, "FAKE",
"pass_min_len": 6, "SPAM",
"pass_max_len": 64, "PORNO",
"hint_max_len": 30 "EXTREMISM",
"THREAT",
"OTHER"
],
"avatars-screen-enabled": True,
"bad-networ-indicator-config": {
"signalingConfig": {
"dcReportNetworkStatEnabled": False
}
}, },
"lebedev-theme-enabled": True, "bots-channel-adding": True,
"quotes-enabled": 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-complaint-enabled": True,
"reactions-settings-enabled": True, "channels-enabled": True,
"channel-statistics-botid": 0, "channels-search-subscribers-visible": True,
"enable-unknown-contact-bottom-sheet": 0, "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, "informer-enabled": True,
"family-protection-botid": 0, "inline-ev-player": True,
"new-year-theme-2026": True, "invalidate-db-msg-exception": True,
"invite-friends-sheet-frequency": [
2,
7
],
"invite-link": "",
"invite-long": "",
"invite-short": "",
"join-requests": True,
"js-download-delegate": False,
"keep-connection": 2,
"lebedev-theme-enabled": True,
"lgce": True,
"markdown-enabled": True,
"markdown-menu": 0,
"max-audio-length": 3600,
"max-description-length": 400,
"max-favorite-chats": 5,
"max-favorite-sticker-sets": 100,
"max-favorite-stickers": 100,
"max-msg-length": 4000,
"max-participants": 20000,
"max-readmarks": 100,
"max-theme-length": 200,
"max-video-duration-download": 1200,
"max-video-message-length": 60,
"media-order": 1,
"media-playlist-enabled": True,
"media-transform": {
"enabled": True,
"hdr_enabled": False,
"hevc_enabled": True,
"max_enc_frames": {
"low": 1,
"avg": 1,
"high": 2
}
},
"media-viewer-rotation-enabled": True,
"media-viewer-video-collage-enabled": True,
"mentions-enabled": True,
"mentions_entity_names_limit": 3,
"migrate-unsafe-warn": True,
"min-image-side-size": 64,
"miui-menu-enabled": True,
"money-transfer-botid": 1134691,
"moscow-theme-enabled": True,
"msg-get-reactions-page-size": 40,
"music-files-enabled": False,
"mytracker-enabled": True,
"net-client-dns-enabled": True,
"net-session-suppress-bad-disconnected-state": True,
"net-stat-config": [
64,
48,
128,
135
],
"new-admin-permissions": True,
"new-logout-logic": False,
"new-media-upload-ui": True,
"new-media-viewer-enabled": True,
"new-settings-storage-screen-enabled": False,
"new-width-text-bubbles-mob": True,
"new-year-theme-2026": False,
"nick-max-length": 60,
"nick-min-length": 7,
"official-org": True,
"one-video-failover": True,
"one-video-player": True,
"one-video-uploader": True,
"one-video-uploader-audio": True,
"one-video-uploader-progress-fix": True,
"perf-events": {
"startup_report": 2,
"web_app": 2
},
"player-load-control": {
"mp_autoplay_enabled": False,
"time_over_size": False,
"buffer_after_rebuffer_ms": 3000,
"buffer_ms": 500,
"max_buffer_ms": 13000,
"min_buffer_ms": 5000,
"use_min_size_lc": True,
"min_size_lc_fmt_mis_sf": 4
},
"progress-diff-for-notify": 1,
"push-delivery": True,
"qr-auth-enabled": True,
"quotes-enabled": True,
"react-errors": [
"error.comment.chat.access",
"error.comment.invalid",
"error.message.invalid",
"error.message.chat.access",
"error.message.like.unknown.like",
"error.message.like.unknown.reaction",
"error.too-many-unlikes-dialog",
"error.too-many-unlikes-chat",
"error.too-many-likes",
"error.reactions.not.allowed"
],
"react-permission": 2,
"reactions-enabled": True,
"reactions-max": 8,
"reactions-menu": [
"👍",
"❤️",
"🤣",
"🔥",
"😭",
"💯",
"💩",
"😡"
],
"reactions-settings-enabled": True,
"reconnect-call-ringtone": True,
"ringtone-am-mode": True,
"saved-messages-aliases": [
"избранное",
"saved",
"favourite",
"favorite",
"личное",
"моё",
"мои",
"мой",
"моя",
"любимое",
"сохраненные",
"сохраненное",
"заметки",
"закладки"
],
"scheduled-messages-enabled": True, "scheduled-messages-enabled": True,
"scheduled-posts-enabled": True, "scheduled-posts-enabled": True,
"scheduled-faves-enabled": True, "search-webapps-showcase": {
"non-contact-complaints-enabled": True, "items": []
"join-requests": True, },
"web-persistent-cache": False, "send-location-enabled": True,
"create-channel-type-screen": True, "send-logs-interval-sec": 900,
"server-side-complains-enabled": True,
"set-audio-device": False,
"set-unread-timeout": 31536000,
"settings-entry-banners": [],
"show-reactions-on-multiselect": True,
"show-warning-links": True, "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": [],
"february-23-26-theme": True, "wm-analytics-enabled": True,
"march-8-26-theme": True, "wm-workers-limit": 80,
"audio-play-cmd": False, "wud": 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": "", "tile": "34c7fd82-723d-4b23-8abb-33376729a893",
"geocoder": "", "geocoder": "34c7fd82-723d-4b23-8abb-33376729a893",
"static": "" "static": "34c7fd82-723d-4b23-8abb-33376729a893",
"logoLight": "https://st.max.ru/icons/ya_maps_logo_light.webp",
"logoDark": "https://st.max.ru/icons/ya_maps_logo_dark.webp"
}, },
"enable-audio-messages-transcription": True, "has-phone": 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
}

View File

@@ -10,13 +10,19 @@ 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 = MobileProto() self.proto_tcp = 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"):
@@ -41,7 +47,7 @@ class OnemeController(ControllerBase):
} }
# Создаем пакет # Создаем пакет
packet = self.proto.pack_packet( packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_MESSAGE, payload=payload cmd=0, seq=1, opcode=self.opcodes.NOTIF_MESSAGE, payload=payload
) )
elif eventType == "typing": elif eventType == "typing":
@@ -58,7 +64,7 @@ class OnemeController(ControllerBase):
} }
# Создаем пакет # Создаем пакет
packet = self.proto.pack_packet( packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_TYPING, payload=payload cmd=0, seq=1, opcode=self.opcodes.NOTIF_TYPING, payload=payload
) )
elif eventType == "profile_updated": elif eventType == "profile_updated":
@@ -71,13 +77,32 @@ class OnemeController(ControllerBase):
} }
# Создаем пакет # Создаем пакет
packet = self.proto.pack_packet( packet = proto.pack_packet(
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PROFILE, payload=payload 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 = {
writer.write(packet) "userId": userId,
await writer.drain() "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():
@@ -94,10 +119,11 @@ 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'],
send_event=api['event'] clients=api['clients'],
send_event=api['event'],
telegram_bot=api.get('telegram_bot'),
).start() ).start()
) )

View File

@@ -54,6 +54,32 @@ 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
@@ -75,6 +101,12 @@ 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
@@ -134,4 +166,17 @@ 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

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 .complains import ComplainsProcessors from .complaints import ComplaintsProcessors
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,
ComplainsProcessors, ComplaintsProcessors,
ContactsProcessors, ContactsProcessors,
FoldersProcessors, FoldersProcessors,
HistoryProcessors, HistoryProcessors,

View File

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

View File

@@ -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): async def auth(self, payload, seq, writer, deviceType, deviceName, appVersion, ip):
"""Обработчик проверки кода""" """Обработчик проверки кода"""
try: try:
VerifyCodePayloadModel.model_validate(payload) VerifyCodePayloadModel.model_validate(payload)
@@ -285,8 +285,10 @@ class AuthProcessors(BaseProcessor):
hashed_login, hashed_login,
deviceType, deviceType,
deviceName, deviceName,
"Little Saint James Island", self.tools.get_geo(
int(time.time()), ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000),
), # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть ), # весь покрытый зеленью, абсолютно весь, остров невезения в океане есть
) )
@@ -295,7 +297,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 + photoId avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = ( description = (
None if not account.get("description") else account.get("description") None if not account.get("description") else account.get("description")
) )
@@ -333,7 +335,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): async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, appVersion, ip):
"""Обработчик подтверждения регистрации нового пользователя""" """Обработчик подтверждения регистрации нового пользователя"""
# Валидируем данные пакета # Валидируем данные пакета
try: try:
@@ -395,15 +397,19 @@ 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
(phone, telegram_id, firstname, lastname, username, (id, 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) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", """,
( (
user_id,
phone, phone,
None, None,
first_name, first_name,
@@ -417,8 +423,6 @@ class AuthProcessors(BaseProcessor):
), ),
) )
user_id = cursor.lastrowid
# Добавляем данные аккаунта # Добавляем данные аккаунта
await cursor.execute( await cursor.execute(
""" """
@@ -456,8 +460,10 @@ class AuthProcessors(BaseProcessor):
hashed_login, hashed_login,
deviceType or "ANDROID", deviceType or "ANDROID",
deviceName or "Unknown", deviceName or "Unknown",
"Little Saint James Island", self.tools.get_geo(
now_s, ip=ip, db_path=self.config.geo_db_path
),
now_ms,
), ),
) )
@@ -515,7 +521,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 return None, None, None
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
chats = [] chats = []
@@ -539,7 +545,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 return None, None, None
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
await cursor.execute( await cursor.execute(
@@ -557,7 +563,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()
@@ -572,7 +578,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 + photoId avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description") description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion): if self._check_legacy_version(appVersion):
@@ -607,6 +613,10 @@ 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,
@@ -614,16 +624,17 @@ 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,
}, },
"token": token, "token": token,
"videoChatHistory": False, "videoChatHistory": False,
"time": int(time.time() * 1000), "time": int(time.time() * 1000),
} }
# Собираем пакет # Собираем пакет
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

View File

@@ -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 ComplainsProcessors(BaseProcessor): class ComplaintsProcessors(BaseProcessor):
async def complain_reasons_get(self, payload, seq, writer): async def complain_reasons_get(self, payload, seq, writer):
"""Обработчик получения причин жалоб""" """Обработчик получения причин жалоб"""
# Валидируем данные пакета # Валидируем данные пакета

View File

@@ -1,6 +1,8 @@
import pydantic import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ContactListPayloadModel from oneme.models import ContactAddByPhonePayloadModel, ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel
class ContactsProcessors(BaseProcessor): class ContactsProcessors(BaseProcessor):
async def contact_list(self, payload, seq, writer, userId): async def contact_list(self, payload, seq, writer, userId):
@@ -64,3 +66,347 @@ 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)

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

View File

@@ -1,5 +1,6 @@
import pydantic import 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
@@ -20,19 +21,21 @@ 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 отправителя
if chatId == 0: isFavourite = chatId == (senderId ^ senderId)
chatId = senderId if isFavourite:
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 chatId != senderId: if not isFavourite:
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()
@@ -58,19 +61,8 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall() result = await cursor.fetchall()
for row in result: for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию messages.append(self.tools.build_message_dict(row, self.type))
messages.append({ backward_count = len(result)
"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",
@@ -80,25 +72,19 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall() result = await cursor.fetchall()
for row in result: for row in result:
messages.append({ messages.append(self.tools.build_message_dict(row, self.type))
"id": row.get("id"), forward_count = len(result)
"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

View File

@@ -1,9 +1,10 @@
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,
PingPayloadModel, PingPayloadModel,
UpdateProfilePayloadModel UpdateProfilePayloadModel
) )
@@ -30,6 +31,7 @@ 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,
@@ -47,7 +49,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): async def ping(self, payload, seq, writer, userId=None):
"""Обработчик пинга""" """Обработчик пинга"""
# Валидируем данные пакета # Валидируем данные пакета
try: try:
@@ -57,6 +59,58 @@ 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
@@ -99,7 +153,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 + photoId avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description") description = None if not user.get("description") else user.get("description")
# Генерируем профиль # Генерируем профиль

View File

@@ -88,10 +88,9 @@ class MessagesProcessors(BaseProcessor):
chatId = userId ^ senderId chatId = userId ^ senderId
# Если клиент хочет отправить сообщение в избранное, # Если клиент хочет отправить сообщение в избранное,
# то выставляем в качестве ID чата ID отправителя # то выставляем в качестве ID чата отрицательный ID отправителя
# (А ещё используем это, если клиент вообще ничего не указал) if chatId == (senderId ^ senderId):
if chatId == 0 or not chatId: chatId = -senderId
chatId = senderId
participants = [senderId] participants = [senderId]
else: else:
# Если все таки клиент хочет отправить сообщение в нормальный чат, # Если все таки клиент хочет отправить сообщение в нормальный чат,
@@ -114,6 +113,14 @@ 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,
@@ -126,16 +133,21 @@ class MessagesProcessors(BaseProcessor):
db_pool=self.db_pool db_pool=self.db_pool
) )
# Готовое тело сообщения # Готовое тело сообщения. Поля cid / elements / reactionInfo / link
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
# ожидает фиксированную msgpack-схему и обрывает соединение
# при отсутствии любого из них (см. регрессию из 87cfc19).
bodyMessage = { bodyMessage = {
"id": messageId, "id": messageId if self.type == "mobile" else str(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, "attaches": attaches if isinstance(attaches, list) else [],
"elements": elements "elements": elements if isinstance(elements, list) else [],
"reactionInfo": {},
"link": {},
} }
# Отправляем событие всем участникам чата # Отправляем событие всем участникам чата
@@ -144,7 +156,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,
@@ -154,7 +166,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

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 + photoId avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description") 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 + photoId avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description") 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
# Добавляем чат в список # Добавляем чат в список

View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
import logging import logging
import time
import traceback import traceback
from common.opcodes import Opcodes from common.opcodes import Opcodes
@@ -32,7 +33,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=5, window_seconds=60) self.auth_rate_limiter = RateLimiter(max_attempts=15, window_seconds=60)
self.read_timeout = 300 # Таймаут чтения из сокета (секунды) self.read_timeout = 300 # Таймаут чтения из сокета (секунды)
self.max_read_size = 65536 # Максимальный размер данных из сокета self.max_read_size = 65536 # Максимальный размер данных из сокета
@@ -113,7 +114,7 @@ class OnemeMobile:
) )
else: else:
await self.processors.auth( await self.processors.auth(
payload, seq, writer, deviceType, deviceName, appVersion payload, seq, writer, deviceType, deviceName, appVersion, address[0]
) )
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]):
@@ -125,7 +126,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 payload, seq, writer, deviceType, deviceName, appVersion, address[0]
) )
else: else:
self.logger.warning( self.logger.warning(
@@ -156,7 +157,7 @@ class OnemeMobile:
) )
break break
case self.opcodes.PING: case self.opcodes.PING:
await self.processors.ping(payload, seq, writer) await self.processors.ping(payload, seq, writer, userId)
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:
@@ -167,6 +168,54 @@ 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,
@@ -194,6 +243,15 @@ 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,
@@ -292,6 +350,32 @@ 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:
@@ -323,6 +407,8 @@ 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,
@@ -333,6 +419,38 @@ class OnemeMobile:
], ],
} }
await self._broadcast_presence(id, True)
async def _broadcast_presence(self, userId, online):
now = int(time.time())
now_ms = int(time.time() * 1000)
if online:
presence_data = {"on": "ON", "seen": now, "status": 1}
else:
presence_data = {"seen": now}
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT owner_id FROM contacts WHERE contact_id = %s",
(userId,)
)
contact_owners = await cursor.fetchall()
for row in contact_owners:
owner_id = int(row.get("owner_id"))
if owner_id in self.clients:
await self.processors.event(
owner_id,
{
"eventType": "presence",
"userId": userId,
"presence": presence_data,
"time": now_ms,
}
)
async def _end_session(self, id, ip, port): async def _end_session(self, id, ip, port):
"""Завершение сессии""" """Завершение сессии"""
# Получаем пользователя в списке # Получаем пользователя в списке
@@ -348,6 +466,20 @@ 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(
@@ -356,5 +488,9 @@ class OnemeMobile:
self.logger.info(f"Сокет запущен на порту {self.port}") self.logger.info(f"Сокет запущен на порту {self.port}")
async with self.server: try:
await self.server.serve_forever() async with self.server:
await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()

View File

@@ -1,6 +1,8 @@
import logging import 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
@@ -8,7 +10,9 @@ from common.opcodes import Opcodes
from common.tools import Tools from common.tools import Tools
class OnemeWS: class OnemeWS:
def __init__(self, host, port, clients, ssl_context, db_pool, send_event): def __init__(
self, host, port, ssl_context, db_pool, clients, send_event, telegram_bot
):
self.host = host self.host = host
self.port = port self.port = port
self.ssl_context = ssl_context self.ssl_context = ssl_context
@@ -20,7 +24,13 @@ class OnemeWS:
self.opcodes = Opcodes() self.opcodes = Opcodes()
self.proto = WebProto() self.proto = WebProto()
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, type="web") self.processors = Processors(
db_pool=db_pool,
clients=clients,
send_event=send_event,
telegram_bot=telegram_bot,
type="web"
)
self.auth_required = Tools().auth_required self.auth_required = Tools().auth_required
# rate limiter # rate limiter
@@ -87,7 +97,7 @@ class OnemeWS:
) )
else: else:
await self.processors.auth( await self.processors.auth(
payload, seq, websocket, deviceType, deviceName, appVersion payload, seq, websocket, deviceType, deviceName, appVersion, address[0]
) )
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]):
@@ -99,7 +109,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 payload, seq, websocket, deviceType, deviceName, appVersion, address[0]
) )
else: else:
self.logger.warning( self.logger.warning(
@@ -118,7 +128,7 @@ class OnemeWS:
userPhone, userPhone,
userId, userId,
hashedToken, hashedToken,
) = await self.processors.login(payload, seq, websocket, deviceType, appVersion) ) = await self.processors.login(payload, seq, websocket, appVersion)
if userPhone: if userPhone:
await self._finish_auth( await self._finish_auth(
@@ -130,7 +140,7 @@ class OnemeWS:
) )
break break
case self.opcodes.PING: case self.opcodes.PING:
await self.processors.ping(payload, seq, websocket) await self.processors.ping(payload, seq, websocket, userId)
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:
@@ -141,6 +151,54 @@ 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,
@@ -168,6 +226,15 @@ 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,
@@ -266,6 +333,32 @@ 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:
@@ -292,23 +385,59 @@ 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):
"""Завершение сессии""" """Завершение сессии"""
# Получаем пользователя в списке # Получаем пользователя в списке
@@ -324,6 +453,20 @@ 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(
@@ -335,4 +478,9 @@ class OnemeWS:
self.logger.info(f"WebSocket запущен на порту {self.port}") self.logger.info(f"WebSocket запущен на порту {self.port}")
await self.server.wait_closed() try:
await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise

View File

@@ -9,319 +9,12 @@ class TTConfig:
"googNoiseSuppression": "true", "googNoiseSuppression": "true",
"googHighpassFilter": "false", "googHighpassFilter": "false",
"googTypingNoiseDetection": "false", "googTypingNoiseDetection": "false",
"googAudioNetworkAdaptorConfig": "ChyyARkNCtcjPBUK1yM8GKjDASCw6gEomHUwoJwBCgfKAQQIABAACgvCAQgIqMMBELiRAgosqgEpChEIuBcVzcxMPhjogQIlCtejOxIRCOgHFc3MTD4YsOoBJQrXozsYyAEKC7oBCAiw6gEQoJwB" "googAudioNetworkAdaptorConfig": ""
}, },
"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,
@@ -383,10 +76,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": "https://tt.me/starwear", "invite-link": "",
"invite-long": "Я общаюсь в ТамТам, присоединяйся https://tt.me/starwear", "invite-long": "",
"invite-short": "Привет! Ставь ТамТам! Жду ответа! https://tt.me/starwear", "invite-short": "",
"keep-connection": 2, "keep-connection": 2,
"l10n": False, "l10n": False,
"live-location-enabled": True, "live-location-enabled": True,
@@ -453,25 +146,12 @@ 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": True, "promo-recent-contacts": False,
"promo_contact_label": "Белый Маг", "promo_contact_label": "",
"proxy": "msgproxy.okcdn.ru", "proxy": "",
"proxy-domains": [ "proxy-domains": [],
"okcdn.ru", "proxy-exclude": [],
"mycdn.me", "proxy-rotation": False,
"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": [],
@@ -519,24 +199,24 @@ class TTConfig:
"TOP" "TOP"
], ],
"stickers-suggestion-keywords-inline": False, "stickers-suggestion-keywords-inline": False,
"support-account": "tt.me/support", "support-account": "",
"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": "https://st.okcdn.ru/static/messages/2022-08-25noto/TamNotoColorEmojiCompat.ttf", "tam-emoji-font-url": "",
"tcp-candidates": False, "tcp-candidates": False,
"tracer-crash-report-enabled": True, "tracer-crash-report-enabled": False,
"tracer-crash-report-host": "https://api-hprof.odkl.ru", "tracer-crash-report-host": "",
"tracer-crash-send-asap-enabled": True, "tracer-crash-send-asap-enabled": False,
"tracer-crash-send-logs-enabled": True, "tracer-crash-send-logs-enabled": False,
"tracer-crash-send-threads-dump-enabled": True, "tracer-crash-send-threads-dump-enabled": False,
"tracer-disk-overflow-report-threshold": 3000000000, "tracer-disk-overflow-report-threshold": 3000000000,
"tracer-disk-usage-probability": 500, "tracer-disk-usage-probability": 500,
"tracer-enabled": True, "tracer-enabled": False,
"tracer-host": "https://api-hprof.odkl.ru", "tracer-host": "",
"tracer-hprof-probability": -1, "tracer-hprof-probability": -1,
"tracer-sampled-conditions": "tag=app_start_ui_freeze_2k;probability=100000;startEvent=app_first_activity_created;interestingEvent=app_freeze;interestingDuration=2000", "tracer-sampled-conditions": "",
"tracer-sampled-duration": 20000, "tracer-sampled-duration": 20000,
"tracer-systrace-duration": 20000, "tracer-systrace-duration": 20000,
"tracer-systrace-interesting-duration": 10000, "tracer-systrace-interesting-duration": 10000,
@@ -569,4 +249,4 @@ class TTConfig:
"iceServers": [], "iceServers": [],
"has-phone": True, "has-phone": True,
"promo-constructors": [] "promo-constructors": []
} }

View File

@@ -3,10 +3,106 @@ 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():

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 osVersion: str = None
timezone: str timezone: str = None
screen: str screen: str = None
pushDeviceType: str = None pushDeviceType: str = None
locale: str locale: str = None
deviceName: str deviceName: str
deviceLocale: str deviceLocale: str = None
class HelloPayloadModel(pydantic.BaseModel): class HelloPayloadModel(pydantic.BaseModel):
userAgent: UserAgentModel userAgent: UserAgentModel
deviceId: str deviceId: str = None
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 authTokenType: str = None
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 interactive: bool = None
token: str token: str
class SearchUsersPayloadModel(pydantic.BaseModel): class SearchUsersPayloadModel(pydantic.BaseModel):
@@ -41,4 +41,99 @@ 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

View File

@@ -2,9 +2,19 @@ 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,
pass AssetsProcessors,
ChatsProcessors,
ContactsProcessors,
MessagesProcessors,
SessionsProcessors):
pass

View File

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

View File

@@ -4,10 +4,12 @@ import time
import json import 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
@@ -17,6 +19,172 @@ 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):
"""Обработчик запроса кода""" """Обработчик запроса кода"""
# Валидируем данные пакета # Валидируем данные пакета
@@ -30,29 +198,51 @@ 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 = {
@@ -71,7 +261,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, packet) await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code}") self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
async def auth(self, payload, seq, writer): async def auth(self, payload, seq, writer):
"""Обработчик проверки кода""" """Обработчик проверки кода"""
@@ -112,13 +302,32 @@ 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)
) )
@@ -158,22 +367,17 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet) await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName): async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, ip):
"""Обработчик финальной аутентификации""" """Обработчик финальной аутентификации / регистрации"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return
# Извлекаем данные из пакета # Извлекаем данные из пакета
token = payload.get("token") 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()
@@ -181,10 +385,9 @@ 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,)
@@ -196,53 +399,36 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return return
# Если авторизация только началась - отдаем ошибку # Если авторизация только началась (код ещё не проверен) - отдаем ошибку
if stored_token.get("state") == "started": if stored_token.get("state") == "started" or stored_token.get("state") == "register":
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, 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()
# Удаляем токен # Если пользователь есть, производим создание сессии
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,)) if account:
resp_payload = await self._finish_auth(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, account, deviceType, deviceName, ip, login
)
else: # в ином случае производим регистрацию
resp_payload = await self._finish_reg(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, deviceType, deviceName, ip, login
)
# Создаем сессию if resp_payload is None:
await cursor.execute( return
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(stored_token.get("phone"), hashed_login, deviceType, deviceName,
"Epstein Island", int(time.time()))
)
# Аватарка с биографией
photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
payload = {
"userToken": "0", # Пока как заглушка
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
# Создаем пакет # Создаем пакет
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=resp_payload
) )
# Отправляем # Отправляем
@@ -257,7 +443,7 @@ class AuthProcessors(BaseProcessor):
self.logger.error(f"Возникли ошибки при валидации пакета: {e}") 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 return None, None, None
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
chats = [] chats = []
@@ -278,7 +464,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 return None, None, None
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
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"),))
@@ -290,8 +476,8 @@ 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()
@@ -330,16 +516,25 @@ 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": "e5903aa8-0000000000000000-80000106-0000000000000001-00000001-0000000000000000-00000000-2-00000001-0000019c9559d057", "hash": "0",
"server": self.server_config, "server": self.server_config,
"user": updated_user_config, "user": updated_user_config,
"chatFolders": { "chatFolders": {
@@ -363,6 +558,10 @@ 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
@@ -370,4 +569,39 @@ 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)

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import json
import pydantic 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):
@@ -35,6 +37,106 @@ 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):
"""Обработчик пинга""" """Обработчик пинга"""

View File

@@ -0,0 +1,168 @@
import pydantic
from classes.baseprocessor import BaseProcessor
from tamtam.models import (
TypingPayloadModel,
SendMessagePayloadModel
)
class MessagesProcessors(BaseProcessor):
async def msg_typing(self, payload, seq, writer, senderId):
"""Обработчик события печатания"""
# Валидируем данные пакета
try:
TypingPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.MSG_TYPING, self.error_types.INVALID_PAYLOAD, writer)
return
# Извлекаем данные из пакета
chatId = payload.get("chatId")
type = payload.get("type") or "TYPING"
# Ищем чат в базе данных
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
# Если чат не найден, отправляем ошибку
if not chat:
await self._send_error(seq, self.opcodes.MSG_TYPING, self.error_types.CHAT_NOT_FOUND, writer)
return
# Участники чата
participants = await self.tools.get_chat_participants(chatId, self.db_pool)
# Проверяем, является ли отправитель участником чата
if int(senderId) not in participants:
await self._send_error(seq, self.opcodes.MSG_TYPING, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Рассылаем событие участникам чата
for participant in participants:
if participant != senderId:
# Если участник не является отправителем, отправляем
await self.event(
participant,
{
"eventType": "typing",
"chatId": chatId,
"type": type,
"userId": senderId,
"writer": writer,
}
)
# Создаем пакет
packet = self.proto.pack_packet(
seq=seq, opcode=self.opcodes.MSG_TYPING
)
# Отправляем пакет
await self._send(writer, packet)
async def msg_send(self, payload, seq, writer, senderId, db_pool):
"""Функция отправки сообщения"""
# Валидируем данные пакета
try:
SendMessagePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.INVALID_PAYLOAD, writer)
return
# Извлекаем данные из пакета
userId = payload.get("userId")
chatId = payload.get("chatId")
message = payload.get("message")
elements = message.get("elements") or []
attaches = message.get("attaches") or []
cid = message.get("cid") or 0
text = message.get("text") or ""
# Вычисляем ID чата по ID пользователя и ID отправителя,
# в случае отсутствия ID чата
if chatId is None:
chatId = userId ^ senderId
async with db_pool.acquire() as db_connection:
async with db_connection.cursor() as cursor:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
# Если нет такого чата - выбрасываем ошибку
if not chat:
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_FOUND, writer)
return
# Список участников
participants = await self.tools.get_chat_participants(chatId, db_pool)
# Проверяем, является ли отправитель участником чата
if int(senderId) not in participants:
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId,
senderId=senderId,
text=text,
attaches=attaches,
elements=elements,
cid=cid,
type="USER",
db_pool=self.db_pool
)
# Готовое тело сообщения
bodyMessage = {
"id": messageId,
"time": messageTime,
"type": "USER",
"sender": senderId,
"cid": cid,
"text": text,
"attaches": attaches,
"elements": elements
}
# Отправляем событие всем участникам чата
for participant in participants:
await self.event(
participant,
{
"eventType": "new_msg",
"chatId": chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
"writer": writer
}
)
# Данные пакета
payload = {
"chatId": chatId,
"message": bodyMessage,
"unread": 0,
"mark": messageTime
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.MSG_SEND, payload=payload
)
# Отправляем
await self._send(writer, packet)

View File

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

View File

@@ -0,0 +1,38 @@
from classes.baseprocessor import BaseProcessor
class SessionsProcessors(BaseProcessor):
async def sessions_info(self, payload, seq, writer, senderPhone, hashedToken):
"""Получение активных сессий на аккаунте"""
# Готовый список сессий
sessions = []
# Ищем сессии в бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM tokens WHERE phone = %s", (str(senderPhone),))
user_sessions = await cursor.fetchall()
# Собираем сессии в список
for session in user_sessions:
sessions.append(
{
"time": int(session.get("time")),
"client": f"TamTam {session.get('device_type')}",
"info": session.get("device_name"),
"location": session.get("location"),
"current": True if session.get("token_hash") == hashedToken else False
}
)
# Создаем данные пакета
payload = {
"sessions": sessions
}
# Создаем пакет
response = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.SESSIONS_INFO, payload=payload
)
# Отправляем
await self._send(writer, response)

View File

@@ -24,7 +24,7 @@ class TamTamMobile:
self.auth_required = Tools().auth_required self.auth_required = Tools().auth_required
# rate limiter # rate limiter
self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60) self.auth_rate_limiter = RateLimiter(max_attempts=15, window_seconds=60)
self.read_timeout = 300 # Таймаут чтения из сокета (секунды) self.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) await self.processors.auth_confirm(payload, seq, writer, deviceType, deviceName, address[0])
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,18 +105,111 @@ 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):
"""Завершение открытия сессии""" """Завершение открытия сессии"""
@@ -130,7 +223,8 @@ 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:
@@ -142,7 +236,8 @@ class TamTamMobile:
"writer": writer, "writer": writer,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "tamtam" "protocol": "tamtam",
"type": "tcp"
} }
] ]
} }
@@ -170,5 +265,9 @@ class TamTamMobile:
self.logger.info(f"Сокет запущен на порту {self.port}") self.logger.info(f"Сокет запущен на порту {self.port}")
async with self.server: try:
await self.server.serve_forever() async with self.server:
await self.server.serve_forever()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()

View File

@@ -1,6 +1,7 @@
import logging import 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
@@ -82,7 +83,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) await self.processors.auth_confirm(payload, seq, websocket, deviceType, deviceName, address[0])
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)
@@ -91,6 +92,11 @@ 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
@@ -99,6 +105,86 @@ 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:
@@ -125,7 +211,8 @@ 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:
@@ -137,7 +224,8 @@ class TamTamWS:
"writer": websocket, "writer": websocket,
"ip": addr[0], "ip": addr[0],
"port": addr[1], "port": addr[1],
"protocol": "tamtam" "protocol": "tamtam",
"type": "web"
} }
] ]
} }
@@ -168,4 +256,9 @@ class TamTamWS:
self.logger.info(f"TT WebSocket запущен на порту {self.port}") self.logger.info(f"TT WebSocket запущен на порту {self.port}")
await self.server.wait_closed() try:
await self.server.wait_closed()
except asyncio.CancelledError:
self.server.close()
await self.server.wait_closed()
raise

View File

@@ -14,7 +14,7 @@ from common.tools import Tools
class TelegramBot: class TelegramBot:
def __init__(self, token, enabled, db_pool, whitelist_ids=None): def __init__(self, token, enabled, db_pool, whitelist_ids=None, whitelist_enabled=False):
self.bot = Bot(token=token) self.bot = Bot(token=token)
self.dp = Dispatcher() self.dp = Dispatcher()
self.router = Router() self.router = Router()
@@ -23,6 +23,7 @@ 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()
@@ -59,12 +60,13 @@ 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_ids and tg_id not in self.whitelist_ids: if self.whitelist_enabled:
await message.answer( if self.whitelist_ids and tg_id not in self.whitelist_ids:
self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED) await message.answer(
) self.get_bot_message(self.msg_types.ID_NOT_WHITELISTED)
return )
return
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:
@@ -81,16 +83,24 @@ 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
message.from_user.first_name[:59], # firstname firstname, # firstname
(message.from_user.last_name or "")[:59], # lastname lastname, # lastname
(message.from_user.username or "")[:60], # username username, # username
json.dumps([]), # profileoptions json.dumps([]), # profileoptions
json.dumps(["TT", "ONEME"]), # options json.dumps(["TT", "ONEME"]), # options
0, # accountstatus 0, # accountstatus
@@ -129,7 +139,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) await self.dp.start_polling(self.bot, handle_signals=False)
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка запуска Telegram бота: {e}") self.logger.error(f"Ошибка запуска Telegram бота: {e}")
else: else:

View File

@@ -19,7 +19,8 @@ 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()

View File

@@ -1,7 +1,7 @@
CREATE TABLE `users` ( CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT, `id` INT NOT NULL,
`phone` VARCHAR(20) UNIQUE, `phone` VARCHAR(20) UNIQUE,
`telegram_id` VARCHAR(64) UNIQUE, `telegram_id` VARCHAR(64),
`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` INT NOT NULL AUTO_INCREMENT, `id` BIGINT NOT NULL,
`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,6 +102,7 @@ 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,