MAX: пуши через firebase (особо не тестил, вроде работает)

This commit is contained in:
Alexey Polyakov
2026-04-24 19:46:08 +03:00
parent 35a4101608
commit 56133416e3
11 changed files with 160 additions and 11 deletions

View File

@@ -26,4 +26,5 @@ telegram_bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
telegram_bot_enabled = "1" telegram_bot_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 = ""

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@ __pycache__
.env .env
*.pem *.pem
*.sqlite *.sqlite
*.crt *.crt
*-adminsdk-*.json

View File

@@ -22,3 +22,26 @@
2. Открываем консоль в той же директории и производим декомпиляцию: `apktool d <имя apk> -o max` 2. Открываем консоль в той же директории и производим декомпиляцию: `apktool d <имя apk> -o max`
3. Заходим в папку проекта и заменяем во всех классах "api.oneme.ru" на свой адрес сервера 3. Заходим в папку проекта и заменяем во всех классах "api.oneme.ru" на свой адрес сервера
4. Производим повторную сборку с помощью команды: `apktool b max -o max_modified.apk` 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`

View File

@@ -7,4 +7,5 @@ pydantic
aiosqlite aiosqlite
aiohttp aiohttp
python-dotenv python-dotenv
cryptography cryptography
firebase-admin

View File

@@ -51,3 +51,6 @@ class ServerConfig:
### sms шлюз ### sms шлюз
sms_gateway_url = os.getenv("sms_gateway_url", "") sms_gateway_url = os.getenv("sms_gateway_url", "")
### Firebase
firebase_credentials_path = os.getenv("firebase_credentials_path", "")

92
src/common/push.py Normal file
View File

@@ -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)

View File

@@ -4,6 +4,7 @@ import logging
import ssl import ssl
from common.config import ServerConfig from common.config import ServerConfig
from common.push import PushService
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
@@ -130,14 +131,33 @@ def set_logging():
async def main(): 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() set_logging()
db = await init_db() db = await init_db()
ssl_context = init_ssl() ssl_context = init_ssl()
clients = {} 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 = { api = {
"db": db, "db": db,

View File

@@ -130,7 +130,7 @@ class MainProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, response) 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) Обработчик 22 опкода (config)
Он отвечает за обновление настроек приватности Он отвечает за обновление настроек приватности
@@ -140,9 +140,14 @@ class MainProcessors(BaseProcessor):
# а отдавать его нужно только при изменении настроек приватности # а отдавать его нужно только при изменении настроек приватности
result_payload = None result_payload = None
if payload.get("pushToken") and payload.get("pushOptions"): if payload.get("pushToken"):
# TODO: Когда сядем за пуши, сделать тут обновление пуш токена push_token = payload.get("pushToken")
pass 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"): elif payload.get("settings") and payload.get("settings").get("user"):
"""Обновление настроек приватности""" """Обновление настроек приватности"""
new_settings = payload.get("settings").get("user") new_settings = payload.get("settings").get("user")

View File

@@ -280,6 +280,7 @@ class OnemeMobile:
seq, seq,
writer, writer,
userPhone, userPhone,
hashedToken,
) )
case _: case _:
self.logger.warning(f"Неизвестный опкод {opcode}") self.logger.warning(f"Неизвестный опкод {opcode}")

View File

@@ -254,6 +254,7 @@ class OnemeWS:
seq, seq,
websocket, websocket,
userPhone, userPhone,
hashedToken,
) )
case _: case _:
self.logger.warning(f"Неизвестный опкод {opcode}") self.logger.warning(f"Неизвестный опкод {opcode}")

View File

@@ -23,6 +23,7 @@ CREATE TABLE `tokens` (
`device_name` VARCHAR(256) NOT NULL, `device_name` VARCHAR(256) NOT NULL,
`location` VARCHAR(256) NOT NULL, `location` VARCHAR(256) NOT NULL,
`time` VARCHAR(16) NOT NULL, `time` VARCHAR(16) NOT NULL,
`push_token` VARCHAR(512) DEFAULT NULL,
PRIMARY KEY (`phone`, `token_hash`) PRIMARY KEY (`phone`, `token_hash`)
); );