diff --git a/.editorconfig b/dialog-sequence/.editorconfig similarity index 100% rename from .editorconfig rename to dialog-sequence/.editorconfig diff --git a/.gitattributes b/dialog-sequence/.gitattributes similarity index 100% rename from .gitattributes rename to dialog-sequence/.gitattributes diff --git a/.gitignore b/dialog-sequence/.gitignore similarity index 94% rename from .gitignore rename to dialog-sequence/.gitignore index 870eb6a..a4628ef 100644 --- a/.gitignore +++ b/dialog-sequence/.gitignore @@ -11,3 +11,6 @@ #!.yarn/cache .pnp.* + +node_modules +.idea diff --git a/dialog-sequence/README.md b/dialog-sequence/README.md new file mode 100644 index 0000000..cf2f898 --- /dev/null +++ b/dialog-sequence/README.md @@ -0,0 +1,33 @@ +# Loop Dialog Sequence Example + +Пример приложения для последовательного открытия модальных окон с формой. + +### Запуск и установка + +В файле index.js меняем переменные: +- SERVER_URL - адрес вашего сервера Loop +- LOOP_URL - адрес приложения +- PORT - по желанию + +Устанавливаем зависимости `yarn` или `npm install` + +Запускаем командой `yarn start` или `npm start` + +Переходим в Loop и устанавливаем приложение через слеш команду в любом канале: +`/apps install http _SERVER_URL_/manifest.json` +в появившемся окне ставим галочку, что мы все понимаем и подтверждаем. + +После установки должна появиться новая кнопка на правой панели или в верхней панели канала. +- Переходим в Loop и создаем тестовый канал +- Приглашаем бота ru.loop.dialog-sequence-example в команду +- Добавляем бота ru.loop.dialog-sequence-example в тестовый канал +- Нажимаем на новую кнопку "Открыть форму" (с иконкой Мистфикса) +- Заполняем форму и нажимаем отправить +- Если все хорошо, в канал придет сообщение от бота и откроется следующая форма +- Enjoy + +#### Важные замечания по работе с apps framework +- Все ответы должны быть с кодом 200 (не 201 и тд.) +- Для получения данных о канале требуется разрешение act_as_user, если его убрать то контекст канала приходить не будет +- Все иконки описываются без указания пути, Loop всегда будет их искать по пути `_SERVER_URL_/static/` +- Без основной иконки (указанной в манифесте) приложение не установится diff --git a/dialog-sequence/icon.png b/dialog-sequence/icon.png new file mode 100644 index 0000000..290dfc4 Binary files /dev/null and b/dialog-sequence/icon.png differ diff --git a/dialog-sequence/index.js b/dialog-sequence/index.js new file mode 100644 index 0000000..eccb6b2 --- /dev/null +++ b/dialog-sequence/index.js @@ -0,0 +1,178 @@ +const express = require('express'); +const bodyParser = require('body-parser'); + +const app = express(); +app.use(bodyParser.json()); + +// Адрес сервера данного приложения (для доступа из Интернета) +const SERVER_URL = ''; +// Адрес сервера Loop +const LOOP_URL = ''; +// Порт на котором будет слушать наш сервер +const PORT = 4000; + +// Описание первой формы +const form1 = { + title: 'Первая форма', + icon: 'icon.png', // Иконку для каждой формы можно задать свою + fields: [ + { + type: 'text', + name: 'message', + modal_label: 'Тут какое-то поле', + position: 1, + is_required: true, + }, + ], + submit: { + // Куда отправлять запрос после нажатия кнопки "Отправить" + path: '/second_modal', + // Контекст для запроса + expand: { + // Запрашиваем данные пользователя который вызвал действие + acting_user: "all", + // запрашиваем канал в котором было вызвано действие + channel: "all", + } + }, +} + +// Описание второй формы +const form2 = { + title: 'Вторая форма', + icon: 'icon.png', + fields: [ + { + type: 'text', + name: 'message2', + modal_label: 'Еще одно поле', + position: 1, + }, + ], + submit: { + // Куда отправлять запрос после нажатия кнопки "Отправить" + path: '/second_result', + expand: { + // Запрашиваем данные пользователя который вызвал действие + acting_user: "all", + // запрашиваем канал в котором было вызвано действие + channel: "all", + } + }, +} + +// Отправка сообщения в канал Loop +const createPost = async (bot_access_token, channel_id, message) => { + try { + await fetch(`${LOOP_URL}/api/v4/posts`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + bot_access_token, + }, + body: JSON.stringify({ + channel_id, + message, + }) + }); + } catch (e) { + console.error('Error when create post', e); + } +} + +// Обработчик результата первой формы +app.post('/second_modal', (req, res) => { + const context = req.body.context; + + // Отправляем данные формы в канал + createPost( + context.bot_access_token, + context.channel.id, + '### Заполнена форма 1\n```json\n' + JSON.stringify(req.body.values, null, 2) + '\n```', + ); + + // Отвечаем второй формой. Обязательно статус 200 (не 201 и тд) + res.status(200).json({ + type: 'form', + form: form2, + }) +}); + +// Обработчик результата второй формы +app.post('/second_result', (req, res) => { + const context = req.body.context; + + // Отправляем результат в канал + createPost( + context.bot_access_token, + context.channel.id, + '### Заполнена форма 2\n```json\n' + JSON.stringify(req.body.values, null, 2) + '\n```', + ); + + // Отвечаем что все хорошо. Обязательно статус 200 (не 201 и тд) + res.status(200).json({ + type: 'ok', + }); +}) + +// Данный метод будет вызываться при открытии любого канала, любым пользователем +// Отвечает за отрисовку кнопки в сайдбаре +app.post('/bindings', (req, res) => { + res.status(200).json({ + type: 'ok', + data: [ + { + // Место где показать кнопку + location: '/channel_header', + // Описание кнопки + bindings: [ + { + // идентификатор, который будет отправлен в запросе + location: 'send-button', + // Иконка кнопки + icon: 'icon.png', + // Подпись для кнопки + label: 'Открыть форму', + // Первая форма + form: form1, + } + ] + }, + ] + }) +}) + +// Манифест для установки приложения +app.get('/manifest.json', (req, res) => { + res.json({ + app_id: 'ru.loop.dialog-sequence-example', + version: '0.1.0', + display_name: 'Loop Dialog Sequence Example', + description: 'An app that opens modal dialogs sequentially', + icon: 'icon.png', + homepage_url: 'https://git.wilix.dev/loop/integration-examples', + // Разрешения для приложения + requested_permissions: [ + // разрешение, чтобы приложение могло писать от имени бота + 'act_as_bot', + // разрешение, чтобы получать данные пользователя, который вызывает действия + 'act_as_user' + ], + // Разрешения для установки кнопок или команд + requested_locations: [ + // Устанавливает кнопку во всех каналах в сайдбаре или в заголовке канала + // (расположение завит от того, включен ли сайдбар) + '/channel_header' + ], + app_type: 'http', + http: { + root_url: SERVER_URL + } + }) +}) + +// Сервим иконки из папки. Все иконки всегда будут искаться по пути /static +app.use('/static', express.static('./static')); + +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/dialog-sequence/package.json b/dialog-sequence/package.json new file mode 100644 index 0000000..8c94c95 --- /dev/null +++ b/dialog-sequence/package.json @@ -0,0 +1,16 @@ +{ + "name": "loop-dialog-sequence-example", + "version": "0.1.0", + "description": "Example App for dialog sequence", + "scripts": { + "start": "node ./index.js" + }, + "engines": { + "node": "21" + }, + "license": "MIT", + "dependencies": { + "body-parser": "^1.20.2", + "express": "4.17.1" + } +} diff --git a/package.json b/package.json deleted file mode 100644 index 11e87f3..0000000 --- a/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "modal-pages-test", - "packageManager": "yarn@4.1.1" -} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 5f1875e..0000000 --- a/yarn.lock +++ /dev/null @@ -1,12 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"modal-pages-test@workspace:.": - version: 0.0.0-use.local - resolution: "modal-pages-test@workspace:." - languageName: unknown - linkType: soft