diff --git a/.gitignore b/.gitignore index fb75514..d84e34d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ iu9-ca-web-chat.db log/ core +config/example.json \ No newline at end of file diff --git a/README.md b/README.md index 642d5ef..d38d611 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ regexis024_build_system.sh Помимо самого бинарника нужен файл с настройками сервиса. Формат настроек: JSON. Комментарии не поддерживаются. Пример такого файла находится в example/config.json. -Вместе с бинарным фалом так же распространяются ассеты, необъходимые для работы сайта. -Их можно найти в папке assets. В настроках (поле `["assets"]`) указывается путь до +Вместе с бинарным фалом так же распространяются ассеты, необходимые для работы сайта. +Их можно найти в папке assets. В настроках (поле `config.assets`) указывается путь до папки с ассетами. Путь может быть как абсолютным, так и относительным к рабочей директории. -Поле настроек `["database"]` указывает как соединиться с базой данных. -Поддерживается только база данных sqlite. Поддерживается только хранение в файле. -Поле `["database"]["file"]` указывает путь где хранится sqlite база данных. +Поле настроек `config.database` указывает как соединиться с базой данных. +Поддерживается только база данных sqlite3. Поддерживается только хранение в файле. +Поле `config.database.file` указывает путь где хранится sqlite база данных. Перед тем как использовать сервис нужно его проинициализировать (а точнее проинициализировать базу данных): @@ -60,6 +60,36 @@ regexis024_build_system.sh Утилита `iu9-ca-web-chat-admin-cli` позволяет администратору сервиса контролировать его через сокет (адрес указан в `config["server"]["admin-command-listen"]`). +По адресам `config.server.admin-command-listen` идёт прослушивание так называемых "команд администратора". +iu9cawebchat определяет свой простой протокол для передачи этих команд. +Утилита iu9-ca-web-chat-admin-cli может отправить текст с некой командой на сервер на этот адрес и получить +ответ от сервера. + +```shell +iu9-ca-web-chat-admin-cli [ ...] +``` + +Дополнительные параметры конкатенируются, разделяясь переводом строки. +Команды администратора: + +`updateroopw ` - сменить пароль пользователя с номером 0 + +`adduser ` - зарегистрировать пользователя сайта + +`8` - остановить сервис + +Если нужно ввести пробел или символ `\ ` в любое из этих полей, перед ними нужно поставить `\ `; +Если указать меньше полей, чем нужно, незаполненные поля станут пустыми строками. + +Параметры конфигурации `config.lang.whitelist` и `config.lang.force-order` определяют на +какие языки будет локализован сервер, и какие переводы приоритетнее каких. +На данный момент поддерживаются + - `ru-RU` + - `en-US` + +Все переводы хранятся в папке `assets/lang`. Для добавления своего перевода нужно форкнуть репозиторий и +сделать копию файла `assets/lang/ru-RU.lang.json` в `assets/lang/XXXXX.lang.json`. + # Список участников 1. [Китанин Фёдор](https://gitflic.ru/user/fed-kit) diff --git a/assets/HypertextPages/chat-members.nytl.html b/assets/HypertextPages/chat-members.nytl.html new file mode 100644 index 0000000..3896ac2 --- /dev/null +++ b/assets/HypertextPages/chat-members.nytl.html @@ -0,0 +1,61 @@ +{% ELDEF main JSON pres JSON userinfo JSON openedchat JSON initial_chatUpdResp %} + + + + + + + + + + {%w pres.chat-members.members-of %} {%w openedchat.name %} + + + {% PUT chat.pass pres userinfo openedchat initial_chatUpdResp %} + + + + + +
+ + +
+ New chat +
+ +
+
+
+ + + + + + +{% ENDELDEF %} diff --git a/assets/HypertextPages/chat.nytl.html b/assets/HypertextPages/chat.nytl.html index cc6617b..904c3d5 100644 --- a/assets/HypertextPages/chat.nytl.html +++ b/assets/HypertextPages/chat.nytl.html @@ -1,40 +1,68 @@ -{% ELDEF main JSON pres JSON userinfo %} +{% ELDEF pass JSON pres JSON userinfo JSON openedchat JSON initial_chatUpdResp %} + +{% ENDELDEF %} + +{% ELDEF main JSON pres JSON userinfo JSON openedchat JSON initial_chatUpdResp %} - + - Веб-Чат + + + + + {%w pres.chat.header-chat %} {%w openedchat.name %} -
-
- Веб чат - + {% PUT chat.pass pres userinfo openedchat initial_chatUpdResp %} + + -
- -
- -
-
-
-
- × -

Все участники

+ +
+ -
-
    - -
+
+
+
+
+
+
+ Loading backward... +
+
+ Loading forward... +
+
+
+
+ + +
-
- {% ENDELDEF %} diff --git a/assets/HypertextPages/edit-profile.nytl.html b/assets/HypertextPages/edit-profile.nytl.html new file mode 100644 index 0000000..04264da --- /dev/null +++ b/assets/HypertextPages/edit-profile.nytl.html @@ -0,0 +1,71 @@ +{% ELDEF main JSON pres JSON userinfo JSON alienprofile JSON errors %} + + + + + + + + + {%w pres.edit-profile.header-profile-of %} {%w alienprofile.name %} + + + +
+ + + {% FOR error IN errors %} +
+ {% W error.text %} +
+ {% ENDFOR %} + +
+

{% W alienprofile.name %}

+

{%w pres.edit-profile.directive-nickname %} {% W alienprofile.nickname %}

+

+ {% W alienprofile.bio %} +

+
+ +
+

{%w pres.edit-profile.change-user-attributes %}

+
+ + + + + + + + + +
+ + + +
+ + + +
+ +
+ + +
+
+
+ + + +{% ENDELDEF%} diff --git a/assets/HypertextPages/err-404.html b/assets/HypertextPages/err-404.html new file mode 100644 index 0000000..3afea8b --- /dev/null +++ b/assets/HypertextPages/err-404.html @@ -0,0 +1,11 @@ + + + + + + Not found + + +

Page not found

+ + diff --git a/assets/HypertextPages/list-rooms.nytl.html b/assets/HypertextPages/list-rooms.nytl.html index ddc4923..3b2f8fb 100644 --- a/assets/HypertextPages/list-rooms.nytl.html +++ b/assets/HypertextPages/list-rooms.nytl.html @@ -1,68 +1,74 @@ -{% ELDEF main JSON pres JSON userinfo %} +{% ELDEF main JSON pres JSON userinfo JSON initial_chatListUpdResp %} - + - Список Чат-Комнат + {%w pres.list-rooms.header %} + + + -{% PUT pass-pres-userinfo pres userinfo %} -
-

Выберите Чат-Комнату

-
    - -
- -
+ + - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + +
+ New chat +
+ +
+
+
+ + + -{% ENDELDEF %} \ No newline at end of file +{% ENDELDEF %} diff --git a/assets/HypertextPages/login.nytl.html b/assets/HypertextPages/login.nytl.html index 674f42b..2283ccd 100644 --- a/assets/HypertextPages/login.nytl.html +++ b/assets/HypertextPages/login.nytl.html @@ -1,26 +1,43 @@ -{% ELDEF main JSON pres JSON userinfo %} +{% ELDEF main JSON pres JSON userinfo JSON errors %} - + - {% WRITE pres.phr.decl.page-login %} + + - + {% W pres.login.header %} - -{% PUT pass-pres-userinfo pres userinfo %} -
-

{% WRITE pres.phr.decl.enter %}

-
- -
- -
- -
-
+ {% FOR error IN errors %} +
+ {% W error.text %} +
+ {% ENDFOR %} + +
+

{% W pres.login.header %}

+
+ + + + + + + + + +
+ +
+ +
+ +
+
diff --git a/assets/HypertextPages/pass-pres-userinfo.nytl.html b/assets/HypertextPages/pass-pres-userinfo.nytl.html deleted file mode 100644 index 8331b75..0000000 --- a/assets/HypertextPages/pass-pres-userinfo.nytl.html +++ /dev/null @@ -1,6 +0,0 @@ -{% ELDEF main JSON pres JSON userinfo %} - -{% ENDELDEF %} diff --git a/assets/HypertextPages/profile.nytl.html b/assets/HypertextPages/profile.nytl.html deleted file mode 100644 index 5c9505a..0000000 --- a/assets/HypertextPages/profile.nytl.html +++ /dev/null @@ -1,39 +0,0 @@ -{% ELDEF main JSON pres JSON userinfo %} - - - - - - Профиль - - -
-
-

Профиль пользователя

- Назад -
-
-
-
-
- -
-
-
-
-
-
-
-

О себе

-
- -
- -
-
- - - - - -{% ENDELDEF%} diff --git a/assets/HypertextPages/register.nytl.html b/assets/HypertextPages/register.nytl.html new file mode 100644 index 0000000..70d2fe6 --- /dev/null +++ b/assets/HypertextPages/register.nytl.html @@ -0,0 +1,51 @@ +{% ELDEF main JSON pres JSON userinfo JSON messages %} + + + + + + + + + {% W pres.register.header %} + + + {% FOR error IN messages %} +
+ {% W error.text %} +
+ {% ENDFOR %} + +
+

{% W pres.register.header %}

+
+ + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+
+ + + +{%ENDELDEF%} \ No newline at end of file diff --git a/assets/HypertextPages/registration.nytl.html b/assets/HypertextPages/registration.nytl.html deleted file mode 100644 index e9810ba..0000000 --- a/assets/HypertextPages/registration.nytl.html +++ /dev/null @@ -1,27 +0,0 @@ -{% ELDEF main JSON pres JSON userinfo %} - - - - - - Страница Регистрации - - - - - -
-

Вход

-
-
-
-
- -
-
-
- - - - -{% ENDELDEF %} diff --git a/assets/HypertextPages/view-profile.nytl.html b/assets/HypertextPages/view-profile.nytl.html new file mode 100644 index 0000000..34598d0 --- /dev/null +++ b/assets/HypertextPages/view-profile.nytl.html @@ -0,0 +1,36 @@ +{% ELDEF main JSON pres JSON userinfo JSON alienprofile %} + + + + + + + + + {%w pres.view-profile.header-profile-of %} {%w alienprofile.name %} + + + +
+ + +
+

{%w alienprofile.name %}

+

{%w pres.view-profile.directive-nickname%} {%w alienprofile.nickname %}

+

+ {%w alienprofile.bio %} +

