diff --git a/.env.example b/.env.example index 4e9911a..90145fa 100644 --- a/.env.example +++ b/.env.example @@ -26,4 +26,5 @@ telegram_bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ" telegram_bot_enabled = "1" telegram_whitelist_ids = "1,2,3" origins="http://127.0.0.1,https://web.openmax.su" -sms_gateway_url = "http://127.0.0.1:8100/sms-gateway" \ No newline at end of file +sms_gateway_url = "http://127.0.0.1:8100/sms-gateway" +firebase_credentials_path = "" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8ee9d44..fe86ff6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__ .env *.pem *.sqlite -*.crt \ No newline at end of file +*.crt +*-adminsdk-*.json \ No newline at end of file diff --git a/faq/patch_apk.md b/faq/patch_apk.md index 1b8a367..cf14532 100644 --- a/faq/patch_apk.md +++ b/faq/patch_apk.md @@ -22,3 +22,26 @@ 2. Открываем консоль в той же директории и производим декомпиляцию: `apktool d <имя apk> -o max` 3. Заходим в папку проекта и заменяем во всех классах "api.oneme.ru" на свой адрес сервера 4. Производим повторную сборку с помощью команды: `apktool b max -o max_modified.apk` + +--- + +# Патчинг Firebase для push-уведомлений + +> [!Important] +> Без замены Firebase-конфига пуши от вашего сервера не будут работать. + +1. Создайте проект в [Firebase Console](https://console.firebase.google.com/) и добавьте Android-приложение с пакетом `ru.oneme.app` +2. Скачайте `google-services.json` +3. В декомпилированном APK откройте `res/values/strings.xml` и замените следующие строки на значения из вашего `google-services.json`: + +| Строка | Оригинал | Откуда взять | +|---|---|---| +| `google_api_key` | `AIzaSyABuDYeeDXIOrKTXLkUj30Ii143ofPe63Q` | `client[0].api_key[0].current_key` | +| `google_app_id` | `1:659634599081:android:9605285443b661167225b8` | `client[0].client_info.mobilesdk_app_id` | +| `gcm_defaultSenderId` | `659634599081` | `project_info.project_number` | +| `project_id` | `max-messenger-app` | `project_info.project_id` | +| `google_crash_reporting_api_key` | `AIzaSyABuDYeeDXIOrKTXLkUj30Ii143ofPe63Q` | `client[0].api_key[0].current_key` | +| `google_storage_bucket` | `max-messenger-app.firebasestorage.app` | `project_info.storage_bucket` | + +4. Соберите и подпишите APK +5. В настройках проекта Firebase создайте сервисный аккаунт и укажите путь в `.env` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1ba8704..42e211a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ pydantic aiosqlite aiohttp python-dotenv -cryptography \ No newline at end of file +cryptography +firebase-admin \ No newline at end of file diff --git a/src/common/config.py b/src/common/config.py index a0dea08..7b340d5 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -51,3 +51,6 @@ class ServerConfig: ### sms шлюз sms_gateway_url = os.getenv("sms_gateway_url", "") + + ### Firebase + firebase_credentials_path = os.getenv("firebase_credentials_path", "") diff --git a/src/common/push.py b/src/common/push.py new file mode 100644 index 0000000..6cd1c00 --- /dev/null +++ b/src/common/push.py @@ -0,0 +1,92 @@ +import asyncio +import logging +import time +import firebase_admin +from firebase_admin import credentials, messaging + +class PushService: + def __init__(self, credentials_path): + self.logger = logging.getLogger(__name__) + + if not credentials_path: + self.logger.warning("Огненная база сегодня не работает, укажите путь к файлу с ключами") + self.enabled = False + return + + cred = credentials.Certificate(credentials_path) + firebase_admin.initialize_app(cred) + self.enabled = True + self.logger.info("Огненная база инициализирована") + + async def send(self, push_token, data): + """Отправка пуша""" + if not self.enabled: + return None + + str_data = {k: str(v) for k, v in data.items() if v is not None} + + message = messaging.Message( + data=str_data, + token=push_token, + android=messaging.AndroidConfig( + priority="high", + ), + ) + + try: + loop = asyncio.get_event_loop() + response = await loop.run_in_executor(None, messaging.send, message) + self.logger.debug(f"Отправил пуш: {response}") + return response + except messaging.UnregisteredError: + self.logger.warning(f"Пуш-токен не зарегистрирован: {push_token}") + return None + except Exception as e: + self.logger.error(f"Не удалось отправить пуш: {e}") + return None + + async def send_to_user(self, db_pool, phone, sender_id=None, msg_id=None, + chat_id=None, text="", is_group=False): + """Отправка пушей на все устройства пользователя""" + if not self.enabled: + return + + # Получаем имя отправителя + user_name = "" + if sender_id: + async with db_pool.acquire() as conn: + async with conn.cursor() as cursor: + await cursor.execute( + "SELECT firstname, lastname FROM users WHERE id = %s", + (sender_id,) + ) + sender = await cursor.fetchone() + if sender: + firstname = sender.get("firstname", "") + lastname = sender.get("lastname", "") + user_name = f"{firstname} {lastname}".strip() + + now_ms = str(int(time.time() * 1000)) + msg_type = "ChatMessage" if is_group else "Message" + data = { + "type": msg_type, + "msgid": str(msg_id) if msg_id else "0", + "suid": str(sender_id) if sender_id else None, + "mc": str(chat_id) if chat_id else None, + "msg": text, + "userName": user_name, + "ttime": now_ms, + "ctime": now_ms, + } + + # Получаем все пуш-токены пользователя + async with db_pool.acquire() as conn: + async with conn.cursor() as cursor: + await cursor.execute( + "SELECT push_token FROM tokens WHERE phone = %s AND push_token IS NOT NULL", + (phone,) + ) + rows = await cursor.fetchall() + + for row in rows: + await self.send(row["push_token"], data) diff --git a/src/main.py b/src/main.py index 212a82f..4e6617d 100644 --- a/src/main.py +++ b/src/main.py @@ -4,6 +4,7 @@ import logging import ssl from common.config import ServerConfig +from common.push import PushService from oneme.controller import OnemeController from tamtam.controller import TTController from telegrambot.controller import TelegramBotController @@ -130,14 +131,33 @@ def set_logging(): async def main(): """Запуск сервера""" - async def api_event(target, eventData): - for client in api.get("clients", {}).get(target, {}).get("clients", {}): - await controllers[client["protocol"]].event(target, client, eventData) - set_logging() db = await init_db() ssl_context = init_ssl() clients = {} + push_service = PushService(server_config.firebase_credentials_path) + + async def api_event(target, eventData): + target_clients = api.get("clients", {}).get(target, {}).get("clients", []) + + for client in target_clients: + await controllers[client["protocol"]].event(target, client, eventData) + + # Если у пользователя нет активных подключений + # и это новое сообщение - отсылаем пуш + if not target_clients and eventData.get("eventType") == "new_msg": + message = eventData.get("message", {}) + sender_id = message.get("sender") + text = message.get("text", "") + chat_id = eventData.get("chatId", "") + msg_id = message.get("id", 0) + await push_service.send_to_user( + db, target, + sender_id=sender_id, + msg_id=msg_id, + chat_id=chat_id, + text=text, + ) api = { "db": db, diff --git a/src/oneme/processors/main.py b/src/oneme/processors/main.py index 0ac7ba6..d0960de 100644 --- a/src/oneme/processors/main.py +++ b/src/oneme/processors/main.py @@ -130,7 +130,7 @@ class MainProcessors(BaseProcessor): # Отправляем await self._send(writer, response) - async def update_config(self, payload, seq, writer, userPhone): + async def update_config(self, payload, seq, writer, userPhone, hashedToken=None): """ Обработчик 22 опкода (config) Он отвечает за обновление настроек приватности @@ -140,9 +140,14 @@ class MainProcessors(BaseProcessor): # а отдавать его нужно только при изменении настроек приватности result_payload = None - if payload.get("pushToken") and payload.get("pushOptions"): - # TODO: Когда сядем за пуши, сделать тут обновление пуш токена - pass + 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") diff --git a/src/oneme/socket.py b/src/oneme/socket.py index 6afff17..46d9868 100644 --- a/src/oneme/socket.py +++ b/src/oneme/socket.py @@ -280,6 +280,7 @@ class OnemeMobile: seq, writer, userPhone, + hashedToken, ) case _: self.logger.warning(f"Неизвестный опкод {opcode}") diff --git a/src/oneme/websocket.py b/src/oneme/websocket.py index db7da50..c5dd566 100644 --- a/src/oneme/websocket.py +++ b/src/oneme/websocket.py @@ -254,6 +254,7 @@ class OnemeWS: seq, websocket, userPhone, + hashedToken, ) case _: self.logger.warning(f"Неизвестный опкод {opcode}") diff --git a/tables.sql b/tables.sql index 7ec68a3..3185d22 100644 --- a/tables.sql +++ b/tables.sql @@ -23,6 +23,7 @@ CREATE TABLE `tokens` ( `device_name` VARCHAR(256) NOT NULL, `location` VARCHAR(256) NOT NULL, `time` VARCHAR(16) NOT NULL, + `push_token` VARCHAR(512) DEFAULT NULL, PRIMARY KEY (`phone`, `token_hash`) );