From d1a09efd0038575c33391b9d69c71d2f745f651a Mon Sep 17 00:00:00 2001 From: Andreev Gregory Date: Wed, 14 Aug 2024 21:49:18 +0300 Subject: [PATCH] =?UTF-8?q?first=20raw=20version=20=D1=81=20=D0=BE=D0=BF?= =?UTF-8?q?=D0=BF=D1=87=D0=B0=D1=82=D0=BA=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + api.typ | 461 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 463 insertions(+) create mode 100644 .gitignore create mode 100644 api.typ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa051f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +api.pdf + diff --git a/api.typ b/api.typ new file mode 100644 index 0000000..e764620 --- /dev/null +++ b/api.typ @@ -0,0 +1,461 @@ +#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 = $~>$ + +#Heading([Руководство], 1) + +Обозначается множество запросов, +которые клиент может посыдать серверу для передачи и +получения данных. На один запрос следует один ответ (как минимум что бы сообщить об +успехе запроса). + +Для каждого запроса есть свой URI адрес на сервере. Для сохранения порядка адреса всех +запросов начинаются в #raw("/internalapi"). Объявление запроса состоит из указания его +адреса (выступающего чем-то вроде имени), формата отправляемых (на сервер) данных и +формата принимаемых данных. После объявления (в прямоугольнике) идёт описание. +Данные запроса и ответа передаются в формате JSON. Ключевое слово #bold[Sent] обозначает +отправляемый JSON-объект. Ключевое слово #bold[Recv] обозначает получаемый JSON-объект. + +#Heading([Структура], 1) + +У каждого чата поддерживается текущий HistoryID. Изначально эта переменная (на сервере) +равна нулю, но с удалением, изменением и написанием новых сообщений, с добавлением +и удалением пользователей из группы, изменяется состояние чата. Эти изменения называются +событиями. Кадое новое событие инкрементирует HistoryID. Клиент так же поддерживает +для каждого чата LocalHistoryID, который соответствует текущему состоянию чата в памяти +клиента. При запросе новых событий, клиент отправляет свой LocalHistoryID у +запрашиваемых чатов и получает события, знание которых необходимо для обновления +состояния "от LocalHistoryID до HistoryID", а так же этот самый новый HistoryID, который +олицетворяет собой состояние чата с учетом новых полученных событий. После прочтения +списка событий клиент заменяет свой LocalHistoryID на полученный HistoryID. +И так далее, по циклу. Любой запрос возвращает обновления. + +Не только чаты могут получать события. Здесь "сущностями с историей" я называю чаты и +список чатов пользователя. + +Определим особую структуру hist_entity_request такого формата: + +`hist_entity_request["type"]` = `"chat"` или `"chatlist"` + +Если запрашивается чат, то нужно указать его ID. +`hist_entity_request["chatId"]` = Число + +`hist_entity_request["LocalHistoryId"]` = Число #funnyArrow указывает версию чата в памяти +клиента + +Пример: ```json {"type": "chat", "chatId": 2, "LocalHistoryId": 12312321}``` +```json {"type": "chatlist", "LocalHistoryId": 31231}``` + +Определим особую структуру hist_entity_response такого формата: + +`hist_entity_response["type"]` = `"chat"` или `"chatlist"` + +Если запрашивается чат, то нужно указать его ID. +`hist_entity_response["chatId"]` = Число + +`hist_entity_response["HistoryId"]` = Число. Это новый HistoryId для этой сущности с историей. + +`history_entity_response["events"]` = Список новых событий. + +Событие - это словарь. Обозначим его ключемым словом #bold[event]: + +`event["type"]` = Строка #funnyArrow Это тип события, которые бывают нескольких типов: + +- `"newMessage"` - приходит новое сообщение в чат +- `"addedMember"` - в чат добавили нового участника +- `"removedMember"` - из чата выкинули участника +- `"addedChat"` - клиента добавили в чат +- `"removedChat"` - клиента выкинули из чата + +#Heading([Событие `newMessage`], 2) + +```json +{ + "type" : "newMessage", + "previous": 111111 // Id of previous message + "id": 1112 // Id of the message + "content": { + "isSystem": false // Булевое значение, true если это системное сообщение вроде "поприветствуйте нового пользователя". Такие должны выделяться особым цветом. + "text": "" // Text of the message + "sender": 1000 // User ID of sender + }, +} +``` + +#Heading([Событие `addedMember`], 2) + +```json +{ + "type": "addedMember", + "member": 1000 // User ID добавленного человека + "content": { + "name": "", + "nickname": "" + "role": "regular" // Роль это либо regular, либо admin + } +} +``` + +#Heading([Событие `removedMember`], 2) + +```json +{ + "type": "removedMember", + "member": 1000 // User ID удалённого человека (или покинувшего чат человека) +} +``` + +#Heading([Событие `addedChat`], 2) + +```json +{ + "type": "addedChat", + "id": 228, // Chat ID + "content": { + "name": "<имя чата>", + "nickname": "", + "lastMsgId": 1212 // Id последнего сообщения. -1 Если чат пуст + } +} +``` + +#Heading([Событие `removedChat`], 2) + +```json +{ + "type": "addedChat", + "id": 228, // Chat ID того чата, из которого клиента удалили / из которого клиент вышел +} +``` + +Пример объекта hist_entity_response: +```json +{ + "type": "chat", "chatId": 2, "HistoryId": 12312325, + "events": [ + { + "type": "newMessage", + "previous": 111111, + "id": 111115, + "content": { + "isSystem": false, + "text": "私は頭骨を劇場から盗んだ", + "sender": 666 + } + }, + { + "type": "newMessage", + "previous": 111115, + "id": 1000000, + "content": { + "isSystem": true, + "text": "Father mushroom left chat", + "dender": 0 + } + }, + { + "type": "removedMember", + "member": 666 + }, + { + "type": "addedMember", + "member": 777, + "content": { + "name": "'Definitely not theatre looter'", + "nickname": "father-mushroom-2" + } + }, + { + "type": "newMessage", + "previous": 1000000, + "id": 1000002, + "content": { + "isSystem": true, + "text": "'Definitely not theatre looter' joined chat", + "sender": 0 + } + } + ] +} +``` + +#Heading([Запросы], 1) + +`Recv["status"] = Integer` #funnyArrow Это поле присутствуент во всех ответах, его значение +0 в случае успеха запроса и отрицательно если произошла ошибка. Клиент должен быть написан так, что +получение ошибки от сервера не вызывает сбоя. Так как это поле есть везде, в описании +запросов оно умалчивается. + +Так же, в ответах может присутствовать поле `Recv["error"]`, которое описывает ошибку. +Но оно не обязательно даже в случае когда ошибка есть, когда ошибки нет оно бесполезно. + +#let bigrect(cnt) = rect(stroke: 1pt, width: 100%, cnt) +#let uritxt(string) = text(fill: rgb(12, 10, 255, 255), raw(string)) + +#Heading([Read-only запросы], 2) + +#Heading([Получение событий], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/pollEvents") \ + - Отправить: \ + `Sent["scope"]` = [hist_entity_request, ..., hist_entity_request] #funnyArrow Список + сущностей, по которым клиент хочет получить обновления. \ + - Получить: \ + `Recv["update"]` = [hist_entity_response, ..., hist_entity_response] #funnyArrow Список + сущностей, для которых пришли изменения. +] + +В списке `Recv["update"]` будут предоставлены обновления только для тех сущностей, +которые были запрошены. + +#Heading([Получение списка чатов где пользователь состоит], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/getChatList") \ + - Отправить: \ + `Sent = {}` #funnyArrow Да, мы просто отправляем пустой словарь \ + - Получить: \ + `Recv["chats"] = Array` \ + `Recv["chats"][i]["id"] = Integer` #funnyArrow Chat ID \ + `Recv["chats"][i]["content"]["name"] = ""` \ + `Recv["chats"][i]["content"]["nickname"] = ""` \ + `Recv["chats"][i]["content"]["lastMsgId"]` = Integer #funnyArrow Id последнего сообщения в чате +] + +#Heading([Получение информации о чате], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/getChatInfo") \ + - Отправить: \ + `Sent["id"] = Integer` Просто отправляем id чата \ + - Получить: \ + `Recv["name"] = ""` \ + `Recv["nickname"] = ""` \ + `Recv["lastMsgId"] = Integer` #funnyArrow ID последнего сообщения в чате. +] + +Если чат пуст, то lastMsgId равен -1. + +#Heading([Получение списка участников чата], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/getChatMemberList") \ + - Отправить: \ + `Sent["chatId"] = Integer` Это id чата \ + - Получить: \ + `Recv["members"] = Array` Получаем мы только список участников чата. \ + Вот какие структуры находятся в этом списке: \ + `Recv["members"]["id"] = Integer` ID Пользователя \ + `Recv["members"]["content"]["name"] = ""` \ + `Recv["members"]["content"]["nickname"] = ""` \ + `Recv["members"]["content"]["role"] = "<Роль этого участника чата>"` +] + +Роль учатника это либо `"regular"`, либо `"admin"`. Думаю, семантика их ясна из +названий. + +#Heading([Получение информации о пользователе], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/getUserInfo") \ + - Отправить: \ + `Sent["id"] = Integer` #funnyArrow Id пользователя о котором хотим получить инфу. \ + - Получить: \ + `Recv["content"]["name"] = ""` \ + `Recv["content"]["nickname"] = ""` \ +] + +#Heading([Получение информации о себе], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/mirror") \ + - Отправить: Пустой словарь \ + - Получить: \ + `Recv["id"] = Integer` #funnyArrow Id себя +] + +Этот запрос нужен понадобиться если по какой-то причине клиент забыл id пользователя, +которого обслуживал. + +#Heading([Получение информации о сообщении], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/getMessageInfo") \ + - Отправить: \ + `Sent["chatId"] = Integer` \ + `Sent["id"] = Integer` \ + - Получить: \ + `Sent["content"]["text"] = ""` \ + `Sent["content"]["isSystem"] = Boolean` \ + `Sent["content"]["sender"] = 1100` #funnyArrow User ID отправителя \ +] + +#Heading([Получение соседей сообщения], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/getMessageNeighboursInfo") \ + - Отправить: \ + `Sent["chatId"] = Integer` \ + `Sent["id"] = Integer` #funnyArrow Выбираем нужное сообщение в чате \ + `Sent["direction"] = "backward" / "forward"` \ + `Sent["amount"] = Integer` \ + - Получить: \ + `Recv["messages"] = Array` \ + Его элементы это ID сообщения и контент сообщения. \ + `Recv["messages"][i]["id"]` \ + `Recv["messages"][i]["previous"]` \ + `Recv["messages"][i]["content"]` +] + +Структура контента сообщения описана в @ev_newMessage. + +Можно узнать соседей сообщения "сверху и снизу". Направление `"backward"` покажет $n$ +сообщений до переданного сообщения (они будут расположены в списке в обратном порядке), +направление `"forward"` покажет $n$ сообщений до указанного. Здесь $n$ это выбранное клиентом +количество желаемых сообщений (`Sent["amount"]`). Сервер ОБЯЗАН вернуть ровно $n$ сообщений, +если они есть и ОБЯЗАН вернуть все сообщения до определённого края истории чата, если край достигнут. + +#Heading([Запросы изменения состояния одного чата], 2) + +#Heading([Отправка сообщения], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/sendMessage") \ + - Отправить: \ + `Sent["chatId"] = Integer` #funnyArrow В какой чат мы хотим отправить сообщение \ + `Sent["LocalHistoryId"] = Integer` #funnyArrow HistoryID для состояния ЭТОГО чата (в памяти клиента) \ + `Sent["content"]["text"] = ""` \ + - Получить: \ + `Recv["update"] = Array` +] + +Здесь поле `Recv["update"]` означает то же самое, что и в +#uritxt("/internalapi/pollEvents") (см. @api_pollEvents). Там +содержатся только один hist_entity_response - указывающий изменения в этом чате. +Клиент ОБЯЗАН отображать отправленное пользователем сообщение только тогда, когда +он найдёт сообщение с сервера о событии с отправкой этого сообщения. + +#Heading([Редактирование сообщения], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/editMessage") \ + - Отправить: \ + `Sent["chatId"] = Integer` \ + `Sent["LocalHistoryId"] = Integer` #funnyArrow HistoryID для состояния ЭТОГО чата \ + `Sent["id"] = Integer` \ + `Sent["content"]["text"] = ""` #funnyArrow Новый текст сообщения + - Получить: \ + `Recv["update"] = Array of hist_entity_response` #funnyArrow Всё то же самое (См. @api_pollEvents) +] + +Обновление получает + +#Heading([Добавление участника в чат], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/addMemberToChat") \ + - Отправить: \ + `Sent["chatId"] = Integer` #funnyArrow К какому чату добавляем \ + `Sent["LocalHistoryId"] = Integer` #funnyArrow HistoryID для состояния ЭТОГО чата \ + `Sent["userId"] = Integer` #funnyArrow ID добавляемого пользователя \ + - Получить: \ + `Recv["update"]` +] + +#Heading([Удаление участнка из чата], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/removeMemberFromChat") \ + - Отправить: + `Sent["chatId"] = Integer` #funnyArrow Из какого чата удалить \ + `Sent["LocalHistoryId"] = Integer` #funnyArrow HistoryID для состояния ЭТОГО чата \ + `Sent["userId"] = Integer` #funnyArrow ID удаляемого пользователя \ + - Получить: \ + `Recv["update"]` +] + +Как можно заметить, все запросы, изменяющие состояние чата имеют побочную функцию: обновление. +Она обязательна, клиент ОБЯЗАН указывать LocalHistoryID, +в запросе приутствует мнимый hist_entity_requet. Он аддресует тот чат, к котороу применим запрос + +#Heading([Запросы изменения состояния списка чатов пользователя], 2) + +В запросах этой категории есть ключ `"LocalHistoryId"`, а в ответах на них есть ключ `"update"`, +но они нужны для поллинга событий не какого-то чата, а списка чатов где есть пользователь. + +#Heading([Создание чата], 3) + +#bigrect[ + - URI: #uritxt("/internalapi/createChat") + - Отправить: \ + `Sent["LocalHistoryId"] = Integer` #funnyArrow Это ID состояния списка чатов пользователя \ + `Sent["content"]["name"] = ""` \ + `Sent["content"]["nickname"] = ""` \ + - Получить: \ + `Sent["update"]` #funnyArrow События, произошедшие cо списком чатов пользователя. + `Sent["chatId"] = Integer` +] + +`Sent["chatId"]` это id созданного по этому запросу чата + +#Heading([Удаление чата], 3) +#bigrect[ + - URI: #uritxt("/internalapi/createChat") + - Отправить: \ + `Sent["LocalHistoryId"] = Integer` #funnyArrow Это ID состояния списка чатов пользователя \ + `Sent["id"] = Integer` #funnyArrow Chat Id того чата, который должен быть удалён \ + - Получить: \ + `Sent["update"]` #funnyArrow События, произошедшие cо списком чатов пользователя. +] + +#Heading([Расположение ресурсов веб-чата по URI], 1) +Эта секция не касается internalApi, она о самом веб-чате. + +#Heading([Заглавная страница], 2) +По адресу #uritxt("/") показывается список чатов пользователя. Еезалогиненный пользователь, +зашедший сюда, будет переслан на #uritxt("/login") + +#Heading([Логин], 2) +Адрес: #uritxt("/login"). На этой странице нет ничего кроме формы для входа. +Форма отправляет данные на #uritxt("/login") POST запросом. +Успешный вход пересылает на #uritxt("/"), неуспешный показывает ошибку на этой же странице. + +#Heading([Создание чата], 2) +Форма создания чата отправляет данные на #uritxt("/createChat") POST запросом. +При успехе пользователь пересылается на страницу чата (#uritxt("/chatRoom/...")) + +Где находится форма - сами решайте. + +#Heading([Редактирование профиля], 2) +Свой профиль редактируется на странице #uritxt("/mirror"). Туда же отправляются данные форму +(POST запросом). При успехе пользователь пересылается на главную страницу, +иначе, остаётся на прежней. + +#Heading([Просмотр профиля], 2) +Профиль любого человека можно посмотреть на странице #uritxt("/user/") + +#Heading([Страница чата], 2) +Адрес: #uritxt("/chat/"). Об остальных страницах договаривайтесь сами. + + +#Heading([Замечания], 1) +Эта спецификация не утверждает, что Id для одних и тех же чатов и сообщений +сохраняется междй пользователями. Так же не утверждается, что Id сообщений +сохраняются между чатами. Для именования чатов и пользователей, +пользователи должны использовать никнеймы. +Когда чат или пользователь занимают никнейм, а потом заменяют его на новый, +старый не перестаёт им пренадлежать, и не может уже никогда быть занятым другим +чем-либо. Имя чата или пользователя может содержать любые символы. Никнейм +отличается тем, что в нём допустимы лишь цифры, буквы латинского алфавита, дефис.