#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 сообщений сохраняются между чатами. Для именования чатов и пользователей, пользователи должны использовать никнеймы. Когда чат или пользователь занимают никнейм, а потом заменяют его на новый, старый не перестаёт им пренадлежать, и не может уже никогда быть занятым другим чем-либо. Имя чата или пользователя может содержать любые символы. Никнейм отличается тем, что в нём допустимы лишь цифры, буквы латинского алфавита, дефис.