«phpClub» — архив тем ("тредов"), посвящённых изучению PHP и веб-технологий.
someApprentice 2019/04/25 07:59:58  №1388324 1
Ответы: >>1388926
Аноним 2019/04/26 02:38:14  №1388926 2
>>1388324

> Однако, как бы подход с массивами не казался элегантным, у него есть один недостаток - для всех ORM можно переопределить условие для взаимоотношений между таблицами (прим. One-To-Many, Many-to-Many) для сущностей. В SQLAlchemy, похоже, такая возможность есть https://docs.sqlalchemy.org/en/13/orm/join_conditions.html#specifying-alternate-join-conditions , а в Доктрине я такой возможности не нашел. Она есть?

Я думаю, что нету. Судя по https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/annotations-reference.html#annref_joincolumn там связь делается только через колонку с внешним ключом и никак иначе.

Я еще должен предупредить, что, может быть, где-то придется делать SQL-запросы напрямую, если это будет давать значительную разницу в времени выполнения запроса.

> // Получить все сообщения получателя

Вот такие запросы лучше даже не планировать. Сообщений может быть много, и все сразу получать невыгодно. Обычно сообщения всегда запрашивают какими-то кусочками, а когда пользователь прокручивает страницу, подгружают еще.

>>1382907

> Значит, нужно на серверной стороне написать такой клиент, который будет подписываться на все сообщения, валидировать их, сохранять в БД, а затем только перенаправлять клиенту пользователя.