+
+
+ + + + +{% ENDELDEF%} diff --git a/assets/css/chat-members.css b/assets/css/chat-members.css new file mode 100644 index 0000000..af0d1c6 --- /dev/null +++ b/assets/css/chat-members.css @@ -0,0 +1,33 @@ +#CM-btn-add { + margin-top: 6px; + margin-bottom: 4px; + display: none; +} + +.CM-member-box { + display: flex; + flex-direction: row; +} + +.CL-member-box-nickname { + margin-left: 8px; + justify-self: flex-start; +} + +.CM-member-box-name { + margin-left: 14px; + justify-self: flex-start; +} + +.CM-member-box-role { + margin-left: auto; + justify-self: flex-end; +} + +.CM-member-box-leave-btn { + margin-left: 10px; + margin-right: 8px; + justify-self: flex-end; + width: 16px; + cursor: pointer; +} diff --git a/assets/css/chat.css b/assets/css/chat.css index abafc9a..3317915 100644 --- a/assets/css/chat.css +++ b/assets/css/chat.css @@ -1,202 +1,143 @@ -body { - font-family: Arial, sans-serif; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - margin: 0; - background-color: #e5e5e5; -} - -.chat-container { - width: 100%; - max-width: 800px; - height: 90vh; - background-color: white; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - border-radius: 8px; - overflow: hidden; -} - -.chat-header { - background-color: #007bb5; - color: white; - padding: 25px; - display: flex; - justify-content: center; - align-items: center; - position: relative; -} -.room-name { - position: absolute; - left: 50%; - font-size: 24px; -} -.members { - border: none; - position: absolute; - left: 80%; - border-radius: 10px; - cursor: pointer; - width: 150px; - background-color: #f7f7f7; - height: 25px; - transition: background-color 0.3s ease; -} -.members:hover { - background-color: #218838; -} -.chat-messages { - flex: 1; - padding: 15px; - overflow-y: auto; - background-color: #f7f7f7; -} - -.chat-message { - display: flex; - align-items: flex-start; - margin-bottom: 15px; -} - -.chat-message .avatar { - width: 40px; - height: 40px; - border-radius: 50%; - overflow: hidden; - margin-right: 10px; -} - -.chat-message .avatar img { - width: 100%; +body, html { height: 100%; - object-fit: cover; } -.chat-message .message-content { - max-width: 70%; +#chat-widget { + position: relative; + flex: 1; + background-color: #f1f1f1; + overflow: hidden; +} + +.message-supercontainer{ + position: absolute; + width: 100%; + left: 0; + /*background-color: rgba(150, 0, 100, 50);*/ + background-color: rgba(0, 0, 0, 0); + /*display: flex;*/ + /*flex-direction: row;*/ + /*justify-content: center;*/ +} + +.message-box{ + /*display: inline-block;*/ + padding: 5px; +} + +.message-box-mine { + margin-right: 5px; + margin-left: auto; + max-width: 400px; + border: 2px solid #82a173; + padding: 5px; + background-color: #cdff9b; + color: black; + /*justify-self: flex-end;*/ +} + +.message-box-alien { + margin-left: 5px; + margin-right: auto; + max-width: 400px; + border: 2px solid dimgrey; + padding: 5px; background-color: white; - padding: 10px; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + color: black; + /*justify-self: flex-start;*/ } -.chat-message .message-content .username { +/* Only non-system messages can be deleted. Deleted messages do not have delete button + This class should be used with (and, ofcourse, after) class message-box-my/message-box-alien */ +.message-box-deleted { + border: 2px solid #cb0005; + background-color: #ffc1bc; +} + +.message-box-deleted .message-box-msg{ font-weight: bold; - margin-bottom: 5px; } -.chat-message .message-content .text { +.message-box-system { + margin-left: auto; + margin-right: auto; + max-width: 500px; + padding: 4px; + background-color: #2d2d2d; + color: white; + font-weight: bold; + justify-self: center; +} + +/* in #chat-widget .message-box */ +.message-box-top{ + /* You see, each message contains a 20+2+2 px high icon that HAS TO BE LOADED FIRST. + This happens after window.onload, so I added a crutch: loading won't update height in + unpredictable moment. cause it will be already high enough. BUGA-GA-GA!! */ + min-height: 30px; + display: block; +} + +.message-box-sender-name{ + color: black; + text-decoration: none; + padding: 2px; + display: inline; + font-size: 0.8em; +} + +/* Additional to message-box-sender-name */ +.message-box-sender-shortname { + font-weight: bold; + padding-left: 3px; + font-size: 0.94em; +} + +.message-box-sender-name:hover{ + color: #1060ff +} + +.message-box-button{ + width: 20px; + padding: 2px; + cursor: pointer; + display: inline; +} + +.message-box-msg{ word-wrap: break-word; } -.chat-footer { - display: flex; +#input-panel { + min-height: 20px; +} + +#message-input { padding: 15px; - padding-left: 50px; - border-top: 1px solid #ddd; -} - -.chat-input { - flex: 1; - padding: 10px; - border: 1px solid #ddd; - border-radius: 20px; - margin-right: 10px; - outline: none; -} - -.chat-send-button { - padding: 10px 20px; - border: none; - background-color: #0088cc; - color: white; - border-radius: 20px; - cursor: pointer; - outline: none; -} -.members-list { - display: none; - position: fixed; - background-color: #fff; - margin: 10% auto; - padding: 20px; - border: 1px solid #888; - width: 80%; - max-width: 400px; - border-radius: 10px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); -} -.members-list-header { - display: flex; -} -.all-members { - position: absolute; - left: 32%; - top: 0%; - margin-bottom: 30px; - font-family: Arial, sans-serif; -} -.close { - position: absolute; - right: 5%; - font-size: 24px; - font-weight: bold; -} -.members-list span { - cursor: pointer; -} -.members-list-body ul { - list-style-type: none; - left: 0%; -} -.members-list-body img { - margin-top: 10px; - left: 0%; - height: 30px; - width: 30px; - border-radius: 50%; -} -.members-list-body a { - margin-left: 5px; - margin-top: 10px; - text-decoration: none; - color: black; - -} -.members-list-body a:hover { - text-decoration: underline; - color: #0088cc; -} -.members-list-body button { - padding: 5px 10px; - border: none; - background-color: #dc2e45; - color: white; - border-radius: 20px; - position: absolute; - left: 300px; - margin-top: 20px; - cursor: pointer; - transition: background-color 0.3s ease; -} -.members-list-body button:hover { - background-color: #881527; -} -.overlay { - display: none; - position: fixed; - top: 0; - left: 0; + height: auto; width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - justify-content: center; - align-items: center; - z-index: 1000; + display: inline-block; + background-color: white; + border: 1px solid #1000d0; + border-radius : 7px; + font-size: .9rem; + margin: 10px; } -.chat-send-button:hover { - background-color: #007bb5; + +.message-in-popup-preview{ + border: 4px solid red; + width: 80%; + max-width: 200px; + margin-left: auto; + margin-right: auto; + max-height: 20%; + word-wrap: break-word; +} + +.loading-spinner{ + margin-left: auto; + margin-right: auto; + background-color: rgba(0, 0, 0, 0); + width: 25px; + display: block; } \ No newline at end of file diff --git a/assets/css/common-popup.css b/assets/css/common-popup.css new file mode 100644 index 0000000..c9dec24 --- /dev/null +++ b/assets/css/common-popup.css @@ -0,0 +1,50 @@ +.popup-overlay-veil { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + + z-index: 99; + display: none; /* Hidden by default */ +} + +.popup-window { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + + z-index: 100; + display: none; +} + +.popup-btn { + display: inline; + padding: 5px; + border-bottom: 3px; +} + +.popup-window-btn-yes { + background-color: #0c7f0e; + border-radius: 5px; + padding: 12px; + color: white; +} + +.popup-window-btn-no { + background-color: #ff0005; + border-radius: 5px; + padding: 12px; + color: white; +} + +.popup-window-msg { + padding-left: 20px; + font-weight: bold; + font-size: 1.3em; +} \ No newline at end of file diff --git a/assets/css/common.css b/assets/css/common.css new file mode 100644 index 0000000..b5f7406 --- /dev/null +++ b/assets/css/common.css @@ -0,0 +1,206 @@ +/* Profile view elements */ +.profile-container { + background: white; + border-radius: 5px; + padding: 20px; + margin-top: 60px; /* Space below the fixed panel */ + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.3); +} + +.profile-name-text { + color: black; +} + +.profile-nickname-text{ + color: #444; + text-align: left; +} + +.profile-bio-text { + padding-top: 40px; + text-align: left; + line-height: 1.6; + color: black; +} + +/* Panels */ +.panel { + width: 100%; + border: 2px solid blue; + background-color: #54b3ff; + display: flex; + flex-direction: row; + align-items: center; +} + +.panel-thing { + padding: 6px; +} + +.panel-header-txt{ + color: white; + font-size: 1.9em; + flex: 1; + text-align: center; +} + + +/* Containers for the whole document */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: Arial, sans-serif; +} + +.document-container { + width: 80%; /* Full width of the viewport */ + margin: 0 auto; /* Center the container horizontally */ +} + +.fullscreen-container { + width: 80%; /* Full width of the viewport */ + height: 100vh; /* Full height of the viewport */ + display: flex; + flex-direction: column; /* Stack children vertically */ + margin: 0 auto; /* Center the container horizontally */ +} + +@media (orientation: landscape) { + .resp-container{ + width: 80%; + } +} + +@media (orientation: portrait){ + .resp-container{ + width: 100%; + } +} + +body { + background-color: #f000f0; + background-image: url("/assets/img/clavicle-transparent.png"), url("/assets/img/broken-clavicle.png"); + background-repeat: revert; + background-size: 10%, 25%; +} + +/* Notifications, returned from server and embedded into html page at render-time */ + +.server-notif-error-msg-box{ + font-size: 1.3em; + text-align: center; + padding: 10px; + border: 2px solid red; + border-radius: 30px; + background-color: #ff5050; + max-width: 40%; + margin: 15px auto; +} + +/* Centered headers */ + +.wide-centered-header { + width: 100%; + text-align: center; + font-size: 1.4em; +} +/* Cool buttons with text */ + +.action-button { + padding: 10px 15px; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + + +.action-button:hover { + background-color: #0056b3; /* Darker blue on hover */ +} + +/* This is for centering non-100%wide block */ + +.centered-block-el { + display: block; + margin-left: auto; + margin-right: auto; +} + +/* Beautiful text input */ + +.one-line-input { + width: 100%; + padding: 8px; + margin: 8px 0; + border: 1px solid #ccc; + border-radius: 4px; +} + +.multiline-input { + width: 100%; + /*max-width: 600px;*/ + height: 200px; + padding: 10px; + font-size: 1.15em; + border: 2px solid #ccc; + border-radius: 5px; + box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); /* Subtle shadow */ + outline: none; /* Remove default outline on focus */ + resize: vertical; /* Allow resizing vertically */ + transition: border-color 0.15s, box-shadow 0.15s; /* Smooth transition for border color and shadow */ +} + +.multiline-input:focus { + border-color: #007bff; /* Change border color on focus */ + box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Shadow on focus */ +} + +/* Handles the case of list of elements with dickanme, name, role and delete button + For list of chats and list of users in chat */ +.dynamic-block-list { + margin-top:12px; + display: flex; + flex-direction: column; + background-color: white; + border: 1px solid #c7c7c7; + align-items: stretch; + padding-left: 8px; + padding-right: 8px; + padding-bottom: 8px; +} + +.dynamic-block-list-el { + margin-top: 8px; + background-color: white; + border: 1px solid #c7c7c7; + color: black; + padding: 5px; +} + +.button-add{ + width: 50px; + cursor: pointer; +} + +.dynamic-block-list-el-container{ + width: 100%; +} + +.entity-nickname-txt { + font-weight: bold; + color: black; + text-decoration: none; + font-size: 1.5em; +} + +.entity-reg-field-txt { + /* For name and role */ + color: #242424; + text-decoration: none; + font-size: 1.5em; +} diff --git a/assets/css/debug.css b/assets/css/debug.css new file mode 100644 index 0000000..017070f --- /dev/null +++ b/assets/css/debug.css @@ -0,0 +1,8 @@ +.chat-debug-rect{ + width: 100%; + position: absolute; + left: 0; + opacity: 0.3; + height: 3px; + z-index: 2; +} \ No newline at end of file diff --git a/assets/css/edit-profile.css b/assets/css/edit-profile.css new file mode 100644 index 0000000..ae0932d --- /dev/null +++ b/assets/css/edit-profile.css @@ -0,0 +1,23 @@ +/* The morbid thing */ +table.logins-input-table { + width: 100%; + border-collapse: collapse; /* Combine borders */ +} +.logins-input-td1, .logins-input-td2 { + border: none; +} +.logins-input-td1 { + text-align: left; + padding-right: 5px; + white-space: nowrap; /* Prevent text wrap, keeping it in one line */ + overflow: hidden; /* Hide overflow content */ + text-overflow: ellipsis; /* Show ellipsis for overflowing text */ +} +.logins-input-td2 { + width: 100%; +} + +#input-change-bio{ + margin-top: 5px; + margin-bottom: 5px; +} diff --git a/assets/css/list-rooms.css b/assets/css/list-rooms.css index 0fa8ff2..a3941b7 100644 --- a/assets/css/list-rooms.css +++ b/assets/css/list-rooms.css @@ -1,253 +1,166 @@ +/* Общие стили */ body { - font-family: Arial, sans-serif; - background-color: #f0f0f0; + font-family: 'Roboto', sans-serif; + background-color: #f7f9fc; + color: #333; margin: 0; padding: 0; + box-sizing: border-box; } -.container { - max-width: 800px; - margin: 30px auto; - padding: 20px; +/* Панель навигации */ +.panel { background-color: #007bff; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - border-radius: 8px; -} - -h1 { - text-align: center; - color: #fff; -} - -.room-list { - list-style-type: none; - padding: 0; -} - -.room-item { + padding: 10px; + color: white; display: flex; - justify-content: space-between; align-items: center; - padding: 15px; - margin: 10px 0; - background-color: #fafafa; - border: 1px solid #ddd; - border-radius: 5px; - transition: background-color 0.3s ease; } -.room-item:hover { - background-color: #eaeaea; +.panel-thing { + margin-right: 20px; + text-decoration: none; + color: white; } -.room-name { +.panel-header-txt { font-size: 18px; - color: #555; + font-weight: bold; } -.join-button { - padding: 10px 15px; - font-size: 16px; - color: white; - background-color: #007bff; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease; -} -.add-members-header { - text-align: center; -} -.add-members-footer { - text-align: right; - margin-top: 5px; -} -.add-members-button { - background-color: #218838; - padding: 10px 15px; - font-size: 16px; - color: white; - border: none; - border-radius: 5px; - position: absolute; - margin-left: 502px; - cursor: pointer; - transition: background-color 0.3s ease; -} -.add-members-button:hover { - background-color: #006509 -} -.delete-chat-button { - background-color: #dc2e45; - border: none; - color: white; - font-size: 16px; - border-radius: 5px; - position: absolute; - cursor: pointer; - transition: background-color 0.3s ease; - padding: 10px 15px; - margin-left: 380px; -} -.delete-chat-button:hover { - background-color: #881527; -} -#newMemberLogin { - width: 93.5%; - padding: 10px; - margin: 10px 0; - border: 1px solid #ddd; - border-radius: 5px; -} -.add-member-button { - background-color: #218838; - padding: 10px 15px; - font-size: 16px; - color: white; - border: none; - border-radius: 5px; - position: absolute; - margin-left: -105px; - cursor: pointer; - transition: background-color 0.3s ease; -} -.join-button:hover { - background-color: #0056b3; -} - -.modal { - display: none; - position: fixed; - z-index: 1; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0, 0, 0, 0.4); -} - -.modal-content { - background-color: #fff; - margin: 10% auto; +/* Стили динамических блоков */ +.dynamic-block-list { + display: flex; + flex-direction: column; + align-items: center; padding: 20px; - border: 1px solid #888; - width: 80%; - max-width: 400px; - border-radius: 10px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); } -.modal-header, .modal-footer { - padding: 10px; - color: #333; -} - -.modal-header { - text-align: center; -} - -.modal-footer { - text-align: right; -} - -.modal input { - width: 93.5%; - padding: 10px; - margin: 10px 0; - border: 1px solid #ddd; - border-radius: 5px; -} - -.create-room-button { - display: block; +.dynamic-block-list-el-container { width: 100%; - padding: 10px; - font-size: 16px; - color: white; - background-color: #1609ab; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease; + max-width: 600px; margin-top: 20px; } -.create-room-button:hover { - background-color: #218838; +/* Кнопка добавления */ +.button-add { + width: 50px; + height: 50px; + cursor: pointer; + transition: transform 0.2s; } -.overlay { - display: none; - position: fixed; - top: 0; - left: 0; +.button-add:hover { + transform: scale(1.1); +} + +/* Всплывающие окна */ +.popup-window { + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + padding: 20px; + max-width: 500px; + margin: 0 auto; +} + +.popup-window-msg { + font-size: 20px; + margin-bottom: 15px; + color: #333; +} + +.popup-window-btn-yes, .popup-window-btn-no { + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 16px; + cursor: pointer; +} + +.popup-window-btn-yes { + background-color: #28a745; + color: white; + margin-right: 10px; +} + +.popup-window-btn-no { + background-color: #dc3545; + color: white; +} + +/* Таблица ввода */ +table.id-str-input-table { width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - justify-content: center; + border-collapse: collapse; + margin-bottom: 20px; +} + +.id-str-input-td1, .id-str-input-td2 { + border: none; + padding: 10px; +} + +.id-str-input-td1 { + text-align: left; + padding-right: 10px; + font-weight: bold; + color: #555; + white-space: nowrap; +} + +.id-str-input-td2 { + width: 100%; +} + +.one-line-input { + width: 100%; + padding: 8px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; +} + +/* Стили комнат */ +.CL-my-chat-box { + display: flex; + flex-direction: row; + position: relative; + padding: 10px; + background-color: #e0f7fa; + border-radius: 8px; + margin-bottom: 10px; align-items: center; - z-index: 1000; + min-height: 40px; + width: 100%; } -.overlay .add-members { - background-color: white; - padding: 30px; - border-radius: 10px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); - max-width: 400px; - width: 100%; - height: 18%; - position: fixed; +/* Текст внутри блока комнаты */ +.CL-my-chat-box-nickname, .CL-my-chat-box-name, .CL-my-chat-box-my-role { + margin-left: 8px; + justify-self: flex-start; } -.overlay .delete-chat { - background-color: white; - padding: 30px; - border-radius: 10px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); - max-width: 400px; - width: 100%; - height: 18%; - position: fixed; -} -.delete-close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - cursor: pointer; -} -.delete-chat-header { - text-align: center; -} -.confirm { - background-color: #1609ab; - padding: 20px 70px; - font-size: 16px; - color: white; - border: none; - border-radius: 5px; + +/* Крестик в правом верхнем углу */ +.CL-my-chat-box-leave-btn { position: absolute; - margin-left: 20px; + top: 8px; + right: 8px; + width: 16px; + height: 16px; + background: url('/assets/img/close.svg') no-repeat center; + background-size: cover; cursor: pointer; - transition: background-color 0.3s ease; -} -.cancel { - background-color: #1609ab; - padding: 20px 70px; - font-size: 16px; - color: white; border: none; - border-radius: 5px; - position: absolute; - margin-left: 220px; - cursor: pointer; - transition: background-color 0.3s ease; + transition: transform 0.2s; +} + +.CL-my-chat-box-leave-btn:hover { + transform: scale(1.2); +} + +/* Дизайн списка комнат остается таким же */ +#CL-bacbe { + margin-top: 6px; + margin-bottom: 4px; } -.close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - cursor: pointer; -} \ No newline at end of file diff --git a/assets/css/login.css b/assets/css/login.css index b42c992..27971b4 100644 --- a/assets/css/login.css +++ b/assets/css/login.css @@ -1,77 +1,46 @@ -dy { - font-family: Arial, sans-serif; +body { display: flex; + flex-direction: column; justify-content: center; align-items: center; - height: 100vh; + height: 100vh; /* Full viewport height */ margin: 0; - background-color: #e5e5e5; } .form-container { - width: 100%; - max-width: 400px; - background-color: white; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - border-radius: 8px; - padding: 40px; - text-align: center; + background-color: #ffffff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); } -h1 { - margin-bottom: 20px; - color: #2F4F4F; +@media (orientation: landscape) { + .form-container{ + width: 50%; + } } -input { - width: 100%; - background: #f7f7f7; - font-size: 16px; - padding: 10px; - border: 1px solid #ddd; - border-radius: 20px; - margin-bottom: 15px; - outline: none; +@media (orientation: portrait){ + .form-container{ + width: 85%; + } } -button { + +/* The morbid thing */ +table.logins-input-table { width: 100%; - padding: 15px; + border-collapse: collapse; /* Combine borders */ +} +.logins-input-td1, .logins-input-td2 { border: none; - background-color: #0088cc; - color: white; - border-radius: 20px; - cursor: pointer; - outline: none; - font-size: 16px; - font-weight: bold; - transition: background-color 0.3s; } - -button:hover, -button:focus-visible { - background-color: #007bb5; +.logins-input-td1 { + padding-right: 5px; + white-space: nowrap; /* Prevent text wrap, keeping it in one line */ + overflow: hidden; /* Hide overflow content */ + text-overflow: ellipsis; /* Show ellipsis for overflowing text */ } - -.hide-cursor::placeholder { - color: #000; -} - -.hide-cursor { - caret-color: transparent; -} - -.no-select { - -webkit-user-select: none; /* Для Safari */ - -moz-user-select: none; /* Для Firefox */ - user-select: none; /* Для всех остальных браузеров */ -} - -div { - color: red; - font-size: 15px; - margin-top: 10px; - display: none; +.logins-input-td2 { + width: 100%; } diff --git a/assets/css/profile.css b/assets/css/profile.css deleted file mode 100644 index c60dbf5..0000000 --- a/assets/css/profile.css +++ /dev/null @@ -1,129 +0,0 @@ -body { - display: flex; - justify-content: center; - align-items: center; - height: 90vh; - background-color: #e5e5e5; - font-family: Arial, sans-serif; -} -.main-container { - width: 700px; - height: 700px; - border-color: antiquewhite; - background-color: white; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - border-radius: 10px; -} -.profile-header { - width: 700px; - height: 160px; - border-color: antiquewhite; - background-color: #0088cc; - border-radius: 10px; - position: relative; -} -.return { - background-color: #f0f0f0; - cursor: pointer; - width: 100px; - text-decoration: none; - color: black; - display: flex; - justify-content: center; - align-items: center; - height: 30px; - border-radius: 10px; - position: absolute; - left: 20px; - top: 25px; - border: none; -} -.return:hover{ - text-decoration: underline; - color: #0088cc; -} -form { - display: flex; - flex-direction: column; - align-items: center; -} - -.columns { - display: flex; - justify-content: center; - align-items: flex-start; - gap: 20px; - margin-bottom: 20px; -} - -.column { - display: flex; - flex-direction: column; - align-items: center; -} -.add { - width: 100px; - height: 40px; - border-width: 2px; - cursor: pointer; - font-size: 16px; - border-radius: 10px; -} -.add:hover { - background-color: #007bb5; -} -.image-button:hover { - opacity: 0.8; -} - -.image-button:active { - transform: scale(0.95); -} -#login { - font-family: Arial, sans-serif; - font-size:16px; - width: 150px; - height: 20px; - border-radius: 10px; - border-color: #2F4F4F; -} -#username { - font-family: Arial, sans-serif; - font-size:16px; - width: 150px; - height: 20px; - margin-bottom: 1px; - margin-top: 50px; - border-radius: 10px; - border-color: #2F4F4F; -} -#bio { - height: 150px; - width: 500px; - padding: 10px; - box-sizing: border-box; - font-family: Arial, sans-serif; - font-size:14px; - text-align: left; - vertical-align: top; - margin-bottom: 5px; -} -.save { - cursor:pointer; - font-size: 16px; - border-radius: 15px; - border-color: #2F4F4F; - height: 40px; - width: 150px; -} -.save:hover { - background-color: #007bb5; -} -.avatar { - border-radius: 50%; - object-fit: cover; -} \ No newline at end of file diff --git a/assets/css/register.css b/assets/css/register.css new file mode 100644 index 0000000..3a4893c --- /dev/null +++ b/assets/css/register.css @@ -0,0 +1,45 @@ +body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; /* Full viewport height */ + margin: 0; +} + +.form-container { + background-color: #ffffff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); +} + +@media (orientation: landscape) { + .form-container{ + width: 60%; + } +} + +@media (orientation: portrait){ + .form-container{ + width: 90%; + } +} + +/* The morbid thing */ +table.reg-input-table { + width: 100%; + border-collapse: collapse; /* Combine borders */ +} +.reg-input-td1, .reg-input-td2 { + border: none; +} +.reg-input-td1 { + padding-right: 5px; + white-space: nowrap; /* Prevent text wrap, keeping it in one line */ + overflow: hidden; /* Hide overflow content */ + text-overflow: ellipsis; /* Show ellipsis for overflowing text */ +} +.reg-input-td2 { + width: 100%; +} diff --git a/assets/css/registration.css b/assets/css/registration.css deleted file mode 100644 index b42c992..0000000 --- a/assets/css/registration.css +++ /dev/null @@ -1,77 +0,0 @@ -dy { - font-family: Arial, sans-serif; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - margin: 0; - background-color: #e5e5e5; -} - -.form-container { - width: 100%; - max-width: 400px; - background-color: white; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - border-radius: 8px; - padding: 40px; - text-align: center; -} - -h1 { - margin-bottom: 20px; - color: #2F4F4F; -} - -input { - width: 100%; - background: #f7f7f7; - font-size: 16px; - padding: 10px; - border: 1px solid #ddd; - border-radius: 20px; - margin-bottom: 15px; - outline: none; -} - -button { - width: 100%; - padding: 15px; - border: none; - background-color: #0088cc; - color: white; - border-radius: 20px; - cursor: pointer; - outline: none; - font-size: 16px; - font-weight: bold; - transition: background-color 0.3s; -} - -button:hover, -button:focus-visible { - background-color: #007bb5; -} - -.hide-cursor::placeholder { - color: #000; -} - -.hide-cursor { - caret-color: transparent; -} - -.no-select { - -webkit-user-select: none; /* Для Safari */ - -moz-user-select: none; /* Для Firefox */ - user-select: none; /* Для всех остальных браузеров */ -} - -div { - color: red; - font-size: 15px; - margin-top: 10px; - display: none; -} diff --git a/assets/gif/loading.gif b/assets/gif/loading.gif new file mode 100644 index 0000000..05e1f57 Binary files /dev/null and b/assets/gif/loading.gif differ diff --git a/assets/img/add.svg b/assets/img/add.svg new file mode 100644 index 0000000..e65c886 --- /dev/null +++ b/assets/img/add.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/assets/img/broken-clavicle.png b/assets/img/broken-clavicle.png new file mode 100644 index 0000000..6608e43 Binary files /dev/null and b/assets/img/broken-clavicle.png differ diff --git a/assets/img/clavicle-transparent.png b/assets/img/clavicle-transparent.png new file mode 100644 index 0000000..6fd8a9a Binary files /dev/null and b/assets/img/clavicle-transparent.png differ diff --git a/assets/img/delete.svg b/assets/img/delete.svg new file mode 100644 index 0000000..e169ed0 --- /dev/null +++ b/assets/img/delete.svg @@ -0,0 +1,18 @@ + + + + + diff --git a/assets/img/empty_avatar.png b/assets/img/empty_avatar.png deleted file mode 100644 index c1aa714..0000000 Binary files a/assets/img/empty_avatar.png and /dev/null differ diff --git a/assets/img/exit.png b/assets/img/exit.png new file mode 100644 index 0000000..1d89f99 Binary files /dev/null and b/assets/img/exit.png differ diff --git a/assets/img/favicon.png b/assets/img/favicon.png new file mode 100644 index 0000000..614d564 Binary files /dev/null and b/assets/img/favicon.png differ diff --git a/assets/img/link.svg b/assets/img/link.svg new file mode 100644 index 0000000..1bb5ef3 --- /dev/null +++ b/assets/img/link.svg @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/assets/img/list-rooms.svg b/assets/img/list-rooms.svg new file mode 100644 index 0000000..7499b7f --- /dev/null +++ b/assets/img/list-rooms.svg @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/assets/img/return.svg b/assets/img/return.svg new file mode 100644 index 0000000..f3a18d4 --- /dev/null +++ b/assets/img/return.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/assets/img/settings-iron.svg b/assets/img/settings-iron.svg new file mode 100644 index 0000000..50307c1 --- /dev/null +++ b/assets/img/settings-iron.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/assets/img/user.svg b/assets/img/user.svg new file mode 100644 index 0000000..228c197 --- /dev/null +++ b/assets/img/user.svg @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/assets/js/chat-members.js b/assets/js/chat-members.js new file mode 100644 index 0000000..cf9f42a --- /dev/null +++ b/assets/js/chat-members.js @@ -0,0 +1,188 @@ +let LocalHistoryId = 0; + +function genSentBase(){ + return { + 'chatUpdReq': { + 'LocalHistoryId': LocalHistoryId, + 'chatId': openedchat.id + } + }; +} + +let members = new Map(); +let memberBoxes = new Map(); +let myRoleHere = null; // Dung local state updates should be updated first + +let userDeletionWinStoredUId = -1; + +function shouldShowDeleteButton(memberSt){ + return userinfo.uid !== memberSt.userId && myRoleHere === userChatRoleAdmin && memberSt.roleHere !== userChatRoleDeleted; +} + +function updateBoxWithSt(box, memberSt){ + let ID = memberSt.userId; + let roleP = box.querySelector(".CM-member-box-role"); + roleP.innerText = memberSt.roleHere; + box.style.backgroundColor = roleToColor(memberSt.roleHere); + box.querySelector(".CM-member-box-leave-btn").style.display = + (shouldShowDeleteButton(memberSt) ? "block" : "none"); +} + +function convertMemberStToBox(memberSt){ + let ID = memberSt.userId; + let userProfileURI = "/user/" + memberSt.nickname; + + let box = document.createElement("div"); + box.className = "dynamic-block-list-el CM-member-box"; + box.style.backgroundColor = roleToColor(memberSt.roleHere); + + let inBoxNickname = document.createElement("a"); + box.appendChild(inBoxNickname); + inBoxNickname.className = "entity-nickname-txt CM-member-box-nickname"; + inBoxNickname.innerText = memberSt.nickname; + inBoxNickname.href = userProfileURI; + + let inBoxName = document.createElement("a"); + box.appendChild(inBoxName); + inBoxName.className = "entity-reg-field-txt CM-member-box-name"; + inBoxName.innerText = memberSt.name; + inBoxName.href = userProfileURI; + + let inBoxUserRoleHere = document.createElement("p"); + box.appendChild(inBoxUserRoleHere); + inBoxUserRoleHere.className = "entity-reg-field-txt CM-member-box-role"; + inBoxUserRoleHere.innerText = memberSt.roleHere; + + let inBoxLeaveBtn = document.createElement("img"); + box.appendChild(inBoxLeaveBtn); + inBoxLeaveBtn.className = "CM-member-box-leave-btn"; + inBoxLeaveBtn.src = "/assets/img/delete.svg"; + inBoxLeaveBtn.onclick = function (ev) { + if (ev.button !== 0) + return; + userDeletionWinStoredUId = ID; + document.getElementById("user-deletion-win-title").innerText = + pres['chat-members']['reask-kick-user-X'] + " " + memberSt.nickname + "?"; + activatePopupWindowById("user-deletion-win"); + }; + box.querySelector(".CM-member-box-leave-btn").style.display = + (shouldShowDeleteButton(memberSt) ? "block" : "none"); + + return box; +} + +function updateLocalStateFromChatUpdResp(chatUpdResp){ + LocalHistoryId = chatUpdResp.HistoryId; + // If my role is updated, we need to update all the boes of already set users (kick button can appear and disappear) + let literalMemberList = document.getElementById("CM-list"); + // We ignore messages and everything related to them. Dang, I really should add an argument to disable message lookup here + for (let memberSt of chatUpdResp.members){ + console.log([memberSt, userinfo.uid, myRoleHere]); + if (memberSt.userId === userinfo.uid && myRoleHere !== memberSt.roleHere){ + myRoleHere = memberSt.roleHere; + for (let [id, memberSt] of members){ + let box = memberBoxes.get(id); + updateBoxWithSt(box, memberSt); + } + document.getElementById("CM-btn-add").style.display = + (memberSt.roleHere === userChatRoleAdmin ? "block" : "none"); + console.log("DEBUG " + (memberSt.roleHere === userChatRoleAdmin ? "block" : "none")); + break; + } + } + for (let memberSt of chatUpdResp.members){ + let id = memberSt.userId; + if (members.has(id)){ + updateBoxWithSt(memberBoxes.get(id), memberSt); + } else { + if (memberSt.roleHere !== userChatRoleDeleted){ + members.set(id, memberSt); + let box = convertMemberStToBox(memberSt); + memberBoxes.set(id, box); + literalMemberList.appendChild(box); + } + } + } +} + +function updateLocalStateFromRecv(Recv){ + updateLocalStateFromChatUpdResp(Recv.chatUpdResp); +} + +function configureSummonUserInterface(){ + document.getElementById("user-summoning-yes").onclick = function(ev ){ + if (ev.button !==0) + return; + let nickname = String(document.getElementById("summoned-user-nickname").value); + let isReadOnly = document.getElementById("summoned-user-is-read-only").checked; + deactivateActivePopup(); + let Sent = genSentBase(); + Sent.nickname = nickname; + Sent.makeReadOnly = Boolean(isReadOnly); + apiRequest("addMemberToChat", Sent). + then((Recv) => { + updateLocalStateFromRecv(Recv); + }).catch((e) => { + console.log(e); + alert(pres['chat-members']["failed-summon-member"]); + }); + }; + + document.getElementById("user-summoning-no").onclick = function (ev) { + if (ev.button !== 0) + return; + deactivateActivePopup(); + }; + + document.getElementById("CM-btn-add").onclick = function(ev) { + if (ev.button !== 0) + return; + document.getElementById("summoned-user-nickname").value = ""; + // read-only flag persists throughout user summoning sessions, and IT IS NOT A BUG + activatePopupWindowById("user-summoning-win"); + }; +} + +/* Popup activation button is configured for each box separately */ +function configureKickUserInterfaceWinPart(){ + document.getElementById("user-deletion-yes").onclick = function (ev){ + if (ev.button !== 0) + return; + deactivateActivePopup(); + if (userDeletionWinStoredUId < 0) + throw new Error("Karaul"); + let Sent = genSentBase(); + Sent.userId = userDeletionWinStoredUId; + apiRequest("removeMemberFromChat", Sent). + then((Recv) => { + updateLocalStateFromRecv(Recv); + }).catch((e) => { + console.log(e); + alert(pres['chat-members']["failed-kick-member"]); + }); + } + + document.getElementById("user-deletion-no").onclick = function (ev) { + if (ev.button !== 0) + return; + deactivateActivePopup(); + }; +} + +__mainloopDelayMS = 5000; +__guestMainloopPollerAction = function (){ + console.log("Hello, world"); + apiRequest("chatPollEvents", genSentBase()). + then((Recv) => { + console.log(Recv); + updateLocalStateFromRecv(Recv); + }); +} + +window.onload = function(){ + console.log("Page loaded"); + configureSummonUserInterface(); + configureKickUserInterfaceWinPart(); + updateLocalStateFromChatUpdResp(initial_chatUpdResp); + mainloopPoller(); +} diff --git a/assets/js/chat.js b/assets/js/chat.js index 259c984..58ef64d 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -1,162 +1,477 @@ -let members = [ - { username: 'Адель', nickname: 'cold_siemens52', avatar: 'https://sun9-59.userapi.com/impg/t8GhZ7FkynVifY1FQCnaf31tGprbV_rfauZzgg/fSq4lyc6V0U.jpg?size=1280x1280&quality=96&sign=e3c309a125cb570d2e18465eba65f940&type=album' }, - { username: 'Антон', nickname: 'antyak_01', avatar: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png' }, - { username: 'Владимир', nickname: 'kkrkk2006', avatar: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png' } -]; -let currentHistoryId = 0; -let currentChatID = null; -function renderMembersList() { - const membersListBody = document.getElementById('members-list-body'); - membersListBody.innerHTML = ''; +let LocalHistoryId = 0; - members.forEach((member, index) => { - const memberItem = document.createElement('li'); - memberItem.innerHTML = ` - ${member.username} - ${member.username} - - `; - membersListBody.appendChild(memberItem); - }); -} +let members = new Map(); -function deleteMember(index) { - members.splice(index, 1); - renderMembersList(); -} +let loadedMessages = new Map(); // messageSt objects +/* +container: EL, box: EL, offset: number (msgPres) */ +let visibleMessages = new Map(); // HTMLElement objects -async function sendMessage() { - const chatMessages = document.getElementById('chat-messages'); - const chatInput = document.getElementById('chat-input'); - const message = chatInput.value; +let anchoredMsg = -1; +let visibleMsgSegStart = -1; +let visibleMsgSegEnd = -2; +let offsetOfAnchor = 500; +let highestPoint = null; +let lowestPoint = null; - if (message.trim() !== '') { - const request = { - 'chatId': currentChatID, - 'LocalHistoryId': currentHistoryId, - 'content': { - 'text': message - } - }; +let lastMsgId = -1; +let myRoleHere = null; // Dung local state updates should be updated first - const response = await fetch("/internalapi/sendMessage", { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }); +// Would start with true if opened `/chat/<>` +let bumpedAtBottom = false; - const res = await response.json(); +// Hidden variable. When deletion window popup is active +// Persists from popup activation until popup deactivation +let storeHiddenMsgIdForDeletionWin = -1; - if (res.update) { - const update = res.update[0]; - currentHistoryId = update.HistoryId; +let debugMode = false; - const messageElement = document.createElement('div'); - messageElement.classList.add('chat-message'); +// Positive in production, negative for debug +let softZoneSz = debugMode ? -150 : 300; +let chatPadding = debugMode ? 300 : 5; +let msgGap = 5; +const msgErased = pres.chat.msgErased; - const avatarElement = document.createElement('div'); - avatarElement.classList.add('avatar'); - - const avatarImage = document.createElement('img'); - avatarImage.src = 'https://sun9-59.userapi.com/impg/t8GhZ7FkynVifY1FQCnaf31tGprbV_rfauZzgg/fSq4lyc6V0U.jpg?size=1280x1280&quality=96&sign=e3c309a125cb570d2e18465eba65f940&type=album'; - avatarElement.appendChild(avatarImage); - - const messageContentElement = document.createElement('div'); - messageContentElement.classList.add('message-content'); - - const usernameElement = document.createElement('div'); - usernameElement.classList.add('username'); - usernameElement.textContent = await getUserName(); - - const textElement = document.createElement('div'); - textElement.classList.add('text'); - textElement.textContent = message; - - messageContentElement.appendChild(usernameElement); - messageContentElement.appendChild(textElement); - - messageElement.appendChild(avatarElement); - messageElement.appendChild(messageContentElement); - - chatMessages.appendChild(messageElement); - - chatInput.value = ''; - chatMessages.scrollTop = chatMessages.scrollHeight; - } - } -} - -function openMembersList() { - renderMembersList(); - document.getElementById("members-list").style.display = "block"; - document.getElementById("overlay").style.display = "flex"; -} - -function closeMembersList() { - document.getElementById("members-list").style.display = "none"; - document.getElementById("overlay").style.display = "none"; -} - -document.getElementById('chat-input').addEventListener('keydown', function (event) { - if (event.key === 'Enter') { - sendMessage(); - } -}); - -async function getUserID() { - const response = await fetch('/internalapi/mirror', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) - }); - - const res = await response.json(); - return res.id; -} -async function getChatID() { - const chatNickname = window.location.pathname.split('/').pop(); - const response = await fetch('/internalapi/getChatList', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) - }); - - const res = await response.json(); - for (const chat of res.chats) { - if (chat.content.nickname === chatNickname) { - return chat.id; - } - } - return -1; -} -async function editMessage(new_message) { - const req = { - 'chatId': currentChatID, - 'LocalHistoryId': currentHistoryId, - 'id': getUserID(), - 'content': { - 'text': new_message +function genSentBase(){ + return { + 'chatUpdReq': { + 'LocalHistoryId': LocalHistoryId, + 'chatId': openedchat.id } }; - const res = await fetch('/internalapi/editMessage', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(req) - }); +} - const response = await res.json(); - if (response.update) { - currentHistoryId = response.update[0].HistoryId; +function genSentBaseGMN(){ + let Sent = genSentBase(); + Sent.amount = debugMode ? 2 : 14; + return Sent; +} + +function getChatWgSz(){ + let chatWg = document.getElementById("chat-widget"); + return [chatWg.offsetWidth, chatWg.offsetHeight]; +} + +function elSetOffsetInChat(el, offset){ + el.style.bottom = String(offset) + "px"; +} + +function isMissingPrimaryMsgHeap(){ + return lastMsgId >= 0 && anchoredMsg < 0; +} + +function isMissingTopMsgHeap(){ + let [W, H] = getChatWgSz(); + return anchoredMsg >= 0 && (highestPoint < H + softZoneSz && visibleMsgSegStart > 0); +} + +function isMissingBottomMsgHeap(){ + return anchoredMsg >= 0 && (lowestPoint > - softZoneSz && visibleMsgSegEnd < lastMsgId); +} + +function updateOffsetOfVisibleMsg(msgId, offset){ + visibleMessages.get(msgId).container.style.bottom = String(offset) + "px"; +} + +function updateOffsetsUpToTop(){ + let offset = offsetOfAnchor; + for (let curMsg = anchoredMsg; curMsg >= visibleMsgSegStart; curMsg--){ + updateOffsetOfVisibleMsg(curMsg, offset); + let height = visibleMessages.get(curMsg).container.offsetHeight; + offset += height + msgGap; + } + return offset - msgGap; +} + +function updateOffsetsDown(){ + let offset = offsetOfAnchor; + for (let curMsg = anchoredMsg + 1; curMsg <= visibleMsgSegEnd; curMsg++){ + let height = visibleMessages.get(curMsg).container.offsetHeight; + offset -= (height + msgGap); + updateOffsetOfVisibleMsg(curMsg, offset); + } + return offset; +} + +function updateOffsetsSane(){ + if (anchoredMsg < 0) + return; + highestPoint = updateOffsetsUpToTop(); + lowestPoint = updateOffsetsDown(); +} + +function heightOfPreloadGhost(){ + let [W, H] = getChatWgSz(); + return Math.min(H * 0.9, Math.max(H * 0.69, 30)); +} + +function updateOffsets(){ + let spinnerTop = document.getElementById("top-loading"); + let spinnerBottom = document.getElementById("bottom-loading"); + let SbH = spinnerBottom.offsetHeight; + if (anchoredMsg < 0){ + hideHTMLElement(spinnerBottom); + elSetOffsetInChat(spinnerTop, chatPadding); + setElementVisibility(spinnerTop, isMissingPrimaryMsgHeap()); + } else { + let [W, H] = getChatWgSz(); + updateOffsetsSane(); + let lowestLowestPoint = isMissingBottomMsgHeap() ? lowestPoint - heightOfPreloadGhost(): lowestPoint; + let highestHighestPoint = isMissingTopMsgHeap() ? highestPoint + heightOfPreloadGhost() : highestPoint; + if (lowestLowestPoint > chatPadding || (highestHighestPoint - lowestLowestPoint) <= H - chatPadding * 2 || + (!isMissingBottomMsgHeap() && bumpedAtBottom)) { + + offsetOfAnchor += (-lowestLowestPoint + chatPadding); + updateOffsetsSane(); + } else if (highestHighestPoint < H - chatPadding) { + offsetOfAnchor += (-highestHighestPoint + (H - chatPadding)); + updateOffsetsSane(); + } + /* Messages weere updated (and only them). They were talking with ghosts. + Now we are trying to show spinners of ghosts */ + elSetOffsetInChat(spinnerTop, highestPoint); + setElementVisibility(spinnerTop, isMissingTopMsgHeap()); + elSetOffsetInChat(spinnerBottom, lowestPoint - SbH); + setElementVisibility(spinnerBottom, isMissingBottomMsgHeap()); + /* Fix anchor */ + let oldAnchor = anchoredMsg; + while (true){ + let h = visibleMessages.get(anchoredMsg).container.offsetHeight; + if (!(offsetOfAnchor + h < chatPadding && visibleMsgSegStart < anchoredMsg)) + break + offsetOfAnchor += (msgGap + h); + anchoredMsg--; + } + while (offsetOfAnchor > H - chatPadding && anchoredMsg < visibleMsgSegEnd){ + anchoredMsg++; + let h = visibleMessages.get(anchoredMsg).container.offsetHeight; + offsetOfAnchor -= (msgGap + h); + } + if (oldAnchor !== anchoredMsg) + console.log("anchoredMsg: " + String(oldAnchor) + " -> " + String(anchoredMsg)) } } -document.addEventListener("DOMContentLoaded", async function() { - currentChatID = await getChatID(); -}); \ No newline at end of file + +function shouldShowDeleteMesgBtn(messageSt){ + return !messageSt.isSystem && messageSt.exists && (myRoleHere !== userChatRoleReadOnly) &&( + myRoleHere === userChatRoleAdmin || messageSt.senderUserId === userinfo.uid); +} + +function getMsgTypeClassSenderBased(messageSt){ + if (messageSt.isSystem) + return "message-box-system" + if (messageSt.senderUserId === userinfo.uid) + return "message-box-mine" + return "message-box-alien"; +} + +function getMsgFullTypeClassName(messageSt){ + return getMsgTypeClassSenderBased(messageSt) + (messageSt.exists ? "" : " message-box-deleted"); +} + +/* Two things can be updated: messages existance and delete button visibility +* Supercontainer.container is persistent, Supercontainer.box can change it's class */ +function updateMessageSupercontainer(supercontainer, messageSt){ + let box = supercontainer.box; + if (messageSt.isSystem) + return; + setElementVisibility(box.querySelector(".message-box-button-delete"), shouldShowDeleteMesgBtn(messageSt), "inline"); + box.className = getMsgFullTypeClassName(messageSt); + // Notice, that no check of previous state is performed. Double loading is a rare event, I can afford to be slow + if (!messageSt.exists) + box.querySelector(".message-box-msg").innerText = msgErased; +} + +function decodeSystemMessage(text){ + let [subject, verb, object] = text.split(','); + let subjectId = Number(subject); + let objectId = Number(object); + let subjectRef = members.has(subjectId) ? members.get(subjectId).nickname : "???"; + let objectRef = members.has(objectId) ? members.get(objectId).nickname : "???"; + if (verb === "kicked"){ + return subjectRef + " " + pres.chat.syslog.kicked + " " + objectRef; + } else if (verb === "summoned"){ + return subjectRef + " " + pres.chat.syslog.summoned + " " + objectRef; + } else if (verb === "left"){ + return subjectRef + " " + pres.chat.syslog.left; + } else if (verb === "created"){ + return subjectRef + " " + pres.chat.syslog.created; + } + return "... Bad log ..."; +} + +function convertMessageStToSupercontainer(messageSt){ + let container = document.createElement("div"); + container.className = "message-supercontainer"; + + let box = document.createElement("div"); + container.appendChild(box); + box.className = getMsgFullTypeClassName(messageSt); + + let ID = messageSt.id; + + if (messageSt.isSystem){ + + } else { + let topPart = document.createElement("div"); + box.appendChild(topPart); + topPart.className = "message-box-top"; + + if (!members.has(messageSt.senderUserId)) + throw new Error("First - update members"); + let senderMemberSt = members.get(messageSt.senderUserId); + let senderProfileURI = "/user/" + senderMemberSt.nickname; + + let inTopPartSenderName = document.createElement("a"); + topPart.appendChild(inTopPartSenderName); + inTopPartSenderName.className = "message-box-sender-name"; + inTopPartSenderName.innerText = senderMemberSt.name; + inTopPartSenderName.href = senderProfileURI; + + let inTopPartSenderNickname = document.createElement("a"); + topPart.appendChild(inTopPartSenderNickname); + inTopPartSenderNickname.className = "message-box-sender-name message-box-sender-shortname" + inTopPartSenderNickname.innerText = senderMemberSt.nickname; + inTopPartSenderNickname.href = senderProfileURI; + + let inTopPartButtonDelete = document.createElement("img"); + topPart.appendChild(inTopPartButtonDelete); + inTopPartButtonDelete.className = "message-box-button message-box-button-delete"; + inTopPartButtonDelete.src = "/assets/img/delete.svg"; + inTopPartButtonDelete.onclick = (ev) => { + if (ev.button !== 0) + return; + let msgText = box.querySelector(".message-box-msg").innerText; + let previewText = senderMemberSt.nickname + ":\n" + msgText; + if (previewText.length > 1000) + previewText = previewText.substring(0, 1000 - 3); + document.getElementById("win-deletion-msg-preview").innerText = previewText; + storeHiddenMsgIdForDeletionWin = ID; + activatePopupWindowById("msg-deletion-win"); + }; + setElementVisibility(inTopPartButtonDelete, shouldShowDeleteMesgBtn(messageSt), "inline"); + + let inTopPartButtonGetLink = document.createElement("img"); + topPart.appendChild(inTopPartButtonGetLink); + inTopPartButtonGetLink.className = "message-box-button"; + inTopPartButtonGetLink.src = "/assets/img/link.svg"; + inTopPartButtonGetLink.onclick = (ev) => { + if (ev.button !== 0) + return; + let URI = window.location.host + "/chat/" + openedchat.nickname + "/m/" + String(ID); + document.getElementById("message-input").innerText += (" " + URI + ""); + }; + } + + let msgPart = document.createElement("p"); + box.appendChild(msgPart); + msgPart.className = "message-box-msg"; + if (messageSt.exists){ + if (messageSt.isSystem) + msgPart.innerText = decodeSystemMessage(messageSt.text); + else + msgPart.innerText = messageSt.text; + } else + msgPart.innerText = msgErased; + + return {'container': container, 'box': box}; +} + +function makeVisible(msgId){ + let supercontainer = convertMessageStToSupercontainer(loadedMessages.get(msgId)); + const chatWin = document.getElementById("chat-widget"); + chatWin.appendChild(supercontainer.container); + visibleMessages.set(msgId, supercontainer); +} + +function opaNewMessageSt(messageSt){ + let msgId = messageSt.id; + if (loadedMessages.has(msgId)){ + loadedMessages.set(msgId, messageSt); + if (visibleMessages.has(msgId)){ + updateMessageSupercontainer(visibleMessages.get(msgId), messageSt); + } + } else { + loadedMessages.set(msgId, messageSt); + if (anchoredMsg < 0){ + anchoredMsg = msgId; + visibleMsgSegStart = msgId; + visibleMsgSegEnd = msgId; + makeVisible(msgId); + } else if (msgId + 1 === visibleMsgSegStart) { + visibleMsgSegStart--; + makeVisible(msgId); + while (loadedMessages.has(visibleMsgSegStart - 1)){ + visibleMsgSegStart--; + makeVisible(visibleMsgSegStart); + } + } else if (msgId - 1 === visibleMsgSegEnd){ + visibleMsgSegEnd++; + makeVisible(msgId); + while (loadedMessages.has(visibleMsgSegEnd + 1)){ + visibleMsgSegEnd++; + makeVisible(visibleMsgSegEnd); + } + } + } +} + +function canISendMessages(){ + return myRoleHere === userChatRoleRegular || myRoleHere === userChatRoleAdmin; +} + +function updateLocalStateFromChatUpdRespBlind(chatUpdResp){ + LocalHistoryId = chatUpdResp.HistoryId; + for (let memberSt of chatUpdResp.members){ + let id = memberSt.userId; + if (id === userinfo.uid && myRoleHere !== memberSt.roleHere) { + myRoleHere = memberSt.roleHere; + for (let [msgId, sc] of visibleMessages){ + updateMessageSupercontainer(sc, loadedMessages.get(msgId)); + } + setElementVisibility(document.getElementById("message-input"), canISendMessages()); + } + } + for (let memberSt of chatUpdResp.members){ + let id = memberSt.userId; + members.set(id, memberSt); + } + lastMsgId = chatUpdResp.lastMsgId; + for (let messageSt of chatUpdResp.messages){ + opaNewMessageSt(messageSt); + } + updateOffsets(); +} + +function updateLocalStateFromRecvBlind(Recv){ + updateLocalStateFromChatUpdRespBlind(Recv.chatUpdResp); +} + +async function requestMessageNeighbours(fromMsg, direction){ + let Sent = genSentBaseGMN(); + Sent.msgId = fromMsg; + Sent.direction = direction; + let Recv = await apiRequest("getMessageNeighbours", Sent); + updateLocalStateFromRecvBlind(Recv); // Blind to non-loaded whitespaces +} + +function needToLoadWhitespace(){ + return isMissingPrimaryMsgHeap() || isMissingTopMsgHeap() || isMissingBottomMsgHeap(); +} + +async function tryLoadWhitespaceSingle(){ + if (isMissingPrimaryMsgHeap()){ + await requestMessageNeighbours(-1, "backward"); + } else if (isMissingTopMsgHeap()){ + await requestMessageNeighbours(visibleMsgSegStart, "backward"); + } else if (isMissingBottomMsgHeap()){ + await requestMessageNeighbours(visibleMsgSegEnd, "forward"); + } +} + +async function loadWhitespaceMultitry(){ + if (needToLoadWhitespace()){ + cancelMainloopTimeout(); + do { + try { + await tryLoadWhitespaceSingle(); + if (debugMode) + await sleep(900); + } catch (e) { + console.error(e); + await sleep(1500); + } + } while (needToLoadWhitespace()); + setMainloopTimeout(); + } +} + +async function updateLocalStateFromRecv(Recv){ + updateLocalStateFromRecvBlind(Recv); + await loadWhitespaceMultitry(); +} + +async function safeApiRequestWithLocalStUpdate(type, Sent, errMsg){ + try { + let Recv = await apiRequest(type, Sent) + await updateLocalStateFromRecv(Recv); + } catch(e) { + console.error(e); + alert(errMsg); + } +} + +function configureMsgDeletionPopupButtons(){ + document.getElementById("msg-deletion-yes").onclick = function(ev){ + if (ev.button !== 0) + return; + deactivateActivePopup(); + let Sent = genSentBase(); + Sent.id = storeHiddenMsgIdForDeletionWin; + safeApiRequestWithLocalStUpdate("deleteMessage", Sent, pres.chat['failed-delete-message']); + }; + + document.getElementById("msg-deletion-no").onclick = function (ev){ + if (ev.button !== 0) + return; + deactivateActivePopup(); + } +} + +__mainloopDelayMs = 1000; +async function UPDATE(){ + let Recv = await apiRequest("chatPollEvents", genSentBase()); + await updateLocalStateFromRecv(Recv); +} +__guestMainloopPollerAction = UPDATE; + +window.onload = function (){ + console.log("Page was loaded"); + + document.body.addEventListener("wheel", function (event) { + let offset = event.deltaY / 3; + if (offset < 0){ + bumpedAtBottom = false; + } else if (offset > 0 && !isMissingBottomMsgHeap() && lowestPoint + offset > chatPadding){ + bumpedAtBottom = true; + } + offsetOfAnchor += offset; + updateOffsets(); + loadWhitespaceMultitry().then(dopDopYesYes); + }); + + document.getElementById("message-input").addEventListener("keyup", function (event) { + if (event.ctrlKey && event.key === 'Enter'){ + let textarea = document.getElementById("message-input"); + let text = String(textarea.innerText); + textarea.innerText = ""; + let Sent = genSentBase(); + Sent.content = {}; + Sent.content.text = text; + safeApiRequestWithLocalStUpdate("sendMessage", Sent, pres.chat['failed-send-message']); + } + }); + + bumpedAtBottom = (openedchat.selectedMessageId < 0); + + let chatWg = document.getElementById("chat-widget"); + let chatWgDebugLinesFnc = function (){ + let H = chatWg.offsetHeight; + elSetOffsetInChat(document.getElementById("debug-line-lowest"), -softZoneSz); + elSetOffsetInChat(document.getElementById("debug-line-highest"), H + softZoneSz); + elSetOffsetInChat(document.getElementById("debug-line-top-padding"), H - chatPadding); + elSetOffsetInChat(document.getElementById("debug-line-bottom-padding"), chatPadding) + }; + if (debugMode){ + window.addEventListener("resize", chatWgDebugLinesFnc); + chatWgDebugLinesFnc(); + } + + configureMsgDeletionPopupButtons(); + + updateLocalStateFromChatUpdRespBlind(initial_chatUpdResp); + + setMainloopTimeout(); + + loadWhitespaceMultitry(); +} diff --git a/assets/js/common-popup.js b/assets/js/common-popup.js new file mode 100644 index 0000000..1148577 --- /dev/null +++ b/assets/js/common-popup.js @@ -0,0 +1,26 @@ +let activePopupWinId = ""; + +function activatePopupWindow__(el){ + let veil = document.createElement("div"); + veil.id = "popup-overlay-veil-OBJ" + veil.className = "popup-overlay-veil"; + veil.style.display = "block"; + document.body.appendChild(veil); + el.style.display = "block"; +} + +function activatePopupWindowById(id){ + if (activePopupWinId !== "") + return; + /* Lmao, this thing is just... SO unsafe */ + activePopupWinId = id; + activatePopupWindow__(document.getElementById(id)) +} + +function deactivateActivePopup(){ + if (activePopupWinId === "") + return + document.getElementById("popup-overlay-veil-OBJ").remove(); + document.getElementById(activePopupWinId).style.display = "none"; + activePopupWinId = ""; +} diff --git a/assets/js/common.js b/assets/js/common.js new file mode 100644 index 0000000..637b0b6 --- /dev/null +++ b/assets/js/common.js @@ -0,0 +1,77 @@ +let dopDopYesYes = (ign) => {}; + +function sleep(ms){ + return new Promise(res => setTimeout(res, ms)); +} + +async function apiRequest(type, req){ + let A = await fetch("/api/" + type, + {method: 'POST', body: JSON.stringify(req)}); + let B = await A.json(); + if (B.status !== 0) + throw Error("Server returned non-zero status"); + return B; +} + +/* Framework for pages with mainloop (it can be npt only polling, but also literally anything else */ +let __mainloopDelayMs = 3000; +let mainloopTimeout = null; +let __guestMainloopPollerAction = null; +function setMainloopTimeout(){ + if (mainloopTimeout !== null) + return; + mainloopTimeout = setTimeout(mainloopPoller, __mainloopDelayMs); +} +function cancelMainloopTimeout(){ + if (mainloopTimeout === null){ + console.log("cancelling nothing") + return; + } + clearTimeout(mainloopTimeout); + mainloopTimeout = null; +} +function mainloopPoller(){ + mainloopTimeout = null; + try { + if (__guestMainloopPollerAction) + __guestMainloopPollerAction(); + } catch (error){ + console.log(error) + } + setMainloopTimeout(); +} + +// 1 +const userChatRoleAdmin = "admin"; +// 2 +const userChatRoleRegular = "regular"; +// 3 +const userChatRoleReadOnly = "read-only"; +// 4 +const userChatRoleDeleted = "not-a-member"; + +function roleToColor(role) { + if (role === userChatRoleAdmin) { + return "#aafff3"; + } else if (role === userChatRoleRegular){ + return "#ffffff"; + + } else if (role === userChatRoleReadOnly){ + return "#bfb2b2"; + } else if (role === userChatRoleDeleted) { + return "#fb4a4a"; + } + return "#286500" // Bug +} + +function hideHTMLElement(el){ + el.style.display = "none"; +} + +function showHTMLElement(el){ + el.style.display = "block"; +} + +function setElementVisibility(el, isVisible, howVisible = "block"){ + el.style.display = isVisible ? howVisible : "none"; +} \ No newline at end of file diff --git a/assets/js/list-rooms.js b/assets/js/list-rooms.js index bf2b8c0..a1b0a73 100644 --- a/assets/js/list-rooms.js +++ b/assets/js/list-rooms.js @@ -1,186 +1,188 @@ -let rooms = {}; -let roomToDelete = null; -let currentRoom = null; -let currentHistoryId = 0; +let LocalHistoryId = 0; -function openRoom(currentRoom) { - alert('Вы вошли в комнату: ' + currentRoom); -} - -function closeAdd() { - document.getElementById('add_members').style.display = 'none'; -} - -function openAdd() { - document.getElementById('add_members').style.display = 'flex'; -} - -function openConfirm(roomNickname) { - roomToDelete = roomNickname; - document.getElementById("delete-chat").style.display = "flex"; -} - -function closeConfirm() { - roomToDelete = null; - document.getElementById("delete-chat").style.display = "none"; -} - -function deleteChat() { - if (roomToDelete && rooms[roomToDelete]) { - delete rooms[roomToDelete]; - removeRoomFromList(roomToDelete); - closeConfirm(); - } else { - alert("Не удалось найти выбранную комнату."); - } -} - -function addMember() { - const login = document.getElementById('newMemberLogin').value; - if (login) { - alert(`Участник с никнеймом '${login}' добавлен`); - closeAdd(); - } else { - alert('Пожалуйста, введите логин участника'); - } -} - -function openCreateRoomModal() { - document.getElementById('createRoomModal').style.display = 'block'; -} - -function closeCreateRoomModal() { - document.getElementById('createRoomModal').style.display = 'none'; -} - -async function createRoom() { - const errorElement = document.getElementById('error'); - const roomName = document.getElementById('newRoomName').value.trim(); - const roomNickname = document.getElementById('newRoomNickname').value.trim(); - - - errorElement.style.display = 'none'; - errorElement.textContent = ''; - - if (roomName === '' || roomNickname === '') { - errorElement.textContent = 'Пожалуйста, заполните все поля'; - errorElement.style.display = 'block'; - return; - } - - const request = { - LocalHistoryId: currentHistoryId, - content: { - name: roomName, - nickname: roomNickname +function genSentBase(){ + return { + 'chatListUpdReq': { + 'LocalHistoryId': LocalHistoryId } }; +} - try { - const response = await fetch('/internalapi/createChat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }); +let myChats = new Map(); +let chatBoxes = new Map(); - const res = await response.json(); +/* Generate text that is displayed on the right side of chat intro box */ +function youAreXHere(myRoleHere){ + // todo: TRANSLATE IT + return pres['list-rooms']['you-are-X-here'][0] + " " + myRoleHere + " " + pres['list-rooms']['you-are-X-here'][1]; +} - if (res.status === 0) { - addRoomToList(roomName, roomNickname); - rooms[roomNickname] = true; - closeCreateRoomModal(); - currentHistoryId = res.update.LocalHistoryId; - window.location.href = '/chat/' + roomNickname; + +let chatRenunciationWinStoredId = -1; + +function shouldShowDeleteButton(myMembershipSt){ + return myMembershipSt.myRoleHere === userChatRoleDeleted; +} + +/* Updating chat html box after myMembershipSt in it was updated */ +function updateBoxWithNewSt(box, myMembershipSt){ + let ID = myMembershipSt.chatId; + let roleP = box.querySelector(".CL-my-chat-box-my-role"); + roleP.innerText = youAreXHere(myMembershipSt.myRoleHere); + box.style.backgroundColor = roleToColor(myMembershipSt.myRoleHere); + box.querySelector(".CL-my-chat-box-leave-btn").style.display = + (shouldShowDeleteButton(myMembershipSt) ? "none" : "block"); +} + +function convertMyMembershipStToBox(myMembershipSt){ + let chatURI = "/chat/" + myMembershipSt.chatNickname; + let ID = myMembershipSt.chatId; + + let box = document.createElement("div"); + box.className = "dynamic-block-list-el CL-my-chat-box"; + box.style.backgroundColor = roleToColor(myMembershipSt.myRoleHere); + + let inBoxNickname = document.createElement("a"); + box.appendChild(inBoxNickname); + inBoxNickname.className = "entity-nickname-txt CL-my-chat-box-nickname"; + inBoxNickname.innerText = myMembershipSt.chatNickname; + inBoxNickname.href = chatURI; + + let inBoxName = document.createElement("a"); + box.appendChild(inBoxName); + inBoxName.className = "entity-reg-field-txt CL-my-chat-box-name"; + inBoxName.innerText = myMembershipSt.chatName; + inBoxName.href = chatURI; + + let inBoxMyRoleHere = document.createElement("p"); + box.appendChild(inBoxMyRoleHere); + inBoxMyRoleHere.className = "entity-reg-field-txt CL-my-chat-box-my-role"; + inBoxMyRoleHere.innerText = youAreXHere(myMembershipSt.myRoleHere); + + let inBoxLeaveBtn = document.createElement("img"); + box.appendChild(inBoxLeaveBtn); + inBoxLeaveBtn.className = "CL-my-chat-box-leave-btn"; + inBoxLeaveBtn.src = "/assets/img/delete.svg"; + inBoxLeaveBtn.onclick = function (ev) { + if (ev.button !== 0) + return; + chatRenunciationWinStoredId = ID; + document.getElementById("chat-renunciation-win-title").innerText = + pres['list-rooms']['reask-leave-chat-X'] + " " + myMembershipSt.chatNickname + "?"; + activatePopupWindowById("chat-renunciation-win"); + }; + box.querySelector(".CL-my-chat-box-leave-btn").style.display = + (shouldShowDeleteButton(myMembershipSt) ? "none" : "block"); + return box; +} + +function updateLocalStateFromChatListUpdResp(chatListUpdResp){ + LocalHistoryId = chatListUpdResp.HistoryId; + + let literalChatList = document.getElementById("CL-dblec"); + + for (let myMembershipSt of chatListUpdResp.myChats){ + let chatId = myMembershipSt.chatId; + console.log(myMembershipSt); + if (myChats.has(chatId)){ + myChats.set(chatId, myMembershipSt); + updateBoxWithNewSt(chatBoxes.get(chatId), myMembershipSt); } else { - throw new Error(res.error || 'Ошибка'); + if (myMembershipSt.myRoleHere === userChatRoleDeleted) + continue; + myChats.set(chatId, myMembershipSt); + let box = convertMyMembershipStToBox(myMembershipSt) + chatBoxes.set(chatId, box); + literalChatList.appendChild(box); } - } catch (error) { - alert('Ошибка создания чата: ' + error.message); } } -function addRoomToList(roomName) { - const roomList = document.querySelector('.room-list'); - const existingRoomItem = Array.from(roomList.children).find(item => item.querySelector('.room-name').textContent === roomName); - if (existingRoomItem) { - existingRoomItem.remove(); - } - - const roomItem = document.createElement('li'); - roomItem.classList.add('room-item'); - - roomItem.innerHTML = ` - ${roomName} - - - - `; - - roomList.appendChild(roomItem); +/* Use it ONLY if `Recv` reported success */ +function updateLocalStateFromRecv(Recv){ + updateLocalStateFromChatListUpdResp(Recv.chatListUpdResp); } -function removeRoomFromList(roomName, roomNickname) { - const roomList = document.querySelector('.room-list'); - const roomItem = Array.from(roomList.children).find(item => item.querySelector('.room-name').textContent === roomName); - if (roomItem) { - roomList.removeChild(roomItem); - } -} - -async function initializeRoomList() { - try { - const response = await fetch('/internalapi/getChatList', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) +function configureChatCreationInterface(){ + document.getElementById("chat-creation-win-yes").onclick = function (ev) { + if (ev.button !== 0) + return; + let chatNicknameInput = document.getElementById("chat-nickname-input"); + let chatNameInput = document.getElementById("chat-name-input"); + let nickname = String(chatNicknameInput.value); + let name = String(chatNameInput.value); + deactivateActivePopup(); + let Sent = genSentBase(); + Sent.content = {}; + Sent.content.nickname = nickname; + Sent.content.name = name; + apiRequest("createChat", Sent + ).then((Recv) => { + updateLocalStateFromRecv(Recv); + }).catch((e) => { + alert(pres['list-rooms']["failed-create-chat"]); + console.log(e); }); + }; - const res = await response.json(); + document.getElementById("chat-creation-win-no").onclick = function (ev) { + if (ev.button !== 0) + return; + deactivateActivePopup(); + } - if (res.status === 0) { - res.chats.forEach(chat => { - addRoomToList(chat.content.name, chat.content.nickname); - }); - } else { - throw new Error(res.error || 'Неизвестная ошибка'); - } - } catch (error) { - alert('Ошибка загрузки списка чатов: ' + error.message); + document.getElementById("CL-bacbe").onclick = function (ev){ + if (ev.button !== 0) + return; + let chatNicknameInput = document.getElementById("chat-nickname-input"); + let chatNameInput = document.getElementById("chat-name-input"); + chatNicknameInput.value = ""; + chatNameInput.value = ""; + activatePopupWindowById("chat-creation-win"); + }; +} + +function configureChatRenunciationInterfaceWinPart(){ + document.getElementById("chat-renunciation-win-yes").onclick = function (ev){ + if (ev.button !== 0) + return; + deactivateActivePopup(); + if (chatRenunciationWinStoredId < 0) + throw new Error("chatRenunciationWinStoredId < 0"); + let chatId = chatRenunciationWinStoredId; + let Sent = genSentBase(); + Sent.chatId = chatId; + apiRequest("leaveChat", Sent + ).then((Recv) => { + updateLocalStateFromRecv(Recv); + }).catch((e) => { + alert(pres['list-rooms']["failed-create-chat"]); + console.log(e); + }); + } + + document.getElementById("chat-renunciation-win-no").onclick = function(ev) { + if (ev.button !== 0) + return; + deactivateActivePopup(); } } - -async function getChatID() { - const chatNickname = window.location.pathname.split('/').pop(); - const response = await fetch('/internalapi/getChatList', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) +__mainloopDelayMs = 3000; +__guestMainloopPollerAction = function(){ + let Sent = genSentBase(); + apiRequest("chatListPollEvents", Sent + ).then((Recv) => { + console.log("Got a response"); + console.log(Recv); + updateLocalStateFromRecv(Recv); }); - - const res = await response.json(); - for (const chat of res.chats) { - if (chat.content.nickname === chatNickname) { - return chat.id; - } - } - return -1; -} -window.onclick = function(event) { - if (event.target === document.getElementById('createRoomModal')) { - closeCreateRoomModal(); - } } -document.getElementById('newRoomName').addEventListener('keydown', function(event) { - if (event.key === 'Enter') { - createRoom(); - } -}); -document.addEventListener('DOMContentLoaded', initializeRoomList); \ No newline at end of file +window.onload = function () { + console.log("Loading complete"); + updateLocalStateFromChatListUpdResp(initial_chatListUpdResp); + configureChatCreationInterface(); + configureChatRenunciationInterfaceWinPart(); + mainloopPoller(); +}; diff --git a/assets/js/profile.js b/assets/js/profile.js deleted file mode 100644 index fc92988..0000000 --- a/assets/js/profile.js +++ /dev/null @@ -1,10 +0,0 @@ -document.getElementById('fileInput').addEventListener('change', function(event) { - const file = event.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = function(e) { - document.getElementById('avatar').src = e.target.result; - }; - reader.readAsDataURL(file); - } -}); \ No newline at end of file diff --git a/assets/lang/en-US.lang.json b/assets/lang/en-US.lang.json new file mode 100644 index 0000000..bb41154 --- /dev/null +++ b/assets/lang/en-US.lang.json @@ -0,0 +1,91 @@ +{ + "lang": "en", + "login": { + "header": "Login", + "directive-nickname": "Enter your nickname:", + "placeholder-nickname": "Nickname", + "directive-password": "Enter password:", + "placeholder-password": "Password", + "act": "Login", + "incorrect-nickname-or-password": "Incorrect nickname or password" + }, + "view-profile": { + "header-profile-of": "Profile of", + "directive-nickname": "Nickname:" + }, + "edit-profile": { + "header-profile-of": "Profile of", + "change-user-attributes": "Change user attributes", + "directive-nickname": "Nickname:", + "directive-name": "Enter new name:", + "placeholder-name": "New name", + "directive-password": "Enter new password:", + "placeholder-password": "New password", + "directive-bio": "Change description:", + "act-submit": "Submit changes", + "incorrect-profile-data": "Incorrec profile data" + }, + "list-rooms": { + "header": "List of chat rooms", + "new-chat-header": "Input identifying information for your new chat", + "directive-nickname": "Enter nickname for new chat:", + "placeholder-nickname": "Take a nickname", + "directive-name": "Enter name for new chat:", + "placeholder-name": "Come up with name", + "reask-create-new-chat": "Create new chat?", + "yes-create": "Yes, create", + "no-create": "No, cancel", + "reask-leave-chat-X": "Do you really want to leave chat", + "yes-leave": "Yes, leave", + "no-leave": "No, cancel", + "page-description": "List of available rooms", + "you-are-X-here": ["You are", "here"], + + "failed-create-chat": "Failed to create chat", + "failed-to-leave-chat": "Failed to leave chat" + }, + "chat-members": { + "members-of": "Members of", + "summon-label-nickname": "Nickname for summoned user", + "summon-label-ro": "Make read only", + "yes-summon": "Yes, summon", + "no-summon": "No, cancel", + "yes-kick": "Yes, delete", + "no-kick": "No, cancel", + "members-list-of": "Members list of", + "reask-kick-user-X" : "Do you really want to kick user", + + "failed-summon-member": "Failed to add user to chat", + "failed-kick-member": "Failed to kick user from chat" + }, + "chat": { + "header-chat": "Chat", + "reask-delete-message": "Are you sure you want to delete this message?", + "yes-delete": "Yes, delete", + "no-delete": "No, cancel", + "msgErased": "[ ERASED ]", + "syslog": { + "kicked": "kicked", + "summoned": "summoned", + "left": "left chat", + "created": "created this chat" + }, + "failed-delete-message": "Failed to delete message", + "failed-send-message": "Failed to send message" + }, + "register": { + "header": "Admin control - Registration", + "directive-nickname": "Nickname for new user", + "placeholder-nickname": "Nickname", + "directive-name": "Name for new user:", + "placeholder-name": "Name", + "directive-password": "Temporary password:", + "placeholder-password": "Password", + "act": "Register him", + "incorrect-nickname": "Incorrect nickname", + "incorrect-name": "Incorrect name", + "incorrect-password": "Incorrect password", + "nickname-taken": "Nickname already taken", + "add_user_error": "add_user failed" + } +} diff --git a/assets/lang/ru-RU.lang.json b/assets/lang/ru-RU.lang.json new file mode 100644 index 0000000..154f0c4 --- /dev/null +++ b/assets/lang/ru-RU.lang.json @@ -0,0 +1,91 @@ +{ + "lang": "ru", + "login": { + "header": "Вход", + "directive-nickname": "Введите свой никнейм:", + "placeholder-nickname": "Никнейм", + "directive-password": "Введите пароль:", + "placeholder-password": "Пароль", + "act": "Войти", + "incorrect-nickname-or-password": "Неверный логин или пароль" + }, + "view-profile": { + "header-profile-of": "Профиль", + "directive-nickname": "Никнейм:" + }, + "edit-profile": { + "header-profile-of": "Профиль", + "change-user-attributes": "Изменить аттрибуты пользователя", + "directive-nickname": "Никнейм:", + "directive-name": "Введите новое имя:", + "placeholder-name": "Новое имя", + "directive-password": "Введите новый пароль", + "placeholder-password": "Новый пароль", + "directive-bio": "Изменить 'о себе':", + "act-submit": "Применить", + "incorrect-profile-data": "Недопустимые данные профиля" + }, + "list-rooms": { + "header": "Список чат-комнат", + "new-chat-header": "Введите идентификационные данные для вашего нового чата", + "directive-nickname": "Введите никнейм для нового чата:", + "placeholder-nickname": "Займите никнейм", + "directive-name": "Введите имя для нового чата:", + "placeholder-name": "Придумайте имя", + "reask-create-new-chat": "Создать новый чат?", + "yes-create": "Да, создай", + "no-create": "Нет, отмена", + "reask-leave-chat-X": "Вы действительно хотите покинуть чат", + "yes-leave": "Да, покидаю", + "no-leave": "Нет, отмена", + "page-description": "Список доступных чат-комнат", + "you-are-X-here": ["Вы", "здесь"], + + "failed-create-chat": "Не смог создать чат", + "failed-to-leave-chat": "Не смог покинуть чат" + }, + "chat-members": { + "members-of": "Участники", + "summon-label-nickname": "Никнейм для призываемого пользователя", + "summon-label-ro": "Сделать 'лишь читающим'", + "yes-summon": "Да, призваю", + "no-summon": "Нет, отмена", + "yes-kick": "Да, выкидываю", + "no-kick": "Нет, отмена", + "members-list-of": "Список участников", + "reask-kick-user-X" : "Вы действительно хотите выкинуть участника", + + "failed-summon-member": "Не смог добавить участника", + "failed-kick-member": "Не смог выкинуть участника" + }, + "chat": { + "header-chat": "Чат", + "reask-delete-message": "Удалить это сообщение?", + "yes-delete": "Да, удаляю", + "no-delete": "Нет, отмена", + "msgErased": "[ СТЁРТО ]", + "syslog": { + "kicked": "выкинул", + "summoned": "призвал", + "left": "покинул чат", + "created": "создал этот чат" + }, + "failed-delete-message": "Не смог удалить сообщение", + "failed-send-message": "Не смог отправить сообщение" + }, + "register": { + "header": "Admin control - Регистрация", + "directive-nickname": "Никнейм для нового пользователя:", + "placeholder-nickname": "Никнейм", + "directive-name": "Имя для нового пользователя:", + "placeholder-name": "Имя", + "directive-password": "Временный пароль:", + "placeholder-password": "Пароль", + "act": "Зарегистрируй его", + "incorrect-nickname": "Плохой никнейм", + "incorrect-name": "Плохое имя", + "incorrect-password": "Плохой пароль", + "nickname-taken": "Никнейм уже занят", + "add_user_error": "add_user failed" + } +} diff --git a/building/main.cpp b/building/main.cpp index 128ee14..5cc83b5 100644 --- a/building/main.cpp +++ b/building/main.cpp @@ -78,6 +78,7 @@ struct CAWebChat { "http_structures/client_request_parse.cpp", "http_structures/response_gen.cpp", "http_structures/cookies.cpp", + "http_structures/accept_language.cpp", "connecting_assets/static_asset_manager.cpp", "running_mainloop.cpp", "form_data_structure/urlencoded_query.cpp", @@ -97,6 +98,7 @@ struct CAWebChat { "http_structures/client_request.h", "http_structures/cookies.h", "http_structures/response_gen.h", + "http_structures/accept_language.h", "running_mainloop.h", "form_data_structure/urlencoded_query.h", "socket_address.h", @@ -141,6 +143,7 @@ struct CAWebChat { CTargetDependenceOnExternalLibrary{"sqlite3", {true, true}} }; T.units = { + "localizator.cpp", "initialize.cpp", "run.cpp", "str_fields.cpp", @@ -149,16 +152,20 @@ struct CAWebChat { "login_cookie.cpp", "backend_logic/server_data_interact.cpp", "backend_logic/client_server_interact.cpp", - "backend_logic/when_list_rooms.cpp", + "backend_logic/when_login.cpp", + "backend_logic/when_list_rooms.cpp", "backend_logic/when_chat.cpp", "backend_logic/when_user.cpp", - "backend_logic/when_api_pollevents.cpp", - "backend_logic/when_api_getchatlist.cpp", - "backend_logic/when_api_getchatinfo.cpp", - "backend_logic/when_api_getchatmemberlist.cpp", - "backend_logic/when_api_getuserinfo.cpp", - "backend_logic/when_api_getmessageinfo.cpp", + "backend_logic/when_register.cpp", + "backend_logic/polling.cpp", + "backend_logic/api_sendmessage.cpp", + "backend_logic/api_deletemessage.cpp", + "backend_logic/api_addmembertochat.cpp", + "backend_logic/api_removememberfromchat.cpp", + "backend_logic/api_createchat.cpp", + "backend_logic/api_leavechat.cpp", + "backend_logic/admin_control_procedure.cpp", }; for (std::string& u: T.units) u = "web_chat/iu9_ca_web_chat_lib/" + u; diff --git a/example/config.json b/example/config.json index c88f13f..8735c40 100644 --- a/example/config.json +++ b/example/config.json @@ -1,33 +1,13 @@ { - "presentation": { - "lang": "ru", - "instance-identity": { - "top-title": "Вэб чат от ИУ9" - }, - "phr": { - "decl": { - "enter": "Вход", - "nickname": "Никнейм", - "password": "Пароль", - "page-login": "Вход", - "list-of-chat-rooms": "Список Чат-Коsмнат", - "name-of-room": "Название комнаты", - "create-room": "Создать комнату" - }, - "ask" : { - "select-chat-room": "Выберете чат комнату" - }, - "act": { - "enter": "Войти", - "create-room": "Создать комнату", - "confirm": "Подтвердить", - "create": "Создать" - } - } + "lang": { + "whitelist": ["*"], + "force-order": [ + "ru" + ] }, "assets": "./assets", "database": { - "type": "sqlite", + "type": "sqlite3", "file": "./iu9-ca-web-chat.db" }, "limits": { @@ -37,7 +17,7 @@ "storage-size-limit": 100000000000 }, "server": { - "workers": 8, + "workers": 16, "http-listen": ["127.0.0.1:1025"], "admin-command-listen": ["[::1]:1026"] } diff --git a/src/http_server/engine_engine_number_9/http_structures/accept_language.cpp b/src/http_server/engine_engine_number_9/http_structures/accept_language.cpp new file mode 100644 index 0000000..20dd850 --- /dev/null +++ b/src/http_server/engine_engine_number_9/http_structures/accept_language.cpp @@ -0,0 +1,72 @@ +#include "accept_language.h" +#include +#include "grammar.h" +#include "../baza_inter.h" + +namespace een9 { + bool AcceptLanguageSpec(char ch) { + return ch == ',' || ch == ';' || ch == '='; + } + + /* todo: This is one of many places in een9, where bad alloc does not interrupt request, + * todo: completely changing response instead. (see cookies and login cookies lol) + * todo: I have to do something about it. Maybe add more exception types */ + std::vector parse_header_Accept_Language(const std::string &AcceptLanguage) { + size_t n = AcceptLanguage.size(); + struct LR { + std::string lr; + float q = 1; + }; + size_t i = 0; + auto skipOWS = [&]() { + while (i < n && isSPACE(AcceptLanguage[i])) + i++; + }; + auto isThis = [&](char ch) { + skipOWS(); + return i >= n ? false : AcceptLanguage[i] == ch; + }; + auto readTkn = [&]() -> std::string { + skipOWS(); + if (i >= n) + return ""; + size_t bg = i; + while (i < n && !AcceptLanguageSpec(AcceptLanguage[i]) && !isSPACE(AcceptLanguage[i])) + i++; + return AcceptLanguage.substr(bg, i - bg); + }; + std::vector lrs; +#define myMsg "Bad Accept-Language" + while (i < n) { + skipOWS(); + if (i >= n) + break; + if (!lrs.empty()) { + if (isThis(',')) + i++; + else + break; + } + lrs.emplace_back(); + lrs.back().lr = readTkn(); + LR lr{readTkn(), 0}; + if (isThis(';')) { + i++; + if (readTkn() != "q") + THROW(myMsg); + if (!isThis('=')) + THROW(myMsg); + i++; + lrs.back().q = std::stof(readTkn()); + } + } + std::sort(lrs.begin(), lrs.end(), [](const LR& A, const LR& B) { + return A.q > B.q; + }); + std::vector result; + result.reserve(lrs.size()); + for (const LR& lr: lrs) + result.push_back(lr.lr == "*" ? "" : lr.lr); + return result; + } +} diff --git a/src/http_server/engine_engine_number_9/http_structures/accept_language.h b/src/http_server/engine_engine_number_9/http_structures/accept_language.h new file mode 100644 index 0000000..b6c4342 --- /dev/null +++ b/src/http_server/engine_engine_number_9/http_structures/accept_language.h @@ -0,0 +1,13 @@ +#ifndef ENGINE_ENGINE_NUMBER_9_HTTP_STRUCTURES_ACCEPT_LANGUAGE_H +#define ENGINE_ENGINE_NUMBER_9_HTTP_STRUCTURES_ACCEPT_LANGUAGE_H + +#include +#include + +namespace een9 { + /* Returns language ranges, sorted by priority (reverse) + * throws std::exception if header is incorrect! But it is not guaranteed. Maybe it won't */ + std::vector parse_header_Accept_Language(const std::string& AcceptLanguage); +} + +#endif diff --git a/src/http_server/engine_engine_number_9/http_structures/client_request_parse.cpp b/src/http_server/engine_engine_number_9/http_structures/client_request_parse.cpp index c02a744..bfae1dd 100644 --- a/src/http_server/engine_engine_number_9/http_structures/client_request_parse.cpp +++ b/src/http_server/engine_engine_number_9/http_structures/client_request_parse.cpp @@ -111,7 +111,11 @@ namespace een9 { status = -1; return status; } - res.body.reserve(body_size); + res.body.reserve(std::min(100000ul, body_size)); + if (body_size == 0) { + status = 1; + } + break; } } if (!res.has_body) { diff --git a/src/http_server/engine_engine_number_9/http_structures/cookies.cpp b/src/http_server/engine_engine_number_9/http_structures/cookies.cpp index dee5ba4..7520612 100644 --- a/src/http_server/engine_engine_number_9/http_structures/cookies.cpp +++ b/src/http_server/engine_engine_number_9/http_structures/cookies.cpp @@ -1,14 +1,20 @@ #include "cookies.h" #include "../baza_inter.h" +#include "grammar.h" + namespace een9 { bool isSPACE(char ch) { return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'; } + bool isALPHANUM(char ch) { + return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9'); + } + bool isToken(const std::string &str) { for (char ch : str) { - if (!(('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9') + if (!(isALPHANUM(ch) || ch == '!' || ch == '#' || ch == '$' || ch == '%' || ch == '&' || ch == '\'' || ch == '*' || ch == '+' || ch == '-' || ch == '.' || ch == '^' || ch == '_' || ch == '`' || ch == '|' || ch == '~' )) @@ -43,7 +49,7 @@ namespace een9 { pos++; return hv.substr(S, pos - S); }; - auto read_to_space_or_dq_or_semc = [&]() -> std::string { + auto read_to_space_or_semc = [&]() -> std::string { size_t S = pos; while (hv.size() > pos && !isSPACE(hv[pos]) && hv[pos] != '"' && hv[pos] != ';') pos++; @@ -68,16 +74,7 @@ namespace een9 { THROW("Incorrect Cookie header line, missing ="); pos++; skip_ows(); - std::string value_of_pechenye; - if (isThis('"')) { - pos++; - value_of_pechenye = read_to_space_or_dq_or_semc(); - if (!isThis('"')) - THROW("Incorrect Cookie header line, missing \""); - pos++; - } else { - value_of_pechenye = read_to_space_or_dq_or_semc(); - } + std::string value_of_pechenye = read_to_space_or_semc(); // ASSERT(isCookieValue(value_of_pechenye), "Incorrect Cookie value"); result.emplace_back(name_of_pechenye, value_of_pechenye); skip_ows(); diff --git a/src/http_server/engine_engine_number_9/http_structures/grammar.h b/src/http_server/engine_engine_number_9/http_structures/grammar.h new file mode 100644 index 0000000..b82c69a --- /dev/null +++ b/src/http_server/engine_engine_number_9/http_structures/grammar.h @@ -0,0 +1,11 @@ +#ifndef ENGINE_ENGINE_NUMBER_9_HTTP_STRUCTURES_GRAMMAR_H +#define ENGINE_ENGINE_NUMBER_9_HTTP_STRUCTURES_GRAMMAR_H + +#include + +namespace een9 { + bool isSPACE(char ch); + bool isALPHANUM(char ch); +} + +#endif diff --git a/src/http_server/engine_engine_number_9/running_mainloop.cpp b/src/http_server/engine_engine_number_9/running_mainloop.cpp index b5deb27..7af1e4e 100644 --- a/src/http_server/engine_engine_number_9/running_mainloop.cpp +++ b/src/http_server/engine_engine_number_9/running_mainloop.cpp @@ -258,10 +258,10 @@ namespace een9 { } errno = 0; ret = poll(pollfds.data(), Nip, params.mainloop_recheck_interval_us); - if (ret != 0) { - printf("poll() error :> %d\n", errno); + if (ret != 0 && errno != 0) { + printf("poll() error :> %s\n", een9::prettyprint_errno("").c_str()); + continue; } - ASSERT_on_iret(ret, "poll()"); for (size_t i = 0; i < Nip; i++) { if ((pollfds[i].revents & POLLRDNORM)) { try { diff --git a/src/http_server/misc_tests/HypertextPages/test.nytl.html b/src/http_server/misc_tests/HypertextPages/test.nytl.html index a6f6f60..5614b90 100644 --- a/src/http_server/misc_tests/HypertextPages/test.nytl.html +++ b/src/http_server/misc_tests/HypertextPages/test.nytl.html @@ -1,8 +1,6 @@ -{% ELDEF main JSON cba %} +{% ELDEF main JSON userprofile %} AAA - {% FOR val IN cba.arr %} - --> {% WRITE val %} - {% ENDFOR %} + --> {% WRITE userprofile.name %} AAA {% ENDELDEF %} diff --git a/src/http_server/misc_tests/accept_language_test.cpp b/src/http_server/misc_tests/accept_language_test.cpp new file mode 100644 index 0000000..4aa4e43 --- /dev/null +++ b/src/http_server/misc_tests/accept_language_test.cpp @@ -0,0 +1,35 @@ +#include +#include + +using namespace een9; + +void test(const std::string& al, const std::vector& rls) { + std::vector got = parse_header_Accept_Language(al); + if (got != rls) { + printf("Test failed: wrong answer\n"); + abort(); + } + printf("Test passed\n"); +} + +void btest(const std::string& al) { + try { + parse_header_Accept_Language(al); + } catch (std::exception& e) {} + printf("...\n"); +} + +int main() { + test("RU-RU, uk-EN; q = 12.22", {"uk-EN", "RU-RU"}); + test(" RU-RU ,uk-EN; q = 12.22 ", {"uk-EN", "RU-RU"}); + test(" AAA; q=0.1, BBB-bb ; q=3, *; q=3", {"BBB-bb", "", "AAA"}); + test(" AAA; q=0.1, BBB-bb ; q=2.5, *; q=4.5", {"", "BBB-bb", "AAA"}); + test("ABB, AAA; q=0.1,AAB, BBB-bb ; q=2.5, *; q=4.5", {"", "BBB-bb", "ABB", "AAB", "AAA"}); + test("", {}); + test(" ", {}); + btest(";;;;"); + btest(";;==;;"); + btest("-;"); + btest("-=="); + return 0; +} diff --git a/src/http_server/misc_tests/nytl_test1.cpp b/src/http_server/misc_tests/nytl_test1.cpp index 6f7340c..afd4dde 100644 --- a/src/http_server/misc_tests/nytl_test1.cpp +++ b/src/http_server/misc_tests/nytl_test1.cpp @@ -9,16 +9,19 @@ int main(int argc, char** argv) { exit(1); } - std::string dir_path = argv[1]; + // std::string dir_path = "./src/http_server/misc_tests/HypertextPages"; + std::string dir_path = "/home/gregory/cpp_projects/iu9-ca-web-chat/assets/HypertextPages"; nytl::Templater templater(nytl::TemplaterSettings{nytl::TemplaterDetourRules{dir_path}}); templater.update(); - std::string config_file = argv[2]; - std::string config_text; - een9::readFile(config_file, config_text); - const json::JSON config = json::parse_str_flawless(config_text); - - std::string answer2 = templater.render("login", {&config["presentation"].g()}); + json::JSON userprofile; + userprofile["uid"].asInteger() = json::Integer(0l); + userprofile["name"].asString() = "radasdasdasdadsdasd"; + userprofile["nickname"].asString() = "root"; + userprofile["bio"].asString() = "Your mother"; + json::JSON errors; + errors = json::JSON(json::array); + std::string answer2 = templater.render("err-404", {}); printf("%s\n<>\n", answer2.c_str()); return 0; diff --git a/src/http_server/new_york_transit_line/debug_print.cpp b/src/http_server/new_york_transit_line/debug_print.cpp index 063f17d..f4faa3b 100644 --- a/src/http_server/new_york_transit_line/debug_print.cpp +++ b/src/http_server/new_york_transit_line/debug_print.cpp @@ -7,8 +7,13 @@ namespace nytl { void debug_print_templater(const Templater& T) { printf("===== TEMPLATER INTERNAL RESOURCES =====\n"); for (auto& p: T.elements) { + if (!p.second.is_element) { + printf("=== %s is empty =====\n", p.first.c_str()); + continue; + } printf("=== %s element =====\n", p.first.c_str()); - const Element& el = p.second; + assert(p.second.when_element); + const Element& el = *p.second.when_element; printf("%s, %s\n", el.base ? "BASE" : "NOT BASE", el.is_hidden ? "HIDDEN" : "NOT HIDDEN"); if (!el.is_hidden) { std::string signature; diff --git a/src/http_server/new_york_transit_line/parser.cpp b/src/http_server/new_york_transit_line/parser.cpp index 69e4a78..d031d89 100644 --- a/src/http_server/new_york_transit_line/parser.cpp +++ b/src/http_server/new_york_transit_line/parser.cpp @@ -55,13 +55,16 @@ namespace nytl { } char skip(ParsingContext& ctx) { - ASSERT(ctx.pos < ctx.text.size(), "Unexpected EOF"); + if (ctx.pos >= ctx.text.size()) + THROW("Unexpected EOF"); return advance(ctx); } void skip(ParsingContext& ctx, char ch) { - ASSERT(ctx.pos < ctx.text.size(), "Unexpected EOF"); - ASSERT(ctx.text[ctx.pos] == ch, "Unexpected character"); + if (ctx.pos >= ctx.text.size()) + THROW("Unexpected EOF"); + if (ctx.text[ctx.pos] != ch) + THROW("Unexpected character"); advance(ctx); } @@ -147,16 +150,41 @@ namespace nytl { return concatenateLines(lines); } - void parse_bare_file(const std::string& filename, const std::string& content, - global_elem_set_t& result) - { - ASSERT(result.count(filename) == 0, "Repeated element " + filename); + Element& add_hidden_element(const std::string& new_el_name, global_elem_set_t& result) { + if (result.count(new_el_name) != 0) + THROW("Repated element " + new_el_name); + TemplaterRegPref& rp = result[new_el_name]; + rp.is_element = 1; + rp.when_element = std::make_unique(); + rp.when_element->is_hidden = true; + return *rp.when_element; + } + + Element& add_new_element(const std::string& new_el_name, global_elem_set_t& result) { + if (!is_uname_dotted_sequence(new_el_name)) + THROW("Krabovaya oshibka"); + if (result.count(new_el_name) != 0 && result.at(new_el_name).is_element) + THROW("Repated element " + new_el_name); + size_t n = new_el_name.size(); + for (size_t i = 0; i < n; i++) { + if (new_el_name[i] == '.') { + std::string pref = new_el_name.substr(0, i); + result[pref]; + } + } + TemplaterRegPref& rp = result[new_el_name]; + rp.is_element = 1; + rp.when_element = std::make_unique(); + return *rp.when_element; + } + + void parse_bare_file(const std::string& filename, const std::string& content, global_elem_set_t& result) { + Element& el = add_new_element(filename, result); std::string txt = clement_lstrip(content); rstrip(txt); size_t cut = 9999999999999; one_part_update_min_start_wsp_non_empty(txt, 0, 1, cut); txt = one_part_cut_excess_tab(txt, 0, 1, cut); - Element& el = result[filename]; el.parts = {ElementPart{}}; el.parts[0].when_code.lines = mv(txt); } @@ -170,15 +198,17 @@ namespace nytl { uptr toMe(bool returned, ParsingContext& ctx) { if (!returned) { std::string nm = readName(ctx); - ASSERT(!nm.empty(), "Type specification expected"); + if (nm.empty()) + THROW("Type specification expected"); nm = make_uppercase(nm); if (nm == "JSON") { result = json::JSON(true); return NULL; } - ASSERT(nm == "EL", "Type of argument variable is either JSON or EL(...signature)") + if (nm != "EL") + THROW("Type of argument variable is either JSON or EL(...signature)"); skip(ctx, '('); - result = json::JSON(json::array); + result.asArray(); assert(result.isArray()); } skipWhitespace(ctx); @@ -217,19 +247,21 @@ namespace nytl { uptr toMe(bool returned, ParsingContext& ctx, const arg_name_list_t& local_var_names) { if (!returned) { std::string first = readName(ctx); - ASSERT(!first.empty(), "Expression should start with 'root' name of global package or local variable"); - ASSERT(first != "_", "_ ??? ARE YOU KIDDING???"); + if (first.empty()) + THROW("Expression should start with 'root' name of global package or local variable"); + if (first == "_") + THROW("Expression root can't be _"); if (local_var_names.count(first) == 1) { - result["V"] = json::JSON(json::Integer((int64_t)local_var_names.at(first))); + result["V"].asInteger() = json::Integer((int64_t)local_var_names.at(first)); } else { - result["V"] = json::JSON(first); + result["V"].asString() = first; } - result["C"] = json::JSON(json::array); + result["C"].asArray(); } else { skipWhitespace(ctx); skip(ctx, ']'); } - std::vector& chain = result["C"].g().asArray(); + std::vector& chain = result["C"].asArray(); while (true) { if (peep(ctx) == '.') { skip(ctx, '.'); @@ -243,7 +275,8 @@ namespace nytl { t = readUint(ctx); if (!t.empty()) { size_t v = std::stoul(t); - ASSERT(v < INT64_MAX, "Index is too big"); + if (v >= INT64_MAX) + THROW("Index is too big"); chain.back() = json::JSON((int64_t)v); continue; } @@ -352,7 +385,8 @@ namespace nytl { ElementPart::when_for_put_S& P = result.parts.back().when_for_put; skipWhitespace(ctx); std::string V1 = readName(ctx); - ASSERT(!V1.empty(), "Expected variable name"); + if (V1.empty()) + THROW("Expected variable name"); skipWhitespace(ctx); bool have_colon_and_2 = false; std::string V2; @@ -364,21 +398,23 @@ namespace nytl { skipWhitespace(ctx); } op = make_uppercase(readName(ctx)); - ASSERT(op == "IN", "Expected IN"); + if (op != "IN") + THROW("Expected IN"); skipWhitespace(ctx); P.ref_over = parse_expression(ctx, local_var_names); P.internal_element = el_name + ".~" + std::to_string(free_hidden++); - Element& newborn = elem_ns[P.internal_element]; - newborn.is_hidden = true; + Element& newborn = add_hidden_element(P.internal_element, elem_ns); arg_name_list_t local_var_names_of_nxt = local_var_names; if (V1 != "_") { - ASSERT(local_var_names_of_nxt.count(V1) == 0, "Repeated local variable"); + if (local_var_names_of_nxt.count(V1) != 0) + THROW("Repeated local variable"); size_t k = local_var_names_of_nxt.size(); local_var_names_of_nxt.emplace(V1, k); (have_colon_and_2 ? P.where_key_var : P.where_value_var) = (ssize_t)k; } if (have_colon_and_2 && V2 != "_") { - ASSERT(local_var_names_of_nxt.count(V2) == 0, "Repeated local variable"); + if (local_var_names_of_nxt.count(V2) != 0) + THROW("Repeated local variable"); size_t k = local_var_names_of_nxt.size(); local_var_names_of_nxt.emplace(V2, k); P.where_value_var = (ssize_t)k; @@ -395,16 +431,16 @@ namespace nytl { ElementPart::when_ref_put_S& P = result.parts.back().when_ref_put; skipWhitespace(ctx); std::string Vn = readName(ctx); - ASSERT(!Vn.empty(), "Expected variable name"); - ASSERT(Vn != "_", "Are you kidding???"); + if (Vn.empty() || Vn == "_") + THROW("REF: expected variable name"); skipWhitespace(ctx); op = make_uppercase(readName(ctx)); - ASSERT(op == "AS", "Expected AS"); + if (op != "AS") + THROW("Expected AS"); skipWhitespace(ctx); P.ref_over = parse_expression(ctx, local_var_names); P.internal_element = el_name + ".~" + std::to_string(free_hidden++); - Element& newborn = elem_ns[P.internal_element]; - newborn.is_hidden = true; + Element& newborn = add_hidden_element(P.internal_element, elem_ns); arg_name_list_t local_var_names_of_nxt = local_var_names; size_t k = local_var_names_of_nxt.size(); local_var_names_of_nxt.emplace(Vn, k); @@ -413,7 +449,7 @@ namespace nytl { return std::make_unique(P.internal_element, gone_for_ref, local_var_names_of_nxt, ret_data_int, newborn); } - if (op == "PUT") { + if (op == "PUT" || op == "P") { result.parts.emplace_back(); result.parts.back().type = ElementPart::p_put; ElementPart::when_put_S& P = result.parts.back().when_put; @@ -439,11 +475,11 @@ namespace nytl { P.passed_arguments = {parse_expression(ctx, local_var_names)}; skip_magic_block_end(ctx, syntax); }; - if (op == "WRITE") { + if (op == "WRITE" || op == "W") { mediocre_operator("str2text"); goto ya_e_ya_h_i_ya_g_d_o;; } - if (op == "ROUGHINSERT") { + if (op == "ROUGHINSERT" || op == "RI") { mediocre_operator("str2code"); goto ya_e_ya_h_i_ya_g_d_o;; } @@ -467,13 +503,15 @@ namespace nytl { } }; if (op == "ENDELDEF") { - ASSERT(myself == gone_for_nothing, "Unexpected end of element"); + if (myself != gone_for_nothing) + THROW("Unexpected ENDELDEF"); skip_magic_block_end(ctx, syntax); prepare_to_depart_parts(); return NULL; } if (op == "ENDFOR") { - ASSERT(myself == gone_for_for, "Unexpected end of for cycle"); + if (myself != gone_for_for) + THROW("Unexpected ENDFOR"); skipWhitespace(ctx); /* Here I am using ret_data_int to return info about NOLF(1)/LF(2) decision */ ret_data_int = 2; // Default is to do LF @@ -491,7 +529,8 @@ namespace nytl { return NULL; } if (op == "ENDREF") { - assert(myself == gone_for_ref); + if (myself != gone_for_ref) + THROW("Unexpected ENDREF"); skip_magic_block_end(ctx, syntax); prepare_to_depart_parts(); return NULL; @@ -525,13 +564,14 @@ namespace nytl { if (peep(ctx) == EOFVAL) break; skip_magic_block_start(ctx, syntax); - ASSERT(make_uppercase(readName(ctx)) == "ELDEF", "Expected ELDEF"); + if (make_uppercase(readName(ctx)) != "ELDEF") + THROW("Expected ELDEF"); skipWhitespace(ctx); std::string elname_postfix = readName(ctx); - ASSERT(elname_postfix != "_", "please don't"); + if (elname_postfix == "_") + THROW("Can't use _ as element name"); std::string fullname = elname_postfix == "main" ? filename : filename + "." + elname_postfix; - ASSERT(result.count(fullname) == 0, "Element " + fullname + " has been already defined"); - Element& newborn = result[fullname]; + Element& newborn = add_new_element(fullname, result); arg_name_list_t arglist; while (true) { skipWhitespace(ctx); @@ -540,9 +580,11 @@ namespace nytl { newborn.arguments.push_back(parse_type(ctx)); skipWhitespace(ctx); std::string argname = readName(ctx); - ASSERT(!argname.empty(), "Expected argument name"); + if (argname.empty()) + THROW("Expected argument name"); if (argname != "_") { - ASSERT(arglist.count(argname) == 0, "Repeated argument (" + argname + ")"); + if (arglist.count(argname) != 0) + THROW("Repeated argument (" + argname + ")"); size_t k = arglist.size(); arglist[argname] = k; } diff --git a/src/http_server/new_york_transit_line/rendering.cpp b/src/http_server/new_york_transit_line/rendering.cpp index bbdddbc..50bf47a 100644 --- a/src/http_server/new_york_transit_line/rendering.cpp +++ b/src/http_server/new_york_transit_line/rendering.cpp @@ -23,58 +23,64 @@ namespace nytl { result(result) { } - void descend(const json::JSON& what) { + void descend(const json::JSON& what, const global_elem_set_t& global_elems) { if (result.is_json) { const json::JSON& P = *result.JSON_subval; if (P.isArray() && what.isInteger()) { const std::vector& arr_p = P.asArray(); int64_t ind_w = what.asInteger().get_int(); - ASSERT(ind_w > 0 && ind_w < arr_p.size(), "Expression \"array[integer]\" caused out-of-bound situation"); + if (!(ind_w > 0 && ind_w < arr_p.size())) + THROW("Expression \"array[integer]\" caused out-of-bound situation"); result = LocalVarValue{true, "", &arr_p[ind_w]}; } else if (P.isDictionary() && what.isString()) { const std::map& dict_p = P.asDictionary(); const std::string& key_w = what.asString(); - ASSERT(dict_p.count(key_w) == 1, "No such key exception (" + key_w + ")"); + if (dict_p.count(key_w) != 1) + THROW("No such key exception (" + key_w + ")"); result = LocalVarValue{true, "", &dict_p.at(key_w)}; } else THROW("Incorrect type of \"json[json]\" expression. Unallowed signature of [] operator"); } else { - ASSERT(what.isString(), "Expression \"element[X]\" allowed only if X is string (json object)"); - if (what.asString().empty()) - return; - if (!is_uname_dotted_sequence(what.asString())) - THROW("Incorrect X in \"element[X]\""); + if (!what.isString()) + THROW("Expression \"element[X]\" allowed only if X is string (json object)"); + if (!isUname(what.asString())) + THROW("Expression \"element[str]\" has incorrect str (" + what.asString() + ")"); result.EL_name += ("." + what.asString()); + if (global_elems.count(result.EL_name) != 1) + THROW("Can't descend. No such element (" + result.EL_name + ")"); } } uptr toMe(bool returned, const global_elem_set_t& global_elems, const std::vector& local_vars) { if (returned) { - ASSERT(temp_ret.is_json, "Expression \"X[ element ]\" is not allowed"); + if (!temp_ret.is_json) + THROW("Expression \"X[ element ]\" is not allowed"); assert(temp_ret.JSON_subval); - descend(*(temp_ret.JSON_subval)); + descend(*(temp_ret.JSON_subval), global_elems); } else { assert(expr.isDictionary()); - const json::JSON& val = expr["V"].g(); + const json::JSON& val = expr["V"]; if (val.isInteger()) { size_t lv_ind = val.asInteger().get_int(); assert(lv_ind < local_vars.size()); result = local_vars[lv_ind]; } else if (val.isString()) { - std::string cur_el_name_str = expr["V"].g().asString(); + std::string cur_el_name_str = expr["V"].asString(); + if (global_elems.count(cur_el_name_str) != 1) + THROW("Bad expression, no such element (" + cur_el_name_str + ")"); result = LocalVarValue{false, cur_el_name_str, NULL}; } else assert(false); } - const std::vector& chain = expr["C"].g().asArray(); + const std::vector& chain = expr["C"].asArray(); while (true) { if (chain_el >= chain.size()) return NULL; const json::JSON& t = chain[chain_el++]; if (t.isDictionary()) return std::make_unique(t, temp_ret); - descend(t); + descend(t, global_elems); } } }; @@ -84,6 +90,8 @@ namespace nytl { * and dictionaries. They are stored in json in rendering stack */ LocalVarValue rendering_core_execute_expression(const global_elem_set_t& global_elems, const std::vector& local_vars, const json::JSON& expr) { + + // todo: check if root element exists (if root value is not local variable, then it is element of package) bool returned = false; std::vector> stack; LocalVarValue result; @@ -211,8 +219,9 @@ namespace nytl { uptr RFrame_OverParts::toMe(bool returned, const global_elem_set_t &elem_ns, Ditch &result, const std::function &escape) { if (!returned) - ASSERT(elem_ns.count(name) == 1, "No such element"); - const Element& el = elem_ns.at(name); + if ((elem_ns.count(name) != 1) || (!elem_ns.at(name).is_element)) + THROW("Can't render. No such element (" + name + ")"); + const Element& el = *elem_ns.at(name).when_element; if (!returned) { /* Continue to do checks */ /* hidden elements (internal) do not need any check */ @@ -221,14 +230,17 @@ namespace nytl { ASSERT(n == passed_args.size(), "Argument count mismatch"); for (size_t i = 0; i < n; i++) { if (el.arguments[i].type == json::true_symbol) { - ASSERT(passed_args[i].is_json, "Expected json element argument, got element"); + if (!passed_args[i].is_json) + THROW("Expected json element argument, got element"); } else { // If not json is expected, element must be expected assert(el.arguments[i].isArray()); - ASSERT(!passed_args[i].is_json, "Expected element element arguemnt, got json"); - ASSERT(elem_ns.count(passed_args[i].EL_name), "No such element, can't compare signatures of argument value"); - const Element& arg_element = elem_ns.at(passed_args[i].EL_name); - // ASSERT(passed_args); + if (passed_args[i].is_json) + THROW("Expected element element arguemnt, got json"); + const std::string& passed_el_as_arg = passed_args[i].EL_name; + if ((elem_ns.count(passed_el_as_arg) != 1) || !elem_ns.at(passed_el_as_arg).is_element) + THROW("No such element, can't compare signatures of argument value (" + passed_el_as_arg + ")"); + const Element& arg_element = elem_ns.at(passed_el_as_arg).when_element.operator*(); if(el.arguments[i].asArray() != arg_element.arguments) THROW("Signature of argument " + std::to_string(i) + " does not match"); } diff --git a/src/http_server/new_york_transit_line/templater.cpp b/src/http_server/new_york_transit_line/templater.cpp index b20b365..26758ab 100644 --- a/src/http_server/new_york_transit_line/templater.cpp +++ b/src/http_server/new_york_transit_line/templater.cpp @@ -109,16 +109,17 @@ namespace nytl { return result; } + TemplaterRegPref gen_base_element() { + Element* e = new Element{{json::JSON(true)}, true, false, {}}; + return {1, std::unique_ptr(e)}; + } + void Templater::update() { - elements = { - {"jsinsert", Element{{json::JSON(true)}, true}}, - {"jesc", Element{{json::JSON(true)}, true}}, - {"jesccomp", Element{{json::JSON(true)}, true}}, - /* str2text base element has a dedicated operator - WRITE */ - {"str2text", Element{{json::JSON(true)}, true}}, - /* str2code base element has a dedicated operator - ROUGHINSERT */ - {"str2code", Element{{json::JSON(true)}, true}}, - }; + elements[""] = TemplaterRegPref{0, NULL}; + elements["jsinsert"] = gen_base_element(); + elements["jesc"] = gen_base_element(); + elements["str2text"] = gen_base_element(); + elements["str2code"] = gen_base_element(); std::vector intersting_files = indexing_detour(settings.det); for (const InterestingFile& file: intersting_files) { std::string content = readFile(file.path); @@ -132,7 +133,8 @@ namespace nytl { /* Still can throw some stuff derived from std::exception (like bad alloc) */ std::string Templater::render(const std::string& element, const std::vector &arguments) const { - ASSERT(is_uname_dotted_sequence(element), "Incorrect entry element name"); + if (!is_uname_dotted_sequence(element)) + THROW("Incorrect entry element name"); return rendering_core(element, arguments, elements, settings.escape); } } diff --git a/src/http_server/new_york_transit_line/templater.h b/src/http_server/new_york_transit_line/templater.h index 599ae75..b1f235f 100644 --- a/src/http_server/new_york_transit_line/templater.h +++ b/src/http_server/new_york_transit_line/templater.h @@ -6,6 +6,8 @@ #include #include #include "html_case.h" +#include +#include namespace nytl { typedef json::JSON expression_t; @@ -61,7 +63,12 @@ namespace nytl { std::function escape = html_case_espace_string; }; - typedef std::map global_elem_set_t; + struct TemplaterRegPref { + int is_element = 0; + std::unique_ptr when_element = NULL; + }; + + typedef std::map global_elem_set_t; struct Templater { TemplaterSettings settings; diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/admin_control_procedure.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/admin_control_procedure.cpp new file mode 100644 index 0000000..d1325e3 --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/admin_control_procedure.cpp @@ -0,0 +1,81 @@ +#include "server_data_interact.h" +#include +#include "../str_fields.h" +#include + +namespace iu9cawebchat { + /* nagative `forced_id` means id isn't forced */ + void add_user(SqliteConnection& conn, const std::string& nickname, const std::string& name, + const std::string& password, const std::string& bio, int64_t forced_id) { + if (!check_nickname(nickname)) + een9_THROW("Bad user nickname " + nickname + ". Can't reg"); + if (!check_name(name)) + een9_THROW("Bad user name " + name + ". Can't reg"); + if (!check_strong_password(password)) + een9_THROW("Bad user password. Can't reg"); + if (!is_orthodox_string(bio)) + een9_THROW("Bad user bio. Can't reg"); + if (is_nickname_taken(conn, nickname)) + een9_THROW("Nickname taken already. Can't reg"); + reserve_nickname(conn, nickname); + SqliteStatement req(conn, + "INSERT INTO `user` (`id`, `nickname`, `name`, `chatList_HistoryId`, `password`, `bio`) " + "VALUES (?1, ?2, ?3, 0, ?4, ?5)", {}, {{2, nickname}, {3, name}, {4, password}, {5, bio}}); + if (forced_id >= 0) + sqlite_stmt_bind_int64(req, 1, forced_id); + int must_be_done = sqlite_stmt_step(req, {}, {}); + if (must_be_done != SQLITE_DONE) + een9_THROW("sqlite error"); + } + + std::string admin_control_procedure(SqliteConnection& conn, const std::string& req, bool& termination) { + size_t nid = 0; + auto read_thing = [&]() -> std::string { + while (nid < req.size() && isSPACE(req[nid])) + nid++; + std::string result; + bool esc = false; + while (nid < req.size() && (esc || !isSPACE(req[nid]))) { + if (esc) { + result += req[nid]; + esc = false; + } else if (req[nid] == '\\') { + esc = true; + } else { + result += req[nid]; + } + nid++; + } + return result; + }; + + std::string cmd = read_thing(); + if (cmd == "hello") { + return ":0 omg! hiii!! Hewwou :3 !!!!\n"; + } + if (cmd == "8") { + termination = true; + return "Bye\n"; + } + const char* adduser_pref = "adduser"; + if (cmd == "updaterootpw") { + + std::string new_password = read_thing(); + if (!check_strong_password(new_password)) + return "Bad password. Can't update"; + sqlite_nooutput(conn, + "UPDATE `user` SET `password` = ?1 WHERE `id` = 0", {}, {{1, new_password}}); + return "Successul update\n"; + } + /* adduser */ + if (cmd == "adduser") { + std::string nickname = read_thing(); + std::string name = read_thing(); + std::string password = read_thing(); + std::string bio = read_thing(); + add_user(conn, nickname, name, password, bio); + return "User " + nickname + " successfully registered"; + } + return "Incorrect command\n"; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_addmembertochat.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_addmembertochat.cpp new file mode 100644 index 0000000..d6ce36a --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_addmembertochat.cpp @@ -0,0 +1,75 @@ +#include "server_data_interact.h" +#include +#include + +namespace iu9cawebchat { + bool is_membership_row_present(SqliteConnection& conn, int64_t chatId, int64_t alienUserId) { + SqliteStatement req(conn, + "SELECT EXISTS(SELECT 1 FROM `user_chat_membership` WHERE `chatId` = ?1 AND `userId` = ?2)", + {{1, chatId}, {2, alienUserId}}, {}); + fsql_integer_or_null r{true, 0}; + int status = sqlite_stmt_step(req, {{0, &r}}, {}); + return (bool)r.value; + } + + void alter_user_chat_role(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role) { + int64_t chat_HistoryId_BEFORE_EV = get_current_history_id_of_chat(conn, chatId); + int64_t alien_chatlist_HistoryId_BEFORE_EV = get_current_history_id_of_user_chatList(conn, alienUserId); + if (!is_membership_row_present(conn, chatId, alienUserId)) { + sqlite_nooutput(conn, + "INSERT INTO `user_chat_membership` (`userId`, `chatId`, `user_chatList_IncHistoryId`," + "`chat_IncHistoryId`, `role`) VALUES (?1, ?2, ?3, ?4, ?5)", + {{1, alienUserId}, {2, chatId}, {3, alien_chatlist_HistoryId_BEFORE_EV + 1}, + {4, chat_HistoryId_BEFORE_EV + 1}, {5, role}}, {}); + + } else { + sqlite_nooutput(conn, + "UPDATE `user_chat_membership` SET `user_chatList_IncHistoryId` = ?3,`chat_IncHistoryId` = ?4," + "`role` = ?5 WHERE `userId` = ?1 AND `chatId` = ?2", + {{1, alienUserId}, {2, chatId}, {3, alien_chatlist_HistoryId_BEFORE_EV + 1}, + {4, chat_HistoryId_BEFORE_EV + 1}, {5, role}}, {}); + } + sqlite_nooutput(conn, + "UPDATE `chat` SET `it_HistoryId` = ?1 WHERE `id` = ?2", {{1, chat_HistoryId_BEFORE_EV + 1}, + {2, chatId}}, {}); + + sqlite_nooutput(conn, + "UPDATE `user` SET `chatList_HistoryId` = ?1 WHERE `id` = ?2", + {{1, alien_chatlist_HistoryId_BEFORE_EV + 1}, {2, alienUserId}}, {}); + } + + void make_her_a_member_of_the_midnight_crew(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role) { + assert(role != user_chat_role_deleted); + alter_user_chat_role(conn, chatId, alienUserId, role); + } + + json::JSON internalapi_addMemberToChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int(); + int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId); + if (my_role_here != user_chat_role_admin) + een9_THROW("Non-admin user tries to access internalapi_getChatInfo"); + + std::string alien_nickname = Sent["nickname"].asString(); + RowUser_Content alien; + try { + alien = lookup_user_content_by_nickname(conn, alien_nickname); + } catch (std::exception& e) { + return at_api_error_gen_bad_recv(-1l); + } + + bool makeReadOnly = Sent["makeReadOnly"].toBool(); + + int64_t aliens_old_role = get_role_of_user_in_chat(conn, alien.id, chatId); + if (aliens_old_role == user_chat_role_deleted) { + make_her_a_member_of_the_midnight_crew(conn, chatId, alien.id, + makeReadOnly ? user_chat_role_read_only : user_chat_role_regular); + } else { + return at_api_error_gen_bad_recv(-2l); + } + insert_system_message_svo(conn, chatId, uid, "summoned", alien.id); + + json::JSON Recv; + poll_update_chat(conn, Sent, Recv); + return Recv; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_createchat.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_createchat.cpp new file mode 100644 index 0000000..8ab1b94 --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_createchat.cpp @@ -0,0 +1,30 @@ +#include "server_data_interact.h" +#include +#include "../str_fields.h" + +namespace iu9cawebchat { + json::JSON internalapi_createChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + std::string new_chat_name = Sent["content"]["name"].asString(); + std::string new_chat_nickname = Sent["content"]["nickname"].asString(); + if (!check_nickname(new_chat_nickname) || !check_name(new_chat_name)) + return at_api_error_gen_bad_recv(-1l); + if (is_nickname_taken(conn, new_chat_nickname)) + return at_api_error_gen_bad_recv(-2l); + if (is_nickname_taken(conn, new_chat_nickname)) + return at_api_error_gen_bad_recv(-3l); + reserve_nickname(conn, new_chat_nickname); + + sqlite_nooutput(conn, + "INSERT INTO `chat` (`nickname`, `name`, `it_HistoryId`, `lastMsgId`) VALUES (?1, ?2, 0, -1)", + {}, {{1, new_chat_nickname}, {2, new_chat_name}}); + + int64_t CHAT_ID = sqlite_trsess_last_insert_rowid(conn); + + make_her_a_member_of_the_midnight_crew(conn, CHAT_ID, uid, user_chat_role_admin); + + // todo: send a message into chat + json::JSON Recv; + poll_update_chat_list(conn, uid, Sent, Recv); + return Recv; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_deletemessage.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_deletemessage.cpp new file mode 100644 index 0000000..1aaf62e --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_deletemessage.cpp @@ -0,0 +1,33 @@ +#include "server_data_interact.h" +#include +#include "../debug.h" + +namespace iu9cawebchat { + json::JSON internalapi_deleteMessage(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int(); + int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId); + if (my_role_here == user_chat_role_deleted) + een9_THROW("Unauthorized user tries to access internalapi_getChatInfo"); + if (my_role_here == user_chat_role_read_only) + een9_THROW("read-only user can't send messages"); + + int64_t LocalHistoryId = Sent["chatUpdReq"]["LocalHistoryId"].asInteger().get_int(); + int64_t msgId = Sent["id"].asInteger().get_int(); + RowMessage_Content msgInQuestion = lookup_message_content(conn, chatId, msgId); + if (!(!msgInQuestion.isSystem && (msgInQuestion.senderUserId == uid || my_role_here == user_chat_role_admin) )) + een9_THROW("Can't delete: permission denied"); + + int64_t chat_HistoryId_BEFORE_EV = get_current_history_id_of_chat(conn, chatId); + + sqlite_nooutput(conn, + "UPDATE `message` SET `exists` = 0, `text` = NULL, `chat_IncHistoryId` = ?1 WHERE `id` = ?2", + {{1, chat_HistoryId_BEFORE_EV + 1}, {2, msgId}}); + + sqlite_nooutput(conn, "UPDATE `chat` SET `it_HistoryId` = ?1 WHERE `id` = ?2", + {{1, chat_HistoryId_BEFORE_EV + 1}, {2, chatId}}, {}); + + json::JSON Recv; + poll_update_chat(conn, Sent, Recv); + return Recv; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_leavechat.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_leavechat.cpp new file mode 100644 index 0000000..cb847ad --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_leavechat.cpp @@ -0,0 +1,15 @@ +#include "server_data_interact.h" +#include + +namespace iu9cawebchat { + json::JSON internalapi_leaveChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["chatId"].asInteger().get_int(); + if (get_role_of_user_in_chat(conn, uid, chatId) == user_chat_role_deleted) + een9_THROW("Not a member"); + kick_from_chat(conn, chatId, uid); + insert_system_message_svo(conn, chatId, uid, "left", -1); + json::JSON Recv; + poll_update_chat_list(conn, uid, Sent, Recv); + return Recv; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_removememberfromchat.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_removememberfromchat.cpp new file mode 100644 index 0000000..cb70a6e --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_removememberfromchat.cpp @@ -0,0 +1,21 @@ +#include "server_data_interact.h" +#include + +namespace iu9cawebchat { + void kick_from_chat(SqliteConnection& conn, int64_t chatId, int64_t alienUserId) { + alter_user_chat_role(conn, chatId, alienUserId, user_chat_role_deleted); + } + + json::JSON internalapi_removeMemberFromChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int(); + int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId); + if (my_role_here != user_chat_role_admin) + een9_THROW("Only admin can delete members of chat"); + int64_t badAlienId = Sent["userId"].asInteger().get_int(); + kick_from_chat(conn, chatId, badAlienId); + insert_system_message_svo(conn, chatId, uid, "kicked", badAlienId); + json::JSON Recv; + poll_update_chat(conn, Sent, Recv); + return Recv; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_sendmessage.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_sendmessage.cpp new file mode 100644 index 0000000..59246c6 --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/api_sendmessage.cpp @@ -0,0 +1,50 @@ +#include "server_data_interact.h" +#include +#include "../str_fields.h" +#include "../debug.h" + +namespace iu9cawebchat { + /* No authorization check is performed + * Chat's HistoryId will increment after this operation + * if adding system message, uid is ignored + */ + void insert_new_message(SqliteConnection& conn, int64_t uid, int64_t chatId, const std::string& text, bool isSystem) { + int64_t chat_HistoryId_BEFORE_MSG = get_current_history_id_of_chat(conn, chatId); + int64_t chat_lastMsgId = get_lastMsgId_of_chat(conn, chatId); + SqliteStatement req(conn, + "INSERT INTO `message` (`chatId`, `id`, `senderUserId`, `exists`, `isSystem`, `chat_IncHistoryId`, " + "`text`) VALUES (?1, ?2, ?3, 1, ?4, ?5, ?6)", + {{1, chatId}, {2, chat_lastMsgId + 1}, {4, (int64_t)isSystem}, {5, chat_HistoryId_BEFORE_MSG + 1}}, {{6, text}}); + if (!isSystem) + sqlite_stmt_bind_int64(req, 3, uid); + if (sqlite_stmt_step(req, {}, {}) != SQLITE_DONE) + een9_THROW("There must be something wrong"); + sqlite_nooutput(conn, "UPDATE `chat` SET `lastMsgId` = ?1, `it_HistoryId` = ?2 WHERE `id` = ?3", + {{1, chat_lastMsgId + 1}, {2, chat_HistoryId_BEFORE_MSG + 1}, {3, chatId}}, {}); + } + + void insert_system_message_svo(SqliteConnection& conn, int64_t chatId, + int64_t subject, const std::string& verb, int64_t object) { + + insert_new_message(conn, -1, chatId, + std::to_string(subject) + "," + verb + "," + std::to_string(object), true); + } + + json::JSON internalapi_sendMessage(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int(); + int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId); + if (my_role_here == user_chat_role_deleted) + een9_THROW("Unauthorized user tries to access internalapi_getChatInfo"); + if (my_role_here == user_chat_role_read_only) + een9_THROW("read-only user can't send messages"); + + std::string text = Sent["content"]["text"].asString(); + if (!is_orthodox_string(text) || text.empty()) + een9_THROW("Bad input text"); + insert_new_message(conn, uid, chatId, text, false); + + json::JSON Recv; + poll_update_chat(conn, Sent, Recv); + return Recv; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.cpp index 5ca51d3..08add79 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.cpp @@ -1,5 +1,6 @@ #include "client_server_interact.h" #include +#include namespace iu9cawebchat { void initial_extraction_of_all_the_useful_info_from_cookies( @@ -19,60 +20,72 @@ namespace iu9cawebchat { if (ret_logged_in_user >= 0) { ret_userinfo["uid"] = json::JSON(ret_logged_in_user); ret_userinfo["nickname"] = json::JSON(tried.nickname); - ret_userinfo["name"] = json::JSON(find_user_name(conn, ret_logged_in_user)); + ret_userinfo["name"] = json::JSON(get_user_name(conn, ret_logged_in_user)); } } } - std::string RTEE(const std::string& el_name, - const json::JSON& config_presentation, WorkerGuestData& wgd, - const json::JSON& userinfo) { - std::string page = wgd.templater->render(el_name, {&config_presentation, &userinfo}); + std::string http_R200(const std::string &el_name, WorkerGuestData &wgd, + const std::vector &args) { + std::string page = wgd.templater->render(el_name, args); return een9::form_http_server_response_200("text/html", page); } + json::JSON jsonify_html_message_list(const std::vector& messages) { + json::JSON jmessages(json::array); + for (size_t i = 0; i < messages.size(); i++) { + jmessages[i]["class"].asString() = messages[i].class_; + jmessages[i]["text"].asString() = messages[i].text; + } + return jmessages; + } + + std::string page_E404(WorkerGuestData &wgd) { + return een9::form_http_server_response_404("text/html", + wgd.templater->render("err-404", {})); + } + /* ========================= API =========================*/ - - - std::string when_internalapi_pollevents(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid) { + std::string when_internalapi(WorkerGuestData& wgd, const een9::ClientRequest& req, int64_t uid, + const std::function& F) { const json::JSON& Sent = json::parse_str_flawless(req.body); - std::string result = json::generate_str(internalapi_pollEvents(*wgd.db, uid, Sent), json::print_pretty); + std::string result = json::generate_str(F(*wgd.db, uid, Sent), json::print_pretty); return een9::form_http_server_response_200("text/json", result); } - std::string when_internalapi_getchatlist(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid) { - const json::JSON& Sent = json::parse_str_flawless(req.body); - std::string result = json::generate_str(internalapi_getChatList(*wgd.db, uid), json::print_pretty); - return een9::form_http_server_response_200("text/json", result); + std::string when_internalapi_chatpollevents(WorkerGuestData& wgd, const een9::ClientRequest& req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_chatPollEvents); } - std::string when_internalapi_getchatinfo(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid) { - const json::JSON& Sent = json::parse_str_flawless(req.body); - std::string result = json::generate_str(internalapi_getChatInfo(*wgd.db, uid, Sent), json::print_pretty); - return een9::form_http_server_response_200("text/json", result); + std::string when_internalapi_chatlistpollevents(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_chatListPollEvents); } - std::string when_internalapi_getchatmemberlist(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid) { - const json::JSON& Sent = json::parse_str_flawless(req.body); - std::string result = json::generate_str(internalapi_getChatMemberList(*wgd.db, uid, Sent), json::print_pretty); - return een9::form_http_server_response_200("text/json", result); + std::string when_internalapi_getmessageneighbours(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_getMessageNeighbours); } - std::string when_internalapi_getuserinfo(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid) { - const json::JSON& Sent = json::parse_str_flawless(req.body); - std::string result = json::generate_str(internalapi_getUserInfo(*wgd.db, uid, Sent), json::print_pretty); - return een9::form_http_server_response_200("text/json", result); + std::string when_internalapi_sendmessage(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_sendMessage); } - std::string when_internalapi_getmessageinfo(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid) { - const json::JSON& Sent = json::parse_str_flawless(req.body); - std::string result = json::generate_str(internalapi_getMessageInfo(*wgd.db, uid, Sent), json::print_pretty); - return een9::form_http_server_response_200("text/json", result); + std::string when_internalapi_deletemessage(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_deleteMessage); } -} \ No newline at end of file + + std::string when_internalapi_addmembertochat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_addMemberToChat); + } + + std::string when_internalapi_removememberfromchat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_removeMemberFromChat); + } + + std::string when_internalapi_createchat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_createChat); + } + + std::string when_internalapi_leavechat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid) { + return when_internalapi(wgd, req, uid, internalapi_leaveChat); + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.h b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.h index 3d31417..13fa046 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.h +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/client_server_interact.h @@ -11,12 +11,14 @@ #include #include #include +#include "../localizator.h" namespace iu9cawebchat { struct WorkerGuestData { /* Because templaters use libjsonincpp, they can't be READ by two thread simultaneously */ std::unique_ptr templater; std::unique_ptr db; + std::unique_ptr locales; }; void initial_extraction_of_all_the_useful_info_from_cookies( @@ -27,9 +29,17 @@ namespace iu9cawebchat { int64_t& ret_logged_in_user ); - std::string RTEE(const std::string& el_name, - const json::JSON& config_presentation, WorkerGuestData& wgd, - const json::JSON& userinfo); + std::string http_R200(const std::string& el_name, WorkerGuestData& wgd, + const std::vector& args); + + struct HtmlMsgBox { + std::string class_; + std::string text; + }; + + json::JSON jsonify_html_message_list(const std::vector& messages); + + std::string page_E404(WorkerGuestData& wgd); /* ========================== PAGES ================================== */ @@ -43,28 +53,29 @@ namespace iu9cawebchat { const een9::ClientRequest& req, const json::JSON& userinfo); std::string when_page_user(WorkerGuestData& wgd, const json::JSON& config_presentation, - const een9::ClientRequest& req, const json::JSON& userinfo); + const een9::ClientRequest& req, const std::vector& login_cookies, const json::JSON& userinfo); + std::string when_page_register(WorkerGuestData& wgd, const json::JSON& config_presentation, + const een9::ClientRequest& req, const json::JSON& userinfo); /* ======================== API ============================== */ + std::string when_internalapi_chatpollevents(WorkerGuestData& wgd, const een9::ClientRequest& req, int64_t uid); - std::string when_internalapi_pollevents(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid); + std::string when_internalapi_chatlistpollevents(WorkerGuestData& wgd, const een9::ClientRequest& req, int64_t uid); - std::string when_internalapi_getchatlist(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid); + std::string when_internalapi_getmessageneighbours(WorkerGuestData& wgd, const een9::ClientRequest& req, int64_t uid); - std::string when_internalapi_getchatinfo(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid); + std::string when_internalapi_sendmessage(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid); - std::string when_internalapi_getchatmemberlist(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid); + std::string when_internalapi_deletemessage(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid); - std::string when_internalapi_getuserinfo(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid); + std::string when_internalapi_addmembertochat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid); - std::string when_internalapi_getmessageinfo(WorkerGuestData& wgd, - const een9::ClientRequest& req, int64_t uid); + std::string when_internalapi_removememberfromchat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid); + + std::string when_internalapi_createchat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid); + + std::string when_internalapi_leavechat(WorkerGuestData &wgd, const een9::ClientRequest &req, int64_t uid); } #endif diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/polling.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/polling.cpp new file mode 100644 index 0000000..9fff9fb --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/polling.cpp @@ -0,0 +1,198 @@ +#include "server_data_interact.h" +#include +#include +#include "../debug.h" + +namespace iu9cawebchat { + json::JSON poll_update_chat_list_resp(SqliteConnection& conn, int64_t userId, int64_t LocalHistoryId) { + printf("Userid: %ld\n", userId); + json::JSON chatListUpdResp; + SqliteStatement my_membership_changes(conn, + "SELECT `chatId`, `role` FROM `user_chat_membership` WHERE `userId` = ?1 " + "AND `user_chatList_IncHistoryId` > ?2", {{1, userId}, {2, LocalHistoryId}}); + json::jarr& myChats = chatListUpdResp["myChats"].asArray(); + while (true) { + fsql_integer_or_null ev_chatId, usersRoleHere; + int status = sqlite_stmt_step(my_membership_changes, {{0, &ev_chatId}, {1, &usersRoleHere}}, {}); + if (status != SQLITE_ROW) + break; + myChats.emplace_back(); + json::JSON& myMembershipSt = myChats.back(); + myMembershipSt["chatId"].asInteger() = json::Integer(ev_chatId.value); + myMembershipSt["myRoleHere"].asString() = stringify_user_chat_role(usersRoleHere.value); + if (usersRoleHere.value != user_chat_role_deleted) { + RowChat_Content CHAT = lookup_chat_content(conn, ev_chatId.value); + myMembershipSt["chatName"].asString() = CHAT.name; + myMembershipSt["chatNickname"].asString() = CHAT.nickname; + } + } + int64_t HistoryId = get_current_history_id_of_user_chatList(conn, userId); + chatListUpdResp["HistoryId"].asInteger() = json::Integer(HistoryId); + return chatListUpdResp; + } + + void poll_update_chat_list(SqliteConnection& conn, int64_t userId, const json::JSON& Sent, json::JSON& Recv) { + Recv["status"].asInteger() = json::Integer(0l); + // todo: in libjsonincpp: get rid of Integer + Recv["chatListUpdResp"] = poll_update_chat_list_resp(conn, userId, Sent["chatListUpdReq"]["LocalHistoryId"].asInteger().get_int()); + } + + json::JSON make_messageSt_obj(int64_t id, int64_t senderUserId, bool exists, bool isSystem, const std::string& text) { + json::JSON messageSt; + messageSt["id"].asInteger() = json::Integer(id); + if (!isSystem) + messageSt["senderUserId"].asInteger() = json::Integer(senderUserId); + messageSt["exists"] = json::JSON(exists); + messageSt["isSystem"] = json::JSON(isSystem); + if (exists) + messageSt["text"].asString() = text; + return messageSt; + } + + json::jarr poll_update_chat_resp_messages(SqliteConnection& conn, int64_t chatId, int64_t LocalHistoryId, + int64_t QSEG_A, int64_t QSEG_B) { + + json::jarr messages; + SqliteStatement messages_changes(conn, + "SELECT `id`, `senderUserId`, `exists`, `isSystem`, `text` FROM `message` " + "WHERE `chatId` = ?1 AND ( `chat_IncHistoryId` > ?2 OR ( ?3 <= `id` AND `id` <= ?4 ) )", + {{1, chatId}, {2, LocalHistoryId}, {3, QSEG_A}, {4, QSEG_B}}, {}); + while (true) { + fsql_integer_or_null msgId, msgSenderUserId, msgExists, msgIsSystem; + fsql_text8_or_null msgText; + int status = sqlite_stmt_step(messages_changes, + {{0, &msgId}, {1, &msgSenderUserId}, {2, &msgExists}, {3, &msgIsSystem}}, + {{4, &msgText}}); + if (status != SQLITE_ROW) + break; + messages.push_back(make_messageSt_obj(msgId.value, msgSenderUserId.value, msgExists.value, + msgIsSystem.value, msgText.value)); + } + return messages; + } + + json::jarr poll_update_chat_resp_members(SqliteConnection& conn, int64_t chatId, int64_t LocalHistoryId) { + json::jarr members; + + SqliteStatement membership_changes(conn, + "SELECT `userId`, `role` FROM `user_chat_membership` WHERE `chatId` = ?1 " + "AND `chat_IncHistoryId` > ?2", {{1, chatId}, {2, LocalHistoryId}}, {}); + while (true) { + fsql_integer_or_null alienUserId; + fsql_integer_or_null alienRoleHere; + int status = sqlite_stmt_step(membership_changes, + {{0, &alienUserId}, {1, &alienRoleHere}}, {}); + if (status != SQLITE_ROW) + break; + members.emplace_back(); + json::JSON& memberSt = members.back(); + memberSt["userId"].asInteger() = json::Integer(alienUserId.value); + memberSt["roleHere"].asString() = stringify_user_chat_role(alienRoleHere.value); + if (alienRoleHere.value != user_chat_role_deleted) { + RowUser_Content alien = lookup_user_content(conn, alienUserId.value); + memberSt["name"].asString() = alien.name; + memberSt["nickname"].asString() = alien.nickname; + } + } + return members; + } + + json::JSON poll_update_chat_ONE_MSG_resp(SqliteConnection& conn, int64_t chatId, int64_t selectedMsg) { + json::JSON chatUpdResp; + + chatUpdResp["members"].asArray() = poll_update_chat_resp_members(conn, chatId, 0); + + + json::jarr& messages = chatUpdResp["messages"].asArray(); + if (selectedMsg >= 0) { + RowMessage_Content msg = lookup_message_content(conn, chatId, selectedMsg); + messages.push_back(make_messageSt_obj(msg.id, msg.senderUserId, msg.exists, msg.isSystem, msg.text)); + } + + int64_t lastMsgId = get_lastMsgId_of_chat(conn, chatId); + chatUpdResp["lastMsgId"].asInteger() = json::Integer(lastMsgId); + + int64_t HistoryId = get_current_history_id_of_chat(conn, chatId); + chatUpdResp["HistoryId"].asInteger() = json::Integer(HistoryId); + return chatUpdResp; + } + + json::JSON poll_update_chat_important_segment_resp(SqliteConnection& conn, int64_t chatId, int64_t LocalHistoryId, + int64_t QSEG_A, int64_t QSEG_B) { + + json::JSON chatUpdResp; + + chatUpdResp["members"].asArray() = poll_update_chat_resp_members(conn, chatId, LocalHistoryId); + chatUpdResp["messages"].asArray() = poll_update_chat_resp_messages(conn, chatId, LocalHistoryId, QSEG_A, QSEG_B); + + int64_t lastMsgId = get_lastMsgId_of_chat(conn, chatId); + chatUpdResp["lastMsgId"].asInteger() = json::Integer(lastMsgId); + + int64_t HistoryId = get_current_history_id_of_chat(conn, chatId); + chatUpdResp["HistoryId"].asInteger() = json::Integer(HistoryId); + return chatUpdResp; + } + + /* chat polling function MUST have one queer feature: it accepts a range of msgId, which are guaranteed to be + * lookud up. */ + void poll_update_chat_important_segment(SqliteConnection& conn, const json::JSON& Sent, json::JSON& Recv, + int64_t QSEG_A, int64_t QSEG_B) { + + Recv["status"].asInteger() = json::Integer(0l); + Recv["chatUpdResp"] = poll_update_chat_important_segment_resp(conn, + Sent["chatUpdReq"]["chatId"].asInteger().get_int(), + Sent["chatUpdReq"]["LocalHistoryId"].asInteger().get_int(), QSEG_A, QSEG_B); + } + + void poll_update_chat(SqliteConnection& conn, const json::JSON& Sent, json::JSON& Recv) { + poll_update_chat_important_segment(conn, Sent, Recv, -1, -2); + } + + json::JSON internalapi_chatPollEvents(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int(); + if (get_role_of_user_in_chat(conn, uid, chatId) == user_chat_role_deleted) + een9_THROW("chatPollEvents: trying to access chat that user does not belong to"); + json::JSON Recv; + poll_update_chat(conn, Sent, Recv); + return Recv; + } + + json::JSON internalapi_chatListPollEvents(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + json::JSON Recv; + poll_update_chat_list(conn, uid, Sent, Recv); + return Recv; + } + + + /* Reznya */ + json::JSON internalapi_getMessageNeighbours(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int(); + if (get_role_of_user_in_chat(conn, uid, chatId) == user_chat_role_deleted) + een9_THROW("Authentication failure"); + int64_t lastMsgId = get_lastMsgId_of_chat(conn, chatId); + bool dir_forward = Sent["direction"].asString() == "forward"; + int64_t amount = Sent["amount"].asInteger().get_int(); + int64_t K = Sent["msgId"].asInteger().get_int(); + if (amount <= 0 || amount > 15) + een9_THROW("Incorrect amount"); + json::JSON Recv; + int64_t qBeg = -1; + int64_t qEnd = -2; + if (lastMsgId >= 0) { + if (K < 0) { + if (dir_forward) + een9_THROW("Can't go from the top of chat"); + qBeg = std::max(0l, lastMsgId - amount + 1); + qEnd = lastMsgId; + } else if (dir_forward) { + qBeg = K + 1; + qEnd = K + amount; + } else { + qBeg = K - amount; + qEnd = K - 1; + } + } + poll_update_chat_important_segment(conn, Sent, Recv, qBeg, qEnd); + return Recv; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.cpp index 75cfd5a..1a910d6 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.cpp @@ -2,8 +2,13 @@ #include #include +#include "../str_fields.h" namespace iu9cawebchat { + json::JSON at_api_error_gen_bad_recv(int64_t code) { + return json::JSON(json::jdict{{"status", json::JSON(code)}}); + } + const char* stringify_user_chat_role(int64_t role) { if (role == user_chat_role_admin) return "admin"; @@ -27,7 +32,7 @@ namespace iu9cawebchat { return -1; } - std::string find_user_name (SqliteConnection& conn, int64_t uid) { + std::string get_user_name (SqliteConnection& conn, int64_t uid) { een9_ASSERT(uid >= 0, "Are you crazy?"); SqliteStatement sql_req(conn, "SELECT `name` FROM `user` WHERE `id` = ?1", @@ -50,7 +55,20 @@ namespace iu9cawebchat { fsql_text8_or_null name_col; int status = sqlite_stmt_step(sql_req, {}, {{0, &nickname_col}, {1, &name_col}}); if (status == SQLITE_ROW) { - return {std::move(nickname_col.value), std::move(name_col.value)}; + return {uid, std::move(nickname_col.value), std::move(name_col.value)}; + } + een9_THROW("No such user"); + } + + RowUser_Content lookup_user_content_by_nickname(SqliteConnection& conn, const std::string& nickname) { + SqliteStatement sql_req(conn, + "SELECT `id`, `name` FROM `user` WHERE `nickname` = ?1", + {}, {{1, nickname}}); + fsql_integer_or_null id_col; + fsql_text8_or_null name_col; + int status = sqlite_stmt_step(sql_req, {{0, &id_col}}, {{1, &name_col}}); + if (status == SQLITE_ROW) { + return {id_col.value, nickname, std::move(name_col.value)}; } een9_THROW("No such user"); } @@ -65,7 +83,22 @@ namespace iu9cawebchat { fsql_integer_or_null last_msg_id_col; int status = sqlite_stmt_step(sql_req, {{2, &last_msg_id_col}}, {{0, &nickname_col}, {1, &name_col}}); if (status == SQLITE_ROW) { - return {std::move(nickname_col.value), std::move(name_col.value), + return {chatId, std::move(nickname_col.value), std::move(name_col.value), + last_msg_id_col.exist ? last_msg_id_col.value : -1}; + } + een9_THROW("No such chat"); + } + + RowChat_Content lookup_chat_content_by_nickname(SqliteConnection &conn, const std::string& nickname) { + SqliteStatement sql_req(conn, + "SELECT `id`, `name`, `lastMsgId` FROM `chat` WHERE `nickname` = ?1", + {}, {{1, nickname}}); + fsql_integer_or_null id_col; + fsql_text8_or_null name_col; + fsql_integer_or_null last_msg_id_col; + int status = sqlite_stmt_step(sql_req, {{0, &id_col}, {2, &last_msg_id_col}}, {{1, &name_col}}); + if (status == SQLITE_ROW) { + return {id_col.value, nickname, std::move(name_col.value), last_msg_id_col.exist ? last_msg_id_col.value : -1}; } een9_THROW("No such chat"); @@ -73,15 +106,15 @@ namespace iu9cawebchat { RowMessage_Content lookup_message_content(SqliteConnection& conn, int64_t chatId, int64_t msgId) { SqliteStatement req(conn, - "SELECT `previousId`, `senderUserId`, `exists`, `isSystem`, `text` FROM `message` WHERE " + "SELECT `senderUserId`, `exists`, `isSystem`, `text` FROM `message` WHERE " "`chatId` = ?1 AND `id` = ?2", {{1, chatId}, {2, msgId}}, {}); - fsql_integer_or_null previousId, senderUserId, exists, isSystem; + fsql_integer_or_null senderUserId, exists, isSystem; fsql_text8_or_null msg_text; - int status = sqlite_stmt_step(req, {{0, &previousId}, {1, &senderUserId}, {2, &exists}, {3, &isSystem}}, - {{4, &msg_text}}); + int status = sqlite_stmt_step(req, {{0, &senderUserId}, {1, &exists}, {2, &isSystem}}, + {{3, &msg_text}}); if (status == SQLITE_ROW) { - return {(bool)isSystem.value, msg_text.value, senderUserId.exist ? senderUserId.value : -1, - previousId.exist ? previousId.value : -1}; + return {msgId, senderUserId.exist ? senderUserId.value : -1, (bool)exists.value, + (bool)isSystem.value, msg_text.value}; } een9_THROW("No such message"); } @@ -98,5 +131,116 @@ namespace iu9cawebchat { return user_chat_role_deleted; } - /* All the api calls processing is done in dedicated files */ + int64_t get_lastMsgId_of_chat(SqliteConnection &conn, int64_t chatId) { + een9_ASSERT(chatId >= 0, "Are you crazy?"); + SqliteStatement sql_req(conn, + "SELECT `lastMsgId` FROM `chat` WHERE `id` = ?1", {{1, chatId}}, {}); + fsql_integer_or_null last_msg_id_col; + int status = sqlite_stmt_step(sql_req, {{0, &last_msg_id_col}}, {}); + if (status == SQLITE_ROW) { + return last_msg_id_col.exist ? last_msg_id_col.value : -1; + } + een9_THROW("No such chat"); + } + + /* All the api calls processing is done in dedicated files. + * All functions related to polling are defined in api_pollevents.cpp */ + + int64_t get_current_history_id_of_chat(SqliteConnection& conn, int64_t chatId) { + SqliteStatement req(conn, "SELECT `it_HistoryId` FROM `chat` WHERE `id` = ?1", {{1, chatId}}, {}); + fsql_integer_or_null HistoryId; + int status = sqlite_stmt_step(req, {{0, &HistoryId}}, {}); + een9_ASSERT_pl(status == SQLITE_ROW); + return HistoryId.value; + } + + int64_t get_current_history_id_of_user_chatList(SqliteConnection& conn, int64_t userId) { + SqliteStatement req(conn, "SELECT `chatList_HistoryId` FROM `user` WHERE `id` = ?1", {{1, userId}}, {}); + fsql_integer_or_null HistoryId; + int status = sqlite_stmt_step(req, {{0, &HistoryId}}, {}); + een9_ASSERT_pl(status == SQLITE_ROW); + return HistoryId.value; + } + + + // todo: extract useful clues from deprecated code. + // todo: deprecated code goes here: + /* !!! DEPRECATED FUNCTION */ + json::JSON toremoveinternalapi_getChatList(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + json::JSON Recv; + Recv["status"] = json::JSON(0l); + Recv["chats"] = json::JSON(json::array); + std::vector& chats = Recv["chats"].asArray(); + SqliteStatement req(conn, + "SELECT `chat`.`id`, `chat`.`nickname`, `chat`.`name`, `chat`.`lastMsgId`, " + "`user_chat_membership`.`role` FROM `chat` " + "RIGHT JOIN `user_chat_membership` ON `chat`.`id` = `user_chat_membership`.`chatId` " + "WHERE `user_chat_membership`.`userId` = ?1 ", {{1, uid}}, {}); + while (true) { + fsql_integer_or_null chat_id; + fsql_text8_or_null chat_nickname, chat_name; + fsql_integer_or_null chat_lastMsgId, role_here; + int status = sqlite_stmt_step(req, {{0, &chat_id}, {3, &chat_lastMsgId}, {4, &role_here}}, + {{1, &chat_nickname}, {2, &chat_name}}); + if (status != SQLITE_ROW) + break; + chats.emplace_back(); + json::JSON& chat = chats.back(); + chat["id"] = json::JSON(chat_id.value); + chat["content"]["nickname"] = json::JSON(chat_nickname.value); + chat["content"]["name"] = json::JSON(chat_name.value); + chat["content"]["lastMsgId"] = json::JSON(chat_lastMsgId.exist ? chat_lastMsgId.value : -1); + chat["content"]["roleHere"] = json::JSON(stringify_user_chat_role(role_here.value)); + } + return Recv; + } + + /* !!! DEPRECATED FUNCTION */ + json::JSON toremoveinternalapi_getChatMemberList(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { + int64_t chatId = Sent["id"].asInteger().get_int(); + int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId); + if (my_role_here == user_chat_role_deleted) + een9_THROW("Unauthorized user tries to access internalapi_getChatInfo"); + json::JSON Recv; + Recv["status"] = json::JSON(0l); + Recv["members"] = json::JSON(json::array); + std::vector& members = Recv["members"].asArray(); + SqliteStatement req(conn, + "SELECT `user`.`id`, `user`.`nickname`, `user`.`name`, `user_chat_membership`.`role` FROM " + "`user` RIGHT JOIN `user_chat_membership` ON `user`.`id` = `user_chat_membership`.`userId` " + "WHERE `user_chat_membership`.`chatId` = ?1", + {{1, chatId}}, {}); + while (true) { + fsql_integer_or_null this_user_id; + fsql_text8_or_null this_user_nickname, this_user_name; + fsql_integer_or_null this_users_role; + int status = sqlite_stmt_step(req, {{0, &this_user_id}, {3, &this_users_role}}, + {{1, &this_user_nickname}, {2, &this_user_name}}); + if (status != SQLITE_ROW) + break; + members.emplace_back(); + json::JSON& member = members.back(); + member["id"] = json::JSON(this_user_id.value); + member["content"]["nickname"] = json::JSON(this_user_nickname.value); + member["content"]["name"] = json::JSON(this_user_name.value); + member["content"]["role"] = json::JSON(this_users_role.value); + } + return Recv; + } + + bool is_nickname_taken(SqliteConnection& conn, const std::string& nickname) { + if (!check_nickname(nickname)) + return true; + SqliteStatement req(conn, "SELECT EXISTS(SELECT 1 FROM `nickname` WHERE `it` = ?1)", + {}, {{1, nickname}}); + fsql_integer_or_null r{true, 0}; + int status = sqlite_stmt_step(req, {{0, &r}}, {}); + return r.value; + } + + void reserve_nickname(SqliteConnection& conn, const std::string& nickname) { + if (!check_nickname(nickname)) + een9_THROW("PRECAUTION! Trying to insert incorrect nickname into nickname table"); + sqlite_nooutput(conn, "INSERT INTO `nickname` (`it`) VALUES (?1)", {}, {{1, nickname}}); + } } diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.h b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.h index 2f2bdfa..6b591cd 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.h +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/server_data_interact.h @@ -8,6 +8,8 @@ #include namespace iu9cawebchat { + json::JSON at_api_error_gen_bad_recv(int64_t code = -1); + constexpr int64_t user_chat_role_admin = 1; constexpr int64_t user_chat_role_regular = 2; constexpr int64_t user_chat_role_read_only = 3; @@ -16,41 +18,77 @@ namespace iu9cawebchat { const char* stringify_user_chat_role(int64_t role); int64_t find_user_by_credentials (SqliteConnection& conn, const std::string& nickname, const std::string& password); - std::string find_user_name (SqliteConnection& conn, int64_t uid); + std::string get_user_name (SqliteConnection& conn, int64_t uid); struct RowUser_Content { + int64_t id; std::string nickname; std::string name; }; struct RowChat_Content { + int64_t id; std::string nickname; std::string name; int64_t lastMsgId; // Negative if it does not exist }; RowUser_Content lookup_user_content(SqliteConnection& conn, int64_t uid); + RowUser_Content lookup_user_content_by_nickname(SqliteConnection& conn, const std::string& nickname); + /* Does not make authorization check */ RowChat_Content lookup_chat_content(SqliteConnection& conn, int64_t chatId); + RowChat_Content lookup_chat_content_by_nickname(SqliteConnection &conn, const std::string& nickname); struct RowMessage_Content { + int64_t id; + int64_t senderUserId; + bool exists; bool isSystem; std::string text; - int64_t senderUserId; - int64_t previous = -1; }; RowMessage_Content lookup_message_content(SqliteConnection& conn, int64_t chatId, int64_t msgId); + /* Does not make authorization check */ int64_t get_role_of_user_in_chat(SqliteConnection& conn, int64_t userId, int64_t chatId); + /* Does not make authorization check */ + int64_t get_lastMsgId_of_chat(SqliteConnection& conn, int64_t chatId); - /* ============================= API ====================================*/ - json::JSON internalapi_pollEvents(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); - json::JSON internalapi_getChatList(SqliteConnection& conn, int64_t uid); - json::JSON internalapi_getChatInfo(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); - json::JSON internalapi_getChatMemberList(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); - json::JSON internalapi_getUserInfo(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); - json::JSON internalapi_getMessageInfo(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); - // todo: complete the list + int64_t get_current_history_id_of_chat(SqliteConnection& conn, int64_t chatId); + int64_t get_current_history_id_of_user_chatList(SqliteConnection& conn, int64_t userId); + + json::JSON poll_update_chat_list_resp(SqliteConnection& conn, int64_t userId, int64_t LocalHistoryId); + void poll_update_chat_list(SqliteConnection& conn, int64_t userId, const json::JSON& Sent, json::JSON& Recv); + + json::JSON poll_update_chat_ONE_MSG_resp(SqliteConnection& conn, int64_t chatId, int64_t selectedMsg); + void poll_update_chat(SqliteConnection& conn, const json::JSON& Sent, json::JSON& Recv); + + void alter_user_chat_role(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role); + void make_her_a_member_of_the_midnight_crew(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role); + void kick_from_chat(SqliteConnection& conn, int64_t chatId, int64_t alienUserId); + + bool is_nickname_taken(SqliteConnection& conn, const std::string& nickname); + void reserve_nickname(SqliteConnection& conn, const std::string& nickname); + + void insert_new_message(SqliteConnection& conn, int64_t uid, int64_t chatId, const std::string& text, bool isSystem); + void insert_system_message_svo(SqliteConnection& conn, int64_t chatId, + int64_t subject, const std::string& verb, int64_t object); + + /* ============================= API ==================================== */ + json::JSON internalapi_chatPollEvents(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_chatListPollEvents(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_getMessageNeighbours(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_sendMessage(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_deleteMessage(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_addMemberToChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_removeMemberFromChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_createChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + json::JSON internalapi_leaveChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent); + + /**/ + void add_user(SqliteConnection& conn, const std::string& nickname, const std::string& name, + const std::string& password, const std::string& bio, int64_t forced_id = -1); + std::string admin_control_procedure(SqliteConnection& conn, const std::string& req, bool& termination); } #endif diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatinfo.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatinfo.cpp deleted file mode 100644 index e60939b..0000000 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatinfo.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "server_data_interact.h" -#include - -namespace iu9cawebchat { - json::JSON internalapi_getChatInfo(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { - int64_t chatId = Sent["id"].g().asInteger().get_int(); - int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId); - if (my_role_here == user_chat_role_deleted) - een9_THROW("Unauthorized user tries to access internalapi_getChatInfo"); - json::JSON Recv; - Recv["status"] = json::JSON(0l); - RowChat_Content content = lookup_chat_content(conn, chatId); - Recv["name"] = json::JSON(content.name); - Recv["nickname"] = json::JSON(content.nickname); - Recv["lastMsgId"] = json::JSON(content.lastMsgId); - Recv["roleHere"] = json::JSON(stringify_user_chat_role(my_role_here)); - return Recv; - } -} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatlist.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatlist.cpp deleted file mode 100644 index 28aba88..0000000 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatlist.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "server_data_interact.h" -#include - -namespace iu9cawebchat { - json::JSON internalapi_getChatList(SqliteConnection& conn, int64_t uid) { - json::JSON Recv; - Recv["status"] = json::JSON(0l); - Recv["chats"] = json::JSON(json::array); - std::vector& chats = Recv["chats"].g().asArray(); - SqliteStatement req(conn, - "SELECT `chat`.`id`, `chat`.`nickname`, `chat`.`name`, `chat`.`lastMsgId`, " - "`user_chat_membership`.`role` FROM `chat` " - "RIGHT JOIN `user_chat_membership` ON `chat`.`id` = `user_chat_membership`.`chatId` " - "WHERE `user_chat_membership`.`userId` = ?1 ", {{1, uid}}, {}); - while (true) { - fsql_integer_or_null chat_id; - fsql_text8_or_null chat_nickname, chat_name; - fsql_integer_or_null chat_lastMsgId, role_here; - int status = sqlite_stmt_step(req, {{0, &chat_id}, {3, &chat_lastMsgId}, {4, &role_here}}, - {{1, &chat_nickname}, {2, &chat_name}}); - if (status != SQLITE_ROW) - break; - chats.emplace_back(); - json::JSON& chat = chats.back(); - chat["id"] = json::JSON(chat_id.value); - chat["content"]["nickname"] = json::JSON(chat_nickname.value); - chat["content"]["name"] = json::JSON(chat_name.value); - chat["content"]["lastMsgId"] = json::JSON(chat_lastMsgId.exist ? chat_lastMsgId.value : -1); - chat["content"]["roleHere"] = json::JSON(stringify_user_chat_role(role_here.value)); - } - return Recv; - } -} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatmemberlist.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatmemberlist.cpp deleted file mode 100644 index 78f39b3..0000000 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getchatmemberlist.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "server_data_interact.h" -#include - -namespace iu9cawebchat { - json::JSON internalapi_getChatMemberList(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { - int64_t chatId = Sent["id"].g().asInteger().get_int(); - int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId); - if (my_role_here == user_chat_role_deleted) - een9_THROW("Unauthorized user tries to access internalapi_getChatInfo"); - json::JSON Recv; - Recv["status"] = json::JSON(0l); - Recv["members"] = json::JSON(json::array); - std::vector& members = Recv["members"].g().asArray(); - SqliteStatement req(conn, - "SELECT `user`.`id`, `user`.`nickname`, `user`.`name`, `user_chat_membership`.`role` FROM " - "`user` RIGHT JOIN `user_chat_membership` ON `user`.`id` = `user_chat_membership`.`userId` " - "WHERE `user_chat_membership`.`chatId` = ?1", - {{1, chatId}}, {}); - while (true) { - fsql_integer_or_null this_user_id; - fsql_text8_or_null this_user_nickname, this_user_name; - fsql_integer_or_null this_users_role; - int status = sqlite_stmt_step(req, {{0, &this_user_id}, {3, &this_users_role}}, - {{1, &this_user_nickname}, {2, &this_user_name}}); - if (status != SQLITE_ROW) - break; - members.emplace_back(); - json::JSON& member = members.back(); - member["id"] = json::JSON(this_user_id.value); - member["content"]["nickname"] = json::JSON(this_user_nickname.value); - member["content"]["name"] = json::JSON(this_user_name.value); - member["content"]["role"] = json::JSON(this_users_role.value); - } - return Recv; - } -} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getmessageinfo.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getmessageinfo.cpp deleted file mode 100644 index 3a71a53..0000000 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getmessageinfo.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "server_data_interact.h" -#include - -namespace iu9cawebchat { - /* This is literally the most dumb and useless query */ - json::JSON internalapi_getMessageInfo(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { - int64_t chatId = Sent["chatId"].g().asInteger().get_int(); - int64_t msgId = Sent["id"].g().asInteger().get_int(); - if (get_role_of_user_in_chat(conn, uid, chatId) == user_chat_role_deleted) - een9_THROW("Authentication failure"); - json::JSON Recv; - Recv["status"] = json::JSON(0l); - RowMessage_Content content = lookup_message_content(conn, chatId, msgId); - Recv["text"] = json::JSON(content.text); - Recv["isSystem"] = json::JSON(content.isSystem); - Recv["sender"] = json::JSON(content.senderUserId); - // todo: sync that addition with api documentation - Recv["previous"] = json::JSON(content.previous); - return Recv; - } -} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getuserinfo.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getuserinfo.cpp deleted file mode 100644 index 71816d8..0000000 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_getuserinfo.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "server_data_interact.h" - -namespace iu9cawebchat { - json::JSON internalapi_getUserInfo(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { - int64_t otherUserId = Sent["id"].g().asInteger().get_int(); - json::JSON Recv; - Recv["status"] = json::JSON(0l); - RowUser_Content content = lookup_user_content(conn, otherUserId); - Recv["name"] = json::JSON(content.name); - Recv["nickname"] = json::JSON(content.nickname); - return Recv; - } -} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_pollevents.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_pollevents.cpp deleted file mode 100644 index c803860..0000000 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_api_pollevents.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "server_data_interact.h" -#include - -namespace iu9cawebchat { - int64_t get_current_history_id_of_chat(SqliteConnection& conn, int64_t chatId) { - SqliteStatement req(conn, "SELECT `it_HistoryId` FROM `chat` WHERE `id` = ?1", {{1, chatId}}, {}); - fsql_integer_or_null HistoryId; - int status = sqlite_stmt_step(req, {{0, &HistoryId}}, {}); - een9_ASSERT_pl(status == SQLITE_ROW); - return HistoryId.value; - } - - int64_t get_current_history_id_of_user_chatList(SqliteConnection& conn, int64_t userId) { - SqliteStatement req(conn, "SELECT `chatList_HistoryId` FROM `user` WHERE `id` = ?1", {{1, userId}}, {}); - fsql_integer_or_null HistoryId; - int status = sqlite_stmt_step(req, {{0, &HistoryId}}, {}); - een9_ASSERT_pl(status == SQLITE_ROW); - return HistoryId.value; - } - - void internalapi_pollEvents_in_chat_collect_membership_events(SqliteConnection& conn, - std::vector& events, int64_t chatId, int64_t LocalHistoryId) { - SqliteStatement membership_changes(conn, - "SELECT `userId`, `role`, FROM `user_chat_membership` WHERE `chatId` = ?1 " - "AND `chat_IncHistoryId` > ?2", {{1, chatId}, {2, LocalHistoryId}}, {}); - fsql_integer_or_null ev_userId; - fsql_integer_or_null ev_user_role; - while (true) { - int status = sqlite_stmt_step(membership_changes, - {{0, &ev_userId}, {1, &ev_user_role}}, {}); - if (status != SQLITE_ROW) - break; - events.emplace_back(); - json::JSON& event = events.back(); - event["member"] = json::JSON(ev_userId.value); - if (ev_user_role.value == user_chat_role_deleted) { - event["type"] = json::JSON("removedMember"); - } else { - event["type"] = json::JSON("addedMember"); - RowUser_Content USER = lookup_user_content(conn, ev_userId.value); - event["content"]["name"] = json::JSON(USER.name); - event["content"]["nickname"] = json::JSON(USER.nickname); - event["content"]["role"] = json::JSON(stringify_user_chat_role(ev_user_role.value)); - } - events.push_back(event); - } - } - - void internalapi_pollEvents_in_chat_collect_messages_events(SqliteConnection& conn, - std::vector& events, int64_t chatId, int64_t LocalHistoryId) { - SqliteStatement messages_changes(conn, - "SELECT `id`, `previous`, `senderUserId`, `exists`, `isSystem`, `text` FROM `messages` WHERE " - "WHERE `chatId` = ?1 AND `chat_IncHistoryId` > ?2", {{1, chatId}, {2, LocalHistoryId}}, {}); - fsql_integer_or_null ev_msgId, ev_previousMsgId, msgSenderUserId, msgExists, msgIsSystem; - fsql_text8_or_null msgText; - while (true) { - int status = sqlite_stmt_step(messages_changes, - {{0, &ev_msgId}, {1, &ev_previousMsgId}, {2, &msgSenderUserId}, {3, &msgExists}, {4, &msgIsSystem}}, - {{5, &msgText}}); - if (status != SQLITE_ROW) - break; - events.emplace_back(); - json::JSON& event = events.back(); - event["type"] = json::JSON("newMessage"); - event["id"] = json::JSON(ev_msgId.value); - event["previous"] = json::JSON(ev_previousMsgId.value); - event["content"]["sender"] = json::JSON(msgSenderUserId.value); - event["content"]["isSystem"] = json::JSON((bool)msgIsSystem.value); - event["content"]["text"] = json::JSON(msgText.value); - events.push_back(event); - } - } - - void internalapi_pollEvents_in_user_chatList_collect_events(SqliteConnection& conn, - std::vector& events, int64_t userId, int64_t LocalHistoryId) { - SqliteStatement membership_changes(conn, - "SELECT `chatId`, `role` FROM `user_chat_membership` WHERE `userId` = ?1 " - "AND `user_chatList_IncHistoryId` > ?2", {{1, userId}, {2, LocalHistoryId}}); - fsql_integer_or_null ev_chatId, usersRoleHere; - while (true) { - int status = sqlite_stmt_step(membership_changes, {{0, &ev_chatId}, {1, &usersRoleHere}}, {}); - if (status != SQLITE_ROW) - break; - events.emplace_back(); - json::JSON& event = events.back(); - event["id"] = json::JSON(ev_chatId.value); - if (usersRoleHere.value == user_chat_role_deleted) { - event["type"] = json::JSON("removedChat"); - } else { - event["type"] = json::JSON("addedChat"); - RowChat_Content CHAT = lookup_chat_content(conn, ev_chatId.value); - event["content"]["name"] = json::JSON(CHAT.name); - event["content"]["nickname"] = json::JSON(CHAT.nickname); - event["content"]["lastMsgId"] = json::JSON(CHAT.lastMsgId); - event["content"]["roleHere"] = json::JSON(stringify_user_chat_role(usersRoleHere.value)); - } - } - } - - json::JSON internalapi_pollEvents(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) { - json::JSON Recv; - Recv["status"] = json::JSON(0l); - Recv["update"] = json::JSON(json::array); - const std::vector& req_scope = Sent["scope"].g().asArray(); - std::vector& updated = Recv["update"].g().asArray(); - for (const json::JSON& hist_entity_request: req_scope) { - updated.emplace_back(); - json::JSON& hist_entity_response = updated.back(); - hist_entity_response["type"] = hist_entity_request["type"].g(); - hist_entity_response["events"] = json::JSON(json::array); - std::vector& events = hist_entity_response["events"].g().asArray(); - const int64_t LocalHistoryId = hist_entity_request["LocalHistoryId"].g().asInteger().get_int(); - if (hist_entity_request["type"].g().asString() == "chat") { - int64_t chatId = hist_entity_request["chatId"].g().asInteger().get_int(); - if (get_role_of_user_in_chat(conn, uid, chatId) == user_chat_role_deleted) - een9_THROW("internalapi/pollEvents: trying to access chat that user does not belong to"); - hist_entity_response["chatId"] = json::JSON(chatId); - hist_entity_response["HistoryId"] = json::JSON(get_current_history_id_of_chat(conn, chatId)); - /* Two classes of 'real events' can happen to chat: membership table change, message table change */ - /* Here, I collect membership changes (related to this chat) */ - internalapi_pollEvents_in_chat_collect_membership_events(conn, events, chatId, LocalHistoryId); - /* Here, I collect message changes (related to this chat) */ - internalapi_pollEvents_in_chat_collect_messages_events(conn, events, chatId, LocalHistoryId); - } else if (hist_entity_request["type"].g().asString() == "chatlist") { - hist_entity_response["HistotyId"] = json::JSON(get_current_history_id_of_user_chatList(conn, uid)); - internalapi_pollEvents_in_user_chatList_collect_events(conn, events, uid, LocalHistoryId); - } else - een9_THROW("Bad request"); - } - return Recv; - } -} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_chat.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_chat.cpp index 24cd323..6fa5cdc 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_chat.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_chat.cpp @@ -1,8 +1,61 @@ #include "client_server_interact.h" +#include +#include "../str_fields.h" + namespace iu9cawebchat { std::string when_page_chat(WorkerGuestData& wgd, const json::JSON& config_presentation, - const een9::ClientRequest& req, const json::JSON& userinfo) { - return RTEE("chat", config_presentation, wgd, userinfo); + const een9::ClientRequest& req, const json::JSON& userinfo) { + + std::vector path_segs = {}; + path_segs.reserve(4); + if (req.uri_path.empty() || req.uri_path[0] != '/') + return page_E404(wgd); + for (char ch: req.uri_path) { + if (ch == '/') + path_segs.emplace_back(); + else + path_segs.back() += ch; + } + // Parameter, passed from server to javascript + std::string chat_nickname; + int64_t selected_message_id = -1; + if (path_segs.size() >= 2) { + chat_nickname = std::move(path_segs[1]); + } + if (!check_nickname(chat_nickname)) + return page_E404(wgd); + bool show_chat_members = (path_segs[0] == "chat-members"); + if (path_segs.size() == 4 && !show_chat_members) { + if (path_segs[2] != "m") + return page_E404(wgd); + selected_message_id = std::stoll(path_segs[3]); + } else if (path_segs.size() != 2) { + return page_E404(wgd); + } + + if (userinfo.isNull()) + return een9::form_http_server_response_303("/"); + + RowChat_Content chatInfo; + try { + chatInfo = lookup_chat_content_by_nickname(*wgd.db, chat_nickname); + } catch (const std::exception& e) { + return page_E404(wgd); + } + if (get_role_of_user_in_chat(*wgd.db, userinfo["uid"].asInteger().get_int(), chatInfo.id) == user_chat_role_deleted) { + return page_E404(wgd); + } + + json::JSON openedchat; + openedchat["name"].asString() = chatInfo.name; + openedchat["nickname"].asString() = chatInfo.nickname; + openedchat["id"].asInteger() = json::Integer(chatInfo.id); + // -1 means that nothing was selected + openedchat["selectedMessageId"].asInteger() = json::Integer(selected_message_id); + json::JSON initial_chatUpdResp = poll_update_chat_ONE_MSG_resp(*wgd.db, chatInfo.id, selected_message_id); + if (show_chat_members) + return http_R200("chat-members", wgd, {&config_presentation, &userinfo, &openedchat, &initial_chatUpdResp}); + return http_R200("chat", wgd, {&config_presentation, &userinfo, &openedchat, &initial_chatUpdResp}); } -} \ No newline at end of file +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_list_rooms.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_list_rooms.cpp index 93d4902..39898ab 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_list_rooms.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_list_rooms.cpp @@ -6,6 +6,9 @@ namespace iu9cawebchat { if (userinfo.isNull()) { return een9::form_http_server_response_303("/login"); } - return RTEE("list-rooms", config_presentation, wgd, userinfo); + json::JSON initial_chatListUpdResp = poll_update_chat_list_resp(*wgd.db, + userinfo["uid"].asInteger().get_int(), 0); + printf("%s\n", json::generate_str(initial_chatListUpdResp, json::print_pretty).c_str()); + return http_R200("list-rooms", wgd, {&config_presentation, &userinfo, &initial_chatListUpdResp}); } } diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_login.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_login.cpp index 83dddf1..4828bcd 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_login.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_login.cpp @@ -18,21 +18,27 @@ namespace iu9cawebchat { if (cmp.first == "password") password = cmp.second; } - een9_ASSERT(check_nickname(nickname), "/login/accpet-data rejected impossible nickname"); - een9_ASSERT(check_password(password), "/login/accpet-data rejected impossible password"); + if (!check_nickname(nickname)) + een9_THROW("/login/accpet-data rejected impossible nickname"); + if (!check_password(password)) + een9_THROW("/login/accpet-data rejected impossible password"); uid = find_user_by_credentials(*wgd.db, nickname, password); } catch(const std::exception& e){} if (uid < 0) { printf("Redirecting back to /login because of incorrect credentials\n"); - /* todo: Here I need to tell somehow to user (through fancy red box, maybe), that login was incorrect */ - return RTEE("login", config_presentation, wgd, userinfo); + json::JSON msg_list = jsonify_html_message_list({{"", + config_presentation["login"]["incorrect-nickname-or-password"].asString()}}); + return http_R200("login", wgd, {&config_presentation, &userinfo, &msg_list}); } std::vector> response_hlines; LoginCookie new_login_cookie = create_login_cookie(nickname, password); add_set_cookie_headers_to_login(login_cookies, response_hlines, new_login_cookie); return een9::form_http_server_response_303_spec_head("/", response_hlines); } - return RTEE("login", config_presentation, wgd, userinfo); + if (req.method != "GET") + een9_THROW("Bad method"); + json::JSON empty_msg_list = json::JSON(json::array); + return http_R200("login", wgd, {&config_presentation, &userinfo, &empty_msg_list}); } } diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_register.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_register.cpp new file mode 100644 index 0000000..2fbc733 --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_register.cpp @@ -0,0 +1,56 @@ +#include "client_server_interact.h" +#include +#include +#include +#include "../str_fields.h" + +namespace iu9cawebchat { + std::string when_page_register(WorkerGuestData& wgd, const json::JSON& config_presentation, + const een9::ClientRequest& req, const json::JSON& userinfo) { + + const json::JSON& reg_pres = config_presentation["register"]; + json::JSON msg_list = json::JSON(json::array); + if (req.method == "POST") { + if (userinfo.isNull() || userinfo["uid"].asInteger().get_int() != 0) + een9_THROW("Unauthorized access"); + // Kod dlya dobaldal lkslkfjgk + std::vector> query = een9::split_html_query(req.body); + std::string nickname; + std::string name; + std::string password; + std::vector problems; // We explain problem to root + for (const std::pair& cmp: query) { + if (cmp.first == "nickname") + nickname = cmp.second; + if (cmp.first == "name") + name = cmp.second; + if (cmp.first == "password") + password = cmp.second; + } + if (!check_nickname(nickname)) { + problems.push_back({"", reg_pres["incorrect-nickname"].asString()}); + } + if (!check_name(name)) { + problems.push_back({"", reg_pres["incorrect-name"].asString()}); + } + if (!check_strong_password(password)) { + problems.push_back({"", reg_pres["incorrect-password"].asString()}); + } + if (is_nickname_taken(*wgd.db, nickname)) { + problems.push_back({"", reg_pres["nickname-taken"].asString()}); + } + if (problems.empty()) { + try { + add_user(*wgd.db, nickname, name, password, ""); + } catch (std::exception& err) { + problems.push_back({"", reg_pres["add_user_error"].asString()}); + } + } + msg_list = jsonify_html_message_list(problems); + return http_R200("register", wgd, {&config_presentation, &userinfo, &msg_list}); + } + if (userinfo.isNull() || userinfo["uid"].asInteger().get_int() != 0) + return page_E404(wgd); + return http_R200("register", wgd, {&config_presentation, &userinfo, &msg_list}); + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_user.cpp b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_user.cpp index 532a9f0..2137387 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_user.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/backend_logic/when_user.cpp @@ -1,8 +1,123 @@ #include "client_server_interact.h" +#include +#include +#include +#include "../str_fields.h" +#include "../login_cookie.h" namespace iu9cawebchat { + std::string get_user_bio(SqliteConnection& conn, int64_t userId) { + een9_ASSERT(userId >= 0, "Are you crazy?"); + SqliteStatement sql_req(conn, "SELECT `bio` FROM `user` WHERE `id` = ?1", {{1, userId}}, {}); + fsql_text8_or_null bio_col; + int status = sqlite_stmt_step(sql_req, {}, {{0, &bio_col}}); + if (status == SQLITE_ROW) + return bio_col.value; + een9_THROW("No such user"); + } + + static const char* pr_path = "/user/"; + + bool is_page_of_certain_user(const std::string& path) { + if (!een9::beginsWith(path, pr_path)) + return false; + std::string r = path.substr(strlen(pr_path)); + return check_nickname(r); + } + + std::string obtain_nickname_in_user_page_path(const std::string& path) { + return path.substr(strlen(pr_path)); + } + + json::JSON user_row_to_userprofile_obj(SqliteConnection& conn, const RowUser_Content& alien) { + return json::JSON(json::jdict{ + {"name", json::JSON(alien.name)}, + {"nickname", json::JSON(alien.nickname)}, + {"bio", json::JSON(get_user_bio(conn, alien.id))} + }); + } + std::string when_page_user(WorkerGuestData& wgd, const json::JSON& config_presentation, - const een9::ClientRequest& req, const json::JSON& userinfo) { - return RTEE("profile", config_presentation, wgd, userinfo); + const een9::ClientRequest& req, const std::vector& login_cookies, const json::JSON& userinfo) { + if (userinfo.isNull()) + return een9::form_http_server_response_303("/"); + SqliteConnection& conn = *wgd.db; + if (!is_page_of_certain_user(req.uri_path)) + return page_E404(wgd); + std::string alien_nickname = obtain_nickname_in_user_page_path(req.uri_path); + RowUser_Content alien; + try { + alien = lookup_user_content_by_nickname(conn, alien_nickname); + } catch (const std::exception& e) { + return page_E404(wgd); + } + // todo: in libjsonincpp: fix '999999 problem' + bool can_edit = false; + int64_t myuid = -1; + if (userinfo.isDictionary()) { + myuid = userinfo["uid"].asInteger().get_int(); + can_edit = (alien.id == myuid && myuid >= 0); + } + if (req.method == "POST") { + std::vector> response_hlines; + try { + if (!can_edit) + een9_THROW("Unauthorized access"); + std::vector> query = een9::split_html_query(req.body); + // Profile update processing + std::string bio; + std::string name; + std::string password; + for (const std::pair& p: query) { + if (p.first == "bio") + bio = p.second; + else if (p.first == "name") + name = p.second; + else if (p.first == "password") + password = p.second; + } + if (!bio.empty()) { + if (!is_orthodox_string(bio) || bio.size() > 100000) + een9_THROW("Incorrect `bio`"); + sqlite_nooutput(conn, + "UPDATE `user` SET `bio` = ?1 WHERE `id` = ?2", + {{2, alien.id}}, {{1, bio}}); + } + if (!name.empty()) { + if (!check_name(name)) + een9_THROW("Incorrect `name`"); + sqlite_nooutput(conn, + "UPDATE `user` SET `name` = ?1 WHERE `id` = ?2", + {{2, alien.id}}, {{1, name}}); + } + if (!password.empty()) { + if (!check_strong_password(password)) + een9_THROW("Incorrect `password`"); + sqlite_nooutput(conn, + "UPDATE `user` SET `password` = ?1 WHERE `id` = ?2", + {{2, alien.id}}, {{1, password}}); + if (alien.id == myuid) { + LoginCookie new_login_cookie = create_login_cookie(userinfo["nickname"].asString(), password); + add_set_cookie_headers_to_login(login_cookies, response_hlines, new_login_cookie); + } + } + } catch (const std::exception& e) { + printf("Redirecting back to /user/... because of incorrect credentials\n"); + json::JSON msg_list = jsonify_html_message_list({{"", + config_presentation["edit-profile"]["incorrect-profile-data"].asString()}}); + json::JSON alien_userprofile = user_row_to_userprofile_obj(conn, alien); + return http_R200("edit-profile", wgd, {&config_presentation, &userinfo, &alien_userprofile, &msg_list}); + } + return een9::form_http_server_response_303_spec_head("/user/" + alien_nickname, response_hlines); + } + if (req.method == "GET") { + json::JSON alien_userprofile = user_row_to_userprofile_obj(conn, alien); + if (can_edit) { + json::JSON empty_msg_list = jsonify_html_message_list({}); + return http_R200("edit-profile", wgd, {&config_presentation, &userinfo, &alien_userprofile, &empty_msg_list}); + } + return http_R200("view-profile", wgd, {&config_presentation, &userinfo, &alien_userprofile}); + } + een9_THROW("Bad method"); } } diff --git a/src/web_chat/iu9_ca_web_chat_lib/debug.cpp b/src/web_chat/iu9_ca_web_chat_lib/debug.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/web_chat/iu9_ca_web_chat_lib/debug.h b/src/web_chat/iu9_ca_web_chat_lib/debug.h new file mode 100644 index 0000000..3d21e02 --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/debug.h @@ -0,0 +1,6 @@ +#ifndef IU9_CA_WEB_CHAT_LIB_DEBUG_H +#define IU9_CA_WEB_CHAT_LIB_DEBUG_H + +#define debug_print_json(x) printf("%s\n", json::generate_str(x, json::print_pretty).c_str()) + +#endif diff --git a/src/web_chat/iu9_ca_web_chat_lib/find_db.cpp b/src/web_chat/iu9_ca_web_chat_lib/find_db.cpp index 56e39e4..5d5f050 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/find_db.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/find_db.cpp @@ -2,15 +2,17 @@ namespace iu9cawebchat{ int find_db_sqlite_file_path(const json::JSON& config, std::string& res_path) { - const json::JSON& type = config["database"]["type"].g(); - if (!type.isString() && type.asString() == "sqlite3") + try { + const std::string& type = config["database"]["type"].asString(); + if (type != "sqlite3") + return -1; + const std::string& path = config["database"]["file"].asString(); + if (path.empty() || path[0] == ':') + return -1; + res_path = path; + } catch (const json::misuse& e) { return -1; - const json::JSON& path = config["database"]["file"].g(); - if (!path.isString()) - return -1; - if (path.asString().empty() || path.asString()[0] == ':') - return -1; - res_path = path.asString(); + } return 0; } } diff --git a/src/web_chat/iu9_ca_web_chat_lib/initialize.cpp b/src/web_chat/iu9_ca_web_chat_lib/initialize.cpp index d5cd081..0d2a042 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/initialize.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/initialize.cpp @@ -7,24 +7,27 @@ #include #include #include "sqlite3_wrapper.h" +#include "backend_logic/server_data_interact.h" namespace iu9cawebchat { void initialize_website(const json::JSON& config, const std::string& root_pw) { printf("Initialization...\n"); - een9_ASSERT(check_password(root_pw), "Bad root password"); + if (!check_strong_password(root_pw)) + een9_THROW("Bad root password"); std::string db_path; int ret; ret = find_db_sqlite_file_path(config, db_path); - een9_ASSERT(ret == 0, "Invalid settings[\"database\"] field"); + if (ret != 0) + een9_THROW("Invalid settings[\"database\"] field"); if (een9::isRegularFile(db_path)) { // todo: plaese, don't do this ret = unlink(db_path.c_str()); - een9_ASSERT_pl(ret == 0); + if (ret != 0) + een9_THROW("unlink"); } - een9_ASSERT(!een9::isRegularFile(db_path), "Database file exists prior to initialization. " - "Can't preceed withut harming existing data"); + if (een9::isRegularFile(db_path)) + een9_THROW("Database file exists prior to initialization. Can't preceed withut harming existing data"); SqliteConnection conn(db_path.c_str()); - assert(sqlite3_errcode(conn.hand) == SQLITE_OK); /* Role of memeber of chat: * 1 - admin * 2 - regular @@ -44,36 +47,38 @@ namespace iu9cawebchat { "`nickname` TEXT UNIQUE REFERENCES `nickname` NOT NULL," "`name` TEXT NOT NULL," "`chatList_HistoryId` INTEGER NOT NULL," - "`password` TEXT NOT NULL" + "`password` TEXT NOT NULL," + "`bio` TEXT NOT NULL" ")"); sqlite_nooutput(conn, "CREATE TABLE `chat` (" "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," "`nickname` TEXT UNIQUE REFERENCES `nickname` NOT NULL," "`name` TEXT NOT NULL," "`it_HistoryId` INTEGER NOT NULL," - "`lastMsgId` INTEGER REFERENCES `message`" + "`lastMsgId` INTEGER NOT NULL" ")"); sqlite_nooutput(conn, "CREATE TABLE `user_chat_membership` (" "`userId` INTEGER REFERENCES `user` NOT NULL," "`chatId` INTEGER REFERENCES `chat` NOT NULL," "`user_chatList_IncHistoryId` INTEGER NOT NULL," "`chat_IncHistoryId` INTEGER NOT NULL," - "`role` INTEGER NOT NULL" + "`role` INTEGER NOT NULL," + "UNIQUE (`userId`, `chatId`)" ")"); sqlite_nooutput(conn, "CREATE TABLE `message` (" - "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," "`chatId` INTEGER REFERENCES `chat` NOT NULL," - "`previous` INTEGER REFERENCES `message`," - "`senderUserId` INTEGER REFERENCES `user` NOT NULL," + "`id` INTEGER NOT NULL," + "`senderUserId` INTEGER REFERENCES `user`," "`exists` BOOLEAN NOT NULL," "`isSystem` BOOLEAN NOT NULL," - "`text` TEXT NOT NULL," - "`chat_IncHistoryId` INTEGER NOT NULL" + "`text` TEXT," + "`chat_IncHistoryId` INTEGER NOT NULL," + "PRIMARY KEY (`chatId`, `id`)" ")"); - sqlite_nooutput(conn, "INSERT INTO `nickname` VALUES (?1)", {}, {{1, "root"}}); - sqlite_nooutput(conn, "INSERT INTO `user` (`id`, `nickname`, `name`, `chatList_HistoryId`, `password`) VALUES " - "(0, ?1, ?2, 0, ?3)", {}, - {{1, "root"}, {2, "Rootov Root Rootovich"}, {3, root_pw}}); + std::vector sus = {"unknown", "undefined", "null", "none", "None", "NaN"}; + for (auto& s: sus) + reserve_nickname(conn, s); + add_user(conn, "root", "Rootov Root Rootovich", root_pw, "One admin to rule them all", 0); sqlite_nooutput(conn, "END"); } catch (const std::exception& e) { sqlite_nooutput(conn, "ROLLBACK", {}, {}); diff --git a/src/web_chat/iu9_ca_web_chat_lib/localizator.cpp b/src/web_chat/iu9_ca_web_chat_lib/localizator.cpp new file mode 100644 index 0000000..880b15c --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/localizator.cpp @@ -0,0 +1,154 @@ +#include "localizator.h" + +#include +#include +#include +#include +#include +#include + +namespace iu9cawebchat { + std::string languageRangeSimpler(const std::string& a) { + return a == "*" ? "" : a; + } + + // I won't use iterators. c plus plus IS a scripting language and I do not want to mess with iterators + std::vector languageRangeGetPrefixes(const std::string& lr) { + if (lr.empty()) + return {""}; + std::vector result = {"", ""}; + for (size_t i = 0; i < lr.size(); i++) { + if (lr[i] == '-') + result.push_back(result.back()); + result.back() += lr[i]; + + } + return result; + } + + bool isInWhitelist(const std::string& lr, const std::vector& whitelist) { + for (const std::string& prefix : languageRangeGetPrefixes(lr)) + for (const std::string& nicePrefix: whitelist) + if (prefix == nicePrefix) + return true; + return false; + } + + std::vector collect_lang_dir_content(const std::string& lang_dir, + const std::vector& whitelist) { + + std::vector result; + errno = 0; + DIR* D = opendir(lang_dir.c_str()); + struct Guard1{ DIR*& D; ~Guard1(){ closedir(D); } } g1{D}; + if (!D) + een9_THROW_on_errno("opendir (" + lang_dir + ")"); + while (true) { + errno = 0; + struct dirent* Dent = readdir(D); + if (Dent == NULL) { + if (errno == 0) + break; + een9_THROW_on_errno("dirent"); + } + std::string entry = Dent->d_name; + if (entry == "." || entry == "..") + continue; + std::string filename = lang_dir + "/" + entry; + struct stat info; + int ret = stat(filename.c_str(), &info); + een9_ASSERT_on_iret(ret, "stat(" + filename + ")"); + if (!S_ISREG(info.st_mode)) + continue; + const std::string postfix = ".lang.json"; + if (!een9::endsWith(entry, postfix)) + continue; + std::string lang_antirange = entry.substr(0, entry.size() - postfix.size()); + if (!isInWhitelist(lang_antirange, whitelist)) + continue; + std::string content; + een9::readFile(filename, content); + + result.emplace_back(); + result.back().languagerange = languageRangeSimpler(lang_antirange); + result.back().content = json::parse_str_flawless(content); + } + return result; + } + + + Localizator::Localizator(const LocalizatorSettings &settings) : settings(settings) { + /* First - length of the longest prefix that was found so far (in force_order) + * Second - index in force_order that was assigned to this thingy + */ + + files = collect_lang_dir_content(settings.lang_dir, settings.whitelist); + size_t n = files.size(); +#define redundantFileMsg "Redundant localization file" + for (size_t i = 0; i < n; i++) { + for (size_t j = i + 1; j < n; j++) { + std::string A = files[i].languagerange; + std::string B = files[j].languagerange; + for (std::string& pa: languageRangeGetPrefixes(A)) + if (pa == B) + een9_THROW(redundantFileMsg); + for (std::string& pb: languageRangeGetPrefixes(B)) + if (pb == A) + een9_THROW(redundantFileMsg); + } + } + std::map> pref_to_files; + for (size_t k = 0; k < n; k++) { + for (const std::string& prefix: languageRangeGetPrefixes(files[k].languagerange)) { + pref_to_files[prefix].push_back(k); + } + } + std::vector> assignment; + constexpr size_t inf_bad_order = 999999999; + assignment.assign(n, {0, inf_bad_order}); + if (settings.force_order.size() >= inf_bad_order - 2) + een9_THROW("o_O"); + for (ssize_t i = 0; i < settings.force_order.size(); i++) { + const std::string& ip = settings.force_order[i]; + if (pref_to_files.count(ip) != 1) + een9_THROW("force-order list contains entries that match no files (" + ip + ")"); + for (size_t k: pref_to_files.at(ip)) { + if (assignment[k].first <= ip.size()) { + assignment[k].first = ip.size(); + assignment[k].second = i; + } + } + } + for (auto& p: pref_to_files) { + const std::vector& candidates = p.second; + assert(!candidates.empty()); + size_t bestSoFar = candidates[0]; + size_t f = inf_bad_order; + for (size_t k: candidates) { + if (assignment[k].second <= f) { + f = assignment[k].second; + bestSoFar = k; + } + } + prefix_to_file[p.first] = bestSoFar; + } + if (prefix_to_file.count("") != 1) + een9_THROW("No locales were provided"); + // todo: remove DEBUG + // for (size_t k = 0; k < n; k++) { + // printf("%s has priority %lu\n", files[k].languagerange.c_str(), assignment[k].second); + // } + // printf("==============\n"); + // for (const auto& p : prefix_to_file) { + // printf("%s -> %s\n", p.first.c_str(), files[p.second].languagerange.c_str()); + // } + } + + const LanguageFile& Localizator::get_right_locale(const std::vector &preferred_langs) { + for (const std::string& lr: preferred_langs) { + if (prefix_to_file.count(lr) == 1) + return files[prefix_to_file.at(lr)]; + } + return files[prefix_to_file.at("")]; + } +} diff --git a/src/web_chat/iu9_ca_web_chat_lib/localizator.h b/src/web_chat/iu9_ca_web_chat_lib/localizator.h new file mode 100644 index 0000000..7627a59 --- /dev/null +++ b/src/web_chat/iu9_ca_web_chat_lib/localizator.h @@ -0,0 +1,36 @@ +#ifndef IU9_CA_WEB_CHAT_LIB_LOCALIZATOR_H +#define IU9_CA_WEB_CHAT_LIB_LOCALIZATOR_H + +#include + +namespace iu9cawebchat { + /* '*' -> ''; X -> X */ + std::string languageRangeSimpler(const std::string& a); + + struct LocalizatorSettings { + std::string lang_dir; + std::vector whitelist; + std::vector force_order; + }; + + /* There is no need to put http Content-Language response value into json file. When is is in the name */ + struct LanguageFile { + std::string languagerange; + json::JSON content; + }; + + /* Localizator uses libjsonincpp internally, and thus can't be read by two treads simultaneously */ + struct Localizator { + LocalizatorSettings settings; + std::vector files; + std::map prefix_to_file; + + /* Throws std::exception if something goes wrong */ + explicit Localizator(const LocalizatorSettings& settings); + + /* Returns a reference to object inside Localizator */ + const LanguageFile& get_right_locale(const std::vector& preferred_langs); + }; +} + +#endif diff --git a/src/web_chat/iu9_ca_web_chat_lib/login_cookie.cpp b/src/web_chat/iu9_ca_web_chat_lib/login_cookie.cpp index 193a8e5..571d85d 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/login_cookie.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/login_cookie.cpp @@ -23,8 +23,8 @@ namespace iu9cawebchat { uint64_t nsec = std::stoull(ft.substr(s_ + 1, ft.size() - s_ - 1)); een9_ASSERT_pl(nsec < 1000000000); const json::JSON cnt = json::parse_str_flawless(base64_decode(login_cookie_encoded.second)); - std::string nickname = cnt[0].g().asString(); - std::string password = cnt[1].g().asString(); + std::string nickname = cnt[0].asString(); + std::string password = cnt[1].asString(); return LoginCookie{{(time_t)sec, (time_t)nsec}, nickname, password}; } @@ -37,8 +37,8 @@ namespace iu9cawebchat { std::pair encode_login_cookie(const LoginCookie& cookie) { json::JSON cnt; - cnt[1].g() = cookie.password; - cnt[0].g() = cookie.nickname; + cnt[1].asString() = cookie.password; + cnt[0].asString() = cookie.nickname; return {"login_" + std::to_string(cookie.login_time.tv_sec) + "_" + std::to_string(cookie.login_time.tv_nsec), base64_encode(json::generate_str(cnt, json::print_compact))}; } diff --git a/src/web_chat/iu9_ca_web_chat_lib/run.cpp b/src/web_chat/iu9_ca_web_chat_lib/run.cpp index 1863a21..5c7340e 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/run.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/run.cpp @@ -5,6 +5,7 @@ #include #include "find_db.h" #include +#include #include #include "str_fields.h" #include "backend_logic/client_server_interact.h" @@ -16,22 +17,50 @@ namespace iu9cawebchat { termination = true; } + struct ONE_SQLITE_TRANSACTION_GUARD { + SqliteConnection& conn; + bool rollback = false; + + explicit ONE_SQLITE_TRANSACTION_GUARD(SqliteConnection& conn_) : conn(conn_) { + sqlite_nooutput(conn, "BEGIN", {}, {}); + } + + ~ONE_SQLITE_TRANSACTION_GUARD() { + if (rollback) + sqlite_nooutput(conn, "ROLLBACK", {}, {}); + else + sqlite_nooutput(conn, "END", {}, {}); + } + }; + + LocalizatorSettings make_localizator_settings(const std::string& assets_dir, const json::JSON& config) { + std::vector whitelist; + for (const json::JSON& entry: config["lang"]["whitelist"].asArray()) + whitelist.push_back(languageRangeSimpler(entry.asString())); + std::vector force_order; + for (const json::JSON& entry: config["lang"]["force-order"].asArray()) + force_order.push_back(languageRangeSimpler(entry.asString())); + return LocalizatorSettings{assets_dir + "/lang", whitelist, force_order}; + } + void run_website(const json::JSON& config) { - een9_ASSERT(config["assets"].g().isString(), "config[\"assets\"] is not string"); - std::string assets_dir = config["assets"].g().asString(); + een9_ASSERT(config["assets"].isString(), "config[\"assets\"] is not string"); + const std::string& assets_dir = config["assets"].asString(); een9_ASSERT(een9::isDirectory(assets_dir), "\"" + assets_dir + "\" is not a directory"); + LocalizatorSettings localizator_settings = make_localizator_settings(assets_dir, config); + een9::StaticAssetManagerSlaveModule samI; samI.update({ een9::StaticAssetManagerRule{assets_dir + "/css", "/assets/css", {{".css", "text/css"}} }, een9::StaticAssetManagerRule{assets_dir + "/js", "/assets/js", {{".js", "text/javascript"}} }, + een9::StaticAssetManagerRule{assets_dir + "/gif", "/assets/gif", {{".gif", "image/gif"}} }, een9::StaticAssetManagerRule{assets_dir + "/img", "/assets/img", { {".jpg", "image/jpg"}, {".png", "image/png"}, {".svg", "image/svg+xml"} } }, }); - const json::JSON& config_presentation = config["presentation"].g(); - int64_t slave_number = config["server"]["workers"].g().asInteger().get_int(); + int64_t slave_number = config["server"]["workers"].asInteger().get_int(); een9_ASSERT(slave_number > 0 && slave_number <= 200, "E"); std::string sqlite_db_path; @@ -44,21 +73,24 @@ namespace iu9cawebchat { nytl::TemplaterSettings{nytl::TemplaterDetourRules{assets_dir + "/HypertextPages"}}); worker_guest_data[i].templater->update(); worker_guest_data[i].db = std::make_unique(sqlite_db_path); + worker_guest_data[i].locales = std::make_unique(localizator_settings); } een9::MainloopParameters params; - params.guest_core = [&samI, &worker_guest_data, config_presentation] + params.guest_core = [&samI, &worker_guest_data] (const een9::SlaveTask& task, const een9::ClientRequest& req, een9::worker_id_t worker_id) -> std::string { een9_ASSERT_pl(0 <= worker_id && worker_id < worker_guest_data.size()); WorkerGuestData& wgd = worker_guest_data[worker_id]; een9::StaticAsset sa; - sqlite_nooutput(*wgd.db, "BEGIN", {}, {}); - struct guard {SqliteConnection& conn; bool rollback = false; ~guard() { - if (rollback) - sqlite_nooutput(conn, "ROLLBACK", {}, {}); - else - sqlite_nooutput(conn, "END", {}, {}); - }} guard_{*wgd.db}; + ONE_SQLITE_TRANSACTION_GUARD conn_guard(*wgd.db); + std::string AcceptLanguage; + for (const std::pair& p: req.headers) { + if (p.first == "Accept-Language") + AcceptLanguage = p.second; + } + std::vector AcceptLanguageB = een9::parse_header_Accept_Language(AcceptLanguage); + const LanguageFile& locale = wgd.locales->get_right_locale(AcceptLanguageB); + const json::JSON& pres = locale.content; try { std::vector> cookies; std::vector login_cookies; @@ -67,38 +99,50 @@ namespace iu9cawebchat { initial_extraction_of_all_the_useful_info_from_cookies(*wgd.db, req, cookies, login_cookies, userinfo, logged_in_user); if (req.uri_path == "/" || req.uri_path == "/list-rooms") { - return when_page_list_rooms(wgd, config_presentation, req, userinfo); + return when_page_list_rooms(wgd, pres, req, userinfo); } if (req.uri_path == "/login") { - return when_page_login(wgd, config_presentation, req, login_cookies, userinfo); + return when_page_login(wgd, pres, req, login_cookies, userinfo); } - if (een9::beginsWith(req.uri_path, "/chat")) { - return when_page_chat(wgd, config_presentation, req, userinfo); + // todo: split + if (een9::beginsWith(req.uri_path, "/chat/") || een9::beginsWith(req.uri_path, "/chat-members/")) { + return when_page_chat(wgd, pres, req, userinfo); } - if (req.uri_path == "/user") { - return when_page_user(wgd, config_presentation, req, userinfo); + if (een9::beginsWith(req.uri_path, "/user/")) { + return when_page_user(wgd, pres, req, login_cookies, userinfo); } - if (req.uri_path == "/internalapi/pollEvents") { - return when_internalapi_pollevents(wgd, req, logged_in_user); + if (req.uri_path == "/register") { + return when_page_register(wgd, pres, req, userinfo); } - if (req.uri_path == "/internalapi/getChatList") { - return when_internalapi_getchatlist(wgd, req, logged_in_user); + if (req.uri_path == "/api/chatPollEvents") { + return when_internalapi_chatpollevents(wgd, req, logged_in_user); } - if (req.uri_path == "/internalapi/getChatInfo") { - return when_internalapi_getchatinfo(wgd, req, logged_in_user); + if (req.uri_path == "/api/chatListPollEvents") { + return when_internalapi_chatlistpollevents(wgd, req, logged_in_user); } - if (req.uri_path == "/internalapi/getChatMemberList") { - return when_internalapi_getchatmemberlist(wgd, req, logged_in_user); + if (req.uri_path == "/api/getMessageNeighbours") { + return when_internalapi_getmessageneighbours(wgd, req, logged_in_user); } - if (req.uri_path == "/internalapi/getUserInfo") { - return when_internalapi_getuserinfo(wgd, req, logged_in_user); + if (req.uri_path == "/api/sendMessage") { + return when_internalapi_sendmessage(wgd, req, logged_in_user); } - if (req.uri_path == "/internalapi/getMessageInfo") { - return when_internalapi_getmessageinfo(wgd, req, logged_in_user); + if (req.uri_path == "/api/deleteMessage") { + return when_internalapi_deletemessage(wgd, req, logged_in_user); + } + if (req.uri_path == "/api/addMemberToChat") { + return when_internalapi_addmembertochat(wgd, req, logged_in_user); + } + if (req.uri_path == "/api/removeMemberFromChat") { + return when_internalapi_removememberfromchat(wgd, req, logged_in_user); + } + if (req.uri_path == "/api/createChat") { + return when_internalapi_createchat(wgd, req, logged_in_user); + } + if (req.uri_path == "/api/leaveChat") { + return when_internalapi_leavechat(wgd, req, logged_in_user); } - // todo: write all the other interfaces } catch (const std::exception& e) { - guard_.rollback = true; + conn_guard.rollback = true; throw; } /* Trying to interpret request as asset lookup */ @@ -113,29 +157,11 @@ namespace iu9cawebchat { (const een9::SlaveTask& task, const std::string& req, een9::worker_id_t worker_id) -> std::string { een9_ASSERT_pl(0 <= worker_id && worker_id < worker_guest_data.size()); WorkerGuestData& wgd = worker_guest_data[worker_id]; + ONE_SQLITE_TRANSACTION_GUARD conn_guad(*wgd.db); try { - if (req == "hello") { - return ":0 omg! hiii!! Hewwou :3 !!!!\n"; - } - if (req == "8") { - termination = true; - return "Bye\n"; - } - std::string updaterootpw_pref = "updaterootpw"; - if (een9::beginsWith(req, "updaterootpw")) { - size_t nid = updaterootpw_pref.size(); - if (nid >= req.size() || !isSPACE(req[nid])) - return "Bad command syntax. Missing whitespace\n"; - std::string new_password = req.substr(nid + 1); - if (!check_password(new_password)) - een9_THROW("Bad password"); - sqlite_nooutput(*wgd.db, - "UPDATE `user` SET `password` = ?1 WHERE `id` = 0 ", - {}, {{1, new_password}}); - return "Successul update\n"; - } - return "Incorrect command\n"; + return admin_control_procedure(*wgd.db, req, termination); } catch (std::exception& e) { + conn_guad.rollback = true; return std::string("Server error\n") + e.what(); } }; @@ -151,8 +177,8 @@ namespace iu9cawebchat { een9_ASSERT(ret == 0, "Incorrect ear address: " + source[i].asString()); } }; - translate_addr_list_conf(params.client_regular_listened, config["server"]["http-listen"].g().asArray()); - translate_addr_list_conf(params.admin_control_listened, config["server"]["admin-command-listen"].g().asArray()); + translate_addr_list_conf(params.client_regular_listened, config["server"]["http-listen"].asArray()); + translate_addr_list_conf(params.admin_control_listened, config["server"]["admin-command-listen"].asArray()); signal(SIGINT, sigterm_action); signal(SIGTERM, sigterm_action); diff --git a/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.cpp b/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.cpp index 55f150c..3196e33 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.cpp @@ -76,7 +76,7 @@ namespace iu9cawebchat { int ret = sqlite3_prepare_v2(connection.hand, req_statement.c_str(), -1, &stmt_obj, NULL); if (ret != 0) { - int err_pos = sqlite3_error_offset(connection.hand); + int err_pos = -1; een9_THROW("Compilation of request\n" + req_statement + "\nfailed" + ((err_pos >= 0) ? " with offset " + std::to_string(err_pos) : "")); } @@ -101,9 +101,14 @@ namespace iu9cawebchat { sqlite3_finalize(stmt_obj); } + void sqlite_stmt_bind_int64(SqliteStatement &stmt, int paramId, int64_t value) { + int ret = sqlite3_bind_int64(stmt.stmt_obj, paramId, value); + een9_ASSERT(ret == 0, "sqlite3_bind_int64"); + } + int sqlite_stmt_step(SqliteStatement &stmt, - const std::vector> &ret_of_integer_or_null, - const std::vector> &ret_of_text8_or_null) { + const std::vector> &ret_of_integer_or_null, + const std::vector> &ret_of_text8_or_null) { int ret = sqlite3_step(stmt.stmt_obj); if (ret == SQLITE_DONE) return ret; @@ -137,4 +142,9 @@ namespace iu9cawebchat { } return SQLITE_ROW; } + + int64_t sqlite_trsess_last_insert_rowid(SqliteConnection& conn) { + int64_t res = sqlite3_last_insert_rowid(conn.hand); + return res; + } } diff --git a/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.h b/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.h index 2c880a8..8c8f6bb 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.h +++ b/src/web_chat/iu9_ca_web_chat_lib/sqlite3_wrapper.h @@ -41,9 +41,13 @@ namespace iu9cawebchat { ~SqliteStatement(); }; + void sqlite_stmt_bind_int64(SqliteStatement& stmt, int paramId, int64_t value); + int sqlite_stmt_step(SqliteStatement& stmt, const std::vector>& ret_of_integer_or_null, const std::vector>& ret_of_text8_or_null); + + int64_t sqlite_trsess_last_insert_rowid(SqliteConnection& conn); } #endif diff --git a/src/web_chat/iu9_ca_web_chat_lib/str_fields.cpp b/src/web_chat/iu9_ca_web_chat_lib/str_fields.cpp index 0c3aba5..6a502b1 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/str_fields.cpp +++ b/src/web_chat/iu9_ca_web_chat_lib/str_fields.cpp @@ -28,11 +28,15 @@ namespace iu9cawebchat { } bool check_password(const std::string &pwd) { - return is_orthodox_string(pwd) && pwd.size() >= 8; + return is_orthodox_string(pwd) && pwd.size() <= 150; + } + + bool check_strong_password(const std::string& pwd) { + return check_password(pwd) && pwd.size() >= 8; } bool check_name(const std::string &name) { - return is_orthodox_string(name); + return is_orthodox_string(name) && name.size() <= 150 && !name.empty(); } bool check_nickname(const std::string &nickname) { @@ -42,7 +46,7 @@ namespace iu9cawebchat { if (!isUNCHAR(ch)) return false; } - return true; + return nickname.size() <= 150; } /* Yeah baby, it's base64 time!!! */ diff --git a/src/web_chat/iu9_ca_web_chat_lib/str_fields.h b/src/web_chat/iu9_ca_web_chat_lib/str_fields.h index c6cc2bd..642f5ae 100644 --- a/src/web_chat/iu9_ca_web_chat_lib/str_fields.h +++ b/src/web_chat/iu9_ca_web_chat_lib/str_fields.h @@ -13,6 +13,7 @@ namespace iu9cawebchat { bool is_orthodox_string(const std::string& str); bool check_password(const std::string& pwd); + bool check_strong_password(const std::string& pwd); bool check_name(const std::string& name); bool check_nickname(const std::string& nickname); diff --git a/src/web_chat/iu9_ca_web_chat_service/service.cpp b/src/web_chat/iu9_ca_web_chat_service/service.cpp index fea961a..6374748 100644 --- a/src/web_chat/iu9_ca_web_chat_service/service.cpp +++ b/src/web_chat/iu9_ca_web_chat_service/service.cpp @@ -34,6 +34,8 @@ int main(int argc, char** argv){ iu9cawebchat::initialize_website(config, root_pw); } else if (cmd == "run") { iu9cawebchat::run_website(config); + } else if (cmd == "version") { + printf("IU9 Collarbone Annihilation Web Chat (service) V 1.0\n"); } else een9_THROW("unknown action (known are 'run', 'initialize')"); } catch (std::exception& e) {