Compare commits

..

5 Commits

Author SHA1 Message Date
Alexey Polyakov
810d480dbd MAX: фикс версий < 25.8.0 2026-04-24 21:30:02 +03:00
Alexey Polyakov
227f90c3c3 MAX: Рефактор папок 2026-04-24 20:54:28 +03:00
Alexey Polyakov
56133416e3 MAX: пуши через firebase (особо не тестил, вроде работает) 2026-04-24 19:46:08 +03:00
Alexey Polyakov
35a4101608 MAX: обновление настроек приватности 2026-04-24 17:17:33 +03:00
Alexey Polyakov
9fcba1af86 MAX: Рабочие баннеры 2026-04-24 15:51:01 +03:00
18 changed files with 419 additions and 72 deletions

View File

@@ -27,3 +27,4 @@ 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"
firebase_credentials_path = ""

1
.gitignore vendored
View File

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

View File

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

View File

@@ -8,3 +8,4 @@ aiosqlite
aiohttp
python-dotenv
cryptography
firebase-admin

View File

@@ -51,3 +51,6 @@ class ServerConfig:
### sms шлюз
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

@@ -13,6 +13,12 @@ class SQLQueries:
INSERT_USER_DATA = """
INSERT INTO user_data
(phone, folders, user_config, chat_config)
VALUES (%s, %s, %s, %s)
(phone, user_config, chat_config)
VALUES (%s, %s, %s)
"""
INSERT_DEFAULT_FOLDER = """
INSERT INTO user_folders
(id, phone, title, sort_order)
VALUES ('all.chat.folder', %s, 'Все', 0)
"""

View File