Обычно WAMP и websocket используют как PubSub-брокер (про PubSub: http://design-pattern.ru/patterns/pubsub.html ). PubSub - это технология, когда есть программа-брокер, и к ней могут подсоединяться издатели и подписчики. Подписчики подключаются к брокеру и подписываются на интересующие их каналы или "топики". Издатели подключаются к брокеру и шлют сообщения в выбранные ими каналы. И брокер пересылает сообщения только тем подписчикам, кто подписан на канал. Список каналов нигде не хранится, это просто строка, которая позволяет отобрать получателей сообщения. Каналы/топики нужны для маршрутизации сообщений, чтобы клиенты получали только нужные им события, а не все подряд, что происходит в системе.

WAMP-сервер (программа, которая принимает вебсокет соединения на стороне сервера) обычно и служит брокером PubSub.

Это напоминает очередь сообщений вроде RabbitMQ, только без гарантий сохранности и доставки сообщений. Сообщения не сохраняются, как в очереди, в ожидании получателя, а передаются только тем, кто в данный момент подключен к брокеру. Если никто не подписан на канал, то сообщение от издателя просто отбрасывается.

Пример. Клиент (браузер) подсоединяется к WAMP-серверу. При этом код в браузере устанавливает постоянное веб-сокет соединение с WAMP-сервером, автоматически переподсоединяясь при обрыве. Используя функционал PubSub, он подписывается на канал msg-for-1234, где 1234 - это id залогиненного пользователя.

Когда кто-то шлет пользователю 1234 сообщение, серверный скрипт подсоединяется к WAMP-серверу и публикует это сообщение в канал msg-for-1234. WAMP-сервер передает это сообщение всем подписанным на канал msg-for-1234 клиентам. В данном случае это один браузер. Ему отправляется сообщение по вебсокет-соединению, и после получения браузер генерирует событие, и вызывается какой-то коллбек в JS коде. Таким образом JS код узнает о приходе нового сообщения, и отображает его на экране.

При этом обычно используются и обычные аякс-запросы, и вебсокет. Когда пользователь запускает клиент, тот получает список последних сообщений обычным АЯКС-запросом, а вебсокет используется только для получения новых сообщений с момента запуска клиента. Еще, иногда вебсокет используется только для уведомления о новых сообщениях, а само сообщение запрашивается АЯКС-запросом. Также, можно не полагаться только на вебсокет, а дополнительно раз в N минут делать классический АЯКС-запрос - на случай, если сообщение где-то потерялось, или на случай, если у нас был разрыв вебсокет-соединения и в этот момент пришло сообщение (так как WAMP вроде бы не гарантирует их сохранность и работает по принципу best effort).

Еще, тут надо помнить о безопасности: подключиться к каналу msg-for-1234 должен иметь возможность только пользователь 1234, а не кто угодно. То есть нужна какая-то авторизация на WAMP-сервере.

Так как pubSub - ненадежный механизм, использовать его для отправки сообщений с клиента не стоит. Тебе надо рассмотреть другие варианты:

- AJAX
- RPC в WAMP

RPC - это "удаленный вызов процедур". Ты с клиента делаешь вызов какой-то функции на сервере, передаешь ей параметры, а в ответ получаешь либо результат, либо ошибку. Это наверно подойдет для твоей задачи.

Плюс RPC в сравнении с аяксом - нет накладных расходов на создание соединения и на оборачивание данных в HTTP-запрос, на передачу HTTP-заголовков. Плюс аякса - больше старых браузеров его поддерживают.

Опять же, не забудь про авторизацию. А то кто угодно сможет отправлять сообщения от имени пользователя.

Вот пример вызова RPC из документации autobahn-js:

// 4) call a remote procedure
session.call('com.myapp.add2', [2, 3]).then(
function (res) {
console.log("Result:", res);
}
);

Можно сделать что-то вроде:

session.call('message.send', { to: 5678, cyphertext: 'zzzzzz', auth: 'xyz1234' }).then( ...);

На стороне сервера ты должен будешь "зарегистрировать" процедуру message.send и тогда брокер будет перенаправлять вызов от клиента твоему серверу, он обработает его и пошлет ответ, который брокер перешлет клиенту. Ты упоминаешь crossbar, и в нем есть пример "компонента", который регистрирует RPC и отвечает на вызовы:

https://github.com/crossbario/crossbar-examples/blob/master/getting-started/3.rpc/client_component_rpc_callee.py

Там есть функция utcnow(), и клиент (браузер) может ее вызывать удаленно, используя session.call().

callee в название файла обозначает "вызываемая сторона", в отличие от caller - "вызывающая сторона".

Тут конечно надо еще поподробнее изучить crossbar, если ты хочешь его использовать, чтобы понять, как он работает, как оптимальнее его настроить, итд. Нужно его потестировать, итд.

> Чтобы авторизовать публикации для клиентов напомню что они могут приходить только от сервера, нужно воспользоваться встроенным в роутер механизмом - функции динамической авторизации.

Да, наверно так. Хотя у меня ощущение, что в документации приведены примеры для небольшого числа пользователей. Например, когда ты используешь crossbar для обмена данными между несколькими устройствами, а не для многопользовательского веб-сервиса.

Потому, возможно, тебе придется делать дополнительную авторизацию на уровне RPC, например, передавать какой-то токен в RPC функцию отправки сообщения.

> // нужно проверить что это делает именно сервер
> // для этого я решил создать jwt со свойством isServer: true

Можно так, да. Хотя выглядит как усложение, конечно.

> Вопрос прост - Как вынести это n-ое количество проверок в отдельную архитектуру?

Там есть какие-то роли, может быть их можно использовать. Сделать роли "фронтенд/браузер" и "бекенд".

Ответы: >>1393712 >>1393712
someApprentice 2019/05/04 06:49:49  №1393712 3
>>1389598 -> >>1388926


>>1388926
>> // нужно проверить что это делает именно сервер
>> // для этого я решил создать jwt со свойством isServer: true
>
>Можно так, да. Хотя выглядит как усложение, конечно.
Мне хочется сделать проверку на сервер как проверку на обычного пользователя. То есть, при установки приложения сделать в БД первого пользователя с именем root, и дальше делать все проверки от него.

Это очень удобно:

def authenticate(token):
... return user

# if user.name == 'root':
if user.email == 'root@crypter.com' #понадёжней т.к. уникальный идентификатор
...

Следует сделать скрипт, который будет принимать все credentials, выполнять всю установку, а на выход выдавать все хэши и токены. Возможно генерировать сразу .env файл.

Как обычно делаются такие файлы? Через Докер (не разу с ним не знакомился)? Приложение становится всё больше мултиязычным и делать этот скрипт на каком-то из языков не логично. Лучше сделать это на системном языке и чтобы он был кроссплатформенный. Можете что-нибудь посоветовать пожалуйста?


Вопрос по синтаксису или скорее архитектуре Питона:

Я создавал модели в соответствии с задуманной схемы БД и из-за наследования в ней, мне пришлось делать наследования и в моделях - такое поддерживается и "приветствуется" в выбранной мной ORM.

Чтобы описать проблему, следует сперва показать код https://repl.it/repls/PoliteHappyScales

Как можно заметить, при инициализации приложения возникает ошибка в text_message.py. Поискав в интернете, я выяснил что это проблема называется cyclic dependency inheritance.

Чтобы решить её, я вынес все наследования в отдельный модуль: https://repl.it/repls/PowderblueInsidiousCubase

Я правильно поступил?
Ответы: >>1394573
Аноним 2019/05/05 15:29:48  №1394573 4
>>1393712

По поводу импортов: я не очень понимаю, зачем ты в model/message.py импортируешь наследников. Обычно импортируют только то, что нужно в данном файле. И обычно наследник импортирует предка, а не наоборот. Потому я думаю, эти импорты надо просто убрать:

# models/message.py
import text_message
import voice_message

Зачем они добавлены? Странно, что у тебя предок зависит от своих наследников (что они ему нужны).

Также, ты похоже выбрал Concrete Table Inheritance, возможно, что запросы к ней потребуют лишних UNION, судя по мануалу: https://docs.sqlalchemy.org/en/13/orm/inheritance.html#concrete-table-inheritance

> Следует сделать скрипт, который будет принимать все credentials, выполнять всю установку, а на выход выдавать все хэши и токены. Возможно генерировать сразу .env файл.

Я думаю, достаточно только сгенерировать токены. В .env может быть еще куча других параметров. Но можно, конечно, выдавать заготовку .env файла.

> Как обычно делаются такие файлы? Через Докер (не разу с ним не знакомился)?

В dev среде можно использовать docker-compose для оркестрации докеров с отдельными приложениями. Докер, как правило, используется чтобы упаковать в образ программу с нужными ей библиотеками, например, определенную версию Питона или Ноды, чтобы ее не надо было устанавливать в систему руками. Код твоего приложения в докер-образ не кладется, а подмонтируется в него как внешний раздел. docker-compose заниамется тем, что просто запускает несколько докеров (например: микросервис авторизации и основное приложение). На Винде и Маке Докер запускает код в виртуальной машине с линуксом, а файлы прокидываются через сетевую файловую систему со всеми вытекающими.

Ты можешь найти готовый пример приложения на PHP + nginx + mysql в докере и разберешься, я думаю.

> Лучше сделать это на системном языке и чтобы он был кроссплатформенный. Можете что-нибудь посоветовать пожалуйста?

Обычно используют Питон, bash для таких скриптов. Если у тебя код на Питоне, логично в нем сделать CLI скрипт для генерации токенов.

>>1393656

У тебя описан CGI, он не используется на практике, используется FastCGI, а там все посложнее, и один процесс php обрабатывает много запросов по очереди.

Ответы: >>1397648
someApprentice 2019/05/11 13:13:34  №1397648 5
image.png (249, 1920x1080)
1080x1920
image.png (276, 1920x1080)
1080x1920
image.png (356, 1920x1080)
1080x1920
image.png (290, 1920x1080)
1080x1920
>>1394573
>По поводу импортов: я не очень понимаю, зачем ты в model/message.py импортируешь наследников. Обычно импортируют только то, что нужно в данном файле. И обычно наследник импортирует предка, а не наоборот. Потому я думаю, эти импорты надо просто убрать:
>
># models/message.py
>import text_message
>import voice_message
>
>Зачем они добавлены? Странно, что у тебя предок зависит от своих наследников (что они ему нужны).
Я понял свою ошибку. Я когда пытался получить все сообщения я использовал команду вида session.query(Message).join(Message_Reference).filter(Message_Reference.user == self.user).all() и получал только сущности родительского класса Message, и я подумал что родителю необходимо знать о детях чтобы получить их всех.
Я теперь понял, что импорты должны выполнятся там где выполняется этот запрос - это работает.

>Также, ты похоже выбрал Concrete Table Inheritance, возможно, что запросы к ней потребуют лишних UNION, судя по мануалу: https://docs.sqlalchemy.org/en/13/orm/inheritance.html#concrete-table-inheritance
К сожалению, мне не удалось найти какой паттерн наследования использует psql. Я косвенно предположил, что это именно он.pic-1

Использование UNION приводит к нагрузке?

Вообще, ORM не очень дружат с наследованием, как я могу посудить. Отношения между сущностями тяжело совершить с помощью встроенных инструментов и приходится делать метод для совершения запроса в ручную.pic-2 При получении сущностей выдается массив из сначала сущностей родителя, затем сущности ребёнка.pic-3 Я не знаю должно ли быть такое поведение. Хочется самому отсортировать только сущности детей и возвращать этот массив.


>> Следует сделать скрипт, который будет принимать все credentials, выполнять всю установку, а на выход выдавать все хэши и токены. Возможно генерировать сразу .env файл.
>
>Я думаю, достаточно только сгенерировать токены. В .env может быть еще куча других параметров. Но можно, конечно, выдавать заготовку .env файла.
>
>> Как обычно делаются такие файлы? Через Докер (не разу с ним не знакомился)?
>
>В dev среде можно использовать docker-compose для оркестрации докеров с отдельными приложениями. Докер, как правило, используется чтобы упаковать в образ программу с нужными ей библиотеками, например, определенную версию Питона или Ноды, чтобы ее не надо было устанавливать в систему руками. Код твоего приложения в докер-образ не кладется, а подмонтируется в него как внешний раздел. docker-compose заниамется тем, что просто запускает несколько докеров (например: микросервис авторизации и основное приложение). На Винде и Маке Докер запускает код в виртуальной машине с линуксом, а файлы прокидываются через сетевую файловую систему со всеми вытекающими.
>
>Ты можешь найти готовый пример приложения на PHP + nginx + mysql в докере и разберешься, я думаю.
Я ошибся когда писал, что у разных ролей wamp'а разные uri. Сейчас я сделал авторизацию действий засчет ролей и даже для сервера не нужно применять динамическую авторизацию.pic-4

...я когда придумывал архитектуру мне сразу приходил в голову такой подход, но я тестировал и ничего не вышло. Я не помню при каких настройках это было, возможно я что-то не так указал. Тяжело на ходу держать всё в уме.


>> Лучше сделать это на системном языке и чтобы он был кроссплатформенный. Можете что-нибудь посоветовать пожалуйста?
>
>Обычно используют Питон, bash для таких скриптов. Если у тебя код на Питоне, логично в нем сделать CLI скрипт для генерации токенов.
>Если у тебя код на Питоне
Страница выдаётся из Ноды, API я собираюсь переписать на PHP, а код wamp'а на Питоне.


>>1394587
>Такие запросы с джойнами будут плохо работать на больших нагрузках. Они же почти не оптимизируются индексами никак и требуют перебор строк. Возможно, тут придется сделать денормализацию, например, добавить в Dialog либо в Participant дополнительные поля. Чтобы, например, запрос бы имел вид
>
>SELECT FROM Participant WHERE user = ? AND partner = ? AND private =1
>
>Это ложится на индексы. Но, конечно, денормализацию стоит делать во вторую очередь.
Я думаю, что партнер должен добавляться скорее в ссылку на конференцию, потому что получатель это отдельная сущность, которая не отвечает за то с кем она должна вести диалог. А если партнёров будет много (публичная конференция)? Конечно тут можно прийти к созданию отдельной таблицы Partners... не будет ли и здесь плохо работать запрос с джоинами на больших нагрузках?

Далее, даже учитывая что мы создадим поле partner в Conference_Reference, это создаст запрос для получения вида:

// неизвестно какой пользователь сначала "создал" конференцию,
// а какой оказался получателем (партнёром)
SELECT FROM Conference_Reference WHERE (user = sender.id OR user = receiver.id) AND (partner = receiver.id OR partner = sender.id)

Такой запрос может вернуть две записи и сама его форма не элегантна.

Я предлагаю вернуться к идеи выше с массивами >>1387125

SELECT FROM Conference WHERE private = true AND (sender.id = ANY(participants) AND receiver.id = ANY(participants));

Как писалось выше такой запрос использует индексы (https://stackoverflow.com/questions/4058731/can-postgresql-index-array-columns).

У вас есть опыт с работой с индексами. Как вам кажется, индексирование массивов приведёт к желаемому повышению производительности?



>> Можно я оптимизацию БД оставлю на последнее?
>
>Можно. Но я просто хотел подчеркнуть, что мессенджеры это высоконагруженные штуки и там приходится об этом думать. И о шардинге, если ты хочешь, чтобы проект расширялся. Потому мы сначала проектируем нормализованную схему, а потом смотрим на типичные запросы и оптимизируем схему для них.
>
>Более того, часто под мессенджеры даже пишутся специализированные хранилища.
Я это прекрасно понимаю..

>Насчет reply - а недостаточно тут просто сделать поле в сообщении "replyToUuid" со ссылкой на исходное сообщение? Без вложений.
Судя по API телеграма там используется как раз такой подход ( https://core.telegram.org/bots/api#message ), но мне никогда не было понятно почему только можно ответить на одно сообщение, мне иногда хочется ответить на несколько сразу, да и выглядит это как прикрепление к сообщению.

>Так, схема выглядит хорошо. Позже стоит продумать операции, которые будут выполняться (их явно будет больше, например, может понадобиться постраничное получение участников огромного чата). И как их оптимизировать.
Опять же с массивами это тоже делается очень просто

SELECT participants[offset:limit] FROM Conference WHERE id = conference.id;


Мне хочется сделать, так же, и наследование и в конференциях, чтобы была отдельно приватная конференция и отдельно публичная. Публичная будет иметь свой приватный ключ (иначе шифровать сообщения для каждого отдельного пользователя будет затратно).


Также, я озаботился вопросом как работает с ключами protonmail, и оказалось, что он хранит их на сервере зашифрованные паролем пользователя, как я изначально и собирался это и делать. Я считал, что ключи должны храниться только у пользователя, но подход с генерацией ключей для каждого клиента пользователя создаёт только больше проблем - если пользователь часто меняет клиент (чистит кэш браузера/удаляет приложения/использует несколько устройств), это может превратиться в кошмарный список ключей и при зашифровки сообщения для них всех будет требовать всё больше и больше ресурсов.
Вынуждать пользователя самому заниматься менеджментом своих ключей было бы отталкивающе и тяжело доступно для понимания.

https://protonmail.com/support/knowledge-base/how-is-the-private-key-stored/

Я собираюсь повторить эту практику.

Такой подход вновь открывает возможность создания NoJS версии, но тратить время на это нужно только в случае, если за это можно выиграть лакомый кусочек пользователей. Или для надёжности, которые будут требовать текущие пользователи.


>> Я думаю, что пока подходы к авторизации методов WAMP не изучены, следует писать сырой код на if'ах, а подход с роутингом взять на заметку и держать в уме. А как думаете вы?
>
>Я, увы, так хорошо авторизацию в WAMP не знаю и сейчас не могу подсказать.
Рано или поздно мы разберёмся какая архитектура будет лучше.

P.S.
Я понимаю, что ваши советы по архитектуры базы данных содержат опыт, и я парой прихожу к тому что ваш совет, в конечном итоге, оказывается проще, но я всё равно нахожу либо ошибку в своих действиях в применении новых технологий, либо решение появляйщейся проблеме. Я чувствую, что я на правильном пути применяя новые технологии и идеи. В конечном итоге, это кристаллизуется в опыт и можно будет сравнить плюсы или минусы того или иного подхода.



Ответы: >>1397655 >>1398326
Аноним 2019/05/11 13:16:23  №1397655 6
>>1397648
Илюша,ты когда трезвый,такой добрый! кек
Аноним 2019/05/12 11:52:50  №1398326 7
>>1397648

> Я теперь понял, что импорты должны выполнятся там где выполняется этот запрос - это работает.

Общий принцип такой, что ты импортируешь только то, что как-то используешь именно в этом файле (вызываешь, создаешь объекты этого класса итд). Если не используешь - то не импортируешь.

> К сожалению, мне не удалось найти какой паттерн наследования использует psql

Эти паттерны наследования реализуются не на уровне Postgres, а на уровне ORM. И там в документации к SQLAlchemy перечислено несколько вариантов на выбор.

> Я косвенно предположил, что это именно он

Это Concrete TI, так как при STI таблица одна, а при Class TI в text_message были бы только те поля, которые отсутствуют в message.

> Использование UNION приводит к нагрузке?

Это надо смотреть EXPLAIN-ом и делать тесты, но скорее всего, да. Представь, что ты хочешь получить последние 10 сообщений: самый быстрый способ сделать это - это пройтись по индексу (где сообщения отсортированы по времени) и взять первые 10 записей из единственной общей таблицы. В случае с UNION нам надо брать по несколько сообщений из каждой таблицы и как-то объединять списки. Это в случае, если СУБД будет работать максимально оптимально, а это не факт.

Но вообще, выбор схемы наследования зависит от того, какие мы запросы делаем и какие у нас данные.

> Отношения между сущностями тяжело совершить с помощью встроенных инструментов и приходится делать метод для совершения запроса в ручную

Вообще, в документации есть похожий пример и там используются дополнительные параметры (foreign_keys): https://docs.sqlalchemy.org/en/13/orm/join_conditions.html#creating-custom-foreign-conditions

Может, в этом проблема?

Ведь ORM надо знать, как связывать объекты между собой. Допустим, у тебя есть объект класса A, и он связан отношением с объектом класса B. Если ты создаешь несколько объектов класса B, добавишь их в класс A, и попробуешь сбросить данные в БД, то ORM должен понять: как сохранить в БД связь между объектами?

Думаю, явное указание foreign_keys и remote_side как раз говорит ORM, что надо взять id из одного поля объекта и прописать его в поле другого объекта.

Но я могу ошибаться, так как лишь бегло пролистал документацию.

> При получении сущностей выдается массив из сначала сущностей родителя, затем сущности ребёнка.

Если ты используешь наследование, то Text_Message - это наследник Message, в соответствие с принципом Лисков наследник можно использовать вместо предка, и когда ты запрашиваешь список Message, то в нем будут и объекты Text_Message. Если ты это не хочешь, то тебе надо указать дополнительное условие (например: искать только Text_Message).

> Сейчас я сделал авторизацию действий засчет ролей и даже для сервера не нужно применять динамическую авторизацию

Только не забудь, что надо использовать принцип "белого списка" и все, что не указано явно - запрещается.

> Лучше сделать это на системном языке и чтобы он был кроссплатформенный

Вообще, "системное программирование" - это написание всяких модулей ядра, драйверов на языках вроде Си. А так, пиши на чем удобнее - на Питоне, JS или bash.

> А если партнёров будет много (публичная конференция)?

Часто в больших публичных конференциях список участников не показывают, пока ты не нажмешь какую-то кнопку. Потому, тут стоит учесть только такие варианты:

- получить полный список для маленькой конференции
- получить (по запросу пользователя) полный список для средней конференции
- если у тебя есть большие конференции (> 1000 - 5000 - 10000 польз.), то там может потребоваться получать список постранично

> не будет ли и здесь плохо работать запрос с джоинами на больших нагрузках?

Это зависит от того, как выглядит запрос. Одно дело - приджойнить 1 запись по ключу, другое дело - сджойнить два больших списка, к каждому из которых применяется какое-то условие.

> // неизвестно какой пользователь сначала "создал" конференцию,

Я думал, что у тебя там у каждого пользователя есть своя запись для конференции (со своим числом непрочитанных сообщений), и потому такой сложный запрос не нужен: мы ищем именно свою запись, а не любую.

> Как вам кажется, индексирование массивов приведёт к желаемому повышению производительности?

Без индексов тут нельзя - это я точно могу сказать. Но вид запроса очень неудачный, и скорее всего будет плохо индексироваться. Вот пример хорошего запроса:

SELECT WHERE a = 1 AND b = 2

Такой запрос при использовании UNIQUE INDEX (a, b) будет искать в индексе пару (1; 2) и будет оптимальным. Индекс выглядит так:

(a, b -> id)

Он отсортирован по возрастанию пар значений a; b.

А как выглядит индекс в твоем случае? Скорее всего, как отсортированный список (participant -> id). И как в нем найти соответствующие условию записи? Ищем по participant = sender, получаем большой список id, ищем по participant = receiver, получаем большой список id, ищем в 2 списках одинаковые id.

Твой запрос эквивалентен такому:

SELECT a.id FROM table AS a, table AS b WHERE a.participant = :sender AND b.participant = :receiver AND a.id = b.id

То есть, попробуй этот индекс нарисовать и придумать, как вообще в теории в нем найти нужную запись, и ты возможно увидишь, какие запросы будут работать быстро, а какие - в принципе не могут работать быстро.

Потому я и советовал поступать так:

- сделать нормализованную, не оптимальную схему
- сделать список запросов, которые будут выполняться
- оптимизировать схему под них

> но мне никогда не было понятно почему только можно ответить на одно сообщение,

Потому что это упрощает пользовательский интерфейс, наверно. Но твой подход тоже имеет право на жизнь. Кто знает, может это будет как раз преимуществом в сравнении с другими мессенджерами.

> Опять же с массивами это тоже делается очень просто

Только там происходит полная выборка списка в память. Если участников очень много (десятки тысяч), кто знает, может это неэффективно (нужно мерять).

> Я считал, что ключи должны храниться только у пользователя, но подход с генерацией ключей для каждого клиента пользователя создаёт только больше проблем

Это решается, если делать клиентское приложение на Electron (или на любой другой технологии) - оно работает на стороне пользователя и может хранить что угодно, при этом очистка кук ему не грозит. Но с синхронизацией да, придется помучаться.

Шифрование ключа паролем - это симметричное шифрование. Пароль относительно короткий и владелец сервера может попытаться его сбрутфорсить. Особенно, если у него есть "друзья" из NSA с дата-центрами, построенными специально для брутфорса паролей. Но можно придумать более интересные схемы синхронизации: 2 устройства одного пользователя одновременно выходят в сеть и передают ключ друг другу с использованием асимметричного шифрования, так, что сервер не может увидеть передаваемый ключ (т.к. при асиметричном шифровании используется случайный очень длинный ключ, как в HTTPS).

Это просто рассуждения на тему того, что тут можно сделать в теории.