mirror of
https://github.com/openmax-server/server.git
synced 2026-07-09 05:20:12 +03:00
fc6ec5eec5
* Различные фиксы (#14)
* Исключаем только ошибку валидации
* Небольшие правки вебсокета тамтама
* Теперь номера брутить чутка сложнее
* Авторизация теперь проверяется для некоторых команд
* Теперь проверяется orign у вебсокета тамтама
* Дополнил пример env
* Починил немного сокет тамтама
* Теперь все должно работать исправно
* Документация: описал про SESSION_INIT
* Update deviceId description in tamtam_ws.md
* feat: 16 опкод для обновления профиля
* feat: 23 опкод для регистрации, смс шлюз, докер
* SQLite: Ошибка
И вообще не юзайте sqlite пожалуйста, он баганный
* Серверный конфиг снова сломался
* Починил изменение профиля
* История сообщений (вроде как даже работает)
* Дополнил немного 48 опкод, и починил отдачу истории в избранном (избранное все равно не работает)
* Доделал авторизацию в ТамТаме
* Швырнул архитектуру, чтобы позже объединить контроллеры веба и сокета в одно, а также разделить процессоры
* Удалил tamtam.proto, поскольку нигде больше не используется
* Поделил процессоры в таме
* Коды стран
* Генерируем айди, за вместо того, чтобы писать его попорядку
* Вынес список участников чата в отдельную таблицу
* Теперь мы не должны отправлять 128 опкод самому себе. вроде
* ТамТам: реализация 32 опкода. Надеюсь, будет работать
* ТамТам: обработчик 5 и 1 опкода
* ТамТам: починил 32 опкод и добавил серверный конфиг. Избранное больше не добавляется в список чатов
MAX: починил 32 опкод
* TT: oops
* MAX: заглушка в 158 опкоде
* Common: поправил 1 прикол
* TamTam: websocket transport for web version
* TamTam && MAX: история (в мохе она вроде теперь получше работает)
* Правка контроллеров небольшая
* MAX: Разделил процессоры
* TG Bot: переписал на aiogram
* Common: фикс года
* Начальная реализация транспорта ws для max web и прочие улучшения
* Common: добавил сжатие lz4
* Исправление конфликтов
* TT: фикс вебсокета
* Fix OpenMAX mobile compatibility and Telegram auth fallback (#30)
* Fix OpenMAX mobile compatibility and Telegram auth fallback
* Common: Убрал скобку в конфиге
---------
Co-authored-by: Alexey Polyakov <starwear3000@mail.ru>
* MAX: заглушка для баннеров, правка пакета со списком жалоб, отдача контактов и прочие улучшения
* oops
* MAX: Рабочие баннеры
* MAX: обновление настроек приватности
* MAX: пуши через firebase (особо не тестил, вроде работает)
* MAX: Рефактор папок
* MAX: фикс версий < 25.8.0
* MAX: отдача черного списка
* Теперь ID пользователей идут по порядку, как и раньше
* MAX: создание папок
* MAX: oops
* MAX: добавление/удаление контактов, статусы
* MAX & TT: теперь в качестве страны локации используется настоящая страна пользователя, а также зафиксировал версии библиотек в зависимостях
* TT: ну вроде шире поддержка, а вообще обратная совместимость с максом клас
* oops
* TG Bot: возможность включения белого списка
* Решение проблемы обработки sigterm
* nothing
* fix
* TG Bot: fix
* TG Bot: fix username
* MAX && TT: общение в таме, и корректировки под веб морду
* MAX: Почистил серверный конфиг
* nothing
* MAX: web fix
* MAX: web fix 2
* oops
* nothing
* MAX: теперь для избранного не сравниваем айди с нулём
* Вынес sqlite в отдельный модуль
* MAX: блокировка и разблокировка контакта
* oops
* TT: тоже самое что в макс
* MAX && TT: обновление контакта
* MAX: добавление контакта по номеру телефона
* MAX: исправление уязвимости с избранными
* Генерируем айди пользователей рандомно (опять, да)
* update sql scheme
* Фикс сборки избранного
* Попытка починить историю (спойлер, нихуя не получилось)
* 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 тоже теперь шлют корректную схему.
* 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.
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* fix(chat history): фикс
* MAX & TT: теперь полноценный чёрный список
* TT: регистрация через клиент
* Вроде как нормальные заглушки под ассеты
* maybe fixes
---------
Co-authored-by: WowInceptionGood <143893762+WowInceptionGood@users.noreply.github.com>
Co-authored-by: zavolo <me@kirillka.ru>
Co-authored-by: Aleksandr Kosachev <38947781+gohryt@users.noreply.github.com>
640 lines
25 KiB
Python
640 lines
25 KiB
Python
import json
|
|
import random
|
|
import secrets
|
|
import time
|
|
|
|
import geoip2.database
|
|
|
|
|
|
class Tools:
|
|
def __init__(self):
|
|
pass
|
|
|
|
def build_message_dict(self, row, protocol_type="mobile"):
|
|
"""Сборка тела сообщения"""
|
|
try:
|
|
attaches = json.loads(row.get("attaches") or "[]")
|
|
except (TypeError, ValueError):
|
|
attaches = []
|
|
try:
|
|
elements = json.loads(row.get("elements") or "[]")
|
|
except (TypeError, ValueError):
|
|
elements = []
|
|
|
|
message = {
|
|
"id": row.get("id") if protocol_type == "mobile" else str(row.get("id")),
|
|
"cid": int(row.get("cid") or 0),
|
|
"chatId": int(row.get("chat_id") or 0),
|
|
"time": int(row.get("time")),
|
|
"type": row.get("type") or "USER",
|
|
"sender": row.get("sender"),
|
|
"text": row.get("text") or "",
|
|
"attaches": attaches,
|
|
"elements": elements,
|
|
"reactionInfo": {},
|
|
"link": {}
|
|
}
|
|
|
|
return message
|
|
|
|
def generate_profile(
|
|
self,
|
|
id=None,
|
|
phone=None,
|
|
avatarUrl=None,
|
|
photoId=None,
|
|
updateTime=None,
|
|
firstName=None,
|
|
lastName=None,
|
|
options=None,
|
|
description=None,
|
|
accountStatus=None,
|
|
profileOptions=None,
|
|
includeProfileOptions=True,
|
|
username=None,
|
|
|
|
# для контактов, собственно
|
|
custom_firstname=None,
|
|
custom_lastname=None,
|
|
|
|
blocked=False
|
|
):
|
|
contact = {
|
|
"id": id,
|
|
"updateTime": updateTime,
|
|
"phone": phone,
|
|
"names": [
|
|
{
|
|
"name": firstName,
|
|
"firstName": firstName,
|
|
"lastName": lastName,
|
|
"type": "ONEME",
|
|
}
|
|
],
|
|
"options": options,
|
|
"accountStatus": accountStatus,
|
|
"location": "RU",
|
|
"registrationTime": int(time.time() * 1000)
|
|
}
|
|
|
|
if avatarUrl:
|
|
contact["photoId"] = photoId
|
|
contact["baseUrl"] = avatarUrl
|
|
contact["baseRawUrl"] = avatarUrl
|
|
|
|
if description:
|
|
contact["description"] = description
|
|
|
|
if username:
|
|
contact["link"] = "https://max.ru/" + username
|
|
|
|
if custom_firstname:
|
|
contact["names"].append(
|
|
{
|
|
"name": custom_firstname,
|
|
"firstName": custom_firstname,
|
|
"lastName": custom_lastname,
|
|
"type": "CUSTOM"
|
|
}
|
|
)
|
|
|
|
if blocked:
|
|
contact["status"] = "BLOCKED"
|
|
|
|
if includeProfileOptions:
|
|
return {"contact": contact, "profileOptions": profileOptions}
|
|
else:
|
|
return contact
|
|
|
|
def generate_profile_tt(
|
|
self,
|
|
id=None,
|
|
phone=None,
|
|
avatarUrl=None,
|
|
photoId=None,
|
|
updateTime=None,
|
|
firstName=None,
|
|
lastName=None,
|
|
options=None,
|
|
description=None,
|
|
username=None,
|
|
custom_firstname=None,
|
|
custom_lastname=None,
|
|
blocked=None
|
|
):
|
|
# Так как TT не поддерживает фамилию, и если нам ее не передали в функцию
|
|
# то используем только имя, чтобы избежать None в фамилии
|
|
if firstName and lastName:
|
|
name = f"{firstName} {lastName}"
|
|
else:
|
|
name = firstName
|
|
|
|
# Используем такой же костыль, как и выше
|
|
if custom_firstname and custom_lastname:
|
|
custom_name = f"{custom_firstname} {custom_lastname}"
|
|
elif custom_firstname:
|
|
custom_name = custom_firstname
|
|
else:
|
|
custom_name = None
|
|
|
|
contact = {
|
|
"id": id,
|
|
"updateTime": updateTime,
|
|
"phone": phone,
|
|
"names": [{"name": name, "type": "TT"}],
|
|
"options": options,
|
|
}
|
|
|
|
if avatarUrl:
|
|
contact["photoId"] = photoId
|
|
contact["baseUrl"] = avatarUrl
|
|
contact["baseRawUrl"] = avatarUrl
|
|
|
|
if description:
|
|
contact["description"] = description
|
|
|
|
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
|
|
# можно потом как нибудь сделать возможность редактирования этого момента, но это
|
|
# позже, так как по юзернейму искать пока нельзя
|
|
if username:
|
|
contact["link"] = "https://tt.me/" + username
|
|
|
|
if custom_firstname:
|
|
contact["names"].append(
|
|
{"name": custom_name, "type": "CUSTOM"}
|
|
)
|
|
|
|
if blocked:
|
|
contact["status"] = "BLOCKED"
|
|
|
|
return contact
|
|
|
|
def generate_chat(
|
|
self, id, owner, type, participants, lastMessage, lastEventTime, prevMessageId=0
|
|
):
|
|
"""Генерация чата"""
|
|
# Генерируем список участников
|
|
if isinstance(participants, dict):
|
|
result_participants = {
|
|
int(k): int(v) if v is not None else 0 for k, v in participants.items()
|
|
}
|
|
else:
|
|
# assume list
|
|
result_participants = {int(participant): 0 for participant in participants}
|
|
|
|
result = None
|
|
|
|
# Генерируем нужный список в зависимости от типа чата
|
|
if type == "DIALOG":
|
|
result = {
|
|
"id": id,
|
|
"type": type,
|
|
"status": "ACTIVE",
|
|
"owner": owner,
|
|
"participants": result_participants,
|
|
"lastMessage": lastMessage,
|
|
"lastEventTime": lastEventTime,
|
|
"lastDelayedUpdateTime": 0,
|
|
"lastFireDelayedErrorTime": 0,
|
|
"created": 1,
|
|
"cid": id,
|
|
"prevMessageId": prevMessageId,
|
|
"joinTime": 1,
|
|
"modified": lastEventTime,
|
|
}
|
|
|
|
# Возвращаем
|
|
return result
|
|
|
|
async def generate_chats(
|
|
self,
|
|
chatIds,
|
|
db_pool,
|
|
senderId,
|
|
include_favourites=True,
|
|
protocol_type="mobile",
|
|
):
|
|
"""Генерирует чаты для отдачи клиенту"""
|
|
# Готовый список с чатами
|
|
chats = []
|
|
|
|
# Формируем список чатов
|
|
for chatId in chatIds:
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
# Получаем чат по id
|
|
await cursor.execute(
|
|
"SELECT * FROM `chats` WHERE id = %s", (chatId,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
|
|
if row:
|
|
# Получаем последнее сообщение из чата
|
|
message, messageTime = await self.get_last_message(
|
|
chatId, db_pool, protocol_type=protocol_type
|
|
)
|
|
|
|
# Формируем список участников с временем последней активности
|
|
participant_ids = await self.get_chat_participants(
|
|
chatId, db_pool
|
|
)
|
|
|
|
participants = await self.get_participant_last_activity(
|
|
chatId, participant_ids, db_pool
|
|
)
|
|
|
|
# Получаем ID предыдущего сообщения
|
|
prevMessageId = await self.get_previous_message_id(
|
|
chatId, db_pool, protocol_type=protocol_type
|
|
)
|
|
|
|
# Выносим результат в лист
|
|
chats.append(
|
|
self.generate_chat(
|
|
row.get("id"),
|
|
row.get("owner"),
|
|
row.get("type"),
|
|
participants,
|
|
message,
|
|
messageTime,
|
|
prevMessageId,
|
|
)
|
|
)
|
|
|
|
if include_favourites:
|
|
# Получаем последнее сообщение из избранного
|
|
favouriteChatId = -senderId
|
|
message, messageTime = await self.get_last_message(
|
|
favouriteChatId, db_pool, protocol_type=protocol_type
|
|
)
|
|
|
|
# ID избранного для клиента
|
|
chatId = senderId ^ senderId
|
|
|
|
# Получаем последнюю активность в избранном
|
|
participants = await self.get_participant_last_activity(
|
|
favouriteChatId, [senderId], db_pool
|
|
)
|
|
|
|
# Получаем ID предыдущего сообщения для избранного
|
|
prevMessageId = await self.get_previous_message_id(
|
|
favouriteChatId, db_pool, protocol_type=protocol_type
|
|
)
|
|
|
|
# Хардкодим в лист чатов избранное
|
|
chats.append(
|
|
self.generate_chat(
|
|
chatId,
|
|
senderId,
|
|
"DIALOG",
|
|
participants,
|
|
message,
|
|
messageTime,
|
|
prevMessageId,
|
|
)
|
|
)
|
|
|
|
return chats
|
|
|
|
async def generate_contacts(
|
|
self,
|
|
contacts,
|
|
db_pool,
|
|
avatar_base_url="",
|
|
):
|
|
"""
|
|
Генерация контакт-листа для отдачи клиенту
|
|
|
|
[notes]
|
|
В contacts должен поступать список вида
|
|
|
|
[
|
|
{
|
|
"firstname": "test",
|
|
"lastname": "testovich",
|
|
"id": 4323
|
|
}
|
|
]
|
|
|
|
А формировать мы должны его до вызова функции,
|
|
ибо я хочу вынести контакты в отдельную таблицу,
|
|
по моему мнению так будет намного практичнее и лучше
|
|
"""
|
|
# Готовый список с контакт-листом
|
|
contact_list = []
|
|
|
|
# Формируем список контактов
|
|
for contact in contacts:
|
|
# ID контакта
|
|
contact_id = contact.get("id")
|
|
|
|
# Имя и фамилия которые указал юзер для контакта
|
|
firstname = contact.get("firstname")
|
|
lastname = contact.get("lastname")
|
|
blocked = contact.get("blocked", False)
|
|
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
# Получаем контакт по id
|
|
await cursor.execute(
|
|
"SELECT * FROM `users` WHERE id = %s", (contact_id,)
|
|
)
|
|
user = await cursor.fetchone()
|
|
|
|
if user:
|
|
# Аватарка с биографией
|
|
photoId = (
|
|
None
|
|
if not user.get("avatar_id")
|
|
else int(user.get("avatar_id"))
|
|
)
|
|
avatar_url = (
|
|
None
|
|
if not photoId
|
|
else avatar_base_url + str(photoId)
|
|
)
|
|
description = (
|
|
None
|
|
if not user.get("description")
|
|
else user.get("description")
|
|
)
|
|
|
|
# Создаем профиль
|
|
contact = self.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")),
|
|
description=description,
|
|
accountStatus=int(user.get("accountstatus")),
|
|
includeProfileOptions=False,
|
|
username=user.get("username"),
|
|
custom_firstname=firstname,
|
|
custom_lastname=lastname,
|
|
blocked=blocked,
|
|
)
|
|
|
|
# Выносим результат в лист
|
|
contact_list.append(contact)
|
|
|
|
return contact_list
|
|
|
|
async def collect_user_contacts(
|
|
self,
|
|
owner_id,
|
|
db_pool,
|
|
avatar_base_url="",
|
|
):
|
|
"""Собирает все контакты пользователя и возвращает готовый контакт-лист"""
|
|
contacts = []
|
|
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
await cursor.execute(
|
|
"SELECT * FROM `contacts` WHERE owner_id = %s AND is_blocked = FALSE",
|
|
(owner_id,),
|
|
)
|
|
rows = await cursor.fetchall()
|
|
|
|
for row in rows:
|
|
contacts.append(
|
|
{
|
|
"id": int(row.get("contact_id")),
|
|
"firstname": row.get("custom_firstname"),
|
|
"lastname": row.get("custom_lastname"),
|
|
"blocked": bool(row.get("is_blocked")),
|
|
}
|
|
)
|
|
|
|
return await self.generate_contacts(
|
|
contacts, db_pool, avatar_base_url=avatar_base_url
|
|
)
|
|
|
|
async def insert_message(
|
|
self, chatId, senderId, text, attaches, elements, cid, type, db_pool
|
|
):
|
|
"""Добавление сообщения в историю"""
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
# Получаем id последнего сообщения в чате
|
|
await cursor.execute(
|
|
"SELECT id FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1",
|
|
(chatId,),
|
|
)
|
|
|
|
row = await cursor.fetchone() or {}
|
|
last_message_id = row.get("id") or 0 # последнее id сообщения в чате
|
|
message_time = int(time.time() * 1000) # время отправки сообщения
|
|
|
|
# Генерируем ID сообщения
|
|
message_id = int(time.time() * 1000000) * 1000 + random.randint(100, 999)
|
|
|
|
# Вносим новое сообщение в таблицу
|
|
await cursor.execute(
|
|
"INSERT INTO `messages` (id, chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
|
(
|
|
message_id,
|
|
chatId,
|
|
senderId,
|
|
message_time,
|
|
text,
|
|
json.dumps(attaches),
|
|
cid,
|
|
json.dumps(elements),
|
|
type,
|
|
),
|
|
)
|
|
|
|
# Возвращаем айдишки
|
|
return int(message_id), int(last_message_id), message_time
|
|
|
|
async def get_last_message(self, chatId, db_pool, protocol_type="mobile"):
|
|
"""Получение последнего сообщения в чате"""
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
# Получаем id последнего сообщения в чате
|
|
await cursor.execute(
|
|
"SELECT * FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1",
|
|
(chatId,),
|
|
)
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
# Если нет результатов - возвращаем None
|
|
if not row:
|
|
return None, None
|
|
|
|
# Возвращаем
|
|
return self.build_message_dict(row, protocol_type), int(row.get("time"))
|
|
|
|
async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
|
|
"""Получение ID предыдущего сообщения (второго с конца) в чате."""
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
await cursor.execute(
|
|
"SELECT id FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT 1 OFFSET 1",
|
|
(chatId,),
|
|
)
|
|
row = await cursor.fetchone()
|
|
|
|
# Если результат есть, возвращаем его
|
|
if row:
|
|
return (
|
|
row.get("id")
|
|
if protocol_type == "mobile"
|
|
else str(row.get("id"))
|
|
)
|
|
|
|
# В ином случае возвращаем 0
|
|
return 0 if protocol_type == "mobile" else "0"
|
|
|
|
async def get_participant_last_activity(self, chatId, participant_ids, db_pool):
|
|
"""Возвращает словарь {participant_id: last_activity_time} для участников чата."""
|
|
if not participant_ids:
|
|
return {}
|
|
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
# Собираем всех участников
|
|
placeholders = ",".join(["%s"] * len(participant_ids))
|
|
query = f"""
|
|
SELECT sender, MAX(time) as last_time
|
|
FROM messages
|
|
WHERE chat_id = %s AND sender IN ({placeholders})
|
|
GROUP BY sender
|
|
"""
|
|
params = (chatId,) + tuple(participant_ids)
|
|
await cursor.execute(query, params)
|
|
rows = await cursor.fetchall()
|
|
|
|
# Собираем список участников без времени последней активности в чате
|
|
result = {int(pid): 0 for pid in participant_ids}
|
|
|
|
# Обновляем для каждого участника время последней активности в чате
|
|
for row in rows:
|
|
sender = int(row["sender"])
|
|
last_time = row["last_time"]
|
|
if last_time is not None:
|
|
result[sender] = int(last_time)
|
|
|
|
return result
|
|
|
|
async def get_chat_participants(self, chatId, db_pool):
|
|
"""Возвращает список ID участников чата из таблицы chat_participants."""
|
|
async with db_pool.acquire() as db_connection:
|
|
async with db_connection.cursor() as cursor:
|
|
await cursor.execute(
|
|
"SELECT user_id FROM chat_participants WHERE chat_id = %s",
|
|
(chatId,),
|
|
)
|
|
rows = await cursor.fetchall()
|
|
return [int(row["user_id"]) for row in rows]
|
|
|
|
async def auth_required(self, userPhone, coro, *args):
|
|
if userPhone:
|
|
await coro(*args)
|
|
|
|
async def update_user_config(self, cursor, phone, user_settings, default_settings):
|
|
"""Функция для обновления юзер конфига из бд в случае его изменения"""
|
|
|
|
user_config = json.loads(user_settings)
|
|
updated_config = {**default_settings, **user_config}
|
|
|
|
if updated_config != user_config:
|
|
await cursor.execute(
|
|
"UPDATE user_data SET user_config = %s WHERE phone = %s",
|
|
(json.dumps(updated_config), phone),
|
|
)
|
|
|
|
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 |