mirror of
https://github.com/openmax-server/server.git
synced 2026-05-22 19:41:41 +03:00
MAX: заглушка для баннеров, правка пакета со списком жалоб, отдача контактов и прочие улучшения
This commit is contained in:
@@ -116,15 +116,84 @@ class Static:
|
||||
|
||||
### Причины для жалоб
|
||||
COMPLAIN_REASONS = [
|
||||
"Порнография или эротика",
|
||||
"Экстремизм или терроризм",
|
||||
"Фейк",
|
||||
"Мошенничество",
|
||||
"Нарушение авторского права",
|
||||
"Шокирующий контент",
|
||||
"Персональные данные",
|
||||
"Незаконная услуга",
|
||||
"Это законно, но надо удалить"
|
||||
{"typeId": 5, "reasons": [
|
||||
{"reasonTitle": "Мошенничество", "reasonId": 8},
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Порнографический контент", "reasonId": 23},
|
||||
{"reasonTitle": "Насилие", "reasonId": 18},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Экстремизм", "reasonId": 20},
|
||||
{"reasonTitle": "Запрещенные товары", "reasonId": 21},
|
||||
{"reasonTitle": "Мне не нравится", "reasonId": 22},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
{"typeId": 4, "reasons": [
|
||||
{"reasonTitle": "Мошенничество", "reasonId": 8},
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Порнографический контент", "reasonId": 23},
|
||||
{"reasonTitle": "Насилие", "reasonId": 18},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Экстремизм", "reasonId": 20},
|
||||
{"reasonTitle": "Запрещенные товары", "reasonId": 21},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
{"typeId": 3, "reasons": [
|
||||
{"reasonTitle": "Мошенничество", "reasonId": 8},
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Порнографический контент", "reasonId": 23},
|
||||
{"reasonTitle": "Насилие", "reasonId": 18},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Экстремизм", "reasonId": 20},
|
||||
{"reasonTitle": "Запрещенные товары", "reasonId": 21},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
{"typeId": 7, "reasons": [
|
||||
{"reasonTitle": "Мошенничество", "reasonId": 8},
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Порнографический контент", "reasonId": 23},
|
||||
{"reasonTitle": "Насилие", "reasonId": 18},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Экстремизм", "reasonId": 20},
|
||||
{"reasonTitle": "Запрещенные товары", "reasonId": 21},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
{"typeId": 8, "reasons": [
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Шантаж", "reasonId": 10},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
{"typeId": 2, "reasons": [
|
||||
{"reasonTitle": "Мошенничество", "reasonId": 8},
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Порнографический контент", "reasonId": 23},
|
||||
{"reasonTitle": "Насилие", "reasonId": 18},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Экстремизм", "reasonId": 20},
|
||||
{"reasonTitle": "Запрещенные товары", "reasonId": 21},
|
||||
{"reasonTitle": "Мне не нравится", "reasonId": 22},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
{"typeId": 6, "reasons": [
|
||||
{"reasonTitle": "Мошенничество", "reasonId": 8},
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Порнографический контент", "reasonId": 23},
|
||||
{"reasonTitle": "Насилие", "reasonId": 18},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Экстремизм", "reasonId": 20},
|
||||
{"reasonTitle": "Запрещенные товары", "reasonId": 21},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
{"typeId": 1, "reasons": [
|
||||
{"reasonTitle": "Мошенничество", "reasonId": 8},
|
||||
{"reasonTitle": "Спам", "reasonId": 9},
|
||||
{"reasonTitle": "Порнографический контент", "reasonId": 23},
|
||||
{"reasonTitle": "Насилие", "reasonId": 18},
|
||||
{"reasonTitle": "Оскорбления", "reasonId": 11},
|
||||
{"reasonTitle": "Экстремизм", "reasonId": 20},
|
||||
{"reasonTitle": "Запрещенные товары", "reasonId": 21},
|
||||
{"reasonTitle": "Другое", "reasonId": 7},
|
||||
]},
|
||||
]
|
||||
|
||||
### Заглушка для папок
|
||||
|
||||
@@ -23,6 +23,12 @@ class Tools:
|
||||
profileOptions=[],
|
||||
includeProfileOptions=True,
|
||||
username=None,
|
||||
|
||||
# для контактов, собственно
|
||||
custom_firstname=None,
|
||||
custom_lastname=None,
|
||||
|
||||
blocked=False
|
||||
):
|
||||
contact = {
|
||||
"id": id,
|
||||
@@ -51,6 +57,19 @@ class Tools:
|
||||
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:
|
||||
@@ -216,6 +235,124 @@ class Tools:
|
||||
|
||||
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",
|
||||
(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
|
||||
):
|
||||
|
||||
@@ -18,6 +18,10 @@ class OnemeController(ControllerBase):
|
||||
eventType = eventData.get("eventType")
|
||||
writer = client.get("writer")
|
||||
|
||||
# Не отправляем событие самому себе
|
||||
if writer == eventData.get("writer"):
|
||||
return
|
||||
|
||||
# Обрабатываем событие
|
||||
if eventType == "new_msg":
|
||||
# Данные сообщения
|
||||
@@ -72,9 +76,8 @@ class OnemeController(ControllerBase):
|
||||
)
|
||||
|
||||
# Отправляем пакет
|
||||
if writer != eventData.get("writer"):
|
||||
writer.write(packet)
|
||||
await writer.drain()
|
||||
writer.write(packet)
|
||||
await writer.drain()
|
||||
|
||||
def launch(self, api):
|
||||
async def _start_all():
|
||||
|
||||
@@ -30,6 +30,27 @@ class AuthProcessors(BaseProcessor):
|
||||
self.server_config = OnemeConfig().SERVER_CONFIG
|
||||
self.telegram_bot = telegram_bot
|
||||
|
||||
async def _send_banners(self, writer):
|
||||
"""Функция отправки баннеров клиенту"""
|
||||
payload = {
|
||||
"showTime": 86400000, # Сколько будет показываться баннер, тут сутки в миллисекундах
|
||||
# можно в будущем переделать, и сделать выбор в конфигурации
|
||||
# думаю, было бы прикольно
|
||||
"updateTime": int(time.time() * 1000),
|
||||
"banners": [
|
||||
# TODO: разобраться как работают баннеры и их реализовать
|
||||
# думаю админам инстансов было бы прикольно, и нам
|
||||
]
|
||||
}
|
||||
|
||||
# Собираем пакет
|
||||
packet = self.proto.pack_packet(
|
||||
cmd=0, opcode=self.opcodes.NOTIF_BANNERS, payload=payload
|
||||
)
|
||||
|
||||
# Отправляет
|
||||
await self._send(writer, packet)
|
||||
|
||||
async def auth_request(self, payload, seq, writer):
|
||||
"""Обработчик запроса кода"""
|
||||
try:
|
||||
@@ -356,12 +377,11 @@ class AuthProcessors(BaseProcessor):
|
||||
await cursor.execute(
|
||||
"""
|
||||
INSERT INTO user_data
|
||||
(phone, contacts, folders, user_config, chat_config)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
(phone, folders, user_config, chat_config)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
phone,
|
||||
json.dumps([]),
|
||||
json.dumps(self.static.USER_FOLDERS),
|
||||
json.dumps(self.static.USER_SETTINGS),
|
||||
json.dumps({}),
|
||||
@@ -506,17 +526,23 @@ class AuthProcessors(BaseProcessor):
|
||||
username=user.get("username"),
|
||||
)
|
||||
|
||||
# Генерируем список чатов
|
||||
chats = await self.tools.generate_chats(
|
||||
chats, self.db_pool, user.get("id"), protocol_type=self.type
|
||||
)
|
||||
|
||||
# Генерируем список контактов
|
||||
contacts = await self.tools.collect_user_contacts(
|
||||
user.get("id"), self.db_pool, self.config.avatar_base_url
|
||||
)
|
||||
|
||||
# Формируем данные пакета
|
||||
payload = {
|
||||
"profile": profile,
|
||||
"chats": chats,
|
||||
"chatMarker": 0,
|
||||
"messages": {},
|
||||
"contacts": [],
|
||||
"contacts": contacts,
|
||||
"presence": {},
|
||||
"config": {
|
||||
"server": self.server_config,
|
||||
@@ -532,8 +558,16 @@ 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)
|
||||
|
||||
# Отправляем баннеры
|
||||
await self._send_banners(writer)
|
||||
|
||||
return int(user.get("phone")), int(user.get("id")), hashed_token
|
||||
|
||||
async def logout(self, seq, writer, hashedToken):
|
||||
|
||||
@@ -67,7 +67,8 @@ class HistoryProcessors(BaseProcessor):
|
||||
"text": row.get("text"),
|
||||
"attaches": json.loads(row.get("attaches")),
|
||||
"elements": json.loads(row.get("elements")),
|
||||
"reactionInfo": {}
|
||||
"reactionInfo": {},
|
||||
"options": 1,
|
||||
})
|
||||
|
||||
if forward > 0:
|
||||
|
||||
@@ -49,7 +49,8 @@ class MessagesProcessors(BaseProcessor):
|
||||
"eventType": "typing",
|
||||
"chatId": chatId,
|
||||
"type": type,
|
||||
"userId": senderId
|
||||
"userId": senderId,
|
||||
"writer": writer,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from oneme.models import (
|
||||
|
||||
|
||||
class SearchProcessors(BaseProcessor):
|
||||
async def contact_info(self, payload, seq, writer):
|
||||
async def contact_info(self, payload, seq, writer, senderId):
|
||||
"""Поиск пользователей по ID"""
|
||||
# Валидируем данные пакета
|
||||
try:
|
||||
@@ -39,6 +39,16 @@ class SearchProcessors(BaseProcessor):
|
||||
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
|
||||
description = None if not user.get("description") else user.get("description")
|
||||
|
||||
# Получаем данные контакта
|
||||
await cursor.execute(
|
||||
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
|
||||
(senderId, contactId),
|
||||
)
|
||||
contact_row = await cursor.fetchone()
|
||||
custom_firstname = contact_row.get("custom_firstname") if contact_row else None
|
||||
custom_lastname = contact_row.get("custom_lastname") if contact_row else None
|
||||
blocked = bool(contact_row.get("is_blocked")) if contact_row else False
|
||||
|
||||
# Генерируем профиль
|
||||
users.append(
|
||||
self.tools.generate_profile(
|
||||
@@ -54,7 +64,10 @@ class SearchProcessors(BaseProcessor):
|
||||
accountStatus=int(user.get("accountstatus")),
|
||||
profileOptions=json.loads(user.get("profileoptions")),
|
||||
includeProfileOptions=False,
|
||||
username=user.get("username")
|
||||
username=user.get("username"),
|
||||
custom_firstname=custom_firstname,
|
||||
custom_lastname=custom_lastname,
|
||||
blocked=blocked,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -119,6 +132,18 @@ class SearchProcessors(BaseProcessor):
|
||||
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
|
||||
description = None if not user.get("description") else user.get("description")
|
||||
|
||||
# Получаем данные контакта
|
||||
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",
|
||||
(senderId, user.get("id")),
|
||||
)
|
||||
contact_row = await cursor.fetchone()
|
||||
custom_firstname = contact_row.get("custom_firstname") if contact_row else None
|
||||
custom_lastname = contact_row.get("custom_lastname") if contact_row else None
|
||||
blocked = bool(contact_row.get("is_blocked")) if contact_row else False
|
||||
|
||||
# Генерируем профиль
|
||||
profile = self.tools.generate_profile(
|
||||
id=user.get("id"),
|
||||
@@ -133,7 +158,10 @@ class SearchProcessors(BaseProcessor):
|
||||
accountStatus=int(user.get("accountstatus")),
|
||||
profileOptions=json.loads(user.get("profileoptions")),
|
||||
includeProfileOptions=False,
|
||||
username=user.get("username")
|
||||
username=user.get("username"),
|
||||
custom_firstname=custom_firstname,
|
||||
custom_lastname=custom_lastname,
|
||||
blocked=blocked,
|
||||
)
|
||||
|
||||
# Создаем данные пакета
|
||||
|
||||
@@ -250,6 +250,7 @@ class OnemeMobile:
|
||||
payload,
|
||||
seq,
|
||||
writer,
|
||||
userId,
|
||||
)
|
||||
case self.opcodes.COMPLAIN_REASONS_GET:
|
||||
await self.auth_required(
|
||||
|
||||
@@ -224,6 +224,7 @@ class OnemeWS:
|
||||
payload,
|
||||
seq,
|
||||
websocket,
|
||||
userId,
|
||||
)
|
||||
case self.opcodes.COMPLAIN_REASONS_GET:
|
||||
await self.auth_required(
|
||||
|
||||
36
tables.sql
36
tables.sql
@@ -1,5 +1,5 @@
|
||||
CREATE TABLE `users` (
|
||||
`id` INT PRIMARY KEY,
|
||||
`id` INT NOT NULL,
|
||||
`phone` VARCHAR(20) UNIQUE,
|
||||
`telegram_id` VARCHAR(64) UNIQUE,
|
||||
`firstname` VARCHAR(59) NOT NULL,
|
||||
@@ -12,7 +12,8 @@ CREATE TABLE `users` (
|
||||
`options` JSON NOT NULL,
|
||||
`accountstatus` VARCHAR(16) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`username` VARCHAR(60) UNIQUE
|
||||
`username` VARCHAR(60) UNIQUE,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE `tokens` (
|
||||
@@ -21,7 +22,8 @@ CREATE TABLE `tokens` (
|
||||
`device_type` VARCHAR(256) NOT NULL,
|
||||
`device_name` VARCHAR(256) NOT NULL,
|
||||
`location` VARCHAR(256) NOT NULL,
|
||||
`time` VARCHAR(16) NOT NULL
|
||||
`time` VARCHAR(16) NOT NULL,
|
||||
PRIMARY KEY (`phone`, `token_hash`)
|
||||
);
|
||||
|
||||
CREATE TABLE `auth_tokens` (
|
||||
@@ -29,25 +31,27 @@ CREATE TABLE `auth_tokens` (
|
||||
`token_hash` VARCHAR(64) NOT NULL,
|
||||
`code_hash` VARCHAR(64) NOT NULL,
|
||||
`expires` VARCHAR(16) NOT NULL,
|
||||
`state` VARCHAR(16)
|
||||
`state` VARCHAR(16),
|
||||
PRIMARY KEY (`phone`, `token_hash`)
|
||||
);
|
||||
|
||||
CREATE TABLE `user_data` (
|
||||
`phone` VARCHAR(20) NOT NULL UNIQUE PRIMARY KEY,
|
||||
`contacts` JSON NOT NULL,
|
||||
`phone` VARCHAR(20) NOT NULL UNIQUE,
|
||||
`folders` JSON NOT NULL,
|
||||
`user_config` JSON NOT NULL,
|
||||
`chat_config` JSON NOT NULL
|
||||
`chat_config` JSON NOT NULL,
|
||||
PRIMARY KEY (`phone`)
|
||||
);
|
||||
|
||||
CREATE TABLE `chats` (
|
||||
`id` INT NOT NULL PRIMARY KEY,
|
||||
`id` INT NOT NULL,
|
||||
`owner` INT NOT NULL,
|
||||
`type` VARCHAR(16) NOT NULL
|
||||
`type` VARCHAR(16) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE `messages` (
|
||||
`id` INT NOT NULL PRIMARY KEY,
|
||||
`id` INT NOT NULL,
|
||||
`chat_id` INT NOT NULL,
|
||||
`sender` INT NOT NULL,
|
||||
`time` VARCHAR(32) NOT NULL,
|
||||
@@ -55,7 +59,8 @@ CREATE TABLE `messages` (
|
||||
`attaches` JSON NOT NULL,
|
||||
`cid` VARCHAR(32) NOT NULL,
|
||||
`elements` JSON NOT NULL,
|
||||
`type` VARCHAR(16) NOT NULL
|
||||
`type` VARCHAR(16) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE `chat_participants` (
|
||||
@@ -64,3 +69,12 @@ CREATE TABLE `chat_participants` (
|
||||
`joined_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`chat_id`, `user_id`)
|
||||
);
|
||||
|
||||
CREATE TABLE `contacts` (
|
||||
`owner_id` INT NOT NULL,
|
||||
`contact_id` INT NOT NULL,
|
||||
`custom_firstname` VARCHAR(59),
|
||||
`custom_lastname` VARCHAR(59),
|
||||
`is_blocked` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY (`owner_id`, `contact_id`)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user