@@ -196,25 +196,6 @@ class Static:
]},
]
### Заглушка для папок
ALL_CHAT_FOLDER = [{
"id": "all.chat.folder",
"title": "Все",
"filters": [],
"updateTime": 0,
"options": [],
"sourceId": 1
}]
ALL_CHAT_FOLDER_ORDER = ["all.chat.folder"]
### Стандартные папки с настройками пользователя
USER_FOLDERS = {
"folders": [],
"foldersOrder": [],
"allFilterExcludeFolders": []
}
USER_SETTINGS = {
"CHATS_PUSH_NOTIFICATION": "ON",
"PUSH_DETAILS": True,

View File

@@ -503,3 +503,17 @@ class Tools:
# Возвращаем
return unique_id
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

View File

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

View File

@@ -16,7 +16,6 @@ from oneme.models import (
VerifyCodePayloadModel,
)
class AuthProcessors(BaseProcessor):
def __init__(
self,
@@ -30,17 +29,60 @@ class AuthProcessors(BaseProcessor):
self.server_config = OnemeConfig().SERVER_CONFIG
self.telegram_bot = telegram_bot
def _check_legacy_version(self, app_version):
"""
Функция определения легаси версий клиентов,
для которых потребуются некоторые корректировки ответов сервера
Сейчас данная функция используется для форматирования ответов
под версии ниже 25.8.0
Функция вернет True, если версия слишком старая,
или False в противном случае
[fun fact] С 25.8.0, похоже, начали корректировать протокол, поскольку это
самая старая версия, которая работала без всяких корректировок сервера
"""
return tuple(int(v) for v in app_version.split(".")) < (25, 8, 0)
async def _send_banners(self, writer):
"""Функция отправки баннеров клиенту"""
# Итоговый список баннеров для отдачи клиенту
banners = []
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Собираем все баннеры, которые есть в бд
await cursor.execute(
"SELECT * FROM banners WHERE enabled = TRUE"
)
rows = await cursor.fetchall()
# Добавляем каждый баннер в лист
for row in rows:
banner = {
"description": row.get("description"),
"title": row.get("title"),
"priority": row.get("priority"),
"type": row.get("type"),
"hideCloseButton": bool(row.get("hide_close_button")),
"rerun": row.get("rerun"),
"url": row.get("url"),
"animojiId": row.get("animoji_id"),
"repeat": row.get("repeat"),
"hideOnClick": bool(row.get("hide_on_click")),
"id": row.get("id"),
"isTitleAnimated": bool(row.get("is_title_animated")),
}
banners.append(banner)
# Собираем данные пакета
payload = {
"showTime": 86400000, # Сколько будет показываться баннер, тут сутки в миллисекундах
# можно в будущем переделать, и сделать выбор в конфигурации
# думаю, было бы прикольно
"showTime": 86400000,
"updateTime": int(time.time() * 1000),
"banners": [
# TODO: разобраться как работают баннеры и их реализовать
# думаю админам инстансов было бы прикольно, и нам
]
"banners": banners
}
# Собираем пакет
@@ -155,7 +197,7 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
async def auth(self, payload, seq, writer, deviceType, deviceName):
async def auth(self, payload, seq, writer, deviceType, deviceName, appVersion):
"""Обработчик проверки кода"""
try:
VerifyCodePayloadModel.model_validate(payload)
@@ -258,6 +300,11 @@ class AuthProcessors(BaseProcessor):
None if not account.get("description") else account.get("description")
)
if self._check_legacy_version(appVersion):
include_profile_options = False
else:
include_profile_options = True
# Собираем данные пакета
payload = {
"tokenAttrs": {"LOGIN": {"token": login}},
@@ -273,7 +320,7 @@ class AuthProcessors(BaseProcessor):
description=description,
accountStatus=int(account.get("accountstatus")),
profileOptions=json.loads(account.get("profileoptions")),
includeProfileOptions=True,
includeProfileOptions=include_profile_options,
username=account.get("username"),
),
}
@@ -286,7 +333,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем
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, appVersion):
"""Обработчик подтверждения регистрации нового пользователя"""
# Валидируем данные пакета
try:
@@ -377,17 +424,26 @@ class AuthProcessors(BaseProcessor):
await cursor.execute(
"""
INSERT INTO user_data
(phone, folders, user_config, chat_config)
VALUES (%s, %s, %s, %s)
(phone, user_config, chat_config)
VALUES (%s, %s, %s)
""",
(
phone,
json.dumps(self.static.USER_FOLDERS),
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,)
@@ -406,6 +462,11 @@ class AuthProcessors(BaseProcessor):
),
)
if self._check_legacy_version(appVersion):
include_profile_options = False
else:
include_profile_options = True
# Генерируем профиль
profile = self.tools.generate_profile(
id=user_id,
@@ -419,7 +480,7 @@ class AuthProcessors(BaseProcessor):
description=None,
accountStatus=0,
profileOptions=[],
includeProfileOptions=True,
includeProfileOptions=include_profile_options,
username=None,
)
@@ -445,7 +506,7 @@ class AuthProcessors(BaseProcessor):
f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={first_name} {last_name}"
)
async def login(self, payload, seq, writer):
async def login(self, payload, seq, writer, appVersion):
"""Обработчик авторизации клиента на сервере"""
# Валидируем данные пакета
try:
@@ -504,11 +565,22 @@ class AuthProcessors(BaseProcessor):
for chat in user_chats:
chats.append(chat.get("chat_id"))
# Обновляем юзер конфиг
updated_user_config = await self.tools.update_user_config(
cursor, token_data.get("phone"),
user_data.get("user_config"), self.static.USER_SETTINGS
)
# Аватарка с биографией
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
description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion):
include_profile_options = False
else:
include_profile_options = True
# Генерируем профиль
profile = self.tools.generate_profile(
id=user.get("id"),
@@ -522,7 +594,7 @@ class AuthProcessors(BaseProcessor):
description=description,
accountStatus=int(user.get("accountstatus")),
profileOptions=json.loads(user.get("profileoptions")),
includeProfileOptions=True,
includeProfileOptions=include_profile_options,
username=user.get("username"),
)
@@ -546,7 +618,7 @@ class AuthProcessors(BaseProcessor):
"presence": {},
"config": {
"server": self.server_config,
"user": json.loads(user_data.get("user_config")),
"user": updated_user_config,
},
"token": token,
"videoChatHistory": False,
@@ -558,10 +630,6 @@ class AuthProcessors(BaseProcessor):
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.LOGIN, payload=payload
)
# print(
# json.dumps(payload, indent=4)
# )
# Отправляем
await self._send(writer, packet)

View File

