iu9-ca-chat-api/api.typ

345 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pagebreak(weak: true)
#set text(size: 13pt)
#align(center, text([Протокол для взаимодействия веб-клиента и сервера iu9-ca-chat],
size: 30pt))
#let Heading(c, d) = {
heading(c, depth: d, numbering: "1.");
}
#let bold(txt) = text(txt, weight: "bold")
#let funnyArrow = $~>$
#let bigrect(cnt) = rect(stroke: 1pt, width: 100%, cnt)
#let uritxt(string) = text(fill: rgb(12, 10, 255, 255), raw(string))
#Heading([Руководство], 1)
Обозначается множество запросов,
которые клиент может посыдать серверу для передачи и
получения данных. На один запрос следует один ответ.
Обозначаются переменные, которые сервер передаёт nytl шаблонизатору, который в свою очередь
засовывает в html тег ```html <script> ```, что бы при инициализации клиентсайдовой жс-программы,
у неё были эти переменные.
Для каждого запроса есть свой URI адрес на сервере. Для сохранения порядка адреса всех
запросы начинаются с #uritxt("/api/"). Объявление запроса состоит из указания его
адреса (выступающего чем-то вроде имени), формата отправляемых (на сервер) данных и
формата принимаемых данных. После объявления (в прямоугольнике) идёт описание.
Данные запроса и ответа передаются в формате JSON. Ключевое слово #bold[Sent] обозначает
отправляемый JSON-объект. Ключевое слово #bold[Recv] обозначает получаемый JSON-объект.
#Heading([Структура], 1)
Здесь и далее слово "чат" означает то же самое что и комната.
У каждого чата поддерживается текущий HistoryID. Изначально эта переменная (на сервере)
равна нулю, но с удалением, изменением и написанием новых сообщений, с добавлением
и удалением пользователей из группы, изменяется состояние чата. Эти изменения называются
событиями. Кадое новое событие инкрементирует HistoryID. Клиент так же поддерживает
для каждого чата LocalHistoryID, который соответствует текущему состоянию чата в памяти
клиента. При запросе новых событий, клиент отправляет свой LocalHistoryID у
запрашиваемых чатов и получает события, знание которых необходимо для обновления
состояния "от LocalHistoryID до HistoryID", а так же этот самый новый HistoryID, который
олицетворяет собой состояние чата с учетом новых полученных событий. После прочтения
списка событий клиент заменяет свой LocalHistoryID на полученный HistoryID.
И так далее, по циклу. Любой запрос возвращает обновления.
Не только чаты могут получать события.
Список чатов, в которые входит пользователь, тоже может.
Здесь "сущностями с историей" я называю чаты и список чатов пользователя.
Запросов много, но все они обновляют какую-то из "сущностей с историей".
Например, #uritxt("/api/sendMessage") обновляет историю сообщений чата,
в который новое сообщение было отправлено, а #uritxt("/api/createChat") обновляет
список чатов пользователя, который вызвал этот запрос.
Значит, для каждого запроса нужен свой способ указать нужную сущность с историей и её
LocalHistoryId, соответствующий сущности, изменяемой запросом, а также нужно уметь
принимать ответ с сервера об изменениях, произошедших с сущностью, что бы уметь
обновлять её состояние.
#Heading([Чаты], 2) <update_chat>
Запросы, изменяющие чат, содержат поле `"chatUpdReq"`, в котором хранится объект с полями
chatId и LocalHistoryId,
а получают ответ с полем `"chatUpdResp`с полями HistoryId (обновлённое), обновлённое lastMsgId,
полем-массивом messages, полем-массивом `members` (о них расскажу позже).
`Sent.chatUpdReq.chatId = Integer` - указывает чат \
`Sent.chatUpdReq.LocalHistoryId = Integer` - через это поле клиент сообщает свой
(возможно устаревший) LocalHistoryId. \
`Recv.chatUpdResp.HistoryId = Integer` - это новое значение HistoryId, прешедшее на замену LocalHistoryId \
`Recv.chatUpdResp.lastMsgId = Integer` - Номер последнего сообщения в чате. -1 если чат пуст. \
`Recv.chatUpdResp.messages = Array` - массив объектов типа `messageSt`. Они выглядят так так: \
`messageSt.id = Integer` - id сообщения (в этом чате). \
`messageSt.isSystem = Boolean` \
`messageSt.exists = Boolean` \
`messageSt.text` \
`messageSt.senderUserId` \
Это всё поля, определяющие внутренности сообщения. Структура messageSt не обязательно означает, что
за время обновления чата появилось такое сообщение, она говорит, что в момент `HistoryId` было такое сообщение
с таким id.\
`Recv.chatUpdResp.members` - массив объектов типа `memberSt`. Они выглядят так: \
`memberSt.userId = Integer` - id пользователя, о членстве которого в этом чате говорит этот объект \
`memberSt.nickname = String` \
`memberSt.name = String` \
`memberSt.roleHere = String` \
`roleHere` это что-то из "admin", "regular", "read-only", "not-a-member".
Самое главное - различать роль "not-a-member" от остальных. Ведь она означает что человек
больше не состоит в чате, и если он отображался в списке, то
его надо удалить. Различие остальных ролей не так принципиально.
#Heading([Список чатов, где есть пользователь], 2) <update_chatList>
Запросы, изменяющеи список чатов залогиненного пользователя содержат поле `"chatListUpdReq"`,
который содержит объект с полем `LocalHistoryId` (сами понимаете для чего),
а `"chatListUpdResp"`с полем HistoryId (обновлённое), массивом `myChats`
`Recv.chatListUpdResp.myChats` - массив объектов типа `myMembershipSt`.
Каждый такой объект говорит о состоянии "нахождения пользователя (залогиненного) в чате" в момент HistoryId.
Он может говорить как о нахождении пользователя в чате, так и о его уходе оттуда.
Он выглядит так:
`myMembershipSt.chatId = Integer` - id чата \
`myMembershipSt.chatName = String` \
`myMembershipSt.chatNickname = String` \
`myMembershipSt.myRoleHere = String` \
`myRoleHere` это что-то из "admin", "regular", "read-only", "not-a-member".
Самое главное - различать роль "not-a-member" от остальных. Ведь она означает что
чат из списка чатов надо удалить. Различие остальных ролей не так принципиально.
#Heading([Запросы], 1)
`Recv["status"] = Integer` #funnyArrow Это поле присутствуент во всех ответах, его значение
0 в случае успеха запроса и отрицательно если произошла ошибка. Клиент должен быть написан так, что
получение ошибки от сервера не вызывает сбоя. Так как это поле есть везде, в описании
запросов оно умалчивается.
Так же, в ответах может присутствовать поле `Recv["error"]`, которое описывает ошибку.
Но оно не обязательно даже в случае когда ошибка есть, когда ошибки нет оно бесполезно.
#Heading([Read-only запросы], 2)
#Heading([Получение событий в чате], 3)
#bigrect[
- URI: #uritxt("/api/chatPollEvents") \
- Отправить: \
`Sent.chatUpdReq` - про это поле говорилось в @update_chat \
- Получить: \
`Recv.chatUpdResp` - про это поле тоже говорилось выше
]
Эта функция может только обновлять чат и только один чат.
#Heading([Получение событий в списке чатов вошедшего пользователя], 3)
#bigrect[
- URI: #uritxt("/api/chatListPollEvents") \
- Отправить: \
`Sent.chatListUpdReq` - про это поле говорилось в @update_chatList \
- Получить: \
`Recv.chatListUpdResp` - про это поле тоже говорилось выше
]
#Heading([Получение соседей сообщения], 3)
#bigrect[
- URI: #uritxt("/api/getMessageNeighbours") \
- Отправить: \
`Sent.chatUpdReq` - это поле везде значит одно и то же. Отсюда берётся `chatId`,
что бы значть о каком чате идёт речь \
`Sent.msgId` - id сообщения, с которого начинается отсчет. Можно указать -1 при backward-запросе
и отсчет пойдёт "со дна" чата \
`Sent.direction = String` - это либо `"backward"`, либо `"forward"` \
`Sent.amount = Integer` - Либит на пролистывание сообщений \
- Получить: \
`Sent.chatUpdResp`
]
Можно получать полную информацию об $n$ сообщения до или после определённого сообщения.
$n = "Sent"."amount"$.
Можно заметить, что в ответе мы не получаем никакого списка сообщений, только класический `chatUpdResp`.
Дело в том, что поля `msgId`, `direction`, `amount` лишь говорят обнови состояние чата, но
ОБ ЭТИХ ВОТ
сообщениях пришли мне всю информацию, что только есть, даже если они не новые и никак не менялись.
Рекомендуется держать сообщения в разреженном массиве.
#Heading([Запросы изменения состояния одного чата], 2)
#Heading([Отправка сообщения], 3)
#bigrect[
- URI: #uritxt("/api/sendMessage") \
- Отправить: \
`Sent.chatUpdReq` - ... \
`Sent.content.text = "<text of message>"` \
- Получить: \
`Recv.chatUpdResp = Array`
]
Клиент ОБЯЗАН отображать отправленное пользователем сообщение только тогда, когда
он найдёт сообщение с сервера о событии с отправкой этого сообщения.
#Heading([Удаление сообщения], 3)
#bigrect[
- URI: #uritxt("/api/deleteMessage") \
- Отправить: \
`Sent.chatUpdReq` \
`Sent.id = Integer` - Id выбранного на расстрел сообщения \
- Получить: \
`Recv.chatUpdResp`
]
#Heading([Добавление участника в чат], 3)
#bigrect[
- URI: #uritxt("/api/addMemberToChat") \
- Отправить: \
`Sent.chatUpdReq` \
`Sent.nickname = String` - никнейм того участника, которого мы хотим добавить.
- Получить: \
`Recv.chatUpdResp`
]
Никнейм добавляемого участника должен быть введён безошибочно, иначе вернётся ошибка.
Выполняется только администратором.
#Heading([Удаление участнка из чата], 3)
#bigrect[
- URI: #uritxt("/api/removeMemberFromChat") \
- Отправить:
`Sent.chatUpdReq` \
`Sent.userId` - id пользователя, которого мы хотим удалить из чата
- Получить: \
`Recv.chatUpdResp`
]
Как можно заметить, все запросы, изменяющие состояние чата имеют побочную функцию: обновление.
Она обязательна, клиент ОБЯЗАН указывать chatUpdReq.
Выполняется только администратором.
#Heading([Запросы изменения состояния списка чатов пользователя], 2)
#Heading([Создание чата], 3)
#bigrect[
- URI: #uritxt("/api/createChat")
- Отправить: \
`Sent.chatListUpdReq` \
`Sent.content.name = String` \
`Sent.content.nickname = String` \
- Получить: \
`Recv.chatListUpdResp`
]
`Sent.content.name` и `Sent.content.nickname` это выбранные названия для нашего чата.
Если введены невозможные названия (слишком длинные или занятый никнейм), то вернётся ошибка
Пользователь, создавший чат, мгновенно вступает в него в качестве администратора.
#Heading([Удаление чата (Уйти из чата)], 3)
#bigrect[
- URI: #uritxt("/api/leaveChat")
- Отправить: \
`Sent.chatListUpdReq`
`Sent.chatId` - id чата, который мы покидаем.
- Получить: \
`chatListUpdResp`
]
#Heading([Инициализация списка чатов], 1)
Возникает вопрос "А как программа-клиент узнаёт с какого состояния чата начинать работу,
если мы можем запрашивать только обновление сущностей с историей? Откуда брать 'стартовые данные'?"
Эту информацию сервер динамически вставляет в отправляемую страничку (в тег ```html <script>```) с помощью
New York Transit Line.
Вот какие переменные доступны жс-коду на странице списка чатов:
`pres` - локаль (описана в config.json, поле "presentation". (pres - от слова presentation)) \
`userinfo.id`, `userinfo.nickname`, `userinfo.name` - это всё информация о вошедшем пользователе.
(О себе).
`initial_chatListUpdResp` - этот объект имеет такую же структуру, как и
`Recv.chatListUpdResp` из всех тех запросов к списку чатов, отсюда и название.
В `initial_chatListUpdResp.HistoryId` хранится то значение LocalHistoryId, с которого мы начинаем,
а каждый объект типа `myMembershipSt` в `initial_chatListUpdResp.myChats` говорит об одном чате,
в котором мы членствуем.
Эти данные эквивалентны запросу #uritxt("/api/chatListPollEvents") с LocalHistoryId = 0,
но сейчас вы увидете, что в случае инициализации ЧАТА подобный подход НЕОБХОДИМ.
#Heading([Инициализация чата], 1)
Вот какие переменные доступны жс-коду на странице списка чатов:
`pres` - локаль (описана в config.json, поле "presentation". (pres - от слова presentation)) \
`userinfo.id`, `userinfo.nickname`, `userinfo.name` - это всё информация о вошедшем пользователе.
(О себе). Это всё было и в list-rooms, но теперь добавляются такие переменные:
`chatinfo.id`, `chatinfo.name`, `chatinfo.nickname` - это всё информация о том чате, что мы открыли.
`initial_chatUpdResp` - этот объект имеет такую же структуру, как и `Recv.chatUpdResp`
из всех тех запросов к чату, отсюда и название.
Здесь поля `.HistoryId`, `.members` и `.lastMsgId` имеют то же значение, что и при обычном запросе обновления
(в массиве `members` будет информация о ВСЕХ участниках чата (в начале выполнения жс-кода)),
но массив `.messages` немного хитрее. Если мы зашли по ссылке #uritxt("/chat/<nickanme>"), то
мы просматриваем чат "с конца". И ".messages" будет пустовать. Если же мы зашли по ссылке
#uritxt("/chat/<nickname>/m/<msgId>"), то массив .messages будет содержать ровно один объект типа `messageSt`:
о том самом сообщении, с которого начался просмотр чата. Т.е. отличие от запроса #uritxt("/api/chatPollEvents")
в том, что мы НЕ ХОТИМ ПОЛУЧАТЬ СРАЗУ ВСЕ СООБЩЕНИЯ ИЗ ЧАТА. Мы хотим начать только с одного.
#Heading([Расположение ресурсов веб-чата по URI], 1)
Эта секция не касается API, она о самом веб-чате.
#Heading([Заглавная страница], 2)
По адресу #uritxt("/") показывается список чатов пользователя. Незалогиненный пользователь,
зашедший сюда, будет переслан на #uritxt("/login")
#uritxt("/list-rooms") это алиас для #uritxt("/")
#Heading([Логин], 2)
Адрес: #uritxt("/login"). На этой странице нет ничего кроме формы для входа.
Форма отправляет данные на #uritxt("/login") POST запросом.
Успешный вход пересылает на #uritxt("/"), неуспешный показывает ошибку на этой же странице.
#Heading([Редактирование профиля], 2)
Свой профиль редактируется на странице #uritxt("/user/<your nickname>"). Туда же отправляются данные формы
(POST запросом). При успехе пользователь пересылается на главную страницу,
иначе, остаётся на прежней и высвечиваются красные коробочки с ошибкой.
#Heading([Просмотр профиля], 2)
Профиль любого человека можно посмотреть на странице #uritxt("/user/<nickname>")
#Heading([Страница чата], 2)
Адрес: #uritxt("/chat/<nickname>").
Можно при открытии чата сразу перейти к сообщению с заранее известным id:
#uritxt("/chat/<nickname>/m/<id of message in chat>")
Просмотр списка пользователей на странице #uritxt("/chat-members/<nickname>")
Здесь же администратор может добавлять и удалять участников чата (через api запросы)
#Heading([Замечания], 1)
Утверждается, что id сообщений в чате последовательны (от 1 до какого-то натурального числа).
Т.е. все участники чата видят одни и те же номера ссобщений.
Эта спецификация не утверждает, что id для одних и тех же чатов и людей сохраняется между
сессиями. Для именования чатов и пользователей, пользователи должны использовать никнеймы.
Когда чат или пользователь занимают никнейм, а потом заменяют его на новый,
старый не перестаёт им пренадлежать, и не может уже никогда быть занятым другим
чем-либо. Имя чата или пользователя может содержать любые символы. Никнейм
отличается тем, что в нём допустимы лишь цифры, буквы латинского алфавита, дефис.