@@ -18,16 +18,31 @@ class FoldersProcessors(BaseProcessor):
# Ищем папки в бд
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT folders FROM user_data WHERE phone = %s", (int(senderPhone),))
result_folders = await cursor.fetchone()
user_folders = json.loads(result_folders.get("folders"))
await cursor.execute(
"SELECT id, title, filters, 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"]),
"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": self.static.ALL_CHAT_FOLDER + user_folders.get("folders"),
"foldersOrder": self.static.ALL_CHAT_FOLDER_ORDER + user_folders.get("foldersOrder"),
"allFilterExcludeFolders": user_folders.get("allFilterExcludeFolders")
"folders": folders,
"foldersOrder": [folder["id"] for folder in result_folders],
"allFilterExcludeFolders": []
}
# Собираем пакет

View File

@@ -20,11 +20,13 @@ class MainProcessors(BaseProcessor):
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.SESSION_INIT, self.error_types.INVALID_PAYLOAD, writer)
return None, None
return None, None, None
# Получаем данные из пакета
deviceType = payload.get("userAgent").get("deviceType")
deviceName = payload.get("userAgent").get("deviceName")
userAgent = payload.get("userAgent")
deviceType = userAgent.get("deviceType")
deviceName = userAgent.get("deviceName")
appVersion = userAgent.get("appVersion")
# Данные пакета
payload = {
@@ -43,7 +45,7 @@ class MainProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
return deviceType, deviceName
return deviceType, deviceName, appVersion
async def ping(self, payload, seq, writer):
"""Обработчик пинга"""
@@ -129,3 +131,60 @@ class MainProcessors(BaseProcessor):
# Отправляем
await self._send(writer, response)
async def update_config(self, payload, seq, writer, userPhone, hashedToken=None):
"""
Обработчик 22 опкода (config)
Он отвечает за обновление настроек приватности
и пуш токена для пушей
"""
# Пейлоад, который отдадим клиенту
# а отдавать его нужно только при изменении настроек приватности
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)

View File

@@ -45,6 +45,7 @@ class OnemeMobile:
deviceType = None
deviceName = None
appVersion = None
userPhone = None
userId = None
@@ -89,7 +90,7 @@ class OnemeMobile:
match opcode:
case self.opcodes.SESSION_INIT:
deviceType, deviceName = await self.processors.session_init(
deviceType, deviceName, appVersion = await self.processors.session_init(
payload, seq, writer
)
case self.opcodes.AUTH_REQUEST:
@@ -112,7 +113,7 @@ class OnemeMobile:
)
else:
await self.processors.auth(
payload, seq, writer, deviceType, deviceName
payload, seq, writer, deviceType, deviceName, appVersion
)
case self.opcodes.AUTH_CONFIRM:
if not self.auth_rate_limiter.is_allowed(address[0]):
@@ -124,7 +125,7 @@ class OnemeMobile:
)
elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm(
payload, seq, writer, deviceType, deviceName
payload, seq, writer, deviceType, deviceName, appVersion
)
else:
self.logger.warning(
@@ -143,7 +144,7 @@ class OnemeMobile:
userPhone,
userId,
hashedToken,
) = await self.processors.login(payload, seq, writer)
) = await self.processors.login(payload, seq, writer, appVersion)
if userPhone:
await self._finish_auth(
@@ -272,6 +273,16 @@ class OnemeMobile:
seq,
writer,
)
case self.opcodes.CONFIG:
await self.auth_required(
userPhone,
self.processors.update_config,
payload,
seq,
writer,
userPhone,
hashedToken,
)
case _:
self.logger.warning(f"Неизвестный опкод {opcode}")
except Exception as e:

View File

@@ -37,6 +37,7 @@ class OnemeWS:
deviceType = None
deviceName = None
appVersion = None
userPhone = None
userId = None
@@ -63,7 +64,7 @@ class OnemeWS:
match opcode:
case self.opcodes.SESSION_INIT:
deviceType, deviceName = await self.processors.session_init(
deviceType, deviceName, appVersion = await self.processors.session_init(
payload, seq, websocket
)
case self.opcodes.AUTH_REQUEST:
@@ -86,7 +87,7 @@ class OnemeWS:
)
else:
await self.processors.auth(
payload, seq, websocket, deviceType, deviceName
payload, seq, websocket, deviceType, deviceName, appVersion
)
case self.opcodes.AUTH_CONFIRM:
if not self.auth_rate_limiter.is_allowed(address[0]):
@@ -98,7 +99,7 @@ class OnemeWS:
)
elif payload and payload.get("tokenType") == "REGISTER":
await self.processors.auth_confirm(
payload, seq, websocket, deviceType, deviceName
payload, seq, websocket, deviceType, deviceName, appVersion
)
else:
self.logger.warning(
@@ -117,7 +118,7 @@ class OnemeWS:
userPhone,
userId,
hashedToken,
) = await self.processors.login(payload, seq, websocket)
) = await self.processors.login(payload, seq, websocket, deviceType, appVersion)
if userPhone:
await self._finish_auth(
@@ -246,6 +247,16 @@ class OnemeWS:
seq,
websocket,
)
case self.opcodes.CONFIG:
await self.auth_required(
userPhone,
self.processors.update_config,
payload,
seq,
websocket,
userPhone,
hashedToken,
)
case _:
self.logger.warning(f"Неизвестный опкод {opcode}")
except websockets.exceptions.ConnectionClosed:

View File

@@ -300,6 +300,12 @@ class AuthProcessors(BaseProcessor):
chat.get("chat_id")
)
# Обновляем юзер конфиг
updated_user_config = await self.tools.update_user_config(
cursor, token_data.get("phone"),
user_data.get("user_config"), self.static.USER_SETTINGS
)
# Аватарка с биографией
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)
@@ -335,7 +341,7 @@ class AuthProcessors(BaseProcessor):
"config": {
"hash": "e5903aa8-0000000000000000-80000106-0000000000000001-00000001-0000000000000000-00000000-2-00000001-0000019c9559d057",
"server": self.server_config,
"user": json.loads(user_data.get("user_config")),
"user": updated_user_config,
"chatFolders": {
"FOLDERS": [],
"ALL_FILTER_EXCLUDE": []

View File

@@ -105,12 +105,17 @@ class TelegramBot:
self.sql_queries.INSERT_USER_DATA,
(
new_phone, # phone
json.dumps(self.static.USER_FOLDERS), # folders
json.dumps(self.static.USER_SETTINGS), # user settings
json.dumps({}), # chat_config
),
)
# Добавляем дефолтную папку
await cursor.execute(
self.sql_queries.INSERT_DEFAULT_FOLDER,
(new_phone,),
)
await message.answer(
self.get_bot_message(
self.msg_types.REGISTRATION_SUCCESS

View File

@@ -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`)
);
@@ -37,7 +38,6 @@ CREATE TABLE `auth_tokens` (
CREATE TABLE `user_data` (
`phone` VARCHAR(20) NOT NULL UNIQUE,
`folders` JSON NOT NULL,
`user_config` JSON NOT NULL,
`chat_config` JSON NOT NULL,
PRIMARY KEY (`phone`)
@@ -78,3 +78,33 @@ CREATE TABLE `contacts` (
`is_blocked` BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (`owner_id`, `contact_id`)
);
CREATE TABLE `banners` (
`id` VARCHAR(64) NOT NULL,
`title` VARCHAR(256) NOT NULL,
`description` VARCHAR(512) NOT NULL,
`url` VARCHAR(512) NOT NULL,
`type` INT NOT NULL DEFAULT 1,
`priority` INT NOT NULL DEFAULT 0,
`animoji_id` INT NOT NULL DEFAULT 0,
`repeat` INT NOT NULL DEFAULT 1,
`rerun` BIGINT NOT NULL DEFAULT 0,
`hide_close_button` BOOLEAN NOT NULL DEFAULT FALSE,
`hide_on_click` BOOLEAN NOT NULL DEFAULT FALSE,
`is_title_animated` BOOLEAN NOT NULL DEFAULT FALSE,
`enabled` BOOLEAN NOT NULL DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
CREATE TABLE `user_folders` (
`id` VARCHAR(64) NOT NULL,
`phone` VARCHAR(20) NOT NULL,
`title` VARCHAR(128) NOT NULL,
`filters` JSON NOT NULL DEFAULT ('[]'),
`options` JSON NOT NULL DEFAULT ('[]'),
`source_id` INT NOT NULL DEFAULT 1,
`update_time` BIGINT NOT NULL DEFAULT 0,
`sort_order` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`, `phone`)
);