From ccc24d472c2c3d082a411730ea899b981f11ef27 Mon Sep 17 00:00:00 2001 From: yhoupert Date: Thu, 18 Jan 2024 11:58:09 +0100 Subject: [PATCH] Merge of linto-platform services to a mono repository --- .gitignore | 68 +- README.md | 8 + client/rpi/.envdefault | 11 + client/rpi/.gitignore | 4 + client/rpi/README.md | 31 + client/rpi/components/audio/index.js | 21 + client/rpi/components/localmqtt/index.js | 106 + client/rpi/components/logicmqtt/index.js | 162 ++ client/rpi/config.js | 33 + client/rpi/controller/lasvegas.js | 33 + client/rpi/controller/localmqttevents.js | 27 + client/rpi/controller/logicmqttevents.js | 158 ++ client/rpi/index.js | 53 + client/rpi/lib/soundfetch/index.js | 23 + client/rpi/lib/terminal/index.js | 43 + client/rpi/lib/terminal/linto.sample.json | 56 + client/rpi/package-lock.json | 901 ++++++++ client/rpi/package.json | 21 + client/web/README.md | 240 ++ client/web/package.json | 54 + client/web/src/assets/audio/beep.mp3 | Bin 0 -> 7572 bytes client/web/src/assets/css/.gitkeep | 15 + client/web/src/assets/css/linto-ui.min.css | 1 + .../web/src/assets/img/widget/chatbot-mic.svg | 66 + client/web/src/assets/img/widget/close.svg | 49 + .../src/assets/img/widget/collapse copy.svg | 3 + client/web/src/assets/img/widget/collapse.svg | 62 + client/web/src/assets/img/widget/feedback.svg | 93 + client/web/src/assets/img/widget/linto.svg | 23 + .../web/src/assets/img/widget/mic-muted.svg | 85 + client/web/src/assets/img/widget/mic-on.svg | 73 + client/web/src/assets/img/widget/mic.svg | 16 + client/web/src/assets/img/widget/play.svg | 77 + client/web/src/assets/img/widget/send.svg | 12 + client/web/src/assets/img/widget/settings.svg | 16 + client/web/src/assets/img/widget/writing.gif | Bin 0 -> 147485 bytes client/web/src/assets/json/error.json | 1 + client/web/src/assets/json/linto-awake.json | 1 + client/web/src/assets/json/linto-sleep.json | 1 + client/web/src/assets/json/linto-talking.json | 1 + client/web/src/assets/json/linto-think.json | 1 + client/web/src/assets/json/microphone.json | 64 + client/web/src/assets/json/validation.json | 1 + client/web/src/assets/scss/linto-ui.scss | 667 ++++++ client/web/src/assets/scss/mixin.scss | 125 ++ client/web/src/assets/scss/styles.scss | 3 + .../src/assets/template/widget-default.html | 98 + client/web/src/audio.js | 98 + client/web/src/handlers/audio.js | 3 + client/web/src/handlers/linto-ui.js | 364 +++ client/web/src/handlers/linto.js | 193 ++ client/web/src/handlers/mqtt.js | 150 ++ client/web/src/lib/lottie.min.js | 15 + client/web/src/linto-ui.js | 999 +++++++++ client/web/src/linto.js | 413 ++++ client/web/src/mqtt.js | 193 ++ client/web/tests/index.html | 15 + client/web/tests/index.js | 167 ++ client/web/tests/linto-ui/index.html | 33 + client/web/tests/linto-ui/index.js | 16 + platform/business-logic-server/.dockerenv | 27 + platform/business-logic-server/.dockerignore | 11 + platform/business-logic-server/.envdefault | 24 + platform/business-logic-server/.eslintrc.json | 16 + .../workflows/dockerhub-description.yml | 20 + platform/business-logic-server/.gitignore | 28 + platform/business-logic-server/Dockerfile | 40 + platform/business-logic-server/README.md | 51 + platform/business-logic-server/RELEASE.md | 17 + .../business-logic-server/asset/linto.png | Bin 0 -> 53054 bytes .../business-logic-server/asset/linto_min.png | Bin 0 -> 40118 bytes platform/business-logic-server/config.js | 52 + .../docker-entrypoint.sh | 75 + .../docker-healthcheck.js | 7 + platform/business-logic-server/index.js | 19 + .../jenkins-deployment/Dockerfile | 32 + .../lib/node-red/css/nodered-custom.css | 184 ++ .../lib/node-red/index.js | 161 ++ .../lib/node-red/js/nodered-custom.js | 144 ++ .../lib/node-red/settings/settings.js | 289 +++ .../lib/webserver/index.js | 31 + .../catalogue/catalogue/rawCatalogue.js | 17 + .../routes/catalogue/catalogue/verdaccio.js | 58 + .../lib/webserver/routes/catalogue/index.js | 51 + .../lib/webserver/routes/index.js | 29 + .../lib/webserver/routes/red/index.js | 23 + .../lib/webserver/routes/routes.js | 16 + platform/business-logic-server/package.json | 48 + platform/linto-admin/.docker_env | 47 + platform/linto-admin/.dockerignore | 1 + .../workflows/dockerhub-description.yml | 20 + platform/linto-admin/.gitignore | 13 + platform/linto-admin/Dockerfile | 22 + platform/linto-admin/README.md | 115 + platform/linto-admin/RELEASE.md | 50 + platform/linto-admin/docker-compose.yml | 51 + platform/linto-admin/docker-entrypoint.sh | 106 + platform/linto-admin/vue_app/.browserslistrc | 3 + platform/linto-admin/vue_app/.env.development | 9 + platform/linto-admin/vue_app/.env.production | 9 + platform/linto-admin/vue_app/.eslintrc.js | 3 + platform/linto-admin/vue_app/README.md | 34 + platform/linto-admin/vue_app/babel.config.js | 5 + platform/linto-admin/vue_app/package.json | 27 + .../linto-admin/vue_app/postcss.config.js | 5 + platform/linto-admin/vue_app/public/404.html | 36 + .../vue_app/public/animations/error.json | 1 + .../vue_app/public/animations/validation.json | 1 + .../linto-admin/vue_app/public/css/styles.css | 1 + .../linto-admin/vue_app/public/default.html | 38 + .../linto-admin/vue_app/public/favicon.ico | Bin 0 -> 1150 bytes .../vue_app/public/img/admin-logo-dark@2x.png | Bin 0 -> 2590 bytes .../public/img/admin-logo-light@2x.png | Bin 0 -> 2537 bytes .../vue_app/public/img/admin-logo@2x.png | Bin 0 -> 11049 bytes .../vue_app/public/img/bg-login.jpg | Bin 0 -> 116925 bytes .../vue_app/public/img/btn-icons@2x.png | Bin 0 -> 1218 bytes .../vue_app/public/img/close-icon@2x.png | Bin 0 -> 598 bytes .../public/img/deploy-status-icons@2x.png | Bin 0 -> 1111 bytes .../img/favicon/android-icon-144x144.png | Bin 0 -> 3889 bytes .../img/favicon/android-icon-192x192.png | Bin 0 -> 3833 bytes .../public/img/favicon/android-icon-36x36.png | Bin 0 -> 1647 bytes .../public/img/favicon/android-icon-48x48.png | Bin 0 -> 1976 bytes .../public/img/favicon/android-icon-72x72.png | Bin 0 -> 2429 bytes .../public/img/favicon/android-icon-96x96.png | Bin 0 -> 2916 bytes .../public/img/favicon/apple-icon-114x114.png | Bin 0 -> 3266 bytes .../public/img/favicon/apple-icon-120x120.png | Bin 0 -> 3406 bytes .../public/img/favicon/apple-icon-144x144.png | Bin 0 -> 3889 bytes .../public/img/favicon/apple-icon-152x152.png | Bin 0 -> 4039 bytes .../public/img/favicon/apple-icon-180x180.png | Bin 0 -> 4671 bytes .../public/img/favicon/apple-icon-57x57.png | Bin 0 -> 2180 bytes .../public/img/favicon/apple-icon-60x60.png | Bin 0 -> 2227 bytes .../public/img/favicon/apple-icon-72x72.png | Bin 0 -> 2429 bytes .../public/img/favicon/apple-icon-76x76.png | Bin 0 -> 2532 bytes .../img/favicon/apple-icon-precomposed.png | Bin 0 -> 4405 bytes .../vue_app/public/img/favicon/apple-icon.png | Bin 0 -> 4405 bytes .../public/img/favicon/browserconfig.xml | 2 + .../public/img/favicon/favicon-16x16.png | Bin 0 -> 1190 bytes .../public/img/favicon/favicon-32x32.png | Bin 0 -> 1583 bytes .../public/img/favicon/favicon-96x96.png | Bin 0 -> 2916 bytes .../vue_app/public/img/favicon/favicon.ico | Bin 0 -> 1150 bytes .../vue_app/public/img/favicon/manifest.json | 41 + .../public/img/favicon/ms-icon-144x144.png | Bin 0 -> 3889 bytes .../public/img/favicon/ms-icon-150x150.png | Bin 0 -> 4016 bytes .../public/img/favicon/ms-icon-310x310.png | Bin 0 -> 12651 bytes .../public/img/favicon/ms-icon-70x70.png | Bin 0 -> 2370 bytes .../public/img/full-screen-icons@2x.png | Bin 0 -> 1601 bytes .../vue_app/public/img/linto-say@2x.png | Bin 0 -> 1258 bytes .../vue_app/public/img/loading@2x.png | Bin 0 -> 650 bytes .../vue_app/public/img/login-icons@2x.png | Bin 0 -> 1378 bytes .../vue_app/public/img/monitoring@2x.png | Bin 0 -> 692 bytes .../vue_app/public/img/mute-unmute@2x.png | Bin 0 -> 1764 bytes .../vue_app/public/img/nav-arrows@2x.png | Bin 0 -> 676 bytes .../vue_app/public/img/ping@2x.png | Bin 0 -> 630 bytes .../vue_app/public/img/publish-icon@2x.png | Bin 0 -> 1481 bytes .../linto-admin/vue_app/public/img/say@2x.png | Bin 0 -> 1280 bytes .../vue_app/public/img/svg/add.svg | 129 ++ .../vue_app/public/img/svg/android-users.svg | 131 ++ .../vue_app/public/img/svg/android.svg | 155 ++ .../vue_app/public/img/svg/app.svg | 82 + .../vue_app/public/img/svg/apply.svg | 58 + .../vue_app/public/img/svg/arrow.svg | 114 + .../vue_app/public/img/svg/back.svg | 116 + .../vue_app/public/img/svg/barcode.svg | 232 ++ .../vue_app/public/img/svg/cancel.svg | 116 + .../vue_app/public/img/svg/chat.svg | 140 ++ .../vue_app/public/img/svg/circuit.svg | 108 + .../vue_app/public/img/svg/close.svg | 76 + .../vue_app/public/img/svg/cpu.svg | 299 +++ .../vue_app/public/img/svg/delete.svg | 80 + .../vue_app/public/img/svg/edit.svg | 130 ++ .../vue_app/public/img/svg/fullscreen.svg | 155 ++ .../vue_app/public/img/svg/goto.svg | 122 + .../vue_app/public/img/svg/install.svg | 132 ++ .../public/img/svg/leave-fullscreen.svg | 158 ++ .../vue_app/public/img/svg/levels.svg | 145 ++ .../vue_app/public/img/svg/loading.svg | 56 + .../vue_app/public/img/svg/logout.svg | 62 + .../vue_app/public/img/svg/multi-user.svg | 141 ++ .../vue_app/public/img/svg/mute.svg | 151 ++ .../vue_app/public/img/svg/nlu.svg | 153 ++ .../vue_app/public/img/svg/ping.svg | 115 + .../linto-admin/vue_app/public/img/svg/qr.svg | 57 + .../vue_app/public/img/svg/reset.svg | 123 + .../vue_app/public/img/svg/rocket.svg | 82 + .../vue_app/public/img/svg/save.svg | 138 ++ .../vue_app/public/img/svg/say.svg | 62 + .../vue_app/public/img/svg/settings.svg | 62 + .../vue_app/public/img/svg/single-user.svg | 109 + .../public/img/svg/single-user.svg.svg | 104 + .../vue_app/public/img/svg/skills-manager.svg | 129 ++ .../vue_app/public/img/svg/terminal.svg | 160 ++ .../vue_app/public/img/svg/unmute.svg | 119 + .../vue_app/public/img/svg/upload.svg | 131 ++ .../vue_app/public/img/svg/user-list.svg | 139 ++ .../vue_app/public/img/svg/user-settings.svg | 91 + .../vue_app/public/img/svg/users.svg | 107 + .../vue_app/public/img/svg/webapp.svg | 112 + .../vue_app/public/img/svg/workflow.svg | 135 ++ .../vue_app/public/img/warning@2x.png | Bin 0 -> 904 bytes .../vue_app/public/img/workflows@2x.png | Bin 0 -> 876 bytes .../linto-admin/vue_app/public/index.html | 39 + .../vue_app/public/js/lottie.min.js | 1 + .../vue_app/public/sass/_break-points.scss | 30 + .../vue_app/public/sass/_global.scss | 348 +++ .../vue_app/public/sass/_mixin.scss | 211 ++ .../vue_app/public/sass/_variables.scss | 11 + .../public/sass/components/app-header.scss | 31 + .../sass/components/app-notify-top.scss | 76 + .../public/sass/components/app-notify.scss | 131 ++ .../sass/components/app-vertical-nav.scss | 153 ++ .../vue_app/public/sass/components/app.scss | 40 + .../public/sass/components/buttons.scss | 367 +++ .../sass/components/clients-overview.scss | 73 + .../vue_app/public/sass/components/forms.scss | 401 ++++ .../public/sass/components/healthcheck.scss | 37 + .../public/sass/components/iframe.scss | 39 + .../sass/components/linto-monitoring.scss | 134 ++ .../vue_app/public/sass/components/login.scss | 58 + .../vue_app/public/sass/components/modal.scss | 156 ++ .../public/sass/components/skills.scss | 40 + .../vue_app/public/sass/styles.scss | 18 + platform/linto-admin/vue_app/src/App.vue | 122 + .../vue_app/src/components/AppFormLabel.vue | 26 + .../vue_app/src/components/AppHeader.vue | 22 + .../vue_app/src/components/AppIframe.vue | 18 + .../vue_app/src/components/AppInput.vue | 178 ++ .../vue_app/src/components/AppNotif.vue | 83 + .../vue_app/src/components/AppNotifTop.vue | 139 ++ .../vue_app/src/components/AppSelect.vue | 144 ++ .../vue_app/src/components/AppTextarea.vue | 25 + .../vue_app/src/components/AppVerticalNav.vue | 91 + .../vue_app/src/components/ModalAddDomain.vue | 155 ++ .../src/components/ModalAddTerminal.vue | 109 + .../vue_app/src/components/ModalAddUsers.vue | 231 ++ .../src/components/ModalDeleteDomain.vue | 100 + .../components/ModalDeleteMultiUserApp.vue | 185 ++ .../src/components/ModalDeleteTerminal.vue | 85 + .../src/components/ModalDeleteUser.vue | 101 + .../components/ModalDissociateTerminal.vue | 90 + .../src/components/ModalEditDomain.vue | 186 ++ .../ModalEditDomainApplications.vue | 515 +++++ .../vue_app/src/components/ModalEditUser.vue | 240 ++ .../src/components/ModalEditUserApps.vue | 270 +++ .../src/components/ModalManageDomains.vue | 221 ++ .../src/components/ModalManageUsers.vue | 262 +++ .../src/components/ModalReplaceTerminal.vue | 127 ++ .../ModalUpdateApplicationServices.vue | 584 +++++ .../vue_app/src/components/NodeRedIframe.vue | 132 ++ .../vue_app/src/components/TockIframe.vue | 45 + .../linto-admin/vue_app/src/filters/index.js | 438 ++++ platform/linto-admin/vue_app/src/login.js | 8 + platform/linto-admin/vue_app/src/main.js | 15 + platform/linto-admin/vue_app/src/page404.js | 8 + platform/linto-admin/vue_app/src/router.js | 284 +++ .../vue_app/src/router/router-404.js | 16 + .../vue_app/src/router/router-healthcheck.js | 16 + .../vue_app/src/router/router-login.js | 16 + .../vue_app/src/router/router-setup.js | 18 + platform/linto-admin/vue_app/src/setup.js | 8 + platform/linto-admin/vue_app/src/store.js | 614 +++++ .../linto-admin/vue_app/src/views/404.vue | 14 + .../vue_app/src/views/DeviceAppDeploy.vue | 655 ++++++ .../src/views/DeviceAppWorkflowEditor.vue | 166 ++ .../vue_app/src/views/DeviceApps.vue | 218 ++ .../linto-admin/vue_app/src/views/Domains.vue | 150 ++ .../linto-admin/vue_app/src/views/Login.vue | 116 + .../vue_app/src/views/MultiUserAppDeploy.vue | 575 +++++ .../src/views/MultiUserAppWorkflowEditor.vue | 166 ++ .../vue_app/src/views/MultiUserApps.vue | 242 ++ .../linto-admin/vue_app/src/views/Setup.vue | 114 + .../vue_app/src/views/SkillsManager.vue | 504 +++++ .../vue_app/src/views/Terminals.vue | 215 ++ .../vue_app/src/views/TerminalsMonitoring.vue | 373 +++ .../vue_app/src/views/TockView.vue | 65 + .../linto-admin/vue_app/src/views/Users.vue | 154 ++ platform/linto-admin/vue_app/vue.config.js | 47 + platform/linto-admin/wait-for-it.sh | 182 ++ platform/linto-admin/webserver/.envdefault | 44 + platform/linto-admin/webserver/app.js | 41 + platform/linto-admin/webserver/config.js | 100 + .../webserver/controller/mqtt-http/index.js | 48 + .../linto-admin/webserver/doc/swagger.json | 1990 +++++++++++++++++ .../webserver/docker-healthcheck.js | 8 + .../linto-admin/webserver/lexicalseeding.js | 517 +++++ .../webserver/lib/mqtt-monitor/index.js | 215 ++ .../linto-admin/webserver/lib/redis/index.js | 70 + .../webserver/lib/webserver/index.js | 96 + .../lib/webserver/iohandler/index.js | 65 + .../lib/webserver/middlewares/index.js | 81 + .../middlewares/json/device-workflow.json | 107 + .../middlewares/json/multi-user-workflow.json | 108 + .../webserver/middlewares/lexicalseeding.js | 518 +++++ .../lib/webserver/middlewares/nodered.js | 718 ++++++ .../lib/webserver/routes/_root/index.js | 16 + .../lib/webserver/routes/admin/index.js | 13 + .../routes/api/androidusers/index.js | 335 +++ .../routes/api/clients/static/index.js | 190 ++ .../lib/webserver/routes/api/flow/index.js | 187 ++ .../webserver/routes/api/flow/node/index.js | 94 + .../lib/webserver/routes/api/index.js | 11 + .../webserver/routes/api/localskills/index.js | 80 + .../lib/webserver/routes/api/stt/index.js | 180 ++ .../lib/webserver/routes/api/swagger/index.js | 15 + .../lib/webserver/routes/api/tock/index.js | 97 + .../webserver/routes/api/webapphosts/index.js | 277 +++ .../routes/api/workflows/application/index.js | 225 ++ .../webserver/routes/api/workflows/index.js | 137 ++ .../routes/api/workflows/static/index.js | 189 ++ .../lib/webserver/routes/healthcheck/index.js | 85 + .../webserver/lib/webserver/routes/index.js | 41 + .../lib/webserver/routes/login/index.js | 84 + .../lib/webserver/routes/logout/index.js | 21 + .../webserver/lib/webserver/routes/routes.js | 47 + .../lib/webserver/routes/setup/index.js | 43 + platform/linto-admin/webserver/package.json | 73 + .../public/img/nodered-linto-logo.png | Bin 0 -> 3645 bytes .../linto-admin/webserver/readme.md | 0 platform/mongodb-migration/.docker_env | 12 + platform/mongodb-migration/.dockeringore | 4 + platform/mongodb-migration/.envdefault | 12 + .../workflows/dockerhub-description.yml | 20 + .../mongodb-migration/.gitignore | 0 platform/mongodb-migration/Dockerfile | 17 + platform/mongodb-migration/LICENSE | 661 ++++++ platform/mongodb-migration/README.md | 39 + platform/mongodb-migration/RELEASE.md | 21 + platform/mongodb-migration/config.js | 56 + platform/mongodb-migration/docker-compose.yml | 21 + .../mongodb-migration/docker-entrypoint.sh | 33 + platform/mongodb-migration/index.js | 138 ++ .../mongodb-migration/migrations/1/index.js | 265 +++ .../1/json/linto-fleet-default-flow.json | 216 ++ .../migrations/1/schemas/context.json | 45 + .../migrations/1/schemas/context_types.json | 13 + .../migrations/1/schemas/dbversion.json | 16 + .../migrations/1/schemas/flow_pattern.json | 24 + .../1/schemas/flow_pattern_tmp.json | 19 + .../migrations/1/schemas/linto_users.json | 20 + .../migrations/1/schemas/lintos.json | 41 + .../migrations/1/schemas/users.json | 26 + .../mongodb-migration/migrations/2/index.js | 378 ++++ .../2/json/device-default-flow.json | 202 ++ .../2/json/multi-user-default-flow.json | 204 ++ .../migrations/2/schemas/android_users.json | 25 + .../migrations/2/schemas/clients_static.json | 51 + .../migrations/2/schemas/db_version.json | 16 + .../migrations/2/schemas/flow_tmp.json | 19 + .../migrations/2/schemas/local_skills.json | 20 + .../migrations/2/schemas/mqtt_acls.json | 16 + .../migrations/2/schemas/mqtt_users.json | 25 + .../migrations/2/schemas/users.json | 26 + .../migrations/2/schemas/webapp_hosts.json | 37 + .../2/schemas/workflows_application.json | 38 + .../2/schemas/workflows_static.json | 41 + .../2/schemas/workflows_templates.json | 23 + .../mongodb-migration/migrations/3/index.js | 312 +++ .../migrations/3/schemas/android_users.json | 25 + .../migrations/3/schemas/clients_static.json | 51 + .../migrations/3/schemas/db_version.json | 16 + .../migrations/3/schemas/flow_tmp.json | 19 + .../migrations/3/schemas/local_skills.json | 20 + .../migrations/3/schemas/mqtt_acls.json | 16 + .../migrations/3/schemas/mqtt_users.json | 25 + .../migrations/3/schemas/users.json | 26 + .../migrations/3/schemas/webapp_hosts.json | 37 + .../3/schemas/workflows_application.json | 38 + .../3/schemas/workflows_static.json | 41 + platform/mongodb-migration/package.json | 23 + platform/mongodb-migration/wait-for-it.sh | 182 ++ platform/overwatch/.dockerignore | 8 + platform/overwatch/.envdefault | 42 + .../workflows/dockerhub-description.yml | 20 + platform/overwatch/Dockerfile | 17 + platform/overwatch/README.md | 63 + platform/overwatch/RELEASE.md | 35 + platform/overwatch/config.js | 104 + platform/overwatch/doc/api/auth/ldap.md | 2 + platform/overwatch/doc/api/auth/local.md | 238 ++ platform/overwatch/doc/api/default.md | 48 + platform/overwatch/docker-compose.yml | 7 + platform/overwatch/docker-entrypoint.sh | 27 + platform/overwatch/docker-healthcheck.js | 7 + platform/overwatch/index.js | 40 + .../overwatch/lib/overwatch/mongodb/driver.js | 70 + .../overwatch/lib/overwatch/mongodb/model.js | 107 + .../overwatch/mongodb/models/android_users.js | 51 + .../overwatch/mongodb/models/linto_users.js | 41 + .../lib/overwatch/mongodb/models/lintos.js | 33 + .../lib/overwatch/mongodb/models/logs.js | 21 + .../overwatch/mongodb/models/mqtt_users.js | 58 + .../lib/overwatch/mongodb/models/scopes.js | 29 + .../overwatch/mongodb/models/webapp_hosts.js | 49 + .../mongodb/models/workflows_application.js | 39 + platform/overwatch/lib/overwatch/overwatch.js | 11 + .../overwatch/slotsManager/slotsManager.js | 60 + .../watcher/mqttController/status.js | 37 + .../lib/overwatch/watcher/watcher.js | 85 + platform/overwatch/package.json | 52 + platform/overwatch/wait-for-it.sh | 182 ++ .../overwatch/webserver/config/auth/local.js | 81 + .../webserver/config/auth/refresh/index.js | 24 + .../webserver/config/error/exception/auth.js | 127 ++ .../webserver/config/error/handler.js | 42 + platform/overwatch/webserver/config/index.js | 13 + .../webserver/config/passport/local.js | 102 + .../config/passport/tokenGenerator/index.js | 50 + platform/overwatch/webserver/index.js | 61 + .../overwatch/webserver/lib/authWrapper.js | 47 + platform/overwatch/webserver/lib/user.js | 24 + .../webserver/lib/workflowApplication.js | 51 + .../overwatch/webserver/routes/auth/index.js | 122 + platform/overwatch/webserver/routes/index.js | 51 + .../webserver/routes/overwatch/index.js | 46 + platform/overwatch/webserver/routes/routes.js | 33 + platform/service-broker/Dockerfile | 3 + platform/service-broker/README.md | 59 + platform/service-broker/RELEASE.md | 4 + platform/service-broker/docker-compose.yml | 18 + platform/service-broker/redis_conf/redis.conf | 54 + platform/stt-service-manager/.defaultparam | 46 + platform/stt-service-manager/.envdefault | 28 + .../workflows/dockerhub-description.yml | 20 + platform/stt-service-manager/Dockerfile | 74 + platform/stt-service-manager/README.md | 97 + platform/stt-service-manager/RELEASE.md | 26 + platform/stt-service-manager/app.js | 51 + .../ClusterManager/DockerSwarm/index.js | 205 ++ .../controllers/eventsFrom/LinSTT.js | 18 + .../controllers/eventsFrom/WebServer.js | 80 + .../controllers/eventsFrom/myself.js | 68 + .../components/ClusterManager/index.js | 23 + .../IngressController/Nginx/index.js | 175 ++ .../IngressController/Nginx}/nginx.conf | 0 .../IngressController/Traefik/index.js | 69 + .../controllers/eventsFrom/ClusterManager.js | 50 + .../components/IngressController/index.js | 20 + .../components/LinSTT/Kaldi/index.js | 492 ++++ .../components/LinSTT/Kaldi/scripts/fst.awk | 34 + .../components/LinSTT/Kaldi/scripts/path.sh | 0 .../LinSTT/Kaldi/scripts/prepare_HCLG.sh | 96 + .../controllers/eventsFrom/WebServer.js | 399 ++++ .../LinSTT/controllers/eventsFrom/myself.js | 15 + .../components/LinSTT/index.js | 167 ++ .../controllers/eventsFrom/WebServer.js | 178 ++ .../components/ServiceManager/index.js | 24 + .../components/WebServer/controllers/.gitkeep | 0 .../components/WebServer/index.js | 75 + .../components/WebServer/middlewares/index.js | 24 + .../WebServer/routes/healthcheck.js | 18 + .../WebServer/routes/modelManager/amodel.js | 47 + .../WebServer/routes/modelManager/amodels.js | 19 + .../WebServer/routes/modelManager/element.js | 91 + .../WebServer/routes/modelManager/elements.js | 19 + .../WebServer/routes/modelManager/lmodel.js | 72 + .../WebServer/routes/modelManager/lmodels.js | 19 + .../components/WebServer/routes/router.js | 41 + .../components/WebServer/routes/routes.js | 17 + .../routes/serviceManager/service.js | 114 + .../routes/serviceManager/services.js | 30 + .../components/component.js | 55 + platform/stt-service-manager/config.js | 96 + .../stt-service-manager/config/nginx.conf | 4 + .../stt-service-manager/config/seed}/init.js | 0 .../stt-service-manager/config/seed/user.js | 8 + .../stt-service-manager/config/swagger.yml | 892 ++++++++ .../stt-service-manager/docker-compose.yml | 55 + .../stt-service-manager/docker-entrypoint.sh | 39 + .../stt-service-manager/docker-healthcheck.js | 5 + .../stt-service-manager/lib/customErrors.js | 11 + platform/stt-service-manager/models/.gitkeep | 0 platform/stt-service-manager/models/driver.js | 110 + platform/stt-service-manager/models/model.js | 136 ++ .../models/models/AMUpdates.js | 56 + .../models/models/LMUpdates.js | 118 + .../models/models/ServiceUpdates.js | 70 + platform/stt-service-manager/package.json | 40 + platform/stt-service-manager/wait-for-it.sh | 182 ++ stack/.gitignore | 1 + stack/LICENSE | 661 ++++++ .../config}/bls/flowsStorage.json | 0 .../config}/jitsi/env/jitsienv_template | 0 .../jitsi/jigasi/sip-communicator.properties | 0 .../jitsi/prosody/conf.d/jitsi-meet.cfg.lua | 0 .../jitsi/prosody/user/prosody-user.dat | 0 .../config}/jitsi/traefik/upd-jvb.toml | 0 .../config}/jitsi/web/custom-config.js | 0 .../config}/mongoseeds/admin-users.js | 0 stack/config/mosquitto/auth/.gitkeep | 0 {config => stack/config}/mosquitto/auth/acls | 0 .../conf-tempalte/go-auth-template.conf | 0 stack/config/mosquitto/conf.d/.gitkeep | 0 .../config}/mosquitto/mosquitto.conf | 0 stack/config/servicemanager/init.js | 11 + stack/config/servicemanager/nginx.conf | 4 + .../config}/servicemanager/user.js | 0 .../tock/scripts/admin-web-entrypoint.sh | 0 .../config}/tock/scripts/setup.sh | 0 .../config}/traefik/http-auth.toml | 0 .../config}/traefik/ssl-redirect.toml | 0 .../config}/traefik/stt-manager-path.toml | 0 .../config}/traefik/tock-path.toml | 0 stack/devcerts/.gitkeep | 0 .../dockerenv_template | 0 {docs => stack/docs}/Jitsi.md | 0 {docs => stack/docs}/README.md | 0 .../optional-stack-files}/grafana.yml | 0 .../linto-platform-jitsi.yml | 0 .../linto-platform-tasks-monitor.yml | 0 .../optional-stack-files}/network_tool.yml | 0 {scripts => stack/scripts}/README.md | 0 {scripts => stack/scripts}/bls_backup.sh | 0 {scripts => stack/scripts}/bls_restore.sh | 0 {scripts => stack/scripts}/db_backup.sh | 0 {scripts => stack/scripts}/db_restore.sh | 0 {scripts => stack/scripts}/start-jitsi.sh | 0 {scripts => stack/scripts}/start-optional.sh | 0 .../stack-files}/linto-docker-visualizer.yml | 0 .../stack-files}/linto-edge-router.yml | 0 .../stack-files}/linto-mongo-migration.yml | 0 .../stack-files}/linto-mqtt-broker.yml | 0 .../stack-files}/linto-platform-admin.yml | 0 .../stack-files}/linto-platform-bls.yml | 0 .../stack-files}/linto-platform-mongo.yml | 0 .../stack-files}/linto-platform-overwatch.yml | 0 .../stack-files}/linto-platform-redis.yml | 0 ...nto-platform-stt-service-manager-nginx.yml | 0 .../linto-platform-stt-service-manager.yml | 0 .../stack-files}/linto-platform-tock.yml | 0 start.sh => stack/start.sh | 0 529 files changed, 44895 insertions(+), 1 deletion(-) create mode 100644 README.md create mode 100644 client/rpi/.envdefault create mode 100644 client/rpi/.gitignore create mode 100644 client/rpi/README.md create mode 100644 client/rpi/components/audio/index.js create mode 100644 client/rpi/components/localmqtt/index.js create mode 100644 client/rpi/components/logicmqtt/index.js create mode 100644 client/rpi/config.js create mode 100644 client/rpi/controller/lasvegas.js create mode 100644 client/rpi/controller/localmqttevents.js create mode 100644 client/rpi/controller/logicmqttevents.js create mode 100644 client/rpi/index.js create mode 100644 client/rpi/lib/soundfetch/index.js create mode 100644 client/rpi/lib/terminal/index.js create mode 100644 client/rpi/lib/terminal/linto.sample.json create mode 100644 client/rpi/package-lock.json create mode 100644 client/rpi/package.json create mode 100644 client/web/README.md create mode 100644 client/web/package.json create mode 100644 client/web/src/assets/audio/beep.mp3 create mode 100644 client/web/src/assets/css/.gitkeep create mode 100644 client/web/src/assets/css/linto-ui.min.css create mode 100644 client/web/src/assets/img/widget/chatbot-mic.svg create mode 100644 client/web/src/assets/img/widget/close.svg create mode 100644 client/web/src/assets/img/widget/collapse copy.svg create mode 100644 client/web/src/assets/img/widget/collapse.svg create mode 100644 client/web/src/assets/img/widget/feedback.svg create mode 100644 client/web/src/assets/img/widget/linto.svg create mode 100644 client/web/src/assets/img/widget/mic-muted.svg create mode 100644 client/web/src/assets/img/widget/mic-on.svg create mode 100644 client/web/src/assets/img/widget/mic.svg create mode 100644 client/web/src/assets/img/widget/play.svg create mode 100644 client/web/src/assets/img/widget/send.svg create mode 100644 client/web/src/assets/img/widget/settings.svg create mode 100644 client/web/src/assets/img/widget/writing.gif create mode 100644 client/web/src/assets/json/error.json create mode 100644 client/web/src/assets/json/linto-awake.json create mode 100644 client/web/src/assets/json/linto-sleep.json create mode 100644 client/web/src/assets/json/linto-talking.json create mode 100644 client/web/src/assets/json/linto-think.json create mode 100644 client/web/src/assets/json/microphone.json create mode 100644 client/web/src/assets/json/validation.json create mode 100644 client/web/src/assets/scss/linto-ui.scss create mode 100644 client/web/src/assets/scss/mixin.scss create mode 100644 client/web/src/assets/scss/styles.scss create mode 100644 client/web/src/assets/template/widget-default.html create mode 100644 client/web/src/audio.js create mode 100644 client/web/src/handlers/audio.js create mode 100644 client/web/src/handlers/linto-ui.js create mode 100644 client/web/src/handlers/linto.js create mode 100644 client/web/src/handlers/mqtt.js create mode 100644 client/web/src/lib/lottie.min.js create mode 100644 client/web/src/linto-ui.js create mode 100644 client/web/src/linto.js create mode 100644 client/web/src/mqtt.js create mode 100644 client/web/tests/index.html create mode 100644 client/web/tests/index.js create mode 100644 client/web/tests/linto-ui/index.html create mode 100644 client/web/tests/linto-ui/index.js create mode 100644 platform/business-logic-server/.dockerenv create mode 100644 platform/business-logic-server/.dockerignore create mode 100644 platform/business-logic-server/.envdefault create mode 100644 platform/business-logic-server/.eslintrc.json create mode 100644 platform/business-logic-server/.github/workflows/dockerhub-description.yml create mode 100644 platform/business-logic-server/.gitignore create mode 100644 platform/business-logic-server/Dockerfile create mode 100644 platform/business-logic-server/README.md create mode 100644 platform/business-logic-server/RELEASE.md create mode 100644 platform/business-logic-server/asset/linto.png create mode 100644 platform/business-logic-server/asset/linto_min.png create mode 100644 platform/business-logic-server/config.js create mode 100755 platform/business-logic-server/docker-entrypoint.sh create mode 100644 platform/business-logic-server/docker-healthcheck.js create mode 100644 platform/business-logic-server/index.js create mode 100644 platform/business-logic-server/jenkins-deployment/Dockerfile create mode 100644 platform/business-logic-server/lib/node-red/css/nodered-custom.css create mode 100644 platform/business-logic-server/lib/node-red/index.js create mode 100644 platform/business-logic-server/lib/node-red/js/nodered-custom.js create mode 100644 platform/business-logic-server/lib/node-red/settings/settings.js create mode 100644 platform/business-logic-server/lib/webserver/index.js create mode 100644 platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/rawCatalogue.js create mode 100644 platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/verdaccio.js create mode 100644 platform/business-logic-server/lib/webserver/routes/catalogue/index.js create mode 100644 platform/business-logic-server/lib/webserver/routes/index.js create mode 100644 platform/business-logic-server/lib/webserver/routes/red/index.js create mode 100644 platform/business-logic-server/lib/webserver/routes/routes.js create mode 100644 platform/business-logic-server/package.json create mode 100644 platform/linto-admin/.docker_env create mode 100644 platform/linto-admin/.dockerignore create mode 100644 platform/linto-admin/.github/workflows/dockerhub-description.yml create mode 100644 platform/linto-admin/.gitignore create mode 100644 platform/linto-admin/Dockerfile create mode 100644 platform/linto-admin/README.md create mode 100644 platform/linto-admin/RELEASE.md create mode 100644 platform/linto-admin/docker-compose.yml create mode 100755 platform/linto-admin/docker-entrypoint.sh create mode 100644 platform/linto-admin/vue_app/.browserslistrc create mode 100644 platform/linto-admin/vue_app/.env.development create mode 100644 platform/linto-admin/vue_app/.env.production create mode 100644 platform/linto-admin/vue_app/.eslintrc.js create mode 100644 platform/linto-admin/vue_app/README.md create mode 100644 platform/linto-admin/vue_app/babel.config.js create mode 100644 platform/linto-admin/vue_app/package.json create mode 100644 platform/linto-admin/vue_app/postcss.config.js create mode 100644 platform/linto-admin/vue_app/public/404.html create mode 100644 platform/linto-admin/vue_app/public/animations/error.json create mode 100644 platform/linto-admin/vue_app/public/animations/validation.json create mode 100644 platform/linto-admin/vue_app/public/css/styles.css create mode 100644 platform/linto-admin/vue_app/public/default.html create mode 100644 platform/linto-admin/vue_app/public/favicon.ico create mode 100755 platform/linto-admin/vue_app/public/img/admin-logo-dark@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/admin-logo-light@2x.png create mode 100644 platform/linto-admin/vue_app/public/img/admin-logo@2x.png create mode 100644 platform/linto-admin/vue_app/public/img/bg-login.jpg create mode 100755 platform/linto-admin/vue_app/public/img/btn-icons@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/close-icon@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/deploy-status-icons@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/android-icon-144x144.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/android-icon-192x192.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/android-icon-36x36.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/android-icon-48x48.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/android-icon-72x72.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/android-icon-96x96.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-114x114.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-120x120.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-144x144.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-152x152.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-180x180.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-57x57.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-60x60.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-72x72.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-76x76.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon-precomposed.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/apple-icon.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/browserconfig.xml create mode 100755 platform/linto-admin/vue_app/public/img/favicon/favicon-16x16.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/favicon-32x32.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/favicon-96x96.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/favicon.ico create mode 100755 platform/linto-admin/vue_app/public/img/favicon/manifest.json create mode 100755 platform/linto-admin/vue_app/public/img/favicon/ms-icon-144x144.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/ms-icon-150x150.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/ms-icon-310x310.png create mode 100755 platform/linto-admin/vue_app/public/img/favicon/ms-icon-70x70.png create mode 100755 platform/linto-admin/vue_app/public/img/full-screen-icons@2x.png create mode 100644 platform/linto-admin/vue_app/public/img/linto-say@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/loading@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/login-icons@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/monitoring@2x.png create mode 100644 platform/linto-admin/vue_app/public/img/mute-unmute@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/nav-arrows@2x.png create mode 100644 platform/linto-admin/vue_app/public/img/ping@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/publish-icon@2x.png create mode 100644 platform/linto-admin/vue_app/public/img/say@2x.png create mode 100644 platform/linto-admin/vue_app/public/img/svg/add.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/android-users.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/android.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/app.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/apply.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/arrow.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/back.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/barcode.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/cancel.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/chat.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/circuit.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/close.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/cpu.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/delete.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/edit.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/fullscreen.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/goto.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/install.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/leave-fullscreen.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/levels.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/loading.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/logout.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/multi-user.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/mute.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/nlu.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/ping.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/qr.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/reset.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/rocket.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/save.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/say.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/settings.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/single-user.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/single-user.svg.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/skills-manager.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/terminal.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/unmute.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/upload.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/user-list.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/user-settings.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/users.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/webapp.svg create mode 100644 platform/linto-admin/vue_app/public/img/svg/workflow.svg create mode 100644 platform/linto-admin/vue_app/public/img/warning@2x.png create mode 100755 platform/linto-admin/vue_app/public/img/workflows@2x.png create mode 100644 platform/linto-admin/vue_app/public/index.html create mode 100644 platform/linto-admin/vue_app/public/js/lottie.min.js create mode 100644 platform/linto-admin/vue_app/public/sass/_break-points.scss create mode 100644 platform/linto-admin/vue_app/public/sass/_global.scss create mode 100644 platform/linto-admin/vue_app/public/sass/_mixin.scss create mode 100644 platform/linto-admin/vue_app/public/sass/_variables.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/app-header.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/app-notify-top.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/app-notify.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/app-vertical-nav.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/app.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/buttons.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/clients-overview.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/forms.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/healthcheck.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/iframe.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/linto-monitoring.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/login.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/modal.scss create mode 100644 platform/linto-admin/vue_app/public/sass/components/skills.scss create mode 100644 platform/linto-admin/vue_app/public/sass/styles.scss create mode 100644 platform/linto-admin/vue_app/src/App.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppFormLabel.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppHeader.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppIframe.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppInput.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppNotif.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppNotifTop.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppSelect.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppTextarea.vue create mode 100644 platform/linto-admin/vue_app/src/components/AppVerticalNav.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalAddDomain.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalAddTerminal.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalAddUsers.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalDeleteDomain.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalDeleteMultiUserApp.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalDeleteTerminal.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalDeleteUser.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalDissociateTerminal.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalEditDomain.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalEditDomainApplications.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalEditUser.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalEditUserApps.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalManageDomains.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalManageUsers.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalReplaceTerminal.vue create mode 100644 platform/linto-admin/vue_app/src/components/ModalUpdateApplicationServices.vue create mode 100644 platform/linto-admin/vue_app/src/components/NodeRedIframe.vue create mode 100644 platform/linto-admin/vue_app/src/components/TockIframe.vue create mode 100644 platform/linto-admin/vue_app/src/filters/index.js create mode 100644 platform/linto-admin/vue_app/src/login.js create mode 100644 platform/linto-admin/vue_app/src/main.js create mode 100644 platform/linto-admin/vue_app/src/page404.js create mode 100644 platform/linto-admin/vue_app/src/router.js create mode 100644 platform/linto-admin/vue_app/src/router/router-404.js create mode 100644 platform/linto-admin/vue_app/src/router/router-healthcheck.js create mode 100644 platform/linto-admin/vue_app/src/router/router-login.js create mode 100644 platform/linto-admin/vue_app/src/router/router-setup.js create mode 100644 platform/linto-admin/vue_app/src/setup.js create mode 100644 platform/linto-admin/vue_app/src/store.js create mode 100644 platform/linto-admin/vue_app/src/views/404.vue create mode 100644 platform/linto-admin/vue_app/src/views/DeviceAppDeploy.vue create mode 100644 platform/linto-admin/vue_app/src/views/DeviceAppWorkflowEditor.vue create mode 100644 platform/linto-admin/vue_app/src/views/DeviceApps.vue create mode 100644 platform/linto-admin/vue_app/src/views/Domains.vue create mode 100644 platform/linto-admin/vue_app/src/views/Login.vue create mode 100644 platform/linto-admin/vue_app/src/views/MultiUserAppDeploy.vue create mode 100644 platform/linto-admin/vue_app/src/views/MultiUserAppWorkflowEditor.vue create mode 100644 platform/linto-admin/vue_app/src/views/MultiUserApps.vue create mode 100644 platform/linto-admin/vue_app/src/views/Setup.vue create mode 100644 platform/linto-admin/vue_app/src/views/SkillsManager.vue create mode 100644 platform/linto-admin/vue_app/src/views/Terminals.vue create mode 100644 platform/linto-admin/vue_app/src/views/TerminalsMonitoring.vue create mode 100644 platform/linto-admin/vue_app/src/views/TockView.vue create mode 100644 platform/linto-admin/vue_app/src/views/Users.vue create mode 100644 platform/linto-admin/vue_app/vue.config.js create mode 100755 platform/linto-admin/wait-for-it.sh create mode 100644 platform/linto-admin/webserver/.envdefault create mode 100644 platform/linto-admin/webserver/app.js create mode 100644 platform/linto-admin/webserver/config.js create mode 100644 platform/linto-admin/webserver/controller/mqtt-http/index.js create mode 100644 platform/linto-admin/webserver/doc/swagger.json create mode 100644 platform/linto-admin/webserver/docker-healthcheck.js create mode 100644 platform/linto-admin/webserver/lexicalseeding.js create mode 100644 platform/linto-admin/webserver/lib/mqtt-monitor/index.js create mode 100644 platform/linto-admin/webserver/lib/redis/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/iohandler/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/middlewares/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/middlewares/json/device-workflow.json create mode 100644 platform/linto-admin/webserver/lib/webserver/middlewares/json/multi-user-workflow.json create mode 100644 platform/linto-admin/webserver/lib/webserver/middlewares/lexicalseeding.js create mode 100644 platform/linto-admin/webserver/lib/webserver/middlewares/nodered.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/_root/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/admin/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/androidusers/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/clients/static/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/flow/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/flow/node/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/localskills/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/stt/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/swagger/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/tock/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/webapphosts/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/workflows/application/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/workflows/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/api/workflows/static/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/healthcheck/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/login/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/logout/index.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/routes.js create mode 100644 platform/linto-admin/webserver/lib/webserver/routes/setup/index.js create mode 100644 platform/linto-admin/webserver/package.json create mode 100644 platform/linto-admin/webserver/public/img/nodered-linto-logo.png rename config/mosquitto/auth/.gitkeep => platform/linto-admin/webserver/readme.md (100%) create mode 100644 platform/mongodb-migration/.docker_env create mode 100644 platform/mongodb-migration/.dockeringore create mode 100644 platform/mongodb-migration/.envdefault create mode 100644 platform/mongodb-migration/.github/workflows/dockerhub-description.yml rename config/mosquitto/conf.d/.gitkeep => platform/mongodb-migration/.gitignore (100%) create mode 100644 platform/mongodb-migration/Dockerfile create mode 100644 platform/mongodb-migration/LICENSE create mode 100644 platform/mongodb-migration/README.md create mode 100644 platform/mongodb-migration/RELEASE.md create mode 100644 platform/mongodb-migration/config.js create mode 100644 platform/mongodb-migration/docker-compose.yml create mode 100755 platform/mongodb-migration/docker-entrypoint.sh create mode 100644 platform/mongodb-migration/index.js create mode 100644 platform/mongodb-migration/migrations/1/index.js create mode 100644 platform/mongodb-migration/migrations/1/json/linto-fleet-default-flow.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/context.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/context_types.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/dbversion.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/flow_pattern.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/flow_pattern_tmp.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/linto_users.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/lintos.json create mode 100644 platform/mongodb-migration/migrations/1/schemas/users.json create mode 100644 platform/mongodb-migration/migrations/2/index.js create mode 100644 platform/mongodb-migration/migrations/2/json/device-default-flow.json create mode 100644 platform/mongodb-migration/migrations/2/json/multi-user-default-flow.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/android_users.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/clients_static.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/db_version.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/flow_tmp.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/local_skills.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/mqtt_acls.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/mqtt_users.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/users.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/webapp_hosts.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/workflows_application.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/workflows_static.json create mode 100644 platform/mongodb-migration/migrations/2/schemas/workflows_templates.json create mode 100644 platform/mongodb-migration/migrations/3/index.js create mode 100644 platform/mongodb-migration/migrations/3/schemas/android_users.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/clients_static.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/db_version.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/flow_tmp.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/local_skills.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/mqtt_acls.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/mqtt_users.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/users.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/webapp_hosts.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/workflows_application.json create mode 100644 platform/mongodb-migration/migrations/3/schemas/workflows_static.json create mode 100644 platform/mongodb-migration/package.json create mode 100755 platform/mongodb-migration/wait-for-it.sh create mode 100644 platform/overwatch/.dockerignore create mode 100644 platform/overwatch/.envdefault create mode 100644 platform/overwatch/.github/workflows/dockerhub-description.yml create mode 100644 platform/overwatch/Dockerfile create mode 100644 platform/overwatch/README.md create mode 100644 platform/overwatch/RELEASE.md create mode 100644 platform/overwatch/config.js create mode 100644 platform/overwatch/doc/api/auth/ldap.md create mode 100644 platform/overwatch/doc/api/auth/local.md create mode 100644 platform/overwatch/doc/api/default.md create mode 100644 platform/overwatch/docker-compose.yml create mode 100755 platform/overwatch/docker-entrypoint.sh create mode 100644 platform/overwatch/docker-healthcheck.js create mode 100644 platform/overwatch/index.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/driver.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/model.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/android_users.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/linto_users.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/lintos.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/logs.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/mqtt_users.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/scopes.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/webapp_hosts.js create mode 100644 platform/overwatch/lib/overwatch/mongodb/models/workflows_application.js create mode 100644 platform/overwatch/lib/overwatch/overwatch.js create mode 100644 platform/overwatch/lib/overwatch/slotsManager/slotsManager.js create mode 100644 platform/overwatch/lib/overwatch/watcher/mqttController/status.js create mode 100644 platform/overwatch/lib/overwatch/watcher/watcher.js create mode 100644 platform/overwatch/package.json create mode 100755 platform/overwatch/wait-for-it.sh create mode 100644 platform/overwatch/webserver/config/auth/local.js create mode 100644 platform/overwatch/webserver/config/auth/refresh/index.js create mode 100644 platform/overwatch/webserver/config/error/exception/auth.js create mode 100644 platform/overwatch/webserver/config/error/handler.js create mode 100644 platform/overwatch/webserver/config/index.js create mode 100644 platform/overwatch/webserver/config/passport/local.js create mode 100644 platform/overwatch/webserver/config/passport/tokenGenerator/index.js create mode 100644 platform/overwatch/webserver/index.js create mode 100644 platform/overwatch/webserver/lib/authWrapper.js create mode 100644 platform/overwatch/webserver/lib/user.js create mode 100644 platform/overwatch/webserver/lib/workflowApplication.js create mode 100644 platform/overwatch/webserver/routes/auth/index.js create mode 100644 platform/overwatch/webserver/routes/index.js create mode 100644 platform/overwatch/webserver/routes/overwatch/index.js create mode 100644 platform/overwatch/webserver/routes/routes.js create mode 100644 platform/service-broker/Dockerfile create mode 100644 platform/service-broker/README.md create mode 100644 platform/service-broker/RELEASE.md create mode 100644 platform/service-broker/docker-compose.yml create mode 100644 platform/service-broker/redis_conf/redis.conf create mode 100644 platform/stt-service-manager/.defaultparam create mode 100644 platform/stt-service-manager/.envdefault create mode 100644 platform/stt-service-manager/.github/workflows/dockerhub-description.yml create mode 100644 platform/stt-service-manager/Dockerfile create mode 100644 platform/stt-service-manager/README.md create mode 100644 platform/stt-service-manager/RELEASE.md create mode 100644 platform/stt-service-manager/app.js create mode 100644 platform/stt-service-manager/components/ClusterManager/DockerSwarm/index.js create mode 100644 platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/LinSTT.js create mode 100644 platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/WebServer.js create mode 100644 platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/myself.js create mode 100644 platform/stt-service-manager/components/ClusterManager/index.js create mode 100644 platform/stt-service-manager/components/IngressController/Nginx/index.js rename {config/servicemanager => platform/stt-service-manager/components/IngressController/Nginx}/nginx.conf (100%) create mode 100644 platform/stt-service-manager/components/IngressController/Traefik/index.js create mode 100644 platform/stt-service-manager/components/IngressController/controllers/eventsFrom/ClusterManager.js create mode 100644 platform/stt-service-manager/components/IngressController/index.js create mode 100644 platform/stt-service-manager/components/LinSTT/Kaldi/index.js create mode 100755 platform/stt-service-manager/components/LinSTT/Kaldi/scripts/fst.awk rename devcerts/.gitkeep => platform/stt-service-manager/components/LinSTT/Kaldi/scripts/path.sh (100%) mode change 100644 => 100755 create mode 100755 platform/stt-service-manager/components/LinSTT/Kaldi/scripts/prepare_HCLG.sh create mode 100644 platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/WebServer.js create mode 100644 platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/myself.js create mode 100644 platform/stt-service-manager/components/LinSTT/index.js create mode 100644 platform/stt-service-manager/components/ServiceManager/controllers/eventsFrom/WebServer.js create mode 100644 platform/stt-service-manager/components/ServiceManager/index.js create mode 100644 platform/stt-service-manager/components/WebServer/controllers/.gitkeep create mode 100644 platform/stt-service-manager/components/WebServer/index.js create mode 100644 platform/stt-service-manager/components/WebServer/middlewares/index.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/healthcheck.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/modelManager/amodel.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/modelManager/amodels.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/modelManager/element.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/modelManager/elements.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/modelManager/lmodel.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/modelManager/lmodels.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/router.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/routes.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/serviceManager/service.js create mode 100644 platform/stt-service-manager/components/WebServer/routes/serviceManager/services.js create mode 100644 platform/stt-service-manager/components/component.js create mode 100644 platform/stt-service-manager/config.js create mode 100644 platform/stt-service-manager/config/nginx.conf rename {config/servicemanager => platform/stt-service-manager/config/seed}/init.js (100%) create mode 100644 platform/stt-service-manager/config/seed/user.js create mode 100644 platform/stt-service-manager/config/swagger.yml create mode 100755 platform/stt-service-manager/docker-compose.yml create mode 100755 platform/stt-service-manager/docker-entrypoint.sh create mode 100755 platform/stt-service-manager/docker-healthcheck.js create mode 100644 platform/stt-service-manager/lib/customErrors.js create mode 100644 platform/stt-service-manager/models/.gitkeep create mode 100644 platform/stt-service-manager/models/driver.js create mode 100644 platform/stt-service-manager/models/model.js create mode 100644 platform/stt-service-manager/models/models/AMUpdates.js create mode 100644 platform/stt-service-manager/models/models/LMUpdates.js create mode 100644 platform/stt-service-manager/models/models/ServiceUpdates.js create mode 100644 platform/stt-service-manager/package.json create mode 100755 platform/stt-service-manager/wait-for-it.sh create mode 100644 stack/.gitignore create mode 100644 stack/LICENSE rename {config => stack/config}/bls/flowsStorage.json (100%) rename {config => stack/config}/jitsi/env/jitsienv_template (100%) rename {config => stack/config}/jitsi/jigasi/sip-communicator.properties (100%) rename {config => stack/config}/jitsi/prosody/conf.d/jitsi-meet.cfg.lua (100%) rename {config => stack/config}/jitsi/prosody/user/prosody-user.dat (100%) rename {config => stack/config}/jitsi/traefik/upd-jvb.toml (100%) rename {config => stack/config}/jitsi/web/custom-config.js (100%) rename {config => stack/config}/mongoseeds/admin-users.js (100%) create mode 100644 stack/config/mosquitto/auth/.gitkeep rename {config => stack/config}/mosquitto/auth/acls (100%) rename {config => stack/config}/mosquitto/conf-tempalte/go-auth-template.conf (100%) create mode 100644 stack/config/mosquitto/conf.d/.gitkeep rename {config => stack/config}/mosquitto/mosquitto.conf (100%) create mode 100644 stack/config/servicemanager/init.js create mode 100644 stack/config/servicemanager/nginx.conf rename {config => stack/config}/servicemanager/user.js (100%) rename {config => stack/config}/tock/scripts/admin-web-entrypoint.sh (100%) rename {config => stack/config}/tock/scripts/setup.sh (100%) rename {config => stack/config}/traefik/http-auth.toml (100%) rename {config => stack/config}/traefik/ssl-redirect.toml (100%) rename {config => stack/config}/traefik/stt-manager-path.toml (100%) rename {config => stack/config}/traefik/tock-path.toml (100%) create mode 100644 stack/devcerts/.gitkeep rename dockerenv_template => stack/dockerenv_template (100%) rename {docs => stack/docs}/Jitsi.md (100%) rename {docs => stack/docs}/README.md (100%) rename {optional-stack-files => stack/optional-stack-files}/grafana.yml (100%) rename {optional-stack-files => stack/optional-stack-files}/linto-platform-jitsi.yml (100%) rename {optional-stack-files => stack/optional-stack-files}/linto-platform-tasks-monitor.yml (100%) rename {optional-stack-files => stack/optional-stack-files}/network_tool.yml (100%) rename {scripts => stack/scripts}/README.md (100%) rename {scripts => stack/scripts}/bls_backup.sh (100%) rename {scripts => stack/scripts}/bls_restore.sh (100%) rename {scripts => stack/scripts}/db_backup.sh (100%) rename {scripts => stack/scripts}/db_restore.sh (100%) rename {scripts => stack/scripts}/start-jitsi.sh (100%) rename {scripts => stack/scripts}/start-optional.sh (100%) rename {stack-files => stack/stack-files}/linto-docker-visualizer.yml (100%) rename {stack-files => stack/stack-files}/linto-edge-router.yml (100%) rename {stack-files => stack/stack-files}/linto-mongo-migration.yml (100%) rename {stack-files => stack/stack-files}/linto-mqtt-broker.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-admin.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-bls.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-mongo.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-overwatch.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-redis.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-stt-service-manager-nginx.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-stt-service-manager.yml (100%) rename {stack-files => stack/stack-files}/linto-platform-tock.yml (100%) rename start.sh => stack/start.sh (100%) diff --git a/.gitignore b/.gitignore index 85902e2..33bca0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,67 @@ -devcerts/*.pem +build/ +stack/devcerts/*.pem + +.vscode +**/dev-build +**/.cache +lib/terminal/linto.json + +# Env +.env + +# node Version +.nvmrc + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz +tmp/ + +# Other +.history/ +temp/ + +**/.env +**/node_modules + +**/settings.tmp.js +**/json_tmp +**/public/tockapp.json +**/public/tocksentences.json +**/dist +**/.local_cmd +**/data +**/dump.rdb +/webserver/model/mongodb/schemas + +**/model/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8eec159 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# LinTO-Agent + +LinTO-Agent contain all tools allowing to play with LinTO + - linto-admin : central manager for a given fleet of LinTO clients + - business-logic-server : deploy and executes a linto workflow + - overwatch : handle the authentification and loging aspect of a linto fleet + - service-broker : the communication pipeline between services and subservices + - stt-service-manager : deploy speech to text service \ No newline at end of file diff --git a/client/rpi/.envdefault b/client/rpi/.envdefault new file mode 100644 index 0000000..82348b9 --- /dev/null +++ b/client/rpi/.envdefault @@ -0,0 +1,11 @@ +# Components loading in this order, components are orchestration plugins for the LinTO client actions +COMPONENTS = localmqtt,logicmqtt,audio + +# Local MQTT bus setup +LOCAL_MQTT_ADDRESS = 127.0.0.1 +LOCAL_MQTT_PORT = 1883 +LOCAL_MQTT_KEEP_ALIVE = 15 + +# Audio and Mic +AUDIO_FILE = /tmp/command.raw +TTS_LANG = fr-FR,en-US,en-GB,es-ES,de-DE,it-IT \ No newline at end of file diff --git a/client/rpi/.gitignore b/client/rpi/.gitignore new file mode 100644 index 0000000..4b56583 --- /dev/null +++ b/client/rpi/.gitignore @@ -0,0 +1,4 @@ +.vscode/ +node_modules +.env +lib/terminal/linto.json diff --git a/client/rpi/README.md b/client/rpi/README.md new file mode 100644 index 0000000..5f696cd --- /dev/null +++ b/client/rpi/README.md @@ -0,0 +1,31 @@ +# LinTO client + +LinTO Client-server connectivity + +This module sequences actions, dispatches informations or triggers between any other modules on the LinTO device +This module is part of the LinTO project, it's the only remote subscriber/publisher, through MQTT(s) protocol, to the LinTO business-logic and exploitation servers. + +## Dependencies + +This program uses a **node.js 9+** runtime. before first run you first need to install node module dependencies + +``` +npm install +``` + +## HOW TO Use the module + +create a .env file based on .envdefault +run the software with : +``` +node index.js +``` +You might overload any of environement variables at runtime +``` +node MY_PREFERED_VALUE=42 index.js +``` + +You shall use something like **PM2** to maintain the process. + + +**Have fun building your LinTO** diff --git a/client/rpi/components/audio/index.js b/client/rpi/components/audio/index.js new file mode 100644 index 0000000..bb2d6aa --- /dev/null +++ b/client/rpi/components/audio/index.js @@ -0,0 +1,21 @@ +const moduleName = 'audio' +const debug = require('debug')(`linto-client:${moduleName}`) +const EventEmitter = require('eventemitter3') + +class Audio extends EventEmitter { + constructor(app) { + super() + this.nlpProcessing = new Array() //array of audiofiles being submitted + this.mic = require(`${process.cwd()}/lib/soundfetch`) + return this.init(app) + } + + async init(app) { + return new Promise((resolve, reject) => { + app[moduleName] = this + resolve(app) + }) + } +} + +module.exports = Audio \ No newline at end of file diff --git a/client/rpi/components/localmqtt/index.js b/client/rpi/components/localmqtt/index.js new file mode 100644 index 0000000..480e7d5 --- /dev/null +++ b/client/rpi/components/localmqtt/index.js @@ -0,0 +1,106 @@ +const moduleName = 'localmqtt' +const debug = require('debug')(`linto-client:${moduleName}`) +const EventEmitter = require('eventemitter3') +const Mqtt = require('mqtt') + +class LocalMqtt extends EventEmitter { + constructor(app) { + super() + this.subTopics = [ + "wuw/wuw-spotted", + "utterance/stop" + ] + this.cnxParam = { + clean: true, + servers: [{ + host: process.env.LOCAL_MQTT_ADDRESS, + port: process.env.LOCAL_MQTT_PORT + }], + keepalive: parseInt(process.env.LOCAL_MQTT_KEEP_ALIVE), //can live for LOCAL_MQTT_KEEP_ALIVE seconds without a single message sent on broker + reconnectPeriod: Math.floor(Math.random() * 1000) + 1000, // ms for reconnect, + will: { + topic: `lintoclient/status`, + retain: true, + payload: JSON.stringify({ + connexion: "offline" + }) + }, + qos: 2 + } + this.on(`localmqtt::connect`, () => { + console.log(`${new Date().toJSON()} Local MQTT connexion up`) + this.publish('status', { //send retained online status + "connexion": "online", + "on": new Date().toJSON() + }, 0, true, true) + }) + return this.init(app) + } + + async init(app) { + return new Promise((resolve, reject) => { + this.client = Mqtt.connect(this.cnxParam) + this.client.on("error", e => { + console.error(`${new Date().toJSON()} Local MQTT broker error ${e}`) + }) + this.client.on("connect", () => { + this.emit(`${moduleName}::connect`) + //clear any previous subsciptions + this.subTopics.map((topic) => { + this.client.unsubscribe(topic, (err) => { + if (err) debug('disconnecting while unsubscribing', err) + //Subscribe to the client topics + debug(`subscribing topics...`) + this.client.subscribe(topic, (err) => { + if (!err) { + debug(`subscribed successfully to ${topic}`) + } else { + debug(err) + } + }) + }) + }) + }) + + this.client.on("offline", () => { + console.error(`${new Date().toJSON()} Local MQTT connexion down`) + }) + + this.client.on('message', (topic, payload) => { + debug(topic, payload) + try { + let subTopics = topic.split("/") + payload = JSON.parse(payload.toString()) + let command = subTopics.pop() + let topicRoot = subTopics.pop() + this.emit(`${moduleName}::${topicRoot}/${command}`, payload) + } catch (err) { + debug(err) + } + }) + app[moduleName] = this + resolve(app) + }) + } + + publish(topic, value, qos = 2, retain = false, requireOnline = false) { + const pubTopic = 'lintoclient' + '/' + topic + const pubOptions = { + "qos": qos, + "retain": retain + } + if (requireOnline === true) { + if (this.client.connected === true) { + this.client.publish(pubTopic, JSON.stringify(value), pubOptions, function (err) { + if (err) debug("publish error", err) + }) + } + } else { + this.client.publish(pubTopic, JSON.stringify(value), pubOptions, function (err) { + if (err) debug("publish error", err) + }) + } + } +} + +module.exports = LocalMqtt \ No newline at end of file diff --git a/client/rpi/components/logicmqtt/index.js b/client/rpi/components/logicmqtt/index.js new file mode 100644 index 0000000..f741f96 --- /dev/null +++ b/client/rpi/components/logicmqtt/index.js @@ -0,0 +1,162 @@ +const moduleName = 'logicmqtt' +const debug = require('debug')(`linto-client:${moduleName}`) +const EventEmitter = require('eventemitter3') +const Mqtt = require('mqtt') +const stream = require('stream') + + + +class LogicMqtt extends EventEmitter { + constructor(app) { + super() + this.app = app + this.pubTopicRoot = `${app.terminal.info.config.mqtt.scope}/${app.terminal.info.config.mqtt.frommetopic}/${app.terminal.info.sn}` + this.subTopic = `${app.terminal.info.config.mqtt.scope}/${app.terminal.info.config.mqtt.towardsmetopic}/${app.terminal.info.sn}/#` + this.cnxParam = { + protocol: app.terminal.info.config.mqtt.protocol, + clean: true, + servers: [{ + host: app.terminal.info.config.mqtt.host, + port: app.terminal.info.config.mqtt.port + }], + keepalive: parseInt(app.terminal.info.config.mqtt.keepalive), //can live for LOCAL_MQTT_KEEP_ALIVE seconds without a single message sent on broker + reconnectPeriod: Math.floor(Math.random() * 1000) + 1000, // ms for reconnect, + will: { + topic: `${this.pubTopicRoot}/status`, + retain: true, + payload: JSON.stringify({ + connexion: "offline" + }) + }, + qos: 2 + } + + if (app.terminal.info.config.mqtt.uselogin) { + this.cnxParam.username = app.terminal.info.config.mqtt.username + this.cnxParam.password = app.terminal.info.config.mqtt.password + } + + this.on(`${moduleName}::connect`, () => { + console.log(`${new Date().toJSON()} Logic MQTT connexion up`) + this.publishStatus() + }) + return this.init(app) + } + + async init(app) { + return new Promise((resolve, reject) => { + this.client = Mqtt.connect(this.cnxParam) + this.client.on("error", e => { + console.error(`${new Date().toJSON()} Logic MQTT broker error ${e}`) + }) + this.client.on("connect", () => { + this.emit(`${moduleName}::connect`) + //clear any previous subsciptions + this.client.unsubscribe(this.subTopic, (err) => { + if (err) debug('disconnecting while unsubscribing', err) + //Subscribe to the client topics + debug(`subscribing topics...`) + this.client.subscribe(this.subTopic, (err) => { + if (!err) { + debug(`subscribed successfully to ${this.subTopic}`) + } else { + debug(err) + } + }) + }) + }) + + this.client.on("offline", () => { + app.localmqtt.publish(`disconnected`, { //send retained connected status + "connexion": "offline", + "on": new Date().toJSON() + }, 0, false, true) + console.error(`${new Date().toJSON()} Logic MQTT connexion down `) + }) + + this.client.on('message', (topic, payload) => { + try { + let topicArray = topic.split("/") + payload = JSON.parse(payload.toString()) + payload = Object.assign(payload, { + topicArray + }) + this.emit(`${moduleName}::message`, payload) + } catch (err) { + debug(err) + } + }) + app[moduleName] = this + resolve(app) + }) + } + + publish(topic, value, qos = 2, retain = false, requireOnline = false) { + const pubTopic = this.pubTopicRoot + '/' + topic + const pubOptions = { + "qos": qos, + "retain": retain + } + if (requireOnline === true) { + if (this.client.connected === true) { + this.client.publish(pubTopic, JSON.stringify(value), pubOptions, function (err) { + if (err) debug("publish error", err) + }) + } + } else { + this.client.publish(pubTopic, JSON.stringify(value), pubOptions, function (err) { + if (err) debug("publish error", err) + }) + } + } + + publishaudio(audioStream, conversationData = {}) { + const FileWriter = require('wav').FileWriter + const outputFileStream = new FileWriter('/tmp/command.wav', { + sampleRate: 16000, + channels: 1 + }) + audioStream.pipe(outputFileStream) + const pubOptions = { + "qos": 0, + "retain": false + } + const fileId = Math.random().toString(16).substring(4) + const pubTopic = `${this.pubTopicRoot}/nlp/file/${fileId}` + return new Promise((resolve, reject) => { + try { + let fileBuffers = [] + outputFileStream.on('data', (data) => { + fileBuffers.push(data) + }) + outputFileStream.on('end', () => { + let sendFile = Buffer.concat(fileBuffers) + sendFile = sendFile.toString('base64') + const payload = { + "audio": sendFile, + "conversationData": conversationData + } + this.client.publish(pubTopic, JSON.stringify(payload), pubOptions, (err) => { + if (err) return reject(err) + resolve(fileId) + }) + }) + } catch (e) { + console.log(e) + } + + }) + } + + publishStatus() { + this.app.terminal.info.connexion = "online" + this.app.terminal.info.on = new Date().toJSON() + this.publish(`status`, this.app.terminal.info, 0, true, true) + this.app.localmqtt.publish(`connected`, { //send retained connected status in lintoclient/connected + "connexion": "online", + "on": new Date().toJSON() + }, 0, true, true) + } +} + +module.exports = LogicMqtt \ No newline at end of file diff --git a/client/rpi/config.js b/client/rpi/config.js new file mode 100644 index 0000000..10092a1 --- /dev/null +++ b/client/rpi/config.js @@ -0,0 +1,33 @@ +const debug = require('debug')('logic-client:config') +const dotenv = require('dotenv') +const fs = require('fs') + +function noop() { } + +function ifHasNotThrow(element, error) { + if (!element) throw error + return element +} + +function ifHas(element, defaultValue) { + if (!element) return defaultValue + return element +} + +function configureDefaults() { + try { + dotenv.config() + const envdefault = dotenv.parse(fs.readFileSync('.envdefault')) + process.env.AUDIO_FILE = ifHas(process.env.AUDIO_FILE, envdefault.AUDIO_FILE) + process.env.COMPONENTS = ifHas(process.env.COMPONENTS, envdefault.COMPONENTS) + process.env.TTS_LANG = ifHas(process.env.TTS_LANG, envdefault.TTS_LANG) + process.env.LOCAL_MQTT_KEEP_ALIVE = ifHas(process.env.LOCAL_MQTT_KEEP_ALIVE, envdefault.LOCAL_MQTT_KEEP_ALIVE) + process.env.LOCAL_MQTT_ADDRESS = ifHas(process.env.LOCAL_MQTT_ADDRESS, envdefault.LOCAL_MQTT_ADDRESS) + process.env.LOCAL_MQTT_PORT = ifHas(process.env.LOCAL_MQTT_PORT, envdefault.LOCAL_MQTT_PORT) + + } catch (e) { + console.error(debug.namespace, e) + process.exit(1) + } +} +module.exports = configureDefaults() \ No newline at end of file diff --git a/client/rpi/controller/lasvegas.js b/client/rpi/controller/lasvegas.js new file mode 100644 index 0000000..ecf08d1 --- /dev/null +++ b/client/rpi/controller/lasvegas.js @@ -0,0 +1,33 @@ +/** + * Events from app.logicmqtt + */ +const debug = require('debug')(`linto-client:lasvegas:events`) +//Shell execution +const child_process = require('child_process') +const exec = child_process.exec + +function lasVegas(app) { + if (!app.localmqtt || !app.logicmqtt) return + + app.logicmqtt.on("logicmqtt::message", async (payload) => { + debug("Received %O", payload) + let runningVideo = false + if (!!payload.topicArray && payload.topicArray[3] === "demo_mode") { + if (payload.value === "start") { + let cmd = `export DISPLAY=:0 && cvlc --loop --no-osd --aspect-ratio 15:9 -f /home/pi/demo.mp4` + runningVideo = exec(cmd, function (err, stdout, stderr) { + debug(err, stdout, stderr) + }) + debug(runningVideo) + } + if (payload.value === "stop") { + let cmd = "sudo killall vlc" + debug(cmd) + let proc = exec(cmd, function (err, stdout, stderr) { }) + } + } + }) + +} + +module.exports = lasVegas \ No newline at end of file diff --git a/client/rpi/controller/localmqttevents.js b/client/rpi/controller/localmqttevents.js new file mode 100644 index 0000000..fc9d5d2 --- /dev/null +++ b/client/rpi/controller/localmqttevents.js @@ -0,0 +1,27 @@ +/** + * Events from app.localmqtt + */ +const debug = require('debug')(`linto-client:localmqtt:events`) + +function localMqttEvents(app) { + if (!app.localmqtt || !app.logicmqtt || !app.audio) return + + app.localmqtt.on("localmqtt::wuw/spotted", async (payload) => { + return + }) + + app.localmqtt.on("localmqtt::utterance/stop", async (payload) => { + if (payload.reason === "canceled" || payload.reason === "timeout") return + const audioStream = app.audio.mic.readStream() + // Notify for new request beign sent + app.localmqtt.publish("request/send", { + "on": new Date().toJSON() + }, 0, false, true) + const audioRequestID = await app.logicmqtt.publishaudio(audioStream, app.conversationData) + debug("conversationData reset") + app.conversationData = {} + app.audio.nlpProcessing.push(audioRequestID) + }) +} + +module.exports = localMqttEvents \ No newline at end of file diff --git a/client/rpi/controller/logicmqttevents.js b/client/rpi/controller/logicmqttevents.js new file mode 100644 index 0000000..17f20b9 --- /dev/null +++ b/client/rpi/controller/logicmqttevents.js @@ -0,0 +1,158 @@ +/** + * Events from app.logicmqtt + */ +const debug = require('debug')(`linto-client:logicmqtt:events`) +//Shell execution +const child_process = require('child_process') +const exec = child_process.exec + +function logicMqttEvents(app) { + if (!app.localmqtt || !app.logicmqtt) return + + app.logicmqtt.on("logicmqtt::message", async (payload) => { + debug("Received %O", payload) + /****************** + * Utility messages + ******************/ + if (!!payload.topicArray && payload.topicArray[3] === "ping") { + app.logicmqtt.publish("pong", {}, 0, false, true) + } + + if (!!payload.topicArray && payload.topicArray[3] === "mute") { + app.logicmqtt.publish("muteack", {}, 0, false, true) + app.localmqtt.publish("mute", {}, 0, false, true) + } + + if (!!payload.topicArray && payload.topicArray[3] === "unmute") { + app.logicmqtt.publish("unmuteack", {}, 0, false, true) + app.localmqtt.publish("unmute", {}, 0, false, true) + } + + if (!!payload.topicArray && payload.topicArray[3] === "volume") { + app.localmqtt.publish("volume", { "value": payload.value }, 0, false, true) + } + + if (!!payload.topicArray && payload.topicArray[3] === "endvolume") { + try { + if (payload.value) { + // Update memory version of this.terminal configuration (linto.json) + app.terminal.info.config.sound.volume = parseInt(payload.value) + await app.terminal.save() // dumps linto.json down to disk + app.logicmqtt.publishStatus() + } else { + console.error("Error while trying to update volume") + } + } catch (e) { + console.error(e) + } + } + + if (!!payload.topicArray && payload.topicArray[3] === "startreversessh") { + try { + await startReverseSsh(payload.remoteHost, payload.remoteSSHPort, payload.mySSHPort, payload.remoteUser, payload.privateKey) + app.logicmqtt.publish("startreversessh", { "reversesshstatus": "ok" }, 0, false, true) + } catch (e) { + console.error(e) + app.logicmqtt.publish('startreversessh', { "status": e.message }, 0, false, true) + } + } + + if (!!payload.topicArray && payload.topicArray[3] === "shellexec") { + try { + let ret = await shellExec(payload.cmd) + app.logicmqtt.publish("shellexec", { "stdout": ret.stdout, "stderr": ret.stderr }, 0, false, true) + } catch (e) { + console.error(e.err) + app.logicmqtt.publish('shellexec', { "status": e.message }, 0, false, true) + } + } + + + if (!!payload.topicArray && payload.topicArray[3] === "tts_lang" && !!payload.value) { + try { + const availableTTS = process.env.TTS_LANG.split(',') + if (payload.value && availableTTS.includes(payload.value)) { + // Update memory version of this.terminal configuration (linto.json) + app.terminal.info.config.sound.tts_lang = payload.value + await app.terminal.save() // dumps linto.json down to disk + app.logicmqtt.publishStatus() + app.localmqtt.publish("tts_lang", { "value": payload.value }, 0, false, true) + } else { + console.error("Unsupported TTS value") + } + } catch (e) { + console.error(e) + } + } + + if (!!payload.topicArray && payload.topicArray[3] === "say") { + // Basic say for demo purpose + app.localmqtt.publish("say", { + "value": payload.value, + "on": new Date().toJSON() + }, 0, false, true) + } + + /****************** + * Audio handlings + ******************/ + // NLP file Processed + if (!!payload.topicArray && payload.topicArray[3] === "nlp" && payload.topicArray[4] === "file" && !!payload.topicArray[5]) { + // Do i still wait for this file to get processed ? + if (app.audio.nlpProcessing.includes(payload.topicArray[5])) { + app.audio.nlpProcessing = app.audio.nlpProcessing.filter(e => e !== payload.topicArray[5]) //removes from array of files to process + // Single command mode + if (!!payload.behavior.say) { + debug("conversationData reset") + app.conversationData = {} + debug(`Saying : ${payload.behavior.say.text}`) + app.localmqtt.publish("say", { + "value": payload.behavior.say.text, + "on": new Date().toJSON() + }, 0, false, true) + // Conversational mode + } else if (!!payload.behavior.ask && !!payload.behavior.conversationData) { + app.conversationData = payload.behavior.conversationData + debug("conversationData sets to : " + app.conversationData) + debug(`asking : ${payload.behavior.ask.text}`) + app.localmqtt.publish("ask", { + "value": payload.behavior.ask.text, + "on": new Date().toJSON() + }, 0, false, true) + } + } else return + } + }) +} + +function startReverseSsh(remoteHost, remoteSSHPort, mySSHPort = 22, remoteUser, privateKey) { + console.log(`${new Date().toJSON()} Starting reverse SHH :`, remoteHost, remoteSSHPort, mySSHPort, remoteUser, privateKey) + mySSHPort = parseInt(mySSHPort) + remoteSSHPort = parseInt(remoteSSHPort) + return new Promise((resolve, reject) => { + //MUST SET UP SSH KEY FOR THIS TO WORK + //WARNING !!! OMAGAD !!! + let cmd = `ssh -o StrictHostKeyChecking=no -NR ${remoteSSHPort}:localhost:${mySSHPort} ${remoteUser}@${remoteHost} -i ${privateKey}` + let proc = exec(cmd, function (err, stdout, stderr) { + if (stderr) console.error(`${new Date().toJSON()} ${stderr}`) + if (err) return reject(err) + return resolve(stdout) + }) + }) +} + +//execute arbitrary shell command +function shellExec(cmd) { + return new Promise((resolve, reject) => { + var proc = exec(cmd, function (err, stdout, stderr) { + if (err) reject(err) + var ret = {} + ret.stdout = stdout + ret.stderr = stderr + resolve(ret) + }) + }) +} + + +module.exports = logicMqttEvents \ No newline at end of file diff --git a/client/rpi/index.js b/client/rpi/index.js new file mode 100644 index 0000000..dd35298 --- /dev/null +++ b/client/rpi/index.js @@ -0,0 +1,53 @@ +// Copyright (C) 2019 LINAGORA +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const debug = require('debug')('linto-client:ctl') +require('./config') +const ora = require('ora') + +class App { + constructor() { + // This LinTO terminal + this.terminal = require('./lib/terminal') // Specific enrolments for this specific terminal + // Inits conversationData to void object, this is is the "context" transfered between client and server on "nlp/file/####" + this.conversationData = {} + // Load components + process.env.COMPONENTS.split(',').reduce((prev, component) => { + return prev.then(async () => { await this.use(component) }) + }, Promise.resolve()).then(() => { + // All components are now loaded. + // Binding controllers on events sent by components + require('./controller/logicmqttevents.js')(this) + require('./controller/localmqttevents.js')(this) + //require('./controller/lasvegas.js')(this) + }) + } + + async use(component) { + let spinner = ora(`Loading behaviors : ${component}`).start() + try { + const injectComponent = require(`./components/${component}`) //component dependency injections with inversion of control + await new injectComponent(this) //shall allways RESOLVE a component injected version of this. + spinner.succeed(`Loaded : ${component}`) + return + } catch (e) { + spinner.fail(`Error in component invocation : ${component}`) + console.error(debug.namespace, e) + process.exit(1) + } + } +} + +module.exports = new App() \ No newline at end of file diff --git a/client/rpi/lib/soundfetch/index.js b/client/rpi/lib/soundfetch/index.js new file mode 100644 index 0000000..e1ee9e2 --- /dev/null +++ b/client/rpi/lib/soundfetch/index.js @@ -0,0 +1,23 @@ +const debug = require('debug')('linto-client:lib:soundfetch') +const fs = require('fs') + +class SoundFetch { + constructor() { + return this + } + + async getFile() { + return new Promise((resolve, reject) => { + fs.readFile(process.env.AUDIO_FILE, function (err, audiofile) { + if (err) return reject(err) + resolve(audiofile) + }) + }) + } + readStream() { + return fs.createReadStream(process.env.AUDIO_FILE) + } + +} + +module.exports = new SoundFetch() \ No newline at end of file diff --git a/client/rpi/lib/terminal/index.js b/client/rpi/lib/terminal/index.js new file mode 100644 index 0000000..80c9a4c --- /dev/null +++ b/client/rpi/lib/terminal/index.js @@ -0,0 +1,43 @@ +const moduleName = 'terminal' +const debug = require('debug')(`linto-client:${moduleName}`) +const _ = require('lodash') +const network = require('network') +const ora = require('ora') +const fs = require('fs') + +class Terminal { + constructor() { + // This LinTO terminal + try { + this.info = require('./linto.json') + network.get_interfaces_list((err, interfaces) => { + if (err) { + console.error(`${new Date().toJSON()} Network info unavailable`) + return this.info.config.network = [] + } + this.info.config.network = interfaces + }) + + } catch (e) { + console.error("Seems like this LinTO does not have a /lib/terminal/linto.json configuration file...") + process.exit() + } + } + + async save() { + return new Promise((resolve, reject) => { + try { + const formattedJson = JSON.stringify(this.info, null, 2) //keep JSON formatting + fs.writeFile(process.cwd() + '/lib/terminal/linto.json', formattedJson, (e) => { + if (e) throw e + debug('Config written to disk') + return resolve() + }); + } catch (e) { + return reject(e) + } + }) + } +} + +module.exports = new Terminal() \ No newline at end of file diff --git a/client/rpi/lib/terminal/linto.sample.json b/client/rpi/lib/terminal/linto.sample.json new file mode 100644 index 0000000..a2e9a7a --- /dev/null +++ b/client/rpi/lib/terminal/linto.sample.json @@ -0,0 +1,56 @@ +{ + "enrolled": true, + "sn": "0", + "connexion": "offline", + "config": { + "network": [ + { + "name": "eth0", + "ip_address": "0.0.0.0", + "mac_address": "ee:ee:ee:ee:ee:ee", + "gateway_ip": "0.0.0.0", + "type": "Wireless|wired" + } + ], + "firmware": "1.1.0", + "ftp": { + "host": "", + "user": "", + "use_secure": "", + "password": "", + "port": "" + }, + "sound": { + "input": "hw:0,1", + "output": "hw:1,1", + "volume": "100", + "sensibility": "100", + "ww-sensibility": "100", + "tts_lang": "fr-FR" + }, + "disk": { + "root_expand": true, + "mounts": { + "root": "", + "tmp": "", + "var": "", + "storage": "" + } + }, + "mqtt": { + "scope": "blk", + "frommetopic": "fromlinto", + "towardsmetopic": "tolinto", + "client_id": "", + "clean": true, + "host": "my.lintoserver", + "port": "1883", + "username": "myusername", + "password": "mypassword", + "uselogin": true, + "keepalive": "10", + "protocol": "MQTTS", + "keyfile": "" + } + } +} \ No newline at end of file diff --git a/client/rpi/package-lock.json b/client/rpi/package-lock.json new file mode 100644 index 0000000..658f69b --- /dev/null +++ b/client/rpi/package-lock.json @@ -0,0 +1,901 @@ +{ + "name": "linto-client", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "callback-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz", + "integrity": "sha1-RwGlEmbwbgbqpx/BcjOCLYdfSQg=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "> 1.0.0 < 3.0.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-spinners": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.3.1.tgz", + "integrity": "sha512-1QL4544moEsDVH9T/l6Cemov/37iv1RtoKf7NJ04A60+4MREXNfx/QvavbH6QoGdsD4N4Mwy49cmaINR/o2mdg==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "requires": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "requires": { + "es5-ext": "^0.10.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "es5-ext": { + "version": "0.10.50", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", + "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "^1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "help-me": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", + "integrity": "sha1-jy1QjQYAtKRW2i8IZVbn5cBWo8Y=", + "requires": { + "callback-stream": "^1.0.2", + "glob-stream": "^6.1.0", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "requires": { + "chalk": "^2.0.1" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mqtt": { + "version": "2.18.8", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.8.tgz", + "integrity": "sha512-3h6oHlPY/yWwtC2J3geraYRtVVoRM6wdI+uchF4nvSSafXPZnaKqF8xnX+S22SU/FcgEAgockVIlOaAX3fkMpA==", + "requires": { + "commist": "^1.0.0", + "concat-stream": "^1.6.2", + "end-of-stream": "^1.4.1", + "es6-map": "^0.1.5", + "help-me": "^1.0.1", + "inherits": "^2.0.3", + "minimist": "^1.2.0", + "mqtt-packet": "^5.6.0", + "pump": "^3.0.0", + "readable-stream": "^2.3.6", + "reinterval": "^1.1.0", + "split2": "^2.1.1", + "websocket-stream": "^5.1.2", + "xtend": "^4.0.1" + } + }, + "mqtt-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-5.6.1.tgz", + "integrity": "sha512-eaF9rO2uFrIYEHomJxziuKTDkbWW5psLBaIGCazQSKqYsTaB3n4SpvJ1PexKaDBiPnMLPIFWBIiTYT3IfEJfww==", + "requires": { + "bl": "^1.2.1", + "inherits": "^2.0.3", + "process-nextick-args": "^2.0.0", + "safe-buffer": "^5.1.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "needle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-1.1.2.tgz", + "integrity": "sha1-0oQaElv9dP77MMA0QQQ2kGHD4To=", + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "network": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/network/-/network-0.4.1.tgz", + "integrity": "sha1-MLtNQbYkBypNqZBDH3dRNhLEXMA=", + "requires": { + "async": "^1.5.2", + "commander": "2.9.0", + "needle": "1.1.2", + "wmic": "^0.1.0" + } + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "ora": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-2.1.0.tgz", + "integrity": "sha512-hNNlAd3gfv/iPmsNxYoAPLvxg7HuPozww7fFonMZvL84tP6Ox5igfk5j/+a9rtJJwqMgKK+JgWsAQik5o0HTLA==", + "requires": { + "chalk": "^2.3.1", + "cli-cursor": "^2.1.0", + "cli-spinners": "^1.1.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^4.0.0", + "wcwidth": "^1.0.1" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "requires": { + "readable-stream": "^2.0.1" + } + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "requires": { + "through2": "^2.0.2" + } + }, + "stream-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz", + "integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=", + "requires": { + "debug": "2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "wav": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wav/-/wav-1.0.2.tgz", + "integrity": "sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==", + "requires": { + "buffer-alloc": "^1.1.0", + "buffer-from": "^1.0.0", + "debug": "^2.2.0", + "readable-stream": "^1.1.14", + "stream-parser": "^0.3.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "websocket-stream": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.0.tgz", + "integrity": "sha512-EXy/zXb9kNHI07TIMz1oIUIrPZxQRA8aeJ5XYg5ihV8K4kD1DuA+FY6R96HfdIHzlSzS8HiISAfrm+vVQkZBug==", + "requires": { + "duplexify": "^3.5.1", + "inherits": "^2.0.1", + "readable-stream": "^2.3.3", + "safe-buffer": "^5.1.2", + "ws": "^3.2.0", + "xtend": "^4.0.0" + } + }, + "wmic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wmic/-/wmic-0.1.0.tgz", + "integrity": "sha1-eLQasR0VTLgSgZ4SkWdNrVXY4dc=", + "requires": { + "async": "^3.0.1", + "iconv-lite": "^0.4.13" + }, + "dependencies": { + "async": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/async/-/async-3.0.1.tgz", + "integrity": "sha512-ZswD8vwPtmBZzbn9xyi8XBQWXH3AvOQ43Za1KWYq7JeycrZuUYzx01KvHcVbXltjqH4y0MWrQ33008uLTqXuDw==" + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/client/rpi/package.json b/client/rpi/package.json new file mode 100644 index 0000000..e1afd40 --- /dev/null +++ b/client/rpi/package.json @@ -0,0 +1,21 @@ +{ + "name": "linto-client", + "version": "1.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "Affero General Public License v3", + "dependencies": { + "debug": "^3.1.0", + "dotenv": "^6.0.0", + "eventemitter3": "^3.1.0", + "lodash": "^4.17.21", + "mqtt": "^2.18.1", + "network": "^0.4.1", + "ora": "^2.1.0", + "wav": "^1.0.2" + } +} diff --git a/client/web/README.md b/client/web/README.md new file mode 100644 index 0000000..239216e --- /dev/null +++ b/client/web/README.md @@ -0,0 +1,240 @@ + +# linto-web-client + +## Release note + +v1.1.0 : Breaking change, chatbot now uses new transaction mode. Answers are packed in a specific key + +## About + +A full figured LinTO client designed for building custom voice interactions on a webpage. + +See demo here : [linto.ai](https://linto.ai) + +__Note__ : LinTO Web client relies on WebVoiceSDK for handling everything related to audio input, hotword triggers, recordings... See [LinTO WebVoiceSDK on NPM](https://www.npmjs.com/package/@linto-ai/webvoicesdk) for more informations + +__Note__ : Any LinTO web client needs to have a token registered towards a LinTO server. See more information in LinTO's [official documentation](https://doc.linto.ai) + +__Note__ : This library might cause issues (crashes) on your webpage as some browser's implementation of WebAssembly runtimes is still experimental + +## Usage + +With bundler : +``` +npm i @linto.ai/linto-web-client +``` + +Static script from CDN : + +```html + +``` + +Test right away : + +- Tweak content in tests/index.js (your token, LinTO server endpoint etc) + +```bash +npm run test +``` + +## instanciante + +```js +try { + window.linto = await new Linto( + `${My_linto_stack_domain}/overwatch/local/web/login`, + `${my_app_token}`, + `${ms_timeout_delay_for_commands}` + ); +} catch (lintoError) { + // handle the error +} +``` + +### Handling errors + +This command might throw an error if something bad occurs + +## Instance user methods +```js +- startAudioAcquisition(true, "linto", 0.99) // Uses hotword built in WebVoiceSDK by name / model / threshold +- startCommandPipeline() // Start to listen to hotwords and binds a publisher for acquired audio when speaking stop +- stopCommandPipeline() +- startStreamingPipeline() // Start to listen to hotwords and binds streaming start/stop event when audio acquired +- stopStreamingPipeline() +- triggerHotword(dummyHotwordName = "dummy") // Manualy activate a hotword detection, use it when commandPipeline is active. +- pauseAudioAcquisition() +- resumeAudioAcquisition() +- stopAudioAcquisition() +- startStreaming(metadata = 1) // Tries to initiate a streaming transcription session with your LinTO server. The LinTO server needs a streaming skill and a streaming STT service +- addEventNlp() // Bind the event nlp to handle only linto answer +- removeEventNlp() +- stopStreaming() +- login() // Main startup command to initiate connexion towards your LinTO server +- loggout() +- startHotword() +- stopHotword() +- sendCommandText("blahblah") // Use chatbot pipeline +- sendWidgetText("blahblah") // Publish text to linto (bypass transcribe) +- triggerAction(payload, skillName, eventName) // Publish payload to the desired skill/event +- say("blahblah") // Use browser text to speech +- ask("blahblah ?") // Uses browser text to speech and immediatly triggers hotword when audiosynthesis is complete +- stopSpeech() // Stop linto current speech +``` + +## Instance events + +Use events with : + +```js +linto.addEventListener("event_name", customHandlingFunction); +``` + +Available events : + +- "mqtt_connect" +- "mqtt_connect_fail" +- "mqtt_error" +- "mqtt_disconnect" +- "speaking_on" +- "speaking_off" +- "command_acquired" +- "command_published" +- "command_timeout" +- "hotword_on" +- "say_feedback_from_skill" +- "ask_feedback_from_skill" +- "streaming_start" +- "streaming_stop" +- "streaming_chunk" +- "streaming_final" +- "streaming_fail" +- "action_acquired" +- "action_published" +- "action_feedback" +- "action_error" +- "text_acquired" +- "text_published" +- "chatbot_acquired" +- "chatbot_published" +- "chatbot_feedback" +- "chatbot_error" +- "custom_action_from_skill" + +__NOTE__ : See proposed implementation in ./tests/index.js + + +# linto-web-client Widget + + +## Building sources + +``` +npm install +npm run build-widget +``` + +Those commands will build **linto.widget.min.js** file in the */dist* folder + +## Using library + +Import **linto.widget.min.js** file to your web page. Once it's done, you can create a **new Widget()** object and set custom parameters. + +```html + + +``` + +## Parameters + +| Parameter | type | values | description | +| ---------- | ---------- | ---------- | ---------- | +| **debug** | boolean | true / false | enable or disable console informations when events are triggered | +| **containerId** | string | "div-wrapper-id" | ID of the block that will contain the widget |` +| **lintoWebHost** | string | "https://my-host.com" | Url of the host where the application is deployed | +| **lintoWebToken** | string | "yourToken" | Authorization token to connect the application | +| **widgetMode** | string | "multi-modal" (default) / "minimal-streamin" | Set the widget mode | +| **transactionMode** | string | "skills_and_chatbot" / "chatbot_only" | Use "skills_and_chatbot" to publish on "nlp" mqtt channel. Use "chatbot_only" to publish on "chatbot" mqtt channel| +| **hotwordValue** | string | "linto" | Value of the hotword. Change it if you use an other hotword model than "linto" | +| **streamingStopWord** | string | "stop" | Set stop-word for streaming "infinite" mode | +| **lintoCustomEvents** | array of objects | {"flag": "event_name": func: function(){} } | Bind custom functions to events | +| **widgetMicAnimation** | string | "/path/to/animationfile.json" | Set a custom animation file for "widget microphone animation" | +| **widgetThinkAnimation** | string | "/path/to/animationfile.json" | Set a custom animation file for "widget thinking animation" | +| **widgetSleepAnimation** | string | "/path/to/animationfile.json" | Set a custom animation file for "widget sleeping animation" | +| **widgetTalkAnimation** | string | "/path/to/animationfile.json" | Set a custom animation file for "widget talking animation" | +| **widgetAwakeAnimation** | string | "/path/to/animationfile.json" | Set a custom animation file for "widget awaken animation" | +| **widgetErrorAnimation** | string | "/path/to/animationfile.json" | Set a custom animation file for "widget error animation" | +| **widgetValidateAnimation** | string | "/path/to/animationfile.json" | Set a custom animation file for "widget validation animation" | +| **widgetTitle** | string | "LinTO Widget" | Widget Title value | +| **cssPrimarycolor** | string | "#59bbeb" | Value of the widget primary color (default = "#59bbeb") | +| **cssSecondaryColor** | string | "#055e89" | Value of the widget secondary color (default = "#055e89") | + +## Testing + +You can try the library localy by running the following command: +``` +npm run test-widget +``` + +You can change widget parameteres for your tests by updating parameters in the following file: **/tests/widget/index.js** + +## Custom handlers + +To set custom handlers on events, you can write your own functions and attach it to the widget events. Here is an example: + +```javascript + +const myCustomFunction = (event) => { + console.log('Here is the code') +} +window.widget = new Widget({ + ..., + lintoCustomEvents: [{ + flag: 'my_custom_event', + func: (event) => { + myCustomFunction(event) + } + }] +}) +``` + +## Work with your own wakeup-word model + +As mentionned before, *“linto-web-client”* bundler works with *“webVoiceSdk”*. +If you want to use your own wakeup-word model, you’ll have to clone both repositories and update “linto-web-client” package.json as following: + +### Cloning repositories +```bash +#Cloning repositories +cd /your/local/repo +git clone git@github.com:linto-ai/WebVoiceSDK.git +git clone git@github.com:linto-ai/linto-web-client.git +``` +### Update package.json +```bash +cd /linto-web-client +nano package.json +``` + +### Update “@linto-ai/webvoicesdk” devDependencie path +``` +#package.json +{ + ..., + "devDependencies": { + "@linto-ai/webvoicesdk": "../WebVoiceSDK", + ... + } +} +``` + diff --git a/client/web/package.json b/client/web/package.json new file mode 100644 index 0000000..d125246 --- /dev/null +++ b/client/web/package.json @@ -0,0 +1,54 @@ +{ + "name": "@linto-ai/linto-web-client", + "version": "1.1.1", + "description": "LinTO by LINAGORA is now available on your webpage ! Wow !", + "author": "Damien Laine - LINAGORA", + "main": "src/linto.js", + "module": "src/linto.js", + "browser": "dist/linto.js", + "keywords": [ + "speech-recognition", + "wake-word-detection", + "hotword", + "machine-learning", + "voice-commands", + "voice-activity-detection", + "voice-control", + "record-audio", + "voice-assistant", + "offline-speech-recognition", + "mfcc", + "features-extraction" + ], + "scripts": { + "test": "parcel tests/index.html --out-dir dev-build --log-level 4 --no-cache", + "build": "parcel build src/linto.js --log-level 4 --no-cache --no-source-maps --detailed-report --out-file linto.min.js", + "css-linto-ui": "sass ./src/assets/scss/linto-ui.scss ./src/assets/css/linto-ui.min.css --style compressed --no-source-map", + "build-linto-ui": "npm run css-linto-ui && parcel build src/linto-ui.js --log-level 4 --no-cache --no-source-maps --detailed-report --out-file linto.ui.min.js", + "test-linto-ui": "npm run css-linto-ui && parcel tests/linto-ui/index.html --out-dir dev-build --log-level 4 --no-cache" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/linto-ai/linto-web-client.git" + }, + "license": "AGPLV3", + "bugs": { + "url": "https://github.com/linto-ai/linto-web-client/issues" + }, + "homepage": "https://github.com/linto-ai/linto-web-client#readme", + "devDependencies": { + "@linto-ai/webvoicesdk": "^1.2.5", + "axios": "^1.1.2", + "base64-js": "^1.3.1", + "mobile-detect": "^1.4.4", + "mqtt": "^4.2.1", + "npm": "^6.14.8", + "parcel-bundler": "^1.12.5", + "re-tree": "^0.1.7", + "sass": "^1.46.0", + "ua-device-detector": "^1.1.8" + }, + "browserslist": [ + "since 2017-06" + ] +} diff --git a/client/web/src/assets/audio/beep.mp3 b/client/web/src/assets/audio/beep.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..8e4c9e3af9b9972dfd683e2092086424550ae151 GIT binary patch literal 7572 zcmeI#c{Ei2{|E3p292G;*hY(eZ^jZMB7=}*o3W(GGAK*giOhttGh`=gLQEKpC9<#C zA|y(dN)u_}qfPV6_j`VSbk6Vh$M28d_wz;eg`*oj>zuw0BDqvv0q^!*i zkiVbs-?x=-ppUD`AYev30E+uU&xP}lE)yO15(nlra}gYLUqhr|8sks)5pGWRuYyipzP*HK)Gy{@ zgoq@*6we?)j{z9FK#nYgAG+j;(pH?O;L%@$91X;fGfdq$EIGG#k@j#dAH3$b#_YB7w zYzq!qx8edSb(+_(%K_D;`$8UoLc)=A^Dnu>pmWG9CtT1`)YaeDp95mx_Rme zP59;qe^z5(?+-feP3}#8F2Pj|_Ba-RVyy`x4&7p86y!!SRh8&23zwLHn)mxNA}`R`VaRSwFOd>{enjB&5}QliI=117Xnh!3p4uu9WtH%3*vhFnZESB zoayQe@PEJD-bNMCTXl>p#A$W}`s4(y&K4(nw4fN`q=e|?o@Am%DnZ$l) zZl4%R_>P|{_dg*%SEyk=@?Zk-mW^T&>kwUiyz*vymY30I5BY1Ex2JG798DG`lHJvY z6;tWE6q074R>H5~JeT`|*K4^>lOB2I`D>wu=lv}P@ikawotxUO3eA;Im?qDG#(5e; zTaE#DVN9QcY!7M3_9u;UoF0*)l}Ly4^mwRGmfGIPdEFs#mi^0^Ze$fH{i9Zz$!#)) zHBF5te@S@aUXc|MHiD*~xbxZTV+Hx4x48Z>Sdh>85f_E>fMQwk6z=J)pKvW!cprgR z^~a5X-LOwu@%uu2c>YkE7_xS9TkHKTwTg0z(^d2$)O#da+uBbOGeBSu_WSC{34uM> zTM3_9%6>GJ`6%d(<(-o%xwGv*f9+IX(GCi&Zopz^%BL~a_Dxu_eUs<#r=7y4Z0&zM zt1r-~NwfAhuv4SZv^a^=f*TQEg)GQW^%P^&=?7@pRC6~WmMKh3X|IL@#lBDi{vB@bXO&&n$9G*IM)xM1`G`+FH7ffUjbcV&+MXWRTpWHBm?}f;- zrz`Si5*;i#_JtJj{JD0Co5;kau-gnM_M)8^;G z9_zgIJ^M1EL4nz`iz-INU@D37}w3nYVi7d?%rgGM=oHAbEVML$( z#^%ra(JEHsOxFeO`T-Opg8elhz$5{2ZQ%y;d7V6&bvZ9jLJ`i896Y|jAHK@hDIO=+ zeN$q@c~LKynx<(!^*siKWf>vXRHPI?$Jb;ncqNr(=7@^2_aA-qjf?&Z^L~t>H~!>B zuNWN4b+w#mAD0p^q+UT8tep19oCtfVwRx(^{-2Z0K23{NsDh_R#n??W|6B3E7Z9{c zs51w1GEzu)=<;w3Cn$wXurfB{VP%%VD1QRbVx|g4c(EOb0Z43IZy6w-Nnd+e%~PJ# zi`cXs-4`+d_{jzYvbZ88Olm8Fh-{$d6^d5fw0d##WUtb$h|Z(P*F`tnLxYPfN*Kg+ zwr#93%u4Ed7)=wBso&KZ!kVRPbPe@Eyb@Wy8}3-4z$+T548;)B_hz=wI{YpxsbnOM zQNTHTver8heS5;2*zj-5&%4_>>sfir;zc{|2Rvxo=F0LhQGV&(O!k1?`=R-HGGj+p z;xy0c%7k;g1aWh(TYL%86sN@iQHJ{B>qmO@WZoR<;f;Ia_Aql|beNSlx1I8+r)lf` zhC+S~i8l1M9)4>Uy69)DP>)!tP*olxZ3Ahy@EgT4lckn(4Rfs>UWv+?!cuyq3e`1C z-&kS44n}cYz?C>q>tgl1WigmSt8dZcq~Ij$0&0hWDn5c1~-$rcqnGT_1Ys z@NZW`n`fLKX1Zmcs31P@4lYHj&lV2YxFtj3b97o*aX_Hmp#bR z9usABtV9oX&6Y*uCUdJC|7Bb8l{g6qH>S{qB)D$3p~qAwa?9fw7`odC$vu?9OZ(nA zifYVyu`lF?7p!vet?AgjEzs~`XJ2TPDn+Fu7PIC1F6~NM78%Zq+BU9NbHR#GGfqQ8 zhQcu}ztSgWDb(ynXxRHwOP+5r7ZhtT=UWMOyQHDbBK`0X?M&=lWPNGr(sGaUSb=+J zSsW|Gt$Z}?Eb|#RYbld$ES7>~8aUhb9wwbDQcnUeRQmiFe zlxbOgZ}!;^zaK`xWHRGfx}EENj`OAtxVH$Ui#Y7cI!2M8+5cYTa%J}N>eaoNt);Ez zhqP2>8W0=IAQ9qCr~f>vb2VXLyOSWEqsz-!;IiOo95Fa6AS^dqdkVu`vJT@*8fEj0 zBT5gA=cFoFYwDPo!Ba+~(gZWa-1*^#ZR}eX> zcwYzyz-H{C-qt6lx8L7t->s*NrY6?Q>y2v$Nnu12^#;qtgtZ-Z^6jd_RNt_!YN414 zDwNpc1sbLlKKi{jX3?ow>n&OLXCA9U!&AYZ!%CTwUF71CRVb7^A_^_mG}V=~{v&5q zLj5+U$?YS@wPg8V(#Vt~t;itAin$4qgEOaGI$pY3pP$m$91XU3niGrzNjOF>{Q~uwJ$4GzKLJZwHzE8K&PSv5Os@=RuG-A0 zjrqgc3=F(3)-`wMwtg2TU!Jzcy}@actF1|WDo;m>)ogQ58pTr`%-S+0_^pLdwxl2?&e&wAC_I~ zvHX_F#haS`BqKIu2`<~!|M=?YT4hKETJk=;8(}IE<^RL({8w;kz$fs8Sb8~F?tZ0H zHZI5h3NPLp})!G&sh$H{>kfqeK9fHuIuQCZkH4=w5c>N}{+#GQ=+8X(A7;Q#;t literal 0 HcmV?d00001 diff --git a/client/web/src/assets/css/.gitkeep b/client/web/src/assets/css/.gitkeep new file mode 100644 index 0000000..53a0538 --- /dev/null +++ b/client/web/src/assets/css/.gitkeep @@ -0,0 +1,15 @@ +// Copyright (C) 2022 Romain Lopez +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + diff --git a/client/web/src/assets/css/linto-ui.min.css b/client/web/src/assets/css/linto-ui.min.css new file mode 100644 index 0000000..f009e4f --- /dev/null +++ b/client/web/src/assets/css/linto-ui.min.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Spartan:wght@300;400;500;600;700;800;900&display=swap";@-webkit-keyframes blinkRedWhite{0%{background-color:#fff}50%{background-color:#ec5a5a}100%{background-color:#fff}}@keyframes blinkRedWhite{0%{background-color:#fff}50%{background-color:#ec5a5a}100%{background-color:#fff}}@-webkit-keyframes blinkWhiteRed{0%{background-color:#ec5a5a}50%{background-color:#fff}100%{background-color:#ec5a5a}}@keyframes blinkWhiteRed{0%{background-color:#ec5a5a}50%{background-color:#fff}100%{background-color:#ec5a5a}}#widget-mm-wrapper{display:flex;flex-direction:column;position:fixed;bottom:20px;right:20px;z-index:999;font-family:"Spartan","Arial","Helvetica";height:auto}#widget-mm-wrapper button{border:none;margin:0;padding:0}#widget-mm-wrapper button:hover{cursor:pointer}#widget-mm-wrapper .flex{display:flex}#widget-mm-wrapper .flex.col{flex-direction:column;margin:0;padding:0}#widget-mm-wrapper .flex.row{flex-direction:row;margin:0;flex-wrap:nowrap}#widget-mm-wrapper .flex1{flex:1}#widget-mm-wrapper .flex2{flex:2}#widget-mm-wrapper .flex3{flex:3}#widget-mm-wrapper #widget-mm{width:260px;height:480px;display:flex;flex-direction:column;background:#fafeff;background:linear-gradient(0deg, rgb(236, 252, 255) 0%, rgb(250, 254, 255) 100%);-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-moz-box-shadow:0 0px 8px 0 rgba(0,20,66,.3);-webkit-box-shadow:0 0px 8px 0 rgba(0,20,66,.3);box-shadow:0 0px 8px 0 rgba(0,20,66,.3);overflow:hidden;z-index:20}#widget-mm-wrapper .widget-close-btn{display:inline-block;width:30px;height:30px;position:absolute;top:20px;left:100%;margin-left:-50px;mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='close.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='7.8666667' inkscape:cx='-13.983051' inkscape:cy='14.745763' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cpath d='m 22.684485,21.147587 -6.147589,-6.147588 6.147589,-6.1475867 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 L 21.916036,7.3155152 c -0.219556,-0.2195567 -0.548892,-0.2195567 -0.768448,0 L 15,13.463103 8.8524123,7.3155152 c -0.2195568,-0.2195567 -0.5488919,-0.2195567 -0.7684486,0 L 7.3155152,8.0839633 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 6.1475878,6.1475867 -6.1475878,6.147588 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 0.7684485,0.768449 c 0.2195567,0.219556 0.5488918,0.219556 0.7684486,0 L 15,16.536896 l 6.147588,6.147589 c 0.219556,0.219556 0.548892,0.219556 0.768448,0 l 0.768449,-0.768449 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 z' id='path2' inkscape:connector-curvature='0' style='fill:%23ed1c24;stroke-width:1.09778357' /%3E %3C/svg%3E");-webkit-mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='close.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='7.8666667' inkscape:cx='-13.983051' inkscape:cy='14.745763' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cpath d='m 22.684485,21.147587 -6.147589,-6.147588 6.147589,-6.1475867 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 L 21.916036,7.3155152 c -0.219556,-0.2195567 -0.548892,-0.2195567 -0.768448,0 L 15,13.463103 8.8524123,7.3155152 c -0.2195568,-0.2195567 -0.5488919,-0.2195567 -0.7684486,0 L 7.3155152,8.0839633 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 6.1475878,6.1475867 -6.1475878,6.147588 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 0.7684485,0.768449 c 0.2195567,0.219556 0.5488918,0.219556 0.7684486,0 L 15,16.536896 l 6.147588,6.147589 c 0.219556,0.219556 0.548892,0.219556 0.768448,0 l 0.768449,-0.768449 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 z' id='path2' inkscape:connector-curvature='0' style='fill:%23ed1c24;stroke-width:1.09778357' /%3E %3C/svg%3E");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;background-color:#59bbeb}#widget-mm-wrapper .widget-close-btn:hover{background-color:#055e89}#widget-mm-wrapper #widget-show-minimal{display:inline-block;width:30px;height:30px;position:absolute;top:-20px;left:100%;margin-left:-30px;background-color:#fff;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;-moz-box-shadow:0 1px 3px 0 rgba(0,0,0,.3);-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.3);box-shadow:0 1px 3px 0 rgba(0,0,0,.3)}#widget-mm-wrapper #widget-show-minimal .icon{display:inline-block;width:20px;height:20px;mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Created with Inkscape (http://www.inkscape.org/) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='60' height='60' viewBox='0 0 15.875 15.875' version='1.1' id='svg8' inkscape:version='0.92.4 (5da689c313, 2019-01-14)' sodipodi:docname='feedback.svg'%3E %3Cdefs id='defs2' /%3E %3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='7.9195959' inkscape:cx='19.217812' inkscape:cy='28.198286' inkscape:document-units='mm' inkscape:current-layer='g1379' showgrid='false' units='px' inkscape:window-width='1920' inkscape:window-height='1016' inkscape:window-x='1920' inkscape:window-y='27' inkscape:window-maximized='1' /%3E %3Cmetadata id='metadata5'%3E %3Crdf:RDF%3E %3Ccc:Work rdf:about=''%3E %3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E %3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E %3Cdc:title%3E%3C/dc:title%3E %3C/cc:Work%3E %3C/rdf:RDF%3E %3C/metadata%3E %3Cg inkscape:label='Calque 1' inkscape:groupmode='layer' id='layer1' transform='translate(0,-281.12498)'%3E %3Cg id='g1379' transform='translate(-17.481398,-19.087812)'%3E %3Cg id='g1384' transform='matrix(0.93121121,0,0,0.93121121,-1.173127,23.437249)'%3E %3Crect ry='1.0118146' y='301.09048' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' width='12.141764' height='2.0236292' rx='1.0118146' id='rect2' /%3E %3Crect ry='1.0118146' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' y='304.73309' width='12.141764' height='2.0236292' rx='1.0118146' id='rect4' /%3E %3Crect ry='1.0118146' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' y='308.37561' width='8.4992399' height='2.0236292' rx='1.0118146' id='rect6' /%3E %3C/g%3E %3C/g%3E %3C/g%3E %3C/svg%3E");-webkit-mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Created with Inkscape (http://www.inkscape.org/) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='60' height='60' viewBox='0 0 15.875 15.875' version='1.1' id='svg8' inkscape:version='0.92.4 (5da689c313, 2019-01-14)' sodipodi:docname='feedback.svg'%3E %3Cdefs id='defs2' /%3E %3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='7.9195959' inkscape:cx='19.217812' inkscape:cy='28.198286' inkscape:document-units='mm' inkscape:current-layer='g1379' showgrid='false' units='px' inkscape:window-width='1920' inkscape:window-height='1016' inkscape:window-x='1920' inkscape:window-y='27' inkscape:window-maximized='1' /%3E %3Cmetadata id='metadata5'%3E %3Crdf:RDF%3E %3Ccc:Work rdf:about=''%3E %3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E %3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E %3Cdc:title%3E%3C/dc:title%3E %3C/cc:Work%3E %3C/rdf:RDF%3E %3C/metadata%3E %3Cg inkscape:label='Calque 1' inkscape:groupmode='layer' id='layer1' transform='translate(0,-281.12498)'%3E %3Cg id='g1379' transform='translate(-17.481398,-19.087812)'%3E %3Cg id='g1384' transform='matrix(0.93121121,0,0,0.93121121,-1.173127,23.437249)'%3E %3Crect ry='1.0118146' y='301.09048' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' width='12.141764' height='2.0236292' rx='1.0118146' id='rect2' /%3E %3Crect ry='1.0118146' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' y='304.73309' width='12.141764' height='2.0236292' rx='1.0118146' id='rect4' /%3E %3Crect ry='1.0118146' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' y='308.37561' width='8.4992399' height='2.0236292' rx='1.0118146' id='rect6' /%3E %3C/g%3E %3C/g%3E %3C/g%3E %3C/svg%3E");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:#59bbeb;margin:5px}#widget-mm-wrapper #widget-show-minimal:hover .icon{background-color:#055e89}#widget-mm-wrapper #widget-corner-anim{width:80px;height:80px;position:fixed;top:100%;left:100%;margin-left:-100px;margin-top:-100px}#widget-mm-wrapper #widget-corner-anim #widget-show-btn{display:inline-block;width:80px;height:80px;background-color:rgba(0,0,0,0)}#widget-mm-wrapper #widget-init-wrapper{position:relative;padding:20px;justify-content:center;align-items:center}#widget-mm-wrapper #widget-init-wrapper .widget-init-title{display:inline-block;width:100%;text-align:center;font-weight:800;font-size:22px;color:#454545;padding-bottom:10px}#widget-mm-wrapper #widget-init-wrapper .widget-init-logo{display:inline-block;width:60px;height:auto;margin:10px 0}#widget-mm-wrapper #widget-init-wrapper .widget-init-content{display:inline-block;font-size:16px;font-weight:500;text-align:center;line-height:24px;color:#333;margin:10px 0}#widget-mm-wrapper #widget-init-wrapper .widget-init-btn{display:inline-block;padding:10px 0 8px 0;margin:10px 0;width:100%;height:auto;text-align:center;font-size:16px;font-weight:400;color:#fff;-webkit-border-radius:20px;-moz-border-radius:20px;border-radius:20px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;-moz-box-shadow:0 2px 4px 0 rgba(0,20,66,.2);-webkit-box-shadow:0 2px 4px 0 rgba(0,20,66,.2);box-shadow:0 2px 4px 0 rgba(0,20,66,.2);font-family:"Spartan","Arial","Helvetica"}#widget-mm-wrapper #widget-init-wrapper .widget-init-btn.enable{background-color:#59bbeb}#widget-mm-wrapper #widget-init-wrapper .widget-init-btn.enable:hover{background-color:#055e89;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none}#widget-mm-wrapper #widget-init-wrapper .widget-init-btn.close{background-color:#ff9292}#widget-mm-wrapper #widget-init-wrapper .widget-init-btn.close:hover{background-color:#fd3b3b;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none}#widget-mm-wrapper #widget-init-wrapper .widget-init-settings{text-align:left;width:100%}#widget-mm-wrapper #widget-init-wrapper .widget-init-settings .widget-settings-label{font-size:14px}#widget-mm-wrapper .widget-mm-header{height:auto;padding:20px 15px;background:#fff;justify-content:space-between;-moz-box-shadow:0 0 4px 0 rgba(0,20,66,.3);-webkit-box-shadow:0 0 4px 0 rgba(0,20,66,.3);box-shadow:0 0 4px 0 rgba(0,20,66,.3)}#widget-mm-wrapper .widget-mm-header .widget-mm-title{display:inline-block;height:30px;line-height:36px;font-size:14px;font-weight:700;color:#59bbeb}#widget-mm-wrapper .widget-mm-header #widget-mm-settings-btn{display:inline-block;width:20px;height:30px;mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 24 36' style='enable-background:new 0 0 24 36;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23454545;%7D %3C/style%3E %3Cg%3E %3Cpath class='st0' d='M12,15.1c-1.2,0-2.1-1-2.1-2.1s1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,14.8,12.6,15.1,12,15.1z'/%3E %3Cpath class='st0' d='M12,21.5c-1.2,0-2.1-1-2.1-2.1c0-1.2,1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,21.2,12.6,21.5,12,21.5z'/%3E %3Cpath class='st0' d='M12,27.8c-1.2,0-2.1-1-2.1-2.1c0-1.2,1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,27.6,12.6,27.8,12,27.8z'/%3E %3C/g%3E %3C/svg%3E");-webkit-mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 24 36' style='enable-background:new 0 0 24 36;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23454545;%7D %3C/style%3E %3Cg%3E %3Cpath class='st0' d='M12,15.1c-1.2,0-2.1-1-2.1-2.1s1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,14.8,12.6,15.1,12,15.1z'/%3E %3Cpath class='st0' d='M12,21.5c-1.2,0-2.1-1-2.1-2.1c0-1.2,1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,21.2,12.6,21.5,12,21.5z'/%3E %3Cpath class='st0' d='M12,27.8c-1.2,0-2.1-1-2.1-2.1c0-1.2,1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,27.6,12.6,27.8,12,27.8z'/%3E %3C/g%3E %3C/svg%3E");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:#59bbeb;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease}#widget-mm-wrapper .widget-mm-header #widget-mm-settings-btn:hover{background-color:#055e89}#widget-mm-wrapper .widget-mm-header #widget-mm-settings-btn.opened{background-color:#055e89}#widget-mm-wrapper .widget-mm-header #widget-mm-collapse-btn{display:inline-block;width:30px;height:30px;mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='collapse.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='11.125147' inkscape:cx='-19.36456' inkscape:cy='10.31554' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cg id='g835' transform='matrix(-0.46880242,0.45798398,-0.46880242,-0.45798398,3.7385412,35.57659)'%3E%3Crect y='1.8655396' x='-47.999367' height='3.5055718' width='22.112068' id='rect816' style='opacity:1;vector-effect:none;fill:%23000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.77952766;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1' /%3E%3Crect inkscape:transform-center-y='-5.6628467' inkscape:transform-center-x='-9.1684184' transform='rotate(90)' y='25.887299' x='1.8655396' height='3.5055718' width='22.112068' id='rect816-3' style='opacity:1;vector-effect:none;fill:%23000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.77952766;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1' /%3E%3C/g%3E%3C/svg%3E");-webkit-mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='collapse.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='11.125147' inkscape:cx='-19.36456' inkscape:cy='10.31554' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cg id='g835' transform='matrix(-0.46880242,0.45798398,-0.46880242,-0.45798398,3.7385412,35.57659)'%3E%3Crect y='1.8655396' x='-47.999367' height='3.5055718' width='22.112068' id='rect816' style='opacity:1;vector-effect:none;fill:%23000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.77952766;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1' /%3E%3Crect inkscape:transform-center-y='-5.6628467' inkscape:transform-center-x='-9.1684184' transform='rotate(90)' y='25.887299' x='1.8655396' height='3.5055718' width='22.112068' id='rect816-3' style='opacity:1;vector-effect:none;fill:%23000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.77952766;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1' /%3E%3C/g%3E%3C/svg%3E");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:#59bbeb;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease}#widget-mm-wrapper .widget-mm-header #widget-mm-collapse-btn:hover{background-color:#055e89}#widget-mm-wrapper #widget-main-body{max-height:410px}#widget-mm-wrapper #widget-main-content{background:rgba(0,0,0,0);position:relative;z-index:1;overflow:auto;padding:20px;overflow-y:auto;scroll-behavior:smooth}#widget-mm-wrapper #widget-main-content .content-bubble{font-size:14px;margin:10px 0;flex-wrap:wrap}#widget-mm-wrapper #widget-main-content .content-bubble .loading{display:inline-block;width:30px;height:30px;background-image:url("data:image/gif;base64,R0lGODlhyADIAPcAACkmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAwAwACwAAAAAyADIAAAI/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/7qYoOFBw4WLHAQYcKGFThfjAChAQMFChg4eBjRAqcKBQcIDAgQYEABAwpIxHSRIXeC7+DD/id4kGHmCxHGJahfz16CBhEzVSCYDqC+/fsACCBoyX2B+P//WfDSeRS0Z6CBILwkXwD4NdjgASqVwACAFIq3QAgspVDBgRy2R8EJLF0ggIMk4hcABCdt4F+FLIJXXkojFNjhjOvBl5ICDJaoo337jeTCBC0GCd4DJ73gAY1IrqfBSSoYsOOT9hEwEpBCVklkSUcmqeWSJTkJ5ZdSgrRBlWQmsAFJI2ippgQjkKTAl3ACoMBHIaxYppAYhnSCjGsmCWJIEOQYJ5QocuSCA3eSuUBIL2DQp5oUhKTCAIPCGUBHGSRa5oseifDomjZ6hEClcfaYkQt2ahrkAsB19AKf/p8iSUFzHakgKKlPBqBdRpmqSianG3kaq5qhbjQqrnCaetEDvirqkQbDQuoRAchamtEKzZbZqkYtRLsmrRqRUG2cu1o0ZrZVnrlRmt5q2eZGb4775ZwXUYlukBFwlGW7SHLAkZfyPlkARhHcK+SVGnHAb5JcalRAwFCGaRGiBre46EaOLkxjpBtRCvGOl140YcUsXqzRhhrPyLFGI36sY8gWpUoygBzBmvKBHN3qsoMYUTwzgAxwlPHNB1bAkcc7OygARsz+DKADHEFL9IEYcERt0g4OgJEFTgM4AUcgTH2gBxwdgLWDBmD0Qdf/AYuRCWIbWCxGDZzdoLIUucC2/ngocPRC3O2xwJEKduPXQUYj7w11RygDXnVHLReudUZc752A2xmFDbgEc2dkduEA4F2R3nuz6tHfgM/qEeGF67pRr11jrpGwYneu0bFni26RC4nPvIALIL3Q+M0UvACSCpHvHIAKHZXQ9QcipSC2CSJdcHYDH1VOsoAjaZ5ygiN97jKEIBVc8QTAk6Swxh4YT9LDHxvAfEjmo/tA+iWt364G7pcEv7wEmJ9IYOcrC+DPJLQbFgj6ZxLcIesAAhxJCWSmKOitJAU2gxT1VnIBnVkKeyk5l6IycMCVsAtSImDgSuJlKQREECXc6R2FHJCBbQ1IBMPjEAZEAC4FISB5nCQaAALK5RIUbCACD5jQAhjAmwz07SYsGAEHNLAhClQgOSIQ3E06oIACEGBEARCAdRBwuNmY8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJyla60ikBAQAh+QQJBABEACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFpd5lrepxve5tvfaBxgKN0hKZ1c5l3h6qBf6ONjKyOoseSm7KTo8KUqc6YpsOZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCJCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy5Yr5IaKER04XMgAAoSKGziH8JjRIkUKEiZWtIjBw+6NExcoSJ9OXfoIGDN5vCAhorv3791b/uCIuwNE9fPnOfh4yaMF+PfvU/xoKwQ6+vvVR7AcEoM7/P/fvbCWEObhZ+B0HOyQ0hDuAeigd/KhtUMGB1Yo3QXAmfSDCQ922B0JzX3Ugws2DOVDdBamqCBJP/jnoYfzadSDBQMEAMCNNxpgQYk8CcFBikBesJ5IQ6Tw4pEkBIFRDw/YiOOTOD7AY04FApkiByM1eOSLJlz0gZNQhnmjBTnJYOWZKoSUw5ZszlARBGLGiSMEN/l4ppVCflQkm1smOZEFcgYKwAM2sXDnmfp5NAOfbAoY0QaCCrpBTRQeauWQHHHI6JZKPtQDmJGKGcCUMN1g6ZksdMTDpmzSABGc/qEG2gBMOoSgQQWnWglCR4uyemQLD9kAaqxikqpSCQkUUEAEuQbZkaa+ekjCQ4ASGyiZLIWgbAEINGvlihkFEe2WMTJkgLWBDsCSDskqq4C3QMqw0Q/jHpmDQwKgG2i22xawALwppqrRqvV66CpDNugbaA8rYdCvAwBbmKZGOBTsoZsMuaCwnC6spMHDEVd4wkZrWvxgDA0lvLGYKKyk7bYMhHwgdhqVbDKA4zHUw8rFrgREv93KjJ+8Gol7M4D3NjQszy29XMABQuOXYUZDHA1giAwRwPOT6ja9LbNRV3eBEJla/R4JQzgE6dY30ulSDR5IMEHY1e3KUQxmgwes/kM7sw1AxzGZSfd0ApOc93cHO9QA2wbQhOLgmG7kYt6d8r11AIDLpMLgFCTK6+EiOArRBzxjO5MQlUZ9AbgcDQHt0X5KtLjChNoEA90Tf1Sx2RhP9IC+DzB8049CZ0B2SCtYnULaFVUbagCm15k6wKuP5PrNJJRbkQ2zB0pA5jntIDPRIxldMAlJZ+SCBVpDSQAE4O9kKrwX0FxSi/WSkLNHNqBAovBB2QHxTnUB8pkkCEbylQmwdhYhjOBUHYgcSl7AqhVUTi0CPBMHDLiSHyRvSytIn1tkUCX8XKADLDgeTHKgJQCRYAU0YN5cZHCCDlTqAhzgAAhgoMKa5CAGdSs4jghIkIIWvAAHMpyNEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJPkSEAAh+QQJAwBAACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFpd5lve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNeyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCBCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy95740QHDBckSLigocMJHzh/wGhRYgQIECVQqKBh1weJCxCiS58eXQOJmT9ejPDAobv37xxQ/jCH6yOEBOroqUtY8TI7d/Dwv4MYz3YFhfT4qV+YwRIHiPgAgjfCDmqVl9+B1J2g0gsBNgheDWj5oAGCFEoXwkk/qODght69YJaEFYYIQQcmocDhiRy0UFYIIorI3kgwoIgihGKd0GKL/IVEg4wyEihRCg4wQIABCTiQglE+nHdjiBSE9MN7PHIIQkQiDADAlVheKYAFRJGwZIsvesRglCjSt1AOBmSpJpYDyHATDzwgpIOSX1YowUc9QEnmhh40lIIAawYKQABHysRCAwsUUIACD3xQkJd1iqhgRy3sKaOZB8kAqKCBBuDmSzxUoOioo04Qp0D3RRqiBh79Z+mJ/igslCangg4AaqKk5opAnDeo2iJwG+3wKooe/JBQBrTSymVLoubq7ANAQOprhWFmNOawHGJKkJXJChpASyY4K24BJkw4bYXXbWQithx6eFAK3dL6qUoPjPtsquciyOpGrrLrYKwHRRAvpw6wpIC9uuKbb34YcNSvvwGWgFACAwtqwEo8IOzswhTeuRHEfCI0a8VqEoCxxqQewDGCTWr0A8gOTnkQASSvaetKB6NcAAJ0roxewxvpCTN4Eh/EQM1qJsBSvTo3AJ3P6ZG40QhDA6gCQkcjjSUDLIWr8wYYQJ3ehRuVUHV8Kh6Ug9ZYZtAS0whDy6LY1LnAUaVnP5jQ/qZIf9sSDzmPi4ANQPRK93Q6cIRD3uD1kJAFbEcA6gTjNkA4qodX1yrj3RWdELckD5BDTB80MCoCDWxQ0NyHp8uRhpy7m1AOAZBMaE02xICQD5lLACxHPXBeLEMp1B5vAG4D1UHrIMF+tuwLyQA6p20KxbvYFPze0ctng2DsQxHwrWYAEYw+1ApQS2C3jmfTGFEGDhhAwAAGMJCC+UUtv/KkIq0LMvRhCdvCyEYSqkEsbWTxgQCnFQLtieQHBsQWCr5nFtbVSQL8O4nzLAVAs9jogutbkKU8oC206E9EGHRgSjLEIw/AgIJtuUEIFIafC5BAhSzZwQYDNIIXOI4uhzMgAQYwcB8JUIA3JLgBTmrwghL0ywMjQMELfDSbhsCwiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk2MJCAAh+QQJAwBAACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCBCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjyw7s40SICxYiRLCgocMJHTh/wGhRYoQHDyNQqIDRo64PErkfSJ9O/cEFEjN/vDDOobv37xxK/ryI+zxC9fPnQ7zU7gG8e/ct3M6YgL5+9QguWOIA8b4/eA81rHWCefYVOB12KcHQnn8MejfeWT50YOCE011w0g8qNKihdyWcJSGFIFpYUoYbltghWSeAqOIDJ5AEQ4kwcgDDWC4QuCKF+YVUw4IxbhggWD5YcKOKEYT0wwg9wuhBUyxQoMABBygggQkMkTDkigh69EKSMT6okAgLDBBAAAMgkIFNMThQwJpsrqmADQn5YOOVE0YAXEc/8Milhh40h1AKBgAg6KCCDiDDTBsc0Oaia26AkJV0qpjlRlvuCaOXBUEQAKGcCgpBTBswKmoBjhp0QaREelSCpUoeBEGn/rAC8KlLNig6KqMxFKQDqiveqVEPrMbo50AybBprpym4xMCtoiZQUIq8gtjiRi8GW+KMBBFwLKwCtGQCs6N+QNCH0U6oAUckWqshCgRlsG2sFbAkAbiiOkCQBuVSKKJGKKi74YkCIfAurAawpAC9jB5AkJD5GljkRkj62+CSAw0wcKcBsJQAwosqPBB9DRf4sEb8ScwgxQIJcDGnGa9kK8dsEjRnyOhxpKfJ7xFk7MqDGgwzm84OxDDN6E3AUcQ4vwcCQRbzLGi3K837cwH2DnQq0ehZwNGqSb83AkECOw1AwSt9OzWVA4WANXodcNRC1++p0K7YAJzpM8wK8EDQ/gprnzcpRjTA7R6mQDS98gA5tGQDzAewUJAPfVd3A0c/CA7eDgXJwHMAIrwUKsIYHARy5Fp3VLLlXxv06sUNgAruARvobZDakT/wd0ZvW84B4QOtfmwAECQeUwwHi6pArghBHrmdHlVueZ8JpWA4pwMkW9MHEmx8QAIMfCB7QpCufbtGlcLNu0EVIKByAAIQUIHwSfkwOs0R+ADSD6fj7MEPYc2w9goiwQHcaDAW2oVMPSPJncniQxZ8NawD9iNJvySmAv6VxYHRukAESzJBa5XAgmYJX6RCsEGTlM9SLQDhWWYwMyIBcCU4uJmSCNgWaBGJBCVcSbWU9AIVsuU5mPOrjwVI4Kv1vCB//RnBC4YllxucQAMXoE8EJsAbEkzuJjuAAQpKwB8PgCA5L8DcbMZIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlcJl4AAACH5BAkEADwALAAAAADIAMgAhykmZDUzbUI/dVtZiGZzlW97m3VzmXeDoICLp42MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AHkIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLNqzjRIcJEho0eGAhAwgbOHfASBHigwYNHEiYWJGjrg4QuRdIn059wYTfMnesMI6hu/fvGEL+MIerY8SD6ujRd3i5gwUH8PDhp3D7QkL6+9UbqGBJ40P8/+BpEMNaIzSA34HUgaASCxoA6OB3K6ClQwcIVjjdBCftkMKDHHoXwlkUWigihiVt2OGJH5I1gogsLrDeSCycKCMG8yXVgggbbICCDBO9YGCLIu4XEg0NznjigEXhQAECBDTpJAIQQKSDfUCK2IAOIO3gn5EnarADURsc4OSYY17g0IpVsviiRzFyKWONQOEAAZl0OhnlQjqclyaLwHW0w3tuyticQTWUEAEDJdQ0Z52MKrAQmnuKmAGbgc5oQkE1GBAAAJxyaoALMV3A6KgEbKCQBZGy2IBHJFQqowb+BHkgQKe0AhBABC/hUACpjBbA40E6/JiqhX1qtEORrnY4aAW1NgtABS5RwOuoDiCkwrAsjsBRDMnKyAIPNWzqLK0BgMqSmNPWWQBCGWArogUcmdDtiSTwYMC4zQ7AUgvpjvprQai6W+EDHLU6L4cc8CAuvrTWsBIK/TJqqkETCFzhqhuFcDCHGpTAcLOJqiRqxHSaaZCeFh/IEaAbOxjBx7UysNIGJNNJwUHCpnwfR8i2/N/LMHeawEoi1EzmxAVRqXN6BG+0pc/xcVBD0J1CqxIORo+JwkEBL42eBAVD/d8HPFDNqbkqoWv0ugeF6HV1JGpkotjgfXhv0PqytKj+0TcfdMLb6nEEA93ygbswvgGEvBLWa/9bkA6AV/cCRzsQDh4NAjH7scwu0Vxz3wgpDXjTHD1NeMIDMYA45y8pQHK1CkEK+JobtWk5nAJlWmsAA6ANk+vpKoADnih73cAMHv1puQY3IFQBAwmU4HBNI49awAXDM/T37CANTjjuQcm5K5kFOOA4Q8WnfGVILPvsJVIobEDBBRe0kD1EL7yt7ZB0fyuW7AKjHUhstzHwgaVdFpsAlkgir42F4EtlQSC2FHiSBnbrgWgBAbY6sMCTrKBbKYAgWl6QPlXtTyU0aN+MNOA/tgDwYiDoIEsI2CENrECEboHOwDpQLJhsB2GKKRjUXGwwggxM4Dy7kYAFQIC8m+SABSYIwXuQ8wESrKB5s8miFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAzlWgICACH5BAkDAEcALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfkxPh05Nf1VakFtZiF5kmGZuoWZzlWhmkW95qm97m3eDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6W31au816yyxLKyxrLA2rLH7LS6ybTJ7bXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AI8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLVlxkBwsQHDhg0NABRIodOI38uAFjxQoTKFrAqPHD7o4SGCxIn05deokZM3/IMEGiu/fv3WX+8Ii7A0T18+c7BHn5Awb49+9bEGlbJEV09Pipl2BppAZ3+AB+J8NaQXSQ34HUcbAeSkS0EOCD360w31k73IfghRgAZ9IP/0HooQnNleWDhReW6ENJQnTo4YpCFEWDBxtMUEEGHtBwUREclKijdBgUMZIRK6woZHcmGBHUEBtAwMCSTDK5AUVFmLfjjhyIZIR7Qw65AlA0KNnkl0s+4IJEL0xpJnYg4ZDlmuP1JMIDYMa5pAcQFaGBmVP2+JERKKyZZZE8iSDnoAyI8BALeJqZwkc3+LlmDTudACehco7JUBEkJoqhjxwZoaKjHxo5EBBAzDSEl5TG+UBDNmh6Zkf+PYDK5hExJHAAAAAQ0EAMMHmQKqF0LpSCq1Pux1ENsmYpwwUC4OosrhS8hOqvYK660J3E6ohBR30mK+QIz4YLwAUt5UAtoTYmFES2U56oERHeZlmAuM/yupKg58oZLEI+sLujhhkJEe+QBtDrbAMsZZCvnBUoNIO/Or6wEQ8DC7mAwbgKwFIFC8c5gUJlQnyhxBqpWbGHF2MMgA4rTdAxmBGALPKFLGxk8skPOqDyyitt8PKXGSjU6swHoplRrDg/qIDKGq/k689OKrQD0QfasNEPST+IgMoJsEQD1ExailARVOe3YEZGZB3gACqr0NK0HT8wxLVln6fnRt2q/V3+CAFgLIFLT/+8b0Il1F0dCB3JoDd4MFBArwAUlNrSqT/LzdDQhktnNUdIL95dD7Qm4KwACXwQ09cvn9AQ2ZlbgMHZGqXtOQkmTCiQDvbO5PPCTzqEaOaLetSo55DyFPivG8ztkJ2G390Rn4sD2hPqlD6gekQP110zSBTrfUNQ+KrqgfIS5Ug1B5yCFGTWK4galAseZDBBBBVsQAP5EzE/8+s/5l1x7WTpl8gwsLmRCOxkJgBdWabmLwKeBGsDSyBafGA+V3HAXScRwvpktYIWpaUIhdNUB9KHEiMoDlQtcJ9aKGimDgCMJRpcUwtC9BYbSOlAGOgAyWLSAyw9yASKLcCBXWyQgg7oxnUcAEEJbEBCmvSgBi04Du1WAAMZ9ECFs8miFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAzlWAICACH5BAkDAEQALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfkxPh05Nf2ZzlWhmkW97m3eDoHuEsICLp4F/o4GNuIiTrYmYxY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6O34KW31au816yyxK3B6LLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIkIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLdixkhgkOGzBYwPBBRIodOIfkkKECRYkRJVq8oPGjbm0OFihIn06dggUTPmQKVzEihPfv4EP+jJARJO6NDdXTp8f+kgeK8PDhk2+7A736+9VTsPzxPr7/8DSsdUN0+BU4HQcq8dDdfwx+p0JaLBBo4IQbZGdSDQs2qCEK5QEFxAoZQNDAAhNcQAIQG7Ew4YrTYSBESTVoKON3JQzhUwwTKIDAjjzyOIEOGO0gIYsTIjjSDxnOqOGDOwGRgY49RsnjBRb5MCSRE4ogUhBJKqnhCzrZ0ICUZPLYgA0UiYDlmsCB9IKXcDZ3kw5QllmmAmhGdMOaa24AEg9wwonCTUCMaeehC6AIEQd8rjnDRyoECmcONl1w6KUITADRDo326dEPkgpaUwyYYppnQyl0uqaFG9EQKpz+QfQw0wSlXtrAQxioiiULHZXwqpcMCABAAg68BEStmALJEKe6EvkBR6D+qmQEAFQLwAEutLQCspeC0BAMzRJpAUc4SKukB9ZWS0BLlnJrp6YMqRguixzFaO6M6VZb7Eq0ulsmBA2pOe+KL2r05r0yBpCvACwt4O+dDX0w8IptZtQCwjIWkC8ALBn6cJQLBDzxhDdsdDDGDBqwsawqQfCxlLcyZMLIBrKKkQwoNzhAvgewlMHLUVLJUKo048eRqzn/t7EELJEAdI8dNDRD0fdhwFEOSfunQb4EsKzSsU/vqOxCQlCt3rMbDZF1fNRae8AJLnn8MsAO5Wo2dfpx5Ov+2uA9AIAAB1TgNUs2hB3DQzPfPZ3NGeHM93flDf5Svx/D69CeilPgZ0eAPh7CoDUV+nGiEdl3Nwwf9cc3DnM+rMDhETFrtpGfPs7kTTY4jGwDsEsksdklg3Tx2jzoBES7mEKg6ERWUm3Cll0iLENPNlBOJgSnVoT5yBsUHFLnKKNgo09AgHABBAss0MAEHWR/EbgTu1hSuRjXaBbR4VZ4EtLmcojW9qrigPdMAr5XqWB8aNmB3fhkgRQM8CQ/2FugRkADBKqFBQtkEQcqxpIaSHBGKpDTW1jAKANZ4AMcfEkNItWgEbRAhHNhgQg4kCsLWGADHzDBDh44kxq8QAV0vhrBCFDQAhn8wIKzSaISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjORXAgIAIfkECQQAQQAsAAAAAMgAyACHKSZkNTNtTE+HTk1/VVqQW1mIXmSYZm6hZnOVaGaRb3mqb3ubd4OgeIS0gIuniJOtkpuylKnOmZm0mq3Qm6O4nZ+7n7LSo6u/pbfVpqjAq7zXrLLEsrLGssDatLrJtczxt8Xcuc7xvMLQvNDxvcnfwNTyw83hxNbyxcrWyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2Nnh2t/p2uPy3uby4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AgwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8uO/IOGCRAaMEywoKEDCRo4geSAseJEiRAjTqhwkcMuDRITIkifTl16BxYzc7gI8aG79+/dVf7UiIsDRPXz5zEAd8ljBfj370s0Z/sDOvr71Tv8WAlkO/z/4KkAhFo/dIDfgdRNgENKQKgA4IPfhcDDTz2IcMEDDjwAAQUe9PARDhYgKOJ0L5zEwwgQpuidDTy14MACCMQoo4wQiMDRDtGNqON6I/nAnYpAzneTDBDMaKSRDtyQ0Q8Y6OikgiMBUQKQVEp4UwswHqmljChgZKCTTlowkoNUUjmCTR5kueWaG1j0AphwmhCSDWXWCQNNIqypJ5cUMQknmBPs8JGUdZYZgg8yyaDmnmvKMFEKf8IJwkcxFFrnCjI9wCijDkwUYqRgCtoRipaWiehLKGy6aZcQ4QAqnP4pdMRDqXXGAFORqu65QEQmvApmBx3BQGuZKrzUQ66bKvnQp77quN9GpA4LZAYueYAsox5A1CyYPGYkbZkCFNASBdfuCcFDrm6rI3YazfotkAYAwAFLuJa75QMP0aCujrFqlMO7QB4AQAAsOWDvmgw8xMK+I8qpUQ0Aq6gAAADosJLBB2vZqUNvMowgCRvRGTGEDVA8w0qaZnwkvg7p6/GBDmf078gPTjwASxeofCQFD+3w8oHsZuQDzQ/GKwFLeeo8Y5sO/fAzfiVqBATRABKQQEvHKi2jsg4x+zR1omoULdXfVWBxSwxojcDGD5HwdX4duUB2gDClqrWNrb5NXf7M7c793Z0wYawyyxF5/XXY0Prd3akv3aDzAi1MtLDewHoEsd/FymT3wdlSZLjHUH40Ns1WzpSzvRdYNPnTIIN0OdUu2LS5qgt0bpEGT1vwLEgnUD3CgDbJIPieD0SOkZ+gLygSoSOXjpMIwx/pQIcb+exx1D3SzCJPPXhAwQMMMOAABBs46hEOOTY7QdAk8fCjtCGMdxbyr06AvUnM0xrC9mmZB6oGiEOJe0p1AsappTxw0sD9VtKeOp2Af255gf8ONAENpGB3L7HBAB8UghPEAHhzeQEJchOdCWCgAyBgAQZpYgMXGIc7ISiBClZQAxDO5oY4zKEOd8jDHvrwh1tADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPXQkIACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLrkziQgQGCw4kYOCgAoicKUJwwGBBAgUMGz6YsEuiQgID0KNLh+6gw8wUHyhA2M69+/YNJeL+oqgwvXz5BdZdrvjgvX17C+HZuqhwwLz96Q5crHzxQYL7/95t8EJPLHjwgAIFFIBAAxmQ5IID90Uo3QEopPTCBgBm2J0EK+jkQQEBACDiiCMWMAFIKDwn4YrQ/WbSCtppKON2y9nkAQEk5pjjACN0pEJ9LAZJQkkt+DfjkSnU9ECIOjY5YgMbucBAkFRSONILGBypJYcysYCAk2COSIBGEVBpZgL6hcSBlmxSMCBMX4Yp55gXgWDmnRGEZAKbfHIA0wNyBgrAAxctcOedKoBkAZ98tuCSB0wKGmaPFGlw6J0OfCQCo3xu4BKOkso5QEUqXkploh3FyKmWjq6UQaj+gjYoEQqm3qlBRyusyqcILBUAa6AFTHRBrWYy0FEIurKJwUosRPormCxINCWxp3KUZbKsqvTqs3KeGBG1Zg65EbZsJpkSoNyGCSVEKoBL5a0atUCulrymFGe6TgYLEa3usnjBRrnOO2MIKvmKb74RkdAvi3lqlILAM/qZEqgH60jnQ3YuLGEFG+0JsYYfqKRAxU0iEBG/Gt/HsUYBfwxgyCk1QLKOCkTkQsoRppfRCy5nGB9K2848oqwP3YyzfS7u3DOANaLEgtAkniBRqUdLh6pGqi7dXaspGSz0xRCRV7V0C3TEntbdWcBS0DMTKpHRY0P3L0c8o80dwSwN8HX+tBNRXfXVG2WtNdcqjSC0BxV1ELcBmXpUgt0QeOrSyBXXbJHfGlv5keAfc/mS19wWwHdFGR+98kceLw0zTKDDSsDoFk2bMpoiXeuymzNRDqsCsFvkgqGZixvSC4t2bu5MHgggKQFEa/SjxkmLVOTHTdc0AcU6BkDAA71rhAKQ1B4Ab0krGImtBPXq9EADCBAwQAEKTNB9R7+HH31JxJ9f/VllmsoA4ChZ06owQLi0oKB/VELPS1YgQC3BJy4kQOB9DsAADaQJJiloIIAkgAERvGkuIKgAAxhQnwMswAHVuWBNTPABDGDAPxKwwAbA88HZ2PCGOMyhDnfIwx768IdaQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvpRKwEBACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLvuwCRAUGCxIcSOAgwgUUOF+Y+IDBAgUJFDZwCLGirosOCw4YmE69uoEDFVTIfFHCggQI4MP+i4cg4UOLuCQWWF+/PvvLFBbGy5dvvieLERkeNJjggQVKFOqxJ6B1GrC0QnzzJTieCDmd0MAAAEQoYYQENHACSSRIN+CG1DmgUgrfKShieBvYxIICAUyo4oQKXAiSBhpyKOMCLpwkQogj5mjBCzM9kOKKQEr4wEcayGgkdQmYJEKOTIZHQUwsIBDklBIW0FGGR2bJAEkgNuklBi+xUACVZAIwwEYqxJilkReI1AKOXjIZgksKlFkmARpFsOae2oHEQZyAnrfSA3baOeRFJOy5p4cfpQAooCWqdMKPhZLpYkUMKLonCR9h8CigKahUZ6VlWlkRCJou6pEJn0Ka0qT+pNp5qUQXpLpnjRyF0CqgPJ7UQKx2NlBRArau2UFHFOwaZwkoEQBsmQFQhEKxazKq0QrKxhlpSSM8K+tEHVCb5QEclZCtlxKc5IG3ZXowUZHiHsnRkuc2edIE7JJ5aER6xmskrhn9WS+TvZJEaL5TCiuRA/4aCZxGGwzMZHMl/YpwkApMlGnDHD6ckacSj0gxSRlcHOQEE1XAMYd9ZvRByCMKStK6Jq+YwUS1rjxgrjCLeBILNa84Arg6C5jkRub2PN+TJwkQtIRnTqRC0exZm1ELSs+3bUmjPp0xRcRSXd2xHCWbtXjMnnTC0xG6S5HKYlPXskYvnx2ezCZJGbT+qRQlGrcBVmvkqN0QbG0S0EEPbVGAYnPqEYJnhypqzV9b5DfVgW80eNaGozQmwgj4hxHDVDv+UcRZS84ShOwOIDpGUxddgZtZfwDT58AS8HpGlzfMAMAgbS4xBgW7ZHGlASiwu0bw+psA8CHROzAFxb80gt53Kv5RzuIuMPdIutZrAd4yjdCA00AKoID2IfWe6u8pCd8q8TrhN0EDCjzgAfskoRC2ogfQAPRMsgKzPUoCIqgeWjTwvyMx4HsqEYEBm4QB8rFFAxvb0AEi4DGYiABkIpIAB0YmFxdoIAIMINYBDrAAB/xmgDF5gQg4gIFkSUACFtgAcxQ4mx768IdjQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCUiUgACH5BAkEADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLzuyCxAUHCxYcSMDAQQUSOF+kCLHBggUJFDBs+JDCLokIBwxIn05duoMOM1NwkAChu/fv3Tf+lPjJ4sSJlyQcVF+/foGKlyk2gJ8/30KLnB4aDAgAoH9/Agp4gJILFUTH3oHURcDSCx9wR9+D33FQEwsTDODfhRgKMEFJKjCA4IfULYBCSi1gAOGJ31mwgkweWIjhixcOsGFIJCQA4o3SHQCcSSlQgOKP3UnQ3EsNwGgkhg2AhIKBOOI4IkkrOAgkkCuyxAICR2bpHwEeubBAk2Ae8J5IL1gw5ZkS3LdSAVq2CQCXG7mgHphgLkCmfGeeacFKD7jppgIbaUDnoBeEJEKeiIaQUgZ++plBRi7YOGiYY3b0go+IoqlmSSy42KiWAWR0waSDKuhRCJkiKqFJfX7q5oz+FUVK6qAuWIppqme+YJIArroZqkUgzDoodhyZgCui45E0Qq9+CljRqMKC6UBHqB575gYlFclsmwhYJGm0OB7Q0a3W/ihBSZ5ue+SvE6kALp1PZtRCuXlWKRKv6mpZEQrvgrljRivQe+aQIuXbJgsUkdBvkxpslILAU4ow0rIGZzkCRYIufGOhGh0K8Y+KinRCxRZT1IHGN1awUQkf//jBSCyQfOR5EwWL8ofEZmRsyycmKxJ/Mr+4780fgrBRwDxDaAJJ6QYNwACxEo1gvBi9kDSE9oakgNMXAlrRt1JPd0CtG5F7dZC6Tsy1f49WVEHY1U3L0Qdng4dtSfg6HQD+whXZDLd0Rhdb93dLlzTB2kla5AKTYYtpqZRnp3lS3iQLwLdF0MKt8qmDQ/DySYwG/QCkYN/suEeX1i05SliS7HVGJ4fN8Ucsnx1ySgRUTMDlGX1J9AJkg2Rm0haknRILuau7e0eyonwA1R+l3rIEWau0NbMK8L4RvxofEPhISEMsQeEuTUB5mwKMTuPCB+RM0sMCS+AzTA8AnWUAD2j/EQq+C5vAvyZZwfCORQGCzSQD+sFQAAaggBHoLyQuiICwGFAplLyAA8fCwKZwwoIRONAl/BsUAwC4EgEiCgMGbAsI5vShAzBAA8F7iQnwdCIJYEAExpMLCCrAAN0Y4ACAC3BABDoQQ5qY4AMYOA4EJGCBDXCgBDmcjRSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCapEhAAIfkECQMAMAAsAAAAAMgAyACHZnOVb3ubd4OggIuniJOtkpuylKnOmq3Qm6O4n7LSo6u/pbfVq7zXrLLEssDatLrJtczxt8Xcuc7xvMLQvNDxvcnfwNTyw83hxNbyxcrWyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2t/p2uPy3uby4OLo4OTs4ujy5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8vmTOJCBAYLDiRg4KACiJwpQnDAYEECBQwbPpiwS6JCAgPQo0uH7qDDzBQfKEDYzr379g0ldP6OeKAAAYECCBo0ONESRYXp8OEvsO5yxQfv+PFbCE+TRYMBAAQo4IAEKMDeSS5UcEB8DE7ngAsrvfCBBPlV6N0GL8TEggIBDOjhhwqY5IIDDZYo3QEopPTCBha22J0EK7zkQYcf1uhhBiOh8JyJPEL3m0kraOfikNstx9IDNNqoZIANhKTCgj1GSUJJLVBI5JUprITAklwKSMBHLjAQ5ZgojvQCBlemCWNKD3TpJgAPeBTBmHQmAGFIHKSpJwUZmjRBkm8uieNGINBpaAQhmaDnohyYxAKggSoZwIEZLWCooSqAZMGii7ZQUgORvomARhpcaqgDH4nA6aIbkHQCpP6hKkmpRTuaOmamHQm5apqeigRqrG6OehEKthqqQUcr7LqoCCMBCGyXAWB0QbF0MtBRCMrqiYFIIzz75qwTiUntrRyhmS2vIbXpbZdxWjQunVNudK6eWYL067pLCkuRCu+OeaxGLcybJrMgFYAvlwVYRGy/PV6wUbICExlCSAYfrOQAFpHAcI+IapRCxEQ2ChIBFl9sUaEbm1jBRoqC7OIHIW1Zco1fVrRwyg2urBHELlsIM0gKzFyjvhO5gHOJ9GX0Qs8t8vfRvUILGGJFRh/N4I9KM22hkR+xELWHHlxUq9XS4aqRrlp31ytIAnwdYLQXvUe2dAt0dF/a3VkwUv7Qbk9tUdVzQ+cwR0vjzd3EInnt9ggZjU222RuhnfbaMX/t90UdBG4Aqh6VYDgErZKk+MyTbuR4ymV+JLnLa5aUgdDtaoSy1Tp/1LLWP5sk88GXayQuznaKZG7PfKa0u7cFsOCRC5aiHm9IL2zKer0p8f2sAsp/9GTKWItUpctcq/R6rAHEDhIKUI57wL8lrWDluRIQ3BILx3MZQAHggsS8+t2XFD384XPJCDi0pAAggHEomZOtGAA5lORpVxignEw80ADzDGAA6GnACLKnEhQocEzzeckKHpim/cSFBB9s0AEYoIE7wSQFJLSQBDAggj7NBQQVYAADFnSABTigOnkurIkJPoABDFBIAhbYAHhsOJsmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKTiUgACH5BAkDAC8ALAAAAADIAMgAh2ZzlW97m3eDoIiTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AF8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL7txihIUFChAYQNCggYUROF2gAHGhwoQIEzRoAIGi5woRGDqYoDkCgoEC2LNrx04hxUwUGyL+PBhPvvx4DyxsrsBAIACA9/ABEJCwouUIBdvz54fg3SWKCuYFGOAG6cUkQgLuxacgfARMh1ILDegnYX4UsOSCBgJmGKAHL62A4IIgxpdAfSWdgMCEKGqnQAspqTCBhjCWV4ELLIkgQIg4wheACCR9cF2KQBZgwAknlSBejEg+EIEKKnWQYI5QYiDSCD8GCaQB/Y2EwpFJIhlBgSZJ8CSUUYJ0QpVWAokASSpw2SWSE5wkwphkQsljRy3gl2aaC4jkAoBvvnlBSSvcWOehATi4kQV7NvpBSCAEKmkJJDFw6KUAHMBRCmg2eiVILLgpqZcjmUAnpjkqihEFnjbKwUf+HowqKQkiHYDqpQNoxGmrexrgUaiyBhpBSCvciqmqFWXAa6NZahRCsJKC2REGxl7qQEYRLpumBR1hCO2bIIBEQLWHEoBRC51qi6KvG7kg6rcwDvuRoeSSidEI6u7JokYowBsojR2ZUO+hJFbEQb5pEqkRCf6+yWRHIgxc553JImwlcM423GVzHXUgMZlSWsSoxUC+qlGkGiNJ67Qfl2kRqySn+KhGsaYcI6UBt5xjBxeNHPOEM2eEss0a4sxRsTqHiKxEH/yMYrMXlUA0jNJudGrSANzr9IRQW9Tv1BlWrdG4WMdn7kUtbK2fAhy5ALaAFYAkQdnxMZDRAmpvVyH+Rxe8bR6HHyFNNwBLT3Rw3tlhvBHDfpPH8Udkl332uYhjx27bjY8nL0gC082zRhBUzq1HG2Qebq2Sc5R23gjs25Hbfk8AMLFXk7tjR6GrbfJHpb+98kgRtxxAyKqf6HQDfr44tQYnUfvxtR+l4PSKI7Ew9YwoOTBwANCDhC/JQ5b0tcZLqjRntcP3aLEBQZMkdcMRGJ3SCraiOkDhH52prgJdj9QmvBUQW0pEUD8yDYBiJsnTshrgOpT8CVoamN1LTOCAAYAoAANgAAJT8gHj8UlhLSmB8gT1MJuYAAMYkEAHRFCwl2QAbykyQAMUB5MQ9C1GEdDA4+byAQoswHiABlBAAygwggbSpAQeuIDyIlABDXgABRKcjRSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCahEhAAIfkECQQALgAsAAAAAMgAyACHZnOVb3ubd4OggIuniJOtkpuylKnOmq3Qm6O4n7LSo6u/pbfVq7zXrLLEssDatLrJtczxt8Xcuc7xvNDxvcnfwNTyw83hxNbyyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2t/p2uPy3uby4OLo4OTs4ujy5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AXQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8v+bAJDBAYLDhxY4CACBhY4U4DQcKGCBAkVMmgA0SJniAcNFBAogKDBgxAyWVhYYKC79+/dHf5YmNniQwUI6NOrR5/hA00ODQYAmE+/fgAFJVqyoHAAvH/wB2zwUgsdSLDegetJMAJMJSgQQH0QRohAfiltkMB/GIK3gAgsjTABgiCuV8EJLTkY4YkRKoASBRm2CB4GKnUQ4ozrgaCSCgWgqCOEA6hAEgsOuCikdxSc1EIGNCaZXgcolSDfjlDOJwB2IgU55JURmISkklxqYJIKT0YZZQA+gsTilWgKOJKMXLa5IEk5iiknASBhgOadBnAYEght9gkBiSI1IOegAKjYEQv94XllAiG1YKCfXE4gUgkPEionhRtZoOidanr0AaR9vvkRApYOWgBHKCS66ZAHfLTCo/6gKikBSCGUSiiVGWm6KpowdvRprG3a6JGgtsqJwEYX7nqlAx59CCyXGXwkQLFyBqCRCcreCdxGKTzbZ3Mc1UrtpblmmyZHv3qrpKgaPTCunA9kZKW5Qha50ZbqJskkR8S+C6WhF3FHr5DMbnRevklGyxGp/kJ5KkbJDtwiAxw5i/CMF3QUZ8M60omRxEK2utHFSc7KEQEc7zgARiyA7CKjGrVAMo2ScqRAyjoei5GqLvu3AEewznxgBR3djPOJAFskcM/+ZWmw0CB6yZG7R0fYQEYMMP2fvRpdADWC+26kQtURcpDRmVp/12lGbH6tHrsaTUv2fNZmhELa4KHA0f4Kbq+3gkdGz530RRGnTXFHFrudsUclzD2f2RqhnfZ4HbXttnsfbXz0wxq1jPcB23Ikc98SgOvR2GRDvlEEeFPukQZ9Yw5S4CkPnpHnTCcQekejQz2B6SChnHKPH23A9AEeiDQC1BKQQJIKcvsrAKYese5yryLBPrOwJJUQPbUBqA5S1hJzPZLXF4ddkgrCF0sA9SCxQL65FOwuUgvoq9sB8CbRPuh9ZSqJ5DZ1AOydxHKgkgD3VBIChs0JVyex06qQxxI+xap5MAmBiXQUAARAUCUDDNlvXoLAkjHnPQ1oQAEIMIACKOA6AXQJCixQOAwtwAL2a8kKPpA4EFXgA4n8g4sILMAABlzoAAngjQVMgJMTfOACF/iQBCaQnA+kYDZYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgnEpAAAAh+QQJAwAwACwAAAAAyADIAIdmc5Vve5t3g6CAi6eIk62Sm7KUqc6ardCbo7ifstKjq7+lt9WrvNesssSywNq0usm1zPG3xdy5zvG8wtC80PG9yd/A1PLDzeHE1vLFytbI0uLI2PLM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHa3+na4/Le5vLg4ujg5Ozi6PLl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjyybL4oROFyAqMFiQ4EACBxEuoMD5wsQHDBYoSKCwgUOIFTZZeGgwQEAAANgFEFDgQaaLDgsO/hgYT768gQMVVMh8UcKCBAjw48uHIOFDi5gsFFzHzr8/fwW2tUTCAuYVWGB6L6VgwXwMMmhfS/nt59+E/SmwEgoEGqiheRqwtMKCDYY4nwgreSAhhShiF0B3J5Eg3oYwkueASim8J+KN8W2Q0gMnpujjAyZp8GKMRC7gwkki2Ijjkha8YFIDPkbpXwMkaUDkleQlYJIIS3YZHwUlPSDlmPxNIJKLWKbJAEk1eukmBiON0COZPrLokQpDpnnlBSK1oKSbXYYQEgtz0pliAAF2FIGejKoHEgeARnrfR1AaSicCHpHAKKMzfpRCpJHq6NEJhVqaYqIaMbApoyR8hAGo/pGm4JECphqK6UYgrMqpRybAGqpHAtRKZwAcXaAro0dyFIKvkTq5UQbCGmqmRgkcq2cHHVHALKAlcFRptGPeihEK1urZqUYrbAuoqBoNAC6ZxGbUQblpHsBRCeq6KQFHwb4rZbwYWUkvlhxxma+XHPlLJgsZLTrwlclmBOnBXTqL0QkKjzlCRg48fOVwGm1AcZfQZcRCxlKiWpGqHscIckavjoxjyRidjLKPKlNUQcsxOprRBzLjOGlGpd4MgEbG8ryhskHfyJG7Rk8ogEbzKm2glhvh23SDYG5Ea9T+FaCRClYbeG5GLWzdILsZQQt2fxlsVG3Z5WHLkbZqy9ct/kdFKxwAwxrtTDd5PmsEdN7xDa0RAm9jJ/ZGmg5uwNkafYo4BGxrZPPbdmqUId2tegRi3rJ6xDjY4kI+OOUbWa525httfjOiH3Vcdugfiax26R95EHXcH5FtdQV9qv3BSBPcbGFIkbfMQMQguT4yBhaH9LW/qYMk8MMJQB+SwRRTUL1I30ZLZUlJ07tA4SMte7AFipPku7ABTGtS88c+n5L0vlKvEgunG1YBckYSFMxtUwfQgPdMsgK8gUoCIhgfSkYQQB8FgAAba4kGDoglBrBPJSJwoJcwEL+WZAAB/eJPAARQgAwADiYaYBmMDhCBl8FEBDG7kQQ4QDObnGAEiy+0iQs0EAEGVOsAB1iAA4SzwJi8QAQcwIC2JCABC2zgORKcjRa3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpSiXEhAAIfkECQMAMAAsAAAAAMgAyACHZnOVb3ubd4OggIuniJOtkpuylKnOmq3Qm6O4n7LSo6u/pbfVq7zXrLLEssDatLrJtczxt8Xcuc7xvMLQvNDxvcnfwNTyw83hxNbyxcrWyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2t/p2uPy3uby4OLo4OTs4ujy5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sOy2JEhgcTPHg4gdMFiAoMFiQ4kMBBhAsocL4w8QGDBQoSKGzgEGIFTRYeGgwAwL079wAKPP7I9M3ggIHz6NMbOFBBhczlGCRAmE+/PgQJH1q8ZPFAgPf/AD7wEgkLqGegge29lIIF9jXYYH4sebAdgBR6N8AEK6FQ4IEcqncBSysw6OCI9oWQEgsKVKjifwWkRIJ5HcaIHgMqpSAfiTjSh8FJLBSw4o/dDcCCSRrAKOORC7hnkgg35uikBfqNxIJ/QFYZAG8jaXDklugl4EJJIjgpJn0UvCBSj1WmCcAAI6FgJJdH0jjSCk2O6eSOIaWoZpotgqTCm3AeGYFILdRpp5McgJTBnnti+FEEgUaaHEgcHGqpdR2xMCGjVn5EQqSRLgBSCpZaaoFHD3C6ZwMeMQBqpP4gfIRBqZaa0BGVqnbKEQqvhurRCrSaytGiuarpqEYX9BqpkhuFEKylUWbUQLFqIsBRAsoGqkFHFDx7qAgb4UrtjwFsxGu2cDrAEbDe2rmBRieMq+YIGnWALpwHcFRCu3ZKoNEI8qYpXkZa3sslR2HyO6ZGHgRc5bEXQWrwll9qVKnCYpqJUaoO/yhgRg5MvOWkGW2AsZiYXjRBxz+ympHEIsdIwkYXn4xjChkRy3KFEFtUQcwyMovRBzbnGK1FAO9cYQbIAh0jR84WTaJGLChdIZYYgeA0hwlwZILUI1IQrtX/BTBkRi5sfaC6G70AtoPvaqQn2dz1qRG2aqf3If5H3b5dn4ka6Uw30xv9nDd6QmdEtN/0HY2RuEqbzdGnhxsgakekMg7BqRytTLfLHG2Ydwcfieh3CR5B3rEAZ+96uJy/Mo5nR4Kz/LFHIas9M0gmv43zRz7ujEDrHf25dQWEGorxByJtGrCQIlEe8wIVh5S5zRZoDBILATgcAL0j2SuylyXte3KZJGkq7wBYj5SswUmeFDW/UJ4UfK4EEE+S9MoyUL1J13sWBrRnkv5wKgAP0F9JUIA3UB3gAv87yQr6VioJhICAKOGYlRLoEg00kEsMIBlLREDBMWEgZS3JAAJUFIACZECBLNGAq2R0AAeI8CUimFWOJLABFMbkBJQZmEADcOOBEcAwJhqIAAOwdYADLMABFUBBBGciAg5goFsSkIAFNvCBFWBwNmAMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhcSkAAACH5BAkEADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLBssiwwMFChA0aDDhBE4XHSowWHDgQAIHES6owPmixAcMFiRIoLCBQ4gWM0c0GACgu/fvBBr++I7p4gJxA+jTqzfAQLnMFyGiQ5hPvz4EDNddjkDwvb9/AAWMx5ILGiSw3oEHVvDSCyJQYN+DD36wEgsKBPDfhd8pwMJKJCyA4IfrHQACSylYAOGJ9klgAkojCIDhi94JMEJKGhwA4o3qXaCSCBKg6GN9IZjkgYUwFglABia5UAGOTKbHwEkvfPDjlPRhQFIGRBpZJJIkLdnkl0+WJCWVZFoZ0ghZagljADOKpMGXcBqg4EgikGknBBJ+xAJ3amoZwIYgkWBjnF+OGFIKPd5J5ooeKdBnnwiA5IKHhH55gAsgvWCiomRK8EJHJ6T5aJECcvRmpXDO6VGdnNqZ50b+/I2qZgEeuWAgqnAu19ELDrZqJ3YajSDro6VmdCquX0awqq93crBRA8P22UBHDiAL5wEebcCsnRJsREC0agbAkQuDWtukrhq9kOi2VAJ7kbDgqlmsRSCYC6cGHJnArp0iZDRBvGo+sFEE9n7pAEcc7EvmBhk9ALCW02pUbcFMJsCRtgpPSUFG0D5cZKQaMUAxk9huhEHGU3aLUawev0irRreOfCNHvaLsY0Yst3whARuVK/OHHK1r84kZOaozhgpsROnPCFq80aZDQ7gxRh0f/V/SEjP94QIXR32iBRllYPWFE2zkpdbrhanRmF7bZ+ZFLIz9X5sZdYB2ghyV0Hb+hBq5KLd3A4x793okcPTC3valoJHRfwOAtdKDo+c0R1DvPXVGJzTeHZcbHXu3qhuxivirGRXQOM8d2Tr4ASh4xCviEqzAUeZ/e/CR3Z+DpPfepGuUs84vfxTzz5eGVPPQnnrEgt86BzDvRiSgje+hbff7EbwtB8A5SJ5TDDpIoqPce0ce6Fw2SQSPzACmJCWMMgafilQ+wAGcX1L69q5/kvv7wl9Si+DSXkouYK8KsO8kIdjXB+JXEhb8Tk0EeB5JSDC8OB1geipJwfHuJAHrpWQEpoMg3VjSPZJd4IAsCR+VJBACBq4kAwpg3n8CoIARvsQ8FasAumASH419wF2WMPHABBqAAAIUAAEKeIANZ6ICDUSAAQYyzgIccIHW3aQFIuAABhw0HQtsIASym40Yx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqnxIQACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/srCwwMFBAYQKICggYecJC5EYLDgQAIGDiqAyJkiBAcMFiRQwLDhgwmZtREEAMC9u3cABSb+zCRRIYGB8+jTn3fQYWaKDxQgyJ9PX/6GEi49DPjO//sA8S6hUIF6BBK4QHsurfBBfQwyaAF+KY2wX38UejfACCu5UMEBBXaongMurPTCBxI0aGJ9G7xwkgfbVehidxmk5IIDHtaY3gEopPTCBif2SJ8EK5T0QIsvFtnASSiYZ+OS5y1n0grx+SilfNeJ9ECRWHanQEkqcMjklySU1EKJU5aZQkgTZKkmADGK5AIDX8aJ40gvYFDmnUB+NAKRaxaJYUgRxCloAiGGxMGdiFKgYkcE9KmmACGBIOikEYRkAqKYctBRBo6uCaBHC0w6qQogWYAppi1sxMKEnWIZwEf+Gog6qQMfiXAqphtsxGmran66kZKyxklqR1HeemeqGRXAq5oFdIRCsJNq0NEKxmIqQkYs8LnsiyxwdAG0gjLQUQjVIopBRrtui6WvGMEJrrAc2VnusRhdqS6WR270rqBhbjQvomdehMC9WDarkQr7ximtRi38e+e1FylL8IsEbPRswkxesBG1Dk8ZAkasTkzhqxqRgDGTlWqUQsdTanpRyCLzB6lGkp5sYwUbXcqyjx9g1GjMFFas0cU2e4izRhzvfGLPFykANIUIbORC0TUimNELSvcIoUUNPN3flhpNTXWHTl6d9YlVWpSu1zByBOzY6Q2rUbFn04esRSyw/d3+CRwNCHd6C3S0YN30WaDRwHqD15HYf5+nMUdYEz7fxxmlmXi+bjd+ntwb0V333RgJoHcA3XbUgea0elSC5BDkupHlXj8A0ttFz/mR5zvnyZHoTwtQukc1j330RzqfzTRHIzwdQJsguVs0oSLJq7SiH61NsOxuhmrzAf2G9IKpuQf8EeIEgz1SlzaXLdKYO6cNktP3KvD7SCh4+e4BC5e0ApnzSgDxSNZzVACwdxIXaA9aB1BfSb7XP/eNZAS8c9QAmJeSQAWLAZxDyaGMhQHQmeQBEXTVA+anEhRYME4HeskKNninB7nkAT+rUAAIMEKZkOCEHjoAAzRQKJikgIWfJ5IABkSwKJhkoAEIIIAABDCAAihgAiScCQgqwAAGcOgAC3AAe3pYExN8AAMYKJEELLCB+xRxNmhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbBkSkAAACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/spiggICAwQIGFAAwQMWOFVoqMBgwYEDCxxE0OACZwsRHzBYkCDBwgYOIl7EZNFgQAAA4MP+iwdAoMFMFxcWGFjPvv16BhdmvghhAYL9+/jtYwjRksWD7+MFOJ4CL7lQwQHuJZhgBy+98IEE+UUYYQkqeTCAgBiOF0AGLIGQgIIgurcACSyZQIGEKOZnQQon/Zfhi+I9oNIFIdbongYqhZDijvmJQBILCsAoZHgFnORCBDYmyV4FJ73AAY9Q3vfBSEEOaWWRJTmg5JYGRGDSBlGGCQEHIT1g5ZkATEASjVxuyeBIOooZJoUeeQAgmkNyGJIGbbZJYkgiyCknixyxcCGeVgYQkgsI9rnlASG9AKGgYUrQkZmInmneR2w66iZIcVI650Ys3JmpkAGc4JEKjXqqJKT+HrUwqahRWqoRpqdaKWNHnbqqJI4dhUprlD5mRECuZyra0Ye+bsmARycOGyYGGbGALJqqboRCs22qwNEK0srZAka4Xivkrhr1yq2NwGokbLg8FmsRAuZeyZGW6yrp5UZgwhslmRcVUO+QBHCkXr5JOsBRff5CuQFGhw78orIaMYtwjQtwFG3DO1qAkQASw0hxRhcnCatGHENpq0WmhiwgRyXbmABHKfNIAcQuZygAR63GnGDGG81ac4QeX3RszgIWvNHBPif47EYMDx0htRdVifR4CHDEQNMK7qsRBlJLCLBFE1wd4KYaVcB1gu1m9EHYEcpbkbVmi5etRi6s7Z7+txu9AHd+435cN3gDeGTx2kBr/Ld9RWPUwOAAoL2R2nobEF9Hby/OX7WDp+pR3nof0FxHfv8tgXYaPW625BwhufblHj0J9+YasQAy0gEA9xHoTScwukelS00B6ht5cLWaIYHANQgimRC2CR+VOzCBI7leMuwhyZ4y7R4JLHHWJW19MZMlgc3xlCJ5b24BupPkgvjrVvD7SC+YD+8HxIekOrINtG8S5c1qm0kyJy25jcQDt0NUAJCnEj656gDMW0mgaCUB6KVEeqj6zUsA+CjmvISAlcoOS7gTsQwNoAF3e4kKLnA4EC3gAnyDSQtCsLEUWSAEgXuJfxBAAJAFYACNBChAA0aAExJowAEM+NABEpCcC6AAJykQwQYwcCIJUMA6IVjBbLbIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEf5lIAAACH5BAkEADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLRsvCQwMEAwYEEECggAIPOUloqMBgwYEEDiJcIGHXg4IAAKJLnx69wISZJC4sMMC9u3fuEUD+xPVQgLp58wNGvERR4bt79wxUuHzRogVPFs/P66degMVKFxUc8N6A30WQ0gohWCCBBBA0KMEGIaxgEwvl7WehdAKoh5ILDhDooXcLyEdSChY0aOKJJlJQwkwjCHDhi9JlcBIKCXxoI3cHMBcSiSj2iKIFJsA0AnQwFgkcSSoIeOOSKIAkAoM+RmliCC6xMECRWAZwwkguMLDklweIuNELHEhppokYtFQhlkUKMFKHX365QEdlnmlnmiplwOaeDYQEQpyAXrCRCHYWCsEHKVm5J5tafuTCdoCCKaZFJkBp6JlBntTAonsi8JEGkQJqIEYYXFooBSi5yCmbW3ZUY6j+cbpwkQmmGrpiSR6suucDHZEAK6AdXFRirXZKYNKmumJZQEcX/BqnAxatQKyh9pGkarIwBuCqs2BaFMK0hVI50gnYsqlhRipwG2eTFG0AbrEkjVAuljJqhIK6X+o4EQXvnmnsSLnOCyOvGvmK740aVNSvnS+MNIHAMPapUQcH3yjoRC0sfKaEIukJ8YUKbERxxR9WQNELGptZbUgef7zfdRr9SbKHwU6EcspRrgwSuS7vVy+6M3soHkWW4nwiSSz0vN+RGbkQNIH6SjSs0SZaUNK1SksXgH8bvfr0d5NG9AHVJyJKkgJZ89dRe197B21FN5MNQaYjtZw2AARvJHP+29xdXBG/ZP9bEpF3t8qRknyHLVEJcotbErJpe+pRs3y/fRHgOFPQsEksYO1yox654HXQB7B7UQpU33rSw1lL/NHIT5us0dgpm50SAUoPwDVIkM6cgKwbubvwBpsn6nm5AZwLkugzl+6R8OBiULxKPAscwM8i3Uvy0B7RTuwH06805LwBwFySweoeUDNIJmBeqAWqv3TClclenxKN3B7AvUgiuB+lBCIIH0wQoCsCGG5DEfhVfFCSggShSAIW+MAKBCiTEeCOTQTA3kpIMLobLSBhLmnBBH+SAQJeKAAEeMDuXtIBL33oAAzQAPDmkgEFEEA3AAjAAAqAgAmskCZzICDOq46zAAfIcDZITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQTExAAAAh+QQJAwBEACwAAAAAyADIAIcpJmQ1M204N288PnRAQ3ZCP3VLUoBOTX9PVoNTW4VWYIleao9mc5Vrd5hve5t3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3r7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCJCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy64bpIQGCRAcOHhAAYOHHTiFxDjRgcOFCxlAjFDxg7aH3AyiS5/OQIIG4DGFqDBeobv37xU6/pxo/jZIiAfU06fX8FJIiwzg48c/4VYGBPX4qTtgwRIHB/kAgncBDWuF4EB+CE7ngUotXBDgg9+pgFYQGiRooXQSnCTECRB26F0HZ1V44YgZlsShhyiCqNEQQOhgQw059DBUCCPWyAB7I7WA4o4V0HdRDzCQIMKQRIrgQg4/3XCgjSOWIBIPDvKIYgwV9bBCkVgSmQIQPd3H5IgOYPfRf1KieAF5EdkgZJZsimDDTjR+WSMGIOlY5o4jSFRDm3yKUENOQaAnZ41ibiQEfHfuiGZDNvTZJ5I3xTnoiHR2ZGeiKObpEBBrOtomlzG98MEGLxBBwaQ1OuARCJjueMFD/ld6ymcKMKFQAAC4AiBAA6gSypEQUbbq4aIJ6SCrozq4NEGuuQ7Qa40LbkSDsDtKyJALx/bpQksfMJurAc+OSAFHJ1LbIQgMDWFCtnyS0NIB3uKaQLgXPsARq+Z2mAFDPbDbp4wqvRCvvPRaqOpG+Ob74KsL5eAvn2+qtMHAACxQsIVBbISowg8KsVCjD7P5p0rdDmzxxQhmrNHGHAPosUJ7hpzlDCv5QLECKOdn70YdtBzgvgsZKzOWkKp0a7wI5IwfBByN4DOAHPA7NJbJroTCwOAqnV6JGpX79HcqKjREp0OTMERLEcRLgNbrcRTD1/M1BMPUQ27r0gYBMCsA/tvUyfAr3ODh0JDQUxft0qgWvOCDBHxHdzBHPQNeAcMNpTD1CjeV0PiNHr0tuY8NATE1wDUFyrcDN3h0KOAX8ACRwyGPnDnfOHYOOOgPzfAwzToJmnOYIbHM8ZkTgSwrCRHrJIPWIYiEw9ctVATEuo6uUDVPmqNce0iec4w7RTnEiiUJK9hw9k8YXAyByiM5rTAHL2ekQw41zGBDD+cLJeKzErBPktet6kD8yuKBZ2nAfyVRAbVOMECzyMB3THJA81aCA+G5KnptkRSYDviSS5mJgXEJgZcQ9AANpE4mLSDTgzJwAtfRZQchwIAE0LMbCFDAAye0yQ9aMIIOwAc5bhwAgQpcOJsiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychG7iUgACH5BAkDAD8ALAAAAADIAMgAhykmZDUzbUI/dU5Nf2ZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AH8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL5rtjxYUGCRIYQLCgwYQVOHvIGJGhQgUIEixkACHD7ooIBghIn05degQPM2VwgOCgu/fv3Tn+qIi7okH18+cX1HgpIwP49+8t5Gi7Y0J09PipR2DZAwR3+AB+x8FaNSyQ34HUJbAeSjlYEOCD31Uw31kr3IfghQYAZ5IM/0HoIQTNlQWDhReWCENJNnTo4Yo2kLVDAiXGKJ0BO4zUQwUr5tgdBD2ItYN5MsqYgEg9uKejjhVM5AMPOtzAg1IaBCkldiCZcOSV4z10wwkdbOCllyfMYNQOCEgZJI0f9SDBlUfy2NANInwp55ci3GATChgwoAAGMSB0gZlSTvDRCGxeCQJDL3Q556IbvDBTCAMAIOmkA6BQ0A4kAophjRz1oGKhH/aY0AuMlrqBmC/hoEAAk7YqqQL+BJWg6ZQdtQAqlgnpoKipi9rpEgOuBgsArAJNMGuQ+3EEwq1HDogQCbyW+oFLKLAqrKuW/lDmsTEa0NGazOYIAUI0RGsqqitFeq2rAfxQA7dBnqhRDuEe2aJBJ5hb6gksobCusDHAAK+MGmZkQ706hljQrvrO6cNKGPwb7AMeDByjBhupgHCOJhikQ8Ol6rASsBK3ekCUFl+IsUZWbuxhxwXdADKjvqakQMmtKoByygdesFHLLj84gkEyzzwnDRDjPCkFsvJ8IJUZ2Rr0g1kSxIPRc4qsEg5KS4rCCk4fWMJGMkz9YAsG+YC1nE+uJIDSAuCwQ9j5LZhRD2YHOGH+QSmsvQG/LIWgNAYCbUs3dWhuBG7e37lp0Ax+u+DSzRIT+0MEh1fXQEccMA5eBgl9gPUHD0/+7wE4DNR05tKNzZHUnneHNkJFz4w0TCi8zS7hBM3NOgEG2K0R3rE7AMHeB0EOMgs0hUCBAgU8gELqBv3JuqAeERr7oQuRqq+jQpGZeeIdqem54wvpILqpItw+VMWH+wySxowP/dAL0MrZAQkvlF4UjGFLAKdAgiOzVUBUEbkBDW6gA/8dRXw8C56NFrex45FFYCkzgOtGcjCXQWB2ZAHbwDR4krIh7INogQEAZ5UAeZ3EBgW8VQXuhZYdYE5TCxggSnrQOVBZAIGoalGhlBZQMJbA8EoWUJhbSgCkAxlgASuLSQuM9CAIWABmdCnBBBagG+AloAERKIEOadICEFjgOMarQAY40AIgzuaNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCMpSxnScta2vKWcQkIACH5BAkEAEAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLFsyDBQUGChIcSOBAAgYWOH3MCHHBwoQIEzR0IDGjbm0GBwpIn069wAEKNmQKvxDhgffv4B/+RAihIy4LBdXTp5eQ3eUMC+Hjx+9Qnq0NB+rzp8fAUocG+QDGR8JaLCSg34HUMaDSDBME6CB4F6S1QXQIVliAAjGcdEJ3D3b4gAU3mPWBhSRax0NJK3ioong+kBUDhSVWqABJN3C4YocWjMWDgTGS6IBIPjR4o4oaNJVDChmIIAJEFPQYowkhhTDkjS4oJYIBAQCgpZYGpMCQDTA6WWECIOlg45QdTsAQDjC08MILONiUwwJZbmknAAssJIGYMX7wUQdo3rhCQjiMwMGhiHpAw0wpDHDnowAM4OVBMfAZ4wEe3RDojREc9EMLiIaKqAo/wJSDo5A+GgBCGFgaY3v+G5Gw6Y31DfSDoaLm6kGpLjWQaqoIHMSAqyVu0NEFs654QkGg5uosCi6JUOevjy5JkA3EljjjRjoku2KOA9Xg7Lgc1NCSAdSmakBBLGRLIqYbzeCtip0OBAK5znrAUg7TpntnDgRt4C6JJ2p0wrwqtgjEDviOG6dKKfibqrUCCTxwhRkajLCHIQIBQ8POvrCSCBJDCgFBe16MIHAaAbrxg80B8QLIuaqwUgYlP1oBQU2qfCCUGkn5soNVAtEszYi2sFLEOduZAUGt+qxfxhnJOnSAHc+MdNIr5dC0nTIQNKLU+cGKUYpXA1gfDlsjau5K6H49QEEmkJ0fRy6kDSD+QfduvStLJH/9NEE82J3ethr5oHd84Aok7tYwuIRA03kaxKPh0vHHkZCLezcgQSggDa1LOQhQsgAAG9Qz5gVQHXTn33Vsa9/4etADTCmYnm4AgxtUOOuIb6Q47I0T9IMK+JZwu0yT/4pA6gjhh7mxHv3X+bII0ZBv5DWlQMCjBOy8ELaGJ1BwR90uPoHCCdXwggottIADrzhlUEEFIoTtUMpkA/3n4kX7Cg/CdDEJAOlMG+vAWCrlMwWcDySaGpoF2BeWul3sAGYLSd42FoFajcVi2UoAy0pysHlNIGZmaRexGJBBksgrWRfwoFlsgB4xXeeBJ9EBfNA0Hgqm5QOolyMRA1y3khVwTkUXkJ1bNjAsBB3AAUR0yQmQ9aAIaECJcvmABBjAowMowAEUiAEOZ7KCDlxASBGwgAZCcAMfzuaNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCMpSxnScta2vKWagkIACH5BAkDAEMALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31arB5au816yyxK3D6LKyxrLA2rS6ybXJ67XM8bbL7bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjW78jX8MjY8szL2Mza8s3W5c7S29Dd89Pb59fa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+jq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLPvxDBgUGChIcSOBAQgYZOIPcKIHBwoQIEziEQHGjbm0GBwpIn069wAEKOmQKxxDhgffv4B/+RCjhI64MBdXTp8f+8oaF8PDhk2+rw4H6++kzsPTBIb5/+CisJUMC+BVIHQMq3TDBfwyCh0FaHURn4IQFKJCdSSt01+CGD1hQXlkjUCiidT+UFAOHKIoXBFk1SDjihAiOtIOGKW744FEmNIDAAAQYsIAJE/1A4IsiOiBSEAvWiCIHRPVQgQAARClllAJUEBEFRL4IHEglKFljc0G9MMCUZEo5wAsO6eBilhMmAJIPNHq54QRhBlDmnQAEACRDErD5ogofhSBnjTQgJIRNNtiJ550BoKlQDX6+eIBHOwxaYwQF8TDDCSJ8QAIMOcg05qJ4CrBQBpG+eOFGKFha44f+QszQ6Qe01joDTBCQSqqVCTGQ6ogddISBqymuMIQQJ9SqbK0ivASlrngGkBCkv4oYo0aVEovigzMs6+0HLrT0ArSkboBQiNVSOOlGJ2rLYQQ5fPttqCs1QO6iCCDUQboilqjRCu6i2IK83oa7kgH34mkAQqjyO2ENrAbMoQcEL9vsSgQkfKepB/XpsIFbZiSoxA1qUPGyh6qUscZkDoAQlh8XCKhGXZLM4AUnKwvESgiwTCYBL8dc4KoY1WyzfyDkTCsJLC3g85QL6Cs0fkRfBPDR/rGgNLgsmfC0lOYepMLU93FEA9b+8bA1Di0963MAPSD0A9npKcBREGjDZ8H+EC7kDINLFXzdgEJD0i2dfhwlmbd3ASJbsQgptzSqxgLEnRDMhhdQddGLf/fhEDDIm0LkLdnAsp4LzZ253R3h3fneBOXgAgm0iuAC2zJ5fW8AvC5kn+EjfNTf4jEkRPpML7i9qABhM6SD4dd25MPiNwqVa7QQWO6Qx1OH7NHIWINJ1AYLECAAjwZsoP1DP6zpsARHxilxCGNR+7EC/oaULckWrDjW2A5LwOY+cjaJTeBzY9lXuhQAMQwFzAI7QIsMqsWA/JnkBtrCgP/QUgP0sOk6FjzJDt4jp/FsUC0jKJy1GtiSGChuWxGESwd8ZaADOICFMFnBsBoUAQ7EkC6GI5AAA4Z0AAU4gAI1COFMYhACDCQpAhbgQAl2cMLZWPGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOetIsAQEAIfkECQMARQAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOlqvQl6vQmq3Qm6O4n7LSo6u/paW9pbfVp7zhqL7jqsHlq7zXrLLErcPosMftsrLGssDas8LctLrJtczxt8Xcuc7xvMLQvNDxvcnfv77PwNTyw83hxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09vn09/y1uHx19rh2Nnh2t/p2uPy4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AiwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8teHEQGBwcKEhxI4MABBxk4h+RYAeICBQkUQIBYkcOujAkHCkifTl36BBYzc5CQ8KC79+/dSf7QiCuDQfXz5x3seJljA/j370H8aBukAvr75yewHIICvv/3JKx1QwL4FUidAuuhxAMF/zX43QXznSVDdAZWWMABN5yUA3cOdviABDwg5UNHN1BoYYUYlsQDhx52COJQKTQwgAABACCAAQukcNEOBJ544gFBjPQDgy22KMEQQKUwAABMNunkABZQFIR5PvrIgEhDuFdkkRv45AMCToYZpgETcVDlmSaEtMKWbMbAkw8GiClnkwOM+FAQJp5p4QEgDcEimx5KsJMPBMxpqI0QmalnlRx8tCagW66gE5iHGkqAQ3guWiWfHfkJ6ZaC4vRBpZV+0BALmqLZEQ2ftkmEDv4zqOACDDrEVCiphgrQkAOpVqkfRyC0umUILoxg7LEqCOHSC7hWqqNCQfTY654cDUGksB5GcOy2xgLRUgPNHorAQjdMW2WCGfGA7ZYecHvsCS3dGq6cuiokg7k+AqdRDusW2YG7x9rAkgDzzhnAQibge2KaGsXQb4sZAGysCywVbKidCImgsIUibNTCwx5iIPEIJVRssZw4KKToxgUynNGjIDcYscQqsFTjyWEuhCrLBWKnEasxN6jByDCwtCTOdOrMc4H6ZgR00P79K3ENLC2AdJMLLLTD0vihi9EPUP/XLsAqEMESs1cDYCq0eXItHacaeRo2eBBIXEIPLh2Ns/4AGCc0gdvV/boRCXODF4IK7qrgrUspXB0lQwkDPp3PGzlcuHfj2QCDCrLaYDZMcZ5MZkNBSP52kNVe3t2ROfmgd7h1PvS35I16RPjlkurk+rwDpHyn5EB+NMTlrPMUOqkE9O3QykvX7mjhuff0AcGGQimlAlwrgLrwF4R9AZJAfbCA3gEIsMALykeUKcsJeC38n/1SEGFR6Vt078YHNC0SvyBL0FxZ9zPXAShHEv6tSwLjOcuApoWglCwIWxBSSxBmtygGbA8lQ7gdpDYAvrXIQFonYkCGXJKDa7VoAyGCiwl4ZaADMEB/L4lBsBwkgQ38jy4sqAADenQABThgAnsyuCBNaICCDRBJAhcAAQly0MHZOPGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWnIsAQEAIfkECQQAQgAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOlqvQmK7SmZm0mq3Qm6O4nLLWn7LSorneo6u/paW9pbfVq7zXrLLEsMftsrLGssDas8rvtLrJtcjmtczxt8XcuMrovMLQvcnfv77PwNTyw83hxNbyxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09vn09/y19rh2Nnh2t/p4OLo4OTs4ujy5eXq5env5ury6Ort6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AhQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8t+/MMEBgYKDhxI4IAChxw4gcQ4sUHDhAkWPpBQ0aPujxC5C0ifTr0AAwzAYwJpYfyB9+/gH/5sONH8be0E1dOnx/BSuIXw8OGfcCtDgfr76UOwxKEhvn/4LawVwgH4FUgdeym1MMF/DII3X1E88BASBgZWOB0DKJ3Q4IbfbQAUDygsMEAAAJQogAEZcMSBhSxaZ5IKHMYoXk88NEBiiTjmCMACGYXQYosUkNSCjDKSsBMKAuioJI4BoGCRDQT+yKIJIumwIJExxpCTBDcu6aUEFTEgZYsHZPfRBljKOEF5NUng5Zs4pigRC2P+GORHNKRJpJE1vdAlnF46CdEP9tXZopkbAdGfnjKyGRMPAwAKaAAR0WkokB7lyWiRNDUgqaQNQOTApWR69MGmas7Ew5+fLhmAhP4N/RAlqRbKwBEQV6LKYQku1ODDSx60KqmcDFlKq4UIaqSprhxeMMKzM7hkgLCAGuDQisdaiOFGMDLLYQXPPgtDS0lS+yalDVGQrYUHcESCtxxCEO6zO7DEqrk6OjTquga2u9Gp8DYo77zRqnQDvnDCuhB6/Br4w0bvBdwgCPOusBIPCL95Q0OFNowfohctKvF/Hcybgr0ZL+mQmB7jxxGaI/837wgFq1RuyjgK4BCFLauXAEcaxhxfBDPXuxICOOdobUM891zdthoFLXR44IZbs0rBJl2iBw4Z6/R0yWa07NTfOStuEC1hrHUAG8f6dXUs3Ep2eCKkMIPRLnmadP6oD+379gEPcwTw3BMAoerNCL8KkQlvS+eARzHM7d0HNaGQcgBcD8rw17Z2BETEZONgUwYZgykR41/fCfncfNrkJrUBmD7R5h6XGRLoI6+Z0wuIw4m5RTI4rV9IOEwd4E6vnyuBwhV5vW7YII0N74M9ZYDAnwEQkAHzF6nLrwKBj/RuwBoYLtQNN3C/UdO0MhA+SVLruoH5ZvlIKwbvlzSkrifQf5YMtPvRAYanEhzgjkgTOB5b7Ecm7Lxkf2oiT1ygU6EEYMAGM+HOhixwAh04JwQUYAB6dqMAB3AAgzfZDgk28B7kaOADKvDgbGZIwxra8IY4zKEOd8jDHvrwh1ZADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73KJaAAAAh+QQJAwA8ACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7iesdKfstKjq7+lpb2lt9WnvOGovuOqweWrvNesssStw+iyssaywNqyx+u0usm3xdy8wtC9yd/A1PLDzeHFytbI0uLMy9jM2vLN1uXO0tvQ3fPR3O7T2+fV4PDX2uHY2eHa3+ng4ujg5Ozl5erl6e/m6vLo6u3q7fPr7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gB5CBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy5acgwUHBgoSHEjgwAEHFjh3zDCx4QKFCBQ+fDAxwy4LCQcKSJ9OXXoFGjNniIjwoLv3791J/tyIy0JB9fPnHWB3OeMC+PfvP4xnm8MB+vvnK7Dc8QG+//ckEGWDSTEkgN+B1CmQQ0o1UPDfg99dsENPNniAwAABAKChAAZYABIL0SEoYgEHxHDSDNxBqOIDEdSgkw0LZKjhjDQCsIAMHMUQ4ogillhSDSmuqGKLOEEgY41IatiARjTsyKOICZB0Q5BCqkiBTTYgkOSWMxKAUQ4MPCkmAyLtsEGVaG5Akw0GcOkmAANcxIGYdI4Qkglo5rnCTFq+6aaXFOXgJJ09grQDlXkOKRMEfvoJAUVzEipmCB/hmSiaKMBkw5GNbhkAjhEJKqmYB3h06KVoRgBTA536iYBE/ieMSidwHL2Aap7NtbRpq36C+pB9sj4pQUf93VqlCC55wKufHj4karA8lrrRqcYKqWpLbS7rpgEQxQCtmAtqVEO1aMLQkgDauhkARCx8+6SJGs1AbpUg6MASuulyCdEI7vJIa0YrzCtkByqwlK+bAzoUQr8jUqoRCgKviEEJK9lwMJe+MhQpwwfaqZGlET+YAcUrcXrxjPtyjOAJGwUc8oMatMDSACfXKABEsaqMH7wZ2fryfyDgwFKfNWvI7UPe6nzfehmN+7N/LrRkQdEzNutQDkqjF+W0T8M3gUsWUw1Axg1JkHV1+nEkQtfgBegS0ScfnfLZ07HMkctsd/fC/kthnxxAChJhTTeJ4XKdN4sTvsTqyQtQZDbdHHy0dt4myJRtvggkHDjdCRTO0Q55U5C4pjSnO4DmE22ss8OVsp0pTZe3SgDqE+Vgns4KhrSDez9LaNPijQYAAe0UNaly5yNN+bLoOMkA95YEAL5Ruwz7SJK8ERO5EwQG4AtAAAEQAAHZGlH/7QF2l4Q9uRHsDZQMMhDvUYHQKsC0SQ1We8F8aOXwuKQM8NxJdjC5S21gdGo5gYHGxDOWvMBBaXIRXEIQJgQdgAH/ggkKzgShCGwgV3Q5QQUYsMADKMABFWCBAGfyAhJsAIIRuMAHSDADBM7mhjjMoQ53yMMe+vCHW0AMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y9bCQgAIfkECQMALwAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AXwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8uu3IIEBQYKEhxI4EDCBRM4XaDoYKHChAgTMmz4oKJuCxAMDhSYTr16gQMUUsh0UcJChAfgw/6LfxChA4u4JBRYX78++0sUFcbLl2++rQn17PNbv8BSRfz5AI73QU8eNEDAAAIIQIABC3hAEgnS6SchdQyohMJ3AWYYngU5rbDAAACEKOKIASwQkgYRTqiiAtqZFAKGGsZYwXk0rQCBACPmqCMEHmmg4o/UJdBCSSHEaGR4E7gw0woG6OhkjgRwZEKKQKpY4UgqwHhkjBzGdAKIT4YZ4gAiZJRCAlWmKYFILEyw5ZsbwLQCmGKKKcAKGEmQ5p7AgbTBm4A251KTdRYapUUk7LmnAiChACigFbgEQaGUAtCARQwouicIH1nwKKAlsLRCAJUWGsAJFJmg6Z4JeKTCp/6ATsBSA6VSigBFF6y6Z4sbfQAroDSmhGOtdQZAEZq6VqlBR27+umUIKolALKUORqRqslU6wNGrzm6ZgUq0TluniRFxgG2VB3A0QrdbRqASoeKGaYBEPp4L5JAaFcnukUqiREC8dkpEgb1A9plRB/seKehJwwLspLEROUDwjyRslEHCRqKQEqkOOymARHpOPCGnGv2JsYahokRnxyMOILDIE/KKEcInZxisSQiwrOOhEOUKs34yX+RrzQHeXNICOudILkQg/JxfuhuVQDSA7qZ0QtIjYiBRC06zp+1GLkw937cqNaxzAHhKhGzX1PHHUbNihzcguFgDsHREA7NNXf7QM8ctntEmjZr0qanqPR2jHXHrd6QsTarzpRXhxzYHH/0X9wgu/dvxAGkXzvaVrvrdZUtzOnwnRhJ3XTFIF4ut8UsnmF2rAGVilAKVE1PAppYYdyDTCprXSgCqGiUKswL4huRozRX0K5PjlAYAQecaNT2xkCVJjXGSNyEtZonEe+TzuSyeNDS7M+qEgYE4BjAAgyJQ/5HxyTKQvEnLO2uB82eZsPae2LnfSVQAN0CVh39p0cD/fsQAg7EkBAU0kgUW5hYNZGpCB3CAA18SAk9pKAIZoKBcNCABBiDrAApwAAVMIMCZhGADFmhWBCqQgQ6oAIGzyaEOd8jDHvrwh15ADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAzkVQICACH5BAkEADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLxnwCgwQGCg4kYOBAQoecK0BwuGAhwoQLGjiQsKsCg4IC0KNLh+7gt8wWICw82M69+3YNy+H+uqBwYLr56Qmst3zhIYL3994nhNfJ4gOEBQsQNIDwIeWG8ucFKJ0CJ7AkgnvwJdidBSvg9AECAQAg4YQTGlABSS5IIOCG04WQ0gscKCiidybU9MEAFKaY4gAXguSCAxzGGB0GJ72gwYg4cgeCTCwgoOKPKRLwkQsMyGhkAeqJ9MIFOTb5wHwtoUAAkFROOAAKHWl4pJEejhSik02WGKUAVZYJQAAjbLTBlkceUGBIIoDpZAQNrsTClGaWKYBGKgDIpowKhNQCgnLmaAFLBuSZp5AYafknlyB9WWiYKmWgqKIZXFTCo1sm8FEKk4I5gUp4XqrnRTByemSSGt0YqpP+UJJkqal5tjiRC36qGqMEHb1A6Ks4coBSorSaaUBFIejaZkcmADvnSSxEWGyZAbBAUarKylgCR646m2MKJs06bZm2RpRAtkZuwNEE3jYpgkkQjGsmBBSdi26MFKzbbo4emNSAvGUuQNG9MjrA0b45amCSjwBTeaxEKhAcIwMbtYAwjheYtEDDVCJwq8QcBqrRCxePeGhJG3P8o8ASuQDyhryOXLKIwpYUr8oqNkBRri9Ll+9Gv87cXb8liYvzhJlO9FzP5tG4kXZCv7djSSwcTWEAWE7kKNPRdamRpFFzJ2ZJDFv98ETJch3dAS5w1GzY3EXwwkkVWC2hzhS5rDb+db3CzZ3CKJF5tADWVrQ1015vBHbUY4drdbkTnbB3Am13tILfE8ydUqkNM4qq2qxu1K3QsZrEAooNE55RxExTDJLFUWfMEgoNo7kRBT0fsG1IHggdAbgtjYB6sQOkyRG2BIfu0ej7lq7SncUSUDhHLix9788jvQD1vkTHVIHgZgoAwfQdVX99jds72/1MEHCeYgAEjE8S8o8eoPxIzBcagfMzZdAAAgQwAH4qQD6SYEBVCkgcSkDwKgs0Li0ZYtMBNlA5loBIThEQgebccgLyTIyCMllBezCmwbqUAAMMYMC5dKMAB2zgTTZJAQgucAF2GccCGhBBnWbDwx768IdiQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSUisBAQAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8vW7CIEBQYKDhxI4EACBhU4X5jwcMFChAgTNHAA0aKuiw25C0ifTr0AAwrAY74QYfyB9+/gH/5c8NB8JwsWIzJ8+ICipYsOCarLl0/h5QsSE8Lr1+8BJ4sKBAQAwIAEAmAABCygVIIC8zUo3wYspWDBfhTqJwJNLCwgYIEcErhAeyRtcICDJFJXX0oiRFDhiuD1B1OGG3Yo44ALjERBiThOxwBKHrDo43cXvMSCATMWSeAACX60QY5MWmeSCD9GKV5LIwhg5JUACDCCRx002aQDJJEgpZQarMTCAFhiKUCSGp0wopdMdiDSCiqOGSUJKQ2ZZpoEcMQAnE0ekN1HF9gpZQTlmdTAnns2oFEIgHopAUgmGDomByexECOjRgYAokUuMBhpk4Nu9MKElkqZ6EiLcpomAv4YQTpqk5N2VGmqUmJKkqau7vkpRQ7MGqhHGuB6aEkZ9LonBKC+KWyOJXD0Qp3G/pgCSUQqi6UBFsn6bI61anRrtT/qKtKm2hbJpkRLfpujAhxBSe6PFoyEQrppbkmRBO7meABHHMz7YwQjjYAvlh9UFGy/Jf67UbECs0iwSB8cfCWzFInKMIkubIRqxCu+IFKyFhdZQUUab9xgqRd9DDKFq3pkcMkzZlDRnyo7yFGhL1c4Egs0z/hrRDfmPF8CHPXY834TkBS0jENDhIHR88G7EQhL71fvSAg8XSC3FXlL9XQnips1fyRV4DWBjlbkwtjVhSDt2eGZsOvaA0YdEf6/cBdwQMcA0+1dBCKT1LXXYFvUZd9gdiSm4GWWBPTTAeh7UXxwR+tRfnRfq+jTNWa0ONWNe/R41pGfRADNSG6Eec4HnBAS5z1HsEKeVh68JkclUI2BSClkDcJKKOSurZYeid1v2SGNG7GLZq6uLAF6Z8R3vwoAPlLAEVtQeEutMhpAA+t6VPS3DLAsktLkXhAzSygcjiUBlovUrrAUaP8kuR58HxMLEJAehwJAgAbUjyQneB2cDgChlayAdnaKwIVygoIMVKACHxhB+VByPyYdQALqQ1EEOfC+tUAHRweggOxkwh0fRcADt6PLcyTAgPjsRgEOwMAKbbIdDlwgP3DIsYAGQBDD2RjxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrSKgEBACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL5uyiAwUGCg4cSOBAAgYVOF+Q8HDBQoQIEzRwANGirgsMuQtIn069AAMKwGO+AGH8gffv4B/+XPDQ/CaLDw0MDBAggICBBRlautiQoLp9+xRevhAxIbx//x7U9AECAQBg4IEIAmBAfCiVoMB9EFZ3QAgspWDBfxiGF4EJMY2AQIIgJmgACiZtcECEKFKHgUoiRJDhi+CB8BIEBYZo44ENjOQCBSn2OB0DJ73gAYxEfncBSyx8eOOSBhIgEo8+RglkSUMWaeWRKbFgAJNcAjAASBtEKWYB+Y0kgpVoPhAgSkp2yaSTHZ1w4phRdiDSCi6maSUJJ0HgppsLdPQgnVEekN1HF+ppZQTljYRCjX9yOcJGYRIqpgQgnakomhyU1Gakb2rkQn2WinnoRi/0tymajYL0Aaj+fzJ4UaWlRolpR5quamWnIm0Ja5cGZORArWIe4JEGuqIZgUgsQPrrkgGwcJELcxLr46kYvZBnskW22lEGz7pZwUUhWCvmihuZwC2aMoK0QLhdInARlOb26ABHVa5LpAYh+QrvksFaNGy9PSbAEbL6EjlBSAT8y6QAFw1MMIrGboRwwi8uC5IADkN7EakTo+jCRqpi/OILIDnbcYIQWwRyyBCOrFHJJmOI8kcDrGzjlxYxAHOEBm90Qc0ZLgySvzojCGdFEvwMoQIccUA0hhaE9G7SCcprEb1OUzelRvlODR6WH4GLNYKyUtRB1/hxRILYAIbEwtkIkmiRC2xXVwL+Ry/AHV4KIn2adMAX+Zx3ARVzNLTfD2gc0qt0j4vR2oeX6TbjapKE9MpLT/vyzwec4FGqfkewAkkonP3BRpR3bXlHb8O9JklXrxwoR59PbGhINJvM6Embh2uAtByV0PUGIqUgtggosZDzvwMQ31HrE78OUuwmz45S8JESIL1HTU+sgMwjSY2xBTerVDuoC3z/EdfEMkA+SWEne0H6K43Q8J8ETFoSBuaiwPxKAoJ1eQB/LcnA/m4UAAJAICUlyF2hkLeSFPROWcyrSQUWQACOBWAAAzBABdyHEloVSoAvydWiDhiXDQwKRQmggOhkIoJEvWgCHjgdXVSwAQkwoD5/u1GAAzAwQ5u0QAQcuEB/kGMBDYBAh7OJohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMJFUCAgAh+QQJBAAvACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm3xdy8wtC9yd+/vs/DzeHFytbI0uLMy9jN1uXO0tvT2+fX2uHY2eHa3+ng4ujg5Ozl5erl6e/o6u3r7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBfCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/Zs4kKEBQkOIFjQIAKHnCo+bLBQAYIECxk2jOC5QgQGChQ8eFghs3aCAtiza8fe4LfM4BUc/ogfT158huU1PTAYAKC9+/YEFHho2WLCge34tyfw3tJFBwjlBVheBejB5AF77yX43gAUqKTBfflFqF0CJrAUAoACZkheBSq4JIIBCoaYIAEnmNRCBBKmuB0IKbmwgYYwllcCSx4EIOKN72FAUgsNqOhjdhqc5EIGMRY5XggqPWAjjkwCwIBILSzw45QF8CeSCxYYqaUDBZb0QJNgAqBASD1SOSUJJBG5pZYomETBkmEyqaNHGphJ5QEVhhTCmltC0OFIIsAZJ5MidJQChHb+uEBILGDIp5EWkETAoGEO0BGKiVLJ4kcvPrrljCFhQGmcc2ZEQqZmIvARCp6uKYFI/pOOCqYAG5WJ6pRWaqRmq1p2yZEIssY5H0YtIHqrjxF05IKjvBa5AUgMBBvmmBiBcOydHZXQbJ8gxSotkwFkZOu1PqbA0a7bFsmCRyd8G2aJFyFA7pRBbiRBuloi2RGw7jY5rEXyzuvjBRzdi2+RH3jkQb9NNmhRCwL/2MBGLhxsZAYeUcAwkw9cZELEPk6skQoWF4lxRxpvfGPHFqUAsoqLasRCyTFG2pGoKotYKkUQvyxhshpVTLOGz+6bs84YGeuzdhNwxOzQ5HXg0QpHhwivRdctjV+9GoUHdYD6diRA1QtmhKnW2m2qUadfkwdqRwqQ7R61F3GAtnYHdDRC/tvkQQBSu3IDsHNFPd9dANAU8z1e0R+NTbYA1IlreAFqb4Tu1297hHPVDptqeAIteMQq3xW4AGvVBHQ0rs9ofnQ5zW2KBLjKAfyrkctai/zRzF+fPNLCKrPc0QRLH9A6SB1ADUHsJMXNMN0eSfly5SBlSXPmJEXrrgKRf9RC1gI3TZILXh8sdUqbjxpA5yGlAP614pfEQvnbnq+SCAgOSoDtI62e6AG5Ksnr+AQBX6kEA45r0gCEdxLioSoB1DtJ8lpVAey1BAMMSGCCBqAAEXQvJSk425QOoIHQtYQFbNMSBEJgupqcwAPPeQAGRFComJjAPj5aQAm/858iWYCFh3UBwQUWsAB56SYBDdCAuW5Sgg9YwAL3Mk4FMhCCdc3miljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk1MJCAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm3xdy8wtC9yd+/vs/DzeHFytbI0uLMy9jN1uXO0tvT2+fX2uHY2eHa3+ng4ujg5Ozl5erl6e/o6u3r7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/7sogQFBgoSHEjgQAKGEzhfpPBwwcKECBM0cACxAieLDw0WEBggwACCBh9kugjB4ECB7+DD/hc4QEGFzBcmLkR4wL69+wcRPLSY+QFBAAD48+sHYKCCyxIKiCeggOW9lIIF7yWYoHwvfTDAfhDuN0B2KZ0Q4IAYiocBSysgqOCH74HAEgsLRGjifgigVIJ3GbYIHgMqpbAeiDS2d4FKKBBw4o75DTBCSRuw6OKQCphnkggz1qikBfOZNIIAPEYJQAAUhrTBkFiCl4ALJYmg5JftTfBCSSjcJ2WUAfwI0glCZjkkjCOtkCSYSt44Egs6nimlACCpkICbgEogUgsT0GkoByMhoKeeBHwkAaCQAgcSB4ZW2hxIHyy6aAYdlQAppAqAlEKllVoQUp6a7tkRA59CGsJH/heQWqkJH2WQ6qL+aXRCq5Am4NEKslY6wUcG3KqnARthwCukRm4EQrCVNrkRC1AaK2UAG/25rJsbdFQotHSK0JGt1p6Z60W7buumAxwBCy6dGnQEQblnNpBRB+q6eQBHJLxLZwQdKUpvlMhidGW+WXKpkZf+gjnmRsUOzGOjGFGAcJaSZuRBw2BeqlG1Ep+ILUYOXIxlCRtpwPGXKXD0YMgn8onRoya7+KpGlK5cI60boQozhANkZHHNLTaL0cY60yhtRiX+HGGKGClLdIZGX/Rs0iAujVHTTu+3QEYhTI3hvhuZgPWHAHNEbtf6cYqRC2IPyO5GL5ytYLwcsWAm/ttTsqCRtnGDtyFH39rdnogB841fwUIHHl7VRxvuntYZrc32uRil63ioHbkruakfvdz1AH5vdGHgHXzkoeEkgDQC31Xq6jicv0puJ0gR/8w4RyXHjTJIKtvdckgsiC6xAKV3pEKbJlMw6Jwre0BSmSFTGZKnRCugcEijJm3BwyNlOnAAbocUtslblmT2ymKeNILxqfpYktT5FnnS1f4ymRILAqdKQPIkwd62GLA9k3QPXBcAX0o+ADIpEaB8JzkB4CBFngKeZAWFq1R8FLiSCvgsQgEgAAQAmJINTBBLDMgYS0SQwS9dwGMxyUADDEAAAQhgAAZYQAVI2JINsMpFlQdwgApfIoJY1SgCGoChXDYgAQZo6wAKcAAFTmDBmYiAAxf4VgQsoAEPrICDswmjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqExlUwICACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLHstiBAqdLkpgcKAgwYEEDBxQKIHzRQoQGixMiDDhggYPKWyy+IBgQAAA2LELQFBhZgkJBwr+iB9PXryEDjNTcIjwoL379+05kIhJ/Xr2+/gN3G5ZgkH5//8xoMJLKVwA34EHXtBCSywsYB9+EN63wH4ouUBBeABmSJ4ELL3gAXsIhvgeBytl8GCEKGaXAUonKKDhi+QpMCBKK1gg4o3vWbDgSRCcmOKPDZhUAoYwFnkAcSalACKOTEYQXUkI/CglfgSQdAKRRWZ5QkkrLMnklyuQBMGUZGa3gEguJJDlmuId4MJIL0zw5ZztRfCCSBWUqScAK37kggNsBqqASC9oQOehFoSEgo97/kghRxsEKil6IIlw6KXzfbRAo3oa4JELWEpqJEgveHlpkx+NwCinKY7QEQb+okqKwUcgnHopCB4ZwKqeVW4EaqyBHuBRqbYeGkFHLKy6K4qPXhQCsJN2ZEKxmHKUwbJ6QrARoNCyySFHhlJLJ4kb6YotmZ5qpGa3awrLkZziznnsRgKcS2YAGqnAbqBbatRCvIeGqZG9ZbKQUQn7solkRikATOeTGKFAMJmuYtRBwmtSmhEJDs+ZKUYjTDzlBxlFinGRG2xkacdMiqBRyCL/2OdFsJ4M46wa1coyjrhmxELMjmb0rM0vaozRtDvf+DFGQKfYLEUIE61hCBs1nLSIJmykbMz4ZuSC1BrOmNELV4u4Y0ZRNp3fRqGCXYCbHJla9gN2bpSn2vdpq5H+BG6X50BHHMwNnwYc/Yx3dk9XNHTf4lHNEdKCt5c1R+bi3atGXzP+9psckR053XdyJPHhJHPEN+M4A/55zx1VDnS6HP3qNtzDys1y3R4ZDnQAFXdkMtipe7Ry2ax7dC3Qen/kotQKcA6SjVdbEDpIaU98Zkiyn3yA2KTaLm4EZ4dEwMQGGCzSCTYf4PhIK+wcweQkbXruAuaPFPW+6p9kNcDvozTmrgFIXklOsC5oKaBfJ1kBvKhlAYGhZATV41XvTuKC08WKAc5DyQsCV6wLTE8lI3DdjwhQOpYQUFIMWBhLFHipC0DMJSyAwPggFAACQCBxKwkBt150AAakTCaYJgjXjSJwAZfdBAUZ+MAI6meTEFCAAes6gAIcIIEQZJAmJvDABeAVAQtogAMm+OBsxkjGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV9mUgAAAIfkECQQAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sGy2JEhgoQGlT4gAKnixIYHChIcCCBAwcYSuB8kQKEBgsTIkzQoAFECposPiAIAKC7d+8GKv7MLCHhQIHz6NOflxBiZgoOER7In09fPgcTL1ks4P69f/8FL5XAgHoEEuiACi+lcEF9DDKoQQssVSCAfxT2F4B4KrlAQYEcEigBSy940OCIDHKQEgsIVKhifwSkdIICHcaYngIIorSCBSTmSJ8FEJY0wgArBundACOYVIJ5MiZZwAEnnJRCfDpG+UAEK5CEAn9CChlAkSOdgKSSSTJZ0gpQShkllSKxQECWbAIgwEguJADmnAe4MNILE5ipZwQvhGRAm222CJILDsxpKAMivaCBnoxeABIEgAKKoUcbGGppByGJwOimJHjEwoSRshnARy58aamSB4D0QpmbShmBR/4NhApoAx5hcKqlGHwEQqubgsARC1jKmmVvG5V6q6GpdrQqr4y+ulEGwkrKUQjHXtqRCcxyutGf0bJpAEeFVjvnhxwtmq2eJmoEardaciSnuGAmu1Ge55rpLEYjsNsmlxidAK+hTWq0Qr2MVonRB/qymYFGJfw7p3IapUCwntdhVEHCWUKgUQcOg4mpRiRMbGanGEGKcZC0ZlRpx0lusJGmIkcpQkaxnrwigBnZyrKMLmu0a8w6zowRtDarOOlF1O4cY3saYQt0jvgNXbSKC2fUsNIdMp2RxE+TGPVFKExd4QcaqYB1hzVm1ELXJPaIUbBiAxAACxuZevZ5dXLEKv7b8vGpUYpxg8eRBHer50BHHPBdnwbPBu7d0RglXfh5WjetOH1fY8SC43ITq5ELk+NtJ0cvXN53nxvVHDfOg4eeq0eJX+7rr+sWPbdHoBeet0elK+53RxeLnXKthb+uq+Kze7Rm0QTQDRKMWCswOkg4dm0B6p4CebIAzg9q978JpK3q3gRP4PZHYWMcANkjXd3xARCPxLXIEVQs0gi1CzsA+yS5D+8BlZMf/TKXpuUJq3koOcG7qkWjlKyAXtniUUpMFqkAQKB7J3EB4Y7FgOmh5AWx49UFsJeSBbQpAAvAoEpKsEAwMSBgLUkBBM10AYO5BALcolAACAABz72kA6ThitEBGBA/mJDAXDmKwAXsJ5MP3KYBC4BABkagwpmEgAIMeNcBFOAACZTAgzQxgQcuQK8IWEADHEgBCWfDxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhMpSpXycpWuvKVsIylLGdJS6YEBAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm3xdy8wtC9yd+/vs/DzeHFytbI0uLMy9jN1uXO0tvT2+fX2uHY2eHa3+ng4ujg5Ozl5erl6e/o6u3r7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjywY7ogKEBQQMIGhQYQROFyEoMFCQ4EACBxIwnMD5woSHCxYmRJiggQOIFTM/NBgAoLv37wEW/nyQ6aKDggMF0qtfX+AABRUyX5CwEOGB/fv4H0Tw0MLlBwLfBSggAAP45lIJCrCnoILvvZSCBflFGCF/KrGwQAADZvidASisdEKCC4bIHgYsrQChhCjmBwJKIwig4YveBWDgSSWgJ+KN6jGgUgr1pejjfReYlAGGMBYJAAQnbWAjjkwq4MJJIvT445QWvDBSBUZmCUAFJW3A5JfqJWCSCFOWed8EIo1ApJZFzghSjWDGqeNIPJppZ5AfseAim0YG0CFIKiwZ55ckhtSClHaWuaJHCPCppQEhSTDopPCBxEGimPbH0QeOsunmRiVMOqkDIKWAKaYadARgp1kK8BED/qJOWsJHF5yKaQobZcAqmxl0FEKso3pkgq2obtTArloi0BEGwE76JEcgEIuplRntiWyRAXSUQLODdtDRBNImSkJGI1yr5Z8ZncDtoKRutEK4iaaKEZbmGomkRh2sG+cBHJEAr50RZARBvUY2sJGX+oLJEZn/mplRowTDCKlGkib85bMZXdpwmdRaZEDEEm/kgMVfLqeRBhuXid1Fq4KcIQEbwUoyjiZnVGvKP65s0QIua6isRhTMjGOlGXmA84+aWnRszwMusBGzQosI7dE+ZkQv0wFyiW/UIYq5kb9US4gmRixgLeB4GqnA9YLtatRC2BLKi5G1ZmfL0bZrr+ct/kfgwo3fuBnxbHZ3P28UdN7qEa2R0X7fl/RFnA4OQK8chYp4AW1vZGrjD8idEXdmw+wRiHnP6tGJfuO6Ublmo92R5WtnztHmcHuu0cdMT/zRyGub/hHKcKvOEQtrRixjSGpzTYFIb4ftAUiRG095SLCTzADGpVJ9Qcce6RrxvSMhbHEC2IfE8MYTcP/RwOYGAD5JUOurgOIjRduwBY+HpCayAUxvUvXAul5KaCet7Z2EBRBzFAHQhZIT4E1UB8BA+Uyygr6dKgIgUF9JWsQmAvhvJRt4IJgYUDOWiMCCZrqAzlaSgQu9KAAGyAALZLIBmd3oABIo4UtEcDMfRYADmit8yQcg0AAEECA3C4DACGZoExdsQAIM2NYBDqAAByhngjF5gQg4cAFwRSACFtDAdTQ4mzKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJylYmJSAAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8v+yuLDAgQEBggwgKBBhZwlMEhgoOBAAgYOJHTImQIEhwsWIky4oIEDCZksIBAIAKC79+8ADP5AmHkCg4IC6NOrR+9gucwVICw8mE+//nwN11myyCAAvH/wAfzmkgsUHLDegesp4F5LL3gQgX0Q2mdBfiiNMMB/GII3wAgsYWAggiCqp8AJLIHwYIQo1mfBCidBwF2GMHo3HkouSBDijeodUEJKL3CQ4o/1RZBCSQ3EaKR3DZykAgM4NpneBie1cAGQVNInwkgQHKklAAiU5MJ5Toa5oEgvyFflmRR6lOWWWgoYkgsOhClnATuSqcGZeD4wpEcjvMjmkR+IRMGcch6ggkge5IlnBC14RMCfWwYQUgmEzulASCkomqcGHVUAKZtudsRkpXKS+NGUmuLJokYsXPiplv6SehQCqXMq8JEJqeZpwUZrvqrljBzFSWupHt2Zq6oaGeDrlgR05MKwc1LQ0QvH5ulBRiz4uayRKHDUAbSFdkRCtYtmlMG2W4aKkbDgOnnoRsaSW2WjFxWJ7pFdbpRAu2FCudEE8p555UUI3HukARzxGyYGHAV8JggYKWtwjANspILCTkqwUQsOV8kBRv1NDGOsGZ2AcZOXarRCx1RyepGrImMowEYmn3wjAxutzPKPF2D0aMwYVqzRszaHKK1G1O6c4rUEA41hsxsVbXTDSqPItEULOP3fAhx9KPV6Y2J0YtX2pUmRp1qDB2xG7H6dXp0axUs2fXtaxELa4HW70f63bqdnK0fjzk3frhn9jLfQGxHddwFHb5S04A9cfRHaeCfZ0b6Lh+ARwJCbwGrIWgfAgqyL4+wRroL3vBHlTlvuEZhfw92RmWTXrRHoMQsw+kc1S53yRzpX7TJHHzgdQAYi2Vj0AaaC5KPSEaza0bkxrw3SqBgfoPlIqHYcgecgFTwx1yS5gLnC/o70AucODxxS1vcusDtJKsAObvoktUA7ue6L1OurAbBe+bBHqwOEjSQv6F6uImA2kYwAd2wSAPJWMihaMUB2KElUri5gO5NAAIIxCuD8VnIC5RVqAy5wyQqetygRvKAl2gkhASAwQpeU0GsgOgADUPgeDowNRZoRuIALZVKBBRiAAAIQwAAMsAAa4iQEGGAAA/Z1AAUowAE8vIkJQHCBCwAsAhawgAaGOJsymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhMpSpXycpWOiUgACH5BAkEADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/soCwgICAwQIGGAAAQQWOF1soMBAwYEDChxI2OAC5wsRHi5YiBDBggYOIl7EZNFgQAAA4MP+iwdAoMFMFxgUFFjPvv16BhhmvgBh4YH9+/jtXwDRkvv38QCOt8BL6B3g3oEHxufSfBHk56CD/KX0wQABVjheABmwVEICCHboXgIhsJTCBA+WmN8EJpwEwX8WtggeBCptYKCHNLK3gUoiNGjijveJQBILC7goZHgGnOSCBDUmyZ4DJ73AAY9Q3qfBSEEOaWWRJSGp5JZMlvRklGBOCRIEVpYJQAUkbbDlmgV0QJIIYMb5AAkffcCimUJmGFIHbLJZgkgkyClnCh2xQCGeVgYQkgsc9rnlASG9QKKgYEbQEZmIlmneRxg4yqaCHoFAqZwRZsTCnZm6GAAKHrkwo6f+Sh6ggkcv6DhqlBG0oBGmqVoJY0edwrrmjR2JemucPmZEQK9lKtqResI+6lF9x1ZqKrNmsrrRCdGyOetGK1Qrp64X8YqtkL9qpGa3WxKrEZzigpmsRQiceyVHDrDLJUcaxBsmRgbYOyQB+OqrZJca9esvlGJadKjALTqrUaMG05gAR5MuvOMEGAkAsaocUVxxh5BulLHGJVp6EaofA8jRqyMjWLJGtqL8oMoOt2yhABxBGzOCCnBErc0PWoDRsjoHSPBGDPzcIQMcXUB0iRdgVGXS4yHAkZZOuycBR19OnR8HGFWANYCbahRs1+2BmpGxYuNXakUsnD2ethq5wLb+e99q9ELc+ZF7kcd2AzCARyJ3HXRHJ4ttdEYNFA5A2htRsPd6bmvkAeD2zW1R3Xav2urlstLKea4bRX425Ryt7XTmG8E9tecXsUC4zgEA9xGjXR/Q3EeSih2Bdhx9gDWaIYXQtZshmSA2nR6p/vGAI3FdMQUkha2xByEFDDECuo+Ub8Vfl6TwwmSL5P25BIRP0vjsOvC7+QtrQLxI0ve6gPslWd4tBvMzyebEBYL7jcR4qQoA8lSiPFgdgHkqcd6tIgA9lJhrSAFoAP9i1KcDMOcl8IpTBLLDEu7czkIDaADeCISBxAENA317yXwaVzQQCM4lH4CAAQjgsQDsxgCKDRgBTkqwAQcwgEMHSEByMHACnKRABBq4AIkiMAHrgGAFs8miFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAzlUwICACH5BAkDADsALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfkxPh05Nf1VakFtZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrYmYxY2MrJKbspKjzpSpzpmZtJqt0JujuJut15+y0qOrv6O34KWlvaW31au816yyxK3B6LKyxrLA2rS6ybXM8bfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AHcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLbnvjAoQEBwQIOLCgwYUbOHOU4BDhAQMGDyhkKJGj7o0JBwBIn05deoIJM3OEeKCgu/fv3SP+hIj7XED18+exu9TOALx79+PbqiCAvn51AiNYwnDwvj94ByysdYF59hU43QUqldCefwx6VwJJOLSQAggffEDCCSvgoNMNDRjo4XQLnJRDBg2W6B0FIOnQwgcWtOjiiyC0gFOHH9YYYkkkmqgjih3RwOKLQL64wQs1XVDjkQB4QFIJOjapAAob6XBCkFQCScJMHiCJpAoioeCkkzBkpAMJVZbp4gc6wHQDfVrWKEBIOfD3pY4MYDSmmXha8AFME7SJpHoehTCnk/FVlEKeeYLg0g0E+vmhADN4lMOCg5rIQA0VxYAookSy1KejRyLYkaCVNvngRDr8uKmZGLQUHaj+bnrEXal0UtTCqoiusJILsCIZ6UYy0OokphKpimuZrapk5HQB9FqgqBoxKayOp0Jkw7GIapjSAtIVoIEIJkhQgLPn3agRBdPuKNGt2OIp47YAFACuCfSaMC65IHKEbrol8gjRlO2aeWVK9ElQb70d4DsdARzJyS+DDkhEZsBlKkowAPMebEKzCr+5kcMP91dnRMZSDGSyKJmncb0DKAyAxxpRGvJ7I0NUsskubqBSdBWsrIHLABzA0awzv/eARCDgTKXFKCUAwAAai2AA0AlwFEHR/UUg0aFKA3mCSjR6a4IIFSAANAANcJQj1uBlsG7XQOqa0qfMni0doBmRyvb+d4U+pAPcL9Kg0g12o/erRjnsDR6xEW0AuJ4ssVm4dEJ3BDLbR0/ELtxyqwTB5NPhrREHinfXd+NwY5DmSoSDDqmkpV+aKecu0W236BvpjfXpEit9wuosrVm4AMB9FCfbDDR30c24ognTCIUrGRILbENpJ/OIfqAtTDS6DAFJaz/MAUcT4woC8DFxq3DaJe3Lr9sdteA4ohi0gL5M6ju7QPHt80uB8h6RH6vsl5PP9WoC/DMJ6YQVAgCChAYtOAEINrABC63ABj2BnqMEID2VUK9SDLAeW5Z1JAH85iXSahIDmAOX50jOPgeYwOFeop3LGS0EjJOLCi6wgATQRwB+BODNBFyAExiUgAIR4A8DHJCcEMhgNlCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpgsTEAAACH5BAkDAEcALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfk5Nf1VakFtZiGZuoWZzlWhmkW97m3VzmXeDoHiEtICLp4CNvYF/o4iTrY2MrJKbspSpzpmZtJqt0JujuJut15+y0qOrv6WlvaW31au816yyxK3B6LKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2M3W5c7S28/Z7NDd89Pb59Pb7NPf8tfa4djZ4drj8t7m8uDi6ODk7OLo8uXl6uXp7+jq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AI8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLvhukgwQEBgQIILCgAYUdOIeg4DABAgMGDipkAOGjbpALuQFIn04dAAIJwGMOIWFcgffv4BX+TODQ/G1tAtXTp5fwUriD8PDhc3C7woD6++k7sKQBIb5/+CisdYEA+BVIHQUqkcDAfwyCB0JaEhgo4XQIoMRBgxh+N8FZFEzooXUmgZDhiOKVdcGHHzZAEgkkkpjBRUXcoMIHHmwwggkzDCUDgSh6qF9IOizY4ogBTlSEChhYoOSSS6ogBFAI9PihANl9NMGQJDJQHkQwJMnkl0vC4JMIUqKo4kcvYNniiw8VYQKYcC45Ak9B2Ffmh1VuNER/apK4JUMqxCmoBR/sROadKXqUZp8uOgTDoIOKCZMRObhwQgkx2GDEEQsgOqVHFTCaZUNCeAlpnE+6BEQKJbTq6gn+OPDo6YQrcDSEkKJmSAOgpw46Z0s9uCqsqwfM6iF7Gy2aa4bzKVSEqb3CmapKRlw6rLAhDGCshBVuJOKyGW6o0A3RRspSDtdeG8G2BgrAUQbgZsjAQm+WG+evKsWQ7rAasFuguxuFGm+D8yo0gr1xesASq/u6GoK/BQax0XsDNziEQhsgDCcGLFnbcKsBQHxfnhfxWfF/fxrkgcZgbsCSCx+32q/I6nF05cn/LfQBy18qvJINMZfwAM3pEcDRhTjH58BCgfIsJ0vVfhxCAURX161GSCcdnrgJzeB0mMB+nEDV1SGrkbJaf9dsQkV8reS0K/GQ7gk1kF2dCLamHd7+CwzVyzO+LRlhA8wnsGADEZzaLZ0AEnMksN4MXLxQ2zxj8ANNmw7UgeIALOARCnp7V4GjPKugUxDo2V1rR0NQnPauDh2MsAlF7LQ52Wd+rjebD8ke7Qe185Q6zVSG5PrJWkrUQq8YqBA8TytUfYFINGhNAkU/+A3nB5cDdSjEZoeEdrxrVwSDCR9ssIEHI8AAN1ANQGxA4yPBOzAEkpMV4bYI0E9S1rmaQP5MZCwJ+K8kLMoVBwZolhUMr0cCmN5KaHC8FjHgem05EYoEgJ2XJDBL5IkLdCREAAnIYCbcwZADOKAD51ygAQhAz24MsAAKnPAm28nABN6DHAhUAARqLZyNEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIvUSEAAh+QQJBAA9ACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jO0tvT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gB7CBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/ZrA8ICAgMCBBBgAAGEFzhzbKDAQMGBAwkcSNgwo66NBrkBSJ9OHQCBBjNzYDBeoLv37wUY/mCI+zxA9fPnF7zUfgC8e/cU3IoQgL5+9QAZWKpI8L4/+AMmrAWBefYVOB12KW3Qnn8MejfeWTYsYOCE0xFwUg4UNKihdwycJSGFIFpYUoYbltghWRCAqCIAEJC0QYkwFrDBWBkQuCKF+YVkwoIxbhggWDYMcKOKAYSUgwI9wngARDCQcIEFF2gQAgw78dBQikOqiKBHLyYJ44ML0XDBA2SWSaYFN9gkQwkecOAmCjEgZIONWU4YAHAd5cCjlxoe0FxCO5wQgZmEkknCTDyg4Oaii46gg0FY1gnilht1yWeJYB6kQaGcPmBBTDG0yeioHMRJEAGSEukRA5cqmdAJ/p12esJLOohKKqMePCqQDamuiKdGObQa458F3TBorJzS4JKit5JawkCR9jphi5UKC+OMBo2JLKcTtBRDs82aioC0IBrAkQTWluiAQTRsGyuVKzEL7qgoCGQAuRQOwJED6W6ogEEkuNtpByyBMO+tAgmJr4FFboRkvw0uWdCmAhd6AUsH32olfQsbyBF/EDdokAUVF9qtSjxkTOqjdHaMHkd7hvzeyCUT+ulKtqrMQcIu2ycARw/L/F4C2dZs5sXx6uzmCAKh2jN6+m7EqtDv/VtQCEaXGQJL3yrNgkAfPl2diBqRSDV4JxLUbtYPrNBSCTqXYGUPFYh9HqUYfXC2/nuZDkSy0RPs0FLKGXuAw0C82k2dCBwFu/d3Khx0g9ERwNtSDQd7YOpAHCse9cePd2f1QQGXvDVMOIzQbAmHF9SA4tKp1xEGoRcQX0J/u2uB4DK1ALebHpTQwtwFJW53ACl45PjeB7iwUOmxRnAC7zXpoAPxCL1uN94b0b533wjRkLvNyhplQ+cuB2ADSDmALPQBOTy0QggXTGCBBSHQQL1RIohNLUgqOBu2whItfMlOJJaC2O3Gcq+FGWB9JOEXxBwQv7I0UFoEgGBJJGgtBlTQLNpL1QI0aBLvtYoCHzzLfOoUgArox309OsAH3FJAOzWAhCtJ4IYOgIEUsuU5lgor0AAa8Kv1bEdDCsAAseSSAggYgAD02c0ADNCA5N3EBRtwAAP4gxwFOAADzpuNGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKucSEAAh+QQJAwA+ACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG5zvG8wtC9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gB9CBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjyyYMogECAgMCCCBgAEGFnCowSGCg4EACBg4keLBbewCA59CjPzfwW2ZwBQWya9+e3cFyuDcW/gSQTl76gOotdVA4wL09dwXf2YovT988DJbr3et/T0PtDQP1BRhdACCkpIMD+yW43QEqoHWCcwJG+BwEJ72AnYIYZreBWTdAKKGE6Imkw4UZZhhfWP99qCIAJ4x0YIkwFvDCWAusqGIA94VEQYwwHtBfUjm4sMIIJqDQwg4PgWDjigaEpAKPMTqAVA8mcGDllVaOgCRDBCy5YosfMQBljDMuxAMPOMnQAZZsWinDQhl4uSIBH5EwZowMIMQDCxZE8MCfGqRAU5VtFjqCQl3KqWKOHIl5J4w/ElSDBn9WWqkFOMTkQqGccvDmQTcoumIDHenwaIwYFFSCn5a2+oCg/i7tsGanbXawZUEQiHpjRxuc2iNBOLDqaqs1uEQorW0eahCAun7IaEYI+lpipBcMO+wELeWAbKe3DiRAsx9SuFEC0pa4oQ8pWGttCSxtum2hLhgEbrgclWuuQNWq62oELKHwbqEmFATDvBIusBEN9mZIgUD6WoumSsf+e6WyA51AcIRNavRCwhhKiUPDwxar0ggSsxlCQUpeHCACGz3JcYIS+MADyK6KnNIKJWOJQkGhqlyfwRqZ+vJ+C89Ms6WZqtRCzle2wLPP9ZEa9ND7perDBEf/aQFLPTBtpQ0GjQc1eSFixB7V7X0nQtYPiNASyTlTTFCiY0dX4EaOor1d/oMCYU3zBA+v1HXJHeRwUAV1RzdARx7ovZ0CBMVwNAsvzVCy06AmDh3QGwnteAELE7R2wx/E5O+7JvSQ0LeaZ+ARuZ+TYFANfu9LuUzudtqBC6onFGfidHpkp+N5IiRC7Q9EIELgMuUQAqcjGM6Qh1Df7RGJVPOdEA4x1JA0Tja0YMIIIZjQgvQNWTx2xh9tjLaUYSEAdQBggiQB1QeUGRb14Abg+kjYK9cBZDeWG7BuXuJyEezsdS6ynOCAukogSV6wQF81sCw3oJuc/GegvN1pgGupkZwIUL+U7OhODNCfWk4gvxUJAAI3cMkL7hejBGxAB3BhYYQCQAAYymSGihg6AANuWJcMNIAABPhWAAbAGx/ehAQYYAADyHUABSCHiLPJoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMZVsCAgAh+QQJAwBDACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OClLiGmLyIk62InL+NjKyOoseSm7KUqc6ZmbSardCbo7ibrs6fstKhs9Gjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+ne5vLg4ujg5Ozi6PLl5erm6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCHCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy0bc4wQEAgMEBBBgwACEEzh/xPDgQEGCAwkkSPAQw+4JAwEASJ9OHUCABTdmxpBwoID37+AL/hzQoCPuiQHV06dHkN1lDAXh48e3UJ5tDwPq86dfwPKHBPkAxqfBWi4IoN+B1A3QQ0o2JBDgg+Ap8ANaH0SH4IUACODCSSt0B+GHBSRgg1kVYmhiAO2N1CGILB5QH1JCAIGRCxaaiOEAJNngIYsgKmAUDzSgMIIIRJYgAw8T9YCejTYSINIP8PHIowND8YACkVhmKQIKPkQEAZNgfhCSB1KWuUJQNQyp5Zoi0PDQDTWCiaEAIOmwY5kgJgDUC2z2KYIKDiEgJ5gbfGQBnmWS4FMNfvpZA0MuDApmAB7ZgGiZBxAUxAwpdABCCjPIxIOajbLZpUIUSApmihqFcGmZ/vXhwMEEtNbKAQ4wqVCqnyUshJ+qNlLQ0X+v8hjCECxUUOuytLLgEg+7NppDQj3ECSyClG70w53FQngADsoyyyyuLNEQrZ8vJHTCtUwuqFEM3UqZgbjiVtBSCef2OUJCG7BrI6sXkRAvjxHQK66zK+XrpxAIpeovhsC1OjCLDxjMbAorAaFwnzIeJOjDFxaq0aETf8iAxcuCsJIQG7PZsUELgHyhmBppUPKHDaBsK0str8nwQV/KfGDEGZF584MV6zyBCSzh23ORCX0g9IEAW7TC0Q8WrDQMLMnwNJHpIiT11Pm5m9HVWAN4gdIYBMEStF9Pi1APZKuHo7Zpy6cA/g5Kh9rSlT0DqtCSdU/HH0dR5v3dgCmgzLRLPjyNJKqFU0e0xIp/19wQO2BQr98veb2xDAzRXXmGHv2QuXd6EgRDpx2YwILbMvGZ7ws/+3o6BB8Rq7gHQJkbrQy5L2R63QKYzZHqiicwIVA+ON2nCnI/9DHZIhuquKJD5SADqSKM0AIPxTvUg4FTI/Ckg1hbkBQQPpQv0Q1TKziSDlhLSNa6IAewIUnwKtkBRlSWfvkrADQricAGdoAznYVG1xpA1Uaio24p4EVnUZKkrqO8k0DpUuN53lo+gL4m/a8lK2DflAj4FgoQ4EIBMMDlXhICB3zoABLYHF02sAACoC8AfwMwwAJO0MGZkEADDmDfARQgAQ3EQISziaIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKRZAgIAIfkECQQARAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRaXeZb3ubb32gdXOZd4OggIungX+jgpS4hpi8iJOtiJy/i5/DjYysjqLHj5u3kaXKkpuykp66lKnOmZm0mq3Qm6O4o6u/paW9pbfVrLLEsrLGtLrJtczxuc7xvMLQvNDxvcnfv77PwNTyw83hxNbyxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09vn09/y19rh2Nnh2uPy3uby4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AiQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8tm3APFBAIDBAQQYMDABBQ4gcgAAYHBAgQLLlwAIcMuCgUBAEifTl16gxszZXBAUKC79+/dO/7oiItiQPXz5xVgdymDAfj37zmMZ9vDAPr75xuwBHIBvv/3Haz1ggD4FUjdAD2kZMMC/zX4HQNAoBVCdAZWCEAAL5zEAncOdlgAAjYUNcQPOfAQBEcoUGhhhQGsN5IMHHrYIQLz/RREDSmMoOOOK9RwokUvqLhihQKQZEOMMna4wE9DzEDCjlBCOUNFPZg35JAEiASEe0kmCUFPP5gQ5Zg7kvDDRBNcqWYIIYHQ5Zss7ITDk2TWOQIOEd0gpJosgqQDkm/OqNMPdNpZJw8QNcCnmh581EGgb5aA0xBiGmonCT8ypOeiVwbg0Z+QdokATjVYaikMDmnAqZouaiRCqP5v6iAEDSqoQIMQMAVRqKl1ZqqQfasOOUFH/cGaJAYbZKCssiq8VCqvhtbAUA97BlugpxsBAaixDR6w7LcfuLQCtIaSwBAK1l6ZoEYycNulBd8u2+xKQ5Br6RALeZDukBlqVIK7SUoQ77I+rPSDvYaeqZCq+1oInKsAyxjBwMq2sBIPCNuZw0JpNlxhoxq5GXGHDlCcwQkr5ZBxnRsrpKjHBrKp0aMjO/iAySirFMTKZCKqUMcw4ydzRiLX/N/NFMdAL89j+npQCEEX2OpFLBjdIAUmF7xSpUyPkMK5UeM3tUXtWu0f1gPnvNIMXes45UI9hI3eABwBYTZ8CVD8Af6uLNXbts8LESB3dfpxBMHd4FUQ7wYn8N0SDF2j2pC+g0/38Eb/Iu6dDELEcMIJLewQ0xC72oupQ3FXfqFHdmv+YU4YZ0xCyw4poPqwHnHgOgg6xU4uCXhClLrcAqzbUet3LxDhoKVfSjtEtssN8ke63y0pT0NAXq4LTj/UA4FRG6Alg1ZfANQPNXC9YwozKFzRDVEjOJIOVkM4VBA85JDDD91XhK7HGCpJ2SIGIrNAbV8BGBpJqgYwBMTpLEGy1gDGNpIjcYsBNTpLlVZlAOOhZEuwusDy1hIC8GGpXy1hAfm8FCK4aEBwBgqAAS4HExEczkEIuEBz6hKCBhAAfIABGIABGoACD9KEBR2AAPkQwIALdEAGI5yNFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJsUSEAAh+QQJAwBCACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFpd5lsepxve5tvfaBxgKR0hKd1c5l3g6B9jrGAi6eAkbWBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9Wrt86sssSyssa0usm1zPG4wdi5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI2PLMy9jM2vLO0tvQ3fPT2+fW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erm6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCFCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy4bM44IEAgMCBBBgoMGFGjh9hNgwAUKCBA4qZAiRoy4PCrkBSJ9OHQABCjN9fDBeoLv37wUm/nyI+zxA9fPnJbzUngC8e/cb3KIQgL5+9QAgWMJw8L4/+AQsrHWBefYVOB12KYXQnn8MejeeUD/0gMMNO/zwEQ8SGKjhdASc5MMGDYbo3QQ/7RCDCSKkqKIIMezAUYYbxthhSSCKaCOJO+2Qwoo8qpiCixhdEOOQAFxAUgg2JllACDkFEUOPUKoYw0UgEEjkhvmFxMKCSooYoE1BtBDlmCKkEARFPAxw5ZABhOQDBF0mmQCYYpI55goUCbnmkAh6hGScST4405N2ktmCRDxYuaeGAQDXkQ9cAhpiAs3JtEOhheIQkZ6LxtjnRn9KaqOgMK2AqZ0mRERAp2x6NIGo/nLKhMOphd7wEA+sEumoRj7AquQINsBEKK1jvvAQp7lqaCSoviYZgQUd6OASisSOScJDDSQbowEcZdCsjQxYIG6wK/1QrZ09OGSAthsOwFEF34qogLgWYMDSpeeOCSRDarJrYJsbwRlvgwfQawG5Kd2Q75gzOESfvwZyxN/ADRqswkqzLgylrQ0pCjF6HEVK8XsGn7ASvhrzqGlD/X6MngAcCTzyewgYLMNKQaTcY7oNreoyeu5u9OrM780rrgZAsEStziKQcGZDMP5c3Ywa1Ug0eAvQezNLw+p8qEMcSH3epxiVcLV7D9S79b1Mp7hyQ7iKTR0KHPV69nce2JD0/ks76mwmRA/LHbTEd3cHwUw9ML2vQxTILZ16HX1QeAHxzfRCylNGFLfYAbjgkd1nJ0BDTabm+7dEjYtN9kaSn02qTEGUTuwKFk7EQ+AfB8ADSD5MPHMCPuDUdaExPE0RClIvCxIMVzOZ0w5Lj5nC2xYhyy7kIoU6cOU74XB5jySscIPxGK3rrwG7kwTvwBUE/5OEN8ywQw/kb2R+sgSkX9L6zU7gvlmpY5UE9GeS1sFqA/87y3wWFQAO6Md3XUpACdxiPUZRgIAr0Z6IEvCBBLLlOS2zzwAosKv1bCdEEPhApebiggsYgAD02c0ADEABz92EBiGowAT4gxwIVOADaqObjRCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCLDEhAAIfkECQMAOAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRaneZbnydb3ubdXOZd4OgfommgIungX+jiJOtjYyskpuymZm0m6O4o6u/paW9rLLEsrLGsr7WssDXtLrJtczxuc7xvMLQvNDxv77PwNTyxNbyxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09/y1uHx19rh2Nnh2uPy3uby4OLo4ujy5eXq5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AcQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8ueLOOCAwIDBAQQYEABhBY4aYig8KDBggQLIkywAKOuDAkDAgCYTr06gAEOgMekoaFBggLgw/6LL9CAQvO3tQVYX7/ewUvhC8bLl0/BbYkB7POvl8CSRYP5AMqnQVA3zDBDDDWUdIF0+jVInXspifBdgBSGVx9PMagAwgYcduhBCjGABIGDJFI3gAwnWVDhiuE1QENOMWzY4YwzguBCRxKUqON1JmnA4o/k3XQDCTQWSWMIN2hUwY47GkBSB0ACGUFNM3hg5JUezoBRCQwyWWIFIrEwYZQsdjDTDBxgqeYGHGhpEQFeMqndRw+QGeV5L90g45pYepAkRRfEyaQCIIlgZ5QTxEQkn2uGQJEM+Am6Ywke0fDfoUCy8NILjDL6wkSBSrojoR0ZiimQiboUQqd8ejCRAf6i7hiARxGcCmQCLsXAqqcRyaBerDqCuREN8dn6o5ksqbArnyZEFCqwJZKqkanGspjqSlYuqyYHEeUIbYkDcORjtSwy0JK2fP7pkALfljjrRhOQy+IBLM2A7ppuOgRruyRyVKu8K46wkq73YhniQ3Dy6yCKGtUJcIUZDFwwlp8+FKnC+s2J0aUPB4jBSjVMfOXB+mLcoL8dU3jCSjeIbGSCDzlgcn4CcERBygAiYANL2brMIbcQjTjzeuFupCLO8pnLUgo+d9gsRM8OTZ20GVGLdHjXqsRp0xtU/JAMXUoNwAUc0TDm1QWI4FLPIvspEbtiAxAAw/CiDV4CL7a0tf7LN0q0ZNxOdgSl3VO+tKrIjlL0q9QffFTs1SjAdAPb2noAM6hiB+6R1SkXDlPI93LgNUUXYxwApSBx3HECmspUpbaibzk0hCCxgPSFM+m5awiXXxR1u1QXmnLWNLlAuZEerKBuRnC3e2JJ8QLsok4vmJAmjRyQ8MLyG8n87QAaj3QzuQ3guVMNMbzwwgw1cO+Rt7E6QHeP1VKQN1olLO5lAPytxMLjZErAgNoCPx0FQAHhU8m4bjUB8xGwdPkJgANQF5PurCgBFGjdXJ6jAAKoJwABGIABIFCC+c2EOxN4QHwSkIAGRMACLLjfbGZIwxra8IY4zKEOd8jDHvrwh1ZADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73uJWAAAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy7b84QECAgMCBBhgAMGDFThLVIDAQMGBAwocQKjQou6KBgMASJ9OXbqBBjNbTFBQoLv3790d/kyIu2JBgOroqweQ8LJFhAPg44M/gEHoixcqJQhIz7/6AA0sYZCAfASCp0AIO71AAgcZXGDBgxl4QAJ+Iy3Q34XVPaBSBAV2CF4FOKnggYMPlmiiBRy4ANIKBmDo4nQLnNSCAx7W6F0ENb0gAokn9vggCBRy1OKLRCJgEo02JgnBTCxk4OOTJV7AAkcWEmkleyNxmOSW9cGEAo9QQqmCRg9YaSYAAIZUwZZsFoCgSyyAGSaUU160wnlnEilASC3A12aSCbj0gpNzFnqBihY1kKeZWHo0wZ9sdrkSCIVWaoEHFpmA56IvBvBRCn5CauMBLKlgqaV1TqQop1Zq2NGj/qJuCaJKHJxa6QYV7ccqkQZ4NGCsSTqgkgu2WopoRB/saiZwG5UALJvNoURCsZWSoKqyV3IE67M2SmqSB9QWiqlEQ2LrYowbIcltjTiiRGi4UF4wUXTmutjrRtytW6OwKMFbaJAP6VrvhQRw9Ku+HTKAErH+hnnsQwO76OlGCNdI6kkvNOxwRCtEjOGeGrVQsYeB9qvxkwA7tKnH6A3AUagjx6dASnKebEEG87LMn5H4xkzgkijVarOJuEpEgM7poasRAz7L1+5JJwxt4ggTVYk0dY1mpGXT33lbUsZSP/jwQyZcXZ0JHKXANXgpqLRB2BzkarZ0BXd0MNcKq8RC/thjUmT11dh1tDXX460Ers3jUtSx2QEwy5HIax8QrUov1EytlBchYHbgHkGwduEs7d3wBShgtLjOAjjeEeQ+JzB5qf5ecIJGEugcAAUiYeDzAR3EFOflpW+kuceuiuT5yLPG9AKlp6bo0dEDKz0S0wg/zaTQYXLQt0crQI/tAqqL1AL13EbwOk0uLAjmBRyMkGpIfy8aQPEnDQ7pAcnz5IILKZNUJqe3Y8maRMU7t8RPYr95if0sxhy4mKABAuvPABoQvpakYAJ3I5ACJnA+uGigAQQgwH4CIADeNOADOAnBBBjAgAEdIAHJmUAJZkPDGtrwhjjMoQ53yMMe+vCHV0AMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIR64EBAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy8a8QkMDAwMEBBBgwEADDThbhJjgQEGCAwkcOJgQwq4GBAEASJ9OXToC4DJDQDhQoLv3790h/jSHq2FA9fPnDZh4GUIB+PfvHaTYyWKECA4bNngAMYIFyhUIoCfgeQuw1AIE8CX4XgQ3vTDCBhZEKOGEF4jgAkkfCDDghtQNsB5KJSSg4IjfKTCfTC+IcMGELLYogkgaRMfhjAAE8MFJIXBH4o4FHFBCTCxk0OKQLGbgn0cfyEjjjDaWVIKOPO7o40sqrEjklRKi0JEJGi65pAArjJSCiFFGmUALLVWJ5ZoRaqnRCgR4KScBIrXAQJl4MsASC1ayieUFR2LUgJyEShDSBHgmioFKL0DoJ5sXZLSCkoTSGABILUCZKI8HqDTCo4+OIGilhDbwEaKb4jkBSi/0Ceqf/hdWNCmpcl7aUaap4tnpSZ++6icIFlFAK6EUdNRBrol2cJKQvkJqkQHDyolARw4giycEJrHQ7KOxSjRrtEvaqhGu1ka5K0kkbOunqBN9AK6cYWpUQrl4okkSCOqy6QFFGrzr5Y0ahUBvmT+S5EG+a3JAkQT+LoldRhgMHOV4IzmKMJGRTvRAwzQ+sFEFEvNYQUkWX9xiBhQNyjGHhmqEasgjLkoSByYTqfBEDK+8YbEaRQzziMqSJELNQwI7kbA6D/gwRsf+rCDFIvVK9IQvtpv0gB9mNK/TCZ440gtTs+imt1ejJ25GLXAN37kkMRt2xhQFWDZ1BXKEoNrfMWjS/tBhW2D0wnNTx/NGPuPdXdAlgf12oBOtELh0AcS7UdqG92ivSVLXzK5Fcs9tqkd3470qq25fnMELGDk+d+QfUa72AZef5ELNF6igkcpXf36q6CypYPIJG61gXtIDSO5RC+45rUDsKfmu7gXAc2QCpf6CKaamA5/5UpDNXjA2R/1yHMDSIQkc8gFQu5TuoxeMgDpI4b8bwOAkmU/vAYjHlKKrLV7gQbchyRC4PJSSEJXLRDhBwQhAwIEMZEA/ImDB+0oCoGERwHgnORCyGMA8tGigS14iAMBaEgIylYkBBXuLBOLEoQAQgHwuwcCdSHQABqQvLhRYAAG6FIABGOA6eBicSQciwAAyHUABDhBPB2fDxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyakEBAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy96soQECAgMCCCBgYAGFnCEmQGCg4EACBg4idLCrYYEAANCjS4duQMLMEBESFNjOvft2Bxh4/rpggYLEiBMqXrT8sGC6e/cDrLssEcG7ffsKwttUMWKDhf8A/reBCCqgtMICAbyn4HQGrLBSCxEccN+E3jnQwkwqcBDghhtuwEJJKxiw4IjSBfBBSi04QOGK3R1QAkwviHABhzQGCIJ6IX3wHIk8QvebSSVox+KQ2y3XEgsZ1KgkgBl8+JEJCfYopQYlpSAhkViGwBILMy7ppQUFdrQCAVKWaeJILTCA5ZouqsTll19e4ORGCJRppwAOhgTBmnwmcOFJL/gHZ5w4ZkSBnYgiEFIHfDYKAUogDDqoBxsNgCiiJoCkQKONpmCSCpJKOqdFD1yKqAEfVcBpow6YpGGo/nBukNGOppaZaUdCrrqmpyOxAKuoF31QK6IPdFSCro1WQNIIvw4KwkUNDGsnAR1NgCyfDJCUZLNxXkSmtLZypOa1u4rkK7dwumARuHZSuRG5fGoZEqjofhnmRCawW2axGqUA75rKhnRCvV+SUJGw+vbYwEbH/kvkBCKRQLCXI1SkQcI9KqpRCA4T+WhIzE6spAgVHYoxiQtsxGjHLEYgEgoiK3nCwSejzDDLLYtEb8wcolDRCjWPKF9GLeC8on4gvcAzjaNGBHTQCv5ItNEUGhnStkv/d8FFtEIt3a0a5Up1d7yGJELWAJJsUXteSzdAR/WN3Z0CJO2ctc8WPd02/nQLc1S03NxBTJKgS29QaEVdew32RmKPXba5aN9rkQR7A4CqRxgAXkCrJnmwNKUaJX7ymR81znKbJr2ANcEXqKuRyVCnDNLKVLuM0rms473RtzXjKdK4OPupkt3NXjCzmJaO7m5ILWx6urxurh7qBbp3BOXJUotkJctWr/RCpLBy4DpIH0QJbgD8llTCleQeEPBLKkivJAfVh7RC8sMGkH1JzbffPUwokFGNLgCC9KCkTrUiwOJQsiddMeBxNCHPCUYgAhKogAWHQ8kHEFimAexPJSVo4JoU8D+2aICDCwoAAR6QJ5iEQIQUOgADKvCnuVBgAQQgQIICMAADVKeFeDXpQAQYwAAJHUABDgBPDWfDxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJSaoEBAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy+a8QkMDAwMGBBBAwMACDThbhJjgQIGCAwkYOIgQAuiLmhoQBABAvbp16ggkzAwB4UCB7+DD/n+HgAHnCxIcNmS4YCEDBw8kXLTUYOC6ffsETLwM4UC8f/8MpDDTCyhswJ4FCCaoIAcnpLTCAtPdJ6F1CLDUQgTe/adheBDEpMIGCoYoogUbNFiSCQRMqKJ1A+iHUgoMbChjeAoIyNILIIyoY4gekKRBhCsGGQBwJoWQ4YxIHtCcSi94sOOTCW4gH0gfABnklR+UVMKRSHZZQkovZADlmBZcMGVHKwxw5ZrUBbDCSC0o0OWc3x3QwklNkknmBh6tUB+bbA4gUgv90UmnAieJoKeeHHT0AKCQagdSBYZWWh5JKCy6KAobrSAApIC6+VELCVRqqJ0kcaCpnhds1ACo/pAu8NEEplYawUgnrLqoiRetYCWsQr7JUQtc1prknSGpqiuZrWJEAbCRdtSBsZaGxMKyi55Z0QLQAlohRxFQa2iHIJGArZ4kYPRpt2sG0FGp4s55QEhOnjtmjxaZwC6gWWqUQryGfvkRiPZC2WxFH+zLJpEZlQAwnUt6JGbBTx5MkQQKr/nARhg8PGcFIFE85nMVPZpxkBtrRKnHSILs0QsiQ6mtRCafrGIDG63MsowTgHRgzCNe9KzNKkqa0bQ7y3ipRwQDHWIGF2lAtIoUbBRC0jJ2AJKyTi/Y69QTuphRC1hvaKNH5nat4Ajqgm2fqBvBW3Z4qH4Es9oJsoAR/gJuX2dARxDMLZ4DIk2sNp/O9m1d1RwhLfh3WoeUK95sY7SC4m2KrRHZjxdwwNkgGQ50BiRj9KrisnpE6+O3jpRp17xavi7YcHdEquB1j1RvzCB0hLHbOIPU8dw9l/RC0wVvUPpGak49gLAgyYm1Asga//O5F+jd5+wZB6D5qHJ7/HlKLiCvq5QhJXxyAIyP5DDLB0Su0u6rcrD8R1IrzP5JVz8cv0skEN2YLkCC+1GpedAaQL9OUgLpUUsBAntJAMeUgQKiZAV8AxYBoIeSFgTOWAyonofQY7gLuGcE2lvJBxC4JgIwjCUNrBQDImYeA7qEAn9SUQAIkLKYdKBQjjI6AANcRhcKLIAAugFAAAZgAARQgIM06UAEGHAczynAARDogAhnw8UuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScqoBAQAIfkECQMALwAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuymZm0m6O4o6u/paW9rLLEsrLGtLrJtczxuc7xvMLQvNDxv77PwNTyxNbyxcrWyNjyzMvYzNryztLb0N3z09/y1uHx19rh2Nnh2uPy3uby4OLo4ujy5eXq5ury6Ort7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AXwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8v2vILCAgIDBAQQYABBgw84W3SIwEBBggMJHECYUEKnCxYoSIwYcUKFCpkrJAwIAKC79+8ABP4sMCGzBQYFBwqoX8++QIIIKWi6UAHigoX7+PF7OOFSwwDwAAK4wAovhaBAewgiGEELMLlwQgb5RRjhBfyl9MF/AWYI3gMslXBggiC2V4FLKmwg4YkRboDCSRpwp+GL3hmgUgjphWjjeg6wRIJ9KPaI3wglPeAijEQOQKBJFdR445IKMHiSCyD4KCV+HIz0AJFYejcAkkt2uZ4CKEU55ZhVgkTBkFkSSQBJHSjp5ZIMmETCmHRaQMJHH6CZJpEchlSCm28uOeJIKPBY55QrdkTAnoySBxIDgUYaX0gumHjomBd0RAGjjMr4UQeRRppjSHNeSmeFGi3K6Z7AeQRpqP6BNveRCxCaiulGm666p6ccgQproKN6hIKtdaJ60QK67hnAkRtF8GugBzjZkQfE0umBRgIku6cEHSXwbKAYfFRrtVJmitEH2u6JAEclfBsoBB6xQC6d1110ZbpZBsBRBe6+eYBHJ8w75p0XNYBvvhxN0K+X/3ZUqsA+EmwRAgdnySxGECzspbQajQCxlEBeZEDFWLaakQMadynrRiJ87KMIGKlK8ouOZvRqyjZOutHDLksYskXIzkxzszjfqLNGw/Z8orEUBS10hhw5W3SIHqmg9ImJWmTw0wEKkPDUICbwkaFXW3CBCxhpwHWAvGYUAtgJBsuRmGVbcG1G2a79Hf4F3cLdXgcfJV030xVRrHd3AdSsUcZ+q3fA0RvRWvfZGuV6eNsa+dq43B0FXPbPeB8OgAYfedt4CCKN63IGaON6OUiag8151VcTjhGGXJMO0odgoz5Syy7D7JEJay8gUgpwR2AStRCD0LpHaguNwMUfvV00BByPxDy5HDz/0b0VG1kSvyk3mZLH1YrgPUhOpzuA4iNJ3a8CkJukgup0UniS5ckSQD2b/WJA9lLCs3KRYH0k+UDeOBWAB/yvJCUwXagOUIEBruQE25PQBThwwJY8YIFYCoAB4KeSCkiwSwdwQP1ggoITTEcEJKgOAlnyAJlpKAAIMBlMKnCzEB0AAosrk8sKHoAAAmQrAAEYgAF+80CYtKACEGCAtw5wAAU4gDkWnI0Wt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUomRIQACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspmZtJujuKOrv6WlvayyxLKyxrS6ybXM8bnO8bzC0LzQ8b++z8DU8sTW8sXK1sjY8szL2Mza8s7S29Dd89Pf8tbh8dfa4djZ4drj8t7m8uDi6OLo8uXl6ubq8ujq7ert8+3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/rxCQwMEBAYEEEDAwAIKOFuEmACBgYIDCRg4iNAB5wsUIjhsuHBhgwcQJ17I1LBAAIDv4MP+fzcgYWaICAkKqF/PXr0DDDKfT7dAv759Cx5OuPyAQLx//wNo8FIJELRnoIEKhPCSChvc5+B9G+iX0goLBPDfheIZsMJKLURwwIEgtudACyu94MGDKELowkkrGIDhi+EF8EFKLTgQ4o3sHVBCSixkkOKP9V3AQkkfeAfjkd8BZ1IJ6eHopHrNmXTCBUBWSZ+EIZlgIZJcCkhSCh8+KaaCJLFApZVWDgnSCgRw6aaMI7XAgJh06jjSCz6iaeUFK37koptuCjCSjXTSmcBIJ+qJ5gYfUQDoowiE1EGhlEIQEgqKKoqlRisM8OijJnzUggKUUprCRy80mCmaF3T0wKf+kH5UQamVfnTCqppyZCSsbobaUZO00nlqR4niamUGG33A66MPdFRCsJRW0NELZxprZZ8YNbAsoAZ0NAG0hTrQEabWokmCRp5u6+aGG5EKLp0kbgRCuWh6oJG6gHqp0buFkqlRsfT+yEFGJuDrZrMapcAvndJupGrAP7aKkbIGI4lwRs8u/GTDGuUJcYoSXyRBxUg2sBEGGj85AUfVfoxiRo6SDOMCG02aMo4RcPSwyw4ii5EGMsNoskYh3IzjyhtxwDOKjGK0QtAvlqdRC0bfCN9GIiz9IAgZPQ31hUpmRHXVIEap0a1a3zeCRrt+HZ6vGgFLNnvDavRC2vdhe9H+Am6LR0BHEczdHgMe7ax10wT3Hd7QGyks+HpIc4Q23mtv1LbbcG8k99x1c+Tx0hlot9HIinfrEcqPi/sRuVqf29HlMsP50eY32wmS4RBvIDpHMX9NM0g2k51zSNTyLCRIbUItALsgzVl1AvGGpILLF6AQUqdBB6AvSKMafYC/Ik0f8AWbfqSlzGGLBObNZpfZ8qrVE7mlugFIXVIJYb57wNUnpWrsBnobCfa2FYD0laR74DpA+1BCgs9V6QIk2N1J+sMrAmQOJQUKFgM6txIS4M5BHIhgS/jzqAEYUCUEopQCFuiSF5AABBzw0QUyYJ0RqAkmGqDgiwJAgAcw7yWOIcjgjQ7AgApETy4UWEBuLBSAARiAPD+kSQciYJwPHUABDnjPEWfDxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJSqcEBAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjyxb7ggUKFS50fmiAgMCAAAIIGEAgIWeJCRAYKDiQgIEDCBhsviCx4YKF69gteCCRO+buAQDC/osfH95AcZnHFRRYz779egfRYarYkL1+fQ4nXK5YEIC8f/IDnNdSCxEc4N6B7ikQ30oqeGDfg/d1l9ID/f1n4XgDfMBSBQYi6GF7CpSgEgnWQWgidiSgtAICF7Y4XgAapNQCBB/W2N4BIZz0gggn9oidByatYICLRIr3wEktOGDjkuxVUNILDvoo5QYkrQBekVgKKFIL6jHp5YIh8SjlmECKNCSWaMY4kpJetpljSCSMKacF+YH0AJp4BmCCSBW06ecBKYCkQolzSsnCRx9UiCeWBoRUQod+eukASBwUKieVHhGw6KIafsRApJGK2BEKls6JQkcabLroAB+FAGqk/gp4VGmpY2bQ0ZmqpukRm6+6yRELtM4pIUYrKJprkQh01AKkvTIJAUdxBjtmihpRcGyeHXXQ7J8cRSmtj5hmhOu1Re65Ea/bMhmoRhl8O+ZGApCL5pEbJZBum05m5IK7Y76gkbHyttgAR8zeW+MEGgHLr4+HYmRCwIxulILBkmqkwsI+norRBxAX2ahGJVDM5KQZkYrxiRpf9HDHLhIgschLMqDRviebqEJGK7DsYrIatQCzjc9m9ELNJg5rEcA6j7cAwT9/GMFGhBKdHbxJW6glRvY2jSCYF80qNXbhYsRi1f6pqRGNWh/4ZkYnfJ3dCBtJQDZ5AXSEQdruHcDR/tBuX9cwznMrrSze7T3NUbtuhy1u4OFRsCvh63Uwat91bpRq4C575CrhMntEn9QcfDRu0mZ3hG7Ta3NEM9E3e7Ry1R9/NHHaJH8U7ckihLRA1QF0ClIEWh8gKkje8gvCSJrq7PhIn/4s+Uifu7uBvyJZyfLAJHEJM8IlFU+rB9SPZMKV8mJfUgpd3su9SSNICzdKo28awNUlnQ7qAVyXxILXcnLwN0q7y9UASocS4PVKAalLCQoQ5yP8uMQEY0NTAB6wApekAG1tOkAFWgATFpCAf9nhwAj+55IP8IdIBKAgegoUsw3axAUquA0LXBC+mlCgAQQgQLwCMIABGOABe+aySQcmwAAG2OsAClCAAyqwrtk48YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8ikBAQAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjywb7ggUKEiJGjDihIqeGBggIDAgggIABBBJyhpgAgYGCAwkYOICAgeYLFBwuWNjOnbsHEi9k/v4eAKC8+fPlDSSXuVxBgffw4793UN3lCxHau+vXL8KFyxULBIDegOgNsF5LLURwgHwMyqdAfSndl99+FHYnAksBEqhhgSawpGCDIDqYAkoveFDhifptEN5JKxiw4YvnBaBBSi04EOKN8R0QgkksZIDij9xdwIJJH5AH45HlPXBSCe7h6OR7FZDEwoRAAjnkSCsYiSSSFJTUQpNPPtmBSC9sUOWZFlywIkgtbukmAB+MVGOYdBZQAkgloonmBiIt8KabAXQYUgR10nnAiB6NoKeeI4CkwZ9vIhBSCIXWCYFHL1C56I8X+OcRAZC+GedHDFRa550cKbopmiB4REGo/m8S8FEHptbJAEeZrqqnpxuBCqubgnJUaq10IprRCbrqSQJHJvz6ZgMdpUBsnRNsZGKyZ3rA0QPOAtpRBdMaqlGu2J65JkYudrtlsBnZGG6YxlrEQrlo9qaRAOpueWBGCbwbJoQWIUtvlctqlO+WSm7kb5hRYkTCwARr1OzBR0KrkbQLO1ktRqpCjGKrGT1KMYwGbERpxjg6kJEIHv94YcgjwyipRiejHOKlHLeM4ssYrRDziwts1ILNN0aQEQo6n3iCRj7/rKHFGQ1NNIgbX6RC0hWisJGATg+4L0YLTs0gwBW9gDWFvGLka9fnjarRsGLHhypGIJzdHQccScD2/nkDdIRB3PEpsJHAdlvQ6EZN7w1A0BxJDXgBRo9b+HZpZ4Sv4jN21O/jO25Ut90gc/Tq3rJ6RCvgt+Jqt5Afael05h6BOXXnqZ59uEcfsF0ySCXErfJHZuqsYkgIOB2A2x9BMPUBc3f0go8edzqS6/kG0OVIsvt7wJghzQvxBVpjefnBCcu5+cINizQlveATOb6zAZRPUgnnT3tA+iOVie0GlWO5NqzWoxHcarW9lDxsUxcYwblO4idYEYBdKCFUrRgQr5O84HNVuoAH+oeSDxTvTQJ4wApcUgLl1SkBFWiBfUjAgRNdgAMjuBJMPHikABBAhDIpoZMOwIAU0gQFjicYAQhyQwIVLHAmFFgAAQiArwAMwDg4vEkHIsAABvTrAAqQjg9nw8UuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScqnBAQAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuymZm0m6O4o6u/paW9rLLEsrLGtLrJtczxuc7xvMLQvNDxv77PwNTyxNbyxcrWyNjyzMvYzNryztLb0N3z09/y1uHx19rh2Nnh2uPy3uby4OLo4ujy5eXq5ury6Ort6u3z7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sGq4LECBEcPIAYMUIFzhUaGhAYICCAAAMGGmjA2SLEBAYKEhxI4MDBhBAzVYzYYKG79+8XRP74lqnBQAAA6NOrBxBggYmZIRwcKEC/vv0CByKkcKmC+/f/AG6AwksaDLDegQci8J5LIShw34MPQrBfSi54AOCFF3rgwkorGIDghwcuwFILDkBo4oMRoKRCBhi2+N8F4530gQAg1qjeACukVEICJ/ZonwItlETCBS4W+R0JJ1Fwno1MAiDAByd1MJ+PVBaQQAkjkWDklt6dUJKSTYYZwIIjSVnlmQdM+BELRHLJZYwgfbBkmE0OQFIJU55ZpQIgudCmm1tewEJIKxhIJ50EiNSCg3rqycBHFgLqJgchNXDopRSENEGjnHbQkQqSSjqgRybMeWmTAoCUQp6cVplAR/4chApoBh8hcOqlEnwEQaucYrARCrJK6iVHH9x6aQAelcArpwdsNEKwgILQ0QPGXkqmRhUsy6maF7EILZcXdORhtXQ+0FGJ2upZQUYsfAsonBetYCq5NiK7UQusputjsxhp6S6XI2ykAb2H5qhRCPo2GuRFz/67pbQaSUAwnddehEHCenJLEQgOb+nBRtRO3ORy2GJ8JnYXRdqxixtsZKvITOaq0a4mU+nrRbGuzPJGC8DMZKYaRVAzlZ5eJILOLlKqkaU+10hyRpsO3SPKFh2NNIYibERB0zVWbFEHUveo8UQNXw1g1hptzfWHBmcEdtgmLmwRqGYDOGpGK6yNoP6d98INIZ8ZeVt3dxe8wJGheqcnIkeM+l1fihlxPHh3H3MUcuLoPV2y4/VRfRGwk1twt0Z5Y+6kRy1wTt+rGwludgaGi2t6Ax+h6/gEHNFd97Adla63AG1zlLrjCcitUc5XK11r4jLr6vjNHLlgNoyE0sg1AoryGDYEIOm+8ugfmcA1jiOlEDaQIZ2gM9ojDQxzAFCShHDNB2ApUtnuBmySxBMHAHRJF8PYAYo2Eu/J6gK8M4mc6DUAr40ET/pSwNhCwgL/hIoDg1JJoaplgOCdZFHacoDxTIIC1xlpA0hyCQWsh6j4taQD2nOU/ViCAsm16AIcOEHsYPIAAjApAKEG0NxLKsAAKh3AAZ57CQpGAAIOcGADuhHPDmsigQUQwHoBGIABFqABD84EAxFggPYOoAAHRCAEI5yNGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhMpSpXycpWuvKVsIylLJUSEAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/76AoUIEBw2ZPDgYcSJnB8aICAwIIAAAgYQSMhZYgIEBgoOJGDgAAIGmS9IbLhgobv37xY8/pCYaaLBAADo06tHb2C5zBQTFBSYT7/+fAfXWdbOAL4/+Au/ubTCAgGsZ+B6ArjXUgsRHGDfg/YlkB9KKmzg34XgbaACSw8UeOCH6g3wAUsVOAjhifUpUMJJJHCH4YvejYfSCgiAaON6FKTUAgQo9mhfByWJAOOQ3olw0goG3Khkeg2c1IIDPkZJ3wQjCUnklSCUtAIBS3YJgIIitcCAlGQWMKFHJFyppgUBilSjl13mOBKPZZIJpEcsuLgmkRuG9ACcXgYwYkgV1FnmASt2ZOGeV14QkgkeArrkACGlYKKhUirQUZqMqtlmR29KGidIdGJq50Yv8Ndpox9pICqc/gJ8FIKpdSawEaerXikjR0m+6iWYGkFJa5lnWuRBrmpu0NEKkfqqJAIdtXDpsFFCkNELeiI7pAscUeBsoB11QO2hGaGgrZq7ZtTrt0tqwJGw40oZAkZWnjtklhsJwG6XD3CUQLxkVoDRsfYO6QFH+u6r5AL+AixlBAMXPKSyGym8pAEcOSylAxipKjGGjmpkgsVKErBRChpHyQBGi358YQYbrUDyjZRq1ELKPmp6EQcuY0hxRjLPDCK0NuPco7UXgdDzhRxw1KzQ6jG80bRG1wfxRfUu/R2+Gp0HtYFNbiRf1Q9SedEJWvc3Akehfp2enBqVSjZ9d1r0QtrgcbuR/rdupxfAChyJOzd9B7SQEc94W/CzRkH3DQDGHN08+H0a4Zr22qA6DgDcG8lNdt0XpYr3BS949IHjAgDeUQmTJ2C4RmhfDtK6UAO7EbxVF9ux1hmU/tHIX5sMEspkr9yRClqjINICUAfgbkgRVH3AvB7F/rGRI9GusO0e4e6w7hspLTH2I63gtcJSj9TC2A5fHVLWyILgO0nmo+8k++O6L5Llq6ZrkvaSCgD3RuI9TB0AfCBRgcf2dAHlraQBvhoA51AygWEpAHQm4d+QLkCC+a2ERoAKwANUx5IdGeoAFXjdSrQzJA6MwIMu+QCBSjZCmZSgQSpLYUyygxsLXWADjR4AwQhYgBMNNIAABNCXcQZggAcMyiYhmAADGPAv6SjAARVI1Gy2yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShH2ZSAAAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/76goQIDhsyZNjgAQSJFzhXPFhAYECAAAMMIHiwAmeLChEYKDhwQIEDCBVaxHwxYsMFC+DD/ou3wGHEzBUNBgBYz779egINZraYoKCA/fv47TOY0JL79/EAjifCS+gF4N6BBy7w0nwH5OeggxGopMIGAVY43gUosKSBAAh26F4AFLAUQgIPlpjfAR2cRMJ/FrYIHgkqPWCghzSy94BKFTRo4o73VUDSCyC4KGR4Hpy0AgI1JsmeASe1AAGPUN7nwEgiDGkleSYhqeSWBJj0ZJRgMhASCVdeCeNID2ypJgA3jlQBmHAW4KNHKLBYppAZhkTBjGsqGWJIHegYZ5QpcvQChXdaeUFIK6jX55YBhNRCfYOCeUBHIyRapnkfNfDomvF9NEGlcfKn0Qt2auriBS54tAKf/p8mGYAJHrUgKKlQHpCCRpmqeiWnHHkaq5qhcjQqrnCaihEHvl65aEcEDKtmpB0xgCycl2LkQrNltrqRCdKuSetGKVwb564XkcmtlWdqlGa4W7ap0ZvmgjmnRUGuO2SRG2kJb5JMbvRlvVBOeZEH+g7JAUcG/Ktklxs5QHCUYl6EaMItPquRow7TSK1GlE68Y7YWZYDxqhxx2LHHHJEo8sgYpXoygBzBujKCHN368oMYXTwzgBlwxPHNBwrAUcg7O5jAsj9XuAFH0RKN4AAcWZv0gwpgVGXTAILA0QJSI4gARxFc/SAEGJ3ANYDAZiRB2AcWmxEGZjuobEUvrD0e/gscrQC3ex9w1ELd+ZWQkcl6W/B0Ryr/TXVHLhOedUZb6922RmD/DYDcGpVNeAF3W5S33qy6qvmstX6u60a9cn35RsKGzflGx5od+kUvIP7zBcB9tELjNwfQ3EctRL7zAdpxpALXJ4ikQdgSiBSC2Rh8VDnGA46U+coKjuT5yxGChDDGIPQ+UsMdIzD8SBKLDEHy4ifMgfkkoQ8vAeuT1H69DMAfUuu+EgH9ShK7YS0gfyWpHbIi4D+RLE9VF2jeSjRgs2lFbyUh0Bm2qpcSdTlrBAOMUZ8CwJyX0Atb2WEJd3RnoQ2MwFswQQ/wOjSABowLJvMxXokUMAF0vUQFlCTwAAdMdoHdeGAEfLuJBh5gAAJwKAACSE4DAneTEFTAAQwg0QESYJ0JGG42YAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqHRKQAAAIfkECQMAPwAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9qb/lqsHnq8DlrLLErcPpr8brscftsrLGssDas8rvtLrJtczxt8Xcuc7xvNDxv77PwNTyw83hxNbyxcrWyNLiyNjyydbszMvYzNryzdntztLb0N3z09/y1uHx19rh2Nnh2t/p2uPy3uby4ujy5eXq5ury6Ort6u3z7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AfwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8uG62NGDBQmRowwscLFDB84dXRYYGCAAAEEDCCooKOuDxkmQkifTl06ChkzdUAYEACA9+/gAf4YaBD3+Yjq6NHHeKldQPj34QNUcJuDRPr71UfgYFliAPz/4Q1QwloznIffgdPNoBIE3QHo4HcQoOWDCwhWON0KJ+mwwIMcfrcASDBkwIACBxyQgAMUwNAThRa2iGFJG3YoowEc8QCCAgXkqOOOB2Sg0wwtBhmCgiNBIOORAMyXEQw47ujkjgmkcNMNBgpp4X4hldAgkh0OeBEIBzwp5o4+0uSDfVa2OEJIOvjHpYwBXCTBmHTqyABNMqQpJHYfNfAmkuRRBEKdhBZAgUw+VKlnhSP04JEOW/7JYQDNSZRCmIXWKSVMeS4aJJEc+SnpkRFGxEOTmdJ5QEwoeBrkmv4dETDqkXFGNGiqhJbZ0g6uCunoRi3MimQNESWAK6GrusRCr59yZKSwMpbqEA3HFqoiSypgwKyLHCEA7YwQZVAtoRKwZEMEF2xrIQocGfBthwRA5MC4dd65kgcPcKAuoxy59+6DAkCEKr1PJqvSBPnuiyCsGvn7L4C1OmQswWIanBLCHyiMIEeRPgwfRJhS/CRLIjywgcb4kcCRmx7DF/BDA4ucYwLmPqAByvexu5GsLcMX70MMyOykvSudYAHO6bnAUYw9h4cARBQIveOhLb2ANHp8aiRq0+AFOq3UOm7Kkg9XV/erRjpwHR6xEE0s9AE8vIRm2SZ45DDXA0h0q/7QurYUQ9nSZb0R01x73fbbcb9EdtmNPqo2AAGwHVEKQoMgU6dICx6q2oZHFDXFVMd0JtIjAPeRDnc/TKlFc9LrQOIy5YD0DSJ10LS0FX1+LAWwz/S3xuuN5K3HH2aUgtvIWo7TCgq7YPpIPL9rQKUa7T3mASD0fhPz26Lw/Eg6RC8sAdRzlIKIxpqoAAU0aJ8T5p7G8D2M0C5Qvlk4eDoC7Ss9+2cAuFMLkIQ0gt+8ZHi0WoDk2vKcueHHBDI420tasICOwScAC2hBXXIwgxWgwD4jIAFvZLADnJSgAQQQQIMCMIDxaHA2DrkfDGdIwxra8IY4zKEOd8jDHvrwh1RADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox7bEhAAIfkECQQARQAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qn7LSo6u/paW9q7zXrLLErcPpscftsrLGssDas8rvtLrJtMfptMjqtczxt8Xcuc7xvMLQvNDxvcnfv77PwNTyw83hxNbyxcrWyNLiyNjyzMvYzNryzdblztLbz9zv0N3x0N3z09vn09/y1uHx19rh2Nnh2t/p2uPy3uby4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AiwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8vOK6SGDBQmQoQwsaJFDSI4fWRYYGCAAAEEDCCgYJcIDRMfokufHh0FjZk+IAwIAKC79+8ADP40iEtERgjq6NHXeJmdO/j33wMwb6tjRPr71Ef0YFliAPz/4A1Qwlo0nIffgdKtlxIE7gHoYHcQoEVECwhWKJ0MJ/mAwIMcerfAWRRaKOIKJm3Y4YkIlFWDiCx+kANJEJwoIwDzVRSEDSm88EIQRuXQYov7hURBgzNyOKBEQVigwAEFNNkkAxYMRYR9P4oYQkg+CFCkjAFElCSTTobp5ARB0VBliy9+1MCWM9bIUBAOiCmnkwrw2BMRBp5p4ZUe+UAkmw922VAQCsxpaAEJ2BnTEDOccMIMPxBkpp4sKsjRmoDKGOFCcB56KAOLgvDAqKSCMIRAVFJq4QgeaZnpif4DMGSBp55G6RIPEpCq6wMR4ACEqi0KwRELr854g0JBgEnrnAfs0NIQue6qawQwAFspRzEWe+KmCHGwrKcctESCtNJ6YO2IHBmgLYoKMfDtoQew9AO50l5wroUkbkTAuh0akFAQ73rqrEoz0LsrBvdWyOpGrvLroAAJ2RDwoS+spILBum6QMIJ8avSnw+8JetALExsqwkrjYjyqxhvjt7BGID8IMUIplDxnCisVrPIDCLd8nwkcfRyzd7EiJLHNYuKs0hA7P2Cvz+mhwJF/Q8NHwL9IizmwShrs3AHU6bWQbtXwpZhQAlk3qcCz0dIbQQxgo3fdRguQ/d54Cc2atv6tLOEQgdsuEBE3dcJu5IPd4B2LddbNvjSEqLtWEGkRqcYNdEcN2120QnrbzPdLP7hwAgku8ECQDINHNzdHdSMOAN4LuVtynTwJPngIwHV0OOIBKM5poQEn6tOkYK/ekYlkw/6m7MsysHXtlW88Qu592h2ADxF56+kBHCjqkw5g6yASBWS7+VCSwIepgAXPAxXixhiOhLzDH1oURAopiPCCDUihsLHYJdmXwxCAvbKs4F4roN5IfCBAbRGggGZBHbCMZ5LWvWoBEDwL+PQUgjSpJFtsko9bVtSiEPzGJRoqUgAakEG2OCd66TEBDQoHExYsQGjgCcACfCeXHtRgBYIosE8IRsAbGgABJxloAAEaFoABiIcFs3lIC6NIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjdJloAAACH5BAkDAEAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLBozjhYoSIzyAKIFCRY2cIhogIDAggAACBhBksIujxQgO0KNLh46CxkwRCwYA2M69+3YDFeL+7mjhYbr56SOsu0yxIID3994HhGf7g/z5+9NL9FiZoz38/94RIINaP6CA34HSebBDSjkYAOCD3QWQAlo7gIDghdGpV1IKAkDoIXfzkfWDhRiW+BtJOXT44YrLjVVgiTAqOFKDK9Yo4VgqwKijBz+IhECNQAaQQ1g16GikCiFlAOSSCDzEQwwmsBDDTjOcQAIJJ8zAUAlGGrnfRwQsueSAC7EgwQEFpJmmA1PWNIMFD8Qp5wQrJFRklzqi8JGSYgJpgEI8UICmmoQWQMFMPoQg56JydoDQc3jq+CVH2vUJJJkGxaBAoZwWoECbLvkAJ6OkRuBDQTtEaiQMHaVg6ZL+EBzEw6adcpoATIqSqqsGBcGgqo4gdATBq0AKcBAGtdYqgUsu6OrsAy4QxOWvME6aUZjE1oipQCwMmiynLLQ06rOMRkAQtTqeqFG2QLY4kAPf1soASzeQ66yWQPSALoysaiQDuzXGOhAP3sZbqA0rnWCvricIlOq+GL6wkasAf9gAQTEYXGu4KpGwMKkhCIQDxBgiqZEIFX/Y5EAfaNzpBivl+rGcIQNxJ8kHmpwRnyk/uLJAJrjM6QcJz7woCQ7jjGALE/cM4QIEsSB0oSaspIPRckYLxA9KH6ghRjk4/WCIQPAwNaEIrzQB1qYO1DV+6mYkNoDuCsTA2Z621Kz+0Q0PROLb01mLkYpze7ctEEGfDXNLGszMK0EvAI5eRw0UHt9B8ArtwEs+rG1v2wRxLXl0EnMUtuXcXWwQDwm4fAAPMPnQAbkX6HDQ35Lj4BHhlouAULcGH1C1TCtEUGrfB9EwOgd6elQB6gD8mRAPEnzLQNo0uXBlCCfccKpCuHet+0e8i+37Qh/QSqgCiyN1c9c6e8Sz2D8zxIMJH2xgAqhKGdg1CILriIPEJoDDeQVSJPPA+ERSqZQF4Hxi+UF5SPa1kOTAPSkjm1gqtC8P9GtD5bNUAARmlh8gMFIeiFtJctBAEdbtLC1QlX5YsoBXCagtO8iRjkbwQZak4EefQBoACd2Swwt5oAQw6FFMfuihABAAAkOiSw1aUIISWMgDI0BBdZRokwwsgAAE6FAABmAA8ERxNmhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCES0AAACH5BAkDADsALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmYCLp4F/o4iTrY2MrJKbspSpzpmZtJqt0J+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjY8szL2Mza8tDd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AHcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLLmxjxYkRIThwCFHixAodOE08QEBgQIAAAwwgeECjrg4VITRIn05degkVM2kwGACgu/fv3Q3+MIirAwWH6uirc3jxkoaCAODjgw8wwe0LD+nzVw8xg+UEAfIFCN4AIKyFgn4IVreCSgoI6CB4D6ClQwkJVjgdCifRYMCDHHqnwFkUWijiCSZt2OGJCJR1oIgssjdSgyfGWB9TOeRA0Qos5qhBfyE9EOOPABR4lAsiUACBA0hW8EEND+lwno4iehASDfABeaIARuXwwZFIdtnlkg2pAGWOLnrEgJU/ziiUDBF46WaXEMiwEA5PjmkhBx+1UCWaHQaA0A0kWLBAAxaQcMNMMnD55qJyJiSmnSwu2NGZfMYYYUEdHFDAppwWkEFMObS56KgQMIkQfpCKWIJHAFZ6ogH+BMXQQKe0FpDAoS59MOquDmyAkA2p5gjcRia4+mNzAs1aK60LuFSDory+aWpBjwZrYZkZUWpshzNmsOyyn7IkQrS7fnBQiNZWiOFGJm7L4Yc3aPptrTGwVAG5pB4UXboVrroRd+5yCCsJ84LLErT4enkQqvwiOAJHrQbsIAE7WFBwrRKsVEPCo9pYUMMV4rmRxBz6qezFnDarUg4cLzqtQDqAnKCUGtFA8oNYLoBypwkc3LKbB9UpM3ohcLTnzfENsIMEO3Oa8Uqi/uwABfoOnR+J/yIdYIoEN11AByzpKrUD5ho0gtXprasRAVrLB6/XB9Sr8dgOuHDQimhThy3+RjC2/V23TVvg0gZSY4AQDnlXhwNHLfgNXgsD6XzxrS7lgHC0cSbEcN4PdxSx3xQPdEMCBS8gt0sycAxBCgrhnTd2HfXt93gFeVvrARngClOi5K6+UMyJczAsRzY7HgCyBd3QgaALSJABCzXlQPiuFbyc0AmJw+4RAo7TXpQMRXoZwQeNNgS81R4M31HxWguAPFI11OBxRC9YzQEMIk2gdQAXjIW9zJISCfdudqmxnK1hahsJ2yT2obLo4IDWQoH6REKDBW5LAe8ji+vGxIEAnkR2aApAAdGCIzvdjyU+4hP/3LLBkP3mJSAsGXPgggMVbC4/IVDBBFvSAgZ8LkCLA2BABuEyAxWMYAT44YAHeKMCG+AEBAwgAAEAFAABJIcBJpiNFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKNsSEAAh+QQJBABEACwAAAAAyADIAIcpJmQ1M21CP3VOTX9PVoJTW4VWX4lbWYhbZIxeao5jbpJmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKyUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCJCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy048hMeMFCdIjCDBgsUMHjh7mJhwYICAAAIYMJhgwi4PFyNCSJ9OXXqMIDNNOAgAoLv3790j/tiIy+NE9fPnXWB3aWIA+PfvHYxnO4QF+vvnY7DswQC+//cRrPUDCfgVSN0JQ6TUggD/NfjdAD2glUN0BlYYwgg/nOQBdw52CEAALZjFA4UWVjjCeiOZwKGHHQYwX1E7yPDCCzf4cNEPJJZYIQkktbAiix0KQJQPKGxAwZFIWlCCjRINYZ6OOqYgUg/uAQnkAUEJUYIFSHbZZQkSzQDlmDmENIGVaHrwkw8ceOkmkhsw2VAQOY5pIkg2/Ihmiz75gMGbgFJgwQ4OxWDnmDh8FMGeaGrAkxBtBgooBnPWeaiBI3iUJ6NWBkAQEDWAAAIMM6EgqaQiMETDpWOiqFEF/pyiOR4QGTSwwK23ZgCTD1yeGqicCNnHqo4zdNRfrEBOUAMEuDa7AAQ1uGSqr4GmmtAQlg6LX6Yb9aAnsg0GIIGzzj7g0p/UAmqBQjxoC2WCGpkArpUKkOusrivtkK6kwBaEg7s6ZqiRBvMCiYC9zgKx0g37BvpCQqsCbCFwrxbMogEIN0uqSi80DOgKCYkpcYWJanSmxR0SkDGuIKzUscduopCQoSMbWKZGi6LsYAEr39rBSjLA7KYMIdds80Yn6/wfzz2rsJIQQntJNEI5GF2gqxd5oHSDCfS8gA4soRu1BUIk1K7V92Ftkbxb+9f1yviuVELUR36g0BBoo3cC/kc9tA3fAA+s/IDCLPlANwU3LJRC3tXpx9EBfoMXQQ2B2wuB0y6JELXdC/3L+HQUb0Rw5N41R0QG5F5AuEtCGOkxBmUvhPfnF3rUN+kfFqQCCB3AsPquvaZrQeINuUB7sR45gPsEPu3guq8bEN/Q7HmTAG9Ht/stQIQ+CaH5qRzE/pDxeZf8kfJ+OxrUDiIEn+QHhDZJoNUsTMng1gwYJYMMKKzwwg7im0gQrIagkdhgaxAiy9kkhqGSsM1iIDJL1QA2gpuVRGsFC4CazoIjbZ1AbSPxEbgG8KKzOIlVLLgeSqgUKwZwby05mF+UBNYSD9zvSiGCCw0WhykWhA4mjxWAnIMCwADT0SUHMUjB/EZwAhbEgAcqpIkHInCA+wVgAAyIgAleOJsuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSliUgACH5BAkDAEcALAAAAADIAMgAhykmZCwrZzQ1bTUzbUI/dUtRgE5Nf09SgU9WglNbhVRZhlZfiVtZiFtkjF5qjmNukmZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJSpzpmZtJqt0J+y0qOrv6WlvaW31au817KyxrLA2rXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AI8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLbmzkhw0WKk6UOPHihY0fOIGkwMDAAIEBBCJEwJDC7o8YJUZIn05dOg0iM1NMGACgu/fv3S3+5Ij7Q0X18+dfYHeZwgD49+8jjGdr5AX6++dpsAQSAb7/9xasJcQJ+BVInQpGpAQDAf81+J0BQKD1Q3QGVjhCCUKclAJ3DnYIwAAwDFVEDS6ggMIKMhSx0YQWtljCeiNt6OGMA8zXUxEufLBBBjz2mAEIMmBEBIUtWngCSTlwOKOHBPikwwc+RtkjB0FSZIR5RRbJgkhAuLfkkgzsVAQKUpbZIwoU2ZDlmjuEhMGXcHqgkwhm1vmjREOumWUJICUJ55cD5ESmnXWSEBENerL5kQV/xnlTDYQSWsNDRhCZqIV8dgSEko16GGhNRUAZaZ0bPHTDpWvCqJEGncKZwxD+M8wgkwyjEuqCQ/ahWqQNHfXX6pIKSADBsBfg8BIItdpZKkOV6lpkphpt+uuSAQxrLQQmtFREsoQGwdAPzmaZoEYpTPvlA9cOa+xKtHJb560L7RBukRlq5IG5SzaQLgQUsOSCu3WuwNCp81oI3Kr4zrjAvhAMsRIJAJtp6EJqFlzhDRu9mXCHBTAsq0qDRhzlxAoharGBbWrE6MYOIsBwCyutILKUaFJ8soE8ZMyygwkw3MNKkM7sY5UK8XBzgapeFMLODTqwbwUsbSt0j94uBO7R9yVtUblM++d0uh+vxMHUGXzQkBFYo6cCR0B0DZ8A6UrQgUv/Ti1wQyykXZ3+fhwx4DZ4B1xbAcwvjT3zBio2JK/e0+XM0b1/exfCES3M4HBMQYsM79mMSwftRm1H/iFOEEdc80MxdM6rRxOIjkFOyLoLQuKUMn7CuJpGTkCEgnKLAu0QpZ42xiC17rYGPLlguLKbS3Ql1izg7lGXXTPAe/KiSvmBC8BPRMTRt4+UA9O7BxWEDCuQQAIKLviw4skYlsR1wiCaZfS8JThe0tL4DjD5WUKwVKJUoLWRwIBTjTKAjc5ihLxd6gXSOwkQ/NapCFxPLTwgUJZYUK+WhIBBX2JAiOByAwcWqAQvOFhMNEDBBg0gAs2pCw9owAINlkAFL6DBDyI4kxBYgAFzIByAASJggRRccDZITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQBEtAAAAh+QQJAwBCACwAAAAAyADIAIcpJmQzMWw0NW01M205OXA8PnRAQ3ZCP3VESHlHTX1OTX9TW4VbWYhmc5VoZpFve5t1c5mBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHY2eHa3+na4/Lc3eTe5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCFCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy47cIwaLEyVCiDihgkWNnCsmQGCgYMABBg4gaLD7I0aJD9CjS4eu4rfMGRMUANjOvft2B8vh/gZ5EWK6+ekirLfcEWGA9/feD4RnW6P8+fvSS/RgqcE9/P/dKbCCWuPhZ+B0N6TEHoAMetdBUDm0kMIIIJjQQg4fBaHCgRxGF8NJOzjQ4IjcTdATECZsUMGKLK6YwQg8bKRhhzR+IENJIZKoIwAW6ATECBe0KGSLI2jEQo01JjgSBDvu+OBNPHAw5JQsbhCjRTIgWWMI+4VkQZM7DjBgTTlkQOWZFVxAQ0U/2KdlhyWENIN/YJKoQE08BInmmRdcKdGRbyYJEpN1OjkTEFLuiWYGE+kQKJIifERCoU0eMJMJiipqgkQbPlqjehuJSOmO87kEhJ6Z8ulnQ0G46SmH/ix0tAOdo44IQUyYpronCBDd8OqWHXVQ644BxGSmrmheANELvwq6UQTD7rgABi7lgKyiqy70XLMdfriRdtGSmEADD/jAUgvX7pnCQyJw22GsGx0QLokGNNAABSylkC6amzrkbocqcDQviQXY24C5KoGw75keOPTDvxzGqdEMA48ogMEurDTCwlTyyirEB0qc0Q4VN3ixvSislCvHQhbpL8j4nSBwyQASYDAOK8HA8pAtPOQqzNK9wBGtNHeHgL34rgTEzkKu6dC2QE8n9LdFw3d0uS0dy7SyDwEatXRKakRo1d0tIAHOLW3MdAUuO+Tr19GFEARHwpLNXQA2wLT0/tYYPhQE3NEFzBHJdn8n08oct9014B+EvdHYZD+pt4ocZwCERD0ALsLcHa1Q+AE7zJTnwhf0zSncoHIkatWlxkTDvhfAUNHfUcsMEuFFM3BTDpSnuoHpFDELcwg6iAQtzQOQkJO+il5gwuUYdQpx6h+tPnDrNv2otZAXwCgj1NxOPdIO4M4bwU80tGACCCCMkALw37v7AuckkW9+6GdJ/2gI1JNkfaEDwF5ZsvSoEjgOJV+ilAIkRyCv0SgEMqDfSnYAORINwAL4c0sPyMOhE0RQJitoz4gYgMG66CAGJzhBu3RTAhXEoEs2IcEEGMAAeRlHAQ6YwJhmw8Me+vCHYkAMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIrwQEACH5BAkEADMALAAAAADIAMgAhykmZDUzbUI/dUNCd0REeE5Nf1dVhVtZiGJhjmhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLpvyiRIgOGy5g2PAhRIqcIBgoOFAggIACCRREsCujxIYK0KNLh/6hxMwWDAoA2M69+/YEDOL+yhhxYbr56Rd+u2yxIID3994DLG+bovz5+9I3vGAZwT38/90VAMJPMZAgggcciIBCDCONh9+D06mQEnsAVugdBTvFwIEEDzTg4YcNTMCBCx/J8AGEKEZnnUktJGDhi9yFd1MMGXQI4o0fZtCRiSn2WMGKI7UI45AAyEjTChDgqOSHD6Cw0Qg++ijhSAsQSSSGM4lg45JcepCRCVFGuV9IDlhp5YAxecDlmh+KcBEM9oWZIgYhneCfmTAKENMKW7LJpZMVQSmnj+p5VCWeRM7XUgwS+OnnAyROBOegPl7wkZ2IEhnASxw46qgFFIVAaZSFbqRAplYqqlIMfXrKZaT+EWEwqo8feCQAqkQm0JKarvrJgUQqzFppRxTgqmlLE/T6qEQlCEsoRwwYm+hKMSjrKKwOdeBsj0BmdIC0QxqJEgrW+klCrNumGAJHt4L7ogIriVAum15ClG6KtW7kLowIrMTrvEv++hAM96K4wUYn7PviACt1CvCSOj4kQ8EQHqxRCwpbSMBKJDy8pJv2UoxfBxxlXKEBHHus5LkQxSnydOtudKfJ3sG7qso4YtvQcy+bNwJH2tH83gIsJYmzhxJIJGjP0k2p0aFCd4elSg4fHTFEwTId3QUycFRs1NwF0AJL1R79wAoTudxzvhzNLLSuLVXt8dURiap1BaVqdCr+2ACouqrRDz/AoKR3Y9B1RwmDLcDYLpEbOMuhap23qWD7zVLHANdb0cQ9d3C4RxgLfQDjMP3b6wOaWwSmyBewIFKZJgegwZGtsvkA5BidSLEJJLmYsQM3yc3lAxnojBHP6f5cUtDuEo2TCxsuKQEHaHskA/LCjvB5kMwbuwDpORUoAgcZeIBC9SItTekFvKcEdaYBAK/W6oNi4HRKsCMqwNRqyWB3pSbYnkpasDdNOQB8bHkBeVDUgRIIsCUgaM+LDsAABMKFBSXoQAdkpRvelGBMNtEAAw5wgFsZBzkMQNNsVsjCFrrwhTCMoQxnSMMa2vCGOMyhDnfIwx768IdLQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOfIlYAAACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLvpxChAcNGChU0MABxImcFxAYIDAggAACBQ5AsJsCRAUJ0KNLh87BxMwLBwQA2M69+/YCDeL+sgAxvXx5DNZddjjgvX37AeF9ukARosSKlC9AUDDPfzqHFyupcEAA7hXoXQEq5ORCCRYskMCDECZgQQgkvcBBfxhKRwELKalQgIEgdhdABza58AEDEaYI4QIfhMTCcxnGCN1vJnWgXYg4brfcTCU4oOKPEDpQgkct7CfjkSmURAKBOTZ5gUwbOAjklAlswNELGhyp5YYjqUBAk2COCFMGVJaZQAYbeaDlmhUAGJIBYMYpQIItbWCmmS1idMKafHoQEgRxBmpASyhIeSeVQ16EAZ98tgDSAIEGSsJKLvh4aJkLXDQCo3xy8JECkQZawEp2XoqnRTByqqWjHd0YKpj+k6LkAoqmYloRC6ryOUJHHbwaqAIphVDrnXlKJEKua2rQEQK+xklAShEMa2YEFGWJ7KocfdksrCjRKu2UmU507ZpJbrRtnE+atMK3ZrogUQvjarmrRiScCyawJqHAbpmJQoRrvDKKsFGv9uaIwEkl7EtlsQ+lALCMfmp0QcE5DmqSsAoDSWFEez6cIQgbAUpxiAectG7GP/b70L8e9weyRgSPbGDJJrmA8o/3RfRCyximl5EKMoMYn0mG3pxAuDrz3B+NPwdt4I4mTWA0hNROlKrS0rGqkatOdxfrxVM/uLFE5GEtHQYdsdd1dwOkNOvUC+Qs0c5mRycwR0Cvzd3+wSl9MDWaFV2NtdYbcd31193ezIC7FZlQtwSeetSA3gCMulLCKI8duNlcfmT4yGKy5LfCFmTUsdIvfySy0zS3ZMG+E2xkbcttiqStzHOO+S3gGr2wqMcUlBuSCpCCni5MJXh7J4tEGgkw0yItOTLUUC6/AeMeseA8shTMW1IHTG4bAL42fSC1igs8cP1Ivl9LAfQlES8+9TitEEIIH5SAAvYlqamqBoRDCZxeRQDEpYUF/tMSel7SgQGCCT5xSUEC+0MBDYzATTC5gAMNFAACKIBOczkBCDSggf1QAAMcqA4GawKBAxCAAAQKwAAKAB4QzuaGOMyhDnfIwx768IdbQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj1gJCAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy878YgQIDRgoUMDAwcOIFzhVKDhAYECAAAMKGFCgou4LERgkSJ9OXboGETNVIBgAoLv3790J/iCI+5xC9fPnQbzUHgC8e/cHdLoIYeGBgwUJGDyI8CFligroBVgdBSewdIEA7yUIXgAQ2FRCBPglIOGEFEaAQkkjmCfghtONoJIC7SkooncKzISCBRSmqGICE6wQ0gsecCjjdBycpIIBI+boXQExlRDhikBKGAJIMc5oZI0l4ajjkjy6tMGPQQaZgUcjGGmlBB6OpMCSXAJQIksbRCnmhFNuZIKGV85YYEgNhNiljg2q9AGUYwY5ZEYvAJimkRSEpAKCby4ZgEor0FlnkC5iJMKeV2L3EQKBdjkeShMcemgEGL2AJqMyUtCCRyq4GWmOAZBwUgmWWlrCRYtyamWW/hxBOiqXX5YUQaqHOnCRBq5a2WdHBMzK5aAluWAorkAmOhELvV756UYdCNulqSSFgOyhG1RUZbNGwprRltIuWatIt147JqYUFcmtjEhqpGS4OTY50gPmjslARRysO6MGHBUAr44ElMRAvWIuUJGe+m74q0aA/isisSMRPKYLFCGccIALZ9SwwwlCLJLEYlI80aYXo8eRqBy/V9KxIOdXUXQlB1gBR9ylnKAAJTnQ8oq6UsRrzOjxu1GwNr8XMEko7pwiuhOpCzR1HnD0btHfGVDSB0qnWOZE2z5NnaMagUv1d5OO5ELWFCor0QteV/esRiqMDR61JJWbdc8Hty0d/gYebUz1ACehgHYCq1oEgt4SgL3RAXJ3V7atWT+Qqd6egtp4qSitoPQChbPatuKxyv24Sda2nC2eFl9MAXAf/Ul1AM2pVKnEFnCUwtMmiHQB1Q20lLS5Foi80eElqzcS4ynH51LpuC5wukf5JuwB6yP567ABsbuEwuyHPnBhSNFzqwH1JFkfLgHZw1QCvWI+cOdIrfYKAvklySrsAenL5MIGDwws4QIOeEAGvmeSE7iKArlbCQRmFYDe9UR4LOkan37zErEJijlweU7q0IMBEbxtPQjw23sGgAC6ySUFI+CABgBEgQrwRgQswMkFFFAAAiAoAAJIDgI6MJse+vCHY0AMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQmYlIAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy978wgQIDRgoUKjAwYOIFjhVNDhAYECAAAIKGEBAou6LEbklSJ9OXYIGEMBjqlBgHID37+AB/hA40DynixIfNliYkOFDiZa1K1SfPx/ES+ECwuvXf8CmixARLJDAgAQSGMEHKaWAAX0MzmcCSxcMsN+E+jUg038MFKihhgsgWNIIFDQoInUiqKRAABSmCB4CMKHgwIYwaugACiOBMOKN02mA0gEq9vgdAS6VIGCMRBIYQkgi4KikdSYh4OOT4rG0wZBFVulhRyMsuaQHJCkAJZQGqPQBlVVWeSRHLISopZIPhtQBil8+aeFJKJBZZpU0bqTBmktSkN1HBMQJZQDllfTAnYgmsMBGJ/CpJZcfQSDol2GW9EGiiV5p0QsLOrrknxupIOGkUBYakgsvYnrnohg16umW/h5JSiqYJIWgaqIbYMTBq316VMCsg5IUwa2IRnDRC2ryimMKHKkAJ7A+XiCSC3YSS6QLFrmqLI72bSQrtD72F1IJ1iJ6JkVJboujjhs5Ca6PQIZ0abll5lqRB+riSAFHBrzrYwAibUBvmRlYtGu+I+670a/+qghwSBMMXKWxFcmH8IgvbJRfwyqqEJIFEhc5gUWdXtwgqBeNyjGFpnaUQchEWmDRniY3yFGgK1Mo0rwwb1hwRTbWTF8FHPGY834CiGRrzxueO1HQQlfHrkZGHx1evCC5wPSGK2QbdX0cfWv1d+JCvLWBx35d3QnNjh0eBCPxvPXPBqstHQUZc8Sw/tsBeDxthlsvgO1FJtgtAQceNeC2dwVYejbdm1r8NbMdqbDx2NKWBDjMDAyOUeFfQ5q425WWhELPCziNkeQm+xnS5SsTihK5IWuaUQpRjyDSBVYroJLAA8vskbYIdyuS2P6WnRLtxHYYEr4IY5D3SP02PIDfK6GweaIMqO4R1MpqMD1JVUNLAPYtTYnoAht4LlKWyoIwfkleQnsA+i9tcGiMCzzQPkopYJ2WKKC7lVwAdl8KgO9u8oEMZGACEbDABkLgvpTAr0/YeUn9BkWeuEDnRhUAAQtmwp0eCeAAHXDOCDygAfnsBgMcEMEIb7IdAxAgP8gZQAEQkMLZ+PCHZUAMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrSKgEBACH5BAkDAC8ALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31bKyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AF8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL7uxCxIcMFyZMuLChgwgXOFMoOEBgQIAAAwoYUJDCZwsVKmi6CHEhgvXr2K1nCDEzBYIBAML+ix8fngCCmy1IYHCwIIF79wwqkGg5fUL2+/e5u/QegLx//+fJ1EIF7b1n4IEVqIQCBfg1mB0FJrBkgQD/VUieAA+81AIGBR7ooYEJmiSCfQ6WeJ0IKinQn4UsiqdASy1A8OGMBzoQnUgudGDijtdtcFIKBrQopHgFrHQCAzQm+d4CJ4ikI49Q+lhSkENWWSRKJ3SopJJNfiQClGBGUAJJClRpJgANnNQCklu2ucCNHJVAYpg8RhhSAyueOWSGJMXY5p8JONCRCwzSCeUEIaVAoZ5VBlASBoACigFHIRgapn4eIcDomQGG1IKWkSb5pkYuzGnpjhOw4FEKeW4qZAD+I4gEaah/SqBRpaeCiWJHmrpq5oshsUnrlgtolEGuYCLaEQG+mukoSCAMC6gHGK2AbJiqbsRBs2fG+tGs0m5p60VfXgvlrhqVyW2VwHokbLiiYvSkuTtKqRGV6wp5pbvwtonRBvTymAFHBeQ7JAEg9dtmCxcVGnCJymq0qMEsPtvRCQpv2WVFDj/cYMQZTUxxhRZzpELGSs5nkake48dRqyP/91ELKCcJJ0XVtdwgBRyBF3OFAoAEas3vYXSszvgNvBGzP/+H8EcOEO0hA/IijV8HHOHb9HgGgFSB1AdCgFG5VmOHaUbqbj1epx1FC/Z7IGDkQtnZZatRCmqT563+R5++ncACDGPUsdUXeCTy1gOIJIHfIWL0Ad3Wna3RAXmHxzbfb4+a0dx0p7pq5bCOBG7Nk26Eq9WSb9Tr1pfPHHXNDgRO6uAPTwDcR4puHUBzfQ4tLZMeoWD1mCFZsHWaJpGQ8QJxf/R4yx+QRHnMB6SUJbzMiwTww1iXVDDFXaukwuu02kjS9uZucLv3BhfA+0oahLoABrKPdHquIaxv0uq+IvA+Sy1YHLEgcLOSmCBXEyCeSh7gqwAgTyYgkMDQFgABD9QvJWQ71G9ekrZGMScnKiDBCVRwQfqEgHb3uUAI7PYS7xzuPwNAwN7kggIRbCADDJoABXgTghXgxAJ1CigAASgUAAEkBwEcmI0Sl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkqxIQACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL9vwihQgPGDBQqKCBA4gUOFVcQGBgwIAAAggUOHBBp4sQHzZYmJDhQ4mZKUBQkMC9u3fuHEz+zLxwIACA8+jTny/QgKaLDQ4WJJhPn36EDStasuDwvX9/DCy81EEB6hVY4AAdwOSCBfLV56CDFqj0gnb+VfgdBy+spEJ5BnaoXgEqtPQBAw+W+OAGJ73An4UsdldBgCipQKCHNKInQIIpLWjijhCWxEIFLQbZ3QkndSBAjUiiBwFKLkTA45P0OeCCSC1sJ+SVwJFEgnlJdtlcSU1CKWYCD4T0AgZXpklBCyOpMECXcAZAQkkWjDlmBCCtmOaVFYw0I5xdCkDSBnba+YFHJ+ypqAghQQDoowiI5EKDhUK5QH4bnanonmt+5OajgMoZUgaV2jkBRyNsqqgHHykA6qP+BoC0AqWlQolpRkCquiebHR35KqBzekRorWOimBELuio6Qkcd/PqoAh85SayYDGgkQrJ7ctARAs4CWoBHk0475q0W5YqtkBRkuJGv3SYZQIgclSBusRi9cO6eWWakQruAfrnRsPM+aaxFyN57pXgaNctvl+1xBHDAO2aAUQoGX7msRhcs3CW0HE0A8ZN4XmRCxUIyqlEDGicZKUd1frzjqRclSnKLIGzkaMo1HtARqS6bGGHMM7eIcEY34+xhwxt90LOJEl/UQtAsEqkRCUbTuCRHISxd4qEX2Qt1hflitG/VHfqrkQtaP0huReZ+3V2n65JdoKgdOZB2lBqB4Pb+hR0dIPeHHz2sddMT7+2dyRtl/Dd6K3eE9t0JoLBR227zyhG7fwfrkcdp/6xRqoaz6pGri8cK0uNLX8rRC5TPTAGMHamAudEB4AiS0ksTvtHIbiPuEcpyNx6StB/D7JEGX2OgLkgEkD0AvCPZDbGUIGnqOuwgfYpz7Sa5IL24Dqzd0dMkUyD1SFSnHMDVJxFf6wNTjsSCledSMDRJHXDZbgBIoyT4mAvYQPxIYr1kmS8l2nPW+lriAs6JaQEREB9JPJAsDVguJQZwFgE01xIUZOB7JXJABiTHEhYgb08aOB9LOtA8QBGAfTJxwQcyYIEIRMACGdgACWFyAgq2iAKZGhjB8l4CgQzWKAAEUAD05HICEGhANxKgAAY44AETDJEmEDgAAY4DgAAMoAAGaMASZ0PGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKpwQEACH5BAkEADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLJutiJ4sRHjRgoEABAwcPI17g7KDAAIEBAQIMKGBAgYqbKDI8cLBgQYIFDiJkQCHzhQgMEsL+ix8fnoOImSoQDADAvr179gUQyHSRwUGC+/jz32dgweULEBSQJyB5FJjwkgoHBPDegu8F0IBLLmxgnX4U6rfBSiZUMOCG5GGQAksNCMDgiO8NcMFKKNhX4Yr5McCdSf9xKCN5I6SEIIk4vqdASiVMyOKP94VQ0gsczGikeCCcpEIBOTbZ3gEnSQjklPd9MBKRR2YpgQclLenklwAYUNIHPlIJpJAhgaCllgaOdACYYD4oEgplmvnjAi96NMKaa34YkgJwwnkiSC6oaCeVC3z0QoB8ZllBSCooGOiXAoS0waGHWtmRCI2u2aZHCEwKp5wduVAnpnfWtlELjHZ6JAX+H5EgqahOBvBRBqgeOgFHnLqqZY0dhUormDt2xECudia6kYa+ZsmBRyIO+2UBHaGA7KEraMRCs2sKt1EH0sL53EaXXmvmhRn1yu2Rn2YkbLhOkppRBOaa+YBGRa575HkbMQmvk/Jt9EC9VDKgEbP6zvjsRtH+myO1Gx1LMJDKYoRwwhxqwFHDDpNIAEcTU6nqRRgbCetGHTdp60YhT5ntRS+UPOOjGqmQco6VbnRqy/lp1KrMAmq80aw3L/jxRobyrJ8DGoEH9IBcbrRe0QyKKbDSFd6bkQZPD5jkRgRQzSCUG+GKtX4ZaKRm1+SdwNGbYr8HAUcunK1fCdqyTV7+CxyBG7d7JBhr934cXdz1whv/DZ9HZtud9kZr683v24oDEDDdEmO9wMgZxaw3Bd5yZPPfAYzb0QeOe+SB5CAZ8PflHmXeMgOca+T50xWE3tHoVAtgukfW8rwA3h+Z8DQFbofUANUBzC1SCTxrCtLqMgMrkus3FztSuQT3NxLXGH9NUtgdk10S9OYuIL1IL4C/Lgi6i6QC+fAe8DtJKMh+6AJomhS5qxSw3kngNqwAaC8l3DPTAjZQu5LsCYDJUwmgaNW8l2RgZ/pZQAZexhLqaSmA8VMJ9sBkwPu1pATSyRx2HLAdmbAABIYbEAZEEEKWdOAAHBvRABBgQpo0kCaGKRCBBjSgIQpUwDciYAFOLoAAAhBARAEQwHIQ0IHZWPGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOelIqAQEAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIaGaRdXOZgX+jjYyslKnOmZm0mq3Qn7LSpaW9pbfVq7zXsrLGssDatczxt8Xcuc7xvNDxvcnfv77PwNTyw83hxNbyyNLiyNjyzMvYzNryzdbl0N3z09vn09/y1uHx2Nnh2t/p2uPy3uby4OTs4ujy5eXq5env5ury6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sO62IFihAfSqzI+WIECA0YKFCowMHDiBY4VSg4QGBAgAACChhQQKJmiQwOEmjfrv2BBRQyX/6ICC6hvPnzEjSImKkCgXMA8OPLB0AAQcwS2bnr587gg0vxFKAnoIAgvNReAPMlmOABLeG334P6OeCfSilUMOCF6FFwAksXCKDgh/MFAEFKLlgA4Yn6WZDSCAFi6KJ566WkAIIg1hiffSa5EAGKPG73gEkvgPDikOZpcJIKB9ioZHwElOTCAz1GmYADLpAkJJFYGllSkkt22eRIO0oZ5Y8ijYDlmRKMQJICXbYJgAIiZSCmmBOEdEKLaBK5YUgQ0OjmkiN+FMKcc4bw0QsY5HkmBSGpMMCfbQYAEpSESsnAR2YqemaMHrEJaZs4cjRopWJOuNELeGo6JAXIdaSCn/6fKhlAdRzlR2qUC3SUqapYcrqRp7F2GWpGKNw6J3gbacDroh4REGykHMlprJQqavTCsmi2qpEKz7pJa0aUTttjrhrtiu2Qav7abZtwaiSumFVm5MG5WHLAkQHrdlmARsW+GyWyGHFAL5EYcFRAvksOoFEJ/kZZgkaJDvwioxs9irCNkmb0QcM9bqCRhRK/yJGHF9uo0agcn2jqRamGPCBHsJas4MIpo2hoRhG7PGAFHFkss4ICaLRCzScCfJGyOg9Y8EbO/qygwhm5QDSEu2V0ZdLoaakRl07P92VGDEwd4UYmYC2grxg10HWCw15kotjbVZvRtWaflwJH3K4t3/4FGw0Nt3Y3f1x3eUuPrDd8UG8UNtwLxKuRCINLUGBHCBwOAIMcbQx3Bh3RbTYFLHiU99oBdODR4kQz4PhGkJuN9kaVr932yWKvfCrIOlPwAkgqkPxzACqANAHRcnuUAtbpgnRB1+1OmjKVZeo8uUjAXox5SC7YKq7qJAksMQe7k3TwxQUEP9KT7z5QdfcDaxB+SeOvS4D5Jb19qwWrk9T6siC8b1LszzoA/UwSAtSJSUIrqZCqKGACDvnuTwFoQEs2YEAULWAD+VtRniggAv+tpHpLCgACBsiSEGSggttxwHcyuBLx5AxDGBCBtgzkHiUNAAHfkokLbvOBDZQABYksjAkLRsABDVhoOBjggAhCd5MOKKAABPAQdAZQAASYbjZYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOghEpAAAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjywbrIsSHDRYmZPhQwgVOFiI8aMBAoQIGDh5MvMDZAYEBAgMCCBhQwEADFTNRbHCwIIH3798j/mRYIfOFCAwS0qtfn56DiJkqEAwAQL++ffoFELxcYaE7+P//TUBeSy+AQAF7CLJHgQkvqXBAAPdFeF8ADazkQn8AZgigBSyZcGCCIK6HAQssNQChhCjaN0AHKK3wgIYw/ufAgCYVGOKN7I2QkoMp9nifAiaVwECMRH63QAk1coDjkuq9Z5IKBfgoZX36jYSCf0VmieRILyjJ5JcglATllGQCcIBIK2CZZZELoDASCF/GKQGDIx1QZpkVfuTCi2v2yYBII8gpJ4khKXDnnSx6lEGfjCYwAUgvfCgokxWEpMKJh04pgEdpNsoojRyJMKmcdHqEQKZ35rnRBJ4yGoFH/i1IOuqSFHxEAqaoShkARyi02iioGcE5a5ylbmRnrnhutIGvjG7QUQXDxsmBRwIgW2YBG0XAbJ8LcMRCtHFSsNxGHVhbZgDYYeTCtp9uJCq4XxaL0anmkqmqRSWw2+cHG3kJ75JOahRlvVNWedEH+q7prEbQ/rvktBtVS7CU2GK0bMJFZrBRww7fqAFHEk/cIwEZsYoxka9q1PGSlW4kspSbYmTByUQ+mtELK+OIwUYqvOzjABktSjOMHGoka87sfbwRrj7fRzJGCA+t4cIZoYd0gh5wNF/TEhqQUQhSaxjCRhpcnWCYGxHAtYRnqht2hsBaJKzZ653A0bFr2weB/kZDvv2dAxylQPd6FLTA0QV52xcACRoJ7XcCGnPEMd0Qg5w4fhut+3gCW27kweDpBbyRAZcDYHBG2vqdMkctgF7BuByRcLkA6Wqk+dudc/Q53Tp+RHreQHZkstQ2e4Sz2a+D1PPatHvkggNSM+AbSCZcTYHdITXAdQB7f9TpyW2OtPvKvYv0+8vBg5QvxguMTZLVDqNN0tYTty3S+uy2X2PZ/4IA+0gqUBvBDlA7kaAAesxygJtQMrdZUaB8J8FbrgKQvpK4YHiMWkAEppeSQDkQeyox1AS7lxIUYLBID1ggS14wvi898H8qUcH5yETBAqoEBRlAoIYckAEVvoQFpSCYHIg0IAIYtqQDBwhZigiAABu6pDYZyMAEImCBDXzAhzRJgQg0oAFoFec4IiCUTS6AAAIQoFrSoQ4CEjWbNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJyla68pWwjKUsZ0nLWjolIAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/7qosQGCxMcRIhgIUMJnC9SiOCAoQKFCho4gEiBU8UFBAUGCAgggECBAxdk1p6wIIH37+AT/kz4MDOFBwoS0qtfn96DiZkXDAQAQL++ffoGGrgs4SC8//AOkOdSChqwZ6CBGrTw0gUE3OeggwSQoBIKEfxnIYAorPQCCOgd6OF6HrCkwgHzPWiifQagVEJ3F7b4XQgpsYDBhzSuh4GCKHUwwIk82jeAhCRtwKKLRG5wUgod1qgkBcyZdEGJPUYZQHYibUDkld8JOBILSSrpJQsldQBllGR2ENKKWKYJo0gvVODlm+lR8MJIKghA5p30BaDCRygMmSaRC2QI0gscwGkoBiKpUACejA7w0QN//ulASCMYaul7ICnA6Kb6cRRCpJGu2dELXVq6JEgqjLmplB1BCmqa/gt8JIKplorwEQKrborARp+++qeWGpFKq6EUeJRqrowGsFGFvqYZQUcnDHtpRxAgy6lGDDQLa0eFSgtniBwtai2eKWLUq7ZYAouRm96+WSxHdo57p7IYWYkulhls1EK7hoKpEQnyMmrmRRPci+UEG6XAL5xNZnRBwHhSaRGzBrv4rEYmLPwmphk1APGdnVrkasUtTqpRpRorOcJGmn4cpQIYjUyyhSZnNGvKNdqqEa4u97grwTO3+MBG0eJMI8cYVdszjyFXZEHQFyKskcJGf3jCRg8vfSIEGGUAtYUWbPRC1R/imJEKWp8IpEXnfg2eqBmVSrYEcnKkatoA6JmR/p9uL+ACRx7MzR4HHRmA930FaFSw299dTLTg613NkdKH08d1Rm27bSRHY0NO95wcoV153ntiy3gCDPzdUeCQ61z46D9rlHnQm48qd8p1G3u3y3pz1N/XDqjuEcpku+5Ry2nHvtEKXy/wW0gzVo0B6CDtqPUApXdUAtTqeiQszhSYjeru4wawtkf2Vhw2SSyAL/lIHfQcwOUheW2wBcKPRDW/FLxPUtYBm19JtqetBXRvS+ySFgb8dZIOxMtaAxhYSVCQrVc9AG4neQHraKUB6qFEBYZDFgGyd5IPVBBLC9hA/lTCggS+SQMNY4kDN0UAia1kAxSz0AIeoEKZnKBbpDSigAZWJhMIiItHASAAzGQSggxM4AEM0A1vPrDCmZwABBpgFwUwwAEPnMCDNIHAAQgQrwAMoAAGgAAJZ8PGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJyla68pWwjKUsZ0nLpQQEACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/uoihIUHDhgweBBhwgYXOFmI8KABA4UKGDh4MPECZwcEBggMCCBgQAEDDVTEdJHBwYIE4MP+i08QIcPMFyIwSFjPvv16DiJmqkAwAID9+/jtF0DQ0sWG7+MFON4GL70AAgXuJegeBSa8pMIBAeQnYX4BNKBSCQ4IqOF4DJTAkgkIKihiexiwwFIDEU6oIn4DdHDSfxvGKB6BKBk44o3ujZDSgyv2mJ8CJLlggYxEhmfBSS9wgOOS7MVnkgoF+CjlffyJNGSRWEZQUpJMdikBCCVBOeWYABwQ0gZYppnABySB4KWXDY50AJlkWuhRCQCqWaSHIY3w5psmhqQAnXS6yJELGeqJ5QIhvRDin0xWEJIKKRI6pQAdoalomuZ9JAKkb8bpEQKW0mlnRi7kuSmRC6zgUQv+j4K6JAUfkVBpqVIGsJGmq2JJI0duygqnR3PiWqdGD/SaJqMdVSCslxx4JICxZBaAqrJquroRC896SUFzG3VALZkBaHfRB9im+WtGn3bbpagZkTrumKdWNEG6WXKkpLtMOqlRlPNOWaVFEeBbpJYbOcvvktFuNG3AUlp7UbIGy8gARwovfKMGHD0McY8EYMRAxTIyq5HGS0q60cdSYnqRqiQHaDJGL6CMIwYbqcCyjwNglGjMAjrAUaw2u8fxRrfunF/IEwOt4QMcqVe0gh5wVJ/SExqA0ZVOjzcBRxpMrSCYGxGA9YRmntt1gJ1qFKzY7Z3AUbFn4wcBRi6sPZ7+thqlAHd7FLTA0QV14xcACRmNrHcCQjf793vSFq6fRhksnkDbG3nwuAT+bmSA5AAMfFGqerf66uMVgMsRCZILYG5GvDqNOUeaw63jR5/XDSRHigO9AHAf1Sx26iDpfLbrHZXQNZshmTA1BXKH1ADWAdztUewGHzlS7SjfLlLuLO/+0b0Vf12S1AuTTdLVEKcdEtfYRgA8SS+EzS8Iqo+kgtkBH/B6SJXD1uxK8jZZUcB7J6EbrgIgPpKUoHd6WgDzVOInA0ZPJYNaoPVQgj1W/cYlL+Belw6YP5WoAHxjYuD/UrKBn23oARng20tYAIKMjUgDIihhSzpwAI+tiACVCFghS/wzgQeMbAEO6E0GUICTFIhAAxpw1nGSI4JA2eQCCCAAAaZFHesgwFCzCaMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTGVTAgIAOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");background-size:30px 30px}#widget-mm-wrapper #widget-main-content .content-bubble .content-item{display:inline-block;padding:10px;line-height:1.3em;-moz-box-shadow:0 0 4px 0 rgba(0,20,66,.2);-webkit-box-shadow:0 0 4px 0 rgba(0,20,66,.2);box-shadow:0 0 4px 0 rgba(0,20,66,.2)}#widget-mm-wrapper #widget-main-content .content-bubble .widget-link,#widget-mm-wrapper #widget-main-content .content-bubble .widget-content-link{display:inline-block;line-height:20px;font-size:14px;font-weight:500;color:#055e89;background-color:#fff;border:1px solid #055e89;padding:10px;margin:5px 10px 5px 0;width:100%;text-align:center;box-sizing:border-box;-webkit-border-radius:20px;-moz-border-radius:20px;border-radius:20px;-moz-box-shadow:0 2px 4px 0 rgba(0,20,66,.3);-webkit-box-shadow:0 2px 4px 0 rgba(0,20,66,.3);box-shadow:0 2px 4px 0 rgba(0,20,66,.3);-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;text-decoration:none}#widget-mm-wrapper #widget-main-content .content-bubble .widget-link:hover,#widget-mm-wrapper #widget-main-content .content-bubble .widget-content-link:hover{background-color:#055e89;color:#fff}#widget-mm-wrapper #widget-main-content .content-bubble .widget-content-img{display:inline-block;height:auto;max-width:80%}#widget-mm-wrapper #widget-main-content .content-bubble.widget-bubble{justify-content:flex-start}#widget-mm-wrapper #widget-main-content .content-bubble.widget-bubble .content-item{-webkit-border-top-left-radius:10px;-moz-border-radius-topleft:10px;border-top-left-radius:10px;-webkit-border-top-right-radius:10px;-moz-border-radius-topright:10px;border-top-right-radius:10px;-webkit-border-bottom-right-radius:10px;-moz-border-radius-bottomright:10px;border-bottom-right-radius:10px;-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;background-color:#59bbeb;color:#fff;word-break:break-word}#widget-mm-wrapper #widget-main-content .content-bubble.user-bubble{justify-content:flex-end}#widget-mm-wrapper #widget-main-content .content-bubble.user-bubble .content-item{-webkit-border-top-left-radius:10px;-moz-border-radius-topleft:10px;border-top-left-radius:10px;-webkit-border-top-right-radius:10px;-moz-border-radius-topright:10px;border-top-right-radius:10px;-webkit-border-bottom-right-radius:0;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-webkit-border-bottom-left-radius:10px;-moz-border-radius-bottomleft:10px;border-bottom-left-radius:10px;background-color:#fff;color:#333}#widget-mm-wrapper #widget-main-footer{height:auto;padding:20px 15px 10px 15px;background:#59bbeb;align-items:center;justify-content:center;position:relative;-moz-box-shadow:0 0 4px 0 rgba(0,20,66,.2);-webkit-box-shadow:0 0 4px 0 rgba(0,20,66,.2);box-shadow:0 0 4px 0 rgba(0,20,66,.2)}#widget-mm-wrapper #widget-main-footer #widget-mic-btn{display:inline-block;height:30px;width:30px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;-webkit-border-radius:30px;-moz-border-radius:30px;border-radius:30px;background-color:#055e89}#widget-mm-wrapper #widget-main-footer #widget-mic-btn .icon{display:inline-block;width:24px;height:24px;mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 30 30' style='enable-background:new 0 0 30 30;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23FFFFFF;%7D %3C/style%3E %3Cpath class='st0' d='M14.7,19.6h0.6c2.9,0,5.2-2.3,5.2-5.1V7.7c0-2.8-2.3-5.2-5.2-5.2h-0.6c-2.9,0-5.2,2.3-5.2,5.1v6.8 C9.5,17.3,11.8,19.6,14.7,19.6z M14.7,4h0.6c1.9,0,3.5,1.4,3.7,3.3h-1.2C17.4,7.3,17,7.6,17,8c0,0.4,0.3,0.7,0.7,0.7H19v1.7h-2 c-0.4,0-0.7,0.3-0.7,0.7c0,0.4,0.3,0.7,0.7,0.7h2v1.7h-1.2c-0.4,0-0.7,0.3-0.7,0.7s0.3,0.7,0.7,0.7H19c-0.2,1.8-1.8,3.2-3.7,3.2 h-0.6c-1.9,0-3.5-1.4-3.7-3.2h1.2c0.4,0,0.7-0.3,0.7-0.7s-0.3-0.7-0.7-0.7H11v-1.7h2c0.4,0,0.7-0.3,0.7-0.7c0-0.4-0.3-0.7-0.7-0.7 h-2V8.7h1.2C12.6,8.7,13,8.4,13,8c0-0.4-0.3-0.7-0.7-0.7H11C11.2,5.4,12.8,4,14.7,4z'/%3E %3Cpath class='st0' d='M23.7,14.2c0-0.4-0.3-0.7-0.7-0.7s-0.7,0.3-0.7,0.7c0,3.9-3.2,7.1-7.2,7.1s-7.2-3.2-7.2-7.1 c0-0.4-0.3-0.7-0.7-0.7c-0.4,0-0.7,0.3-0.7,0.7c0,4.4,3.5,8.1,8,8.5V26h-4c-0.4,0-0.7,0.3-0.7,0.7s0.3,0.7,0.7,0.7h9.6 c0.4,0,0.7-0.3,0.7-0.7S20.2,26,19.8,26h-4v-3.3C20.2,22.3,23.7,18.7,23.7,14.2z'/%3E %3C/svg%3E");-webkit-mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 30 30' style='enable-background:new 0 0 30 30;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23FFFFFF;%7D %3C/style%3E %3Cpath class='st0' d='M14.7,19.6h0.6c2.9,0,5.2-2.3,5.2-5.1V7.7c0-2.8-2.3-5.2-5.2-5.2h-0.6c-2.9,0-5.2,2.3-5.2,5.1v6.8 C9.5,17.3,11.8,19.6,14.7,19.6z M14.7,4h0.6c1.9,0,3.5,1.4,3.7,3.3h-1.2C17.4,7.3,17,7.6,17,8c0,0.4,0.3,0.7,0.7,0.7H19v1.7h-2 c-0.4,0-0.7,0.3-0.7,0.7c0,0.4,0.3,0.7,0.7,0.7h2v1.7h-1.2c-0.4,0-0.7,0.3-0.7,0.7s0.3,0.7,0.7,0.7H19c-0.2,1.8-1.8,3.2-3.7,3.2 h-0.6c-1.9,0-3.5-1.4-3.7-3.2h1.2c0.4,0,0.7-0.3,0.7-0.7s-0.3-0.7-0.7-0.7H11v-1.7h2c0.4,0,0.7-0.3,0.7-0.7c0-0.4-0.3-0.7-0.7-0.7 h-2V8.7h1.2C12.6,8.7,13,8.4,13,8c0-0.4-0.3-0.7-0.7-0.7H11C11.2,5.4,12.8,4,14.7,4z'/%3E %3Cpath class='st0' d='M23.7,14.2c0-0.4-0.3-0.7-0.7-0.7s-0.7,0.3-0.7,0.7c0,3.9-3.2,7.1-7.2,7.1s-7.2-3.2-7.2-7.1 c0-0.4-0.3-0.7-0.7-0.7c-0.4,0-0.7,0.3-0.7,0.7c0,4.4,3.5,8.1,8,8.5V26h-4c-0.4,0-0.7,0.3-0.7,0.7s0.3,0.7,0.7,0.7h9.6 c0.4,0,0.7-0.3,0.7-0.7S20.2,26,19.8,26h-4v-3.3C20.2,22.3,23.7,18.7,23.7,14.2z'/%3E %3C/svg%3E");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:#fff;margin:3px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease}#widget-mm-wrapper #widget-main-footer #widget-mic-btn:hover{background-color:#fff}#widget-mm-wrapper #widget-main-footer #widget-mic-btn:hover .icon{background-color:#055e89}#widget-mm-wrapper #widget-main-footer #widget-mic-btn.recording{-webkit-animation:blinkWhiteRed 2s infinite;-moz-animation:blinkWhiteRed 2s infinite;-ms-animation:blinkWhiteRed 2s infinite;-o-animation:blinkWhiteRed 2s infinite;animation:blinkWhiteRed 2s infinite}#widget-mm-wrapper #widget-main-footer #widget-mic-btn.recording .icon{-webkit-animation:blinkRedWhite 2s infinite;-moz-animation:blinkRedWhite 2s infinite;-ms-animation:blinkRedWhite 2s infinite;-o-animation:blinkRedWhite 2s infinite;animation:blinkRedWhite 2s infinite}#widget-mm-wrapper #widget-main-footer #widget-msg-btn{display:inline-block;height:30px;width:30px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;-webkit-border-radius:20px;-moz-border-radius:20px;border-radius:20px;background-color:#fff;position:absolute;top:50%;margin-top:-10px}#widget-mm-wrapper #widget-main-footer #widget-msg-btn .icon{display:inline-block;width:20px;height:20px;mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 16 16' style='enable-background:new 0 0 16 16;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23055E89;%7D %3C/style%3E %3Cpath class='st0' d='M1,15.5c-0.1,0-0.3-0.1-0.4-0.1c-0.1-0.1-0.2-0.3-0.1-0.5l0.9-3.5c0-0.1,0.1-0.2,0.1-0.2L11.7,1 C12.6,0,14.1,0,15,1C16,1.9,16,3.4,15,4.3L4.9,14.5c-0.1,0.1-0.1,0.1-0.2,0.1l-3.5,0.9C1.1,15.5,1,15.5,1,15.5z M2.3,11.7l-0.6,2.6 l2.6-0.6l10-10c0.5-0.5,0.5-1.4,0-1.9c-0.5-0.5-1.4-0.5-1.9,0L2.3,11.7z'/%3E %3Cpath class='st0' d='M14.9,15.6H6.4c-0.3,0-0.6-0.3-0.6-0.6s0.3-0.6,0.6-0.6h8.5c0.3,0,0.6,0.3,0.6,0.6S15.2,15.6,14.9,15.6z'/%3E %3C/svg%3E");-webkit-mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 16 16' style='enable-background:new 0 0 16 16;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23055E89;%7D %3C/style%3E %3Cpath class='st0' d='M1,15.5c-0.1,0-0.3-0.1-0.4-0.1c-0.1-0.1-0.2-0.3-0.1-0.5l0.9-3.5c0-0.1,0.1-0.2,0.1-0.2L11.7,1 C12.6,0,14.1,0,15,1C16,1.9,16,3.4,15,4.3L4.9,14.5c-0.1,0.1-0.1,0.1-0.2,0.1l-3.5,0.9C1.1,15.5,1,15.5,1,15.5z M2.3,11.7l-0.6,2.6 l2.6-0.6l10-10c0.5-0.5,0.5-1.4,0-1.9c-0.5-0.5-1.4-0.5-1.9,0L2.3,11.7z'/%3E %3Cpath class='st0' d='M14.9,15.6H6.4c-0.3,0-0.6-0.3-0.6-0.6s0.3-0.6,0.6-0.6h8.5c0.3,0,0.6,0.3,0.6,0.6S15.2,15.6,14.9,15.6z'/%3E %3C/svg%3E");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:#59bbeb;margin:5px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease}#widget-mm-wrapper #widget-main-footer #widget-msg-btn .icon:hover{background-color:#055e89}#widget-mm-wrapper #widget-main-footer #chabtot-msg-input{border:none;background-color:#fff;padding:5px 10px;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;width:auto;min-width:0;margin-left:10px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;outline:none;font-size:14px}#widget-mm-wrapper #widget-main-footer.mic-enabled{padding:15px}#widget-mm-wrapper #widget-main-footer.mic-enabled #widget-mic-btn{width:50px;height:50px}#widget-mm-wrapper #widget-main-footer.mic-enabled #widget-mic-btn .icon{width:34px;height:34px;margin:8px}#widget-mm-wrapper #widget-main-footer.mic-enabled #widget-msg-btn{top:50%;left:50%;margin-top:-15px;margin-left:30px}#widget-mm-wrapper #widget-main-footer.mic-enabled #chabtot-msg-input{display:none}#widget-mm-wrapper #widget-main-footer.mic-disabled #widget-mic-btn{width:30px;height:30px}#widget-mm-wrapper #widget-main-footer.mic-disabled #widget-msg-btn{left:100%;margin-left:-45px}#widget-mm-wrapper #widget-main-footer.mic-disabled #chabtot-msg-input{display:inline-block;height:auto;max-height:150px;min-height:20px;line-height:20px;padding:5px 30px 5px 10px;overflow:auto}#widget-mm-wrapper #chatbot-msg-error{padding:0 10px 10px 10px;background-color:#59bbeb;color:#fd3b3b;z-index:2;justify-content:center;font-size:14px}#widget-mm-wrapper #widget-settings{background:#fff;padding:20px}#widget-mm-wrapper #widget-settings .widget-settings-title{display:inline-block;font-size:18px;font-weight:700;color:#454545;margin:10px 0}#widget-mm-wrapper #widget-settings .widget-settings-checkbox{margin:10px 0}#widget-mm-wrapper #widget-settings .widget-settings-checkbox .widget-settings-label{font-size:14px;line-height:18px;padding-left:5px;font-weight:500;color:#333}#widget-mm-wrapper #widget-settings button{display:inline-block;padding:10px 15px 8px 15px;margin:10px 0;text-align:center;font-size:14px;font-weight:400;color:#fff;height:auto !important;-webkit-border-radius:25px;-moz-border-radius:25px;border-radius:25px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;-moz-box-shadow:0 2px 4px 0 rgba(0,20,66,.2);-webkit-box-shadow:0 2px 4px 0 rgba(0,20,66,.2);box-shadow:0 2px 4px 0 rgba(0,20,66,.2);font-family:"Spartan","Arial","Helvetica"}#widget-mm-wrapper #widget-settings input[type=checkbox]{margin:0}#widget-mm-wrapper input[type=checkbox]{-webkit-appearance:checkbox;padding:0;height:auto !important;width:auto;margin:5px}#widget-mm-wrapper .widget-settings-btn-container{justify-content:space-evenly}#widget-mm-wrapper .widget-settings-btn-container #widget-settings-cancel{background-color:#777}#widget-mm-wrapper .widget-settings-btn-container #widget-settings-cancel:hover{background-color:#333}#widget-mm-wrapper .widget-settings-btn-container #widget-settings-save{background-color:#59bbeb}#widget-mm-wrapper .widget-settings-btn-container #widget-settings-save:hover{background-color:#055e89}#widget-mm-wrapper #widget-quit-btn{width:100%;background-color:#ff9292}#widget-mm-wrapper #widget-quit-btn:hover{background-color:#fd3b3b}#widget-mm-wrapper #widget-minimal-overlay{display:flex;position:fixed;width:100%;bottom:0%;left:0;background-color:rgba(0,0,0,.8);align-items:center;justify-content:center;z-index:900}#widget-mm-wrapper #widget-minimal-overlay.visible{padding:20px 0;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;overflow:visible}#widget-mm-wrapper #widget-minimal-overlay.hidden{height:0px;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-o-transition:all .3s ease;transition:all .3s ease;overflow:hidden}#widget-mm-wrapper #widget-minimal-overlay .widget-ms-container{justify-content:center;max-width:1400px}#widget-mm-wrapper #widget-minimal-overlay #widget-ms-close{display:inline-block;width:30px;height:30px;position:absolute;top:20px;left:100%;margin-left:-50px;background-color:rgba(0,0,0,0);-webkit-border-radius:50px;-moz-border-radius:50px;border-radius:50px;z-index:998;border:none}#widget-mm-wrapper #widget-minimal-overlay #widget-ms-close:after{content:"";display:inline-block;width:30px;height:30px;position:absolute;top:0;left:0%;border:none;mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='close.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='7.8666667' inkscape:cx='-13.983051' inkscape:cy='14.745763' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cpath d='m 22.684485,21.147587 -6.147589,-6.147588 6.147589,-6.1475867 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 L 21.916036,7.3155152 c -0.219556,-0.2195567 -0.548892,-0.2195567 -0.768448,0 L 15,13.463103 8.8524123,7.3155152 c -0.2195568,-0.2195567 -0.5488919,-0.2195567 -0.7684486,0 L 7.3155152,8.0839633 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 6.1475878,6.1475867 -6.1475878,6.147588 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 0.7684485,0.768449 c 0.2195567,0.219556 0.5488918,0.219556 0.7684486,0 L 15,16.536896 l 6.147588,6.147589 c 0.219556,0.219556 0.548892,0.219556 0.768448,0 l 0.768449,-0.768449 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 z' id='path2' inkscape:connector-curvature='0' style='fill:%23ed1c24;stroke-width:1.09778357' /%3E %3C/svg%3E");-webkit-mask-image:url("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='close.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='7.8666667' inkscape:cx='-13.983051' inkscape:cy='14.745763' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cpath d='m 22.684485,21.147587 -6.147589,-6.147588 6.147589,-6.1475867 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 L 21.916036,7.3155152 c -0.219556,-0.2195567 -0.548892,-0.2195567 -0.768448,0 L 15,13.463103 8.8524123,7.3155152 c -0.2195568,-0.2195567 -0.5488919,-0.2195567 -0.7684486,0 L 7.3155152,8.0839633 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 6.1475878,6.1475867 -6.1475878,6.147588 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 0.7684485,0.768449 c 0.2195567,0.219556 0.5488918,0.219556 0.7684486,0 L 15,16.536896 l 6.147588,6.147589 c 0.219556,0.219556 0.548892,0.219556 0.768448,0 l 0.768449,-0.768449 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 z' id='path2' inkscape:connector-curvature='0' style='fill:%23ed1c24;stroke-width:1.09778357' /%3E %3C/svg%3E");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:rgba(255,255,255,.7);z-index:999}#widget-mm-wrapper #widget-minimal-overlay #widget-ms-close:hover:after{background-color:#fff}#widget-mm-wrapper #widget-minimal-overlay.minimal-audio.visible{-moz-box-shadow:0 2px 6px 0 rgba(0,0,0,.3);-webkit-box-shadow:0 2px 6px 0 rgba(0,0,0,.3);box-shadow:0 2px 6px 0 rgba(0,0,0,.3);width:100px;height:100px;left:50%;bottom:20px;margin-left:-50px;-webkit-border-radius:50px;-moz-border-radius:50px;border-radius:50px;padding:0}#widget-mm-wrapper #widget-minimal-overlay.minimal-audio #widget-ms-close{top:-10px;left:100%;margin:0;background-color:rgba(0,0,0,.8);z-index:901}#widget-mm-wrapper .widget-animation{width:100px;height:100px;position:relative;padding:0;margin:0}#widget-mm-wrapper .widget-ms-content{font-family:"Spartan","Arial","Helvetica"}#widget-mm-wrapper .widget-ms-content .widget-ms-content-current{justify-content:center;font-size:25px;font-weight:500;color:#fff;height:80px;padding:0 40px}#widget-mm-wrapper .widget-ms-content .widget-ms-content-previous{display:inline-block;font-size:20px;font-weight:400;color:#939393;height:20px;padding:0 40px}#widget-mm-wrapper .hidden{display:none !important}#widget-error-message{position:absolute;top:0;left:-220px;width:200px;text-align:left;background:#fd3b3b;color:#fff;padding:10px;font-size:14px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px} diff --git a/client/web/src/assets/img/widget/chatbot-mic.svg b/client/web/src/assets/img/widget/chatbot-mic.svg new file mode 100644 index 0000000..aad4284 --- /dev/null +++ b/client/web/src/assets/img/widget/chatbot-mic.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/client/web/src/assets/img/widget/close.svg b/client/web/src/assets/img/widget/close.svg new file mode 100644 index 0000000..aadbc16 --- /dev/null +++ b/client/web/src/assets/img/widget/close.svg @@ -0,0 +1,49 @@ + + + +image/svg+xml + + \ No newline at end of file diff --git a/client/web/src/assets/img/widget/collapse copy.svg b/client/web/src/assets/img/widget/collapse copy.svg new file mode 100644 index 0000000..f182850 --- /dev/null +++ b/client/web/src/assets/img/widget/collapse copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/web/src/assets/img/widget/collapse.svg b/client/web/src/assets/img/widget/collapse.svg new file mode 100644 index 0000000..5ff16cc --- /dev/null +++ b/client/web/src/assets/img/widget/collapse.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml + + \ No newline at end of file diff --git a/client/web/src/assets/img/widget/feedback.svg b/client/web/src/assets/img/widget/feedback.svg new file mode 100644 index 0000000..18ca78b --- /dev/null +++ b/client/web/src/assets/img/widget/feedback.svg @@ -0,0 +1,93 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/client/web/src/assets/img/widget/linto.svg b/client/web/src/assets/img/widget/linto.svg new file mode 100644 index 0000000..a1736c8 --- /dev/null +++ b/client/web/src/assets/img/widget/linto.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/web/src/assets/img/widget/mic-muted.svg b/client/web/src/assets/img/widget/mic-muted.svg new file mode 100644 index 0000000..0fd4369 --- /dev/null +++ b/client/web/src/assets/img/widget/mic-muted.svg @@ -0,0 +1,85 @@ + + + + + + image/svg+xml + + mic-muted + + + + + + + + mic-muted + + + + + + + diff --git a/client/web/src/assets/img/widget/mic-on.svg b/client/web/src/assets/img/widget/mic-on.svg new file mode 100644 index 0000000..e14b826 --- /dev/null +++ b/client/web/src/assets/img/widget/mic-on.svg @@ -0,0 +1,73 @@ + + + + + + image/svg+xml + + mic-on + + + + + + + + mic-on + + + + + diff --git a/client/web/src/assets/img/widget/mic.svg b/client/web/src/assets/img/widget/mic.svg new file mode 100644 index 0000000..791b62f --- /dev/null +++ b/client/web/src/assets/img/widget/mic.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/client/web/src/assets/img/widget/play.svg b/client/web/src/assets/img/widget/play.svg new file mode 100644 index 0000000..036c30b --- /dev/null +++ b/client/web/src/assets/img/widget/play.svg @@ -0,0 +1,77 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/web/src/assets/img/widget/send.svg b/client/web/src/assets/img/widget/send.svg new file mode 100644 index 0000000..1d7e289 --- /dev/null +++ b/client/web/src/assets/img/widget/send.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/client/web/src/assets/img/widget/settings.svg b/client/web/src/assets/img/widget/settings.svg new file mode 100644 index 0000000..90d5a78 --- /dev/null +++ b/client/web/src/assets/img/widget/settings.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/client/web/src/assets/img/widget/writing.gif b/client/web/src/assets/img/widget/writing.gif new file mode 100644 index 0000000000000000000000000000000000000000..ec94f4323848e3e146371476394a0f058b8c1e83 GIT binary patch literal 147485 zcmeF4cT|(>w(h_51|f+^2`xZ?Pz{KPh?oE-p{M~_E=19&NE0+FND(y&y_z7>1PQ%k zKrAR?C@NqB#4Z>V5U>HV?6PiLYqzuaKKJZ%?mlOn>(;*N@O^pw!=>qIby?Ln^YWVW>-&nD2FlAX zmY=;-+j_65=}MFI;pyu$XD>f)xjEC?H*>M)$)&bi(!trz8zWtJXS)ZUcHf`9e(nDC zhqE{Cj`a<_z5VFmhF#lere8%wVqAvvXJx-^yb9Hp$Q+0@fi zel3T(y|=*SRK<_Y_nfM{RYZ$LYq>U8^_Q}an)voMSKqE=m%0|Zp02r5>wPMC zsP}a3y_1~I6fL(ibq`Jj-mdYzcBcN(S?;smLbtOGLl+|7P7Pfu)$UA%MnddsQjx5Q4V+N;kuPu{LtSG)Pf`O`1%Nw!~` zpV}51kHD&!adBHa5*FHSNdt;O5Exp`Hd3u0T#1GukP1i~hX#V6SzTcw-3Y-OS?9b_ zq;Tsv27kq%YRj$70yuv0Po!L|xb5p_07(k$ImrL;>#rN82cndl1H^n%D|rj>Vaa_N z$mEz5;3=n^r6JAA+3kGIGt73BnJfK#f9WTjI?GKW}!`SH3@z_M$x}6ReP3 zReJDq`e+B#9F7`>7+sWs(pe3a?Jg#2l4nQX;i&|n<9ZiA7p7tLSf;@;)L4$EpqYNm z&rkw#(GRSyfa(j&q&Zx$SjyC`^R5u8C4>pmb{e9#v2demMp=}-qXI}oW($qAh|^b@ zuN+s_0mOEw)0paHpYc@f#zY};2)cg9wY9N?#|p4 z>_wBZ6N2z1@U!P87#SAs+siaO&@&l1o2|EAHrrg+pFq6T-N?{CRJXlbrQFqur`euA z%hw#5YKm3!uwTqm;AYL-K&+RFt}b6r0T|ck6~quty4xIIjq4mbfW`7eur&}{5R>G7 zP>j&j;+>9#bLQyxeg&n9P}?-T%@~RWL*gdo7U`M!ND6n(@$suhsR+IQ**!W#|BYfe zlLFZs&NNqT2xn}1Q^lCrOD6>YV(1D!e=(iqlw>1Wy}gZd{8@YgxeORdn{A{OVlDMogS z+^nI3{1iZVtos50Yr3HPDku~6WSC8?@-OT6f(kdF4;U}YDY4>8k4Z3|dK{VS(;dc8 zXPm*~Nv(KIAwN`+1?_x>gF6EjFkMnq0htJZd&daMAtbO;ZXZ}pdR$@dm7$p$2w^SJ zMSGE!>ERx)wiENfIF2(2#|y`C&ppVUNegr@CEn}4Vsm~botsvw`J8pt?%_;EbR z_uW?=KFu8ADU^{AF5QmASz(fUnVxD-H^XjLBupzK8@cqj`pjnLUN19T+S9WpdiJPT zp`5~W>GjN?&8l)QH~G1zcm4SRV-V=KhZPiX{`12s@vn&PzwekF>Om2CJEIGBA2`1I zpm-0jG%T({j`@hhn%_yEQF1l&c+=6+4mt93D>{Ft{_)ata`2bdUiuUJ>zcZL$A9zr zoQ!K3>eY|8f&VG|eTSbThjsFI0_$wk(S#JdmhGl) z034U1VVgLe+po6@O)y;FoYda=I(>mEg;li1_Y+cWaa1cwx8%b6H$xleWl0wx&cD2U zzU)I}Pbuuf`#0;p1e!waK7GE!lxPxSmw);7$wf&vZhRy_7N+@9IDmT%nh;0K7N2&4 zBi2)fVA8y~Sd`NK`&y_Q2M1#@HVR`-VJa0>B$t&Ro);&+4H}GvL7;#%eJPPcPqs(3 z(^F`hB_~);Wz$bZWGLT5N$Gylv!k{S((P+t;5v zNnBQEcF0XO9=h#HvOaX_$H|Dl4jvO~?&+S$c6Atp^33AJAt0pn&ao7A82(kZrIjD= zG#@L?#P*of^3KwFCH+dzwDV$}z+#QSGmlgxK&x>@O@GZV(Gw<4HWM0wN~T-;PC|f< zxfKjs<(I|B67L|Vg&Z}Mz1^-w?-MrKCQ;M{!mU%Ak!@x%i-bWyo4+4+i zeQ{4^xw}TU4HwQ{3xg>@ zg$yk??A7={ZS5SdO^F(F`-o!zZ9mJ@yU_9B!k;vPfr@*1cGB7-qn~b&vo?wpPa#}s z(U*er()2s*-4>Xoa zBL$)XILWnX%dk>_1%gY;##3koORFC0aW!5| zo1|!e5whXU1HC)kO2(&NLTI$9o$*`!t;Z#r+gydmdeB6go1v}gE|8(SZD{^}44R4NWgNb%<5CMU_}d8Smoxr^k06QsWMuyadYTvB3~b zu+-I_^zvng@^A~jD@MLa8ZUVqz4QZP@ zH6~GuBMCZPv>U;OQqPNaOM${$lcIC|NGC`Y*paE9ZU8ZhuYC(wqZJOvYaC;T$Czkm z#oqPy?<3qsI$bNdKiDGmC=Xbf+c#gqI{b*BwCqSKPmwm~`aaB#8|O4E`$Z^(g)o6B z=rbFdZB63O_e`D*EiA;HB`XP?+;yy}CSb)7cHiT~%4?#8f(yslSdh>?aJ5YrPpFjV z2ADBda5tMd84qgj26jR2&|i?+51o05=>hC2L(!Kn+*kfG+p8VDfMmu$XLhxdvu-sm3-aN6^SuKp?YSpE$B;^T0Tj)sTgd&WP z7H{^T9S*S;sM=;&BrYa_;8d;-ip^U}J_lM8JS`y1U4!V;+A%TK#}lNIe?Eo(Rx)o# zA^%Y>{}-VAk89x{T>jt6sDH)p^&N2;`|_D{5=KSOS@Mm`TEBBS4xyyauHyBVHYgEV zbmZ#zYdegv_;4%r=5Jhfoo`M0Z0{&n$4;$-uR*N`-kU$ZnagWC^b69$`zbKfwlY#_ zNnN-g>fJ(Ht^LOBeJ2hpL@yoQ;1I@<#ohMh4k(#eR~1JX6d35nlYG2A6Ew2#>uxaO zRXv3m387BO)V)Bbw*~H;Ui`;+uTFvGE;W+3gEzM{1uv&Daz)1keVrLwECnGbBtA+( zL>)~*M$b8BS~^q-kr)LW4SprfJiJ!jDAnQ3mVNBUvUyk;<(ENwR>BR1!izI6xXs+ z{iL7i)ewSpK=;I+vdW$}c)ypuu|$Q%$8Tc0s5d57OFg=5FCoBmmA7VrXH9Q>-UoVz z4KwMV*dXn%ak;br_I_@lOz zIZEh9vu=5s10x&!sf+bMTm##4d6~%jDuMW9sKF+e(^+%ig-v72a^Kb-fw5$@zqFDL ze^qbCBpBpq6wn~5;QR0!7Es{PCx$^(4j{la+e{E%>a(KVj z+@OH{-xJXCsmm9<*gc_ zfxuAfNirBmD~mlyOS#X(l3vM*L`K09h=+!zY&3WKW?5g#PfZ_6n3j8ux2Mxa08n0- zo%)!gT|tgvsx-X0^T4j&`vvqRSvU~HV9+|CFwI1Q%Ri8k3p&of8R$q~qJ{&2g1x~! zjHR!1q6!)!7Q#eL_jydRmq{sn9vVfWRW{2%AG&CS4OFM8NT1Q|zU0=#9}I4G5;g3% z4TPPx!O&n99?}<8SW;Ps#4JPVq0rOY4OUtbGG$f|;`L_c-oCp5a8&1*mcTUg`d%fI zr#aGdH?UGq%w)!=o%E{n8%H3Tr|lLcYEF|Z+M4f52N0K|}phf8?pfIPePZ0L(8`Ui)#XINDBjP8*!^y#Z zCHG|qBX{?GYHF^U?_a7D*70a+kKBcxBpzf-OHc zUbpDqwC@ShSTXXp=8Dyk9S}>N?ej&(w7sX%bo7UOjnhHP3cx#jb^;Kw;?WnACOpUq z%7G!Ng7(*-7Wh(<7YKsNl5tn-AMugGIv3Bqe@RkYWBZ!k(_)$jvz6I8U9ODT!7}^& z(!i4vLTiVqHVaocFKVrSB@8=kn*9=7s>KR9YdOs?_fWo&lz}ktRrP$osmAb+ou${F_oc zwpnfnVrz=A+Tf-TszAYAW`9A%gVA`c$47ZELuC5_;ad=)P8@Zr=bvn2|2o36f;7nz%TUj5(e%>QX^{clreKT6Ht7c>8dP9sP2k?#b}>o@r!FgQ(HPG29$ zIYrAh{mVJf_RHu66d!5TOy}zif|}X43UhR_`r^agRQhif<{k6Xf2c4ohemybz~vR@ zjkutVrxz)Ki~qSpGOs7Ow^b3@H6d zpD0G%I5rj=ubMsKq8Nh89s{Aa(AJ0P4Z zEoBJY!eV~jx_2K$cuHN~4v~E_>A6@TX&Q%#%+Kp!F8$ErueiJrFe=`g(Zj}*4O5Db zXsQ>Nls8K?b;xLOWLG8NN zS3MM){Ea(dVA+&egK}%!)Jaam`M$!gsU$k{prPp~@>T*y3Ns(0nHSp_Dm$N9ccKc9 zANJqCJ9lgLe5Uw$uezC|gXBVKf;tNSMhNeF5dtyWAv{^IkYXKs$*xU;SAt+#c>N39L4#$|{E@JhD#zfWa3Ouao>ORX{JkM-4-Ymp#wnxcMJlks;Iz5}CP8poK zjws3VsIy&J#p@-W9DQ+XjY%`4AMsFL-ga4;#Op;#oFRkmJ}GRx*YmR+2E$KF2AI_L z4K!=5q;{R&QB?VyF;w%_cOD7inDO1#{@S|R(O}S53#rv3UK&Yd(@p})Sq}%QS6;i( z*O%ZHu!PB4H0-O?Y$&S(P%*gM_ZAiP;U45|d=dBrbSK1( zRZm``t}KV@cTbZe*Ggv{&NWY zNBHZ12wDFsA0tQe_Y(fvZ|0YP!fH^xrPK6pXigjz7K2wM;|+XT#WSV5La{2}!e4~~ z*a8PX^v{-haqr*AoaWPnc=GFX#XGkvug+&8Qsk=n=$-5K zun%&38(}ijH*{FxP^F`f87@#3r{Ndisfe+u(nXJfU|nc+TAviWU=t0D(`clQq}<+0 za&}n~NO=}vpfIvdVF86F&9I-fFXhh(a=11TN3rbe4He)(Mp$S5&RqaOiQAFf zv6_|^^t>1&M&c1M4a+*2CQx($*2b4RQI2#6`myQnCi|5#(LU=Ga<}CNI2nhXuZY*^ z$C(t&)90wSq2-$&N}bfIQVcL!$EIs?6p#Q7jBSNF*XM$JaFyo?A3;pZPT>Y22*k8y)s`Xdu6LukW$)ctS)ov^W|69SIq-GbBDy(v^YNS#gz^&bRxSg8mX0cPr-xLFve{Y>1_kTB{M1)`*} z&V)YQn0405ucI5@lp<1~EVZSzAtYJt*E~FX*frMAu8L}x{d%?$Y9<20j%Zn1JN3Wc zD)qp+e6FOoK_ztqjceajy>xVB*AJi4P!xs74Eh|9qAa4mSV^&C!8YNa_Kd!me<)i4 z@BoCDY>AsqoK~mP;wLK#Z>W!4e;+ZXIPZB$r}IVMiIjD+04C2tyFL~ROE1TbhV9bS_nkqMs9py5F0l0(0&5yd?D9SJmI|FGqfYy zSI^98tqpsPA+&OR+{bPWWlhiLlu}OVS{jcFf?~}2q);ut!}xOxE_i#ksND-x=^t4i zNf;R4n{@YG%~RfccW~R!)ts2X2mGNAetczxY4XbZ8^sJ71X>dh!}#G5CRZ|vEH1-D zczdI_lmgixgJ{OMDC*r2X4_&?Odk#Ws(4$BGrSMjgHq6>crUZ1^FQ6-f(UHwTG z|6Tn4srBm5?ES6v>PP(RUxj-A%g!l>aO`)Yk2Ai+CZKRcs=K_#3_%fX`J=qXr_RYZ z{WVowQKy9>VS(On`WT_Ic;7kX%s19;-s4JPlh^(A9~= zRZJ8r+Mkw5!N@L3X&Np`+K=r|uX+}(|4~|)Lc1ApQ~`HAuuOz~EbLcOo0upQ=z~1K zLNC|Ibr`C4b$J?v<0n44#)q02>*cvjuv*)%Wh`IAdk&n-&BtXb?zoGBtk$cmHp)@9 zL>c8ScsVV~3eQ#-C|UK0$4~6+rFx(bcc40$mdZZQ(N-&@lv1p_^0qP#`XN3gJ3D#8 zjS_M`CY~ z#rVN3qIhPd+O$ElTzh|VXdYO(j{v*(8rAO9u&Y*oKk>#Vb0Hj%g51t!Ul@ZZ^@|%a z8iTVsFVM6BdE9mx=S{wXeOE`Des8shB2Zf;m*f+Y%yz`i2h~oyt{A;XkjP0#XQ`j0|xC|xlHxkXLcZ$FOxI=%Ri zK`<2B4-kYjx2q=RrA=7N&(`53!OG@$p8UkwWrNW6$b zDAv!(IEw7lnWQCZe0;fQaK*rNy|pVMY3&dNstkZ?*iPZG%WRg@`^YoXrOmo&5h}AM z*YGa7T)$21co&q1sDURrB_D7N2Vbc!x91K>zpfuRVOPjIyeo?TE7lsMSjLH1)YuN_ zC<_*J0g?NnK&ZV73Q-&}gv0^CgSJ6b)e@KMQuyfgYzXe=c!KTqVr)crr}n4m#EmqC z5?msyOOH5{#B%>9LfH3Dd9J@`%}o5gH2k~6{3n#kdzSwkqyCez{O_pOe;l8GN4Ngg zy!^vP{$B>5f1$4Pi**1tqiy+?pqR!pXaLbIpc-+I6v_5n7zPl z$Zy?Fs^;A1g(f^dP*KboSti0AP4qva9K@X{#Kw@pr7^o?gpADIWwcc$n_YL5XkG5E z&dzAaqF2oeY$$ki#c(y9X=eV?!|3(?R(&4G^G(U^u4c}kOmodx)&`B0 zd$V>l)z!>Ro!Was4S(7n+k!t6Vwgt|KTbdyIGga?#_EZuX${$}70LnU*T6CO`qxd7 z?TnV2%eNy)OVCF? zLGq|(x+T^yYAl9)YmL>-<}TwzKc5c6gms;`JG4RS*|&IsiKu_O)B8nC`_da)?PiM8 z;q%Q4JbZt-qp_oY$vu;FQZr&pH_)#R&+4nK?-eVPM z#42^2i?dAaMT(2v#htFw36<5yt@8DJUo;(G?18NXOE!)Qit|!61~94ncfEGkMv{y{ z&O7HyuehXojb2@S&l+^u4o;IoF7Av23kS7<-7cnT$@S1*rsil6B#uf0q22oqGA||S z9CYa_+VXY7j{IIgM%i$?hDtlvEEXU3a=hiXo*2DA?*LoZW3ksd#m5>s%Qh=Bhl^Xe16N zN}3&G-Wu4RKrSonlLCM zIB_rK8Q7wL?3koK9@vgqyh8Xih35J}$|QCNlEYECWXBBm1SZ|xjD#D_BB=CvBryGZ z02h+YqycgUb%7f?Yl9((xJoj9;vv6_UcftXb^x;f z;j+P7**}8T6R7lT{FJVL~MUzkXE^ zdqq9SMc>}^g|im>_!vm@n6N&FP%z3ryCa(FA$qFa)1Ud=CTalsGBYaBIr_>@a)bKU z4U3Lm%ybTHTZc9kLFS350)~ddb-#TEUsJe3w0l-ir=~-JsFHAmW$B{6Nplps;3uhN z;z9p8dbd8>b>O_=$=r75(+&2V25M>~0kh+Ur%78PloxNhj8-aKEe zY(P8q64&;YKe5cp) zd5^9QZZ`>WFR>&$#yVAB{_zcmf!oiegz0ET&eH3EloV=V35Q+q+fxprNG>p@KOG0A zb^Di7VV5si3x*?1<~?pcwV=Ldoxi&iG;EdL$Gc5Ki+S&Ke>pMr#u)X)WX5h}DIJ2g zmw`T$i*aX45PiU$dlUH73RkdRm9~%d^d)^u*#Q$gA9OqpG~2ieqVcE?q$jh?O#=M% zG64j`P*pXM1Xdv31z{xs;)_4eGD_|(^Z0%)G_cBjV4t$>@o%zr`+pB=|2zf6(}47c zVIDu^?cY9ofBfWM^9H^n=H7fM=z<|IDw-^*lnx`3r6Qo~K>OIb=<*Etd}QQ*w-LcttCpIHne86XkVH@!`W@bR;hm zv(WkKFn@5BbO58nWj=|0vWL9@vp_-iBwh_orMs9=d9uVobDpECIR_X?ezHL#aM$)u z8c9{fO184dWZ-G~fEG~TV$9|}Ju>9d_t;HQ8c0r163 zUgCk$`Sn9P%S;$E8npnHI-A(#?-z2-$k3E<*y_@`H#c+y5)Y}pQy|!760Rr(8VTu~ zR<(;l>$rCbWxU&}V4wc8o@*m$^%|lB;llH4uAe@9WWASsj>qb2{fL0r#88(Hc3QxE z$HE5+XTun(8{bE`g%YVt&Vk?o^?rse+9}o zPhOs5=G--3=IWjKGMr(Qcw>fXR;0Dp73$?K9aBH(G6(jH_46N4DJvvkT52yGU^bP+ z0C#lChnt`qYMYmwLQl8J1-uUK1B>NkMjx?QO^Z4XPFwS=12O78&vEp}G|3U>fX zfxqI>ZJ6WIONJZTr!!W81UJZ#Pwi{~F6QAU7LhHIcoH?XaaJo2aZTk$q zXedK~a|Dnc8Uhc&Ajjv5AygWq{XjBmI!q76p+T=4#UM?jz>t~@;9Jn(+B?>c72#?+D1_{{_D}Y(67W41oAx zB+=TopRg)^a(ZNu-GXG7SM{33dFCf|Pfv#C?D4U- zC|tKItl`84!Q6}UZfG|V`b|bu9mD+kal=!jg}g;MFmLCOODi@@Jet4@)E!xHow`v# zMI_}ONea$#>~gk26F@3kLOhB!j(fK=QUoubB6LGH>`mXYzM0#r*ieV>ve<}R%tBw^ zRNA(ng|{zxZpWPCE%Vv;{TCfEYTH&ZbZ*FwxGoMY7U?wNPA6(_@~XWvFj6CG@QJu* z$^yq|ahg>xGSA38=&&shhc-|>=lgc84fVLQ&*?dp>Gtf#P?uiRt9MBC^wnn`XcQN| zduVz1)%#Ui&|N@{_A!nvv_7-!!}X)PMk9N793r<|+nJrk?|T&-*L273@L<#3mB-0% z+jDl3EpF~l@!NGPrlR@6S(rp7!ep0d?`ACu>5aZ7`iZ3JkdXTowWdMn{hmv=+UJY25 zH5&A@nz+E-z7Ke-zyT(Lq{ya9f-;5c{x+%yt({x(wJRQ~6VeA#6Z4n1lVRj$A(ToI zINX3i7jgiIN*E7jE4GWWs7ppU@IVS|);Db&8De|4@2YBXnkUSJl@t<4D2cHh(j*04g~lm z&(U}_yIk=yuz(M~e`@E|HM*DGl{NI{?bX({eTh^u3u%zjNC5BsGzce@l#C%^kKR5{ zlnoiApvPfqGWg_WvLpEQUlB&=}Hdmpg|!| z8b=APq+yJ!sr&?L_RoBbSu%;1WZR+2Ly`}2eN$*E94X4^7-@4F%K^tnn}iEF8SHJO z0pvp!6lK~eK?gN9&1qI}eTtCO2?M!o7j(^JqpD z%S=-6uoE`D?aX=YG!tW#o)&Ma91_?SRC|CZ^QdZq6X`6|$0^k{3wKC&mTm}Ec9dia z9^m0uHc!zQc#Go|Fs-e~SI0>_XWHpR0?z|e7@S&iQlo`5b(V_#EGiz&OzuZcz3!1u z@{np!(zWu~Z;Hw5yXb8}glar$zOBcPsi&~%d0~%UhynMe&Ep;Ww8BZ2d7eI9RhMaP zH6CR|eU}%_Iypi3T5{!wDn?_4rhYl-Y{7PfYP#mzxWxf+O*e>7uX{AwB_`E5>wHPu zc1raa%{pyhS6J`>@c6}C9DC#7n?d#U_5J6(uLitcWgb#r1lDW;&hQrU$n*y@mco8; z@a~^==b5Ulq1uT$x~rE0ZfnE1!w9;fWN(^uSTtOHJWz7K2!(T|eDJMn6T z1L!mIStuhqh1CsU-y?&DtqXA*U7gu0trU6N40~=j5oLB+Ur;{8+#xr284tvx01yrU zUN;MRHsi9@+?YQL{e^lwS6%=X(h$yUNsb?|+t`W*QAQla&6zXsebQ&+vmmsYG?E8r z(;yV806tI#)jr3Y)nkJcBLNuUe?}S?|3Ml<0gJx}$@l0|EC9(1KIaG0rGqZ*y&poOVe9M0>f2o*YWze#R4d5p;aKzuNRPnP*lcI zc=B}^II9ib-W>6_*t6EWckY9yPB7%e?OJBh2xK)@5f0?8y>tkG8Td4bxj^gTX++4h zCq|1U8Roxmp@Q8FC^*_d93zQ#$T(CeJ**};%R`!_Q$Ye!!_Vy~^JQuy$#@eU0j7Qi zsSP&?$30DZ!53$uVd5(NI7k(ZfWe-lltm~P3_8NC4f8f5D91=oGT1kbfc<)~WuwRN zxp8IkMGBlu^je1c4!}Xs_4HUE+BP$7)0q~M%d!+>BM@mTZ@duBRU;rn-=RRsdQ0SO z!9dl0Jm%|2)DE!jX^yyP(;AW|N{L`%6tA&CJW&`NJqIc~*!B?*&c^xxObYq@q!1C) z+)+9&HZcaVbhBJ3$JB$)o8(jX&jrK#TsW*FN)3fQN4$Mscti~n&$=B4P`vMS;VYD0mNpI>>4qe@GE_LN}{nc3BbDKA#t z!#pXxzAJ2Vn+xL_zIxs<%7oE_eXIx3T$VbWrW&Rk&}lc2Eb4MRbH4ww%gtUqNb`*? zz%b*z)GL)oyR6bP_m8 z@B2Bmc|arAy%xa_ZpV7cjFnD^p1T|2qM4;mEBKS7;)1q+YwjDGB))ZpCR(71r}EVc&|(d#Z`PndlBxH{cgiBdx7P`K5&?W}@~H zXxgyfVdCHpljKpGaNuQO*IscZ@ZO66PPl>k*{Jb$jqobL;c`tB6~#{uQwE?kwgE)p z!S2lhX{P1#6^em05R%FRQ)G6aFcjL1ZL44|1;5+bgLV+w;(n4rlE1{JBF*FzHaATG zC|Nqpop*FWj0~*$K?pSz7ATc*Z2swkr~ksM{^OR20$1AK*4lvmr8& zN|QeZ?!$0w8NK3LmTO*3E|pvVBFr`Y?AmWxu3R(aO;>MTT6q|D=CpGMczmHZCwfsi zr#(i&DsYS2$5+pv#C%a_Fkj8SqtR`iqdRNHPTuY)S4dMFfefoJ0O%o!tQ$^GmOz*U~?6_|Nj)2)Wkhcv0 z?N{cXNUK2*5Ijco6DFJJY6R96$s3XuAIfqp6UhgLq2hh=x0MhrCB~)Xm^e_?yr@DZ z0#^`ICK;r<-fE_ylvu+A54r{bn!aMwQs!P|!AX%_iwC@e7+k?XY1RfHv^=C{Vgs&M zQ1kZT6g>Lw)PrhoM^Tr@%dz@xMdU7Nh!|EYkp%2EkUK5=OzIM-ip1I0Y6X+zqa$g$d zuoD_KJfm2T2k+i)^p25_-6efMJeXn&4cY1U_R;>}EZw2{Xy~@#rsyu)`%fJmY{xV< zH0&SO2tBm8L??XH{t2Dry8V+nQMmBOI-<~U*uk2w?W_flL$i!*RE9T*SL*qIE+CKZ0akj5Q7f_j&;!ym^qgQ_|d4 z8dOutJ3}UcFd?#Ka@G9LT?z4b$SmLAl=GvY{2epq$IK2$w>50p z^!V#zYGj&RH9J62IzFv_Gc?}&Ujc_{0t!vxSVwM2*ZpnSlA+son7Ed{PsT68j z%$D&rYa&2yrql^p(z|#e0fY9cdU_aE<}8TPx{{;~F)1hMB-;)ow!4@qOXLYJMXA7r zD5X3TVa^3)BA1R&l)*lwcL@&D)~f@%^@F&h*%JmbM;AR?W5a{Wtt8JQKUJWlfPOKD zoWmTj9YC+nm>kW}EwA#=Hy4z39Or7Q^I7JDL6t`}d0Fiz4s8420p&CDHCdXP!PVhf zja1#z^@p70Y_m1br8164?k5!Y8oKp|Z_&v5Oec**d? z7Cyr$t#%wq4Ch%CAh=$R7xue~`;oI1#+w;F!`BR$TW6P7FtnkY#u`=W!PD7Wg1xFc zw&f>#)L?(5a2Qzk*0D16P+nth?q+NH)s;>8qAtCwtPX~1QQCI2I{w<;c-%%W`gJR- z)P97`bG7@edbM-)g=rwimz_A8ee9eh~oMQ2K7C04R?NpQT-SeAqU0h zH7){YV&4Rp89jLXOEvyXaOHcsjXl)nTa-+kbYFT~<7>gvXvYBO<|lJn>7sM4$8Ypk zCTfs^nPpbk_G-;~_p{eDLvte#I>{3eC~f+1BM|T=`dq;!tBe4q;MA8!rf%(u#Vo^1 z>2H}1uhVERw7#~4!{Uf3GYGv=Vi{4N%?)x3*8h*>Tb@ocFV1#9p*g%*h`rQ_M`F5`gyf}VE`g& z+CVS&rwpS&;UI!mLuLs5b|I))BJ@T zuFXGZUDMxZ^B;FBZu+BuhW_OO*dGG=zxHhZ@mt@?4SYwChF{a`lq{+W4X_duHYc=4TwGAcCf5@183f=>eTiZ*ew3h3tx+PA(L0aLS zZw77p;-$j?EbVG0hDerM+lckk0~kFv=Si&6A&H#D(CH`fPmHA zmdCK)xp2U)`bgeL>M)Ax=t|U-$FQe-#hFNgcw+lO-P}=rJXHlngB!N6K^g89SEVkC zVy2&E&26Q5!kvj5$eCnw`NYe$*<^!bem?30$PK}$?ZD!cvi2Ma**?_<%?vtJzSuij<3~glpzW z8w&OOtm&=64iXROyF-%zQ}d=oMXZYULeS+_scqfYx0tCn##BSxQjvE=1;hS}K|U695x)3=G5^*i4tt1qiroPZ8-p1g(h zY3;9Yaynj+qtd9VbgzJv2i{DP3t^1CnMtNU{|KMELMWtR4J%W?H^ z-hP4RvA+HDal{tN*$7D5f%nm5?p(+4yK=Fp|1gAidLXv+xiyphW`#UxZ9S}&qoH-+ zY>h@^%7w=f08t{lOrH|!q#omk4@`@Oiek&&50~xCI(2i=Vdb~CauU4W z_MM3C5CVwI?>&xIm9w2P()0X;$!} z6fXxUs9=ldr*{p9Ge3Xs*y5Qg8`$h>!8wVRydmq}zlV7GGePCcn$^%qZ5I}`)mDoW zZQ{~C^O_8LQSJw=h$Rhq53I=amGR6_I>*R3FuZ{bL34%=gXQV%wl4dHwZM4}K_~s7 z)HjLyr|%fCBR2Gf%Pa7}A1Ob7|ubUgi^uv78> z4r%!UWZM8EX-TJzhvVWDOSWR7u{-!gm=5 zf^`$R+6$9B#oL(Bx#|i)mrt4~`!+A6BzJG02Rx$zC1zsW?W)nF4Lsut^N_BNxWaAf zQsz7{$*x)+*5;a2XoSckVZn#ai!z(8xCApjhNA;&)E7!?^I>6ej*_Fnef{Vw+e{o8 zX45S%Q_Z+xpk~3hto9wf6guO=uGI3AYVD{7iROb1uN&2Tt1YsbyXG3iTX$J6Dhb#e zC%x!xQ!7G$zG7U@F#mGk`Ds;Sx9L=9$XrN=`(_FLGs}${ro@5hOuMSUKv$^n>7f&kHTr$u52>NV5x~&? zLH>BNd|t>XZuGopZ1YNB;L)mEA4SiXKkpGuti`a;GnX7WILEXrx?#=Kx7VT_C$(&} zF4pU*Z7$KNUoCmLDmpuY7}!MPIvh6@~s_Fk|cMjkMuh@fT?{%7SqHi7P?#CzRWY|hW5 zbJI#SpR=yoJ)Fsiyk4sPzWb`fr$2Wn6K913kuS&=ZUjQk&B`sbeB_2*}g zN&NmOtA9)s`R6_DfBZ}Tzu&-j#p`VXFc<Gn`fmKpYJ0F74bncUZ12A|U zdBvL4-T}wzZSL~x=g14x-zJJ6XySHvRaF%=?^|tOqv5LlZOE-4!Pn0pUTsJAXI_qd z^9*Kt0=(!OmR}p>auADt>!VtwwiRU(Mdk6-GE`}BGro`~Ae}!8pv>}U{5Z;Z03B`g zHD@IC=?d`xO!w}@3AjnT@M*@!WO-r?Ev5KH!U{$mp*Ds+n_)&5rJy71;rHzB8Gw5vpg;)S6`Zj=z@0`&-?Xbo#Pn4H*B50TyVmD>T+T&^WoMHAjuUZHykcrUg z(KY0M9SP8hk~ikMVY9;rSYJhE}}m&MXIiXIevm$;1A{wp~3urV&=d5U$?H zGs~~p73X*=v_{&G9_W1lW}Mq>I@&<|rMEs^8cYs3XRopo4<6s#Bg%eZGC9WJbZ8jl zsV-!{5kk3T^2C^()Z^g1V+)9pi2YL1M6^uhrenCup`FWpcp`w%$=btEHA2-gG@XKK zS5+jKk(dULasj6CTNbFxB7g!g>#r1Wu4vrw*%zn1B`oA7Sf#bU-NER?ONN4ymhNfQ zpOWVyk=!oYZS?C}JlxX_DOt5(la}oNuy@{JP3?QSUn{*(5zYs^5 z8IOOH2iEV$^Q=$aZ+YKJ#SMZ3>=%$j$z9@=E!@+-X}Jw{#`Q0X7tTTj@rXlD4{co@ z36J!7eV@Af%h`jrJBI}#fY-^rAFV{#PT#xu`u5XtRnyQn7`4Hz^h4i_SmBDRg3pX8 ztGB%$S6zkuFrnB4m<)ly2jb)!T}nH>!f_2atrQh<`nWnvuneP-PdEc9+!Yj-W}BzU zbM^jWlNYL3!+p24_Z(c3rOmqbI$I-ZZ!p-NHO8Kw*$DA^ah~KWbZ=6*WwUt8Ua9eG zfd%#^x<+joz3{WvZUCoBYIWN*FtEj*=dTS;R@g*Wf4b%f%#$ZN;+e;;NI14iH;IY! zz=)U>oNR+N=!Bs8{?cHD_$13;lf7|crpl9f(LrvJ^6X>g6I;#@PTifvLJ&;JT`2W>FKPVv z3%`;uSanh+UV<=L5`pidyW(U`{YO)DT%Eg}G%+C~X+yh0q;S|+f6WLqMNTS({64u9 zsUt9jg+WDcWqW;0<_^b1~tvxGuEb<~H2|k3(bK;Y_w(rL7J% zU^3sTcvw&xqcO`xabbZZJNd01WF)z*Pp@E0xZBR^g?;TITx_LcySKe+efu-G3KuZo zscd!gZU}eq8mesf-b|&-eNn7}%QjH!OWt$K1Dh!*QSB)&51H*vfc~DeUV6sD!=mqS{8P5U*IUc@IPNaL1DaieK%Ule*T%hhA}M6q9N* zezj?F`_dS)s%_3_tcJbeT*nixLL0%9^~u4sUZ~^YJapQ@QI2_7sq@oZM3&tgcuQBq zArrRw?H!kx?J?**w#&1`ttvH77n%Wxy;}Ph9z5K(m3YDD+ZHKq#Mvv{%-AF&7i?(S z7OCcdm}!?&u@?>!Mds@V`SylhFG9;y+e8Nj(fk+}oZFM=p`Oi-Uxa<@3hIS-^$Lol zKnF>|G6v^*A+uho?c6M?KzBN-4J^cGPA<)T$(J9jv0JKXgqR!GpH(D`y74q zr0G}(DA=0WHLdClb+x@btC6)A!n%+3d!CTWc&IAqWN5UiRUkAlYEA^60ZcyFnmE59 z);W`nD1nxf=LI+fIg2PdvIZe|?7*P3D;be?7zcDA4rhWePcndr6uk5t0v8{g1jGh` z*@;XLNuVwVFaiL_WMb<%I{Qha>vcy?0qDLEvwx_KHB+pdgtqUa{VO%?sk!;m ze@&#*%*)p=X5YSk^Y$P0YX13q{z+~CV(^cW=hUcF9*@F8S&NZWFfK{OJfYWI$m&VJ zD^Sy&U&ozEdME}l_&Sx15-mxnrg(n$|5{|ZQ0Tv4#z;mF->kYb>&i$I6Ft5B%g9C) z;@y1B3a>6HH2NdJjsV4XCy1zrFl(Q&9E`Spk6Nr@ZX_sb`Im%A6>9Zj3E`95(0&^t zN0JqCdP|bX>}nx_I_YICZZSu3!)V^_17b8!Ge?gs8jNs}q;N>x9Gg@TP2!hFA-oLD z9Fmlz#G7Pqu|@CrG_Jc;K8&#H(dDsZ39k>r!>*-~U=;Bl$tNc$)vIT%>7mVvahw-+&$gQTsxUFFLxMMO;A%g8S4g-y{{{BV>KX8>r0k*M%$sViK9%bDqsQtT9|uFSI4 z)T*&fVsLi`KG-IXzrvx9gTMTKisqKAL^IvnV!>#9l|{?U{WtJ$K$N zL-Y(=2hR+lK#Bpd@L;%q*5ILNWRNqsUhc(EwQPqd4iQU_pwL&rJ;5IF3d%d&@?(SZ z8LFK|ZAZnyCCdcMkXO#amq&(?MRKzMw@3YGp~VyPa0&BC$!oKeYnIsK`}5SB243e& zvkVQe#2Kx)+v>^jJ$H=U5WVe38l9J|J6^UmhEcpfeTA(>Z>l!d;BLtKb@E%qSXVSH z8Yt++e&yB@%LcrxC7nv!0F*gkOld5ORNyP&}|d|38#y{O6GN z?!A$}vtl(qJ^nL_;-7?x|Aohd@O$_NS!td)xAS5G3M(Z~<@37Y7n#TO$#Y9+NQ{)B z51`)jG8Kcue7E0>Mo2E*6~Yw#y3%a1P@R0P)au>aXL7@ldYW)3$;oKU|J+X6_oQRG z*F8Amd(!dcE{|n~-;<8Dp`>FA_wV@K03{t8d`~(?0e+ccP|~qF;@UCf_oQQtwY}N* zq+>JAX=|AFC2|qCB7N8zX)tkD1+gmmbX=6ll{_Y6Rclq*kws2pa7oLDf;d-2gr0Ud zDL-WF*wa;EJy?|>QAMuqfY-1DhM}O9q4ti{b&USz?x46vtlqO+jfY;+IC%;WKrJDK zvGQHl&I&O>o>-Xm;xLP6rAi%?^%yunnkt9$oChVeV5vJYl;82ZOVr-y4dT;t+F5yv zM|e~q>x_O4YyokMHsN5&J#^b6bFUJ2PQBe&D3VMeyxFRbVD8Z#TkCqs<_%kVnp>`sg-WtmT9bE z3BQrQL;##jSM>F!8FEBNzTCQ}Vwx|-mpU6w z&6eGs++?>g`1y{@$VY%l+4;(HYJY=1GxQ1?atga@{EIo;%H+mMU2($Gm3qkLH?gE+ z3ilM};xv)~dO8+nG3h^}?e*GEWz-Yt&*6!9;xPyC046TqJ$b z<~tUw^y)jo3x)|iQxt5E< z@6eL#7G8noV#8q*LmAh&!^IIvW;ELy>84KPj%&svh-=p@LDby_;^U?jK-Ximm4pf2 zG=a*3Ay+bK)2Ofg?L1YY;^z`PxAtTijhi1es!}u6_7p!wYOm3FxZ3Sv(r2;EOX-qd z2l0bf?t$3F*`6}#I~;*3DqHi^2C5se4HOUN!M~~4#;(K{Lt9NvPr0^UdP|uzcA=D9 zE3a@L@PWWIGP7Czd7M8B1pJNxQANEdWXA^N%Y+;iueIy(@+61oK?~>20INFCr6gO{ znUW)zb&z$=+u{I}!xg+hgEV`5`Wy-44)})Ybs=PVN+N9@NF=i)0)}7&caiy}Ix5P1 z5JtK_@#i&^(<==9?5}#i_=@K;UNmAmElA zh(jV#xbImN(EOLI`N09qzOG-*!NN#Opd4fr8V7+J>f_?fd~g>CBP=KfS#7}vQ=(~% zM?yKsxtvGiV)&7#mtQ#-H;ALB-&S1~xF(@K0+;CB9)?;x2;56}Lt@g1Nm8U-`Dv># z%tm)6Y)N4lEkZJUo{3zZ_l}jI<#E~C-XOBNG;%d(5j;VAV>MvEJfzh-NjCjr9BWDD zB$Kh)G_ovJ(e#)#P9d2DvyqvTFT<)QP{uNh$=YlvP={BMgDMwr#3`1Wcfd$fUQe@C z)(Qv`l$P0Uy!qYi9@ysK09^oYsKv@#a%k2C?fL#g&v!Ok33$ zFXIH(Vw8KCb=tv$LgcB&NSYOIV%iXAoZ|fK-YLNs2)^WzmB^GzJ~R+)$(0Y+P`>! zR%>cwN!g0_ADiPgz=Vd^lF#s6do3R1gH{cn>bGA~=(h}Gs@zBq8!{LlY$-A!n3fKI z>`HY>-2``6lh~pe_|jqW&?>5x4yQYsVwRJv$RzOwM+=N|GXN+JFP%U-tAss!av*5;llkaD zxRYL9aWD|4)FgjA!}{rI7M5`D250X0caD}kLA4M zVf?8np&ywDc|jSREC6MaK?%QRw!~H{gUDy1Xd!U5{~B-y{+{P({f`EBZP)m3smTpJ zlmAlW?w>h5@cPWv7iTX%Y`gxtuE9@eMNQ%m`CUFzo#&y(x5tzq+v7%Fi6N0hF^V(cU<4;33f zQ9P2&DY7>RyCX`Jba(dLtm#ZHJ@8Am3_OXPDu>v-jA1?+k2%s3m#7y!FLKc_WeNa< zJfDe`{z?_4X;_k63HaN|8fcq$&MYXjN^e&-%#A=q+MI{}9opp0JNH|4QUuvP4i*Bu z#FWT8M_LESlr)WQ8$s+qK_lD%0?@LS;(si~sU?v7GE7NY@oUWPraUVGc{R@Bht5Yb z=_|xMb|R3kDLq(?SSn1+qR(`qG!n;mMsd$aj@XgiJ)R~l&;|yl&@Hzu;Z6jOL+vr_ z63%q8N?1IrPOp&?PtYB%4QkNnKvXnFqBw-|3#syDcE_aBM&p(m&b&Yyx6Hv>%7dBC z$+uk1r|fhCukJut#D3mWZLo~`vJ^uawrbbNsTSg1mfmG|JWf2934Rz20BF^3R_qY3 zM-7yq5sM!@cR*;xR@I>2P4QVSb zuLFkfqysL#yj4^ye=F$JVbYkB0oKq+^fntmX<&NDW;MWt z*wTd2J8N2d3D#Rg7)X?JJ|Ijx{9%MZ=_{a(8Ql{!7vrxLwLMZ9dGzryVq65nwxo(+ z4@{yC?gcuGeNK#M#tuIjU7}_4$!T$GXEm^6X?fhpo$#@q(dD>fyITnEFFBXjCp{PT zxyRWLqwoY0a1<59`)d75{S+TTzofShv(x2s6Ko+d+o)f*-u(7m2}i-i>QJ+kHKP4H z?`>+A0hWj%2Vg6xX=1e(nP2k9Q1`o;-v(XbbNnew*_Hhgk2R}a2RR#Qk_%5O-p!OJ z2X7(9JECKE@!;mn1j^2$(aSMT)Z_s0#4rG9&V(()j5v7{U}9Q&Fj)~58y?bq z&;51kpKc;HWcYjbqwRkJ%Q`NeODKGprOrM(|Q z^#6{}|6)D%Z?Jsx$(!3Fum1>V(deXT?D^>AoAIBe@&DmS{O|Di5SG(^5G;Et+n~_s zU-h&AL@Ssljk$xP-?Ja7Om}WiV>YJ^T_<2 z-wWYZU;LHIVV^!f)8#9(VduYn(5gT1E0&93GbO!nkC1PRw5`KX8{Gxq^1^I* zgao9g5i3dK*7W7h{yP%nb9&)Ps^OSXXFXF269A#S0uWywB9uTBM~;G4>2vTnqch|# z0=0qy08VC8Q1*kfOnlVpPkl^=b?YV{uG0J%9JV%5p%Rqd=RBUT6W`LE6Ld+CC5cMb z^UH##&pQ)ziUUSKl6aR!fvN*POA;{qf3g#0_%bH5+Tx=%kkRT^QqZ zujftrn2{lT>&DtLn$_IhnPO#et8%`oUC8q@mtaCy_%+p30d2X{mR)Rx*UT3=sv$AY zu||&+8w=a@wc^@V!>U{)W@PWN&RxDQ$OPVAR}j#2L^0mS6)z6%O;Z4Z>hXq)f~t!! zDLj6HM$m9~kE%E6HAdSlsHxDLTt0aTX;LUGBQ2M2>ec7FJ?-CN9It;B>0L#vTe7^m ztB=BO(YhWsJ+Pnk$rlkexbW3rpSYnBGb{^l*wQmJUy>%2bpCe7SfEenk~*~s?xo)n z6>6kXr5L4X7D-@irAZpAWSm+6w(pCXU8(4ZRW=_r^h~Zz{_K+ z#-OdY-tqdkGL|)nxfg?19PhmJA8GqA2X-aW^eSLm4Vdk-a`6YjOzJ^C^D&6w3-DLy z*?L#8kw#xme03%4aGBZ?_UaPa)JWPYZCOGX7rYFoFel$!zPPxnzhbiJvGTQlQ!pZ15Vv(vDV5Q{0)!;4d__{v%=@h?Y> z4WS(7=d9NLkGL=nZHhPPA~m)bErc9%3=gS`_HjBuW=|p zzvVX^7dGEM(L7Yz_PDI>Ld73?)c*qFE3dwTxNqV&7PnrRgtT&J|C7HXl>eSYhFJVN zi2p8ye@=t_cOcsT+-HDTobZFN_|TUg1PXM-?<<^MmH_DFXt}n9MmrKw8RP}-`~1d zny&;3PZXOpD@aSi0h~WT6b6tvs^k6sT+?NwnsRaKn4rQZdakQ7@Op30i4(>O%=iWI zuLSKH*((AfVN>aq9a!UGQIDf)i+)X^*k*^i1WaIgO{20WrTf&H ztPO*o59w|vM1N77@3DJ@d`QB)ZRATyVlG5kTPK*MtCh+b>Aya#GEl(oR9HO4$y0rL znRs0D+wxZ!WSv{^6?2@3kYH4o3(yrWDQ`zCy>m_qX8z27kM+tATOXZKL>h%fiW0Y+ za_G|8R*IHTn3)Uf_icGT{KUc_m|(P{{s5qK`O|>|h7TRWBQ?q?aYEy$<6#Tck}p2L z2PO9-W*6XIHAB+$t$yW&V(audVQXHtw$-u=@Fl9 ze}et^F!uhK&S@}jwQxc;S3zo0^JM-_d~#1po2`j?!*+(blZt5V$rA>w?nP0Z!FNd+ z;;-UX?P3Z}K*=iLTe+I6Ct<9kbHM^ddE;U6kXA{i_N+crsdcNl{Z~VvSFq3osvi<}I zR{ke2`#WHZn}#63{!Z6_D|vs@yUmwpAk0Ey_cyx!R)hXcwHq&?X77 zm3lgGjTd~YWB$R^EOAr_l=KMS+RMrdJ3`3B`cJfVJ9(U+s?3!c7ikr(VzyM zBMyDSg0}mm^nEdEBl+axWPwi)HzQa9I6}ng*W{ri!zL^I?SS?0qE9JIn+4r;2i*|m zPG(^7wbLyDb%)rWI0u_o>sisiKJuu*7)J@}PN=Td3goZMX&2U9!qo&i?lGRJ1PK>` zhCFvh z5TJX94A?Z)rmJ-*-23$AFi9kEJ+W+e-wW%<)3HyY6*4}bI&9qHV9rD@d)=7jW~`cpl} znv3+-+7n&WU|y zWOfjx+oTaF5uOTw1OU9J=<@9a7@ixys*U(uJX_3!@XCcXs4r--7vC7u73)7f+gdI* z`_Mj|(&o6wD=g50#k$nHG^ayg6%b0?N+akXRG8p5HWqgsQGy8~Z3?rnLdx?6-Z*6BZ3ohf@7v|DUi${Mq0y#`5fv2@<$+&O7u==y3 z%%V$IbN8Naz`lF_cFP5UtL>MMz>yDD>r_pt0sw}gLUrOusuXr;N>}}WSap1N1n4VY zjI?Or3jiRVJemv|5;9T5wbiAt`}s1kM8!+)T~VeD01MEZsMbWN2Jl9Y4r>q6?A3ey zeIlh>DK~Mb$w{9hh!7uIDz(;qgrP5p^hsB~6b4IPm#8obX!vB)U>hp+SPVla{&<0s zpaquaovJ6m$=h=#iUq8et^%gnGuNyGvWgY9TF=|6V5Aa&%v;oouqw%h4Ki)T$nClq zwav3+o;JUQ1{{-B6kuiYc~iyM$nx&01?F0TE4;!r6|j}7Z}&@6ItZ;An^wl}Y&d|T z5|6iUtg)g8FF@61{S>NUg@hfnYP`WxAgsF{oX>J-S-Yhwg}9Ho=NLM~ly-KD9Z#xc zG%-x#p+hh3kPfG~nxKoErwvX+; z20q?81hag`Ft#CmDcs(!1Z6ogUsi_EhBUoNF8H;ls$SR`JFR+hR)1Ojejw5zKIqQy z>&q^uypxubTHWFNFdO)(HQN>jmtHn17V2aipR1{vUZql|vTr7$+;C0p+loaam(9>> zU*$evjK4m34Ft$te}Am;)1zIE`z>R2?W7{MEn&!|HN1Az&D&N7?DEcr9g|)!sLYX% zAl<|rzvBSZDr)0S!R8ak{I4yVGst?HjRLHmDK1;p#L>3T*q6Cn-N-kkH)iZ@n_;o3 z>A9uT@hp}(iUP!GIDg~5?l<9d*k4_-oAuT&pZ~u2Zrc-kH()vSSkmDDq2Sx0ULdYX z3xPDvM9NOciu(l8&>>9tb}87p05VL0$J=U30#T$H02cxvok)WGcrUoD|3-v9hb2)B zuuk>=E@?ggEY>o`#x}rUfci%S_HXU1|FgLLL)G{vaesI?{dfn2aJ%CNk+zp&VQ~nk zf~@5wqSBHI4X7clUh3WPctvv#zYZ41h7FcECcPA$9grm%uDpA_^K-cP)scmEUWixw z$0=<6+1L?v_Jr{!kHX`Tc!DW&R((A2%`+sq2&T)|V#myC-z`EKh~_{T)Sw-S zmsWHJ?Tr~EQR2-6&JbRiOfHSk8J_C`WGeGU0Xa5gk*R!1UInGw$?Hls4@I+`3`r1f z^`vIGc(FczA8pMw^!Q#rA#2z2E0k^II^`lxjRSZQG!7XKy8F;}w%dM@yHUw?nYMbfg#(@4*NVEy+^81d_QsZFH$C?v!iRSz z?LN>ZUBr)o?TKh%Kan0Na;*_xnp%6mK6tocu<3X&ELB{(*rnHLy;$Z&7ry~1oOoFq z5xGi@5807mRKQlF3}$o_yu6=y29fkqIQcoJaj58`l(RYA*zn-18=9TKyYn`e-B==D z9MCnVvf=Q-j0Y|k6!yj`oo%w&Duzf@jfZ+<4VTuH+BHx~0E6Cn|9&W!odobz|h+mQWw^o-wk^AFe?68yUw-Nq5iOc?IgG;h^^hWP3Ww#Lwq zSFw(aQ#x z%G!$n?4cH`BTi6r7xRmqo~NTG#%t^tl98J!s#DT(8lVC=ldQ|hXutkW;_=RGaAY~){(c=LF+aV zG|TKzK&h=UMLr-aE_kjxKF1+{l93jATnDxwZ)F*nl@;z5Q0waLkXhOI@}r)u#DHQo zT~QS6Z?7=du8ZKE*9$tL9WqBNy0nl7v$xSts{m#DKLs5*9T>Lz^e@x;LzVYs`zvi6 z4;7Tbl&$Pv9+gdxr?m$J+j9{wCnBNTh=I>L5t!#7tXmf!A>#w3^nCg z?Nc%-S12jQLGgjT2Ionx&9ZWvt!x$dKJ==QgIvH9>Y{x@`j`4T3s2~;=yNGqyvbY& zZ+c^Xx6Sg2mD^#;@9w@mB+Zd&Nw>C+X9?eSog-o#Uu}BDz^vRxI=d*!@m-j1YQeI@ zS{p>mSVuOFb>F>}Kzea+B%eQXA8w~YT(}S-7ec)Z+!nGcjLV_0?RAqkm-)7s-^z%Yi@eP}VFO%a1jk2RM;8*Lo~@$uX~i2= zJ9OyU&KWF*o{xC919_~mAxAak(cXLN8A?rQzKxBBV6Q|5T5YPaYSMTDM>PpXWuk_UYUxI*6iNUk z$Lg8k;i-azP&Ql4jR#YcxBBQD`iqVV6*g_{g0Pr9f0~M>qj}8dKHVy6wgULsva9>G z1Wd>i6qFDMaC@3nB({=%3F&KMqNs$Pd-AW7|8z60A^qPcnE&Wz`4hnZ@t5__PyTo} z@B;yO;(00jZvf2s8vx_Q{s>^;4*>q6lO`!HCPpAJnJlD&NBdn4ul?X>8PD^CCF&iA zzo`!YDssIPC2Hem}Q zw2yG4-lbn>XYw05yguk@uIv(Ee>`e$nAJ4}W3kK&mYNtpTd6N%B{1cERMSM+__^RA z?3LZb&b8~8SJ-`AE3TTO#!6epZgw5Ex--AehJ6=E?CrjX$@J&qgEnn~EzlPWR6KDS z2aksd^I__UK8qS`yIM5R-X(q24_zo zoIIpZ6QzPL4lwqCDB!SBzhEI)27pmX+S+~FN;qdTW#-my9;mZN%2I(Nkbg8ZuZ(<{ zB;~Qlfw{QTea9AE-{Y;>gEm85K@(`Dj2;sJ}Z%uvkB&?iXe?El%L~S%GFG`-Xk)dAZ527> zJe#h}9VRtpbj3XTad1(hO!x6Ew80UEhCo3#OCu~B79YOQN@W;&^;L0YIW_{`4v9fx zYKm+BD1Gtj_VIM}9n^rr{k}G|N?(mxBCfa*>LV{?9l=!sO2<;Bvo{8o`__4c_j57L zmn_umj7ZUSM~Pu$?6MBoHjp8PP@2e-XN5d3QaLslbb@qI-zW?8@tHZjx>6u)d?4G; zrt8Kk`KHLJQ(tCV1#4NgduZGv@?V-+)Z4zfKJyf*{3vZNS1oeP>cSc`e-}LL+g*X3 zv2u_PO4UU`$&%daL+f3=ww{ZVl}p)2SKJ%B^RjFO(43kU*h}kQVt1cgt)J$uZ%epf zxT9HdgVU+R8!rn^U6)swITbD)())aH?0`s!qbFfb+TJnB*xsp_fIh(d6s|?Rh4+oG zxqW|6txMJREOa0p$9bW8T5ccnOgK8sl(z6-%nEQl7YgU^w_WvQ8!#yIc9o}4Eo03Y zU^r!WBpA3i{^gy$G4cCvNfZP|!i*w&^`9(i1$N&;gqb$p@IMr0acxg3qV)>+rH*^m zBkhZDVEc)cL*4qP9tshTYJ-*x6{@(^bFGWlD34U_KZcHzcG>Tr(6XIrgmDiK({96- zugS!Fsu><|C^)J}eWqRkESw(A?WKKuP<%2IgOPAxzR+wao&?+U`)B46;Px5>{K)Ph zAU;BBv_u~qUSa>nV*~j7Gy*Qh@M5EQ05NQ_wx0QeYzZD~@N_&f=%y5c=GA_@PyoLX z1%)y>SLfvkR++c6o!M;UEA2d9?BatI!6CxS<Ug&svw$QYVs)>?F3P@4_kt2wvyv zh0J;N>Ap!3mg$_EueHl=+^X52RDxR?5BKrecg@HwO?b)jCV7vY_JZ<`RCzteutdA< zb3sES;|!OZYhRtE+NnEbjGR}>XTD1U_S*#C`Lr*Ui}#l7dug}0u)(WCxyegs;lcW5 z9{0??xU=f?C-)x}CxdS@ws+Ccs-Cy#N-v9VD>Pht-zHx%6Ird&X3$on*7@)qaA*|G}eBhGDXC6HpORPAkw^Pnj$< zh@raXtFKqctg_ySR@dV*^88gN;^qO|H&Zv?K$*ug7=N9tlB#FU-Mthcr}f}){QGG&B~lGF=oo@aVd0pM;Ep6a~A z7YnT`{i>cj;w6@@4}tcTIzNASVZlkZ{~G=N^St=O;LU8SkKev6;dMhE4Zsr?+H;WR zXlUrAc8Ku!tl$v2jcOGM_%ezShERbz29r3iiZ<9a>lIJ-%>^BF$-I%YF+B>&!NC2y zdfv~7puZsF+lsONUDLPuXfu|^Qg+Y&fT>qtth;dbt7=D1)#H>dd6 z=>bm9vgAvsl>A2TVXN-luE&X3i5vNPcA8Er$I%M5sr$G}K|TSOol<#F(($*u&usa{ zVo&Q8Z(T7t((2nw8#v-SSAT7BV7jnn#gq9ZcGRl7hPD{#h8-BCWrZ!tIzi<@1M|LP z>|w?0Q1J1lwAP(=E@nkf>`K_%m<7uD9`E7d!P!;!KLpfq7ZAa@&mh<70r3+k<*(@x z;p;Y;hCLZ|xkemfpUquHpBQ~qXSaRn!utYcu8PH@h)}K9BU<}=mp?U!FGSr?*&q?t zuKab{>|xK9YoU)+6(qudMG-zGcWYu=Lhsebgc%J+A08_m#)U{^W^CF*uw(?Ri}htR zpNM7NEx&IZjPOgc5%i+u4TrF1YNC0D!E@!VF|9q(FJTi$W!{~iKH;-W@T$yoq7h_x z+OOps`|I(x>uv@+s&|2`5+B5^ryE|a9cACVm%0$-SsRP=EOu;16(B{6K)gQ_t}`zRy2Q9-a{P7rpPF=h@9&2w{%kw{&>Z@Cxc|cq z{6L^aUG=s}gt+WB&xBQ_Ny#gd86hMV94+r4isi!MQFv($r$HWQgF9}xnuGy(=OSLA z^cMyU)kmnTx05$dck~hY{srY?#WWykZ!pw1th- znqZtj5^_wt0Rz<3BS16HjU8p8vsnP@7G~38E$u#15eA%i*kKw@fy&h^VwPvxn`|D! zIXft5AJb@}Qgeb}BmsnMV4@J+N*TgS=hgTj7RUQg3-+ABKrxO30c5yFCy!N}?g>aC zn6T6mx{`cg09rjoFj=V;+1nk@ZB`J_jbj69GSSRAT7H3p0*k&N9*^^ftL_M?t&opJ z(CP}<0wUi1%JE%$m5xQ%mDGf{=$}kK-|m9ycDUk#x$JqLiya7U7oHF{2LSYhjVqod zDH|PcDAox?1fKN>)v9Xs(mCf+{OtIYp`9lGfljV`5pc3(O<)VHGks%<5dEn+pJk{0 zWkpq}Vg;37d}oK7Rrh8Z&jl?Jmr`S=nu(+_41KWUVGbol*jT0^Ml)W_OhRbO4o7mSx@)q(id-i9C1SFmh$rJaY@B>^px!{ zXHL$@t~yKS$rYcaR~WpEpv$Edg*GHyXsjDO71QD?L=1{crMnwYEE(qEd%)>pSX47` zT{7^)1#Ef4^kc+2(R=!ioyx#H&25ifgM_2Kr~8O*hC`Us_!Yostv1P5;IS;VuO_*b zSQ5Od%*&2Y#i_zT&jvTqjoGKYZHa)$CpwX`!m$z>y8ON$^;2E{ zAKv3XfBa8$13wb9SCqoykq9x8zw-{nS~+zx#hC+*ZKI_qg@TXW@kw}D9h*EJ3!c1a z=`W^GBN|Fw1Z5XuIa8%^Y&deLZu@o-fLvs>fwMo*>C206stA zipBA(c0L%hgI8W@m@z zw3X5^vo#f zU9K=S0H?G|pkJU6CPS+)i}qnRR0j_W8a*^11uj_oNsCS*q}3c#c##ss1;etbr{cl~ zg@_>?2b!Jc*1GW%3Ppk!yz<}{T5C#WDlun5F{Rv2rC#Y7sLTaU77=|AR-JZcd~WV1 zc0V!y+wpvtGDHui#x7Im%lIjUZTCa+hdq3#y;6I}m>0JjS8-8Fxhaoq>)3_rpi-BE z$q^ktx7}BMX$%s=jCWVp+sO>1@2pUWV!l0k+#@XL2HcoWJVi^xyb!MTwOM=<3{BZP zte)D&z2N4UtYc^BWh9D(l>R&R3rOkTmAaC~J?-mvRd{Ci$!A6jbWwN4lU9Y-LE#a< z?3D?7JWXui3J*QWowF^Jkf5GaPDqUe_w4lvd@9*jM2zt5c}`4@x-l(GIrOyMrOmhO z5qEsI-*w?cNKB*fveP;!2zhtG0`W>n-E|^c4vf zKM<-Hu8j7;5C{~8!sJ;c6lzl#UcB!cRuWoaefcm1glY|^RtVMDd_xl9yT1PL3J_bM z=0$y9bDq3eM}OtjHn0d-P0i;$8AWrn?X>n(FQQtW8G0}6We%<#eJK` z^JSv5$8$Vz@{o*<*Yg8cWY%=$N3N!n(=|K#Cb(+JHeH3GsysGBbu7gXVH9a2II#sq zaK*-7pQ1AK$j7TtDjNX3g5dC8pNi1Td13bP(f(39!>#i*ISZN5GO(Kc`mrsYTOp{Fs2+-eyyZ~Qbxw= zVC407hor-U%Gup(9r!MIiB0{W-Pbi@JL?sSnESGFAw_=KEi{^2r?HbPu+upa3++8O z47@oi=QZa)AiuF*c*-n^++a8SvTzidqRQBDN@>xaupWo9z7|`&`PU~f<%8sR(L24v zyl=g)u;*5J#G!-io*ydmEK5e1wAL13Q;TEMPPd-|N%$o|kl9ACTTB{6S%&m(TW z&z%P1jU99YR7D;px#-xszmIVnC=Ew9G=$fyoi zW<4V+#{3ob`T~jIPgJ*XQPIf88;n>2$BNcY&0QzETM*z?cExUy;fvV0rnD{=*z}X*!ZGAxN%57uJ|mO z*um4jk5var6k2Zf;BJy70C%2Ol}H!2Jx*+8;&-{Skn&6bMi2mEd_Zwj3n32;hrd4n zAR(i#l^rQqY6uGt7{X+sbf?7s8dO{U1=T-YNB-S+_s<{xQ{BK1g=$zg3Z&4^^5I*F!S?=*T9jp0>?=e(PI|+(zBsh;@C4@+?NW-Tm;-`?g=RH-DogO z_@pOHouA!}kj%+@g3u{MbRCw3Cd&}|d%So_rp;%eg)CG5(IX#HsL=3PE7>O!RXD~* zEYs;Ph8D5};8EwtRb31#iGW>PZS|V59H*Q)pKPzSSSiFt$aMs%wTC}`92rK<#CTo$ z2w>F?3QF^7zVfc#gpcN*~Atev9iVkyOUnj-9C|_k`leU z5R$3Xle_fqbECUgPrDqvyfIM!ly7sp5G!o9n6Wc(GpK5md!5?Ax$wPby+x{ySUAeg zueZe%#%R+e_*b>*ioHrYcBs26w7~i4I-gv{cQcE#y*q1jxZ^28 z9t$+pD!Xjmm&6H%Qk}FtqKUO?@IGOkrlxzd-I>xayekrCgGvBpE2X7uT!>A>HAxSq zu-OvLn6(r8@=zVFyp0-`BIo6{Yv`%EfSa@SoGwV;zP3mWEx&=ej9CyU6LjYV>o~W` z`{|}YTf@Rt#b*@T1ZNgvo&?U(9)9G;a4(0$DNMRXgi07)H*HUtow$qy^}dn||3h@} z?lDmG{@NawN8u;$PmebbU5RQ`vOQ!ruH5h`@r}|4}&-Pt3j((ly zCH7}cmW}_*IN6H7^}1fNWY4mtdsbK;p#5^d+V+ssx}>#h4sY<_{5y7>DR~z&((5w| znlsC;XV+Z+-2l6w;m&Uc*kxx$e@vKY==wt%DQ2pDiMcfN+!~*@w(6dzpbn15l*IwrDo?f3f$TQBCIS`u~$& zfdr``Ko`Ej41{m}cdht(AJ#n>>Y)|O7z%*C~lDC62N&cS!`RznEG9+=)_XoE%M~G_rq6QopO@WE%S64;{%aYH;lDq6@XQlV$pc+P=`)uHA+euRMl=PX*`L9V1<3jXP1F|af;tBV_# z*F%+(|AnnzV%IQGQEIO_$QK;9{3x-r{?byFtJEl`S~(va)$Lgo>Ne0>om?->#0fQ( zr|3(>)7qzGt5O4V>#BXnLm_h>f=be2gW4k%G~cc>$pJb2(0+LV2d&F+VV`|ER7phK zeyQJAH#llA)&$4_^_aOlbJJEzNPKXc&Mdkw>qXwdU5;}OF8eO1N>-{u$i2O0ancMs zKhZ8jM|0qfS`h6l_EIaqOXmIN-FBDV@V*WDxfS~_!1VF#D>({%_l926V1Zkx zd~Ge~8bB8H)h@mCo^3~wIG6vTuLFnxQW_HwV_0%FeP7p6`idQ42f2 zcX~?JD5i7TxNd6;Ac~RGdx*D)=)2KUg1L=OFjAJq1=;S=dy^b1FmbkM!$TYb*ItL% zFB-!_XBO|TVLi}!aqaX&)pHW75~W7=Md#_2=c%t>ym8{ps`1rpo}V~&>umt^%Tml# zygU@(#v@IvAZSY=1Xm|QuI3b$!YQkdyPS5z3)Qw|hiQYOAD#XHSy&G2#8bx2HK-iX zh*wF$HDwcc`mr>usgMm9+XJz1$qo2+_9CS$Hk2rlfsuLngdV2lKfT@wT#hz&27faC z=0Ds2LmdAbi-U{(4jlLQn@z5Lai1zJ=&;PCMX>9iKli9y1eELy{{AVNGzr=e$93TBoaC}jv zEDoG$!_gF>0Ft0;l_ZduL!dCEw4pCBQX0&|fh)d68&m-(Mzm8}!+ZNe!;}2Hvqb!5 z?_Ma_@N@!fc!~oXo({0ice25Tr<#YmSfwq3@l2;`3fu)f_n{cLA7jzM(_Z4DW@oMqrn{XwCCAWiC6(Z6GxPpv znvA?RG>2un0=m&=aPyO-pMx#z4hLkm=-3)|8&o3HpW8htFo??XD>Ai)cM0NBF~^82 zT39s9;z4FN5l68ux0g51+ht3O#O%r6*ao-5>NHMIW9F@9Ca2dI3QjWcVk=X58QOoP~MzE?+Q=&2UAU~Wi4b40QE)H`=H zYWNKvhwlZ^)y~Hfzc^06XPN_bg$Kmf>TYAw%*&B9UPsoH3^B6+BpMTe^PpYGYS|>h}ft@n>nScY6L$KChvO&+S|J1zpIy4O&4O;J#Sob ze|K#C@{=;XYA;ad-eg(da8Dfn)a&6mKs&8eNm#Yf$UTO`u<&0GUAOjw)Kk<6SeSmd zUs8N{d*Ehx97a7z$U$j|faNYy)FA4ZR>w+nc;+$rktVcwqubrqJnFo&uQBGsX>H@H z@+hQ#wmkl{Jv8UBqd%)%VX-7??+RinoLz82+uUh91sAGq2Vi=bx{o?|Fe?G<>2?7G zO}!Nij;yiyGrA9ZMexUQWVAZ~4=~BkAS5xmHA(;6V_|P)3BREi4y^w7=mljksDVM^ z{sZaz_mutX%BR06qxc7%#@%;k|GL)rTQ|kuw;BIES)WYL{($Sh1kC=m9tooN(078~ zcY&~lAvF|E0Eg7^idF}HA5vq$A+>pFeFpM(^m33(4=~NW7L+hTX`Y$eU0O85gzw-)i65>^d2*!!CjUjBHir7=?yf1Aj1la{lnERMkE z>u4lUJhr$cs$>spY$dQOH30&z(I&-QLD;cHnb4w{@G-%+le;)riL%8K&niuY#An4p zS9eANcIG@52chT>R==-&Ep|z@txgu$o9SZ4;&6Se4w!GY!zsk_L2X-%8cU*!T}<(f z%h31F#Ch)Pe8)Wq9qEAt7Q-1tmTMXwS_o|L2=5yaVSxr-UiRtf9fox)TrvnkGB z!?bZ;+}Jx=N$z$aKr&^gh_Dlexnj(GL3mWBlXbGAVX4|GL!MMseR2hTMI&jf2r*>t zUz^`A$t-Ek^ysAW-&uR79mnFv(vcY{#ubYiY^uI)(`oup53rZH_QeZ& z|8CHeUtUi?e}aYKRiaY2wkSm&{@z|%ccad*XRqu%O9)sU5pq;_wml(xJue(`_vT0 zgfUjIjpraMRyESliq1?9LPJcw;HsIt-h^RjrP9%nFjP{6P zH<@s$!NSD@&SpAj`-mpZ#p;+2M`0FQi~^5lWr_rK~It_F1Q82x2G*;~s$0Kak;i z7uW?{CBJ6Q*5y?F;`qswvaSSWKcyX8evJV!t(P9k1Tj6+xdm5S=>xn^u(L@wR}=4W zThSuyMC%H~ozgNK=2$8+z6^SFu{GOWMwi8$IKJr!TiX_H-0G92z8Kh*zSOiECS%nr zhGYkM7U?v*bUdXShEuSc5fzcjs6QRwt6ivVIC_yOP?goa`?<0oM6jOv)ck&|&dwB? zO>y=1q7G8-r`cHCE$6f4xzz)-o12?NXK%gbljg*q%cr~G_P_3dIIT9{TP}snKrK6{ z;nS^l=S7Sj?XFC+Shjb?PtGnSJMA}1=toK}c+ST`z1P2i%bXqv>s1|Rxgf&qms9Q4 zCNC5~;lHpfaAr4L8g2`8rB%f$m=}={+@i+tF7@v5SLb!-c;e=~-4bGJ(J`irRO~CK zR_B-D4>-7LJ-OYZp;5jSV+S?0X)k2#C^X5E2?Df2OX(&r$z}KjF~$8&0YQ540MBBB zWh1i_t&t<<&Pi=u6@FQLZ+cUfw+0XfFuB^0JE$yWg|`x<UzDWe z{}V5H*S0@Sli2nr`5^%mq00fq~r?dBLBOYb^T3HP#2%=`=dtr?=|&* zW?+5&!R)u0_3s1g;n~0D_20|pyk=5bu z841;f3&UtLW(Zv`F)+x2m#j)ub(cxi=7J$9UQes<2OKpNAPGT?v^H8@-k|Y3VaQDe56g85B z&}PPMmG|-Y0bXPbXe1k}xv0C*01h9gke56HXy2D;q)`l+axxYIb1vF0n*x(r^p;(i zVJS=LmzjCpQwp!R#neY*c^NXyRD~0bM?AmMA)BZJs8hbUg@b1Sw_}Em6~+ z>{3*y?kiq&xQG4AHLfs|pmO&985~?l7-t=sOE^c50|{oeT7^5Im#NYPOND`QFB%5 z@U3RPQA5z28~o{1J#!70HC96v*pRhzi|=cyRJWAaPEcS~n(1Pw%r7ek;y6EP&6LG$ z*~XJ2tjRL#llsZXDpPuk6XR|h1trvQm@&K$p}S|tel!9sj8a6%aQ4!I1o}M^^$xwH zG}u70ZntY@S$%2yG3u1t*ulphYuFcczn<1HVh(4aKPX@c5*ox@G~p!+MMZH!vSP*~~A$x)AazZ2N0M{{iOOhHkI~vazif9CQEoko>!T{vn?K z+W-l<$Pceve?k{DsFNxSSQI2Y_%opm3b&c0i-c6pQ$c$DUDfDob~CNZd)3;v+yygv2v}X_+Xek~ z*#}h^RUl6bMQ%b<0kiyWXoPh57657~x2#)c#S8&%ukYgTodBs_o{1+%4QfE&e3i44 zGDsJU9WfeUm8>(XZ1G8e@_cR6mxQ;7HrigNb{=t|t*~9UI%oMf51tH}&<9)fWC+Vd z6?tmW12%$v+3ec{A5wrGX6=%)rzM+_lD;BdcnYv#*_HqUy!0MXRi0^pHm!1g-!ug- zWmrj#BbXyxE33l%s(CRr(kpE2zjowlBUO9_wGD>t?uMrm?}}Vx4k5P*DpVkl$}>`G z!spHEEsfB|#7q$Zd)F&hKwrXoSdJJrefZqbmKQihz#Zhht}yX1FZj3#*d3sj8CDz6|L zs`^T`R(@dj>etA1U-i|jcDZ6crgwm@dGE{c4Y?~+vs*hqMY*7D6bGl*87mD@5W0%( zZspELd7Z7UW6RI2d*Sgy{Vwcjdq3Xunbibsb$rHSfDG}rjK;Wpfy~z%-+9<{3rD0x zgnil%?--K&On8OVn;DYDdzL=nU5wlKeun<5=F0*+t((<}F?mk7O*sD`t;%-)=GbEU z@W6TR4NAM6Y1gC;1fPUz`z6+B>t8mQVj^i-ZI7U}4_%){m=Q(<5d&=0R;Vy95GE-}R8f2NZ)A3EafU#mcB$0JnPz(<0J zjSHnBwemCMDj2^Qdc6Lp9@_rpkJ&ZtKLO;RsQhnC-p?)zjj#Ad=9nXA7Sdh(bN_$o zu7Bq{H{bkg8n<)|{v#m&mgoWnGH5z~Q^$WTjsJd>{X-r9Yx3_;_i7Nx3Ev5j6R(eP zK_D+!%wX7>WEB%XXbz!q;b6K8%`Y{T!TxSBL$FI@qJOuTb?2jhx0rR>2MQM~W|c=J z-z;Xzx3KjG_+P@-pv9c<&0;QeSv$w>l3Mv?hA1PeR5=m~&mZqXN+nJX!PmYZ#9=hc zDofCdpNm`*RW1*DJ5W;nwNh5T;B}_1s?Oo2DR~mcc))h58`@HLD;2#0L9{*UQmxdr zjzVCJCw@%m?&LbADhbxxG=f0ZEF-|zt-`Y=c#!yU7~E4g7s&C+T=4`ehiQGnLr2B? z<~yvhx_vA>^FG&Z)t1(ZL;7$xuv(=HD_r5|<-C>5C3v8xJn z?d+%(K~WZ*%K4D1@m*C3k7o!nwX#!{^fjXDakQlf99!?aI76st4e-~lyr`?pL8wkj zWK+A3ICR=dX{(e%$h*X~8Tc)fV4=}hm%4)0R=(^e6={(xVmP&u-J-x0^_M?Ag3-1O zXb5DUROwLO7l#rDLRt-mc&~DnTGyafyVyzA;C;`V(EQc@WYf;ScTB zquQ=N_2hH2T}Imacl6An_0`TcSsR{X6*hF+O4Lyz7-Nm|ssqq9l7X<|#jDm6v2JEK zS^VauxwoaAj^=hgRM5C)Iu&6-#_!a8_VyOtIATB+{YX~_9w@K+*v_^-#}mtPm! z%8c*xjctIJ?dbn>V}s=4)~cPy;d8q)?{);heqDJjT0tI=p1cy_=}5&q@>xxHWwVX? z3X^4LIB?K5HaU9>&=-QPv7Q~_1GW6gC>Qq^*O&^qH58B3s1(6L*BI?Cfd!PF{69%| zb^U=}tN#eS0TH>u(FK3xsbf=4eUrm~gsH*Brjw;D-vsYBzBXLE+aS94H=6g-#ap6* z*}qA2{ksA5Ur(R^qIO3oXD246f57!mhpF%JK_Ghfe<$cIG#=x^zRjkokaz{FJ;{O@ zK?m46rRgjLO6&0$7!=6aaKV}lk!D{hD=xO-LKjw$&BSclo91(fEjunf?r<8xT?Sj2 zJNBkPp1(PLLX^2K?jvxu!p3F!b#q(%vaC=}=%;0jf}7b$69>(PC}VkBqIlZt6uW!Npp+Em?yN=KcG^XOiBvxR1Ey^!My+pVxR}~7(~nDE z@~P8~Ipk_^t=jN|1cWm^1v>_Z`KJ?SY#_FCUZgVvQ>e24c@92ZOWe*=j z0ZS`=P%rs+3>2k*>Ukd`S+-6QJe*lh+5c!iH1Dc=c~i;06&CT7Gwiv9rBkhe=o35S z!p^%Z=7rp~Xr;FD)oKO7bXM~rABWe_=Or7=a(KEKWZq2YtV-{f7XayK=+agxN$XEi zi`!>9@yl4R_|0dmsS!u-xbc>I=or7;1$?RN?i}-V6tN#b*Qz3@kddEyTY{HC-VlYM zA2Dp0i{i1DwfYb_|Do-Lr2w>24_J`56c_Ht8}Ta_MN*?>d2FPe2x{Ci9OTF7B0ZQ% zw7Om_9nsUJ_Gu<*6BS?bzZ|S?InMsJzw|%2^zrv}{YPB=8`1ioAnJ>mH?L>ke2@Q| z|M)xoNxuMyS?})zvrS({eoJR9q)R&D)7+*wd3WDBeuNx_FpmEN(5^W-?9DPzTMpb?FlzW$evkN5i=lRP@RV;5km^ z?aYoi@oN#BgzzvQFM|&ZxswA8^nQZ?O1i1z1$0C`Nqu`4102iiqchVES z>;eUvqWA6t1uYjcFq&qk$&}5o)Wvi-8~aMej_G)bp!N!~dvp$I?avv{e%lcbJ?cWM z;?iZd?9l~)ME8#DAO*I-$(XIIoAr}dW9Px$vIbO!iRhkRCZ=VMd+dNh6em?-8LuLD zOO!=NQBZiREIMIR00*Uz%YIs7hMaN5#?k`DAjVb9s`9W6W%?DH0tWj^=SzYyC645L zk13@5iz|kFWTXYN*1=kkiEHvm^|w=h9-zxoISv?~yisgNJ99i!P~EUO1pe%-rDH%Q zYCs{i%Ffp3nNPZkM(^GXs}ei^78fsb*U%?zgU{%X>b8{%^*grt^JJzxUM0X;b2S%L zm#}5e%qJdvZVNjfWeonCn%kV4P zb$j^WHn+=eYd_4`>TdTez>v#D%OVXb8*K*3;{i>fh=@@0d)S(8+Wg>7H{Z8+2E!Vc z-5aZe*WVa(8#r?xVt$R^6R1w8b2J-puFF!?V1`T7Y=q$>#xI6u5c4y6hwwKxleP-au5l<(^KI?<;G)QlC2hvFGgNIcRyssmZ=Rn9*w4+vv47 z%EaCw;%0jz>d_a_>a+pq+eh>n>-DC_=qCtrIxm26*o;JdttTZt1?hb=&czuVbJwkC z-r1XH_sh!W?u|Z|4yJrHzD#b(5kR2NZUH31#q2UBG(w99)n%<|@fSeg6j3ac0z9^m zz^pJGa3v8NqDq1E=29_!wttJ+&HwwD?Xj=_pWmte!P!44Eq*-fzxxHgBhFrKr`f__ z-=bDjn3UDF_?&N1E3#D%p|c(d|7Pu4LnTXWsaD@Ot1)kpRO7v1?MmfDD$^FMUDdL| zUY~fhw2EaUu$YUuDvuWENe<$K15o&k&Id2}@(bpjNCi9nk$A;q3DiLyZ7xY1kP@mn z>Szlj$-@TbMb2i)td4l&CVei0WOrpG9e~etIB2l!M~@)LE;JCV;!&QlpQOlMy@xTD zZkg@QJ+kWMAkUA(^(80Y>Rr++>F`%js3y(C=gJ_-; zQF9}gfX$Gb8d4?@_C5pb{CGz~KRi-5mE&keozy$2=-bH6jPF+h7$($Q{iCQ#i_X%c zM=1W~@s=~hl9Qt=AdY5pnCYT<#Z49lRUuvvTu)Vv-k)+Px;U+eR&b>n70ZPV*wm+l zvod)XX#spl*VPoNy)-3F53YMg{JivxqeWn&(}QYa{at#ik%M;7vx>?^F+hF3cYr&s zHL}D(7CqfC1zAUGe>RapN}%q`U7I;>b1^W4%7@RJrg+ko@Z^a|tre-}eExT`pS^)C^k1HRDmd9J~!Q?R0sZoC^_=9czfL&&0j4Geh};&MLg zWvOC1+nO{uxCC5`Bh(l@c(e7e^z>YTU5EU>&!RjY7Q@Jew zq)iTVbtTr3O8E{bO%bp}l?st2Y((wu>ejV>o$^mU+fnrc#Q)v@t$+N!@AeCPM<9M& zTl|-GBqk&wUqhGr`xR=b?a+Hz?RjHj$N8Ag*v^OUi+$n}eg)>6IcP&cVE9Y6kyuKr+vUB&?)9Tw!RcM_7YAO|D zT#QO&LfyIb8Jnp6=u6Vr&UI4wKs|`+dbV*9o|ol&cE6Y1sm7*|vM1-xr;Zynv)Y)% zmTOaP}+qZnJ#?cr+0%Q5S&iqa7{i{ctXp) z%%*d-VYMq-deKLJ#`O)gWoUWT)Ws>dpl<&w#&n7-R@3NUXNO+tb@OW`X?FXrlS^Fs zu*;6>@1UDV+Dl?77eq@lO6l-WF?OEO*e6F)?r(DXHKJo(Bh6%}jx^yI7SW!lO}h(Z z2Du*caspS4-ewFHK($}wpSz{7ddPA<6nDVA@zEn2=S+dqJ{SLWo` z2lC;EVC-spCBTbXqkB+jQBKl{oy}EWb~EIp?gjR;4#mnEey>XfzW=KG`SkwQ+OZ3L z&CA9N`dh3%S7i=?s6CsLedJ{@!z3uGtv$G2Z5q)Q0iWn~OY_yaw(0a7txOmqv1X{- zVS<4#+3uak$JGckPKJ!r+Ne88jkMV=BrnF{M)UA$XMLV)hAF_E6O?T@rzskBY0&dr zEZJ|NaSKDIKxHf0h$P!y6k*FL$n%XlI{uP8xtX<1{_Hp$7J#Vn^m5{<@p9nak>N~5 zs|yoU#8fWS9Duo3Bt$NoqJokbz$}0m*eLaX0%C{i-$DFi^8DRW<;P#_zx)NhBM?9N zrihVn0#zh}{;r5Q;PR1@RWboOnU+T)zya!owyX z3sVDzlF{DYIKY&&#e9Z4x8p~s?T3HGmzm$j=?94l5op~?3QDgGMDpz+z2Y#maDWDX z{I$Y6lF(h&iNq%AmmpLuwq#=9&Xpx$u*oDhEIvqoXCxNg(~qO1SME$!VWe2arG?sPCg@iE7%?33ryrFY&YL!Zvh4UxFXL(M_o zxJ@6cQbz&G*lizOaTxc2$^z~Ef~rErpaGzAo@YAK2S2rm)WOiJZL3UCtYhlb(k}M8 z;2l*_B00~ z#jaIgFEW*}p>U<&QH5NQsbFhB7Og!o_9HR*9qoS5I`!w*bot810rLW#8RQUnHz|Z^uCKK$y7_Le=N+6|u%_pbbI=kW2vX!j7|8z%z?{%RmGnOtR6tyZ*^H zrpxv19j;dXp_9%2qx{t$$*cb`cJ;q#4F7P2|I2Lo@f-ffUjW4Oj_<@6e%H?ZtpZ}< zx|U6xjMcW#WYKTC!DMQV3#|gDpwCEG+D}Ws<{D|^e{XAo&l{U-oJAlJC~W19TNkNZ zw1Iz{3uKg=@cF~*LslbhUBCoOvsYh6IZhM-U4=5Qiat=*KYQ z<18kZ(zKc5WEeylNg)QaI~_=@3b0sFS>T4AO7I_xk`~%=WAxu852JNfOElB%q^SZ2 z`GLr>OssiiCXVt!UniEFJ_qo(L@ft0ox)oSq5A7qOyvB`wd>$*m306dRQ0DOGVwWf zojIW%)J}VKL9Q;JoaoLyxzBXPC_{f{%hN2Qa63_9dL~L1#-WpTFidkICvy}-?I5M; zS3kI-`1HJ;j00YdyAwzzSEmX^2jP1v=Q)<|`8Yye>J%vN+ z`k-wul|_7&bu)nil+}Gcs=I%RLx*aguYjxS%mfLPqTK_}x%s8`p&q-e3 zd3hr#12~gK_41|uvZX1H4G$ius?H;ZI)}Va>N=jwZ_B&ZMnpgcYF#hK)KACvA+VwSFP zyJp(;oR0}Kqqt#N_DtDxgg9}VeE=>)^o#lhPYXEtX!ojC@~U6^F378FmAz!JJU8>o zom9zc#)gWOV5g$P&}k^|WEpol)Gab|qF0u>`s7z1>N~g2vmt!&q}E63V=H+!>dR?u z9dpl0X%`NpejNVrm>6Nj5@oMqa<+h5N0N$6EJNsoVo|>`x!gt0_HgY=Y3SBjAa{>G zfHqeKhdXBp;Dy&Tqhr^wYG6~7uhDP0t=KozoJIkZz+^W|vjBt@dxb)N#y`ITqFg`s zk8JdRyGikfnfyOy!H+-S&-DeqBk4T9Ae|SkPX8vIu?uE0!1-M|i$QrTr)d?`!t#zo z5%j!z2Ee{z_HWX8ImO#A8I4(Bv)2U}eo4{BX6mJjF*vmiEnnCR(i!GI%H+p~z&2r1@v^TTJG00O4ofB2e^J3j?s*cBo zc2?(w_-fY%Cq8gJ*4TeAz&>R(U>8y)M)a%{oMzL6S)0Hn#oeU9T}4XZ2`2$E)B% zR?r_@79d*Nl*(^FYNfE~w@knIc9AA9`%`6iXxse4uoM!Jq_ry`_IF~_edtFr@ncYx z^?6H<5Lt7=Ytou&eB_cY{|36oqx7+>D*HuCQ`3DKX=1bN3RfI`7(GnaH2z9KE3cM% zo8ahY7j}n2uj;Gw7+%h$pN~~7JWYZ)Ssv;FuXsIp?|rBbl+J1PRD0s}AvKuVi`uZ; z@b{{Z#VQ8IAEFos;w>Ks?i*-&$l9m<{!!S0@z?#z{>^HB!ieX>UT(3TZI2l`3e%(6_{^W+=iTktFcNVplw?P=5ZW!;2heUVu$^)Jwqf{{O zn&jDY$IA{$tf`+}Xxv*rPeHW@ha5h289{xebr4tun02$PtmLK*IhpD7;Y}q($wdcV zk_A`uWDl=Q85Yz_>q6m*etW`vvYY|tcS*MgCiRBC5~3dXRS^ zHbz=+d;JyyyZ!JiEdSnurwq0yzBC{VI~qnZhvVLdo#=;s{knT35*96TLm)LLy$@lt z1hSaBHvXF7+JZ(XRIv%{RwR{y5i#|bv2l7f(%aHD2pa6a+QUj6^my3<)pmz%l{)lymKl^B#T$mp!bg0%eeTRxRS`2h)Ia#q^`Z-g&?*j8 z%Y%^6c$)_=RYiUC;@}jR z76Z53v2-6vbVsK^dK$9*L^t`5u1mlLRn+0po`71S7mp%cTg za^ZSVCx~pNH%C`k#*Cv_=RiTv*kvT;(_X^kh4j}*+EavJA^inP<=3hNu7j~Jd-Uid zPZ8Haod-(giPbDO%*&C<@k8>w?#_5Bbj3KsNGU}-Q!Z`@rBAI^iyc zV{hitI(by_!&4%JcTxGNJ>K2pX#v@w%^;5wX=ekT)RLWXMV-3a3)tt@S65W1aIiHe zxgqWWb)lMheVDds2dKU7mrs5$)pBZ3!J;5vpN8F0qCVJ7=mS^8ZBn&s{a|6MF5NWz z{4SyLeWl=>%O2gJRDO4g4@?IiWYRJ1D!TCs8czF;Ukk{3mF*ZjzAwks$F5TZ4;*;j zVNa{!H{w=qHKAA2(tT=_S353FM+VvLxol<4e8>G>y?s6%gN2@qt}$` z2yfDGK?B?y%jrBK*19N?OJDPAZSgP4QtK^}Rd$ro`sGW6eD(&J7Bf0-!wrf^9t>tB zwEK()-g}x+-6!<9l4MAyxfE)5@eHMTO?y?KaoU~c*`XfPW|nnwegdFp$nLZ7H{R)jsRDvW8dR~LFN zVd!+%&D#4-*ZMHfR;uiW>Ne(YAqOJnxJT6@tTUf|M2aUE4WAE*ax`w>UL!nq9{kvW zxz_rzQ!33u=YnL>ii>9v(>WpZYl9mz_|-qtAc(~RO)Em zFsiqO$oACNaIaG$oIchrIq-8E8}%tEQ%3I9dT68wyWI^samN56@6U;0h&YQp*)XjZ zS?5)eWdMO1zr>*U7$}eB`r(KpdP)#PDpMnfO9`Psm0^bv!Yq;HLB6$^CX)z6PQ=UX3C{ZYE0EQ&_i|%W7 z!;x57GD9Njh$k%lIem~>40bpv+RcB+uIiuyz~t>36oJV}$<-i}^}$K@S13xej8(<` z{Qg$}^o*TIc;RCXN^_>rNkgAC94A8)?r=1X1SAQHQIbwaaB3|{LYa!(aMD!&(Kw=@ zjD}Qd`Y?4VEDPRJgSqN@*3J zI=RhDL<6bL&?|r^4{+n6rh7&93eag?l*TgFWVu|tg-vOGxR}3?#unJ?UO%|!7?cfG zALnn%rP|MvRiJ__XhJqu+y}et z<#>uNZ+xvPPw@=ef3hTvue&vW9<-GmsOAOtyy9&-qe7gBXz6`Z9rS?>kH$j=eWG>4 z71upDlco~nThrtGq`I$O_ou06>(10)pQYnO*L_cs*10S_Ug)LGy=m%J%`abbj}byY zDLtfhRY5hwB2hEQ&gS;AO+l_GxA&E=?WA@{G4GQ0-gujY+zk&O)HMwvR{FuL0_aAy z8G`wO_nlH2Mal=aMMNmJi}=M+6?&TX>7P7zq$=-U6&ay^KrUo-axQ1y6yGqCqSR@)e+r_S}}W+TtQ z(IwBOKinXy;V>eT{Z3i{pzJ7c@FXVb zUe9IAw$~Y~^iuLu#ue)aua87tFI9Wrb7jq^*GJj-G78F}hen#^B|DdCD)#o!t!MeX z^fGNdhh9)BXXacl(_P-%yCG`!m;nD<2+S>SHmlOP+;DU6)lF@)$A#(T#>ap6o`1x) z{{*1;@q|Cg7x<2dJ^u-sfF^+X>lul6cS^oGi9#_4i48>(7(H8kpdhiOwV4v|*SThl zpqAf=t*~mIm$Rp&<2Pd4?_;)Jms=z!m$UKGd^GCb*1Is&=;|3fl79 zm>V2!ztiEVI~oQ_h7%-v1yCm$xkyRUwA2j5N!|)QlJeHTrPD#1l?3L(GLrhSXf~K2 zl3N&a>%a5gfzn9<#(0lYSl~^ON0q)&QVTal*3cb>;aV7n@#B;%aBIpb9K+^<`^xMr zUj_4Gd+qI2rx1`-_tF!dPpo8*?#jfJ*&DaC=<-z(=YV`Kq`416eqieph*7xjNa1EL zLMBEH+BObolru|;-3-O9dFj>n{p|NSlJpp%J^@eZW;6Hpl&bHc80ODIqvg0&VY)(M zMU9t%mc7HpX`LLBjYDAWZjW)B`hZ|@AC~{AS{tf*jx{B8rSbZ3QJO2uaAa3WkU%wz z`l495SWuUD-f!Spvr~YGFC24JhSE0`uJp}T!H9xS$gr}g?Y^7XeP`Z*Cot?ky{s9B z&O?oN!C{K4${JJ*+RGs16~^T-r7FY4iAMr_xxYBd3SF@z`(A*a78~u2RH>uRW@H5n z(5|ith$5m)cQW?Vm)yc@Hd>B7Dm|yj5x+?wvj%F#(~H~t8tL~?uh+db{i=GARDe3i zSs$#uiVSW5$sRC?Ek4=I+XzDJlv&T+<|z++d;F+FAr-G7I4 zf13;JJhA;}#fN9^Dre32_3Su_m%ZwH`Vk-NQA+8Dx}YS%45E9Cs9o)s`7?a2zIx#c zwUL*?mnsG(i{EM0NzQe;9UbT#l-{B0cDIKsS?Tbk^p*DKu{N{JiJDV^neQ^OzGzDASq#Xw>*qK^(CX+@gYPvU-fr7nX4Vboitf zASaZgL1nUYh9Ro?cAbY6n$T((!_`bdl10ONwxhg-|LBqTKU2Z=suq3UNVR;>onEHf zTL{rR2SARxJbo%-XyW(i9MsjQ8qbF3%=u<9mk?wzo4qh&44I%RU10eUcSsIPK~ie3 zFWhp9r<`clksfHM?88uTeIn*Z7sGE@m6&pYR!{o!!;c#>e#6J3ONsY*%?&ugLGMJH9(0%dcfy zQ&r!|C)^bk>ul;*`tnuwVD@D?2hZ)g;IiQ$AN5JP7h;e6Qsa9M9a-)XX@ zhF>aun^8y4+R(nUNnscJbv1ea;F&9AzW!{FQhC%8Ho2p1370(dX*N#DbJbF|GOPRb z4HQ=7+Ou+Hj;LYn<`#&3pKOK%Aa6Z67l&oK@45|eMSV~=tz^KSxcw^yu2dje#uoW$ z+YF) zL}&gg#ePIOzCIEXu`R3lacx`m$J%qT_0Q-RLSjDBI}>kwq+iKX{j|=EGXDl4ifN>c zEk}tT*_+s1d;@u~zYKa$FS*QSP&QePdmqfXuec05lJ)T__Q?3hYea6m`DM!78n-T0 zE%Pr7*)+V(?5N&#&PVNH88gVUV*{}l@FI1$XBTB5Uma!7dz(-W>r|;WXiC=djW_OQ!f_Zk&x_vqtV&$gl6S83|0^zW9*CMDW|AZK~?Y=iAB6N4LRJv%JxsA+>wFMiQ zj?;a!*egZO91?oL4w0oj1E8G4IYW_5c=Y)cq#6@|vh)EcV*?Btag5+Dg5W8A5Lspc zMk&~mi7)-9cOr*6f}<^sk8lhOSo_~yt@`UCr#}iLsxD1bcRZ~TO@KG9>$;{+Uw(Gx z$_udCsX_GM?A2G_cCVU$d9+X|A%6Ys*mc*9k-xro-Fx@>^=pF*C$I0n_`Ox)uMcz% zPkn2Z_?wg0KUT5+1dRKae*y^U!{5njRpPUqbiAA{J>C3F=P!75tFX{5OePzE!10ty zL212=vMvp*UG%iZDlZMADiMH%+C|8`l0V`7$K>hBzAKScp41QTM_}%(lW}&RKHssI zFt_>uUw$2ta_RI~nx8bxxqU+*3m<|^N+Jk?i8vg3lhqJbZ%QbNBgZxa40B%*8;T%O1M5)!SEX3LJ{MYV@FWFg4=i#q%xPQWkVU`1J_Rw@m4P<$B$sj zShuB+*gYe$8pp+yOjYSfA{w>I;K`9S{o`>lo0_a>4C@b9%F`j+wL8H0b`P+ANtuCm zI0#?e%yCyZtsq{3%5h%fUbQ!zJ93h^xy zF3FxINL^8=)^svtifYry8o-Nm!Y&t+Vp*ZDEnG1uPyiaY2`L!lpimz<(JJrtEAE-2}M&mE6ogY*35(u8A8qtDTio{L#0v~Nr`F{#v@BO>4z3zM8-TBkQW18>#kMFbB=k@-)UTsDNH1N(uv%o!Vbz&l5 zFNUS!h7u!ueqL=3tog4@*e+l1WYN^RLPS4FX*tEt!F*Bj4*wyp90gSwHU<`1Bb zp;e2$Eth214fgG6lfJh-=JCg&gFoAc52mlFv;;uDnmdLZN7!`E5vyML=r~n&#Z}Mi zp}KwAz#K<1Vy$b1!{NRu(z9h_j9Ce-u-=9SxudbhaQ@xd#%ojQqcztx{4-fY-QF4Z zFqq4-U34m+2}^WR#|WTXBge!ha<_T%%9?tjAPkQCw#uXHt_x@*?0+q z))_LMGS=&x`Z>YgE^hfDE1tcb7f%4fEe45Dd7MN1(X}axWXClqw~{o_K9hKI!|{nx zn4QpVPpVmLzaV@0K(`xSGRffFNzBZ{CDMp9fg@S!3nb!csqjHJJfI2^7IHAL8%AkP9!fv&Hb3?&QK;~ zq-5BYQ&4FU5MOD6X_v;HzY`fpwqrO={$(c(X>}ds!BYQ6wK0@S4BwvEVQ}LhhCdN46zrqs427E@Nc3gtDTqkUN%C^ZSJ0= z)V`bI0q8;eOp#f239G#&gl*0`5;S|R^9?$>7rR8ou*3PeuWjWumFJCo3Slj3DcK;? zb}z3t$I}TLPVVwvF^6-v+6$lsV%G1MO z|HMUh$o-kp95=|;l@)>u!afx{9PMnxV+xl2%acg-?Yv8iHO3=&=8!2~dSzHg$Y|VS zky>il!0js*@#U8{;{&BDu1SPaC5Co1kfVos=3Le`(IIHl84?>8M_sv+8m27?Ji#_^ zJ5*qcUK5EDgoRo?1NH9&7J+(`_eyBzpKy}>Iv2~PZ!P18y~;XZOMZRyks3ovI%;YH zRMnq6DM_0m$iV$BFSv09i4_d^U8vl%Yl<21t(_X)vi2B*q#2fkV-dgZq0Zo`PCTQn zzFOpC#Czg+gg^?G<}v*3)c9%QLZ`MM5O0f}pOs}G@iJwT}Jxsb{aEB)>*)X0WcgN@EegZ_%6LSK#97fRSA-3lrMa zmtE@!jJdfd$oRdVzUMOQm3wyemtUVq6XLwr@)%+yNAcG=TPp&hxW{ld#CsKk zAxaQdZ1KQANaLCj?>Qgh5)`xNRh+Ct=SNa*rT;`rHaE2Lh%*|{AMr92l%GYeO^PR_ zlMw86UMt1}N-&yxfKe{Wwrd?umyhq}o8lP`*B27VAltez%JRgzx7~G3JmF>Wp>Rg;;mFB(9qOYG zXksqnDvwl@FV7>L7TbqKBHffe&npPb|gkD~8%?2;^^%LW8wU z^(5VLLW^{(OX7rwDA+D3XN_`AinY}*v?S7u`HCXc zc?)o$OtODP?4&=@lJOeTB^W$&slR$rC?RG4Z0O$BR8Gp_m_`%4U7^Hb7v-KFBRex7 z!-py%tNzq(2J=ebvZgxtz!2=9NmQuJVcAP#!#nolrO-`B04ei@UQit3MelveeJ+w7 z$xdasH6k?JF1i}oy9GT(3e9bh(b1mZB=98{FC}y27U_{iGkp(AZs$^=)ebxSz$T{a!o#)8yW-Fq`)QDJXQhWLP^_t>xGr5rXB6Cn_+Bt??@^Hn z7aj3ku#0m1c#%CSSZ$zv+RL!POlo%Dy0HtM0L(vIgJABVp0v&BP%lY(;FYn7TUM-B zJ!1>H++i-t0|56zB1JM8P zGVu$y)EevGnsU@R#HyW;_EqeG+J(Xpt&Nz z6E9~!mRK}Ts)E83#obVcBj0k5vjA~7l$3ehIK&Vb&#iBMh zvYaufaYCdNuXG(6z1d0)qbH<&mC)zDNoe%%5;}~RWL&?}BTny{m&PXRQIZg2{G#0M znC5Ey^AmtWyG6eQN;jdY^GhD&^b89m8byrWSB;G zF|yX)(a*$5M^5;k!gurWk{dGuHIW(@-E{KQ5R6_4q8Fb8m1omCU6;`pUKAhFC6*N! zn?4lg9=WEt%T7(zO;DCE&$e^5ZfG7a$D1ePW7#QQEGn=$x)@x9UF{{EYmjFPicF81 zPu5EAXRswIUZ934x=o*UABNoaHmbokdBzpDUI`4cJAv4@D_6-qXn*2zfBtlRVLg%D za5q96b^n2DVnTGl3D17A? zEh`AM1~E+lS9!V7YhBqI{z;6Pdl{Dym3wizlvcKx>L&+w3Z%V+XM3Y5|`K4cWG_Kz!>O*+!X5b~G zwj@HSCi473?40^*s-7fkQsR-G+r?OI^>mYD5LZizvI?fJ*87Wv9uJ+;8 zTqHbd+Z3oI!fYxYt-a#blYH3Rm}MJ%Y?e$%y@ZtbvbeGtSEQ&K9FMna4%gUY4RVDlpkWaL?hiU=n5bpQ~M z1wdjVsGIY@B?9cP$J_S*A8g*ZnYC$K!seaH+qNcc^Govy%Ge#k^*@{y;LrKHft`QQ z`k$57|J(`p4{ZNC!RtH!E@Cz(ekouO;*n_jnK}zic`0W$;qM97~(x_Me`h5 zhpCi;_HQ0?bQ)p#daG~ooR75^A12*;ZZGbW+Chs5^aZ~5Nu}*w6%YUP`Q>i@bUSe6 z*E#9x4^Z^Hoa1Bkx{`ht@au~C$G~b{*1Ax{X2lNF+Wb*4LNXjh0!(tQLKEaSTXjTh z=L(7v)ez=8-B)$bbYNG`(}hyRY%fhT@giRvpdH$s1sAh<4_fB7I6d1s-h!DG5Iu8JN;Psqm#bnfKFYSv2-I@4{F3CKx@LjV zewyJyYnPO~N_&}ObwsxPOhAy}Bt95{(MKE}h$?4lM`r4s-HagN@hilmz%4H#RFr6$ z+jo}kE-rV6FXk6ptK*d{_=1{7f8eCl_A|=m`KZC`l^M%J`*&88Hn>4yO_rAd*or2< z%Ja;!yk2~^Wa`c`l@C0t(`7i#2m9Ys^rpUO++AJ8=iEwbN6o%+Ig7rRQiUs4NJIEsw7bmHANcD*4Q zz$xm);q3CmpW1D34QS@!$jSVrC*mUeIf zDOmfhbQ5Bx_<~jPPsUB^&gHU32>JmN>ztjLumKsV;Wy5jEUbilqJez8r0xkhMt&Sb z0NfMmAeTC}f1%JJ@?8w1VM#Jli_Z8ArvbPm2HMB!v}yF))IWRCD_e)~`;Hg&Z~1I& zwAW%q@QUTZGL`VF`swRB62&v@>x4=@4nHmd=BKpWI!>W<*RkNzVf*MLn(nlK{1~TKoal6S3X;j z(d7J0$X7lK0CU_=_7bK%Mh9k@Y@IM({*srN)WPE;Y0#bro1{&T@a+(`A9Fy|GMYUs z!NfZf;$%&?I-3ZaFX=?Cp$uvx?L7SAqa5UKupCKEjiX%Lv?mPXkv*u5ksV}@<*ka3 zZ$Cwk=AA_O4=kLuUss#E2TjmHzdQvUwJ$3OF%`&i^%BBA!-!K65P z8EEG#h-PF^7awgkT$eTJExt?~{Ar2{;`W)qbQ?VF>*Epay;67lb9od4*Uw|n2a@}y zof{5qds%~hg%9B<1&uPBT`bwAFj>Ah2rR5t&|C4|8#P=xf2T=s1&^zfK?A1)$H_tR z+eai++H&cMaC74-=QxV$SQkb!sE!#N)HQxhQcFO`8r2Z$GXp%1)H7}Udg|lQ=EI%N z>^sxDvoLSzhnbZ=vK@CMZ1@M@#=%CRC!UzXUJK`Rg58^4KferZ(Kz4L-mi3|mo4pe z_8f#t3Y91XCY*bjF28Otp|3M8*-@|-`&c0)0{E3AW%w`aCh$I7R#Z(h=TVJn&MS?tNX$Pq9$r7vTo-|>xa z-7yL8TZP7d8K^*Q-)Le>aLe{`JD(lTu-oZNN)jD{BIV0rW>#LV7NmBB1Oalp0xK0D zu6)TR!OeK>Qd(9g@m>t@FhP|8YSEao+v%2$uEjE^y8qn1N7!G_ugQPQ;N?sAe`Bz< zHQjbiC}~}|7{>OFQPlNE#XJ_PX0c@ck4l+&>}=A>n)I}4?&1Hu_riNE3hvNkRbsvoZ{*e`>e4?X^HGNzZrRJWDyZ1?JM@1wW=G3rFVVx zo2BCSZaXXXoBLd5X%+2azxlBNpXL30WUnZ6Wl5Jh0~h_k&ds`k-GL(H^pYht;6)uw zRfQnqQ{X`9iIL_@lTSu*_L5Tn_U$$yipZZ+Hav+A`9Ar5*% zpV4kwZPC%;`R=A+7t%RjC@HrxvaU_PI@9PnS$@K-d%l1`zX8b5%q5N51@G7`<7!Z7|F`L=3uLdd{K1tCX zmG+Gm4KxUfkj940JiYuG@{-6o5Iert>DK^M|4F~2MNomy*816U87oPho#NQ(@di8zkJX7Z^K2-{}Fw?{BM!_P4KQGN7&m(iW$4*r>~6l-IXCe zjSf4UD+a8Xu799aENTBESN|$z{|i*R`<`|G#UQ>tF!moc>>oJ&%F@3D>Ex^TQ?K9t zxVH425bfXoF~rE-`<)=S>`SqIlB9|$?ct23eM&w-eB0PfoVN)<%i8me&UPKgd`0eJ zQCE*V4)YswKf-X&7H=vZ{JP?$5PNz5w-qm_@qyFDUy-}Am8SF?a^--IjZ4`C7E(2d zLB?sPCq9k4SHYHv0XC1T!B<@sFkw;_qLD-iR{;~NabpII*Qe|)a55p%MpB0pA~xZa zLQ&7sHT-)y$p#<2+EcVIqMoyXZpV8Vyr_IMQHsYGqSk!UA92LuoruSmCyV2-w@0%% zXm?8UPDe?vz%jEJp~MZam%KGTGn|4X86|;>s?}sUTsDCen3)h#LMq#ASm}W~8hvw5$)fZw z^U4z%kwl=DpQ0|8(E)=Ax6n=rl)<#nIS0`Lq<*0P}N;ituBk7@VL3+ zrgn*!kNxqOM<46p++qb?#SVWFUEOnODP+Mx> zzD(^gd>P$yfB!JN7vgeX{TBgdN%QdGE2_7R8){8{diD0cbaG`Fyfi4(aPc8f-cl)) zS}Wj{MC|SjqoU(X1P@I)Dpt3Of(jZdXrFI;Tn8R(6LzhRpf^z!u}6XD=D!S0LG!l3 z%DNT!1dm)@B(I%QQ@;R=Di%1_p^42ob>icNz=XA&J_M;0$sm8wGFd+XZ#@SKD+7V^1Cw#SqEKhPwxOUu=mssK zRQKT-XK((>_13_V{ZU|)TR&3<#n&MO+)b4f0I-e(xXw5U=~t@qB$MW9DFmRBbjCI% zq&1FoZ~x}*4*8au1oO0_zjL+8ojO1p1_n1whf>8yX1iY3T-vJtqj*`*x#%w_x zUYBNvBDM+ajww1*L99WA8-qy*S0EuRn`c0%bQx*{q7mQKaaidZ>hS^HFy4+NtB}TF zR~;+fvlDn5Hx|`oTJJf>Ak!Ygw6=_5x~f>AMl;d-1wyQ{YQ@-b?FtgmpP9$e(+&+Jp?4IQ5O4Xx-#?|^l!&jNP8$w^@ zIfXO?=Wonw=xFqBA-H3o2Q^nwi1}DuzHX5qBsZBHzpr&!3g7+w#}`QgC!J}RJ#|v0 z06#4@DAcaqS~7^s$3UHS*~hHO*F>p+&Igl_`3)i0)_(ll`?LBSvYrAvv7dHBx0&(o z#GJz{`IchYDGp{?oMNHV@hADDCY$%e8tH41!J-V^^)!q4{tcOxeSLM6oT?2G3R71% zt!=8aAKhi5hES_}5EcO~v6$+|jv5Q^_*3=FsigC_A1}5W95r8&C$}U0;(*G*#~&U% ze$vCZESKxn2s;zq&v*i)wRm*spHOQW+#c&U-|bsJ+SnoQ*I(CXc8pkm&!W+<{fao= zJNQ;`N=Z%6;fw_nk#ZZq{JLzW3Kzp>oiTOQiHy-pwO2-)-_tU&Ue@ns`ZM@ky-s9X zFWa-t1L+!~0$i~?O1PMUwWa^!cx9&xIIAOVw&sIX4W5kB$Yel1>Qq!zBI4Jf^L-qE zuF?ef;ysyYpFSqYLa#aKWC!`Y4irastT%6FKqX`Wh(2S(VQUa&!DIg720&HIB@z1| zOekKw`-~9)7(g9==f!cWo+8PJ?%(9>wto>@|4UY;7aH|HuZ{S@+JEW|{o{|mmkE4F ztiAKa7lsnE)=eO2?@Us#`bppF+v+n$(T!L98Y`_x{A%B}Af%W7MEKplMK2PPKIfCg z_N`rOzdFzwGlvXqLG2o0#=+5wwn;OZdr~mta4$8u9zT=`ywO4V0QiTVWZ1k8VI*Fb z!XP_o=je%!-%IClA+HSdiz6^S@hrG(S-CG{Nj1I?t4`N{9;%vO!UDBlh+(}n3qO)V za#n;mF1-`rm##BX{+w&u($Idw4w1{GYAXho!qhWL1l;WqH!T+xdxID8n?n3p$G6<^ zqB%;$KP=6VsGJ2ecO$I*C}u!T8Amq01j0ICs>EfO4RZR$(xIJ3ofP8z}tw^nyv zq8c3lQ*2}Co?YhA%c2@KDFtSLY;jXXwzqeOL+YWRJ=u$@b~c8mc4o`kRVLcdIfz>C z!M#*i&&@HOPBTrvmZWPVjcxN;n4qYkhDs}l35@9?6;IDS7bqQh!t6fNtyPYVKC5)g zWaVzuq&Wta_%&HuTK7R;WZWP*s8+8!i%^vV&S$_fyGU)mrf!@wpF-xVD2zoTtvYRK z+cK=IW$7F9nqrtHp?;B=bvo=Wve86s5uqOJ`T@`@VuVAgykV>-GK@AMcqn4a zrJAj4*%eRDupUKL=-2SX8D$q9U3yk`^Oq*4flK)&XXJOff^>~voWg+H6`mDc^7PD( zd%8)X;C+dP%!X$9q*mdGPSr}8=LXkbdQ3E#(p#tty3^p4_QDh&th*fFF`c-EzROW~ zxC;D0+^S&;4t9D%+I-j0Upm%T8wZWvC36Sqb?5K=hf5r||Y32|jFcS%zUqnkEwPCV14wP;UUX7X=ia{`dmK!>7Z4mWJyA7%ujzkI^O+fvGy;y)c(J(O#Q=T=^xDfUSRU$IsT~$d`HZE@}=V& zbIS$oaY^!4KUsWZE(D`UA{D<*m2TGg%3Kx#FW6n_MpOM3i-ou9wLO%L0KwZS}bT8Fd4;mq(LbIJc_%s7viFT<3El6w1RN&7*4jl z=s1?()rXR`(ANsnDlZ`cvSlcBh*rS**BXaw2%?FO`hNe6P3yc+gh(SChdcz&@GMR#0++g@5HcO!8o z$!ve-&Q8NhJQpsbV9j(W@EInQE2Q6Cnot&=Y1eJm-$t%PR5>~SMMm9V3#NB4b*ct&o$$h;oA z<1zYnhMGD|`)%uoyND_iHMr8@F8<;W7-<`Pe482Lm4og>6A-DsLUbtt?iFqM__X(z z6&KVh_-`NfWy7p`aJUTjk)7-EnufBniiR_UBk-OoyWV@69-WSTYi<+)?rPZ0Xx zn~iu}0rb{1=MpNyi2HCcyFlhuSGc0g71VC_oZ~N+wv4AnMf|8@T={G1{+mJOtDSyY z=K_2*S)V)(QHeD5$iB%kk9G1P#{i$qg6RQiSliw;&IYuCY<=G4tnEPfP7LEs$U7#^ zj|P$E3lL<{l}y3;jSr)oenB{#X(GL($N?bMzoS4MEdtc&cTl!GaA6t&RmHcX4%Dkb z@r1SAN+b}^?iie(N!;^iGgrCK@Q;E-v%lxqioIH~QTvan`#)WM`r*?4Q?L3TfAnuN z0Wo!VekZ5Oucygfi8uva*Cu{Z97@4z(|=I9?2f%yIm3V!)Im+whpC4(dr?c5!&$$V z6`uiEr%fDbbUuL@Glw%|2Us5^fBZbxe0f$~Tvq(+%NzY<_^YJs%Y(gc^H!p>UTApPlVE}6$DDcYrRP}5 zzSO*ypcko{bxu>+*nmvCTAJ1)Y4A$kguWvIhb>1c)-d+xREBifUD$i=ld!rkByh@M zrw?|lRypfNaF!sNxWB2`cebQ?m2-u3))XeH^zzK5WFcbqT@FZvkFFwugbzdhSOqy{ zdh*SnhVpjfHA6x)=D^?{2g$X=v@WIft_Kt350{XyIp5jKsZf7@-Mvct*Ex5rz8+f7 z;r_*80$i1;a5w?NfCb%tvn#av&b&mc^o6y*CeBbL@vk(Y@3hjzmhZ^WY$Sr=$?Qjx z8Sc%ScP9o?CPP;HoKthaevBc|M#+PNd#c_+RZy^Vd%|_nRVB1IaAWJfJT6_whvt{qh__LGamGIlcuz)Sn2AuGOqQ&^LKSYi5m$iI-{}fqsFT zL)fx}aK>3(R0R+=>Jr$3UU>kN>NnHzTh)KShkJ>#R8P;4(2n+`+kJ{>gwHjTP}3h} z-l)D_3ndAjjItoo`w3879srMA!jcnpLBcTl0I2`>3e1{b!Yely^ zmx}+3zRfitxv>Rpmm^-Qx-Ry#(g^_L{rJ5?vtCA7s)VFGF=u08Y`lb#cfDw)?btD# zacjRFI4S2P788O|7X00Pac_fs`7A4Z@uzt;q&RVr9EAuk_W=zXW@G6g1Tpv+trtchr8^cQL1$BY!Sj=|)*_ap$$q`j zECI)!73CB@xZPY$$qlPs)X0ye)Uaj!nmFel>nf3wRI>;nb&75D*Jrl+myjyltwDAxOL#vtAjAQV3j|^9fR1}Tuo6h9@c44T@SoH8R_FkzDizJ=?+!a zQ!=22#kM}LP|$HcluFn?8gO%kdarba>1oAXJ`8-6!QM2^yOp7DbDr4U#s0Kk6)PN zRHa_W^gVakc~gkZyz2_4sZ}tVg`fzajMc5sp{aX6wp_S^~293^X4r1qSo~a{@ zy&7oM9+ardBm_>{OcRc#9Db$SOlWJ8tbQ!KN8CjC;OKcyRd8QVdgl~$Q$`mn>!pXe zg_9b6Fa&TsOPtTaJ|ul{^lkIYHPd-?c;J)wGsvCfKu{X5;bhA;mhdvwLD1zviw82Y zexewiLnH+8x{N&Gz(!>&4HuhUTaq;6=(2N(3&!adh!k0GI(~066il0t7O9*Iiqd_crZMj>h^+ zWATqu>VLrt{>MjrClmONNUb~NtCuRRZb9RbKqyj5+eb9b`+Bh#gy8gPBxcHK{bjav zKFy8=m(=njX}(WD;bb}%Q#CZs#3kh#;&FiBN?XiFIKDfS({9K5JpcabURwJY7B9=z zBSgvO%y2PC*vu2w8+b3n#dJTdAXav=vE5nM(_D0XbQc@qB7qW!lHN$r+nx0vZW&2g zSSl!R`t_i`c9eEWJR#ZY$~ls=j%)X`6LSe}eOT3=fRac;ehC@0wj6u{>N$zGu;z}M z%W$av227-m$;9X>KvC>-uJJ&Zur-;y(riRVD3C*2qc4Nmleh33Vis-Dk+&~akqe=B zL}@vimIvr%VUNs`8AtrL+Bq8PD~_Ln#5c4TC3+8XQ0cXUWmJ?a=3Ks7vzuO7#K0wY zY+86U5x%IwAykxaK+L!!%GGT^YJN-Qo^-VYXQP}|;a#NKaNQQp@hko}m{cvv&wD_% zBC#`UB)$%HVIQW^9og5hFibh+v+qTM(ubR`IEDUVy}W^&=MH<3x4@t{)W(cPsVqgG zKlv_rmel6Cj3<5J)1CQgig59?_C*yn7jyPa-*|e5YLLLaQ}U!EKwYw_V$wlT1wBPk zTNyY-(cGB(%E2tt_W4=m3ihFltdP*4TML2xl|7K4R$=u@v-!7_h_zV6e!;OvCQr^LATP?>Xq;(hy30@ z1=EIv$c&g#<|sjFW^PP<&y!`){C%Sf_vGTA2**9Kv=-{XyH?mh^GItuyxUmV%3%K8U%8$19(^e9~=bFr9 zpaJ9UCiF*vpWySw5?J32)o@k5S}q@e9aq64?D6F6P;m{FiV(ol$axlQ-Qxy$Cc=WJ ziSZPy)S`hKOvV4L#gptoyx_7DQ>oN9^<>>E(DL)?LzdeEP z2+NXFzVH;hhI4iI!ta|~Vi&pdtBd^mdA8U^b|V%0C*ve#0-o0X!i38%+qsy8GA(Ld zcrRm-lv4p=9tLKf-$IfAyB?Xr=Bbx=L~$-}WqEo$2BEdz2Y9cOBLYoE8lT2}vKAFO z8;FUVFz!wld&wxCDDgcUuyZ!`0!ETQZDseNK;QA=sHxHtfJGQkEIg{)j0aO}L*^Jx zW{jC<>=%J_G3FnZ`yGL0j&?v-Q*s$lQ#O&v@lxLE!F35A9CcjftTc8)E1fZxE0adz zLUvq2Gay=f2xU1-;zP-)0W!_I9Pz#5FSy#VvK{${kIx9Pilb4waLo#HL18cgkjad9 zz9FFc8%F!5sn?+cPg@e*(e`GqLjn#s6~2{0xq3xKr(W?O$&8~&_at3$k>t6f7b#m=lw`>- zT4z;Or|{Fc{~9@BSifL~l?ApOZsWTOgp zr9KTJx$t@;7(jBdq>*GzXwZdi?q~D~kY8&`qFpfJ;_YrO-jBx`Iw*)()+FW0KR)0)nZS2MX2g_MmpBSb*Kgjf1HM6C%QiqjDuN&+)m>W! zgCG-xKp`kaHvtJMx7>aIS2ua?%~S0~Kd*1nxvy$x;a`+9>E|m)Kf&>>+jkYMn*a6J z^&}q`GX@EaLDf^4XtS@knC2WJcLEaCqvT_NKZ*x8&ZW~>gxW+9YrFy|ir0U%l7!T+ znPD6^UwkQG=*lTROP@|>bK?v?MixSJvjn6x``AVjdc`5ebM9o@UM>bcufON$r}TK% zF$e$TQAacV8>6QPnXs`;KM0-SXhh51!@=^*Wl%e>8Gv!xYHt3yaCvGrcPy;P*>ZhwFn^)wAM#q!NB3?8L zVP}>pGpUOvjy98A*(3IlGI7BGK-F(}q+hPoZoMzRG{kRjZDdFT^L$!JuRFZ+npibM z2;2NY6$iooLM*+D+;Gr?KTJ{c+U5^D+%?*9sW60S*tAnVRao(YTT$tt!ykHyREa{r z;cf_$-P5*dbCeMIsdl28(z|$hv_a)37k=hL--i7i4*qjDnJ? z8R<|eN(^PG(=B%1lTl{7qdfL0c2JbJ7tnH4qy-n__g1Qe9~XENs|TmV(z$Nm5R$n_ zS$E}^0;p;!X)ao}K_G-j4JiO9P+d@X$MdDk#9PkvB zMyEXbRIrR{!tiNtQsvN_fr+4!g`tAOv(}GoV(p`!kiz}y`z>SQFAScK>wo{KA!d{X z^;j-aan#+up7~gFg&+)wIo3aMBRHes^Ub!@+0VDy<1)>zVbj)g?@OLt-#4(N(WLLw z^rfA<2Y+P&pB)X|RN5uoR&jVhbk8BiX}4vKTitm_^xC)8LfBWhO|-}~ZxXER6qjU; zk1<@@54(IyMwZ5Rl=33D_?TZYTRn5k(7k=3+rAxA34<$1k(GU;dYCOMh9VR({7$axKwPxY13qdBU7g1?_FlOYpLOu8#gz%yvyRJl^XALy5(*2V;TE9A4}JVI#wbw-l?(gUN+-^*hvH_Y_GVZ&lW8ph_yV5Gn7)nw>b z&q7e+YU3l(>im)p@crt5;-d(jCpkjl()z#(V{ky$s zvC|XoGaZhGDAJ39SvaqQs*lUn<;f1qN8D2b48|_9VRGm5qpHls>n{6k%m--qzEX5S3aG~R^~WY z6^RQff@;$ceD{PaWlDBbJzPv66;HTTLBUyI_fZp^v3rwLO@d6$82I*AUGjpYa!x zzPxc>KoLH3)xNIy)6nu0np^zrdKI*)IUST^ksTB%v!hxt#XZ2q1o{25|Kk#&bD~g&}4l2vxo3wmMQxi7|H+9^lCzdb-24K{U1F zHm5J~-t{m_@a144b@#drbNQ-8JEES%_}+?y;J!R|7qO?d&yTO&ml@T2`h;P`%`lai z##_;c`=36L6mMGJ^t=4p{Z`=hD?-dpvzcv<%QvTk*ommCF;k`s+La146_ACwOLicm}XQkAKr1oe#OtSts6ZCz|%XOE}g4^haPa5=E@_! zgmERiZh1o$y`0?GHCt8#A$n2ZR6r>>S!54CzD^#jkKjsq;$6f(_M&G83wFf`DzQ5m zy3zqiuJMD)Ug(9W^T@gRylo{G#SFw|-6g1`R4PaSdi-QqI9Cyf>y6`z2V zR5Xvo@9ID(SZz(3;epTujG|Sf`CG_dBn*MVp$IgwaV_E5CH9}&Suj!OV(Lb`W@nFr zmbs(X-7CLDOKSO7lFR|dFQ4aMDUf>mt{#8$9B%alVb%sf;IDPY)#19WVm5=Ldb?sl z3!VpB5>-ADiOUpl(I`WG-xx){zym71pDXD1?NS(UGoHZ|lX z<{b6#Gw*X0so6Z;vuZyPs!yp#5*c-V@{G#*pq3VC?A>T5yTdABB$He)AVP?IShlc_ zQW!o|TCT*gd7X1^vxQyD)+^a1wUw=z!e4W3udkM5=1gcuGr9rH+Jq!DmC^Ljv8&0eQsmiW31hL9ye2Diy zGZe-s2$HRS_^LqQUbpu-5~P?`uRgPQNx2k=i1|%rhdmdoY^C(-2nc!Q=ed|Z`PxE? zlJObRkV@1{6KpZWk1-11j<{DwH-4IYF2}efjpkx$A(U2|bZcr}XU%hJ;R6XGH3ixR z&^7KGhffkFj$E2j%j#WmS~J&t1^kfMr5*r>U%5{sG^{;d=|45~cy0Vef&8!nFkLkfUT2R{B1K-<%ts3AgD1D?U>a$Fzx)#a;v)_Mz2Vv7D8CV&E!Qy zdjs!1pXos<8+GEmc^}+Zt2h60O~*{+b4oI2ZB{vOWVf&ASC4vTVeq)POlru*3t*+C zJ3_Vece&WmffF@JKo-hN)8)%@CNgb5C^^0!;l?;An?!;t(+3W6wOORWQE><-0G2tZ xO%Vic7tm)FuRH$9TV#7SK_I{?;K$F83H+GAj|u#kz>f+1n81$-{HrGLKL9T02)qCQ literal 0 HcmV?d00001 diff --git a/client/web/src/assets/json/error.json b/client/web/src/assets/json/error.json new file mode 100644 index 0000000..69496b6 --- /dev/null +++ b/client/web/src/assets/json/error.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":"#60C337"},"fr":50,"ip":0,"op":100,"w":500,"h":500,"nm":"main comp","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"cross2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[431,512,0],"ix":2},"a":{"a":0,"k":[-323,834,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[668,408],[976,100]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":60,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-796,-534],"ix":2},"a":{"a":0,"k":[-534,796],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.171],"y":[1]},"o":{"x":[0.924],"y":[0]},"t":43,"s":[0]},{"t":56,"s":[100]}],"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":250,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"cross1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[431,500,0],"ix":2},"a":{"a":0,"k":[-323,834,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-408,988],[-100,680]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":60,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.167],"y":[1]},"o":{"x":[0.923],"y":[0]},"t":47,"s":[100]},{"t":60,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":250,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"cross comp","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[500,500,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"w":1000,"h":1000,"ip":0,"op":250,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"ball","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.192],"y":[1]},"o":{"x":[0.772],"y":[0]},"t":0,"s":[0]},{"t":6,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.299,"y":1},"o":{"x":0.65,"y":0},"t":0,"s":[250,88,0],"to":[0,95.167,0],"ti":[0,-54,0]},{"i":{"x":0.378,"y":1},"o":{"x":0.627,"y":0},"t":21,"s":[250,373.5,0],"to":[0,54,0],"ti":[0,41.167,0]},{"t":33,"s":[250,250,0]}],"ix":2},"a":{"a":0,"k":[53,57,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.178,0.178,0.667],"y":[1,1,1]},"o":{"x":[0.776,0.776,0.333],"y":[0,0,0]},"t":33,"s":[10,10,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":50,"s":[57.49999999999999,57.49999999999999,100]},{"t":56,"s":[55.00000000000001,55.00000000000001,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":18,"s":[758,1393]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":21,"s":[758,1114]},{"t":24,"s":[758,1393]}],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.285,"y":1},"o":{"x":0.788,"y":0},"t":18,"s":[0,0],"to":[0,36.5],"ti":[0,0]},{"i":{"x":0.272,"y":1},"o":{"x":0.801,"y":0},"t":21,"s":[0,219],"to":[0,0],"ti":[0,36.5]},{"t":24,"s":[0,0]}],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9686274509803922,0.19607843137254902,0.24705882352941178,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[53,57],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,55.128],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":250,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"end shadow","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2},"a":{"a":0,"k":[53,57,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.007,0.007,0.667],"y":[1,1,1]},"o":{"x":[0.776,0.776,0.333],"y":[0,0,0]},"t":33,"s":[0,0,100]},{"i":{"x":[0.35,0.35,0.833],"y":[1,1,1]},"o":{"x":[0.732,0.732,0.167],"y":[0,0,0]},"t":50,"s":[65,65,100]},{"t":56,"s":[55.00000000000001,55.00000000000001,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[758,1393],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[53,57],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,55.128],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":250,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"ball shadow","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.213],"y":[1]},"o":{"x":[0.871],"y":[0]},"t":16,"s":[0]},{"i":{"x":[0.264],"y":[1]},"o":{"x":[0.891],"y":[0]},"t":21,"s":[20]},{"t":26,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,413,0],"ix":2},"a":{"a":0,"k":[6,356,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.716,0.716,0.333],"y":[0,0,0]},"t":16,"s":[22.5,22.5,100]},{"i":{"x":[0.326,0.326,0.667],"y":[1,1,1]},"o":{"x":[0.813,0.813,0.333],"y":[0,0,0]},"t":21,"s":[50,50,100]},{"t":26,"s":[35,35,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[152,60],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[6,356],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":250,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/client/web/src/assets/json/linto-awake.json b/client/web/src/assets/json/linto-awake.json new file mode 100644 index 0000000..eda7c36 --- /dev/null +++ b/client/web/src/assets/json/linto-awake.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 1.0.0","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":90,"w":640,"h":427,"nm":"_8-THINKING-2-40imgs 5","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"oeil-sensitive 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":57,"s":[263.25,205.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":59.338,"s":[263.25,205.5,0],"to":[0,2.667,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":62.094,"s":[263.25,221.5,0],"to":[0,0,0],"ti":[0,2.667,0]},{"t":65,"s":[263.25,205.5,0]}],"ix":2},"a":{"a":0,"k":[56.75,-8.359,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":57,"s":[100,100,100]},{"i":{"x":[0.583,0.583,0.583],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":59.338,"s":[100,100,100]},{"i":{"x":[0.703,0.703,0.703],"y":[1,0.344,1]},"o":{"x":[0.351,0.351,0.351],"y":[0,0,0]},"t":62.094,"s":[100,30,100]},{"t":65,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.438,0],[0,-12.562],[-9.688,0],[0,12.688]],"o":[[-8.438,0],[0,10.812],[9.625,0],[0,-12.562]],"v":[[56.5,-27.406],[37.5,-8],[56.5,10.688],[76,-8]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":57,"op":177,"st":57,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"oeil-sensitive 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[263.25,205.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5.923,"s":[263.25,205.5,0],"to":[0,2.667,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9.367,"s":[263.25,221.5,0],"to":[0,0,0],"ti":[0,2.667,0]},{"t":13,"s":[263.25,205.5,0]}],"ix":2},"a":{"a":0,"k":[56.75,-8.359,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":3,"s":[100,100,100]},{"i":{"x":[0.583,0.583,0.583],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":5.923,"s":[100,100,100]},{"i":{"x":[0.703,0.703,0.703],"y":[1,0.344,1]},"o":{"x":[0.351,0.351,0.351],"y":[0,0,0]},"t":9.367,"s":[100,30,100]},{"t":13,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.438,0],[0,-12.562],[-9.688,0],[0,12.688]],"o":[[-8.438,0],[0,10.812],[9.625,0],[0,-12.562]],"v":[[56.5,-27.406],[37.5,-8],[56.5,10.688],[76,-8]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":57,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"oeil-thinking2 - Comp","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7.001,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[-12]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":59,"s":[-12]},{"t":65,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":0,"s":[320,213.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":7.001,"s":[320,213.5,0],"to":[4.167,-5.5,0],"ti":[-4.167,5.5,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":15,"s":[345,180.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":59,"s":[345,180.5,0],"to":[-4.167,5.5,0],"ti":[4.167,-5.5,0]},{"t":65,"s":[320,213.5,0]}],"ix":2},"a":{"a":0,"k":[320,213.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":640,"h":427,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"oeil-thinking2 - Comp","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7.001,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":15,"s":[-12]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":59,"s":[-12]},{"t":65,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":0,"s":[434,213.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":7.001,"s":[434,213.5,0],"to":[4.167,-5.5,0],"ti":[-4.167,5.5,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":15,"s":[459,180.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":59,"s":[459,180.5,0],"to":[-4.167,5.5,0],"ti":[4.167,-5.5,0]},{"t":65,"s":[434,213.5,0]}],"ix":2},"a":{"a":0,"k":[320,213.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":640,"h":427,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"bouche 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[320.125,286.25,0],"ix":2},"a":{"a":0,"k":[-4,161,0],"ix":1},"s":{"a":0,"k":[42,42,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.744,8.565],[-18.25,-22.5],[-29.25,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[17.169,21.167],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":-64.004004004004,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"tete","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[319.971,213.547,0],"ix":2},"a":{"a":0,"k":[-4.5,-8.395,0],"ix":1},"s":{"a":0,"k":[43,43,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[900,900],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.5,-8.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[99.398,99.398],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-20.5,88],[-155,-76],[0,0]],"o":[[-150,-61],[176,0],[0,0]],"v":[[308,314],[-20,441],[363,441]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.5,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":-60,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/client/web/src/assets/json/linto-sleep.json b/client/web/src/assets/json/linto-sleep.json new file mode 100644 index 0000000..ffcf56e --- /dev/null +++ b/client/web/src/assets/json/linto-sleep.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE ","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":40,"w":640,"h":427,"nm":"10-SLEEP-40imgs","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":5,"nm":"Z 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9.025,"s":[100]},{"t":19,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":8.55,"s":[370,79.703,0],"to":[13.833,-3.292,0],"ti":[8.792,5.292,0]},{"t":19,"s":[376.5,50.203,0]}],"ix":2},"a":{"a":0,"k":[10.811,-12.797,0],"ix":1},"s":{"a":0,"k":[236.752,236.752,100],"ix":6}},"ao":0,"t":{"d":{"k":[{"s":{"s":36,"f":"Roboto-Bold","t":"Z","j":0,"tr":0,"lh":43.2,"ls":0,"fc":[0,0,0]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[]},"ip":9,"op":20,"st":5,"bm":0},{"ddd":0,"ind":2,"ty":5,"nm":"Z 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4.75,"s":[100]},{"t":14.724609375,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4.275,"s":[320.125,125.703,0],"to":[-8.417,-7.417,0],"ti":[-10.333,3.167,0]},{"t":14.724609375,"s":[326.625,96.203,0]}],"ix":2},"a":{"a":0,"k":[10.811,-12.797,0],"ix":1},"s":{"a":0,"k":[197.68,197.68,100],"ix":6}},"ao":0,"t":{"d":{"k":[{"s":{"s":36,"f":"Roboto-Bold","t":"Z","j":0,"tr":0,"lh":43.2,"ls":0,"fc":[0,0,0]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[]},"ip":5,"op":20,"st":-4,"bm":0},{"ddd":0,"ind":3,"ty":5,"nm":"Z","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0.475,"s":[100]},{"t":10.4501953125,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[365.212,167.203,0],"to":[15.583,-11.104,0],"ti":[-4,5.312,0]},{"t":10.4501953125,"s":[372.087,135.703,0]}],"ix":2},"a":{"a":0,"k":[10.811,-12.797,0],"ix":1},"s":{"a":0,"k":[123.971,123.971,100],"ix":6}},"ao":0,"t":{"d":{"k":[{"s":{"s":36,"f":"Roboto-Bold","t":"Z","j":0,"tr":0,"lh":43.2,"ls":0,"fc":[0,0,0]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[]},"ip":0,"op":20,"st":-13,"bm":0}]}],"fonts":{"list":[{"fName":"Roboto-Bold","fFamily":"Roboto","fStyle":"Bold","ascent":75}]},"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[376.5,221.629,0],"to":[0,-0.717,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[376.5,217.326,0],"to":[0,0,0],"ti":[0,-0.717,0]},{"t":39,"s":[376.5,221.629,0]}],"ix":2},"a":{"a":0,"k":[-55.755,8.129,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[100,118,100]},{"t":39,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.858,1.938],[8.745,-6.671],[-4.295,1.55],[-8.495,-3.268]],"o":[[-10.312,-6.993],[-3.801,2.899],[9.63,-3.477],[9.554,3.676]],"v":[[-39.688,5.493],[-72.995,5.671],[-71.035,15.015],[-42.304,15.074]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.858,1.938],[8.745,-6.671],[-4.295,1.55],[-8.495,-3.268]],"o":[[-10.312,-6.993],[-3.801,2.899],[9.63,-3.477],[9.554,3.676]],"v":[[-39.688,5.493],[-72.995,5.671],[-71.035,15.015],[-42.304,15.074]],"c":true},"ix":2},"nm":"Tracé 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[2.858,1.938],[8.745,-6.671],[-4.295,1.55],[-8.495,-3.268]],"o":[[-10.312,-6.993],[-3.801,2.899],[9.63,-3.477],[9.554,3.676]],"v":[[-39.688,5.493],[-72.995,5.671],[-71.035,15.015],[-42.304,15.074]],"c":true},"ix":2},"nm":"Tracé 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[263.25,221.629,0],"to":[0,-0.717,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[263.25,217.326,0],"to":[0,0,0],"ti":[0,-0.717,0]},{"t":39,"s":[263.25,221.629,0]}],"ix":2},"a":{"a":0,"k":[-55.755,8.129,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[100,118,100]},{"t":39,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.858,1.938],[8.745,-6.671],[-4.295,1.55],[-8.495,-3.268]],"o":[[-10.312,-6.993],[-3.801,2.899],[9.63,-3.477],[9.554,3.676]],"v":[[-39.688,5.493],[-72.995,5.671],[-71.035,15.015],[-42.304,15.074]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[2.858,1.938],[8.745,-6.671],[-4.295,1.55],[-8.495,-3.268]],"o":[[-10.312,-6.993],[-3.801,2.899],[9.63,-3.477],[9.554,3.676]],"v":[[-39.688,5.493],[-72.995,5.671],[-71.035,15.015],[-42.304,15.074]],"c":true},"ix":2},"nm":"Tracé 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[2.858,1.938],[8.745,-6.671],[-4.295,1.55],[-8.495,-3.268]],"o":[[-10.312,-6.993],[-3.801,2.899],[9.63,-3.477],[9.554,3.676]],"v":[[-39.688,5.493],[-72.995,5.671],[-71.035,15.015],[-42.304,15.074]],"c":true},"ix":2},"nm":"Tracé 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"ZZZzz","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[347,213.5,0],"ix":2},"a":{"a":0,"k":[320,213.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[112,112,100]},{"t":39,"s":[100,100,100]}],"ix":6}},"ao":0,"w":640,"h":427,"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"bouche 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[320.125,286.25,0],"to":[0,-0.333,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[320.125,284.25,0],"to":[0,0,0],"ti":[0,-0.333,0]},{"t":38.999902245996,"s":[320.125,286.25,0]}],"ix":2},"a":{"a":0,"k":[-4,161,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[16,26,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":20,"s":[22,26,100]},{"t":38.999902245996,"s":[16,26,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[23.637,-0.146],[-18.25,-22.5],[-29.25,0],[-23.26,16.835],[37.088,0.949],[15.25,0]],"o":[[-23.086,0.142],[17.169,21.167],[29.25,0],[21.881,-16.423],[-25.972,-0.664],[-18.5,0]],"v":[[-59.848,135.184],[-88.494,171.542],[-4.012,191.601],[76.798,175.637],[44.087,136.184],[-4.071,135.255]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-64.004004004004,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"tete","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[319.971,213.547,0],"ix":2},"a":{"a":0,"k":[-4.5,-8.395,0],"ix":1},"s":{"a":0,"k":[43,43,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[900,900],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.5,-8.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[99.398,99.398],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-20.5,88],[-155,-76],[0,0]],"o":[[-150,-61],[176,0],[0,0]],"v":[[308,314],[-20,441],[363,441]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.5,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-60,"bm":0}],"markers":[],"chars":[{"ch":"Z","size":36,"style":"Bold","w":60.6,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[56.934,-62.695],[56.934,-71.094],[3.613,-71.094],[3.613,-59.229],[38.721,-59.229],[3.564,-8.594],[3.564,0],[57.715,0],[57.715,-11.768],[21.875,-11.768]],"c":true},"ix":2},"nm":"Z","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"Z","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Roboto"}]} \ No newline at end of file diff --git a/client/web/src/assets/json/linto-talking.json b/client/web/src/assets/json/linto-talking.json new file mode 100644 index 0000000..513b21b --- /dev/null +++ b/client/web/src/assets/json/linto-talking.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE ","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":40,"w":640,"h":427,"nm":"_7-SPEAKING-40imgs","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"oeil droit 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[376.625,205,0],"to":[0.029,1.917,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":2,"s":[376.801,216.5,0],"to":[0,0,0],"ti":[0.029,1.917,0]},{"t":5,"s":[376.625,205,0]}],"ix":2},"a":{"a":0,"k":[128,-28,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.204,0.343,0.616],"y":[1.001,1.001,1]},"o":{"x":[0.333,0.332,0.333],"y":[0,0.002,0]},"t":0,"s":[42,42,100]},{"i":{"x":[0.493,0.498,0.667],"y":[1.01,1.006,1]},"o":{"x":[0.382,0.512,0.333],"y":[0.001,0.001,0]},"t":2,"s":[53,6,100]},{"t":5,"s":[42,41.859,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[92,92],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[127.875,-27.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-60,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"oeil gauche 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[262.875,205,0],"to":[0.029,1.917,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":2,"s":[263.051,216.5,0],"to":[0,0,0],"ti":[0.029,1.917,0]},{"t":5,"s":[262.875,205,0]}],"ix":2},"a":{"a":0,"k":[-137,-28,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.127,0.192,0.833],"y":[0.905,1.016,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[42,42,100]},{"i":{"x":[0.426,0.629,0.667],"y":[1,1.108,1]},"o":{"x":[0.414,0.465,0.333],"y":[-0.067,-0.001,0]},"t":2,"s":[52.991,6.009,100]},{"t":5,"s":[42,41.859,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[92,92],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-136.625,-27.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-60,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"bouche 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[320.125,286.25,0],"ix":2},"a":{"a":0,"k":[-4,161,0],"ix":1},"s":{"a":0,"k":[42,42,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-599.536,693.738]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":1,"s":[{"i":[[22.744,8.565],[-18.25,-22.5],[-1.339,-1.232],[-27.277,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[1.158,1.428],[18.513,17.036],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-84.743,175.529],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":3.234,"s":[{"i":[[24.28,0],[-16.339,-21.006],[-1.405,-1.603],[-27.041,0],[-15.036,20.887],[28.147,0],[15.25,0]],"o":[[-24.28,0],[1.234,1.586],[17.201,19.625],[29.25,0],[15.036,-20.887],[-28.147,0],[-18.5,0]],"v":[[-59.482,135.024],[-81.351,175.113],[-77.389,179.903],[-3.714,219.577],[68.464,179.804],[44.98,135.909],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":5.471,"s":[{"i":[[6.97,-3.19],[-2.708,-11.137],[-0.567,-2.055],[-27.041,-0.066],[-2.976,25.563],[7.368,5.176],[15.25,0]],"o":[[-14.697,6.727],[0.475,1.953],[4.639,16.811],[22.036,0.054],[2.536,-21.78],[-10.885,-7.647],[-18.5,0]],"v":[[-34.482,130.857],[-47.423,172.732],[-45.543,179.605],[-3.714,219.577],[32.75,179.804],[27.718,133.528],[-4.071,124.22]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":6.588,"s":[{"i":[[6.97,-3.19],[-2.708,-11.137],[-0.567,-2.055],[-27.041,-0.066],[-2.976,25.563],[7.368,5.176],[15.25,0]],"o":[[-14.697,6.727],[0.475,1.953],[4.639,16.811],[22.036,0.054],[2.536,-21.78],[-10.885,-7.647],[-18.5,0]],"v":[[-34.482,130.857],[-47.423,172.732],[-45.543,179.605],[-3.714,219.577],[32.75,179.804],[27.718,133.528],[-4.071,124.22]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":7.705,"s":[{"i":[[6.97,-3.19],[-2.708,-11.137],[-0.567,-2.055],[-27.041,-0.066],[-2.976,25.563],[7.368,5.176],[15.25,0]],"o":[[-14.697,6.727],[0.475,1.953],[4.639,16.811],[22.036,0.054],[2.536,-21.78],[-10.885,-7.647],[-18.5,0]],"v":[[-34.482,130.857],[-47.423,172.732],[-45.543,179.605],[-3.714,219.577],[32.75,179.804],[27.718,133.528],[-4.071,124.22]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":9.941,"s":[{"i":[[24.28,0],[-16.339,-21.006],[-1.405,-1.603],[-27.041,0],[-15.036,20.887],[28.147,0],[15.25,0]],"o":[[-24.28,0],[1.234,1.586],[17.201,19.625],[29.25,0],[15.036,-20.887],[-28.147,0],[-18.5,0]],"v":[[-59.482,135.024],[-81.351,175.113],[-77.389,179.903],[-3.714,219.577],[68.464,179.804],[44.98,135.909],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":11.058,"s":[{"i":[[24.28,0],[-16.339,-21.006],[-1.405,-1.603],[-27.041,0],[-15.036,20.887],[28.147,0],[15.25,0]],"o":[[-24.28,0],[1.234,1.586],[17.201,19.625],[29.25,0],[15.036,-20.887],[-28.147,0],[-18.5,0]],"v":[[-59.482,135.024],[-81.351,175.113],[-77.389,179.903],[-3.714,219.577],[68.464,179.804],[44.98,135.909],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":12.176,"s":[{"i":[[22.744,8.565],[-18.25,-22.5],[-1.339,-1.232],[-27.277,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[1.158,1.428],[18.513,17.036],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-84.743,175.529],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":13.295,"s":[{"i":[[22.744,8.565],[-18.25,-22.5],[-1.339,-1.232],[-27.277,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[1.158,1.428],[18.513,17.036],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-84.743,175.529],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":15.529,"s":[{"i":[[24.28,0],[-3.304,-29.887],[-10.591,-11.454],[-27.041,0],[-1.929,27.625],[28.147,0],[15.25,0]],"o":[[-24.28,0],[0.221,1.998],[10.591,11.454],[29.25,0],[1.579,-22.622],[-28.147,0],[-18.5,0]],"v":[[-59.482,133.833],[-84.327,179.875],[-75.008,216.212],[-3.714,228.506],[63.702,201.827],[45.575,134.718],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":16.646,"s":[{"i":[[24.28,0],[-3.304,-29.887],[-10.591,-11.454],[-27.041,0],[-1.929,27.625],[28.147,0],[15.25,0]],"o":[[-24.28,0],[0.221,1.998],[10.591,11.454],[29.25,0],[1.579,-22.622],[-28.147,0],[-18.5,0]],"v":[[-59.482,133.833],[-84.327,179.875],[-75.008,216.212],[-3.714,228.506],[63.702,201.827],[45.575,134.718],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":17.766,"s":[{"i":[[24.28,0],[-3.304,-29.887],[-10.591,-11.454],[-27.041,0],[-1.929,27.625],[28.147,0],[15.25,0]],"o":[[-24.28,0],[0.221,1.998],[10.591,11.454],[29.25,0],[1.579,-22.622],[-28.147,0],[-18.5,0]],"v":[[-59.482,133.833],[-84.327,179.875],[-75.008,216.212],[-3.714,228.506],[63.702,201.827],[45.575,134.718],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":20,"s":[{"i":[[22.744,8.565],[-18.25,-22.5],[-1.339,-1.232],[-27.277,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[1.158,1.428],[18.513,17.036],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-84.743,175.529],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":22.234,"s":[{"i":[[24.28,0],[-16.339,-21.006],[-1.405,-1.603],[-27.041,0],[-15.036,20.887],[28.147,0],[15.25,0]],"o":[[-24.28,0],[1.234,1.586],[17.201,19.625],[29.25,0],[15.036,-20.887],[-28.147,0],[-18.5,0]],"v":[[-59.482,135.024],[-81.351,175.113],[-77.389,179.903],[-3.714,219.577],[68.464,179.804],[44.98,135.909],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":24.471,"s":[{"i":[[6.97,-3.19],[-2.708,-11.137],[-0.567,-2.055],[-27.041,-0.066],[-2.976,25.563],[7.368,5.176],[15.25,0]],"o":[[-14.697,6.727],[0.475,1.953],[4.639,16.811],[22.036,0.054],[2.536,-21.78],[-10.885,-7.647],[-18.5,0]],"v":[[-34.482,130.857],[-47.423,172.732],[-45.543,179.605],[-3.714,219.577],[32.75,179.804],[27.718,133.528],[-4.071,124.22]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":25.588,"s":[{"i":[[6.97,-3.19],[-2.708,-11.137],[-0.567,-2.055],[-27.041,-0.066],[-2.976,25.563],[7.368,5.176],[15.25,0]],"o":[[-14.697,6.727],[0.475,1.953],[4.639,16.811],[22.036,0.054],[2.536,-21.78],[-10.885,-7.647],[-18.5,0]],"v":[[-34.482,130.857],[-47.423,172.732],[-45.543,179.605],[-3.714,219.577],[32.75,179.804],[27.718,133.528],[-4.071,124.22]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":26.705,"s":[{"i":[[6.97,-3.19],[-2.708,-11.137],[-0.567,-2.055],[-27.041,-0.066],[-2.976,25.563],[7.368,5.176],[15.25,0]],"o":[[-14.697,6.727],[0.475,1.953],[4.639,16.811],[22.036,0.054],[2.536,-21.78],[-10.885,-7.647],[-18.5,0]],"v":[[-34.482,130.857],[-47.423,172.732],[-45.543,179.605],[-3.714,219.577],[32.75,179.804],[27.718,133.528],[-4.071,124.22]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":28.941,"s":[{"i":[[24.28,0],[-16.339,-21.006],[-1.405,-1.603],[-27.041,0],[-15.036,20.887],[28.147,0],[15.25,0]],"o":[[-24.28,0],[1.234,1.586],[17.201,19.625],[29.25,0],[15.036,-20.887],[-28.147,0],[-18.5,0]],"v":[[-59.482,135.024],[-81.351,175.113],[-77.389,179.903],[-3.714,219.577],[68.464,179.804],[44.98,135.909],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":30.058,"s":[{"i":[[24.28,0],[-16.339,-21.006],[-1.405,-1.603],[-27.041,0],[-15.036,20.887],[28.147,0],[15.25,0]],"o":[[-24.28,0],[1.234,1.586],[17.201,19.625],[29.25,0],[15.036,-20.887],[-28.147,0],[-18.5,0]],"v":[[-59.482,135.024],[-81.351,175.113],[-77.389,179.903],[-3.714,219.577],[68.464,179.804],[44.98,135.909],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":31.176,"s":[{"i":[[22.744,8.565],[-18.25,-22.5],[-1.339,-1.232],[-27.277,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[1.158,1.428],[18.513,17.036],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-84.743,175.529],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":32.295,"s":[{"i":[[22.744,8.565],[-18.25,-22.5],[-1.339,-1.232],[-27.277,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[1.158,1.428],[18.513,17.036],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-84.743,175.529],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":34.529,"s":[{"i":[[24.28,0],[-3.304,-29.887],[-10.591,-11.454],[-27.041,0],[-1.929,27.625],[28.147,0],[15.25,0]],"o":[[-24.28,0],[0.221,1.998],[10.591,11.454],[29.25,0],[1.579,-22.622],[-28.147,0],[-18.5,0]],"v":[[-59.482,133.833],[-84.327,179.875],[-75.008,216.212],[-3.714,228.506],[63.702,201.827],[45.575,134.718],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":35.646,"s":[{"i":[[24.28,0],[-3.304,-29.887],[-10.591,-11.454],[-27.041,0],[-1.929,27.625],[28.147,0],[15.25,0]],"o":[[-24.28,0],[0.221,1.998],[10.591,11.454],[29.25,0],[1.579,-22.622],[-28.147,0],[-18.5,0]],"v":[[-59.482,133.833],[-84.327,179.875],[-75.008,216.212],[-3.714,228.506],[63.702,201.827],[45.575,134.718],[-4.071,135.827]],"c":true}]},{"i":{"x":0.54,"y":1},"o":{"x":0.5,"y":0},"t":36.766,"s":[{"i":[[24.28,0],[-3.304,-29.887],[-10.591,-11.454],[-27.041,0],[-1.929,27.625],[28.147,0],[15.25,0]],"o":[[-24.28,0],[0.221,1.998],[10.591,11.454],[29.25,0],[1.579,-22.622],[-28.147,0],[-18.5,0]],"v":[[-59.482,133.833],[-84.327,179.875],[-75.008,216.212],[-3.714,228.506],[63.702,201.827],[45.575,134.718],[-4.071,135.827]],"c":true}]},{"t":38.999902245996,"s":[{"i":[[22.744,8.565],[-18.25,-22.5],[-1.339,-1.232],[-27.277,0],[-23.26,16.835],[34.543,-13.535],[15.25,0]],"o":[[-21.605,-8.137],[1.158,1.428],[18.513,17.036],[29.25,0],[21.881,-16.423],[-22.969,9],[-18.5,0]],"v":[[-58.887,130.857],[-88.494,171.542],[-84.743,175.529],[-4.012,191.601],[76.798,175.637],[44.087,132.337],[-4.071,139.101]],"c":true}]}],"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-64.004004004004,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"tete","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[319.971,213.547,0],"ix":2},"a":{"a":0,"k":[-4.5,-8.395,0],"ix":1},"s":{"a":0,"k":[43,43,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[900,900],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.5,-8.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[99.398,99.398],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-20.5,88],[-155,-76],[0,0]],"o":[[-150,-61],[176,0],[0,0]],"v":[[308,314],[-20,441],[363,441]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.5,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-60,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/client/web/src/assets/json/linto-think.json b/client/web/src/assets/json/linto-think.json new file mode 100644 index 0000000..dc92e17 --- /dev/null +++ b/client/web/src/assets/json/linto-think.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE ","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":40,"w":640,"h":427,"nm":"_8-THINKING-40imgs","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[416.625,205,0],"ix":2},"a":{"a":0,"k":[128,-28,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":12,"s":[73,72.795,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":14,"s":[60,59.831,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":29,"s":[60,59.831,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[73,72.795,100]},{"t":35,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[92,92],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[127.875,-27.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8,"op":48,"st":-52,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[320.214,205.146,0],"ix":2},"a":{"a":0,"k":[128,-28,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":4,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[73,72.795,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,59.831,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[60,59.831,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":27,"s":[73,72.795,100]},{"t":31,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[92,92],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[127.875,-27.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":44,"st":-56,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[222.875,205,0],"ix":2},"a":{"a":0,"k":[-137,-28,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":4,"s":[73,72.795,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":6,"s":[60,59.831,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":21,"s":[60,59.831,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[73,72.795,100]},{"t":27,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[92,92],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-136.625,-27.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-60,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"tete","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[319.971,213.547,0],"ix":2},"a":{"a":0,"k":[-4.5,-8.395,0],"ix":1},"s":{"a":0,"k":[43,43,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[900,900],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-4.5,-8.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[99.398,99.398],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-20.5,88],[-155,-76],[0,0]],"o":[[-150,-61],[176,0],[0,0]],"v":[[308,314],[-20,441],[363,441]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23137255013,0.733333349228,0.945098042488,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Remplissage 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.5,-2],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":-60,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/client/web/src/assets/json/microphone.json b/client/web/src/assets/json/microphone.json new file mode 100644 index 0000000..0ca3053 --- /dev/null +++ b/client/web/src/assets/json/microphone.json @@ -0,0 +1,64 @@ +{ "v": "4.8.0", "meta": { "g": "LottieFiles AE 1.0.0", "a": "", "k": "", "d": "", "tc": "" }, "fr": 30, "ip": 0, "op": 30, "w": 805, "h": 601, "nm": "23-MICRO-15imgs 2", "ddd": 0, "assets": [], "layers": [{ "ddd": 0, "ind": 1, "ty": 4, "nm": "Calque de forme 1", "sr": 1, "ks": { "o": { "a": 0, "k": 100, "ix": 11 }, "r": { "a": 0, "k": 0, "ix": 10 }, "p": { "a": 0, "k": [402.5, 399.5, 0], "ix": 2 }, "a": { "a": 0, "k": [0, 20, 0], "ix": 1 }, "s": { "a": 0, "k": [511, 511, 100], "ix": 6 } }, "ao": 0, "hasMask": true, "masksProperties": [{ "inv": false, "mode": "f", "pt": { "a": 0, "k": { "i": [ + [-3.665, 0], + [0, 3.665], + [0, 0], + [3.665, 0], + [0, -3.665], + [0, 0] + ], "o": [ + [3.665, 0], + [0, 0], + [0, -3.665], + [-3.665, 0], + [0, 0], + [0, 3.665] + ], "v": [ + [0.5, 6.02], + [7.124, -0.604], + [7.124, -13.852], + [0.5, -20.476], + [-6.124, -13.852], + [-6.124, -0.604] + ], "c": true }, "ix": 1 }, "o": { "a": 0, "k": 100, "ix": 3 }, "x": { "a": 0, "k": 0, "ix": 4 }, "nm": "Masque 1" }, { "inv": false, "mode": "f", "pt": { "a": 0, "k": { "i": [ + [0, 0], + [6.094, 0], + [0, 6.094], + [0, 0], + [-7.485, -1.082], + [0, 0], + [0, 0], + [0, 0], + [0, 7.794] + ], "o": [ + [0, 6.094], + [-6.094, 0], + [0, 0], + [0, 7.794], + [0, 0], + [0, 0], + [0, 0], + [7.485, -1.082], + [0, 0] + ], "v": [ + [11.54, -0.604], + [0.5, 10.436], + [-10.54, -0.604], + [-14.956, -0.604], + [-1.708, 14.675], + [-1.708, 21.476], + [2.708, 21.476], + [2.708, 14.675], + [15.956, -0.604] + ], "c": true }, "ix": 1 }, "o": { "a": 0, "k": 100, "ix": 3 }, "x": { "a": 0, "k": 0, "ix": 4 }, "nm": "Masque 2" }], "shapes": [{ "ty": "rc", "d": 1, "s": { "a": 0, "k": [100, 100], "ix": 2 }, "p": { "a": 0, "k": [0, 0], "ix": 3 }, "r": { "a": 0, "k": 0, "ix": 4 }, "nm": "Tracé rectangulaire 1", "mn": "ADBE Vector Shape - Rect", "hd": false }, { "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 1], "ix": 4 }, "o": { "a": 0, "k": 100, "ix": 5 }, "r": 1, "bm": 0, "nm": "Fond 1", "mn": "ADBE Vector Graphic - Fill", "hd": false }], "ip": 0, "op": 30, "st": 0, "bm": 0 }, { "ddd": 0, "ind": 2, "ty": 4, "nm": "tete", "sr": 1, "ks": { "o": { "a": 0, "k": 100, "ix": 11 }, "r": { "a": 0, "k": 0, "ix": 10 }, "p": { "a": 0, "k": [402.471, 300.547, 0], "ix": 2 }, "a": { "a": 0, "k": [-4.5, -8.395, 0], "ix": 1 }, "s": { "a": 0, "k": [43, 43, 100], "ix": 6 } }, "ao": 0, "shapes": [{ "ty": "gr", "it": [{ "d": 1, "ty": "el", "s": { "a": 0, "k": [900, 900], "ix": 2 }, "p": { "a": 0, "k": [0, 0], "ix": 3 }, "nm": "Tracé d'ellipse 1", "mn": "ADBE Vector Shape - Ellipse", "hd": false }, { "ty": "fl", "c": { "a": 0, "k": [0.23137255013, 0.733333349228, 0.945098042488, 1], "ix": 4 }, "o": { "a": 0, "k": 100, "ix": 5 }, "r": 1, "bm": 0, "nm": "Remplissage 1", "mn": "ADBE Vector Graphic - Fill", "hd": false }, { "ty": "tr", "p": { "a": 0, "k": [-4.5, -8.5], "ix": 2 }, "a": { "a": 0, "k": [0, 0], "ix": 1 }, "s": { "a": 0, "k": [99.398, 99.398], "ix": 3 }, "r": { "a": 0, "k": 0, "ix": 6 }, "o": { "a": 0, "k": 100, "ix": 7 }, "sk": { "a": 0, "k": 0, "ix": 4 }, "sa": { "a": 0, "k": 0, "ix": 5 }, "nm": "Transformer " }], "nm": "Ellipse 1", "np": 2, "cix": 2, "bm": 0, "ix": 1, "mn": "ADBE Vector Group", "hd": false }, { "ty": "gr", "it": [{ "ind": 0, "ty": "sh", "ix": 1, "ks": { "a": 0, "k": { "i": [ + [-20.5, 88], + [-155, -76], + [0, 0] + ], "o": [ + [-150, -61], + [176, 0], + [0, 0] + ], "v": [ + [308, 314], + [-20, 441], + [363, 441] + ], "c": true }, "ix": 2 }, "nm": "Tracé 1", "mn": "ADBE Vector Shape - Group", "hd": false }, { "ty": "fl", "c": { "a": 0, "k": [0.23137255013, 0.733333349228, 0.945098042488, 1], "ix": 4 }, "o": { "a": 0, "k": 100, "ix": 5 }, "r": 1, "bm": 0, "nm": "Remplissage 1", "mn": "ADBE Vector Graphic - Fill", "hd": false }, { "ty": "tr", "p": { "a": 0, "k": [-0.5, -2], "ix": 2 }, "a": { "a": 0, "k": [0, 0], "ix": 1 }, "s": { "a": 0, "k": [100, 100], "ix": 3 }, "r": { "a": 0, "k": 0, "ix": 6 }, "o": { "a": 0, "k": 100, "ix": 7 }, "sk": { "a": 0, "k": 0, "ix": 4 }, "sa": { "a": 0, "k": 0, "ix": 5 }, "nm": "Transformer " }], "nm": "Forme 1", "np": 2, "cix": 2, "bm": 0, "ix": 2, "mn": "ADBE Vector Group", "hd": false }], "ip": 0, "op": 30, "st": -60, "bm": 0 }, { "ddd": 0, "ind": 3, "ty": 4, "nm": "Calque de forme 2", "sr": 1, "ks": { "o": { "a": 0, "k": 100, "ix": 11 }, "r": { "a": 0, "k": 0, "ix": 10 }, "p": { "a": 0, "k": [402.5, 300.5, 0], "ix": 2 }, "a": { "a": 0, "k": [0, 0, 0], "ix": 1 }, "s": { "a": 1, "k": [{ "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 1] }, "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0] }, "t": 0, "s": [85, 85, 100] }, { "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 1] }, "o": { "x": [0.333, 0.333, 0.333], "y": [0, 0, 0] }, "t": 15.263, "s": [112, 112, 100] }, { "t": 29, "s": [85, 85, 100] }], "ix": 6 } }, "ao": 0, "shapes": [{ "ty": "gr", "it": [{ "d": 1, "ty": "el", "s": { "a": 0, "k": [533, 533], "ix": 2 }, "p": { "a": 0, "k": [0, 0], "ix": 3 }, "nm": "Tracé d'ellipse 1", "mn": "ADBE Vector Shape - Ellipse", "hd": false }, { "ty": "fl", "c": { "a": 0, "k": [0.017424067482, 0.290245771408, 0.403921574354, 1], "ix": 4 }, "o": { "a": 0, "k": 100, "ix": 5 }, "r": 1, "bm": 0, "nm": "Fond 1", "mn": "ADBE Vector Graphic - Fill", "hd": false }, { "ty": "tr", "p": { "a": 0, "k": [0, -1], "ix": 2 }, "a": { "a": 0, "k": [0, 0], "ix": 1 }, "s": { "a": 0, "k": [100, 100], "ix": 3 }, "r": { "a": 0, "k": 0, "ix": 6 }, "o": { "a": 0, "k": 100, "ix": 7 }, "sk": { "a": 0, "k": 0, "ix": 4 }, "sa": { "a": 0, "k": 0, "ix": 5 }, "nm": "Transformer " }], "nm": "Ellipse 1", "np": 3, "cix": 2, "bm": 0, "ix": 1, "mn": "ADBE Vector Group", "hd": false }], "ip": 0, "op": 30, "st": 0, "bm": 0 }], "markers": [] } \ No newline at end of file diff --git a/client/web/src/assets/json/validation.json b/client/web/src/assets/json/validation.json new file mode 100644 index 0000000..3216720 --- /dev/null +++ b/client/web/src/assets/json/validation.json @@ -0,0 +1 @@ +{"v": "5.5.7", "meta": {"g": "LottieFiles AE 0.1.20", "a": "", "k": "", "d": "", "tc": ""}, "fr": 29.9700012207031, "ip": 0, "op": 60.0000024438501, "w": 80, "h": 80, "nm": "eclats ronds", "ddd": 0, "assets": [], "layers": [{"ddd": 0, "ind": 1, "ty": 4, "nm": "iconeValider", "sr": 1, "ks": {"o": {"a": 0, "k": 100, "ix": 11}, "r": {"a": 0, "k": 0, "ix": 10}, "p": {"a": 0, "k": [41.125, 39.312, 0], "ix": 2}, "a": {"a": 0, "k": [0, 0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100, 100], "ix": 6}}, "ao": 0, "shapes": [{"ty": "gr", "it": [{"ind": 0, "ty": "sh", "ix": 1, "ks": {"a": 0, "k": {"i": [[0, 0], [0, 0], [0, 0]], "o": [[0, 0], [0, 0], [0, 0]], "v": [[-8.625, 1.25], [-4, 5.875], [6.375, -4.5]], "c": false}, "ix": 2}, "nm": "Trac\u00e9 1", "mn": "ADBE Vector Shape - Group", "hd": false}, {"ty": "st", "c": {"a": 0, "k": [1, 1, 1, 1], "ix": 3}, "o": {"a": 0, "k": 100, "ix": 4}, "w": {"a": 0, "k": 3, "ix": 5}, "lc": 1, "lj": 1, "ml": 4, "bm": 0, "nm": "Contour 1", "mn": "ADBE Vector Graphic - Stroke", "hd": false}, {"ty": "tr", "p": {"a": 0, "k": [0, 0], "ix": 2}, "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "r": {"a": 0, "k": 0, "ix": 6}, "o": {"a": 0, "k": 100, "ix": 7}, "sk": {"a": 0, "k": 0, "ix": 4}, "sa": {"a": 0, "k": 0, "ix": 5}, "nm": "Transformer "}], "nm": "Forme 1", "np": 3, "cix": 2, "bm": 0, "ix": 1, "mn": "ADBE Vector Group", "hd": false}, {"ty": "tm", "s": {"a": 0, "k": 0, "ix": 1}, "e": {"a": 1, "k": [{"i": {"x": [0.667], "y": [0.742]}, "o": {"x": [0.333], "y": [0]}, "t": 11, "s": [0]}, {"i": {"x": [0.667], "y": [1]}, "o": {"x": [0.333], "y": [0.611]}, "t": 13, "s": [37.2]}, {"t": 21.0000008553475, "s": [100]}], "ix": 2}, "o": {"a": 0, "k": 0, "ix": 3}, "m": 1, "ix": 2, "nm": "R\u00e9duire les trac\u00e9s 1", "mn": "ADBE Vector Filter - Trim", "hd": false}, {"ty": "st", "c": {"a": 0, "k": [1, 1, 1, 1], "ix": 3}, "o": {"a": 0, "k": 100, "ix": 4}, "w": {"a": 1, "k": [{"i": {"x": [0.667], "y": [1]}, "o": {"x": [0.333], "y": [0]}, "t": 11, "s": [2]}, {"i": {"x": [0.667], "y": [1]}, "o": {"x": [0.333], "y": [0]}, "t": 18, "s": [2]}, {"t": 21.0000008553475, "s": [2]}], "ix": 5}, "lc": 1, "lj": 2, "bm": 0, "nm": "Contour 1", "mn": "ADBE Vector Graphic - Stroke", "hd": false}], "ip": 0, "op": 43.0000017514259, "st": 0, "bm": 0}, {"ddd": 0, "ind": 2, "ty": 4, "nm": "rond vert", "sr": 1, "ks": {"o": {"a": 0, "k": 100, "ix": 11}, "r": {"a": 0, "k": 0, "ix": 10}, "p": {"a": 0, "k": [40, 40, 0], "ix": 2}, "a": {"a": 0, "k": [0, 0, 0], "ix": 1}, "s": {"a": 1, "k": [{"i": {"x": [0.376, 0.376, 0.667], "y": [1.002, 1.002, 1]}, "o": {"x": [0.333, 0.333, 0.333], "y": [0, 0, 0]}, "t": 0, "s": [0, 0, 100]}, {"i": {"x": [0.243, 0.243, 0.833], "y": [0.998, 0.998, 1]}, "o": {"x": [0.857, 0.857, 0.167], "y": [0.004, 0.004, 0]}, "t": 13, "s": [100, 100, 100]}, {"i": {"x": [0.146, 0.146, 0.833], "y": [0.995, 0.995, 1]}, "o": {"x": [0.798, 0.798, 0.167], "y": [0.004, 0.004, 0]}, "t": 16, "s": [140.21, 140.21, 100]}, {"t": 19.0000007738859, "s": [100, 100, 100]}], "ix": 6}}, "ao": 0, "shapes": [{"ty": "gr", "it": [{"d": 1, "ty": "el", "s": {"a": 0, "k": [38.002, 38.002], "ix": 2}, "p": {"a": 0, "k": [0, 0], "ix": 3}, "nm": "Trac\u00e9 d'ellipse 1", "mn": "ADBE Vector Shape - Ellipse", "hd": false}, {"ty": "fl", "c": {"a": 0, "k": [0, 0.776470648074, 0.63137254902, 1], "ix": 4}, "o": {"a": 0, "k": 100, "ix": 5}, "r": 1, "bm": 0, "nm": "Fond 1", "mn": "ADBE Vector Graphic - Fill", "hd": false}, {"ty": "tr", "p": {"a": 0, "k": [0, 0], "ix": 2}, "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "r": {"a": 0, "k": 0, "ix": 6}, "o": {"a": 0, "k": 100, "ix": 7}, "sk": {"a": 0, "k": 0, "ix": 4}, "sa": {"a": 0, "k": 0, "ix": 5}, "nm": "Transformer "}], "nm": "Ellipse 1", "np": 3, "cix": 2, "bm": 0, "ix": 1, "mn": "ADBE Vector Group", "hd": false}], "ip": 0, "op": 43.0000017514259, "st": 0, "bm": 0}, {"ddd": 0, "ind": 3, "ty": 4, "nm": "\u00e9clats ronds", "sr": 1, "ks": {"o": {"a": 0, "k": 100, "ix": 11}, "r": {"a": 0, "k": 0, "ix": 10}, "p": {"a": 0, "k": [40.25, 40.25, 0], "ix": 2}, "a": {"a": 0, "k": [0, 0, 0], "ix": 1}, "s": {"a": 1, "k": [{"i": {"x": [0.667, 0.667, 0.667], "y": [1, 1, 1]}, "o": {"x": [0.333, 0.333, 0.333], "y": [0, 0, 0]}, "t": 13, "s": [68, 68, 100]}, {"t": 32.0000013033867, "s": [146, 146, 100]}], "ix": 6}}, "ao": 0, "shapes": [{"ty": "gr", "it": [{"d": 1, "ty": "el", "s": {"a": 0, "k": [3.455, 3.455], "ix": 2}, "p": {"a": 0, "k": [0, 0], "ix": 3}, "nm": "Trac\u00e9 d'ellipse 1", "mn": "ADBE Vector Shape - Ellipse", "hd": false}, {"ty": "fl", "c": {"a": 0, "k": [0, 0.776470648074, 0.63137254902, 1], "ix": 4}, "o": {"a": 0, "k": 100, "ix": 5}, "r": 1, "bm": 0, "nm": "Fond 1", "mn": "ADBE Vector Graphic - Fill", "hd": false}, {"ty": "tr", "p": {"a": 0, "k": [-0.272, -24.772], "ix": 2}, "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 1, "k": [{"i": {"x": [0.667, 0.667], "y": [1, 1]}, "o": {"x": [0.333, 0.333], "y": [0, 0]}, "t": 12, "s": [0, 0]}, {"i": {"x": [0.667, 0.667], "y": [1, 1]}, "o": {"x": [0.333, 0.333], "y": [0, 0]}, "t": 13, "s": [213, 213]}, {"t": 32.0000013033867, "s": [0, 0]}], "ix": 3}, "r": {"a": 0, "k": 0, "ix": 6}, "o": {"a": 1, "k": [{"i": {"x": [0.667], "y": [1]}, "o": {"x": [0.333], "y": [0]}, "t": 17, "s": [100]}, {"t": 32.0000013033867, "s": [15]}], "ix": 7}, "sk": {"a": 0, "k": 0, "ix": 4}, "sa": {"a": 0, "k": 0, "ix": 5}, "nm": "Transformer "}], "nm": "Ellipse 1", "np": 3, "cix": 2, "bm": 0, "ix": 1, "mn": "ADBE Vector Group", "hd": false}, {"ty": "rp", "c": {"a": 0, "k": 10, "ix": 1}, "o": {"a": 0, "k": 0, "ix": 2}, "m": 1, "ix": 2, "tr": {"ty": "tr", "p": {"a": 0, "k": [0, 0], "ix": 2}, "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "r": {"a": 0, "k": 36, "ix": 4}, "so": {"a": 0, "k": 100, "ix": 5}, "eo": {"a": 0, "k": 100, "ix": 6}, "nm": "Transformer"}, "nm": "R\u00e9p\u00e9tition 1", "mn": "ADBE Vector Filter - Repeater", "hd": false}], "ip": 0, "op": 60.0000024438501, "st": 0, "bm": 0}], "markers": []} \ No newline at end of file diff --git a/client/web/src/assets/scss/linto-ui.scss b/client/web/src/assets/scss/linto-ui.scss new file mode 100644 index 0000000..bf40f0b --- /dev/null +++ b/client/web/src/assets/scss/linto-ui.scss @@ -0,0 +1,667 @@ +@import './mixin.scss'; +@import url('https://fonts.googleapis.com/css2?family=Spartan:wght@300;400;500;600;700;800;900&display=swap'); +$spartan: 'Spartan', +'Arial', +'Helvetica'; +$blueLight: #59BBEB; +$blueDark: #055E89; +$redLight: #FF9292; +$redDark: #fd3b3b; +#widget-mm-wrapper { + display: flex; + flex-direction: column; + position: fixed; + bottom: 20px; + right: 20px; + z-index: 999; + font-family: $spartan; + height: auto; + button { + border: none; + margin: 0; + padding: 0; + &:hover { + cursor: pointer; + } + } + .flex { + display: flex; + &.col { + flex-direction: column; + margin: 0; + padding: 0; + } + &.row { + flex-direction: row; + margin: 0; + flex-wrap: nowrap; + } + } + .flex1 { + flex: 1; + } + .flex2 { + flex: 2; + } + .flex3 { + flex: 3; + } + #widget-mm { + width: 260px; + height: 480px; + display: flex; + flex-direction: column; + background: rgb(250, 254, 255); + background: linear-gradient(0deg, rgba(236, 252, 255, 1) 0%, rgba(250, 254, 255, 1) 100%); + @include borderRadius(5px); + @include boxShadow(0, + 0px, + 8px, + 0, + rgba(0, 20, 66, 0.3)); + overflow: hidden; + z-index: 20; + } + .widget-close-btn { + display: inline-block; + width: 30px; + height: 30px; + position: absolute; + top: 20px; + left: 100%; + margin-left: -50px; + @include maskImage("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='close.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='7.8666667' inkscape:cx='-13.983051' inkscape:cy='14.745763' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cpath d='m 22.684485,21.147587 -6.147589,-6.147588 6.147589,-6.1475867 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 L 21.916036,7.3155152 c -0.219556,-0.2195567 -0.548892,-0.2195567 -0.768448,0 L 15,13.463103 8.8524123,7.3155152 c -0.2195568,-0.2195567 -0.5488919,-0.2195567 -0.7684486,0 L 7.3155152,8.0839633 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 6.1475878,6.1475867 -6.1475878,6.147588 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 0.7684485,0.768449 c 0.2195567,0.219556 0.5488918,0.219556 0.7684486,0 L 15,16.536896 l 6.147588,6.147589 c 0.219556,0.219556 0.548892,0.219556 0.768448,0 l 0.768449,-0.768449 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 z' id='path2' inkscape:connector-curvature='0' style='fill:%23ed1c24;stroke-width:1.09778357' /%3E %3C/svg%3E"); + @include transitionEase(); + background-color: $blueLight; + &:hover { + background-color: $blueDark; + } + } + #widget-show-minimal { + display: inline-block; + width: 30px; + height: 30px; + position: absolute; + top: -20px; + left: 100%; + margin-left: -30px; + background-color: #fff; + @include borderRadius(15px); + @include boxShadow(0, + 1px, + 3px, + 0, + rgba(0, 0, 0, 0.3)); + .icon { + display: inline-block; + width: 20px; + height: 20px; + @include maskImage("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Created with Inkscape (http://www.inkscape.org/) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='60' height='60' viewBox='0 0 15.875 15.875' version='1.1' id='svg8' inkscape:version='0.92.4 (5da689c313, 2019-01-14)' sodipodi:docname='feedback.svg'%3E %3Cdefs id='defs2' /%3E %3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='7.9195959' inkscape:cx='19.217812' inkscape:cy='28.198286' inkscape:document-units='mm' inkscape:current-layer='g1379' showgrid='false' units='px' inkscape:window-width='1920' inkscape:window-height='1016' inkscape:window-x='1920' inkscape:window-y='27' inkscape:window-maximized='1' /%3E %3Cmetadata id='metadata5'%3E %3Crdf:RDF%3E %3Ccc:Work rdf:about=''%3E %3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E %3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E %3Cdc:title%3E%3C/dc:title%3E %3C/cc:Work%3E %3C/rdf:RDF%3E %3C/metadata%3E %3Cg inkscape:label='Calque 1' inkscape:groupmode='layer' id='layer1' transform='translate(0,-281.12498)'%3E %3Cg id='g1379' transform='translate(-17.481398,-19.087812)'%3E %3Cg id='g1384' transform='matrix(0.93121121,0,0,0.93121121,-1.173127,23.437249)'%3E %3Crect ry='1.0118146' y='301.09048' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' width='12.141764' height='2.0236292' rx='1.0118146' id='rect2' /%3E %3Crect ry='1.0118146' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' y='304.73309' width='12.141764' height='2.0236292' rx='1.0118146' id='rect4' /%3E %3Crect ry='1.0118146' style='fill:%2359bbeb;stroke-width:0.54757494' x='22.485502' y='308.37561' width='8.4992399' height='2.0236292' rx='1.0118146' id='rect6' /%3E %3C/g%3E %3C/g%3E %3C/g%3E %3C/svg%3E"); + background-color: $blueLight; + margin: 5px; + } + &:hover { + .icon { + background-color: $blueDark; + } + } + } + /* Widget corner animation */ + #widget-corner-anim { + width: 80px; + height: 80px; + position: fixed; + top: 100%; + left: 100%; + margin-left: -100px; + margin-top: -100px; + #widget-show-btn { + display: inline-block; + width: 80px; + height: 80px; + background-color: transparent; + } + } + /* INIT FRAME */ + #widget-init-wrapper { + position: relative; + padding: 20px; + justify-content: center; + align-items: center; + .widget-init-title { + display: inline-block; + width: 100%; + text-align: center; + font-weight: 800; + font-size: 22px; + color: #454545; + padding-bottom: 10px; + } + .widget-init-logo { + display: inline-block; + width: 60px; + height: auto; + margin: 10px 0; + } + .widget-init-content { + display: inline-block; + font-size: 16px; + font-weight: 500; + text-align: center; + line-height: 24px; + color: #333; + margin: 10px 0; + } + .widget-init-btn { + display: inline-block; + padding: 10px 0 8px 0; + margin: 10px 0; + width: 100%; + height: auto; + text-align: center; + font-size: 16px; + font-weight: 400; + color: #fff; + @include borderRadius(20px); + @include transitionEase(); + @include boxShadow(0, + 2px, + 4px, + 0, + rgba(0, 20, 66, 0.2)); + font-family: $spartan; + &.enable { + background-color: $blueLight; + &:hover { + background-color: $blueDark; + @include noShadow(); + } + } + &.close { + background-color: $redLight; + &:hover { + background-color: $redDark; + @include noShadow(); + } + } + } + .widget-init-settings { + text-align: left; + width: 100%; + .widget-settings-label { + font-size: 14px; + } + } + } + /* widget HEADER */ + .widget-mm-header { + height: auto; + padding: 20px 15px; + background: #fff; + justify-content: space-between; + @include boxShadow(0, + 0, + 4px, + 0, + rgba(0, 20, 66, 0.3)); + .widget-mm-title { + display: inline-block; + height: 30px; + line-height: 36px; + font-size: 14px; + font-weight: 700; + color: $blueLight; + } + #widget-mm-settings-btn { + display: inline-block; + width: 20px; + height: 30px; + @include maskImage("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 24 36' style='enable-background:new 0 0 24 36;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23454545;%7D %3C/style%3E %3Cg%3E %3Cpath class='st0' d='M12,15.1c-1.2,0-2.1-1-2.1-2.1s1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,14.8,12.6,15.1,12,15.1z'/%3E %3Cpath class='st0' d='M12,21.5c-1.2,0-2.1-1-2.1-2.1c0-1.2,1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,21.2,12.6,21.5,12,21.5z'/%3E %3Cpath class='st0' d='M12,27.8c-1.2,0-2.1-1-2.1-2.1c0-1.2,1-2.1,2.1-2.1s2.1,1,2.1,2.1c0,0.6-0.2,1.1-0.6,1.5 C13.1,27.6,12.6,27.8,12,27.8z'/%3E %3C/g%3E %3C/svg%3E"); + background-color: $blueLight; + @include transitionEase(); + &:hover { + background-color: $blueDark; + } + &.opened { + background-color: $blueDark; + } + } + #widget-mm-collapse-btn { + display: inline-block; + width: 30px; + height: 30px; + @include maskImage("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='collapse.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='11.125147' inkscape:cx='-19.36456' inkscape:cy='10.31554' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cg id='g835' transform='matrix(-0.46880242,0.45798398,-0.46880242,-0.45798398,3.7385412,35.57659)'%3E%3Crect y='1.8655396' x='-47.999367' height='3.5055718' width='22.112068' id='rect816' style='opacity:1;vector-effect:none;fill:%23000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.77952766;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1' /%3E%3Crect inkscape:transform-center-y='-5.6628467' inkscape:transform-center-x='-9.1684184' transform='rotate(90)' y='25.887299' x='1.8655396' height='3.5055718' width='22.112068' id='rect816-3' style='opacity:1;vector-effect:none;fill:%23000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3.77952766;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1' /%3E%3C/g%3E%3C/svg%3E"); + background-color: $blueLight; + @include transitionEase(); + &:hover { + background-color: $blueDark; + } + } + } + /* widget MAIN CONTENT */ + #widget-main-body { + max-height: 410px; + } + #widget-main-content { + background: transparent; + position: relative; + z-index: 1; + overflow: auto; + padding: 20px; + overflow-y: auto; + scroll-behavior: smooth; + .content-bubble { + font-size: 14px; + margin: 10px 0; + flex-wrap: wrap; + .loading { + display: inline-block; + width: 30px; + height: 30px; + background-image: url('data:image/gif;base64,R0lGODlhyADIAPcAACkmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAwAwACwAAAAAyADIAAAI/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/7qYoOFBw4WLHAQYcKGFThfjAChAQMFChg4eBjRAqcKBQcIDAgQYEABAwpIxHSRIXeC7+DD/id4kGHmCxHGJahfz16CBhEzVSCYDqC+/fsACCBoyX2B+P//WfDSeRS0Z6CBILwkXwD4NdjgASqVwACAFIq3QAgspVDBgRy2R8EJLF0ggIMk4hcABCdt4F+FLIJXXkojFNjhjOvBl5ICDJaoo337jeTCBC0GCd4DJ73gAY1IrqfBSSoYsOOT9hEwEpBCVklkSUcmqeWSJTkJ5ZdSgrRBlWQmsAFJI2ippgQjkKTAl3ACoMBHIaxYppAYhnSCjGsmCWJIEOQYJ5QocuSCA3eSuUBIL2DQp5oUhKTCAIPCGUBHGSRa5oseifDomjZ6hEClcfaYkQt2ahrkAsB19AKf/p8iSUFzHakgKKlPBqBdRpmqSianG3kaq5qhbjQqrnCaetEDvirqkQbDQuoRAchamtEKzZbZqkYtRLsmrRqRUG2cu1o0ZrZVnrlRmt5q2eZGb4775ZwXUYlukBFwlGW7SHLAkZfyPlkARhHcK+SVGnHAb5JcalRAwFCGaRGiBre46EaOLkxjpBtRCvGOl140YcUsXqzRhhrPyLFGI36sY8gWpUoygBzBmvKBHN3qsoMYUTwzgAxwlPHNB1bAkcc7OygARsz+DKADHEFL9IEYcERt0g4OgJEFTgM4AUcgTH2gBxwdgLWDBmD0Qdf/AYuRCWIbWCxGDZzdoLIUucC2/ngocPRC3O2xwJEKduPXQUYj7w11RygDXnVHLReudUZc752A2xmFDbgEc2dkduEA4F2R3nuz6tHfgM/qEeGF67pRr11jrpGwYneu0bFni26RC4nPvIALIL3Q+M0UvACSCpHvHIAKHZXQ9QcipSC2CSJdcHYDH1VOsoAjaZ5ygiN97jKEIBVc8QTAk6Swxh4YT9LDHxvAfEjmo/tA+iWt364G7pcEv7wEmJ9IYOcrC+DPJLQbFgj6ZxLcIesAAhxJCWSmKOitJAU2gxT1VnIBnVkKeyk5l6IycMCVsAtSImDgSuJlKQREECXc6R2FHJCBbQ1IBMPjEAZEAC4FISB5nCQaAALK5RIUbCACD5jQAhjAmwz07SYsGAEHNLAhClQgOSIQ3E06oIACEGBEARCAdRBwuNmY8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJyla60ikBAQAh+QQJBABEACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFpd5lrepxve5tvfaBxgKN0hKZ1c5l3h6qBf6ONjKyOoseSm7KTo8KUqc6YpsOZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCJCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy5Yr5IaKER04XMgAAoSKGziH8JjRIkUKEiZWtIjBw+6NExcoSJ9OXfoIGDN5vCAhorv3791b/uCIuwNE9fPnOfh4yaMF+PfvU/xoKwQ6+vvVR7AcEoM7/P/fvbCWEObhZ+B0HOyQ0hDuAeigd/KhtUMGB1Yo3QXAmfSDCQ922B0JzX3Ugws2DOVDdBamqCBJP/jnoYfzadSDBQMEAMCNNxpgQYk8CcFBikBesJ5IQ6Tw4pEkBIFRDw/YiOOTOD7AY04FApkiByM1eOSLJlz0gZNQhnmjBTnJYOWZKoSUw5ZszlARBGLGiSMEN/l4ppVCflQkm1smOZEFcgYKwAM2sXDnmfp5NAOfbAoY0QaCCrpBTRQeauWQHHHI6JZKPtQDmJGKGcCUMN1g6ZksdMTDpmzSABGc/qEG2gBMOoSgQQWnWglCR4uyemQLD9kAaqxikqpSCQkUUEAEuQbZkaa+ekjCQ4ASGyiZLIWgbAEINGvlihkFEe2WMTJkgLWBDsCSDskqq4C3QMqw0Q/jHpmDQwKgG2i22xawALwppqrRqvV66CpDNugbaA8rYdCvAwBbmKZGOBTsoZsMuaCwnC6spMHDEVd4wkZrWvxgDA0lvLGYKKyk7bYMhHwgdhqVbDKA4zHUw8rFrgREv93KjJ+8Gol7M4D3NjQszy29XMABQuOXYUZDHA1giAwRwPOT6ja9LbNRV3eBEJla/R4JQzgE6dY30ulSDR5IMEHY1e3KUQxmgwes/kM7sw1AxzGZSfd0ApOc93cHO9QA2wbQhOLgmG7kYt6d8r11AIDLpMLgFCTK6+EiOArRBzxjO5MQlUZ9AbgcDQHt0X5KtLjChNoEA90Tf1Sx2RhP9IC+DzB8049CZ0B2SCtYnULaFVUbagCm15k6wKuP5PrNJJRbkQ2zB0pA5jntIDPRIxldMAlJZ+SCBVpDSQAE4O9kKrwX0FxSi/WSkLNHNqBAovBB2QHxTnUB8pkkCEbylQmwdhYhjOBUHYgcSl7AqhVUTi0CPBMHDLiSHyRvSytIn1tkUCX8XKADLDgeTHKgJQCRYAU0YN5cZHCCDlTqAhzgAAhgoMKa5CAGdSs4jghIkIIWvAAHMpyNEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJPkSEAAh+QQJAwBAACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFpd5lve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNeyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCBCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy95740QHDBckSLigocMJHzh/wGhRYgQIECVQqKBh1weJCxCiS58eXQOJmT9ejPDAobv37xxQ/jCH6yOEBOroqUtY8TI7d/Dwv4MYz3YFhfT4qV+YwRIHiPgAgjfCDmqVl9+B1J2g0gsBNgheDWj5oAGCFEoXwkk/qODght69YJaEFYYIQQcmocDhiRy0UFYIIorI3kgwoIgihGKd0GKL/IVEg4wyEihRCg4wQIABCTiQglE+nHdjiBSE9MN7PHIIQkQiDADAlVheKYAFRJGwZIsvesRglCjSt1AOBmSpJpYDyHATDzwgpIOSX1YowUc9QEnmhh40lIIAawYKQABHysRCAwsUUIACD3xQkJd1iqhgRy3sKaOZB8kAqKCBBuDmSzxUoOioo04Qp0D3RRqiBh79Z+mJ/igslCangg4AaqKk5opAnDeo2iJwG+3wKooe/JBQBrTSymVLoubq7ANAQOprhWFmNOawHGJKkJXJChpASyY4K24BJkw4bYXXbWQithx6eFAK3dL6qUoPjPtsquciyOpGrrLrYKwHRRAvpw6wpIC9uuKbb34YcNSvvwGWgFACAwtqwEo8IOzswhTeuRHEfCI0a8VqEoCxxqQewDGCTWr0A8gOTnkQASSvaetKB6NcAAJ0roxewxvpCTN4Eh/EQM1qJsBSvTo3AJ3P6ZG40QhDA6gCQkcjjSUDLIWr8wYYQJ3ehRuVUHV8Kh6Ug9ZYZtAS0whDy6LY1LnAUaVnP5jQ/qZIf9sSDzmPi4ANQPRK93Q6cIRD3uD1kJAFbEcA6gTjNkA4qodX1yrj3RWdELckD5BDTB80MCoCDWxQ0NyHp8uRhpy7m1AOAZBMaE02xICQD5lLACxHPXBeLEMp1B5vAG4D1UHrIMF+tuwLyQA6p20KxbvYFPze0ctng2DsQxHwrWYAEYw+1ApQS2C3jmfTGFEGDhhAwAAGMJCC+UUtv/KkIq0LMvRhCdvCyEYSqkEsbWTxgQCnFQLtieQHBsQWCr5nFtbVSQL8O4nzLAVAs9jogutbkKU8oC206E9EGHRgSjLEIw/AgIJtuUEIFIafC5BAhSzZwQYDNIIXOI4uhzMgAQYwcB8JUIA3JLgBTmrwghL0ywMjQMELfDSbhsCwiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk2MJCAAh+QQJAwBAACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCBCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjyw7s40SICxYiRLCgocMJHTh/wGhRYoQHDyNQqIDRo64PErkfSJ9O/cEFEjN/vDDOobv37xxK/ryI+zxC9fPnQ7zU7gG8e/ct3M6YgL5+9QguWOIA8b4/eA81rHWCefYVOB12KcHQnn8MejfeWT50YOCE011w0g8qNKihdyWcJSGFIFpYUoYbltghWSeAqOIDJ5AEQ4kwcgDDWC4QuCKF+YVUw4IxbhggWD5YcKOKEYT0wwg9wuhBUyxQoMABBygggQkMkTDkigh69EKSMT6okAgLDBBAAAMgkIFNMThQwJpsrqmADQn5YOOVE0YAXEc/8Milhh40h1AKBgAg6KCCDiDDTBsc0Oaia26AkJV0qpjlRlvuCaOXBUEQAKGcCgpBTBswKmoBjhp0QaREelSCpUoeBEGn/rAC8KlLNig6KqMxFKQDqiveqVEPrMbo50AybBprpym4xMCtoiZQUIq8gtjiRi8GW+KMBBFwLKwCtGQCs6N+QNCH0U6oAUckWqshCgRlsG2sFbAkAbiiOkCQBuVSKKJGKKi74YkCIfAurAawpAC9jB5AkJD5GljkRkj62+CSAw0wcKcBsJQAwosqPBB9DRf4sEb8ScwgxQIJcDGnGa9kK8dsEjRnyOhxpKfJ7xFk7MqDGgwzm84OxDDN6E3AUcQ4vwcCQRbzLGi3K837cwH2DnQq0ehZwNGqSb83AkECOw1AwSt9OzWVA4WANXodcNRC1++p0K7YAJzpM8wK8EDQ/gprnzcpRjTA7R6mQDS98gA5tGQDzAewUJAPfVd3A0c/CA7eDgXJwHMAIrwUKsIYHARy5Fp3VLLlXxv06sUNgAruARvobZDakT/wd0ZvW84B4QOtfmwAECQeUwwHi6pArghBHrmdHlVueZ8JpWA4pwMkW9MHEmx8QAIMfCB7QpCufbtGlcLNu0EVIKByAAIQUIHwSfkwOs0R+ADSD6fj7MEPYc2w9goiwQHcaDAW2oVMPSPJncniQxZ8NawD9iNJvySmAv6VxYHRukAESzJBa5XAgmYJX6RCsEGTlM9SLQDhWWYwMyIBcCU4uJmSCNgWaBGJBCVcSbWU9AIVsuU5mPOrjwVI4Kv1vCB//RnBC4YllxucQAMXoE8EJsAbEkzuJjuAAQpKwB8PgCA5L8DcbMZIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlcJl4AAACH5BAkEADwALAAAAADIAMgAhykmZDUzbUI/dVtZiGZzlW97m3VzmXeDoICLp42MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AHkIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLNqzjRIcJEho0eGAhAwgbOHfASBHigwYNHEiYWJGjrg4QuRdIn059wYTfMnesMI6hu/fvGEL+MIerY8SD6ujRd3i5gwUH8PDhp3D7QkL6+9UbqGBJ40P8/+BpEMNaIzSA34HUgaASCxoA6OB3K6ClQwcIVjjdBCftkMKDHHoXwlkUWigihiVt2OGJH5I1gogsLrDeSCycKCMG8yXVgggbbICCDBO9YGCLIu4XEg0NznjigEXhQAECBDTpJAIQQKSDfUCK2IAOIO3gn5EnarADURsc4OSYY17g0IpVsviiRzFyKWONQOEAAZl0OhnlQjqclyaLwHW0w3tuyticQTWUEAEDJdQ0Z52MKrAQmnuKmAGbgc5oQkE1GBAAAJxyaoALMV3A6KgEbKCQBZGy2IBHJFQqowb+BHkgQKe0AhBABC/hUACpjBbA40E6/JiqhX1qtEORrnY4aAW1NgtABS5RwOuoDiCkwrAsjsBRDMnKyAIPNWzqLK0BgMqSmNPWWQBCGWArogUcmdDtiSTwYMC4zQ7AUgvpjvprQai6W+EDHLU6L4cc8CAuvrTWsBIK/TJqqkETCFzhqhuFcDCHGpTAcLOJqiRqxHSaaZCeFh/IEaAbOxjBx7UysNIGJNNJwUHCpnwfR8i2/N/LMHeawEoi1EzmxAVRqXN6BG+0pc/xcVBD0J1CqxIORo+JwkEBL42eBAVD/d8HPFDNqbkqoWv0ugeF6HV1JGpkotjgfXhv0PqytKj+0TcfdMLb6nEEA93ygbswvgGEvBLWa/9bkA6AV/cCRzsQDh4NAjH7scwu0Vxz3wgpDXjTHD1NeMIDMYA45y8pQHK1CkEK+JobtWk5nAJlWmsAA6ANk+vpKoADnih73cAMHv1puQY3IFQBAwmU4HBNI49awAXDM/T37CANTjjuQcm5K5kFOOA4Q8WnfGVILPvsJVIobEDBBRe0kD1EL7yt7ZB0fyuW7AKjHUhstzHwgaVdFpsAlkgir42F4EtlQSC2FHiSBnbrgWgBAbY6sMCTrKBbKYAgWl6QPlXtTyU0aN+MNOA/tgDwYiDoIEsI2CENrECEboHOwDpQLJhsB2GKKRjUXGwwggxM4Dy7kYAFQIC8m+SABSYIwXuQ8wESrKB5s8miFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAzlWgICACH5BAkDAEcALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfkxPh05Nf1VakFtZiF5kmGZuoWZzlWhmkW95qm97m3eDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6W31au816yyxLKyxrLA2rLH7LS6ybTJ7bXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AI8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLVlxkBwsQHDhg0NABRIodOI38uAFjxQoTKFrAqPHD7o4SGCxIn05deokZM3/IMEGiu/fv3WX+8Ii7A0T18+c7BHn5Awb49+9bEGlbJEV09Pipl2BppAZ3+AB+J8NaQXSQ34HUcbAeSkS0EOCD360w31k73IfghRgAZ9IP/0HooQnNleWDhReW6ENJQnTo4YpCFEWDBxtMUEEGHtBwUREclKijdBgUMZIRK6woZHcmGBHUEBtAwMCSTDK5AUVFmLfjjhyIZIR7Qw65AlA0KNnkl0s+4IJEL0xpJnYg4ZDlmuP1JMIDYMa5pAcQFaGBmVP2+JERKKyZZZE8iSDnoAyI8BALeJqZwkc3+LlmDTudACehco7JUBEkJoqhjxwZoaKjHxo5EBBAzDSEl5TG+UBDNmh6Zkf+PYDK5hExJHAAAAAQ0EAMMHmQKqF0LpSCq1Pux1ENsmYpwwUC4OosrhS8hOqvYK660J3E6ohBR30mK+QIz4YLwAUt5UAtoTYmFES2U56oERHeZlmAuM/yupKg58oZLEI+sLujhhkJEe+QBtDrbAMsZZCvnBUoNIO/Or6wEQ8DC7mAwbgKwFIFC8c5gUJlQnyhxBqpWbGHF2MMgA4rTdAxmBGALPKFLGxk8skPOqDyyitt8PKXGSjU6swHoplRrDg/qIDKGq/k689OKrQD0QfasNEPST+IgMoJsEQD1ExailARVOe3YEZGZB3gACqr0NK0HT8wxLVln6fnRt2q/V3+CAFgLIFLT/+8b0Il1F0dCB3JoDd4MFBArwAUlNrSqT/LzdDQhktnNUdIL95dD7Qm4KwACXwQ09cvn9AQ2ZlbgMHZGqXtOQkmTCiQDvbO5PPCTzqEaOaLetSo55DyFPivG8ztkJ2G390Rn4sD2hPqlD6gekQP110zSBTrfUNQ+KrqgfIS5Ug1B5yCFGTWK4galAseZDBBBBVsQAP5EzE/8+s/5l1x7WTpl8gwsLmRCOxkJgBdWabmLwKeBGsDSyBafGA+V3HAXScRwvpktYIWpaUIhdNUB9KHEiMoDlQtcJ9aKGimDgCMJRpcUwtC9BYbSOlAGOgAyWLSAyw9yASKLcCBXWyQgg7oxnUcAEEJbEBCmvSgBi04Du1WAAMZ9ECFs8miFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAzlWAICACH5BAkDAEQALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfkxPh05Nf2ZzlWhmkW97m3eDoHuEsICLp4F/o4GNuIiTrYmYxY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6O34KW31au816yyxK3B6LLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIkIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLdixkhgkOGzBYwPBBRIodOIfkkKECRYkRJVq8oPGjbm0OFihIn06dggUTPmQKVzEihPfv4EP+jJARJO6NDdXTp8f+kgeK8PDhk2+7A736+9VTsPzxPr7/8DSsdUN0+BU4HQcq8dDdfwx+p0JaLBBo4IQbZGdSDQs2qCEK5QEFxAoZQNDAAhNcQAIQG7Ew4YrTYSBESTVoKON3JQzhUwwTKIDAjjzyOIEOGO0gIYsTIjjSDxnOqOGDOwGRgY49RsnjBRb5MCSRE4ogUhBJKqnhCzrZ0ICUZPLYgA0UiYDlmsCB9IKXcDZ3kw5QllmmAmhGdMOaa24AEg9wwonCTUCMaeehC6AIEQd8rjnDRyoECmcONl1w6KUITADRDo326dEPkgpaUwyYYppnQyl0uqaFG9EQKpz+QfQw0wSlXtrAQxioiiULHZXwqpcMCABAAg68BEStmALJEKe6EvkBR6D+qmQEAFQLwAEutLQCspeC0BAMzRJpAUc4SKukB9ZWS0BLlnJrp6YMqRguixzFaO6M6VZb7Eq0ulsmBA2pOe+KL2r05r0yBpCvACwt4O+dDX0w8IptZtQCwjIWkC8ALBn6cJQLBDzxhDdsdDDGDBqwsawqQfCxlLcyZMLIBrKKkQwoNzhAvgewlMHLUVLJUKo048eRqzn/t7EELJEAdI8dNDRD0fdhwFEOSfunQb4EsKzSsU/vqOxCQlCt3rMbDZF1fNRae8AJLnn8MsAO5Wo2dfpx5Ov+2uA9AIAAB1TgNUs2hB3DQzPfPZ3NGeHM93flDf5Svx/D69CeilPgZ0eAPh7CoDUV+nGiEdl3Nwwf9cc3DnM+rMDhETFrtpGfPs7kTTY4jGwDsEsksdklg3Tx2jzoBES7mEKg6ERWUm3Cll0iLENPNlBOJgSnVoT5yBsUHFLnKKNgo09AgHABBAss0MAEHWR/EbgTu1hSuRjXaBbR4VZ4EtLmcojW9qrigPdMAr5XqWB8aNmB3fhkgRQM8CQ/2FugRkADBKqFBQtkEQcqxpIaSHBGKpDTW1jAKANZ4AMcfEkNItWgEbRAhHNhgQg4kCsLWGADHzDBDh44kxq8QAV0vhrBCFDQAhn8wIKzSaISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjORXAgIAIfkECQQAQQAsAAAAAMgAyACHKSZkNTNtTE+HTk1/VVqQW1mIXmSYZm6hZnOVaGaRb3mqb3ubd4OgeIS0gIuniJOtkpuylKnOmZm0mq3Qm6O4nZ+7n7LSo6u/pbfVpqjAq7zXrLLEsrLGssDatLrJtczxt8Xcuc7xvMLQvNDxvcnfwNTyw83hxNbyxcrWyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2Nnh2t/p2uPy3uby4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AgwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8uO/IOGCRAaMEywoKEDCRo4geSAseJEiRAjTqhwkcMuDRITIkifTl16BxYzc7gI8aG79+/dVf7UiIsDRPXz5zEAd8ljBfj370s0Z/sDOvr71Tv8WAlkO/z/4KkAhFo/dIDfgdRNgENKQKgA4IPfhcDDTz2IcMEDDjwAAQUe9PARDhYgKOJ0L5zEwwgQpuidDTy14MACCMQoo4wQiMDRDtGNqON6I/nAnYpAzneTDBDMaKSRDtyQ0Q8Y6OikgiMBUQKQVEp4UwswHqmljChgZKCTTlowkoNUUjmCTR5kueWaG1j0AphwmhCSDWXWCQNNIqypJ5cUMQknmBPs8JGUdZYZgg8yyaDmnmvKMFEKf8IJwkcxFFrnCjI9wCijDkwUYqRgCtoRipaWiehLKGy6aZcQ4QAqnP4pdMRDqXXGAFORqu65QEQmvApmBx3BQGuZKrzUQ66bKvnQp77quN9GpA4LZAYueYAsox5A1CyYPGYkbZkCFNASBdfuCcFDrm6rI3YazfotkAYAwAFLuJa75QMP0aCujrFqlMO7QB4AQAAsOWDvmgw8xMK+I8qpUQ0Aq6gAAADosJLBB2vZqUNvMowgCRvRGTGEDVA8w0qaZnwkvg7p6/GBDmf078gPTjwASxeofCQFD+3w8oHsZuQDzQ/GKwFLeeo8Y5sO/fAzfiVqBATRABKQQEvHKi2jsg4x+zR1omoULdXfVWBxSwxojcDGD5HwdX4duUB2gDClqrWNrb5NXf7M7c793Z0wYawyyxF5/XXY0Prd3akv3aDzAi1MtLDewHoEsd/FymT3wdlSZLjHUH40Ns1WzpSzvRdYNPnTIIN0OdUu2LS5qgt0bpEGT1vwLEgnUD3CgDbJIPieD0SOkZ+gLygSoSOXjpMIwx/pQIcb+exx1D3SzCJPPXhAwQMMMOAABBs46hEOOTY7QdAk8fCjtCGMdxbyr06AvUnM0xrC9mmZB6oGiEOJe0p1AsappTxw0sD9VtKeOp2Af255gf8ONAENpGB3L7HBAB8UghPEAHhzeQEJchOdCWCgAyBgAQZpYgMXGIc7ISiBClZQAxDO5oY4zKEOd8jDHvrwh1tADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPXQkIACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLrkziQgQGCw4kYOCgAoicKUJwwGBBAgUMGz6YsEuiQgID0KNLh+6gw8wUHyhA2M69+/YNJeL+oqgwvXz5BdZdrvjgvX17C+HZuqhwwLz96Q5crHzxQYL7/95t8EJPLHjwgAIFFIBAAxmQ5IID90Uo3QEopPTCBgBm2J0EK+jkQQEBACDiiCMWMAFIKDwn4YrQ/WbSCtppKON2y9nkAQEk5pjjACN0pEJ9LAZJQkkt+DfjkSnU9ECIOjY5YgMbucBAkFRSONILGBypJYcysYCAk2COSIBGEVBpZgL6hcSBlmxSMCBMX4Yp55gXgWDmnRGEZAKbfHIA0wNyBgrAAxctcOedKoBkAZ98tuCSB0wKGmaPFGlw6J0OfCQCo3xu4BKOkso5QEUqXkploh3FyKmWjq6UQaj+gjYoEQqm3qlBRyusyqcILBUAa6AFTHRBrWYy0FEIurKJwUosRPormCxINCWxp3KUZbKsqvTqs3KeGBG1Zg65EbZsJpkSoNyGCSVEKoBL5a0atUCulrymFGe6TgYLEa3usnjBRrnOO2MIKvmKb74RkdAvi3lqlILAM/qZEqgH60jnQ3YuLGEFG+0JsYYfqKRAxU0iEBG/Gt/HsUYBfwxgyCk1QLKOCkTkQsoRppfRCy5nGB9K2848oqwP3YyzfS7u3DOANaLEgtAkniBRqUdLh6pGqi7dXaspGSz0xRCRV7V0C3TEntbdWcBS0DMTKpHRY0P3L0c8o80dwSwN8HX+tBNRXfXVG2WtNdcqjSC0BxV1ELcBmXpUgt0QeOrSyBXXbJHfGlv5keAfc/mS19wWwHdFGR+98kceLw0zTKDDSsDoFk2bMpoiXeuymzNRDqsCsFvkgqGZixvSC4t2bu5MHgggKQFEa/SjxkmLVOTHTdc0AcU6BkDAA71rhAKQ1B4Ab0krGImtBPXq9EADCBAwQAEKTNB9R7+HH31JxJ9f/VllmsoA4ChZ06owQLi0oKB/VELPS1YgQC3BJy4kQOB9DsAADaQJJiloIIAkgAERvGkuIKgAAxhQnwMswAHVuWBNTPABDGDAPxKwwAbA88HZ2PCGOMyhDnfIwx768IdaQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvpRKwEBACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLvuwCRAUGCxIcSOAgwgUUOF+Y+IDBAgUJFDZwCLGirosOCw4YmE69uoEDFVTIfFHCggQI4MP+i4cg4UOLuCQWWF+/PvvLFBbGy5dvvieLERkeNJjggQVKFOqxJ6B1GrC0QnzzJTieCDmd0MAAAEQoYYQENHACSSRIN+CG1DmgUgrfKShieBvYxIICAUyo4oQKXAiSBhpyKOMCLpwkQogj5mjBCzM9kOKKQEr4wEcayGgkdQmYJEKOTIZHQUwsIBDklBIW0FGGR2bJAEkgNuklBi+xUACVZAIwwEYqxJilkReI1AKOXjIZgksKlFkmARpFsOae2oHEQZyAnrfSA3baOeRFJOy5p4cfpQAooCWqdMKPhZLpYkUMKLonCR9h8CigKahUZ6VlWlkRCJou6pEJn0Ka0qT+pNp5qUQXpLpnjRyF0CqgPJ7UQKx2NlBRArau2UFHFOwaZwkoEQBsmQFQhEKxazKq0QrKxhlpSSM8K+tEHVCb5QEclZCtlxKc5IG3ZXowUZHiHsnRkuc2edIE7JJ5aER6xmskrhn9WS+TvZJEaL5TCiuRA/4aCZxGGwzMZHMl/YpwkApMlGnDHD6ckacSj0gxSRlcHOQEE1XAMYd9ZvRByCMKStK6Jq+YwUS1rjxgrjCLeBILNa84Arg6C5jkRub2PN+TJwkQtIRnTqRC0exZm1ELSs+3bUmjPp0xRcRSXd2xHCWbtXjMnnTC0xG6S5HKYlPXskYvnx2ezCZJGbT+qRQlGrcBVmvkqN0QbG0S0EEPbVGAYnPqEYJnhypqzV9b5DfVgW80eNaGozQmwgj4hxHDVDv+UcRZS84ShOwOIDpGUxddgZtZfwDT58AS8HpGlzfMAMAgbS4xBgW7ZHGlASiwu0bw+psA8CHROzAFxb80gt53Kv5RzuIuMPdIutZrAd4yjdCA00AKoID2IfWe6u8pCd8q8TrhN0EDCjzgAfskoRC2ogfQAPRMsgKzPUoCIqgeWjTwvyMx4HsqEYEBm4QB8rFFAxvb0AEi4DGYiABkIpIAB0YmFxdoIAIMINYBDrAAB/xmgDF5gQg4gIFkSUACFtgAcxQ4mx768IdjQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCUiUgACH5BAkEADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLzuyCxAUHCxYcSMDAQQUSOF+kCLHBggUJFDBs+JDCLokIBwxIn05duoMOM1NwkAChu/fv3Tf+lPjJ4sSJlyQcVF+/foGKlyk2gJ8/30KLnB4aDAgAoH9/Agp4gJILFUTH3oHURcDSCx9wR9+D33FQEwsTDODfhRgKMEFJKjCA4IfULYBCSi1gAOGJ31mwgkweWIjhixcOsGFIJCQA4o3SHQCcSSlQgOKP3UnQ3EsNwGgkhg2AhIKBOOI4IkkrOAgkkCuyxAICR2bpHwEeubBAk2Ae8J5IL1gw5ZkS3LdSAVq2CQCXG7mgHphgLkCmfGeeacFKD7jppgIbaUDnoBeEJEKeiIaQUgZ++plBRi7YOGiYY3b0go+IoqlmSSy42KiWAWR0waSDKuhRCJkiKqFJfX7q5oz+FUVK6qAuWIppqme+YJIArroZqkUgzDoodhyZgCui45E0Qq9+CljRqMKC6UBHqB575gYlFclsmwhYJGm0OB7Q0a3W/ihBSZ5ue+SvE6kALp1PZtRCuXlWKRKv6mpZEQrvgrljRivQe+aQIuXbJgsUkdBvkxpslILAU4ow0rIGZzkCRYIufGOhGh0K8Y+KinRCxRZT1IHGN1awUQkf//jBSCyQfOR5EwWL8ofEZmRsyycmKxJ/Mr+4780fgrBRwDxDaAJJ6QYNwACxEo1gvBi9kDSE9oakgNMXAlrRt1JPd0CtG5F7dZC6Tsy1f49WVEHY1U3L0Qdng4dtSfg6HQD+whXZDLd0Rhdb93dLlzTB2kla5AKTYYtpqZRnp3lS3iQLwLdF0MKt8qmDQ/DySYwG/QCkYN/suEeX1i05SliS7HVGJ4fN8Ucsnx1ySgRUTMDlGX1J9AJkg2Rm0haknRILuau7e0eyonwA1R+l3rIEWau0NbMK8L4RvxofEPhISEMsQeEuTUB5mwKMTuPCB+RM0sMCS+AzTA8AnWUAD2j/EQq+C5vAvyZZwfCORQGCzSQD+sFQAAaggBHoLyQuiICwGFAplLyAA8fCwKZwwoIRONAl/BsUAwC4EgEiCgMGbAsI5vShAzBAA8F7iQnwdCIJYEAExpMLCCrAAN0Y4ACAC3BABDoQQ5qY4AMYOA4EJGCBDXCgBDmcjRSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCapEhAAIfkECQMAMAAsAAAAAMgAyACHZnOVb3ubd4OggIuniJOtkpuylKnOmq3Qm6O4n7LSo6u/pbfVq7zXrLLEssDatLrJtczxt8Xcuc7xvMLQvNDxvcnfwNTyw83hxNbyxcrWyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2t/p2uPy3uby4OLo4OTs4ujy5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8vmTOJCBAYLDiRg4KACiJwpQnDAYEECBQwbPpiwS6JCAgPQo0uH7qDDzBQfKEDYzr379g0ldP6OeKAAAYECCBo0ONESRYXp8OEvsO5yxQfv+PFbCE+TRYMBAAQo4IAEKMDeSS5UcEB8DE7ngAsrvfCBBPlV6N0GL8TEggIBDOjhhwqY5IIDDZYo3QEopPTCBha22J0EK7zkQYcf1uhhBiOh8JyJPEL3m0kraOfikNstx9IDNNqoZIANhKTCgj1GSUJJLVBI5JUprITAklwKSMBHLjAQ5ZgojvQCBlemCWNKD3TpJgAPeBTBmHQmAGFIHKSpJwUZmjRBkm8uieNGINBpaAQhmaDnohyYxAKggSoZwIEZLWCooSqAZMGii7ZQUgORvomARhpcaqgDH4nA6aIbkHQCpP6hKkmpRTuaOmamHQm5apqeigRqrG6OehEKthqqQUcr7LqoCCMBCGyXAWB0QbF0MtBRCMrqiYFIIzz75qwTiUntrRyhmS2vIbXpbZdxWjQunVNudK6eWYL067pLCkuRCu+OeaxGLcybJrMgFYAvlwVYRGy/PV6wUbICExlCSAYfrOQAFpHAcI+IapRCxEQ2ChIBFl9sUaEbm1jBRoqC7OIHIW1Zco1fVrRwyg2urBHELlsIM0gKzFyjvhO5gHOJ9GX0Qs8t8vfRvUILGGJFRh/N4I9KM22hkR+xELWHHlxUq9XS4aqRrlp31ytIAnwdYLQXvUe2dAt0dF/a3VkwUv7Qbk9tUdVzQ+cwR0vjzd3EInnt9ggZjU222RuhnfbaMX/t90UdBG4Aqh6VYDgErZKk+MyTbuR4ymV+JLnLa5aUgdDtaoSy1Tp/1LLWP5sk88GXayQuznaKZG7PfKa0u7cFsOCRC5aiHm9IL2zKer0p8f2sAsp/9GTKWItUpctcq/R6rAHEDhIKUI57wL8lrWDluRIQ3BILx3MZQAHggsS8+t2XFD384XPJCDi0pAAggHEomZOtGAA5lORpVxignEw80ADzDGAA6GnACLKnEhQocEzzeckKHpim/cSFBB9s0AEYoIE7wSQFJLSQBDAggj7NBQQVYAADFnSABTigOnkurIkJPoABDFBIAhbYAHhsOJsmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKTiUgACH5BAkDAC8ALAAAAADIAMgAh2ZzlW97m3eDoIiTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AF8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL7txihIUFChAYQNCggYUROF2gAHGhwoQIEzRoAIGi5woRGDqYoDkCgoEC2LNrx04hxUwUGyL+PBhPvvx4DyxsrsBAIACA9/ABEJCwouUIBdvz54fg3SWKCuYFGOAG6cUkQgLuxacgfARMh1ILDegnYX4UsOSCBgJmGKAHL62A4IIgxpdAfSWdgMCEKGqnQAspqTCBhjCWV4ELLIkgQIg4wheACCR9cF2KQBZgwAknlSBejEg+EIEKKnWQYI5QYiDSCD8GCaQB/Y2EwpFJIhlBgSZJ8CSUUYJ0QpVWAokASSpw2SWSE5wkwphkQsljRy3gl2aaC4jkAoBvvnlBSSvcWOehATi4kQV7NvpBSCAEKmkJJDFw6KUAHMBRCmg2eiVILLgpqZcjmUAnpjkqihEFnjbKwUf+HowqKQkiHYDqpQNoxGmrexrgUaiyBhpBSCvciqmqFWXAa6NZahRCsJKC2REGxl7qQEYRLpumBR1hCO2bIIBEQLWHEoBRC51qi6KvG7kg6rcwDvuRoeSSidEI6u7JokYowBsojR2ZUO+hJFbEQb5pEqkRCf6+yWRHIgxc553JImwlcM423GVzHXUgMZlSWsSoxUC+qlGkGiNJ67Qfl2kRqySn+KhGsaYcI6UBt5xjBxeNHPOEM2eEss0a4sxRsTqHiKxEH/yMYrMXlUA0jNJudGrSANzr9IRQW9Tv1BlWrdG4WMdn7kUtbK2fAhy5ALaAFYAkQdnxMZDRAmpvVyH+Rxe8bR6HHyFNNwBLT3Rw3tlhvBHDfpPH8Udkl332uYhjx27bjY8nL0gC082zRhBUzq1HG2Qebq2Sc5R23gjs25Hbfk8AMLFXk7tjR6GrbfJHpb+98kgRtxxAyKqf6HQDfr44tQYnUfvxtR+l4PSKI7Ew9YwoOTBwANCDhC/JQ5b0tcZLqjRntcP3aLEBQZMkdcMRGJ3SCraiOkDhH52prgJdj9QmvBUQW0pEUD8yDYBiJsnTshrgOpT8CVoamN1LTOCAAYAoAANgAAJT8gHj8UlhLSmB8gT1MJuYAAMYkEAHRFCwl2QAbykyQAMUB5MQ9C1GEdDA4+byAQoswHiABlBAAygwggbSpAQeuIDyIlABDXgABRKcjRSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCahEhAAIfkECQQALgAsAAAAAMgAyACHZnOVb3ubd4OggIuniJOtkpuylKnOmq3Qm6O4n7LSo6u/pbfVq7zXrLLEssDatLrJtczxt8Xcuc7xvNDxvcnfwNTyw83hxNbyyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2t/p2uPy3uby4OLo4OTs4ujy5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AXQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8v+bAJDBAYLDhxY4CACBhY4U4DQcKGCBAkVMmgA0SJniAcNFBAogKDBgxAyWVhYYKC79+/dHf5YmNniQwUI6NOrR5/hA00ODQYAmE+/fgAFJVqyoHAAvH/wB2zwUgsdSLDegetJMAJMJSgQQH0QRohAfiltkMB/GIK3gAgsjTABgiCuV8EJLTkY4YkRKoASBRm2CB4GKnUQ4ozrgaCSCgWgqCOEA6hAEgsOuCikdxSc1EIGNCaZXgcolSDfjlDOJwB2IgU55JURmISkklxqYJIKT0YZZQA+gsTilWgKOJKMXLa5IEk5iiknASBhgOadBnAYEght9gkBiSI1IOegAKjYEQv94XllAiG1YKCfXE4gUgkPEionhRtZoOidanr0AaR9vvkRApYOWgBHKCS66ZAHfLTCo/6gKikBSCGUSiiVGWm6KpowdvRprG3a6JGgtsqJwEYX7nqlAx59CCyXGXwkQLFyBqCRCcreCdxGKTzbZ3Mc1UrtpblmmyZHv3qrpKgaPTCunA9kZKW5Qha50ZbqJskkR8S+C6WhF3FHr5DMbnRevklGyxGp/kJ5KkbJDtwiAxw5i/CMF3QUZ8M60omRxEK2utHFSc7KEQEc7zgARiyA7CKjGrVAMo2ScqRAyjoei5GqLvu3AEewznxgBR3djPOJAFskcM/+ZWmw0CB6yZG7R0fYQEYMMP2fvRpdADWC+26kQtURcpDRmVp/12lGbH6tHrsaTUv2fNZmhELa4KHA0f4Kbq+3gkdGz530RRGnTXFHFrudsUclzD2f2RqhnfZ4HbXttnsfbXz0wxq1jPcB23Ikc98SgOvR2GRDvlEEeFPukQZ9Yw5S4CkPnpHnTCcQekejQz2B6SChnHKPH23A9AEeiDQC1BKQQJIKcvsrAKYese5yryLBPrOwJJUQPbUBqA5S1hJzPZLXF4ddkgrCF0sA9SCxQL65FOwuUgvoq9sB8CbRPuh9ZSqJ5DZ1AOydxHKgkgD3VBIChs0JVyex06qQxxI+xap5MAmBiXQUAARAUCUDDNlvXoLAkjHnPQ1oQAEIMIACKOA6AXQJCixQOAwtwAL2a8kKPpA4EFXgA4n8g4sILMAABlzoAAngjQVMgJMTfOACF/iQBCaQnA+kYDZYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgnEpAAAAh+QQJAwAwACwAAAAAyADIAIdmc5Vve5t3g6CAi6eIk62Sm7KUqc6ardCbo7ifstKjq7+lt9WrvNesssSywNq0usm1zPG3xdy5zvG8wtC80PG9yd/A1PLDzeHE1vLFytbI0uLI2PLM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHa3+na4/Le5vLg4ujg5Ozi6PLl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjyybL4oROFyAqMFiQ4EACBxEuoMD5wsQHDBYoSKCwgUOIFTZZeGgwQEAAANgFEFDgQaaLDgsO/hgYT768gQMVVMh8UcKCBAjw48uHIOFDi5gsFFzHzr8/fwW2tUTCAuYVWGB6L6VgwXwMMmhfS/nt59+E/SmwEgoEGqiheRqwtMKCDYY4nwgreSAhhShiF0B3J5Eg3oYwkueASim8J+KN8W2Q0gMnpujjAyZp8GKMRC7gwkki2Ijjkha8YFIDPkbpXwMkaUDkleQlYJIIS3YZHwUlPSDlmPxNIJKLWKbJAEk1eukmBiON0COZPrLokQpDpnnlBSK1oKSbXYYQEgtz0pliAAF2FIGejKoHEgeARnrfR1AaSicCHpHAKKMzfpRCpJHq6NEJhVqaYqIaMbApoyR8hAGo/pGm4JECphqK6UYgrMqpRybAGqpHAtRKZwAcXaAro0dyFIKvkTq5UQbCGmqmRgkcq2cHHVHALKAlcFRptGPeihEK1urZqUYrbAuoqBoNAC6ZxGbUQblpHsBRCeq6KQFHwb4rZbwYWUkvlhxxma+XHPlLJgsZLTrwlclmBOnBXTqL0QkKjzlCRg48fOVwGm1AcZfQZcRCxlKiWpGqHscIckavjoxjyRidjLKPKlNUQcsxOprRBzLjOGlGpd4MgEbG8ryhskHfyJG7Rk8ogEbzKm2glhvh23SDYG5Ea9T+FaCRClYbeG5GLWzdILsZQQt2fxlsVG3Z5WHLkbZqy9ct/kdFKxwAwxrtTDd5PmsEdN7xDa0RAm9jJ/ZGmg5uwNkafYo4BGxrZPPbdmqUId2tegRi3rJ6xDjY4kI+OOUbWa525httfjOiH3Vcdugfiax26R95EHXcH5FtdQV9qv3BSBPcbGFIkbfMQMQguT4yBhaH9LW/qYMk8MMJQB+SwRRTUL1I30ZLZUlJ07tA4SMte7AFipPku7ABTGtS88c+n5L0vlKvEgunG1YBckYSFMxtUwfQgPdMsgK8gUoCIhgfSkYQQB8FgAAba4kGDoglBrBPJSJwoJcwEL+WZAAB/eJPAARQgAwADiYaYBmMDhCBl8FEBDG7kQQ4QDObnGAEiy+0iQs0EAEGVOsAB1iAA4SzwJi8QAQcwIC2JCABC2zgORKcjRa3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpSiXEhAAIfkECQMAMAAsAAAAAMgAyACHZnOVb3ubd4OggIuniJOtkpuylKnOmq3Qm6O4n7LSo6u/pbfVq7zXrLLEssDatLrJtczxt8Xcuc7xvMLQvNDxvcnfwNTyw83hxNbyxcrWyNLiyNjyzNryzdblztLb0N3z09vn09/y1uHx19rh2t/p2uPy3uby4OLo4OTs4ujy5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sOy2JEhgcTPHg4gdMFiAoMFiQ4kMBBhAsocL4w8QGDBQoSKGzgEGIFTRYeGgwAwL079wAKPP7I9M3ggIHz6NMbOFBBhczlGCRAmE+/PgQJH1q8ZPFAgPf/AD7wEgkLqGegge29lIIF9jXYYH4sebAdgBR6N8AEK6FQ4IEcqncBSysw6OCI9oWQEgsKVKjifwWkRIJ5HcaIHgMqpSAfiTjSh8FJLBSw4o/dDcCCSRrAKOORC7hnkgg35uikBfqNxIJ/QFYZAG8jaXDklugl4EJJIjgpJn0UvCBSj1WmCcAAI6FgJJdH0jjSCk2O6eSOIaWoZpotgqTCm3AeGYFILdRpp5McgJTBnnti+FEEgUaaHEgcHGqpdR2xMCGjVn5EQqSRLgBSCpZaaoFHD3C6ZwMeMQBqpP4gfIRBqZaa0BGVqnbKEQqvhurRCrSaytGiuarpqEYX9BqpkhuFEKylUWbUQLFqIsBRAsoGqkFHFDx7qAgb4UrtjwFsxGu2cDrAEbDe2rmBRieMq+YIGnWALpwHcFRCu3ZKoNEI8qYpXkZa3sslR2HyO6ZGHgRc5bEXQWrwll9qVKnCYpqJUaoO/yhgRg5MvOWkGW2AsZiYXjRBxz+ympHEIsdIwkYXn4xjChkRy3KFEFtUQcwyMovRBzbnGK1FAO9cYQbIAh0jR84WTaJGLChdIZYYgeA0hwlwZILUI1IQrtX/BTBkRi5sfaC6G70AtoPvaqQn2dz1qRG2aqf3If5H3b5dn4ka6Uw30xv9nDd6QmdEtN/0HY2RuEqbzdGnhxsgakekMg7BqRytTLfLHG2Ydwcfieh3CR5B3rEAZ+96uJy/Mo5nR4Kz/LFHIas9M0gmv43zRz7ujEDrHf25dQWEGorxByJtGrCQIlEe8wIVh5S5zRZoDBILATgcAL0j2SuylyXte3KZJGkq7wBYj5SswUmeFDW/UJ4UfK4EEE+S9MoyUL1J13sWBrRnkv5wKgAP0F9JUIA3UB3gAv87yQr6VioJhICAKOGYlRLoEg00kEsMIBlLREDBMWEgZS3JAAJUFIACZECBLNGAq2R0AAeI8CUimFWOJLABFMbkBJQZmEADcOOBEcAwJhqIAAOwdYADLMABFUBBBGciAg5goFsSkIAFNvCBFWBwNmAMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhcSkAAACH5BAkEADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLBssiwwMFChA0aDDhBE4XHSowWHDgQAIHES6owPmixAcMFiRIoLCBQ4gWM0c0GACgu/fvBBr++I7p4gJxA+jTqzfAQLnMFyGiQ5hPvz4EDNddjkDwvb9/AAWMx5ILGiSw3oEHVvDSCyJQYN+DD36wEgsKBPDfhd8pwMJKJCyA4IfrHQACSylYAOGJ9klgAkojCIDhi94JMEJKGhwA4o3qXaCSCBKg6GN9IZjkgYUwFglABia5UAGOTKbHwEkvfPDjlPRhQFIGRBpZJJIkLdnkl0+WJCWVZFoZ0ghZagljADOKpMGXcBqg4EgikGknBBJ+xAJ3amoZwIYgkWBjnF+OGFIKPd5J5ooeKdBnnwiA5IKHhH55gAsgvWCiomRK8EJHJ6T5aJECcvRmpXDO6VGdnNqZ50b+/I2qZgEeuWAgqnAu19ELDrZqJ3YajSDro6VmdCquX0awqq93crBRA8P22UBHDiAL5wEebcCsnRJsREC0agbAkQuDWtukrhq9kOi2VAJ7kbDgqlmsRSCYC6cGHJnArp0iZDRBvGo+sFEE9n7pAEcc7EvmBhk9ALCW02pUbcFMJsCRtgpPSUFG0D5cZKQaMUAxk9huhEHGU3aLUawev0irRreOfCNHvaLsY0Yst3whARuVK/OHHK1r84kZOaozhgpsROnPCFq80aZDQ7gxRh0f/V/SEjP94QIXR32iBRllYPWFE2zkpdbrhanRmF7bZ+ZFLIz9X5sZdYB2ghyV0Hb+hBq5KLd3A4x793okcPTC3valoJHRfwOAtdKDo+c0R1DvPXVGJzTeHZcbHXu3qhuxivirGRXQOM8d2Tr4ASh4xCviEqzAUeZ/e/CR3Z+DpPfepGuUs84vfxTzz5eGVPPQnnrEgt86BzDvRiSgje+hbff7EbwtB8A5SJ5TDDpIoqPce0ce6Fw2SQSPzACmJCWMMgafilQ+wAGcX1L69q5/kvv7wl9Si+DSXkouYK8KsO8kIdjXB+JXEhb8Tk0EeB5JSDC8OB1geipJwfHuJAHrpWQEpoMg3VjSPZJd4IAsCR+VJBACBq4kAwpg3n8CoIARvsQ8FasAumASH419wF2WMPHABBqAAAIUAAEKeIANZ6ICDUSAAQYyzgIccIHW3aQFIuAABhw0HQtsIASym40Yx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqnxIQACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/srCwwMFBAYQKICggYecJC5EYLDgQAIGDiqAyJkiBAcMFiRQwLDhgwmZtREEAMC9u3cABSb+zCRRIYGB8+jTn3fQYWaKDxQgyJ9PX/6GEi49DPjO//sA8S6hUIF6BBK4QHsurfBBfQwyaAF+KY2wX38UejfACCu5UMEBBXaongMurPTCBxI0aGJ9G7xwkgfbVehidxmk5IIDHtaY3gEopPTCBif2SJ8EK5T0QIsvFtnASSiYZ+OS5y1n0grx+SilfNeJ9ECRWHanQEkqcMjklySU1EKJU5aZQkgTZKkmADGK5AIDX8aJ40gvYFDmnUB+NAKRaxaJYUgRxCloAiGGxMGdiFKgYkcE9KmmACGBIOikEYRkAqKYctBRBo6uCaBHC0w6qQogWYAppi1sxMKEnWIZwEf+Gog6qQMfiXAqphtsxGmran66kZKyxklqR1HeemeqGRXAq5oFdIRCsJNq0NEKxmIqQkYs8LnsiyxwdAG0gjLQUQjVIopBRrtui6WvGMEJrrAc2VnusRhdqS6WR270rqBhbjQvomdehMC9WDarkQr7ximtRi38e+e1FylL8IsEbPRswkxesBG1Dk8ZAkasTkzhqxqRgDGTlWqUQsdTanpRyCLzB6lGkp5sYwUbXcqyjx9g1GjMFFas0cU2e4izRhzvfGLPFykANIUIbORC0TUimNELSvcIoUUNPN3flhpNTXWHTl6d9YlVWpSu1zByBOzY6Q2rUbFn04esRSyw/d3+CRwNCHd6C3S0YN30WaDRwHqD15HYf5+nMUdYEz7fxxmlmXi+bjd+ntwb0V333RgJoHcA3XbUgea0elSC5BDkupHlXj8A0ttFz/mR5zvnyZHoTwtQukc1j330RzqfzTRHIzwdQJsguVs0oSLJq7SiH61NsOxuhmrzAf2G9IKpuQf8EeIEgz1SlzaXLdKYO6cNktP3KvD7SCh4+e4BC5e0ApnzSgDxSNZzVACwdxIXaA9aB1BfSb7XP/eNZAS8c9QAmJeSQAWLAZxDyaGMhQHQmeQBEXTVA+anEhRYME4HeskKNninB7nkAT+rUAAIMEKZkOCEHjoAAzRQKJikgIWfJ5IABkSwKJhkoAEIIIAABDCAAihgAiScCQgqwAAGcOgAC3AAe3pYExN8AAMYKJEELLCB+xRxNmhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbBkSkAAACH5BAkDADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/spiggICAwQIGFAAwQMWOFVoqMBgwYEDCxxE0OACZwsRHzBYkCDBwgYOIl7EZNFgQAAA4MP+iwdAoMFMFxcWGFjPvv16BhdmvghhAYL9+/jtYwjRksWD7+MFOJ4CL7lQwQHuJZhgBy+98IEE+UUYYQkqeTCAgBiOF0AGLIGQgIIgurcACSyZQIGEKOZnQQon/Zfhi+I9oNIFIdbongYqhZDijvmJQBILCsAoZHgFnORCBDYmyV4FJ73AAY9Q3vfBSEEOaWWRJTmg5JYGRGDSBlGGCQEHIT1g5ZkATEASjVxuyeBIOooZJoUeeQAgmkNyGJIGbbZJYkgiyCknixyxcCGeVgYQkgsI9rnlASG9AKGgYUrQkZmInmneR2w66iZIcVI650Ys3JmpkAGc4JEKjXqqJKT+HrUwqahRWqoRpqdaKWNHnbqqJI4dhUprlD5mRECuZyra0Ye+bsmARycOGyYGGbGALJqqboRCs22qwNEK0srZAka4Xivkrhr1yq2NwGokbLg8FmsRAuZeyZGW6yrp5UZgwhslmRcVUO+QBHCkXr5JOsBRff5CuQFGhw78orIaMYtwjQtwFG3DO1qAkQASw0hxRhcnCatGHENpq0WmhiwgRyXbmABHKfNIAcQuZygAR63GnGDGG81ac4QeX3RszgIWvNHBPif47EYMDx0htRdVifR4CHDEQNMK7qsRBlJLCLBFE1wd4KYaVcB1gu1m9EHYEcpbkbVmi5etRi6s7Z7+txu9AHd+435cN3gDeGTx2kBr/Ld9RWPUwOAAoL2R2nobEF9Hby/OX7WDp+pR3nof0FxHfv8tgXYaPW625BwhufblHj0J9+YasQAy0gEA9xHoTScwukelS00B6ht5cLWaIYHANQgimRC2CR+VOzCBI7leMuwhyZ4y7R4JLHHWJW19MZMlgc3xlCJ5b24BupPkgvjrVvD7SC+YD+8HxIekOrINtG8S5c1qm0kyJy25jcQDt0NUAJCnEj656gDMW0mgaCUB6KVEeqj6zUsA+CjmvISAlcoOS7gTsQwNoAF3e4kKLnA4EC3gAnyDSQtCsLEUWSAEgXuJfxBAAJAFYACNBChAA0aAExJowAEM+NABEpCcC6AAJykQwQYwcCIJUMA6IVjBbLbIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEf5lIAAACH5BAkEADAALAAAAADIAMgAh2ZzlW97m3eDoICLp4iTrZKbspSpzpqt0JujuJ+y0qOrv6W31au816yyxLLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J38DU8sPN4cTW8sXK1sjS4sjY8sza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4drf6drj8t7m8uDi6ODk7OLo8uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLRsvCQwMEAwYEEECggAIPOUloqMBgwYEEDiJcIGHXg4IAAKJLnx69wISZJC4sMMC9u3fuEUD+xPVQgLp58wNGvERR4bt79wxUuHzRogVPFs/P66degMVKFxUc8N6A30WQ0gohWCCBBBA0KMEGIaxgEwvl7WehdAKoh5ILDhDooXcLyEdSChY0aOKJJlJQwkwjCHDhi9JlcBIKCXxoI3cHMBcSiSj2iKIFJsA0AnQwFgkcSSoIeOOSKIAkAoM+RmliCC6xMECRWAZwwkguMLDklweIuNELHEhppokYtFQhlkUKMFKHX365QEdlnmlnmiplwOaeDYQEQpyAXrCRCHYWCsEHKVm5J5tafuTCdoCCKaZFJkBp6JlBntTAonsi8JEGkQJqIEYYXFooBSi5yCmbW3ZUY6j+cbpwkQmmGrpiSR6suucDHZEAK6AdXFRirXZKYNKmumJZQEcX/BqnAxatQKyh9pGkarIwBuCqs2BaFMK0hVI50gnYsqlhRipwG2eTFG0AbrEkjVAuljJqhIK6X+o4EQXvnmnsSLnOCyOvGvmK740aVNSvnS+MNIHAMPapUQcH3yjoRC0sfKaEIukJ8YUKbERxxR9WQNELGptZbUgef7zfdRr9SbKHwU6EcspRrgwSuS7vVy+6M3soHkWW4nwiSSz0vN+RGbkQNIH6SjSs0SZaUNK1SksXgH8bvfr0d5NG9AHVJyJKkgJZ89dRe197B21FN5MNQaYjtZw2AARvJHP+29xdXBG/ZP9bEpF3t8qRknyHLVEJcotbErJpe+pRs3y/fRHgOFPQsEksYO1yox654HXQB7B7UQpU33rSw1lL/NHIT5us0dgpm50SAUoPwDVIkM6cgKwbubvwBpsn6nm5AZwLkugzl+6R8OBiULxKPAscwM8i3Uvy0B7RTuwH06805LwBwFySweoeUDNIJmBeqAWqv3TClclenxKN3B7AvUgiuB+lBCIIH0wQoCsCGG5DEfhVfFCSggShSAIW+MAKBCiTEeCOTQTA3kpIMLobLSBhLmnBBH+SAQJeKAAEeMDuXtIBL33oAAzQAPDmkgEFEEA3AAjAAAqAgAmskCZzICDOq46zAAfIcDZITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQTExAAAAh+QQJAwBEACwAAAAAyADIAIcpJmQ1M204N288PnRAQ3ZCP3VLUoBOTX9PVoNTW4VWYIleao9mc5Vrd5hve5t3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3r7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCJCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy64bpIQGCRAcOHhAAYOHHTiFxDjRgcOFCxlAjFDxg7aH3AyiS5/OQIIG4DGFqDBeobv37xU6/pxo/jZIiAfU06fX8FJIiwzg48c/4VYGBPX4qTtgwRIHB/kAgncBDWuF4EB+CE7ngUotXBDgg9+pgFYQGiRooXQSnCTECRB26F0HZ1V44YgZlsShhyiCqNEQQOhgQw059DBUCCPWyAB7I7WA4o4V0HdRDzCQIMKQRIrgQg4/3XCgjSOWIBIPDvKIYgwV9bBCkVgSmQIQPd3H5IgOYPfRf1KieAF5EdkgZJZsimDDTjR+WSMGIOlY5o4jSFRDm3yKUENOQaAnZ41ibiQEfHfuiGZDNvTZJ5I3xTnoiHR2ZGeiKObpEBBrOtomlzG98MEGLxBBwaQ1OuARCJjueMFD/ld6ymcKMKFQAAC4AiBAA6gSypEQUbbq4aIJ6SCrozq4NEGuuQ7Qa40LbkSDsDtKyJALx/bpQksfMJurAc+OSAFHJ1LbIQgMDWFCtnyS0NIB3uKaQLgXPsARq+Z2mAFDPbDbp4wqvRCvvPRaqOpG+Ob74KsL5eAvn2+qtMHAACxQsIVBbISowg8KsVCjD7P5p0rdDmzxxQhmrNHGHAPosUJ7hpzlDCv5QLECKOdn70YdtBzgvgsZKzOWkKp0a7wI5IwfBByN4DOAHPA7NJbJroTCwOAqnV6JGpX79HcqKjREp0OTMERLEcRLgNbrcRTD1/M1BMPUQ27r0gYBMCsA/tvUyfAr3ODh0JDQUxft0qgWvOCDBHxHdzBHPQNeAcMNpTD1CjeV0PiNHr0tuY8NATE1wDUFyrcDN3h0KOAX8ACRwyGPnDnfOHYOOOgPzfAwzToJmnOYIbHM8ZkTgSwrCRHrJIPWIYiEw9ctVATEuo6uUDVPmqNce0iec4w7RTnEiiUJK9hw9k8YXAyByiM5rTAHL2ekQw41zGBDD+cLJeKzErBPktet6kD8yuKBZ2nAfyVRAbVOMECzyMB3THJA81aCA+G5KnptkRSYDviSS5mJgXEJgZcQ9AANpE4mLSDTgzJwAtfRZQchwIAE0LMbCFDAAye0yQ9aMIIOwAc5bhwAgQpcOJsiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychG7iUgACH5BAkDAD8ALAAAAADIAMgAhykmZDUzbUI/dU5Nf2ZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AH8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL5rtjxYUGCRIYQLCgwYQVOHvIGJGhQgUIEixkACHD7ooIBghIn05degQPM2VwgOCgu/fv3Tn+qIi7okH18+cX1HgpIwP49+8t5Gi7Y0J09PipR2DZAwR3+AB+x8FaNSyQ34HUJbAeSjlYEOCD31Uw31kr3IfghQYAZ5IM/0HoIQTNlQWDhReWCENJNnTo4Yo2kLVDAiXGKJ0BO4zUQwUr5tgdBD2ItYN5MsqYgEg9uKejjhVM5AMPOtzAg1IaBCkldiCZcOSV4z10wwkdbOCllyfMYNQOCEgZJI0f9SDBlUfy2NANInwp55ci3GATChgwoAAGMSB0gZlSTvDRCGxeCQJDL3Q556IbvDBTCAMAIOmkA6BQ0A4kAophjRz1oGKhH/aY0AuMlrqBmC/hoEAAk7YqqQL+BJWg6ZQdtQAqlgnpoKipi9rpEgOuBgsArAJNMGuQ+3EEwq1HDogQCbyW+oFLKLAqrKuW/lDmsTEa0NGazOYIAUI0RGsqqitFeq2rAfxQA7dBnqhRDuEe2aJBJ5hb6gksobCusDHAAK+MGmZkQ706hljQrvrO6cNKGPwb7AMeDByjBhupgHCOJhikQ8Ol6rASsBK3ekCUFl+IsUZWbuxhxwXdADKjvqakQMmtKoByygdesFHLLj84gkEyzzwnDRDjPCkFsvJ8IJUZ2Rr0g1kSxIPRc4qsEg5KS4rCCk4fWMJGMkz9YAsG+YC1nE+uJIDSAuCwQ9j5LZhRD2YHOGH+QSmsvQG/LIWgNAYCbUs3dWhuBG7e37lp0Ax+u+DSzRIT+0MEh1fXQEccMA5eBgl9gPUHD0/+7wE4DNR05tKNzZHUnneHNkJFz4w0TCi8zS7hBM3NOgEG2K0R3rE7AMHeB0EOMgs0hUCBAgU8gELqBv3JuqAeERr7oQuRqq+jQpGZeeIdqem54wvpILqpItw+VMWH+wySxowP/dAL0MrZAQkvlF4UjGFLAKdAgiOzVUBUEbkBDW6gA/8dRXw8C56NFrex45FFYCkzgOtGcjCXQWB2ZAHbwDR4krIh7INogQEAZ5UAeZ3EBgW8VQXuhZYdYE5TCxggSnrQOVBZAIGoalGhlBZQMJbA8EoWUJhbSgCkAxlgASuLSQuM9CAIWABmdCnBBBagG+AloAERKIEOadICEFjgOMarQAY40AIgzuaNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCMpSxnScta2vKWcQkIACH5BAkEAEAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLFsyDBQUGChIcSOBAAgYWOH3MCHHBwoQIEzR0IDGjbm0GBwpIn069wAEKNmQKvxDhgffv4B/+RAihIy4LBdXTp5eQ3eUMC+Hjx+9Qnq0NB+rzp8fAUocG+QDGR8JaLCSg34HUMaDSDBME6CB4F6S1QXQIVliAAjGcdEJ3D3b4gAU3mPWBhSRax0NJK3ioong+kBUDhSVWqABJN3C4YocWjMWDgTGS6IBIPjR4o4oaNJVDChmIIAJEFPQYowkhhTDkjS4oJYIBAQCgpZYGpMCQDTA6WWECIOlg45QdTsAQDjC08MILONiUwwJZbmknAAssJIGYMX7wUQdo3rhCQjiMwMGhiHpAw0wpDHDnowAM4OVBMfAZ4wEe3RDojREc9EMLiIaKqAo/wJSDo5A+GgBCGFgaY3v+G5Gw6Y31DfSDoaLm6kGpLjWQaqoIHMSAqyVu0NEFs654QkGg5uosCi6JUOevjy5JkA3EljjjRjoku2KOA9Xg7Lgc1NCSAdSmakBBLGRLIqYbzeCtip0OBAK5znrAUg7TpntnDgRt4C6JJ2p0wrwqtgjEDviOG6dKKfibqrUCCTxwhRkajLCHIQIBQ8POvrCSCBJDCgFBe16MIHAaAbrxg80B8QLIuaqwUgYlP1oBQU2qfCCUGkn5soNVAtEszYi2sFLEOduZAUGt+qxfxhnJOnSAHc+MdNIr5dC0nTIQNKLU+cGKUYpXA1gfDlsjau5K6H49QEEmkJ0fRy6kDSD+QfduvStLJH/9NEE82J3ethr5oHd84Aok7tYwuIRA03kaxKPh0vHHkZCLezcgQSggDa1LOQhQsgAAG9Qz5gVQHXTn33Vsa9/4etADTCmYnm4AgxtUOOuIb6Q47I0T9IMK+JZwu0yT/4pA6gjhh7mxHv3X+bII0ZBv5DWlQMCjBOy8ELaGJ1BwR90uPoHCCdXwggottIADrzhlUEEFIoTtUMpkA/3n4kX7Cg/CdDEJAOlMG+vAWCrlMwWcDySaGpoF2BeWul3sAGYLSd42FoFajcVi2UoAy0pysHlNIGZmaRexGJBBksgrWRfwoFlsgB4xXeeBJ9EBfNA0Hgqm5QOolyMRA1y3khVwTkUXkJ1bNjAsBB3AAUR0yQmQ9aAIaECJcvmABBjAowMowAEUiAEOZ7KCDlxASBGwgAZCcAMfzuaNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCMpSxnScta2vKWagkIACH5BAkDAEMALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31arB5au816yyxK3D6LKyxrLA2rS6ybXJ67XM8bbL7bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjW78jX8MjY8szL2Mza8s3W5c7S29Dd89Pb59fa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+jq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLPvxDBgUGChIcSOBAQgYZOIPcKIHBwoQIEziEQHGjbm0GBwpIn069wAEKOmQKxxDhgffv4B/+RCjhI64MBdXTp8f+8oaF8PDhk2+rw4H6++kzsPTBIb5/+CisJUMC+BVIHQMq3TDBfwyCh0FaHURn4IQFKJCdSSt01+CGD1hQXlkjUCiidT+UFAOHKIoXBFk1SDjihAiOtIOGKW744FEmNIDAAAQYsIAJE/1A4IsiOiBSEAvWiCIHRPVQgQAARClllAJUEBEFRL4IHEglKFljc0G9MMCUZEo5wAsO6eBilhMmAJIPNHq54QRhBlDmnQAEACRDErD5ogofhSBnjTQgJIRNNtiJ550BoKlQDX6+eIBHOwxaYwQF8TDDCSJ8QAIMOcg05qJ4CrBQBpG+eOFGKFha44f+QszQ6Qe01joDTBCQSqqVCTGQ6ogddISBqymuMIQQJ9SqbK0ivASlrngGkBCkv4oYo0aVEovigzMs6+0HLrT0ArSkboBQiNVSOOlGJ2rLYQQ5fPttqCs1QO6iCCDUQboilqjRCu6i2IK83oa7kgH34mkAQqjyO2ENrAbMoQcEL9vsSgQkfKepB/XpsIFbZiSoxA1qUPGyh6qUscZkDoAQlh8XCKhGXZLM4AUnKwvESgiwTCYBL8dc4KoY1WyzfyDkTCsJLC3g85QL6Cs0fkRfBPDR/rGgNLgsmfC0lOYepMLU93FEA9b+8bA1Di0963MAPSD0A9npKcBREGjDZ8H+EC7kDINLFXzdgEJD0i2dfhwlmbd3ASJbsQgptzSqxgLEnRDMhhdQddGLf/fhEDDIm0LkLdnAsp4LzZ253R3h3fneBOXgAgm0iuAC2zJ5fW8AvC5kn+EjfNTf4jEkRPpML7i9qABhM6SD4dd25MPiNwqVa7QQWO6Qx1OH7NHIWINJ1AYLECAAjwZsoP1DP6zpsARHxilxCGNR+7EC/oaULckWrDjW2A5LwOY+cjaJTeBzY9lXuhQAMQwFzAI7QIsMqsWA/JnkBtrCgP/QUgP0sOk6FjzJDt4jp/FsUC0jKJy1GtiSGChuWxGESwd8ZaADOICFMFnBsBoUAQ7EkC6GI5AAA4Z0AAU4gAI1COFMYhACDCQpAhbgQAl2cMLZWPGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOetIsAQEAIfkECQMARQAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOlqvQl6vQmq3Qm6O4n7LSo6u/paW9pbfVp7zhqL7jqsHlq7zXrLLErcPosMftsrLGssDas8LctLrJtczxt8Xcuc7xvMLQvNDxvcnfv77PwNTyw83hxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09vn09/y1uHx19rh2Nnh2t/p2uPy4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AiwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8teHEQGBwcKEhxI4MABBxk4h+RYAeICBQkUQIBYkcOujAkHCkifTl36BBYzc5CQ8KC79+/dSf7QiCuDQfXz5x3seJljA/j370H8aBukAvr75yewHIICvv/3JKx1QwL4FUidAuuhxAMF/zX43QXznSVDdAZWWMABN5yUA3cOdviABDwg5UNHN1BoYYUYlsQDhx52COJQKTQwgAABACCAAQukcNEOBJ544gFBjPQDgy22KMEQQKUwAABMNunkABZQFIR5PvrIgEhDuFdkkRv45AMCToYZpgETcVDlmSaEtMKWbMbAkw8GiClnkwOM+FAQJp5p4QEgDcEimx5KsJMPBMxpqI0QmalnlRx8tCagW66gE5iHGkqAQ3guWiWfHfkJ6ZaC4vRBpZV+0BALmqLZEQ2ftkmEDv4zqOACDDrEVCiphgrQkAOpVqkfRyC0umUILoxg7LEqCOHSC7hWqqNCQfTY654cDUGksB5GcOy2xgLRUgPNHorAQjdMW2WCGfGA7ZYecHvsCS3dGq6cuiokg7k+AqdRDusW2YG7x9rAkgDzzhnAQibge2KaGsXQb4sZAGysCywVbKidCImgsIUibNTCwx5iIPEIJVRssZw4KKToxgUynNGjIDcYscQqsFTjyWEuhCrLBWKnEasxN6jByDCwtCTOdOrMc4H6ZgR00P79K3ENLC2AdJMLLLTD0vihi9EPUP/XLsAqEMESs1cDYCq0eXItHacaeRo2eBBIXEIPLh2Ns/4AGCc0gdvV/boRCXODF4IK7qrgrUspXB0lQwkDPp3PGzlcuHfj2QCDCrLaYDZMcZ5MZkNBSP52kNVe3t2ROfmgd7h1PvS35I16RPjlkurk+rwDpHyn5EB+NMTlrPMUOqkE9O3QykvX7mjhuff0AcGGQimlAlwrgLrwF4R9AZJAfbCA3gEIsMALykeUKcsJeC38n/1SEGFR6Vt078YHNC0SvyBL0FxZ9zPXAShHEv6tSwLjOcuApoWglCwIWxBSSxBmtygGbA8lQ7gdpDYAvrXIQFonYkCGXJKDa7VoAyGCiwl4ZaADMEB/L4lBsBwkgQ38jy4sqAADenQABThgAnsyuCBNaICCDRBJAhcAAQly0MHZOPGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWnIsAQEAIfkECQQAQgAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOlqvQmK7SmZm0mq3Qm6O4nLLWn7LSorneo6u/paW9pbfVq7zXrLLEsMftsrLGssDas8rvtLrJtcjmtczxt8XcuMrovMLQvcnfv77PwNTyw83hxNbyxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09vn09/y19rh2Nnh2t/p4OLo4OTs4ujy5eXq5env5ury6Ort6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AhQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8t+/MMEBgYKDhxI4IAChxw4gcQ4sUHDhAkWPpBQ0aPujxC5C0ifTr0AAwzAYwJpYfyB9+/gH/5sONH8be0E1dOnx/BSuIXw8OGfcCtDgfr76UOwxKEhvn/4LawVwgH4FUgdeym1MMF/DII3X1E88BASBgZWOB0DKJ3Q4IbfbQAUDygsMEAAAJQogAEZcMSBhSxaZ5IKHMYoXk88NEBiiTjmCMACGYXQYosUkNSCjDKSsBMKAuioJI4BoGCRDQT+yKIJIumwIJExxpCTBDcu6aUEFTEgZYsHZPfRBljKOEF5NUng5Zs4pigRC2P+GORHNKRJpJE1vdAlnF46CdEP9tXZopkbAdGfnjKyGRMPAwAKaAAR0WkokB7lyWiRNDUgqaQNQOTApWR69MGmas7Ew5+fLhmAhP4N/RAlqRbKwBEQV6LKYQku1ODDSx60KqmcDFlKq4UIaqSprhxeMMKzM7hkgLCAGuDQisdaiOFGMDLLYQXPPgtDS0lS+yalDVGQrYUHcESCtxxCEO6zO7DEqrk6OjTquga2u9Gp8DYo77zRqnQDvnDCuhB6/Br4w0bvBdwgCPOusBIPCL95Q0OFNowfohctKvF/Hcybgr0ZL+mQmB7jxxGaI/837wgFq1RuyjgK4BCFLauXAEcaxhxfBDPXuxICOOdobUM891zdthoFLXR44IZbs0rBJl2iBw4Z6/R0yWa07NTfOStuEC1hrHUAG8f6dXUs3Ep2eCKkMIPRLnmadP6oD+379gEPcwTw3BMAoerNCL8KkQlvS+eARzHM7d0HNaGQcgBcD8rw17Z2BETEZONgUwYZgykR41/fCfncfNrkJrUBmD7R5h6XGRLoI6+Z0wuIw4m5RTI4rV9IOEwd4E6vnyuBwhV5vW7YII0N74M9ZYDAnwEQkAHzF6nLrwKBj/RuwBoYLtQNN3C/UdO0MhA+SVLruoH5ZvlIKwbvlzSkrifQf5YMtPvRAYanEhzgjkgTOB5b7Ecm7Lxkf2oiT1ygU6EEYMAGM+HOhixwAh04JwQUYAB6dqMAB3AAgzfZDgk28B7kaOADKvDgbGZIwxra8IY4zKEOd8jDHvrwh1ZADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73KJaAAAAh+QQJAwA8ACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7iesdKfstKjq7+lpb2lt9WnvOGovuOqweWrvNesssStw+iyssaywNqyx+u0usm3xdy8wtC9yd/A1PLDzeHFytbI0uLMy9jM2vLN1uXO0tvQ3fPR3O7T2+fV4PDX2uHY2eHa3+ng4ujg5Ozl5erl6e/m6vLo6u3q7fPr7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gB5CBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy5acgwUHBgoSHEjgwAEHFjh3zDCx4QKFCBQ+fDAxwy4LCQcKSJ9OXXoFGjNniIjwoLv3791J/tyIy0JB9fPnHWB3OeMC+PfvP4xnm8MB+vvnK7Dc8QG+//ckEGWDSTEkgN+B1CmQQ0o1UPDfg99dsENPNniAwAABAKChAAZYABIL0SEoYgEHxHDSDNxBqOIDEdSgkw0LZKjhjDQCsIAMHMUQ4ogillhSDSmuqGKLOEEgY41IatiARjTsyKOICZB0Q5BCqkiBTTYgkOSWMxKAUQ4MPCkmAyLtsEGVaG5Akw0GcOkmAANcxIGYdI4Qkglo5rnCTFq+6aaXFOXgJJ09grQDlXkOKRMEfvoJAUVzEipmCB/hmSiaKMBkw5GNbhkAjhEJKqmYB3h06KVoRgBTA536iYBE/ieMSidwHL2Aap7NtbRpq36C+pB9sj4pQUf93VqlCC55wKufHj4karA8lrrRqcYKqWpLbS7rpgEQxQCtmAtqVEO1aMLQkgDauhkARCx8+6SJGs1AbpUg6MASuulyCdEI7vJIa0YrzCtkByqwlK+bAzoUQr8jUqoRCgKviEEJK9lwMJe+MhQpwwfaqZGlET+YAcUrcXrxjPtyjOAJGwUc8oMatMDSACfXKABEsaqMH7wZ2fryfyDgwFKfNWvI7UPe6nzfehmN+7N/LrRkQdEzNutQDkqjF+W0T8M3gUsWUw1Axg1JkHV1+nEkQtfgBegS0ScfnfLZ07HMkctsd/fC/kthnxxAChJhTTeJ4XKdN4sTvsTqyQtQZDbdHHy0dt4myJRtvggkHDjdCRTO0Q55U5C4pjSnO4DmE22ss8OVsp0pTZe3SgDqE+Vgns4KhrSDez9LaNPijQYAAe0UNaly5yNN+bLoOMkA95YEAL5Ruwz7SJK8ERO5EwQG4AtAAAEQAAHZGlH/7QF2l4Q9uRHsDZQMMhDvUYHQKsC0SQ1We8F8aOXwuKQM8NxJdjC5S21gdGo5gYHGxDOWvMBBaXIRXEIQJgQdgAH/ggkKzgShCGwgV3Q5QQUYsMADKMABFWCBAGfyAhJsAIIRuMAHSDADBM7mhjjMoQ53yMMe+vCHW0AMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y9bCQgAIfkECQMALwAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AXwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8uu3IIEBQYKEhxI4EDCBRM4XaDoYKHChAgTMmz4oKJuCxAMDhSYTr16gQMUUsh0UcJChAfgw/6LfxChA4u4JBRYX78++0sUFcbLl2++rQn17PNbv8BSRfz5AI73QU8eNEDAAAIIQIABC3hAEgnS6SchdQyohMJ3AWYYngU5rbDAAACEKOKIASwQkgYRTqiiAtqZFAKGGsZYwXk0rQCBACPmqCMEHmmg4o/UJdBCSSHEaGR4E7gw0woG6OhkjgRwZEKKQKpY4UgqwHhkjBzGdAKIT4YZ4gAiZJRCAlWmKYFILEyw5ZsbwLQCmGKKKcAKGEmQ5p7AgbTBm4A251KTdRYapUUk7LmnAiChACigFbgEQaGUAtCARQwouicIH1nwKKAlsLRCAJUWGsAJFJmg6Z4JeKTCp/6ATsBSA6VSigBFF6y6Z4sbfQAroDSmhGOtdQZAEZq6VqlBR27+umUIKolALKUORqRqslU6wNGrzm6ZgUq0TluniRFxgG2VB3A0QrdbRqASoeKGaYBEPp4L5JAaFcnukUqiREC8dkpEgb1A9plRB/seKehJwwLspLEROUDwjyRslEHCRqKQEqkOOymARHpOPCGnGv2JsYahokRnxyMOILDIE/KKEcInZxisSQiwrOOhEOUKs34yX+RrzQHeXNICOudILkQg/JxfuhuVQDSA7qZ0QtIjYiBRC06zp+1GLkw937cqNaxzAHhKhGzX1PHHUbNihzcguFgDsHREA7NNXf7QM8ctntEmjZr0qanqPR2jHXHrd6QsTarzpRXhxzYHH/0X9wgu/dvxAGkXzvaVrvrdZUtzOnwnRhJ3XTFIF4ut8UsnmF2rAGVilAKVE1PAppYYdyDTCprXSgCqGiUKswL4huRozRX0K5PjlAYAQecaNT2xkCVJjXGSNyEtZonEe+TzuSyeNDS7M+qEgYE4BjAAgyJQ/5HxyTKQvEnLO2uB82eZsPae2LnfSVQAN0CVh39p0cD/fsQAg7EkBAU0kgUW5hYNZGpCB3CAA18SAk9pKAIZoKBcNCABBiDrAApwAAVMIMCZhGADFmhWBCqQgQ6oAIGzyaEOd8jDHvrwh15ADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAzkVQICACH5BAkEADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLxnwCgwQGCg4kYOBAQoecK0BwuGAhwoQLGjiQsKsCg4IC0KNLh+7gt8wWICw82M69+3YNy+H+uqBwYLr56Qmst3zhIYL3994nhNfJ4gOEBQsQNIDwIeWG8ucFKJ0CJ7AkgnvwJdidBSvg9AECAQAg4YQTGlABSS5IIOCG04WQ0gscKCiidybU9MEAFKaY4gAXguSCAxzGGB0GJ72gwYg4cgeCTCwgoOKPKRLwkQsMyGhkAeqJ9MIFOTb5wHwtoUAAkFROOAAKHWl4pJEejhSik02WGKUAVZYJQAAjbLTBlkceUGBIIoDpZAQNrsTClGaWKYBGKgDIpowKhNQCgnLmaAFLBuSZp5AYafknlyB9WWiYKmWgqKIZXFTCo1sm8FEKk4I5gUp4XqrnRTByemSSGt0YqpP+UJJkqal5tjiRC36qGqMEHb1A6Ks4coBSorSaaUBFIejaZkcmADvnSSxEWGyZAbBAUarKylgCR646m2MKJs06bZm2RpRAtkZuwNEE3jYpgkkQjGsmBBSdi26MFKzbbo4emNSAvGUuQNG9MjrA0b45amCSjwBTeaxEKhAcIwMbtYAwjheYtEDDVCJwq8QcBqrRCxePeGhJG3P8o8ASuQDyhryOXLKIwpYUr8oqNkBRri9Ll+9Gv87cXb8liYvzhJlO9FzP5tG4kXZCv7djSSwcTWEAWE7kKNPRdamRpFFzJ2ZJDFv98ETJch3dAS5w1GzY3EXwwkkVWC2hzhS5rDb+db3CzZ3CKJF5tADWVrQ1015vBHbUY4drdbkTnbB3Am13tILfE8ydUqkNM4qq2qxu1K3QsZrEAooNE55RxExTDJLFUWfMEgoNo7kRBT0fsG1IHggdAbgtjYB6sQOkyRG2BIfu0ej7lq7SncUSUDhHLix9788jvQD1vkTHVIHgZgoAwfQdVX99jds72/1MEHCeYgAEjE8S8o8eoPxIzBcagfMzZdAAAgQwAH4qQD6SYEBVCkgcSkDwKgs0Li0ZYtMBNlA5loBIThEQgebccgLyTIyCMllBezCmwbqUAAMMYMC5dKMAB2zgTTZJAQgucAF2GccCGhBBnWbDwx768IdiQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSUisBAQAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8vW7CIEBQYKDhxI4EACBhU4X5jwcMFChAgTNHAA0aKuiw25C0ifTr0AAwrAY74QYfyB9+/gH/5c8NB8JwsWIzJ8+ICipYsOCarLl0/h5QsSE8Lr1+8BJ4sKBAQAwIAEAmAABCygVIIC8zUo3wYspWDBfhTqJwJNLCwgYIEcErhAeyRtcICDJFJXX0oiRFDhiuD1B1OGG3Yo44ALjERBiThOxwBKHrDo43cXvMSCATMWSeAACX60QY5MWmeSCD9GKV5LIwhg5JUACDCCRx002aQDJJEgpZQarMTCAFhiKUCSGp0wopdMdiDSCiqOGSUJKQ2ZZpoEcMQAnE0ekN1HF9gpZQTlmdTAnns2oFEIgHopAUgmGDomByexECOjRgYAokUuMBhpk4Nu9MKElkqZ6EiLcpomAv4YQTpqk5N2VGmqUmJKkqau7vkpRQ7MGqhHGuB6aEkZ9LonBKC+KWyOJXD0Qp3G/pgCSUQqi6UBFsn6bI61anRrtT/qKtKm2hbJpkRLfpujAhxBSe6PFoyEQrppbkmRBO7meABHHMz7YwQjjYAvlh9UFGy/Jf67UbECs0iwSB8cfCWzFInKMIkubIRqxCu+IFKyFhdZQUUab9xgqRd9DDKFq3pkcMkzZlDRnyo7yFGhL1c4Egs0z/hrRDfmPF8CHPXY834TkBS0jENDhIHR88G7EQhL71fvSAg8XSC3FXlL9XQnips1fyRV4DWBjlbkwtjVhSDt2eGZsOvaA0YdEf6/cBdwQMcA0+1dBCKT1LXXYFvUZd9gdiSm4GWWBPTTAeh7UXxwR+tRfnRfq+jTNWa0ONWNe/R41pGfRADNSG6Eec4HnBAS5z1HsEKeVh68JkclUI2BSClkDcJKKOSurZYeid1v2SGNG7GLZq6uLAF6Z8R3vwoAPlLAEVtQeEutMhpAA+t6VPS3DLAsktLkXhAzSygcjiUBlovUrrAUaP8kuR58HxMLEJAehwJAgAbUjyQneB2cDgChlayAdnaKwIVygoIMVKACHxhB+VByPyYdQALqQ1EEOfC+tUAHRweggOxkwh0fRcADt6PLcyTAgPjsRgEOwMAKbbIdDlwgP3DIsYAGQBDD2RjxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrSKgEBACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL5uyiAwUGCg4cSOBAAgYVOF+Q8HDBQoQIEzRwANGirgsMuQtIn069AAMKwGO+AGH8gffv4B/+XPDQ/CaLDw0MDBAggICBBRlautiQoLp9+xRevhAxIbx//x7U9AECAQBg4IEIAmBAfCiVoMB9EFZ3QAgspWDBfxiGF4EJMY2AQIIgJmgACiZtcECEKFKHgUoiRJDhi+CB8BIEBYZo44ENjOQCBSn2OB0DJ73gAYxEfncBSyx8eOOSBhIgEo8+RglkSUMWaeWRKbFgAJNcAjAASBtEKWYB+Y0kgpVoPhAgSkp2yaSTHZ1w4phRdiDSCi6maSUJJ0HgppsLdPQgnVEekN1HF+ppZQTljYRCjX9yOcJGYRIqpgQgnakomhyU1Gakb2rkQn2WinnoRi/0tymajYL0Aaj+fzJ4UaWlRolpR5quamWnIm0Ja5cGZORArWIe4JEGuqIZgUgsQPrrkgGwcJELcxLr46kYvZBnskW22lEGz7pZwUUhWCvmihuZwC2aMoK0QLhdInARlOb26ABHVa5LpAYh+QrvksFaNGy9PSbAEbL6EjlBSAT8y6QAFw1MMIrGboRwwi8uC5IADkN7EakTo+jCRqpi/OILIDnbcYIQWwRyyBCOrFHJJmOI8kcDrGzjlxYxAHOEBm90Qc0ZLgySvzojCGdFEvwMoQIccUA0hhaE9G7SCcprEb1OUzelRvlODR6WH4GLNYKyUtRB1/hxRILYAIbEwtkIkmiRC2xXVwL+Ry/AHV4KIn2adMAX+Zx3ARVzNLTfD2gc0qt0j4vR2oeX6TbjapKE9MpLT/vyzwec4FGqfkewAkkonP3BRpR3bXlHb8O9JklXrxwoR59PbGhINJvM6Embh2uAtByV0PUGIqUgtggosZDzvwMQ31HrE78OUuwmz45S8JESIL1HTU+sgMwjSY2xBTerVDuoC3z/EdfEMkA+SWEne0H6K43Q8J8ETFoSBuaiwPxKAoJ1eQB/LcnA/m4UAAJAICUlyF2hkLeSFPROWcyrSQUWQACOBWAAAzBABdyHEloVSoAvydWiDhiXDQwKRQmggOhkIoJEvWgCHjgdXVSwAQkwoD5/u1GAAzAwQ5u0QAQcuEB/kGMBDYBAh7OJohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMJFUCAgAh+QQJBAAvACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm3xdy8wtC9yd+/vs/DzeHFytbI0uLMy9jN1uXO0tvT2+fX2uHY2eHa3+ng4ujg5Ozl5erl6e/o6u3r7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBfCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/Zs4kKEBQkOIFjQIAKHnCo+bLBQAYIECxk2jOC5QgQGChQ8eFghs3aCAtiza8fe4LfM4BUc/ogfT158huU1PTAYAKC9+/YEFHho2WLCge34tyfw3tJFBwjlBVheBejB5AF77yX43gAUqKTBfflFqF0CJrAUAoACZkheBSq4JIIBCoaYIAEnmNRCBBKmuB0IKbmwgYYwllcCSx4EIOKN72FAUgsNqOhjdhqc5EIGMRY5XggqPWAjjkwCwIBILSzw45QF8CeSCxYYqaUDBZb0QJNgAqBASD1SOSUJJBG5pZYomETBkmEyqaNHGphJ5QEVhhTCmltC0OFIIsAZJ5MidJQChHb+uEBILGDIp5EWkETAoGEO0BGKiVLJ4kcvPrrljCFhQGmcc2ZEQqZmIvARCp6uKYFI/pOOCqYAG5WJ6pRWaqRmq1p2yZEIssY5H0YtIHqrjxF05IKjvBa5AUgMBBvmmBiBcOydHZXQbJ8gxSotkwFkZOu1PqbA0a7bFsmCRyd8G2aJFyFA7pRBbiRBuloi2RGw7jY5rEXyzuvjBRzdi2+RH3jkQb9NNmhRCwL/2MBGLhxsZAYeUcAwkw9cZELEPk6skQoWF4lxRxpvfGPHFqUAsoqLasRCyTFG2pGoKotYKkUQvyxhshpVTLOGz+6bs84YGeuzdhNwxOzQ5HXg0QpHhwivRdctjV+9GoUHdYD6diRA1QtmhKnW2m2qUadfkwdqRwqQ7R61F3GAtnYHdDRC/tvkQQBSu3IDsHNFPd9dANAU8z1e0R+NTbYA1IlreAFqb4Tu1297hHPVDptqeAIteMQq3xW4AGvVBHQ0rs9ofnQ5zW2KBLjKAfyrkctai/zRzF+fPNLCKrPc0QRLH9A6SB1ADUHsJMXNMN0eSfly5SBlSXPmJEXrrgKRf9RC1gI3TZILXh8sdUqbjxpA5yGlAP614pfEQvnbnq+SCAgOSoDtI62e6AG5Ksnr+AQBX6kEA45r0gCEdxLioSoB1DtJ8lpVAey1BAMMSGCCBqAAEXQvJSk425QOoIHQtYQFbNMSBEJgupqcwAPPeQAGRFComJjAPj5aQAm/858iWYCFh3UBwQUWsAB56SYBDdCAuW5Sgg9YwAL3Mk4FMhCCdc3miljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk1MJCAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm3xdy8wtC9yd+/vs/DzeHFytbI0uLMy9jN1uXO0tvT2+fX2uHY2eHa3+ng4ujg5Ozl5erl6e/o6u3r7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/7sogQFBgoSHEjgQAKGEzhfpPBwwcKECBM0cACxAieLDw0WEBggwACCBh9kugjB4ECB7+DD/hc4QEGFzBcmLkR4wL69+wcRPLSY+QFBAAD48+sHYKCCyxIKiCeggOW9lIIF7yWYoHwvfTDAfhDuN0B2KZ0Q4IAYiocBSysgqOCH74HAEgsLRGjifgigVIJ3GbYIHgMqpbAeiDS2d4FKKBBw4o75DTBCSRuw6OKQCphnkggz1qikBfOZNIIAPEYJQAAUhrTBkFiCl4ALJYmg5JftTfBCSSjcJ2WUAfwI0glCZjkkjCOtkCSYSt44Egs6nimlACCpkICbgEogUgsT0GkoByMhoKeeBHwkAaCQAgcSB4ZW2hxIHyy6aAYdlQAppAqAlEKllVoQUp6a7tkRA59CGsJH/heQWqkJH2WQ6qL+aXRCq5Am4NEKslY6wUcG3KqnARthwCukRm4EQrCVNrkRC1AaK2UAG/25rJsbdFQotHSK0JGt1p6Z60W7buumAxwBCy6dGnQEQblnNpBRB+q6eQBHJLxLZwQdKUpvlMhidGW+WXKpkZf+gjnmRsUOzGOjGFGAcJaSZuRBw2BeqlG1Ep+ILUYOXIxlCRtpwPGXKXD0YMgn8onRoya7+KpGlK5cI60boQozhANkZHHNLTaL0cY60yhtRiX+HGGKGClLdIZGX/Rs0iAujVHTTu+3QEYhTI3hvhuZgPWHAHNEbtf6cYqRC2IPyO5GL5ytYLwcsWAm/ttTsqCRtnGDtyFH39rdnogB841fwUIHHl7VRxvuntYZrc32uRil63ioHbkruakfvdz1AH5vdGHgHXzkoeEkgDQC31Xq6jicv0puJ0gR/8w4RyXHjTJIKtvdckgsiC6xAKV3pEKbJlMw6Jwre0BSmSFTGZKnRCugcEijJm3BwyNlOnAAbocUtslblmT2ymKeNILxqfpYktT5FnnS1f4ymRILAqdKQPIkwd62GLA9k3QPXBcAX0o+ADIpEaB8JzkB4CBFngKeZAWFq1R8FLiSCvgsQgEgAAQAmJINTBBLDMgYS0SQwS9dwGMxyUADDEAAAQhgAAZYQAVI2JINsMpFlQdwgApfIoJY1SgCGoChXDYgAQZo6wAKcAAFTmDBmYiAAxf4VgQsoAEPrICDswmjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqExlUwICACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLHstiBAqdLkpgcKAgwYEEDBxQKIHzRQoQGixMiDDhggYPKWyy+IBgQAAA2LELQFBhZgkJBwr+iB9PXryEDjNTcIjwoL379+05kIhJ/Xr2+/gN3G5ZgkH5//8xoMJLKVwA34EHXtBCSywsYB9+EN63wH4ouUBBeABmSJ4ELL3gAXsIhvgeBytl8GCEKGaXAUonKKDhi+QpMCBKK1gg4o3vWbDgSRCcmOKPDZhUAoYwFnkAcSalACKOTEYQXUkI/CglfgSQdAKRRWZ5QkkrLMnklyuQBMGUZGa3gEguJJDlmuId4MJIL0zw5ZztRfCCSBWUqScAK37kggNsBqqASC9oQOehFoSEgo97/kghRxsEKil6IIlw6KXzfbRAo3oa4JELWEpqJEgveHlpkx+NwCinKY7QEQb+okqKwUcgnHopCB4ZwKqeVW4EaqyBHuBRqbYeGkFHLKy6K4qPXhQCsJN2ZEKxmHKUwbJ6QrARoNCyySFHhlJLJ4kb6YotmZ5qpGa3awrLkZziznnsRgKcS2YAGqnAbqBbatRCvIeGqZG9ZbKQUQn7solkRikATOeTGKFAMJmuYtRBwmtSmhEJDs+ZKUYjTDzlBxlFinGRG2xkacdMiqBRyCL/2OdFsJ4M46wa1coyjrhmxELMjmb0rM0vaozRtDvf+DFGQKfYLEUIE61hCBs1nLSIJmykbMz4ZuSC1BrOmNELV4u4Y0ZRNp3fRqGCXYCbHJla9gN2bpSn2vdpq5H+BG6X50BHHMwNnwYc/Yx3dk9XNHTf4lHNEdKCt5c1R+bi3atGXzP+9psckR053XdyJPHhJHPEN+M4A/55zx1VDnS6HP3qNtzDys1y3R4ZDnQAFXdkMtipe7Ry2ax7dC3Qen/kotQKcA6SjVdbEDpIaU98Zkiyn3yA2KTaLm4EZ4dEwMQGGCzSCTYf4PhIK+wcweQkbXruAuaPFPW+6p9kNcDvozTmrgFIXklOsC5oKaBfJ1kBvKhlAYGhZATV41XvTuKC08WKAc5DyQsCV6wLTE8lI3DdjwhQOpYQUFIMWBhLFHipC0DMJSyAwPggFAACQCBxKwkBt150AAakTCaYJgjXjSJwAZfdBAUZ+MAI6meTEFCAAes6gAIcIIEQZJAmJvDABeAVAQtogAMm+OBsxkjGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV9mUgAAAIfkECQQAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sGy2JEhgoQGlT4gAKnixIYHChIcCCBAwcYSuB8kQKEBgsTIkzQoAFECposPiAIAKC7d+8GKv7MLCHhQIHz6NOflxBiZgoOER7In09fPgcTL1ks4P69f/8FL5XAgHoEEuiACi+lcEF9DDKoQQssVSCAfxT2F4B4KrlAQYEcEigBSy940OCIDHKQEgsIVKhifwSkdIICHcaYngIIorSCBSTmSJ8FEJY0wgArBundACOYVIJ5MiZZwAEnnJRCfDpG+UAEK5CEAn9CChlAkSOdgKSSSTJZ0gpQShkllSKxQECWbAIgwEguJADmnAe4MNILE5ipZwQvhGRAm222CJILDsxpKAMivaCBnoxeABIEgAKKoUcbGGppByGJwOimJHjEwoSRshnARy58aamSB4D0QpmbShmBR/4NhApoAx5hcKqlGHwEQqubgsARC1jKmmVvG5V6q6GpdrQqr4y+ulEGwkrKUQjHXtqRCcxyutGf0bJpAEeFVjvnhxwtmq2eJmoEardaciSnuGAmu1Ge55rpLEYjsNsmlxidAK+hTWq0Qr2MVonRB/qymYFGJfw7p3IapUCwntdhVEHCWUKgUQcOg4mpRiRMbGanGEGKcZC0ZlRpx0lusJGmIkcpQkaxnrwigBnZyrKMLmu0a8w6zowRtDarOOlF1O4cY3saYQt0jvgNXbSKC2fUsNIdMp2RxE+TGPVFKExd4QcaqYB1hzVm1ELXJPaIUbBiAxAACxuZevZ5dXLEKv7b8vGpUYpxg8eRBHer50BHHPBdnwbPBu7d0RglXfh5WjetOH1fY8SC43ITq5ELk+NtJ0cvXN53nxvVHDfOg4eeq0eJX+7rr+sWPbdHoBeet0elK+53RxeLnXKthb+uq+Kze7Rm0QTQDRKMWCswOkg4dm0B6p4CebIAzg9q978JpK3q3gRP4PZHYWMcANkjXd3xARCPxLXIEVQs0gi1CzsA+yS5D+8BlZMf/TKXpuUJq3koOcG7qkWjlKyAXtniUUpMFqkAQKB7J3EB4Y7FgOmh5AWx49UFsJeSBbQpAAvAoEpKsEAwMSBgLUkBBM10AYO5BALcolAACAABz72kA6ThitEBGBA/mJDAXDmKwAXsJ5MP3KYBC4BABkagwpmEgAIMeNcBFOAACZTAgzQxgQcuQK8IWEADHEgBCWfDxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhMpSpXycpWuvKVsIylLGdJS6YEBAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm3xdy8wtC9yd+/vs/DzeHFytbI0uLMy9jN1uXO0tvT2+fX2uHY2eHa3+ng4ujg5Ozl5erl6e/o6u3r7vHx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjywY7ogKEBQQMIGhQYQROFyEoMFCQ4EACBxIwnMD5woSHCxYmRJiggQOIFTM/NBgAoLv37wEW/nyQ6aKDggMF0qtfX+AABRUyX5CwEOGB/fv4H0Tw0MLlBwLfBSggAAP45lIJCrCnoILvvZSCBflFGCF/KrGwQAADZvidASisdEKCC4bIHgYsrQChhCjmBwJKIwig4YveBWDgSSWgJ+KN6jGgUgr1pejjfReYlAGGMBYJAAQnbWAjjkwq4MJJIvT445QWvDBSBUZmCUAFJW3A5JfqJWCSCFOWed8EIo1ApJZFzghSjWDGqeNIPJppZ5AfseAim0YG0CFIKiwZ55ckhtSClHaWuaJHCPCppQEhSTDopPCBxEGimPbH0QeOsunmRiVMOqkDIKWAKaYadARgp1kK8BED/qJOWsJHF5yKaQobZcAqmxl0FEKso3pkgq2obtTArloi0BEGwE76JEcgEIuplRntiWyRAXSUQLODdtDRBNImSkJGI1yr5Z8ZncDtoKRutEK4iaaKEZbmGomkRh2sG+cBHJEAr50RZARBvUY2sJGX+oLJEZn/mplRowTDCKlGkib85bMZXdpwmdRaZEDEEm/kgMVfLqeRBhuXid1Fq4KcIQEbwUoyjiZnVGvKP65s0QIua6isRhTMjGOlGXmA84+aWnRszwMusBGzQosI7dE+ZkQv0wFyiW/UIYq5kb9US4gmRixgLeB4GqnA9YLtatRC2BLKi5G1ZmfL0bZrr+ct/kfgwo3fuBnxbHZ3P28UdN7qEa2R0X7fl/RFnA4OQK8chYp4AW1vZGrjD8idEXdmw+wRiHnP6tGJfuO6Ublmo92R5WtnztHmcHuu0cdMT/zRyGub/hHKcKvOEQtrRixjSGpzTYFIb4ftAUiRG095SLCTzADGpVJ9Qcce6RrxvSMhbHEC2IfE8MYTcP/RwOYGAD5JUOurgOIjRduwBY+HpCayAUxvUvXAul5KaCet7Z2EBRBzFAHQhZIT4E1UB8BA+Uyygr6dKgIgUF9JWsQmAvhvJRt4IJgYUDOWiMCCZrqAzlaSgQu9KAAGyAALZLIBmd3oABIo4UtEcDMfRYADmit8yQcg0AAEECA3C4DACGZoExdsQAIM2NYBDqAAByhngjF5gQg4cAFwRSACFtDAdTQ4mzKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJylYmJSAAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9pbfVq7zXrLLEsrLGssDatLrJt8XcvMLQvcnfv77Pw83hxcrWyNLizMvYzdblztLb09vn19rh2Nnh2t/p4OLo4OTs5eXq5env6Ort6+7x8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8v+yuLDAgQEBggwgKBBhZwlMEhgoOBAAgYOJHTImQIEhwsWIky4oIEDCZksIBAIAKC79+8ADP5AmHkCg4IC6NOrR+9gucwVICw8mE+//nwN11myyCAAvH/wAfzmkgsUHLDegesp4F5LL3gQgX0Q2mdBfiiNMMB/GII3wAgsYWAggiCqp8AJLIHwYIQo1mfBCidBwF2GMHo3HkouSBDijeodUEJKL3CQ4o/1RZBCSQ3EaKR3DZykAgM4NpneBie1cAGQVNInwkgQHKklAAiU5MJ5Toa5oEgvyFflmRR6lOWWWgoYkgsOhClnATuSqcGZeD4wpEcjvMjmkR+IRMGcch6ggkge5IlnBC14RMCfWwYQUgmEzulASCkomqcGHVUAKZtudsRkpXKS+NGUmuLJokYsXPiplv6SehQCqXMq8JEJqeZpwUZrvqrljBzFSWupHt2Zq6oaGeDrlgR05MKwc1LQ0QvH5ulBRiz4uayRKHDUAbSFdkRCtYtmlMG2W4aKkbDgOnnoRsaSW2WjFxWJ7pFdbpRAu2FCudEE8p555UUI3HukARzxGyYGHAV8JggYKWtwjANspILCTkqwUQsOV8kBRv1NDGOsGZ2AcZOXarRCx1RyepGrImMowEYmn3wjAxutzPKPF2D0aMwYVqzRszaHKK1G1O6c4rUEA41hsxsVbXTDSqPItEULOP3fAhx9KPV6Y2J0YtX2pUmRp1qDB2xG7H6dXp0axUs2fXtaxELa4HW70f63bqdnK0fjzk3frhn9jLfQGxHddwFHb5S04A9cfRHaeCfZ0b6Lh+ARwJCbwGrIWgfAgqyL4+wRroL3vBHlTlvuEZhfw92RmWTXrRHoMQsw+kc1S53yRzpX7TJHHzgdQAYi2Vj0AaaC5KPSEaza0bkxrw3SqBgfoPlIqHYcgecgFTwx1yS5gLnC/o70AucODxxS1vcusDtJKsAObvoktUA7ue6L1OurAbBe+bBHqwOEjSQv6F6uImA2kYwAd2wSAPJWMihaMUB2KElUri5gO5NAAIIxCuD8VnIC5RVqAy5wyQqetygRvKAl2gkhASAwQpeU0GsgOgADUPgeDowNRZoRuIALZVKBBRiAAAIQwAAMsAAa4iQEGGAAA/Z1AAUowAE8vIkJQHCBCwAsAhawgAaGOJsymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhMpSpXycpWOiUgACH5BAkEADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/soCwgICAwQIGGAAAQQWOF1soMBAwYEDChxI2OAC5wsRHi5YiBDBggYOIl7EZNFgQAAA4MP+iwdAoMFMFxgUFFjPvv16BhhmvgBh4YH9+/jtXwDRkvv38QCOt8BL6B3g3oEHxufSfBHk56CD/KX0wQABVjheABmwVEICCHboXgIhsJTCBA+WmN8EJpwEwX8WtggeBCptYKCHNLK3gUoiNGjijveJQBILC7goZHgGnOSCBDUmyZ4DJ73AAY9Q3qfBSEEOaWWRJSGp5JZMlvRklGBOCRIEVpYJQAUkbbDlmgV0QJIIYMb5AAkffcCimUJmGFIHbLJZgkgkyClnCh2xQCGeVgYQkgsc9rnlASG9QKKgYEbQEZmIlmneRxg4yqaCHoFAqZwRZsTCnZm6GAAKHrkwo6f+Sh6ggkcv6DhqlBG0oBGmqVoJY0edwrrmjR2JemucPmZEQK9lKtqResI+6lF9x1ZqKrNmsrrRCdGyOetGK1Qrp64X8YqtkL9qpGa3WxKrEZzigpmsRQiceyVHDrDLJUcaxBsmRgbYOyQB+OqrZJca9esvlGJadKjALTqrUaMG05gAR5MuvOMEGAkAsaocUVxxh5BulLHGJVp6EaofA8jRqyMjWLJGtqL8oMoOt2yhABxBGzOCCnBErc0PWoDRsjoHSPBGDPzcIQMcXUB0iRdgVGXS4yHAkZZOuycBR19OnR8HGFWANYCbahRs1+2BmpGxYuNXakUsnD2ethq5wLb+e99q9ELc+ZF7kcd2AzCARyJ3HXRHJ4ttdEYNFA5A2htRsPd6bmvkAeD2zW1R3Xav2urlstLKea4bRX425Ryt7XTmG8E9tecXsUC4zgEA9xGjXR/Q3EeSih2Bdhx9gDWaIYXQtZshmSA2nR6p/vGAI3FdMQUkha2xByEFDDECuo+Ub8Vfl6TwwmSL5P25BIRP0vjsOvC7+QtrQLxI0ve6gPslWd4tBvMzyebEBYL7jcR4qQoA8lSiPFgdgHkqcd6tIgA9lJhrSAFoAP9i1KcDMOcl8IpTBLLDEu7czkIDaADeCISBxAENA317yXwaVzQQCM4lH4CAAQjgsQDsxgCKDRgBTkqwAQcwgEMHSEByMHACnKRABBq4AIkiMAHrgGAFs8miFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAzlUwICACH5BAkDADsALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfkxPh05Nf1VakFtZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrYmYxY2MrJKbspKjzpSpzpmZtJqt0JujuJut15+y0qOrv6O34KWlvaW31au816yyxK3B6LKyxrLA2rS6ybXM8bfF3LzC0L3J37++z8PN4cXK1sjS4szL2M3W5c7S29Pb59fa4djZ4drf6eDi6ODk7OXl6uXp7+jq7evu8fHy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AHcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLbnvjAoQEBwQIOLCgwYUbOHOU4BDhAQMGDyhkKJGj7o0JBwBIn05deoIJM3OEeKCgu/fv3SP+hIj7XED18+exu9TOALx79+PbqiCAvn51AiNYwnDwvj94ByysdYF59hU43QUqldCefwx6VwJJOLSQAggffEDCCSvgoNMNDRjo4XQLnJRDBg2W6B0FIOnQwgcWtOjiiyC0gFOHH9YYYkkkmqgjih3RwOKLQL64wQs1XVDjkQB4QFIJOjapAAob6XBCkFQCScJMHiCJpAoioeCkkzBkpAMJVZbp4gc6wHQDfVrWKEBIOfD3pY4MYDSmmXha8AFME7SJpHoehTCnk/FVlEKeeYLg0g0E+vmhADN4lMOCg5rIQA0VxYAookSy1KejRyLYkaCVNvngRDr8uKmZGLQUHaj+bnrEXal0UtTCqoiusJILsCIZ6UYy0OokphKpimuZrapk5HQB9FqgqBoxKayOp0Jkw7GIapjSAtIVoIEIJkhQgLPn3agRBdPuKNGt2OIp47YAFACuCfSaMC65IHKEbrol8gjRlO2aeWVK9ElQb70d4DsdARzJyS+DDkhEZsBlKkowAPMebEKzCr+5kcMP91dnRMZSDGSyKJmncb0DKAyAxxpRGvJ7I0NUsskubqBSdBWsrIHLABzA0awzv/eARCDgTKXFKCUAwAAai2AA0AlwFEHR/UUg0aFKA3mCSjR6a4IIFSAANAANcJQj1uBlsG7XQOqa0qfMni0doBmRyvb+d4U+pAPcL9Kg0g12o/erRjnsDR6xEW0AuJ4ssVm4dEJ3BDLbR0/ELtxyqwTB5NPhrREHinfXd+NwY5DmSoSDDqmkpV+aKecu0W236BvpjfXpEit9wuosrVm4AMB9FCfbDDR30c24ognTCIUrGRILbENpJ/OIfqAtTDS6DAFJaz/MAUcT4woC8DFxq3DaJe3Lr9sdteA4ohi0gL5M6ju7QPHt80uB8h6RH6vsl5PP9WoC/DMJ6YQVAgCChAYtOAEINrABC63ABj2BnqMEID2VUK9SDLAeW5Z1JAH85iXSahIDmAOX50jOPgeYwOFeop3LGS0EjJOLCi6wgATQRwB+BODNBFyAExiUgAIR4A8DHJCcEMhgNlCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpgsTEAAACH5BAkDAEcALAAAAADIAMgAhykmZDExbTUzbTs6dUI/dUNFfk5Nf1VakFtZiGZuoWZzlWhmkW97m3VzmXeDoHiEtICLp4CNvYF/o4iTrY2MrJKbspSpzpmZtJqt0JujuJut15+y0qOrv6WlvaW31au816yyxK3B6LKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2M3W5c7S28/Z7NDd89Pb59Pb7NPf8tfa4djZ4drj8t7m8uDi6ODk7OLo8uXl6uXp7+jq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AI8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLvhukgwQEBgQIILCgAYUdOIeg4DABAgMGDipkAOGjbpALuQFIn04dAAIJwGMOIWFcgffv4BX+TODQ/G1tAtXTp5fwUriD8PDhc3C7woD6++k7sKQBIb5/+CisdYEA+BVIHQUqkcDAfwyCB0JaEhgo4XQIoMRBgxh+N8FZFEzooXUmgZDhiOKVdcGHHzZAEgkkkpjBRUXcoMIHHmwwggkzDCUDgSh6qF9IOizY4ogBTlSEChhYoOSSS6ogBFAI9PihANl9NMGQJDJQHkQwJMnkl0vC4JMIUqKo4kcvYNniiw8VYQKYcC45Ak9B2Ffmh1VuNER/apK4JUMqxCmoBR/sROadKXqUZp8uOgTDoIOKCZMRObhwQgkx2GDEEQsgOqVHFTCaZUNCeAlpnE+6BEQKJbTq6gn+OPDo6YQrcDSEkKJmSAOgpw46Z0s9uCqsqwfM6iF7Gy2aa4bzKVSEqb3CmapKRlw6rLAhDGCshBVuJOKyGW6o0A3RRspSDtdeG8G2BgrAUQbgZsjAQm+WG+evKsWQ7rAasFuguxuFGm+D8yo0gr1xesASq/u6GoK/BQax0XsDNziEQhsgDCcGLFnbcKsBQHxfnhfxWfF/fxrkgcZgbsCSCx+32q/I6nF05cn/LfQBy18qvJINMZfwAM3pEcDRhTjH58BCgfIsJ0vVfhxCAURX161GSCcdnrgJzeB0mMB+nEDV1SGrkbJaf9dsQkV8reS0K/GQ7gk1kF2dCLamHd7+CwzVyzO+LRlhA8wnsGADEZzaLZ0AEnMksN4MXLxQ2zxj8ANNmw7UgeIALOARCnp7V4GjPKugUxDo2V1rR0NQnPauDh2MsAlF7LQ52Wd+rjebD8ke7Qe185Q6zVSG5PrJWkrUQq8YqBA8TytUfYFINGhNAkU/+A3nB5cDdSjEZoeEdrxrVwSDCR9ssIEHI8AAN1ANQGxA4yPBOzAEkpMV4bYI0E9S1rmaQP5MZCwJ+K8kLMoVBwZolhUMr0cCmN5KaHC8FjHgem05EYoEgJ2XJDBL5IkLdCREAAnIYCbcwZADOKAD51ygAQhAz24MsAAKnPAm28nABN6DHAhUAARqLZyNEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIvUSEAAh+QQJBAA9ACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jO0tvT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gB7CBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/ZrA8ICAgMCBBBgAAGEFzhzbKDAQMGBAwkcSNgwo66NBrkBSJ9OHQCBBjNzYDBeoLv37wUY/mCI+zxA9fPnF7zUfgC8e/cU3IoQgL5+9QAZWKpI8L4/+AMmrAWBefYVOB12KW3Qnn8MejfeWTYsYOCE0xFwUg4UNKihdwycJSGFIFpYUoYbltghWRCAqCIAEJC0QYkwFrDBWBkQuCKF+YVkwoIxbhggWDYMcKOKAYSUgwI9wngARDCQcIEFF2gQAgw78dBQikOqiKBHLyYJ44ML0XDBA2SWSaYFN9gkQwkecOAmCjEgZIONWU4YAHAd5cCjlxoe0FxCO5wQgZmEkknCTDyg4Oaii46gg0FY1gnilht1yWeJYB6kQaGcPmBBTDG0yeioHMRJEAGSEukRA5cqmdAJ/p12esJLOohKKqMePCqQDamuiKdGObQa458F3TBorJzS4JKit5JawkCR9jphi5UKC+OMBo2JLKcTtBRDs82aioC0IBrAkQTWluiAQTRsGyuVKzEL7qgoCGQAuRQOwJED6W6ogEEkuNtpByyBMO+tAgmJr4FFboRkvw0uWdCmAhd6AUsH32olfQsbyBF/EDdokAUVF9qtSjxkTOqjdHaMHkd7hvzeyCUT+ulKtqrMQcIu2ycARw/L/F4C2dZs5sXx6uzmCAKh2jN6+m7EqtDv/VtQCEaXGQJL3yrNgkAfPl2diBqRSDV4JxLUbtYPrNBSCTqXYGUPFYh9HqUYfXC2/nuZDkSy0RPs0FLKGXuAw0C82k2dCBwFu/d3Khx0g9ERwNtSDQd7YOpAHCse9cePd2f1QQGXvDVMOIzQbAmHF9SA4tKp1xEGoRcQX0J/u2uB4DK1ALebHpTQwtwFJW53ACl45PjeB7iwUOmxRnAC7zXpoAPxCL1uN94b0b533wjRkLvNyhplQ+cuB2ADSDmALPQBOTy0QggXTGCBBSHQQL1RIohNLUgqOBu2whItfMlOJJaC2O3Gcq+FGWB9JOEXxBwQv7I0UFoEgGBJJGgtBlTQLNpL1QI0aBLvtYoCHzzLfOoUgArox309OsAH3FJAOzWAhCtJ4IYOgIEUsuU5lgor0AAa8Kv1bEdDCsAAseSSAggYgAD02c0ADNCA5N3EBRtwAAP4gxwFOAADzpuNGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKucSEAAh+QQJAwA+ACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG5zvG8wtC9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gB9CBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjyyYMogECAgMCCCBgAEGFnCowSGCg4EACBg4keLBbewCA59CjPzfwW2ZwBQWya9+e3cFyuDcW/gSQTl76gOotdVA4wL09dwXf2YovT988DJbr3et/T0PtDQP1BRhdACCkpIMD+yW43QEqoHWCcwJG+BwEJ72AnYIYZreBWTdAKKGE6Imkw4UZZhhfWP99qCIAJ4x0YIkwFvDCWAusqGIA94VEQYwwHtBfUjm4sMIIJqDQwg4PgWDjigaEpAKPMTqAVA8mcGDllVaOgCRDBCy5YosfMQBljDMuxAMPOMnQAZZsWinDQhl4uSIBH5EwZowMIMQDCxZE8MCfGqRAU5VtFjqCQl3KqWKOHIl5J4w/ElSDBn9WWqkFOMTkQqGccvDmQTcoumIDHenwaIwYFFSCn5a2+oCg/i7tsGanbXawZUEQiHpjRxuc2iNBOLDqaqs1uEQorW0eahCAun7IaEYI+lpipBcMO+wELeWAbKe3DiRAsx9SuFEC0pa4oQ8pWGttCSxtum2hLhgEbrgclWuuQNWq62oELKHwbqEmFATDvBIusBEN9mZIgUD6WoumSsf+e6WyA51AcIRNavRCwhhKiUPDwxar0ggSsxlCQUpeHCACGz3JcYIS+MADyK6KnNIKJWOJQkGhqlyfwRqZ+vJ+C89Ms6WZqtRCzle2wLPP9ZEa9ND7perDBEf/aQFLPTBtpQ0GjQc1eSFixB7V7X0nQtYPiNASyTlTTFCiY0dX4EaOor1d/oMCYU3zBA+v1HXJHeRwUAV1RzdARx7ovZ0CBMVwNAsvzVCy06AmDh3QGwnteAELE7R2wx/E5O+7JvSQ0LeaZ+ARuZ+TYFANfu9LuUzudtqBC6onFGfidHpkp+N5IiRC7Q9EIELgMuUQAqcjGM6Qh1Df7RGJVPOdEA4x1JA0Tja0YMIIIZjQgvQNWTx2xh9tjLaUYSEAdQBggiQB1QeUGRb14Abg+kjYK9cBZDeWG7BuXuJyEezsdS6ynOCAukogSV6wQF81sCw3oJuc/GegvN1pgGupkZwIUL+U7OhODNCfWk4gvxUJAAI3cMkL7hejBGxAB3BhYYQCQAAYymSGihg6AANuWJcMNIAABPhWAAbAGx/ehAQYYAADyHUABSCHiLPJoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMZVsCAgAh+QQJAwBDACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OClLiGmLyIk62InL+NjKyOoseSm7KUqc6ZmbSardCbo7ibrs6fstKhs9Gjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+ne5vLg4ujg5Ozi6PLl5erm6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCHCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy0bc4wQEAgMEBBBgwACEEzh/xPDgQEGCAwkkSPAQw+4JAwEASJ9OHUCABTdmxpBwoID37+AL/hzQoCPuiQHV06dHkN1lDAXh48e3UJ5tDwPq86dfwPKHBPkAxqfBWi4IoN+B1A3QQ0o2JBDgg+Ap8ANaH0SH4IUACODCSSt0B+GHBSRgg1kVYmhiAO2N1CGILB5QH1JCAIGRCxaaiOEAJNngIYsgKmAUDzSgMIIIRJYgAw8T9YCejTYSINIP8PHIowND8YACkVhmKQIKPkQEAZNgfhCSB1KWuUJQNQyp5Zoi0PDQDTWCiaEAIOmwY5kgJgDUC2z2KYIKDiEgJ5gbfGQBnmWS4FMNfvpZA0MuDApmAB7ZgGiZBxAUxAwpdABCCjPIxIOajbLZpUIUSApmihqFcGmZ/vXhwMEEtNbKAQ4wqVCqnyUshJ+qNlLQ0X+v8hjCECxUUOuytLLgEg+7NppDQj3ECSyClG70w53FQngADsoyyyyuLNEQrZ8vJHTCtUwuqFEM3UqZgbjiVtBSCef2OUJCG7BrI6sXkRAvjxHQK66zK+XrpxAIpeovhsC1OjCLDxjMbAorAaFwnzIeJOjDFxaq0aETf8iAxcuCsJIQG7PZsUELgHyhmBppUPKHDaBsK0str8nwQV/KfGDEGZF584MV6zyBCSzh23ORCX0g9IEAW7TC0Q8WrDQMLMnwNJHpIiT11Pm5m9HVWAN4gdIYBMEStF9Pi1APZKuHo7Zpy6cA/g5Kh9rSlT0DqtCSdU/HH0dR5v3dgCmgzLRLPjyNJKqFU0e0xIp/19wQO2BQr98veb2xDAzRXXmGHv2QuXd6EgRDpx2YwILbMvGZ7ws/+3o6BB8Rq7gHQJkbrQy5L2R63QKYzZHqiicwIVA+ON2nCnI/9DHZIhuquKJD5SADqSKM0AIPxTvUg4FTI/Ckg1hbkBQQPpQv0Q1TKziSDlhLSNa6IAewIUnwKtkBRlSWfvkrADQricAGdoAznYVG1xpA1Uaio24p4EVnUZKkrqO8k0DpUuN53lo+gL4m/a8lK2DflAj4FgoQ4EIBMMDlXhICB3zoABLYHF02sAACoC8AfwMwwAJO0MGZkEADDmDfARQgAQ3EQISziaIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKRZAgIAIfkECQQARAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRaXeZb3ubb32gdXOZd4OggIungX+jgpS4hpi8iJOtiJy/i5/DjYysjqLHj5u3kaXKkpuykp66lKnOmZm0mq3Qm6O4o6u/paW9pbfVrLLEsrLGtLrJtczxuc7xvMLQvNDxvcnfv77PwNTyw83hxNbyxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09vn09/y19rh2Nnh2uPy3uby4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AiQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8tm3APFBAIDBAQQYMDABBQ4gcgAAYHBAgQLLlwAIcMuCgUBAEifTl16gxszZXBAUKC79+/dO/7oiItiQPXz5xVgdymDAfj37zmMZ9vDAPr75xuwBHIBvv/3Haz1ggD4FUjdAD2kZMMC/zX4HQNAoBVCdAZWCEAAL5zEAncOdlgAAjYUNcQPOfAQBEcoUGhhhQGsN5IMHHrYIQLz/RREDSmMoOOOK9RwokUvqLhihQKQZEOMMna4wE9DzEDCjlBCOUNFPZg35JAEiASEe0kmCUFPP5gQ5Zg7kvDDRBNcqWYIIYHQ5Zss7ITDk2TWOQIOEd0gpJosgqQDkm/OqNMPdNpZJw8QNcCnmh581EGgb5aA0xBiGmonCT8ypOeiVwbg0Z+QdokATjVYaikMDmnAqZouaiRCqP5v6iAEDSqoQIMQMAVRqKl1ZqqQfasOOUFH/cGaJAYbZKCssiq8VCqvhtbAUA97BlugpxsBAaixDR6w7LcfuLQCtIaSwBAK1l6ZoEYycNulBd8u2+xKQ5Br6RALeZDukBlqVIK7SUoQ77I+rPSDvYaeqZCq+1oInKsAyxjBwMq2sBIPCNuZw0JpNlxhoxq5GXGHDlCcwQkr5ZBxnRsrpKjHBrKp0aMjO/iAySirFMTKZCKqUMcw4ydzRiLX/N/NFMdAL89j+npQCEEX2OpFLBjdIAUmF7xSpUyPkMK5UeM3tUXtWu0f1gPnvNIMXes45UI9hI3eABwBYTZ8CVD8Af6uLNXbts8LESB3dfpxBMHd4FUQ7wYn8N0SDF2j2pC+g0/38Eb/Iu6dDELEcMIJLewQ0xC72oupQ3FXfqFHdmv+YU4YZ0xCyw4poPqwHnHgOgg6xU4uCXhClLrcAqzbUet3LxDhoKVfSjtEtssN8ke63y0pT0NAXq4LTj/UA4FRG6Alg1ZfANQPNXC9YwozKFzRDVEjOJIOVkM4VBA85JDDD91XhK7HGCpJ2SIGIrNAbV8BGBpJqgYwBMTpLEGy1gDGNpIjcYsBNTpLlVZlAOOhZEuwusDy1hIC8GGpXy1hAfm8FCK4aEBwBgqAAS4HExEczkEIuEBz6hKCBhAAfIABGIABGoACD9KEBR2AAPkQwIALdEAGI5yNFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJsUSEAAh+QQJAwBCACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFpd5lsepxve5tvfaBxgKR0hKd1c5l3g6B9jrGAi6eAkbWBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKjq7+lpb2lt9Wrt86sssSyssa0usm1zPG4wdi5zvG8wtC80PG9yd+/vs/A1PLDzeHE1vLFytbI2PLMy9jM2vLO0tvQ3fPT2+fW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erm6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCFCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy4bM44IEAgMCBBBgoMGFGjh9hNgwAUKCBA4qZAiRoy4PCrkBSJ9OHQABCjN9fDBeoLv37wUm/nyI+zxA9fPnJbzUngC8e/cb3KIQgL5+9QAgWMJw8L4/+AQsrHWBefYVOB12KYXQnn8MejeeUD/0gMMNO/zwEQ8SGKjhdASc5MMGDYbo3QQ/7RCDCSKkqKIIMezAUYYbxthhSSCKaCOJO+2Qwoo8qpiCixhdEOOQAFxAUgg2JllACDkFEUOPUKoYw0UgEEjkhvmFxMKCSooYoE1BtBDlmCKkEARFPAxw5ZABhOQDBF0mmQCYYpI55goUCbnmkAh6hGScST4405N2ktmCRDxYuaeGAQDXkQ9cAhpiAs3JtEOhheIQkZ6LxtjnRn9KaqOgMK2AqZ0mRERAp2x6NIGo/nLKhMOphd7wEA+sEumoRj7AquQINsBEKK1jvvAQp7lqaCSoviYZgQUd6OASisSOScJDDSQbowEcZdCsjQxYIG6wK/1QrZ09OGSAthsOwFEF34qogLgWYMDSpeeOCSRDarJrYJsbwRlvgwfQawG5Kd2Q75gzOESfvwZyxN/ADRqswkqzLgylrQ0pCjF6HEVK8XsGn7ASvhrzqGlD/X6MngAcCTzyewgYLMNKQaTcY7oNreoyeu5u9OrM780rrgZAsEStziKQcGZDMP5c3Ywa1Ug0eAvQezNLw+p8qEMcSH3epxiVcLV7D9S79b1Mp7hyQ7iKTR0KHPV69nce2JD0/ks76mwmRA/LHbTEd3cHwUw9ML2vQxTILZ16HX1QeAHxzfRCylNGFLfYAbjgkd1nJ0BDTabm+7dEjYtN9kaSn02qTEGUTuwKFk7EQ+AfB8ADSD5MPHMCPuDUdaExPE0RClIvCxIMVzOZ0w5Lj5nC2xYhyy7kIoU6cOU74XB5jySscIPxGK3rrwG7kwTvwBUE/5OEN8ywQw/kb2R+sgSkX9L6zU7gvlmpY5UE9GeS1sFqA/87y3wWFQAO6Md3XUpACdxiPUZRgIAr0Z6IEvCBBLLlOS2zzwAosKv1bCdEEPhApebiggsYgAD02c0ADEABz92EBiGowAT4gxwIVOADaqObjRCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCLDEhAAIfkECQMAOAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRaneZbnydb3ubdXOZd4OgfommgIungX+jiJOtjYyskpuymZm0m6O4o6u/paW9rLLEsrLGsr7WssDXtLrJtczxuc7xvMLQvNDxv77PwNTyxNbyxcrWyNLiyNjyzMvYzNryzdblztLb0N3z09/y1uHx19rh2Nnh2uPy3uby4OLo4ujy5eXq5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AcQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8ueLOOCAwIDBAQQYEABhBY4aYig8KDBggQLIkywAKOuDAkDAgCYTr06gAEOgMekoaFBggLgw/6LL9CAQvO3tQVYX7/ewUvhC8bLl0/BbYkB7POvl8CSRYP5AMqnQVA3zDBDDDWUdIF0+jVInXspifBdgBSGVx9PMagAwgYcduhBCjGABIGDJFI3gAwnWVDhiuE1QENOMWzY4YwzguBCRxKUqON1JmnA4o/k3XQDCTQWSWMIN2hUwY47GkBSB0ACGUFNM3hg5JUezoBRCQwyWWIFIrEwYZQsdjDTDBxgqeYGHGhpEQFeMqndRw+QGeV5L90g45pYepAkRRfEyaQCIIlgZ5QTxEQkn2uGQJEM+Am6Ywke0fDfoUCy8NILjDL6wkSBSrojoR0ZiimQiboUQqd8ejCRAf6i7hiARxGcCmQCLsXAqqcRyaBerDqCuREN8dn6o5ksqbArnyZEFCqwJZKqkanGspjqSlYuqyYHEeUIbYkDcORjtSwy0JK2fP7pkALfljjrRhOQy+IBLM2A7ppuOgRruyRyVKu8K46wkq73YhniQ3Dy6yCKGtUJcIUZDFwwlp8+FKnC+s2J0aUPB4jBSjVMfOXB+mLcoL8dU3jCSjeIbGSCDzlgcn4CcERBygAiYANL2brMIbcQjTjzeuFupCLO8pnLUgo+d9gsRM8OTZ20GVGLdHjXqsRp0xtU/JAMXUoNwAUc0TDm1QWI4FLPIvspEbtiAxAAw/CiDV4CL7a0tf7LN0q0ZNxOdgSl3VO+tKrIjlL0q9QffFTs1SjAdAPb2noAM6hiB+6R1SkXDlPI93LgNUUXYxwApSBx3HECmspUpbaibzk0hCCxgPSFM+m5awiXXxR1u1QXmnLWNLlAuZEerKBuRnC3e2JJ8QLsok4vmJAmjRyQ8MLyG8n87QAaj3QzuQ3guVMNMbzwwgw1cO+Rt7E6QHeP1VKQN1olLO5lAPytxMLjZErAgNoCPx0FQAHhU8m4bjUB8xGwdPkJgANQF5PurCgBFGjdXJ6jAAKoJwABGIABIFCC+c2EOxN4QHwSkIAGRMACLLjfbGZIwxra8IY4zKEOd8jDHvrwh1ZADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73uJWAAAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy7b84QECAgMCBBhgAMGDFThLVIDAQMGBAwocQKjQou6KBgMASJ9OXbqBBjNbTFBQoLv3790d/kyIu2JBgOroqweQ8LJFhAPg44M/gEHoixcqJQhIz7/6AA0sYZCAfASCp0AIO71AAgcZXGDBgxl4QAJ+Iy3Q34XVPaBSBAV2CF4FOKnggYMPlmiiBRy4ANIKBmDo4nQLnNSCAx7W6F0ENb0gAokn9vggCBRy1OKLRCJgEo02JgnBTCxk4OOTJV7AAkcWEmkleyNxmOSW9cGEAo9QQqmCRg9YaSYAAIZUwZZsFoCgSyyAGSaUU160wnlnEilASC3A12aSCbj0gpNzFnqBihY1kKeZWHo0wZ9sdrkSCIVWaoEHFpmA56IvBvBRCn5CauMBLKlgqaV1TqQop1Zq2NGj/qJuCaJKHJxa6QYV7ccqkQZ4NGCsSTqgkgu2WopoRB/saiZwG5UALJvNoURCsZWSoKqyV3IE67M2SmqSB9QWiqlEQ2LrYowbIcltjTiiRGi4UF4wUXTmutjrRtytW6OwKMFbaJAP6VrvhQRw9Ku+HTKAErH+hnnsQwO76OlGCNdI6kkvNOxwRCtEjOGeGrVQsYeB9qvxkwA7tKnH6A3AUagjx6dASnKebEEG87LMn5H4xkzgkijVarOJuEpEgM7poasRAz7L1+5JJwxt4ggTVYk0dY1mpGXT33lbUsZSP/jwQyZcXZ0JHKXANXgpqLRB2BzkarZ0BXd0MNcKq8RC/thjUmT11dh1tDXX460Ers3jUtSx2QEwy5HIax8QrUov1EytlBchYHbgHkGwduEs7d3wBShgtLjOAjjeEeQ+JzB5qf5ecIJGEugcAAUiYeDzAR3EFOflpW+kuceuiuT5yLPG9AKlp6bo0dEDKz0S0wg/zaTQYXLQt0crQI/tAqqL1AL13EbwOk0uLAjmBRyMkGpIfy8aQPEnDQ7pAcnz5IILKZNUJqe3Y8maRMU7t8RPYr95if0sxhy4mKABAuvPABoQvpakYAJ3I5ACJnA+uGigAQQgwH4CIADeNOADOAnBBBjAgAEdIAHJmUAJZkPDGtrwhjjMoQ53yMMe+vCHV0AMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIR64EBAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy8a8QkMDAwMEBBBgwEADDThbhJjgQEGCAwkcOJgQwq4GBAEASJ9OXToC4DJDQDhQoLv3790h/jSHq2FA9fPnDZh4GUIB+PfvHaTYyWKECA4bNngAMYIFyhUIoCfgeQuw1AIE8CX4XgQ3vTDCBhZEKOGEF4jgAkkfCDDghtQNsB5KJSSg4IjfKTCfTC+IcMGELLYogkgaRMfhjAAE8MFJIXBH4o4FHFBCTCxk0OKQLGbgn0cfyEjjjDaWVIKOPO7o40sqrEjklRKi0JEJGi65pAArjJSCiFFGmUALLVWJ5ZoRaqnRCgR4KScBIrXAQJl4MsASC1ayieUFR2LUgJyEShDSBHgmioFKL0DoJ5sXZLSCkoTSGABILUCZKI8HqDTCo4+OIGilhDbwEaKb4jkBSi/0Ceqf/hdWNCmpcl7aUaap4tnpSZ++6icIFlFAK6EUdNRBrol2cJKQvkJqkQHDyolARw4giycEJrHQ7KOxSjRrtEvaqhGu1ka5K0kkbOunqBN9AK6cYWpUQrl4okkSCOqy6QFFGrzr5Y0ahUBvmT+S5EG+a3JAkQT+LoldRhgMHOV4IzmKMJGRTvRAwzQ+sFEFEvNYQUkWX9xiBhQNyjGHhmqEasgjLkoSByYTqfBEDK+8YbEaRQzziMqSJELNQwI7kbA6D/gwRsf+rCDFIvVK9IQvtpv0gB9mNK/TCZ440gtTs+imt1ejJ25GLXAN37kkMRt2xhQFWDZ1BXKEoNrfMWjS/tBhW2D0wnNTx/NGPuPdXdAlgf12oBOtELh0AcS7UdqG92ivSVLXzK5Fcs9tqkd3470qq25fnMELGDk+d+QfUa72AZef5ELNF6igkcpXf36q6CypYPIJG61gXtIDSO5RC+45rUDsKfmu7gXAc2QCpf6CKaamA5/5UpDNXjA2R/1yHMDSIQkc8gFQu5TuoxeMgDpI4b8bwOAkmU/vAYjHlKKrLV7gQbchyRC4PJSSEJXLRDhBwQhAwIEMZEA/ImDB+0oCoGERwHgnORCyGMA8tGigS14iAMBaEgIylYkBBXuLBOLEoQAQgHwuwcCdSHQABqQvLhRYAAG6FIABGOA6eBicSQciwAAyHUABDhBPB2fDxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyakEBAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy96soQECAgMCCCBgYAGFnCEmQGCg4EACBg4idLCrYYEAANCjS4duQMLMEBESFNjOvft2Bxh4/rpggYLEiBMqXrT8sGC6e/cDrLssEcG7ffsKwttUMWKDhf8A/reBCCqgtMICAbyn4HQGrLBSCxEccN+E3jnQwkwqcBDghhtuwEJJKxiw4IjSBfBBSi04QOGK3R1QAkwviHABhzQGCIJ6IX3wHIk8QvebSSVox+KQ2y3XEgsZ1KgkgBl8+JEJCfYopQYlpSAhkViGwBILMy7ppQUFdrQCAVKWaeJILTCA5ZouqsTll19e4ORGCJRppwAOhgTBmnwmcOFJL/gHZ5w4ZkSBnYgiEFIHfDYKAUogDDqoBxsNgCiiJoCkQKONpmCSCpJKOqdFD1yKqAEfVcBpow6YpGGo/nBukNGOppaZaUdCrrqmpyOxAKuoF31QK6IPdFSCro1WQNIIvw4KwkUNDGsnAR1NgCyfDJCUZLNxXkSmtLZypOa1u4rkK7dwumARuHZSuRG5fGoZEqjofhnmRCawW2axGqUA75rKhnRCvV+SUJGw+vbYwEbH/kvkBCKRQLCXI1SkQcI9KqpRCA4T+WhIzE6spAgVHYoxiQtsxGjHLEYgEgoiK3nCwSejzDDLLYtEb8wcolDRCjWPKF9GLeC8on4gvcAzjaNGBHTQCv5ItNEUGhnStkv/d8FFtEIt3a0a5Up1d7yGJELWAJJsUXteSzdAR/WN3Z0CJO2ctc8WPd02/nQLc1S03NxBTJKgS29QaEVdew32RmKPXba5aN9rkQR7A4CqRxgAXkCrJnmwNKUaJX7ymR81znKbJr2ANcEXqKuRyVCnDNLKVLuM0rms473RtzXjKdK4OPupkt3NXjCzmJaO7m5ILWx6urxurh7qBbp3BOXJUotkJctWr/RCpLBy4DpIH0QJbgD8llTCleQeEPBLKkivJAfVh7RC8sMGkH1JzbffPUwokFGNLgCC9KCkTrUiwOJQsiddMeBxNCHPCUYgAhKogAWHQ8kHEFimAexPJSVo4JoU8D+2aICDCwoAAR6QJ5iEQIQUOgADKvCnuVBgAQQgQIICMAADVKeFeDXpQAQYwAAJHUABDgBPDWfDxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJSaoEBAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy+a8QkMDAwMGBBBAwMACDThbhJjgQIGCAwkYOIgQAuiLmhoQBABAvbp16ggkzAwB4UCB7+DD/n+HgAHnCxIcNmS4YCEDBw8kXLTUYOC6ffsETLwM4UC8f/8MpDDTCyhswJ4FCCaoIAcnpLTCAtPdJ6F1CLDUQgTe/adheBDEpMIGCoYoogUbNFiSCQRMqKJ1A+iHUgoMbChjeAoIyNILIIyoY4gekKRBhCsGGQBwJoWQ4YxIHtCcSi94sOOTCW4gH0gfABnklR+UVMKRSHZZQkovZADlmBZcMGVHKwxw5ZrUBbDCSC0o0OWc3x3QwklNkknmBh6tUB+bbA4gUgv90UmnAieJoKeeHHT0AKCQagdSBYZWWh5JKCy6KAobrSAApIC6+VELCVRqqJ0kcaCpnhds1ACo/pAu8NEEplYawUgnrLqoiRetYCWsQr7JUQtc1prknSGpqiuZrWJEAbCRdtSBsZaGxMKyi55Z0QLQAlohRxFQa2iHIJGArZ4kYPRpt2sG0FGp4s55QEhOnjtmjxaZwC6gWWqUQryGfvkRiPZC2WxFH+zLJpEZlQAwnUt6JGbBTx5MkQQKr/nARhg8PGcFIFE85nMVPZpxkBtrRKnHSILs0QsiQ6mtRCafrGIDG63MsowTgHRgzCNe9KzNKkqa0bQ7y3ipRwQDHWIGF2lAtIoUbBRC0jJ2AJKyTi/Y69QTuphRC1hvaKNH5nat4Ajqgm2fqBvBW3Z4qH4Es9oJsoAR/gJuX2dARxDMLZ4DIk2sNp/O9m1d1RwhLfh3WoeUK95sY7SC4m2KrRHZjxdwwNkgGQ50BiRj9KrisnpE6+O3jpRp17xavi7YcHdEquB1j1RvzCB0hLHbOIPU8dw9l/RC0wVvUPpGak49gLAgyYm1Asga//O5F+jd5+wZB6D5qHJ7/HlKLiCvq5QhJXxyAIyP5DDLB0Su0u6rcrD8R1IrzP5JVz8cv0skEN2YLkCC+1GpedAaQL9OUgLpUUsBAntJAMeUgQKiZAV8AxYBoIeSFgTOWAyonofQY7gLuGcE2lvJBxC4JgIwjCUNrBQDImYeA7qEAn9SUQAIkLKYdKBQjjI6AANcRhcKLIAAugFAAAZgAARQgIM06UAEGHAczynAARDogAhnw8UuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScqoBAQAIfkECQMALwAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuymZm0m6O4o6u/paW9rLLEsrLGtLrJtczxuc7xvMLQvNDxv77PwNTyxNbyxcrWyNjyzMvYzNryztLb0N3z09/y1uHx19rh2Nnh2uPy3uby4OLo4ujy5eXq5ury6Ort7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AXwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8v2vILCAgIDBAQQYABBgw84W3SIwEBBggMJHECYUEKnCxYoSIwYcUKFCpkrJAwIAKC79+8ABP4sMCGzBQYFBwqoX8++QIIIKWi6UAHigoX7+PF7OOFSwwDwAAK4wAovhaBAewgiGEELMLlwQgb5RRjhBfyl9MF/AWYI3gMslXBggiC2V4FLKmwg4YkRboDCSRpwp+GL3hmgUgjphWjjeg6wRIJ9KPaI3wglPeAijEQOQKBJFdR445IKMHiSCyD4KCV+HIz0AJFYejcAkkt2uZ4CKEU55ZhVgkTBkFkSSQBJHSjp5ZIMmETCmHRaQMJHH6CZJpEchlSCm28uOeJIKPBY55QrdkTAnoySBxIDgUYaX0gumHjomBd0RAGjjMr4UQeRRppjSHNeSmeFGi3K6Z7AeQRpqP6BNveRCxCaiulGm666p6ccgQproKN6hIKtdaJ60QK67hnAkRtF8GugBzjZkQfE0umBRgIku6cEHSXwbKAYfFRrtVJmitEH2u6JAEclfBsoBB6xQC6d1110ZbpZBsBRBe6+eYBHJ8w75p0XNYBvvhxN0K+X/3ZUqsA+EmwRAgdnySxGECzspbQajQCxlEBeZEDFWLaakQMadynrRiJ87KMIGKlK8ouOZvRqyjZOutHDLksYskXIzkxzszjfqLNGw/Z8orEUBS10hhw5W3SIHqmg9ImJWmTw0wEKkPDUICbwkaFXW3CBCxhpwHWAvGYUAtgJBsuRmGVbcG1G2a79Hf4F3cLdXgcfJV030xVRrHd3AdSsUcZ+q3fA0RvRWvfZGuV6eNsa+dq43B0FXPbPeB8OgAYfedt4CCKN63IGaON6OUiag8151VcTjhGGXJMO0odgoz5Syy7D7JEJay8gUgpwR2AStRCD0LpHaguNwMUfvV00BByPxDy5HDz/0b0VG1kSvyk3mZLH1YrgPUhOpzuA4iNJ3a8CkJukgup0UniS5ckSQD2b/WJA9lLCs3KRYH0k+UDeOBWAB/yvJCUwXagOUIEBruQE25PQBThwwJY8YIFYCoAB4KeSCkiwSwdwQP1ggoITTEcEJKgOAlnyAJlpKAAIMBlMKnCzEB0AAosrk8sKHoAAAmQrAAEYgAF+80CYtKACEGCAtw5wAAU4gDkWnI0Wt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUomRIQACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspmZtJujuKOrv6WlvayyxLKyxrS6ybXM8bnO8bzC0LzQ8b++z8DU8sTW8sXK1sjY8szL2Mza8s7S29Dd89Pf8tbh8dfa4djZ4drj8t7m8uDi6OLo8uXl6ubq8ujq7ert8+3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/rxCQwMEBAYEEEDAwAIKOFuEmACBgYIDCRg4iNAB5wsUIjhsuHBhgwcQJ17I1LBAAIDv4MP+fzcgYWaICAkKqF/PXr0DDDKfT7dAv759Cx5OuPyAQLx//wNo8FIJELRnoIEKhPCSChvc5+B9G+iX0goLBPDfheIZsMJKLURwwIEgtudACyu94MGDKELowkkrGIDhi+EF8EFKLTgQ4o3sHVBCSixkkOKP9V3AQkkfeAfjkd8BZ1IJ6eHopHrNmXTCBUBWSZ+EIZlgIZJcCkhSCh8+KaaCJLFApZVWDgnSCgRw6aaMI7XAgJh06jjSCz6iaeUFK37koptuCjCSjXTSmcBIJ+qJ5gYfUQDoowiE1EGhlEIQEgqKKoqlRisM8OijJnzUggKUUprCRy80mCmaF3T0wKf+kH5UQamVfnTCqppyZCSsbobaUZO00nlqR4niamUGG33A66MPdFRCsJRW0NELZxprZZ8YNbAsoAZ0NAG0hTrQEabWokmCRp5u6+aGG5EKLp0kbgRCuWh6oJG6gHqp0buFkqlRsfT+yEFGJuDrZrMapcAvndJupGrAP7aKkbIGI4lwRs8u/GTDGuUJcYoSXyRBxUg2sBEGGj85AUfVfoxiRo6SDOMCG02aMo4RcPSwyw4ii5EGMsNoskYh3IzjyhtxwDOKjGK0QtAvlqdRC0bfCN9GIiz9IAgZPQ31hUpmRHXVIEap0a1a3zeCRrt+HZ6vGgFLNnvDavRC2vdhe9H+Am6LR0BHEczdHgMe7ax10wT3Hd7QGyks+HpIc4Q23mtv1LbbcG8k99x1c+Tx0hlot9HIinfrEcqPi/sRuVqf29HlMsP50eY32wmS4RBvIDpHMX9NM0g2k51zSNTyLCRIbUItALsgzVl1AvGGpILLF6AQUqdBB6AvSKMafYC/Ik0f8AWbfqSlzGGLBObNZpfZ8qrVE7mlugFIXVIJYb57wNUnpWrsBnobCfa2FYD0laR74DpA+1BCgs9V6QIk2N1J+sMrAmQOJQUKFgM6txIS4M5BHIhgS/jzqAEYUCUEopQCFuiSF5AABBzw0QUyYJ0RqAkmGqDgiwJAgAcw7yWOIcjgjQ7AgApETy4UWEBuLBSAARiAPD+kSQciYJwPHUABDnjPEWfDxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJSqcEBAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjyxb7ggUKFS50fmiAgMCAAAIIGEAgIWeJCRAYKDiQgIEDCBhsviCx4YKF69gteCCRO+buAQDC/osfH95AcZnHFRRYz779egfRYarYkL1+fQ4nXK5YEIC8f/IDnNdSCxEc4N6B7ikQ30oqeGDfg/d1l9ID/f1n4XgDfMBSBQYi6GF7CpSgEgnWQWgidiSgtAICF7Y4XgAapNQCBB/W2N4BIZz0gggn9oidByatYICLRIr3wEktOGDjkuxVUNILDvoo5QYkrQBekVgKKFIL6jHp5YIh8SjlmECKNCSWaMY4kpJetpljSCSMKacF+YH0AJp4BmCCSBW06ecBKYCkQolzSsnCRx9UiCeWBoRUQod+eukASBwUKieVHhGw6KIafsRApJGK2BEKls6JQkcabLroAB+FAGqk/gp4VGmpY2bQ0ZmqpukRm6+6yRELtM4pIUYrKJprkQh01AKkvTIJAUdxBjtmihpRcGyeHXXQ7J8cRSmtj5hmhOu1Re65Ea/bMhmoRhl8O+ZGApCL5pEbJZBum05m5IK7Y76gkbHyttgAR8zeW+MEGgHLr4+HYmRCwIxulILBkmqkwsI+norRBxAX2ahGJVDM5KQZkYrxiRpf9HDHLhIgschLMqDRviebqEJGK7DsYrIatQCzjc9m9ELNJg5rEcA6j7cAwT9/GMFGhBKdHbxJW6glRvY2jSCYF80qNXbhYsRi1f6pqRGNWh/4ZkYnfJ3dCBtJQDZ5AXSEQdruHcDR/tBuX9cwznMrrSze7T3NUbtuhy1u4OFRsCvh63Uwat91bpRq4C575CrhMntEn9QcfDRu0mZ3hG7Ta3NEM9E3e7Ry1R9/NHHaJH8U7ckihLRA1QF0ClIEWh8gKkje8gvCSJrq7PhIn/4s+Uifu7uBvyJZyfLAJHEJM8IlFU+rB9SPZMKV8mJfUgpd3su9SSNICzdKo28awNUlnQ7qAVyXxILXcnLwN0q7y9UASocS4PVKAalLCQoQ5yP8uMQEY0NTAB6wApekAG1tOkAFWgATFpCAf9nhwAj+55IP8IdIBKAgegoUsw3axAUquA0LXBC+mlCgAQQgQLwCMIABGOABe+aySQcmwAAG2OsAClCAAyqwrtk48YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8ikBAQAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjywb7ggUKEiJGjDihIqeGBggIDAgggIABBBJyhpgAgYGCAwkYOICAgeYLFBwuWNjOnbsHEi9k/v4eAKC8+fPlDSSXuVxBgffw4793UN3lCxHau+vXL8KFyxULBIDegOgNsF5LLURwgHwMyqdAfSndl99+FHYnAksBEqhhgSawpGCDIDqYAkoveFDhifptEN5JKxiw4YvnBaBBSi04EOKN8R0QgkksZIDij9xdwIJJH5AH45HlPXBSCe7h6OR7FZDEwoRAAjnkSCsYiSSSFJTUQpNPPtmBSC9sUOWZFlywIkgtbukmAB+MVGOYdBZQAkgloonmBiIt8KabAXQYUgR10nnAiB6NoKeeI4CkwZ9vIhBSCIXWCYFHL1C56I8X+OcRAZC+GedHDFRa550cKbopmiB4REGo/m8S8FEHptbJAEeZrqqnpxuBCqubgnJUaq10IprRCbrqSQJHJvz6ZgMdpUBsnRNsZGKyZ3rA0QPOAtpRBdMaqlGu2J65JkYudrtlsBnZGG6YxlrEQrlo9qaRAOpueWBGCbwbJoQWIUtvlctqlO+WSm7kb5hRYkTCwARr1OzBR0KrkbQLO1ktRqpCjGKrGT1KMYwGbERpxjg6kJEIHv94YcgjwyipRiejHOKlHLeM4ssYrRDziwts1ILNN0aQEQo6n3iCRj7/rKHFGQ1NNIgbX6RC0hWisJGATg+4L0YLTs0gwBW9gDWFvGLka9fnjarRsGLHhypGIJzdHQccScD2/nkDdIRB3PEpsJHAdlvQ6EZN7w1A0BxJDXgBRo9b+HZpZ4Sv4jN21O/jO25Ut90gc/Tq3rJ6RCvgt+Jqt5Afael05h6BOXXnqZ59uEcfsF0ySCXErfJHZuqsYkgIOB2A2x9BMPUBc3f0go8edzqS6/kG0OVIsvt7wJghzQvxBVpjefnBCcu5+cINizQlveATOb6zAZRPUgnnT3tA+iOVie0GlWO5NqzWoxHcarW9lDxsUxcYwblO4idYEYBdKCFUrRgQr5O84HNVuoAH+oeSDxTvTQJ4wApcUgLl1SkBFWiBfUjAgRNdgAMjuBJMPHikABBAhDIpoZMOwIAU0gQFjicYAQhyQwIVLHAmFFgAAQiArwAMwDg4vEkHIsAABvTrAAqQjg9nw8UuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScqnBAQAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuymZm0m6O4o6u/paW9rLLEsrLGtLrJtczxuc7xvMLQvNDxv77PwNTyxNbyxcrWyNjyzMvYzNryztLb0N3z09/y1uHx19rh2Nnh2uPy3uby4OLo4ujy5eXq5ury6Ort6u3z7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sGq4LECBEcPIAYMUIFzhUaGhAYICCAAAMGGmjA2SLEBAYKEhxI4MDBhBAzVYzYYKG79+8XRP74lqnBQAAA6NOrBxBggYmZIRwcKEC/vv0CByKkcKmC+/f/AG6AwksaDLDegQci8J5LIShw34MPQrBfSi54AOCFF3rgwkorGIDghwcuwFILDkBo4oMRoKRCBhi2+N8F4530gQAg1qjeACukVEICJ/ZonwItlETCBS4W+R0JJ1Fwno1MAiDAByd1MJ+PVBaQQAkjkWDklt6dUJKSTYYZwIIjSVnlmQdM+BELRHLJZYwgfbBkmE0OQFIJU55ZpQIgudCmm1tewEJIKxhIJ50EiNSCg3rqycBHFgLqJgchNXDopRSENEGjnHbQkQqSSjqgRybMeWmTAoCUQp6cVplAR/4chApoBh8hcOqlEnwEQaucYrARCrJK6iVHH9x6aQAelcArpwdsNEKwgILQ0QPGXkqmRhUsy6maF7EILZcXdORhtXQ+0FGJ2upZQUYsfAsonBetYCq5NiK7UQusputjsxhp6S6XI2ykAb2H5qhRCPo2GuRFz/67pbQaSUAwnddehEHCenJLEQgOb+nBRtRO3ORy2GJ8JnYXRdqxixtsZKvITOaq0a4mU+nrRbGuzPJGC8DMZKYaRVAzlZ5eJILOLlKqkaU+10hyRpsO3SPKFh2NNIYibERB0zVWbFEHUveo8UQNXw1g1hptzfWHBmcEdtgmLmwRqGYDOGpGK6yNoP6d98INIZ8ZeVt3dxe8wJGheqcnIkeM+l1fihlxPHh3H3MUcuLoPV2y4/VRfRGwk1twt0Z5Y+6kRy1wTt+rGwludgaGi2t6Ax+h6/gEHNFd97Adla63AG1zlLrjCcitUc5XK11r4jLr6vjNHLlgNoyE0sg1AoryGDYEIOm+8ugfmcA1jiOlEDaQIZ2gM9ojDQxzAFCShHDNB2ApUtnuBmySxBMHAHRJF8PYAYo2Eu/J6gK8M4mc6DUAr40ET/pSwNhCwgL/hIoDg1JJoaplgOCdZFHacoDxTIIC1xlpA0hyCQWsh6j4taQD2nOU/ViCAsm16AIcOEHsYPIAAjApAKEG0NxLKsAAKh3AAZ57CQpGAAIOcGADuhHPDmsigQUQwHoBGIABFqABD84EAxFggPYOoAAHRCAEI5yNGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOgDKUoR0nKUprylKhMpSpXycpWuvKVsIylLJUSEAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/76AoUIEBw2ZPDgYcSJnB8aICAwIIAAAgYQSMhZYgIEBgoOJGDgAAIGmS9IbLhgobv37xY8/pCYaaLBAADo06tHb2C5zBQTFBSYT7/+fAfXWdbOAL4/+Au/ubTCAgGsZ+B6ArjXUgsRHGDfg/YlkB9KKmzg34XgbaACSw8UeOCH6g3wAUsVOAjhifUpUMJJJHCH4YvejYfSCgiAaON6FKTUAgQo9mhfByWJAOOQ3olw0goG3Khkeg2c1IIDPkZJ3wQjCUnklSCUtAIBS3YJgIIitcCAlGQWMKFHJFyppgUBilSjl13mOBKPZZIJpEcsuLgmkRuG9ACcXgYwYkgV1FnmASt2ZOGeV14QkgkeArrkACGlYKKhUirQUZqMqtlmR29KGidIdGJq50Yv8Ndpox9pICqc/gJ8FIKpdSawEaerXikjR0m+6iWYGkFJa5lnWuRBrmpu0NEKkfqqJAIdtXDpsFFCkNELeiI7pAscUeBsoB11QO2hGaGgrZq7ZtTrt0tqwJGw40oZAkZWnjtklhsJwG6XD3CUQLxkVoDRsfYO6QFH+u6r5AL+AixlBAMXPKSyGym8pAEcOSylAxipKjGGjmpkgsVKErBRChpHyQBGi358YQYbrUDyjZRq1ELKPmp6EQcuY0hxRjLPDCK0NuPco7UXgdDzhRxw1KzQ6jG80bRG1wfxRfUu/R2+Gp0HtYFNbiRf1Q9SedEJWvc3Akehfp2enBqVSjZ9d1r0QtrgcbuR/rdupxfAChyJOzd9B7SQEc94W/CzRkH3DQDGHN08+H0a4Zr22qA6DgDcG8lNdt0XpYr3BS949IHjAgDeUQmTJ2C4RmhfDtK6UAO7EbxVF9ux1hmU/tHIX5sMEspkr9yRClqjINICUAfgbkgRVH3AvB7F/rGRI9GusO0e4e6w7hspLTH2I63gtcJSj9TC2A5fHVLWyILgO0nmo+8k++O6L5Llq6ZrkvaSCgD3RuI9TB0AfCBRgcf2dAHlraQBvhoA51AygWEpAHQm4d+QLkCC+a2ERoAKwANUx5IdGeoAFXjdSrQzJA6MwIMu+QCBSjZCmZSgQSpLYUyygxsLXWADjR4AwQhYgBMNNIAABNCXcQZggAcMyiYhmAADGPAv6SjAARVI1Gy2yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShH2ZSAAAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKySm7KZmbSbo7ijq7+lpb2sssSyssa0usm1zPG5zvG8wtC80PG/vs/A1PLE1vLFytbI2PLMy9jM2vLO0tvQ3fPT3/LW4fHX2uHY2eHa4/Le5vLg4uji6PLl5erm6vLo6u3q7fPt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/76goQIDhsyZNjgAQSJFzhXPFhAYECAAAMMIHiwAmeLChEYKDhwQIEDCBVaxHwxYsMFC+DD/ou3wGHEzBUNBgBYz779egINZraYoKCA/fv47TOY0JL79/EAjifCS+gF4N6BBy7w0nwH5OeggxGopMIGAVY43gUosKSBAAh26F4AFLAUQgIPlpjfAR2cRMJ/FrYIHgkqPWCghzSy94BKFTRo4o73VUDSCyC4KGR4Hpy0AgI1JsmeASe1AAGPUN7nwEgiDGkleSYhqeSWBJj0ZJRgMhASCVdeCeNID2ypJgA3jlQBmHAW4KNHKLBYppAZhkTBjGsqGWJIHegYZ5QpcvQChXdaeUFIK6jX55YBhNRCfYOCeUBHIyRapnkfNfDomvF9NEGlcfKn0Qt2auriBS54tAKf/p8mGYAJHrUgKKlQHpCCRpmqeiWnHHkaq5qhcjQqrnCaihEHvl65aEcEDKtmpB0xgCycl2LkQrNltrqRCdKuSetGKVwb564XkcmtlWdqlGa4W7ap0ZvmgjmnRUGuO2SRG2kJb5JMbvRlvVBOeZEH+g7JAUcG/Ktklxs5QHCUYl6EaMItPquRow7TSK1GlE68Y7YWZYDxqhxx2LHHHJEo8sgYpXoygBzBujKCHN368oMYXTwzgBlwxPHNBwrAUcg7O5jAsj9XuAFH0RKN4AAcWZv0gwpgVGXTAILA0QJSI4gARxFc/SAEGJ3ANYDAZiRB2AcWmxEGZjuobEUvrD0e/gscrQC3ex9w1ELd+ZWQkcl6W/B0Ryr/TXVHLhOedUZb6922RmD/DYDcGpVNeAF3W5S33qy6qvmstX6u60a9cn35RsKGzflGx5od+kUvIP7zBcB9tELjNwfQ3EctRL7zAdpxpALXJ4ikQdgSiBSC2Rh8VDnGA46U+coKjuT5yxGChDDGIPQ+UsMdIzD8SBKLDEHy4ifMgfkkoQ8vAeuT1H69DMAfUuu+EgH9ShK7YS0gfyWpHbIi4D+RLE9VF2jeSjRgs2lFbyUh0Bm2qpcSdTlrBAOMUZ8CwJyX0Atb2WEJd3RnoQ2MwFswQQ/wOjSABowLJvMxXokUMAF0vUQFlCTwAAdMdoHdeGAEfLuJBh5gAAJwKAACSE4DAneTEFTAAQwg0QESYJ0JGG42YAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqHRKQAAAIfkECQMAPwAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qm6O4n7LSo6u/paW9qb/lqsHnq8DlrLLErcPpr8brscftsrLGssDas8rvtLrJtczxt8Xcuc7xvNDxv77PwNTyw83hxNbyxcrWyNLiyNjyydbszMvYzNryzdntztLb0N3z09/y1uHx19rh2Nnh2t/p2uPy3uby4ujy5eXq5ury6Ort6u3z7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AfwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8uG62NGDBQmRowwscLFDB84dXRYYGCAAAEEDCCooKOuDxkmQkifTl06ChkzdUAYEACA9+/gAf4YaBD3+Yjq6NHHeKldQPj34QNUcJuDRPr71UfgYFliAPz/4Q1QwloznIffgdPNoBIE3QHo4HcQoOWDCwhWON0KJ+mwwIMcfrcASDBkwIACBxyQgAMUwNAThRa2iGFJG3YoowEc8QCCAgXkqOOOB2Sg0wwtBhmCgiNBIOORAMyXEQw47ujkjgmkcNMNBgpp4X4hldAgkh0OeBEIBzwp5o4+0uSDfVa2OEJIOvjHpYwBXCTBmHTqyABNMqQpJHYfNfAmkuRRBEKdhBZAgUw+VKlnhSP04JEOW/7JYQDNSZRCmIXWKSVMeS4aJJEc+SnpkRFGxEOTmdJ5QEwoeBrkmv4dETDqkXFGNGiqhJbZ0g6uCunoRi3MimQNESWAK6GrusRCr59yZKSwMpbqEA3HFqoiSypgwKyLHCEA7YwQZVAtoRKwZEMEF2xrIQocGfBthwRA5MC4dd65kgcPcKAuoxy59+6DAkCEKr1PJqvSBPnuiyCsGvn7L4C1OmQswWIanBLCHyiMIEeRPgwfRJhS/CRLIjywgcb4kcCRmx7DF/BDA4ucYwLmPqAByvexu5GsLcMX70MMyOykvSudYAHO6bnAUYw9h4cARBQIveOhLb2ANHp8aiRq0+AFOq3UOm7Kkg9XV/erRjpwHR6xEE0s9AE8vIRm2SZ45DDXA0h0q/7QurYUQ9nSZb0R01x73fbbcb9EdtmNPqo2AAGwHVEKQoMgU6dICx6q2oZHFDXFVMd0JtIjAPeRDnc/TKlFc9LrQOIy5YD0DSJ10LS0FX1+LAWwz/S3xuuN5K3HH2aUgtvIWo7TCgq7YPpIPL9rQKUa7T3mASD0fhPz26Lw/Eg6RC8sAdRzlIKIxpqoAAU0aJ8T5p7G8D2M0C5Qvlk4eDoC7Ss9+2cAuFMLkIQ0gt+8ZHi0WoDk2vKcueHHBDI420tasICOwScAC2hBXXIwgxWgwD4jIAFvZLADnJSgAQQQQIMCMIDxaHA2DrkfDGdIwxra8IY4zKEOd8jDHvrwh1RADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox7bEhAAIfkECQQARQAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIZnOVaGaRb3ubdXOZd4OggIungX+jiJOtjYyskpuylKnOmZm0mq3Qn7LSo6u/paW9q7zXrLLErcPpscftsrLGssDas8rvtLrJtMfptMjqtczxt8Xcuc7xvMLQvNDxvcnfv77PwNTyw83hxNbyxcrWyNLiyNjyzMvYzNryzdblztLbz9zv0N3x0N3z09vn09/y1uHx19rh2Nnh2t/p2uPy3uby4OLo4OTs4ujy5eXq5env5ury6Ort6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AiwgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8vOK6SGDBQmQoQwsaJFDSI4fWRYYGCAAAEEDCCgYJcIDRMfokufHh0FjZk+IAwIAKC79+8ADP40iEtERgjq6NHXeJmdO/j33wMwb6tjRPr71Ef0YFliAPz/4A1Qwlo0nIffgdKtlxIE7gHoYHcQoEVECwhWKJ0MJ/mAwIMcerfAWRRaKOIKJm3Y4YkIlFWDiCx+kANJEJwoIwDzVRSEDSm88EIQRuXQYov7hURBgzNyOKBEQVigwAEFNNkkAxYMRYR9P4oYQkg+CFCkjAFElCSTTobp5ARB0VBliy9+1MCWM9bIUBAOiCmnkwrw2BMRBp5p4ZUe+UAkmw922VAQCsxpaAEJ2BnTEDOccMIMPxBkpp4sKsjRmoDKGOFCcB56KAOLgvDAqKSCMIRAVFJq4QgeaZnpif4DMGSBp55G6RIPEpCq6wMR4ACEqi0KwRELr854g0JBgEnrnAfs0NIQue6qawQwAFspRzEWe+KmCHGwrKcctESCtNJ6YO2IHBmgLYoKMfDtoQew9AO50l5wroUkbkTAuh0akFAQ73rqrEoz0LsrBvdWyOpGrvLroAAJ2RDwoS+spILBum6QMIJ8avSnw+8JetALExsqwkrjYjyqxhvjt7BGID8IMUIplDxnCisVrPIDCLd8nwkcfRyzd7EiJLHNYuKs0hA7P2Cvz+mhwJF/Q8NHwL9IizmwShrs3AHU6bWQbtXwpZhQAlk3qcCz0dIbQQxgo3fdRguQ/d54Cc2atv6tLOEQgdsuEBE3dcJu5IPd4B2LddbNvjSEqLtWEGkRqcYNdEcN2120QnrbzPdLP7hwAgku8ECQDINHNzdHdSMOAN4LuVtynTwJPngIwHV0OOIBKM5poQEn6tOkYK/ekYlkw/6m7MsysHXtlW88Qu592h2ADxF56+kBHCjqkw5g6yASBWS7+VCSwIepgAXPAxXixhiOhLzDH1oURAopiPCCDUihsLHYJdmXwxCAvbKs4F4roN5IfCBAbRGggGZBHbCMZ5LWvWoBEDwL+PQUgjSpJFtsko9bVtSiEPzGJRoqUgAakEG2OCd66TEBDQoHExYsQGjgCcACfCeXHtRgBYIosE8IRsAbGgABJxloAAEaFoABiIcFs3lIC6NIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjdJloAAACH5BAkDAEAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJKbspSpzpmZtJqt0JujuJ+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AIEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLBozjhYoSIzyAKIFCRY2cIhogIDAggAACBhBksIujxQgO0KNLh46CxkwRCwYA2M69+3YDFeL+7mjhYbr56SOsu0yxIID3994HhGf7g/z5+9NL9FiZoz38/94RIINaP6CA34HSebBDSjkYAOCD3QWQAlo7gIDghdGpV1IKAkDoIXfzkfWDhRiW+BtJOXT44YrLjVVgiTAqOFKDK9Yo4VgqwKijBz+IhECNQAaQQ1g16GikCiFlAOSSCDzEQwwmsBDDTjOcQAIJJ8zAUAlGGrnfRwQsueSAC7EgwQEFpJmmA1PWNIMFD8Qp5wQrJFRklzqi8JGSYgJpgEI8UICmmoQWQMFMPoQg56JydoDQc3jq+CVH2vUJJJkGxaBAoZwWoECbLvkAJ6OkRuBDQTtEaiQMHaVg6ZL+EBzEw6adcpoATIqSqqsGBcGgqo4gdATBq0AKcBAGtdYqgUsu6OrsAy4QxOWvME6aUZjE1oipQCwMmiynLLQ06rOMRkAQtTqeqFG2QLY4kAPf1soASzeQ66yWQPSALoysaiQDuzXGOhAP3sZbqA0rnWCvricIlOq+GL6wkasAf9gAQTEYXGu4KpGwMKkhCIQDxBgiqZEIFX/Y5EAfaNzpBivl+rGcIQNxJ8kHmpwRnyk/uLJAJrjM6QcJz7woCQ7jjGALE/cM4QIEsSB0oSaspIPRckYLxA9KH6ghRjk4/WCIQPAwNaEIrzQB1qYO1DV+6mYkNoDuCsTA2Z621Kz+0Q0PROLb01mLkYpze7ctEEGfDXNLGszMK0EvAI5eRw0UHt9B8ArtwEs+rG1v2wRxLXl0EnMUtuXcXWwQDwm4fAAPMPnQAbkX6HDQ35Lj4BHhlouAULcGH1C1TCtEUGrfB9EwOgd6elQB6gD8mRAPEnzLQNo0uXBlCCfccKpCuHet+0e8i+37Qh/QSqgCiyN1c9c6e8Sz2D8zxIMJH2xgAqhKGdg1CILriIPEJoDDeQVSJPPA+ERSqZQF4Hxi+UF5SPa1kOTAPSkjm1gqtC8P9GtD5bNUAARmlh8gMFIeiFtJctBAEdbtLC1QlX5YsoBXCagtO8iRjkbwQZak4EefQBoACd2Swwt5oAQw6FFMfuihABAAAkOiSw1aUIISWMgDI0BBdZRokwwsgAAE6FAABmAA8ERxNmhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCES0AAACH5BAkDADsALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGZzlWhmkW97m3VzmYCLp4F/o4iTrY2MrJKbspSpzpmZtJqt0J+y0qOrv6WlvaW31au816yyxLKyxrLA2rS6ybXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjY8szL2Mza8tDd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AHcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLLmxjxYkRIThwCFHixAodOE08QEBgQIAAAwwgeECjrg4VITRIn05degkVM2kwGACgu/fv3Q3+MIirAwWH6uirc3jxkoaCAODjgw8wwe0LD+nzVw8xg+UEAfIFCN4AIKyFgn4IVreCSgoI6CB4D6ClQwkJVjgdCifRYMCDHHqnwFkUWijiCSZt2OGJCJR1oIgssjdSgyfGWB9TOeRA0Qos5qhBfyE9EOOPABR4lAsiUACBA0hW8EEND+lwno4iehASDfABeaIARuXwwZFIdtnlkg2pAGWOLnrEgJU/ziiUDBF46WaXEMiwEA5PjmkhBx+1UCWaHQaA0A0kWLBAAxaQcMNMMnD55qJyJiSmnSwu2NGZfMYYYUEdHFDAppwWkEFMObS56KgQMIkQfpCKWIJHAFZ6ogH+BMXQQKe0FpDAoS59MOquDmyAkA2p5gjcRia4+mNzAs1aK60LuFSDory+aWpBjwZrYZkZUWpshzNmsOyyn7IkQrS7fnBQiNZWiOFGJm7L4Yc3aPptrTGwVAG5pB4UXboVrroRd+5yCCsJ84LLErT4enkQqvwiOAJHrQbsIAE7WFBwrRKsVEPCo9pYUMMV4rmRxBz6qezFnDarUg4cLzqtQDqAnKCUGtFA8oNYLoBypwkc3LKbB9UpM3ohcLTnzfENsIMEO3Oa8Uqi/uwABfoOnR+J/yIdYIoEN11AByzpKrUD5ho0gtXprasRAVrLB6/XB9Sr8dgOuHDQimhThy3+RjC2/V23TVvg0gZSY4AQDnlXhwNHLfgNXgsD6XzxrS7lgHC0cSbEcN4PdxSx3xQPdEMCBS8gt0sycAxBCgrhnTd2HfXt93gFeVvrARngClOi5K6+UMyJczAsRzY7HgCyBd3QgaALSJABCzXlQPiuFbyc0AmJw+4RAo7TXpQMRXoZwQeNNgS81R4M31HxWguAPFI11OBxRC9YzQEMIk2gdQAXjIW9zJISCfdudqmxnK1hahsJ2yT2obLo4IDWQoH6REKDBW5LAe8ji+vGxIEAnkR2aApAAdGCIzvdjyU+4hP/3LLBkP3mJSAsGXPgggMVbC4/IVDBBFvSAgZ8LkCLA2BABuEyAxWMYAT44YAHeKMCG+AEBAwgAAEAFAABJIcBJpiNFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKNsSEAAh+QQJBABEACwAAAAAyADIAIcpJmQ1M21CP3VOTX9PVoJTW4VWX4lbWYhbZIxeao5jbpJmc5VoZpFve5t1c5l3g6CAi6eBf6OIk62NjKyUqc6ZmbSardCbo7ifstKjq7+lpb2lt9WrvNesssSyssaywNq0usm1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHX2uHY2eHa3+na4/Le5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCJCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy048hMeMFCdIjCDBgsUMHjh7mJhwYICAAAIYMJhgwi4PFyNCSJ9OXXqMIDNNOAgAoLv3790j/tiIy+NE9fPnXWB3aWIA+PfvHYxnO4QF+vvnY7DswQC+//cRrPUDCfgVSN0JQ6TUggD/NfjdAD2glUN0BlYYwgg/nOQBdw52CEAALZjFA4UWVjjCeiOZwKGHHQYwX1E7yPDCCzf4cNEPJJZYIQkktbAiix0KQJQPKGxAwZFIWlCCjRINYZ6OOqYgUg/uAQnkAUEJUYIFSHbZZQkSzQDlmDmENIGVaHrwkw8ceOkmkhsw2VAQOY5pIkg2/Ihmiz75gMGbgFJgwQ4OxWDnmDh8FMGeaGrAkxBtBgooBnPWeaiBI3iUJ6NWBkAQEDWAAAIMM6EgqaQiMETDpWOiqFEF/pyiOR4QGTSwwK23ZgCTD1yeGqicCNnHqo4zdNRfrEBOUAMEuDa7AAQ1uGSqr4GmmtAQlg6LX6Yb9aAnsg0GIIGzzj7g0p/UAmqBQjxoC2WCGpkArpUKkOusrivtkK6kwBaEg7s6ZqiRBvMCiYC9zgKx0g37BvpCQqsCbCFwrxbMogEIN0uqSi80DOgKCYkpcYWJanSmxR0SkDGuIKzUscduopCQoSMbWKZGi6LsYAEr39rBSjLA7KYMIdds80Yn6/wfzz2rsJIQQntJNEI5GF2gqxd5oHSDCfS8gA4soRu1BUIk1K7V92Ftkbxb+9f1yviuVELUR36g0BBoo3cC/kc9tA3fAA+s/IDCLPlANwU3LJRC3tXpx9EBfoMXQQ2B2wuB0y6JELXdC/3L+HQUb0Rw5N41R0QG5F5AuEtCGOkxBmUvhPfnF3rUN+kfFqQCCB3AsPquvaZrQeINuUB7sR45gPsEPu3guq8bEN/Q7HmTAG9Ht/stQIQ+CaH5qRzE/pDxeZf8kfJ+OxrUDiIEn+QHhDZJoNUsTMng1gwYJYMMKKzwwg7im0gQrIagkdhgaxAiy9kkhqGSsM1iIDJL1QA2gpuVRGsFC4CazoIjbZ1AbSPxEbgG8KKzOIlVLLgeSqgUKwZwby05mF+UBNYSD9zvSiGCCw0WhykWhA4mjxWAnIMCwADT0SUHMUjB/EZwAhbEgAcqpIkHInCA+wVgAAyIgAleOJsuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSliUgACH5BAkDAEcALAAAAADIAMgAhykmZCwrZzQ1bTUzbUI/dUtRgE5Nf09SgU9WglNbhVRZhlZfiVtZiFtkjF5qjmNukmZzlWhmkW97m3VzmXeDoICLp4F/o4iTrY2MrJSpzpmZtJqt0J+y0qOrv6WlvaW31au817KyxrLA2rXM8bfF3LnO8bzC0LzQ8b3J37++z8DU8sPN4cTW8sXK1sjS4sjY8szL2Mza8s3W5c7S29Dd89Pb59Pf8tbh8dfa4djZ4drf6drj8t7m8uDi6ODk7OLo8uXl6uXp7+bq8ujq7ert8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AI8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLbmzkhw0WKk6UOPHihY0fOIGkwMDAAIEBBCJEwJDC7o8YJUZIn05dOg0iM1NMGACgu/fv3S3+5Ij7Q0X18+dfYHeZwgD49+8jjGdr5AX6++dpsAQSAb7/9xasJcQJ+BVInQpGpAQDAf81+J0BQKD1Q3QGVjhCCUKclAJ3DnYIwAAwDFVEDS6ggMIKMhSx0YQWtljCeiNt6OGMA8zXUxEufLBBBjz2mAEIMmBEBIUtWngCSTlwOKOHBPikwwc+RtkjB0FSZIR5RRbJgkhAuLfkkgzsVAQKUpbZIwoU2ZDlmjuEhMGXcHqgkwhm1vmjREOumWUJICUJ55cD5ESmnXWSEBENerL5kQV/xnlTDYQSWsNDRhCZqIV8dgSEko16GGhNRUAZaZ0bPHTDpWvCqJEGncKZwxD+M8wgkwyjEuqCQ/ahWqQNHfXX6pIKSADBsBfg8BIItdpZKkOV6lpkphpt+uuSAQxrLQQmtFREsoQGwdAPzmaZoEYpTPvlA9cOa+xKtHJb560L7RBukRlq5IG5SzaQLgQUsOSCu3WuwNCp81oI3Kr4zrjAvhAMsRIJAJtp6EJqFlzhDRu9mXCHBTAsq0qDRhzlxAoharGBbWrE6MYOIsBwCyutILKUaFJ8soE8ZMyygwkw3MNKkM7sY5UK8XBzgapeFMLODTqwbwUsbSt0j94uBO7R9yVtUblM++d0uh+vxMHUGXzQkBFYo6cCR0B0DZ8A6UrQgUv/Ti1wQyykXZ3+fhwx4DZ4B1xbAcwvjT3zBio2JK/e0+XM0b1/exfCES3M4HBMQYsM79mMSwftRm1H/iFOEEdc80MxdM6rRxOIjkFOyLoLQuKUMn7CuJpGTkCEgnKLAu0QpZ42xiC17rYGPLlguLKbS3Ql1izg7lGXXTPAe/KiSvmBC8BPRMTRt4+UA9O7BxWEDCuQQAIKLviw4skYlsR1wiCaZfS8JThe0tL4DjD5WUKwVKJUoLWRwIBTjTKAjc5ihLxd6gXSOwkQ/NapCFxPLTwgUJZYUK+WhIBBX2JAiOByAwcWqAQvOFhMNEDBBg0gAs2pCw9owAINlkAFL6DBDyI4kxBYgAFzIByAASJggRRccDZITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQBEtAAAAh+QQJAwBCACwAAAAAyADIAIcpJmQzMWw0NW01M205OXA8PnRAQ3ZCP3VESHlHTX1OTX9TW4VbWYhmc5VoZpFve5t1c5mBf6OIk62NjKySm7KUqc6ZmbSardCbo7ifstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLFytbI0uLI2PLMy9jM2vLN1uXO0tvQ3fPT2+fT3/LW4fHY2eHa3+na4/Lc3eTe5vLg4ujg5Ozi6PLl5erl6e/m6vLo6u3q7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gCFCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy47cIwaLEyVCiDihgkWNnCsmQGCgYMABBg4gaLD7I0aJD9CjS4eu4rfMGRMUANjOvft2B8vh/gZ5EWK6+ekirLfcEWGA9/feD4RnW6P8+fvSS/RgqcE9/P/dKbCCWuPhZ+B0N6TEHoAMetdBUDm0kMIIIJjQQg4fBaHCgRxGF8NJOzjQ4IjcTdATECZsUMGKLK6YwQg8bKRhhzR+IENJIZKoIwAW6ATECBe0KGSLI2jEQo01JjgSBDvu+OBNPHAw5JQsbhCjRTIgWWMI+4VkQZM7DjBgTTlkQOWZFVxAQ0U/2KdlhyWENIN/YJKoQE08BInmmRdcKdGRbyYJEpN1OjkTEFLuiWYGE+kQKJIifERCoU0eMJMJiipqgkQbPlqjehuJSOmO87kEhJ6Z8ulnQ0G46SmH/ix0tAOdo44IQUyYpronCBDd8OqWHXVQ644BxGSmrmheANELvwq6UQTD7rgABi7lgKyiqy70XLMdfriRdtGSmEADD/jAUgvX7pnCQyJw22GsGx0QLokGNNAABSylkC6amzrkbocqcDQviQXY24C5KoGw75keOPTDvxzGqdEMA48ogMEurDTCwlTyyirEB0qc0Q4VN3ixvSislCvHQhbpL8j4nSBwyQASYDAOK8HA8pAtPOQqzNK9wBGtNHeHgL34rgTEzkKu6dC2QE8n9LdFw3d0uS0dy7SyDwEatXRKakRo1d0tIAHOLW3MdAUuO+Tr19GFEARHwpLNXQA2wLT0/tYYPhQE3NEFzBHJdn8n08oct9014B+EvdHYZD+pt4ocZwCERD0ALsLcHa1Q+AE7zJTnwhf0zSncoHIkatWlxkTDvhfAUNHfUcsMEuFFM3BTDpSnuoHpFDELcwg6iAQtzQOQkJO+il5gwuUYdQpx6h+tPnDrNv2otZAXwCgj1NxOPdIO4M4bwU80tGACCCCMkALw37v7AuckkW9+6GdJ/2gI1JNkfaEDwF5ZsvSoEjgOJV+ilAIkRyCv0SgEMqDfSnYAORINwAL4c0sPyMOhE0RQJitoz4gYgMG66CAGJzhBu3RTAhXEoEs2IcEEGMAAeRlHAQ6YwJhmw8Me+vCHYkAMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIrwQEACH5BAkEADMALAAAAADIAMgAhykmZDUzbUI/dUNCd0REeE5Nf1dVhVtZiGJhjmhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGcIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLpvyiRIgOGy5g2PAhRIqcIBgoOFAggIACCRREsCujxIYK0KNLh/6hxMwWDAoA2M69+/YEDOL+yhhxYbr56Rd+u2yxIID3994DLG+bovz5+9I3vGAZwT38/90VAMJPMZAgggcciIBCDCONh9+D06mQEnsAVugdBTvFwIEEDzTg4YcNTMCBCx/J8AGEKEZnnUktJGDhi9yFd1MMGXQI4o0fZtCRiSn2WMGKI7UI45AAyEjTChDgqOSHD6Cw0Qg++ijhSAsQSSSGM4lg45JcepCRCVFGuV9IDlhp5YAxecDlmh+KcBEM9oWZIgYhneCfmTAKENMKW7LJpZMVQSmnj+p5VCWeRM7XUgwS+OnnAyROBOegPl7wkZ2IEhnASxw46qgFFIVAaZSFbqRAplYqqlIMfXrKZaT+EWEwqo8feCQAqkQm0JKarvrJgUQqzFppRxTgqmlLE/T6qEQlCEsoRwwYm+hKMSjrKKwOdeBsj0BmdIC0QxqJEgrW+klCrNumGAJHt4L7ogIriVAum15ClG6KtW7kLowIrMTrvEv++hAM96K4wUYn7PviACt1CvCSOj4kQ8EQHqxRCwpbSMBKJDy8pJv2UoxfBxxlXKEBHHus5LkQxSnydOtudKfJ3sG7qso4YtvQcy+bNwJH2tH83gIsJYmzhxJIJGjP0k2p0aFCd4elSg4fHTFEwTId3QUycFRs1NwF0AJL1R79wAoTudxzvhzNLLSuLVXt8dURiap1BaVqdCr+2ACouqrRDz/AoKR3Y9B1RwmDLcDYLpEbOMuhap23qWD7zVLHANdb0cQ9d3C4RxgLfQDjMP3b6wOaWwSmyBewIFKZJgegwZGtsvkA5BidSLEJJLmYsQM3yc3lAxnojBHP6f5cUtDuEo2TCxsuKQEHaHskA/LCjvB5kMwbuwDpORUoAgcZeIBC9SItTekFvKcEdaYBAK/W6oNi4HRKsCMqwNRqyWB3pSbYnkpasDdNOQB8bHkBeVDUgRIIsCUgaM+LDsAABMKFBSXoQAdkpRvelGBMNtEAAw5wgFsZBzkMQNNsVsjCFrrwhTCMoQxnSMMa2vCGOMyhDnfIwx768IdLQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOfIlYAAACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLvpxChAcNGChU0MABxImcFxAYIDAggAACBQ5AsJsCRAUJ0KNLh87BxMwLBwQA2M69+/YCDeL+sgAxvXx5DNZddjjgvX37AeF9ukARosSKlC9AUDDPfzqHFyupcEAA7hXoXQEq5ORCCRYskMCDECZgQQgkvcBBfxhKRwELKalQgIEgdhdABza58AEDEaYI4QIfhMTCcxnGCN1vJnWgXYg4brfcTCU4oOKPEDpQgkct7CfjkSmURAKBOTZ5gUwbOAjklAlswNELGhyp5YYjqUBAk2COCFMGVJaZQAYbeaDlmhUAGJIBYMYpQIItbWCmmS1idMKafHoQEgRxBmpASyhIeSeVQ16EAZ98tgDSAIEGSsJKLvh4aJkLXDQCo3xy8JECkQZawEp2XoqnRTByqqWjHd0YKpj+k6LkAoqmYloRC6ryOUJHHbwaqAIphVDrnXlKJEKua2rQEQK+xklAShEMa2YEFGWJ7KocfdksrCjRKu2UmU507ZpJbrRtnE+atMK3ZrogUQvjarmrRiScCyawJqHAbpmJQoRrvDKKsFGv9uaIwEkl7EtlsQ+lALCMfmp0QcE5DmqSsAoDSWFEez6cIQgbAUpxiAectG7GP/b70L8e9weyRgSPbGDJJrmA8o/3RfRCyximl5EKMoMYn0mG3pxAuDrz3B+NPwdt4I4mTWA0hNROlKrS0rGqkatOdxfrxVM/uLFE5GEtHQYdsdd1dwOkNOvUC+Qs0c5mRycwR0Cvzd3+wSl9MDWaFV2NtdYbcd31193ezIC7FZlQtwSeetSA3gCMulLCKI8duNlcfmT4yGKy5LfCFmTUsdIvfySy0zS3ZMG+E2xkbcttiqStzHOO+S3gGr2wqMcUlBuSCpCCni5MJXh7J4tEGgkw0yItOTLUUC6/AeMeseA8shTMW1IHTG4bAL42fSC1igs8cP1Ivl9LAfQlES8+9TitEEIIH5SAAvYlqamqBoRDCZxeRQDEpYUF/tMSel7SgQGCCT5xSUEC+0MBDYzATTC5gAMNFAACKIBOczkBCDSggf1QAAMcqA4GawKBAxCAAAQKwAAKAB4QzuaGOMyhDnfIwx768IdbQAyiEIdIxCIa8YhITKISl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj1gJCAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy878YgQIDRgoUMDAwcOIFzhVKDhAYECAAAMKGFCgou4LERgkSJ9OXboGETNVIBgAoLv3790J/iCI+5xC9fPnQbzUHgC8e/cHdLoIYeGBgwUJGDyI8CFligroBVgdBSewdIEA7yUIXgAQ2FRCBPglIOGEFEaAQkkjmCfghtONoJIC7SkooncKzISCBRSmqGICE6wQ0gsecCjjdBycpIIBI+boXQExlRDhikBKGAJIMc5oZI0l4ajjkjy6tMGPQQaZgUcjGGmlBB6OpMCSXAJQIksbRCnmhFNuZIKGV85YYEgNhNiljg2q9AGUYwY5ZEYvAJimkRSEpAKCby4ZgEor0FlnkC5iJMKeV2L3EQKBdjkeShMcemgEGL2AJqMyUtCCRyq4GWmOAZBwUgmWWlrCRYtyamWW/hxBOiqXX5YUQaqHOnCRBq5a2WdHBMzK5aAluWAorkAmOhELvV756UYdCNulqSSFgOyhG1RUZbNGwprRltIuWatIt147JqYUFcmtjEhqpGS4OTY50gPmjslARRysO6MGHBUAr44ElMRAvWIuUJGe+m74q0aA/isisSMRPKYLFCGccIALZ9SwwwlCLJLEYlI80aYXo8eRqBy/V9KxIOdXUXQlB1gBR9ylnKAAJTnQ8oq6UsRrzOjxu1GwNr8XMEko7pwiuhOpCzR1HnD0btHfGVDSB0qnWOZE2z5NnaMagUv1d5OO5ELWFCor0QteV/esRiqMDR61JJWbdc8Hty0d/gYebUz1ACehgHYCq1oEgt4SgL3RAXJ3V7atWT+Qqd6egtp4qSitoPQChbPatuKxyv24Sda2nC2eFl9MAXAf/Ul1AM2pVKnEFnCUwtMmiHQB1Q20lLS5Foi80eElqzcS4ynH51LpuC5wukf5JuwB6yP567ABsbuEwuyHPnBhSNFzqwH1JFkfLgHZw1QCvWI+cOdIrfYKAvklySrsAenL5MIGDwws4QIOeEAGvmeSE7iKArlbCQRmFYDe9UR4LOkan37zErEJijlweU7q0IMBEbxtPQjw23sGgAC6ySUFI+CABgBEgQrwRgQswMkFFFAAAiAoAAJIDgI6MJse+vCHY0AMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQmYlIAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy978wgQIDRgoUKjAwYOIFjhVNDhAYECAAAIKGEBAou6LEbklSJ9OXYIGEMBjqlBgHID37+AB/hA40DynixIfNliYkOFDiZa1K1SfPx/ES+ECwuvXf8CmixARLJDAgAQSGMEHKaWAAX0MzmcCSxcMsN+E+jUg038MFKihhgsgWNIIFDQoInUiqKRAABSmCB4CMKHgwIYwaugACiOBMOKN02mA0gEq9vgdAS6VIGCMRBIYQkgi4KikdSYh4OOT4rG0wZBFVulhRyMsuaQHJCkAJZQGqPQBlVVWeSRHLISopZIPhtQBil8+aeFJKJBZZpU0bqTBmktSkN1HBMQJZQDllfTAnYgmsMBGJ/CpJZcfQSDol2GW9EGiiV5p0QsLOrrknxupIOGkUBYakgsvYnrnohg16umW/h5JSiqYJIWgaqIbYMTBq316VMCsg5IUwa2IRnDRC2ryimMKHKkAJ7A+XiCSC3YSS6QLFrmqLI72bSQrtD72F1IJ1iJ6JkVJboujjhs5Ca6PQIZ0abll5lqRB+riSAFHBrzrYwAibUBvmRlYtGu+I+670a/+qghwSBMMXKWxFcmH8IgvbJRfwyqqEJIFEhc5gUWdXtwgqBeNyjGFpnaUQchEWmDRniY3yFGgK1Mo0rwwb1hwRTbWTF8FHPGY834CiGRrzxueO1HQQlfHrkZGHx1evCC5wPSGK2QbdX0cfWv1d+JCvLWBx35d3QnNjh0eBCPxvPXPBqstHQUZc8Sw/tsBeDxthlsvgO1FJtgtAQceNeC2dwVYejbdm1r8NbMdqbDx2NKWBDjMDAyOUeFfQ5q425WWhELPCziNkeQm+xnS5SsTihK5IWuaUQpRjyDSBVYroJLAA8vskbYIdyuS2P6WnRLtxHYYEr4IY5D3SP02PIDfK6GweaIMqO4R1MpqMD1JVUNLAPYtTYnoAht4LlKWyoIwfkleQnsA+i9tcGiMCzzQPkopYJ2WKKC7lVwAdl8KgO9u8oEMZGACEbDABkLgvpTAr0/YeUn9BkWeuEDnRhUAAQtmwp0eCeAAHXDOCDygAfnsBgMcEMEIb7IdAxAgP8gZQAEQkMLZ+PCHZUAMohCHSMQiGvGISEyiEpfIxCY68YlQjKIUp0jFKlrxiljMoha3yMUuevGLYAyjGMdIxjKa8YxoTKMa18jGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrSKgEBACH5BAkDAC8ALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31bKyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AF8IHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL7uxCxIcMFyZMuLChgwgXOFMoOEBgQIAAAwoYUJDCZwsVKmi6CHEhgvXr2K1nCDEzBYIBAML+ix8fngCCmy1IYHCwIIF79wwqkGg5fUL2+/e5u/QegLx//+fJ1EIF7b1n4IEVqIQCBfg1mB0FJrBkgQD/VUieAA+81AIGBR7ooYEJmiSCfQ6WeJ0IKinQn4UsiqdASy1A8OGMBzoQnUgudGDijtdtcFIKBrQopHgFrHQCAzQm+d4CJ4ikI49Q+lhSkENWWSRKJ3SopJJNfiQClGBGUAJJClRpJgANnNQCklu2ucCNHJVAYpg8RhhSAyueOWSGJMXY5p8JONCRCwzSCeUEIaVAoZ5VBlASBoACigFHIRgapn4eIcDomQGG1IKWkSb5pkYuzGnpjhOw4FEKeW4qZAD+I4gEaah/SqBRpaeCiWJHmrpq5oshsUnrlgtolEGuYCLaEQG+mukoSCAMC6gHGK2AbJiqbsRBs2fG+tGs0m5p60VfXgvlrhqVyW2VwHokbLiiYvSkuTtKqRGV6wp5pbvwtonRBvTymAFHBeQ7JAEg9dtmCxcVGnCJymq0qMEsPtvRCQpv2WVFDj/cYMQZTUxxhRZzpELGSs5nkake48dRqyP/91ELKCcJJ0XVtdwgBRyBF3OFAoAEas3vYXSszvgNvBGzP/+H8EcOEO0hA/IijV8HHOHb9HgGgFSB1AdCgFG5VmOHaUbqbj1epx1FC/Z7IGDkQtnZZatRCmqT563+R5++ncACDGPUsdUXeCTy1gOIJIHfIWL0Ad3Wna3RAXmHxzbfb4+a0dx0p7pq5bCOBG7Nk26Eq9WSb9Tr1pfPHHXNDgRO6uAPTwDcR4puHUBzfQ4tLZMeoWD1mCFZsHWaJpGQ8QJxf/R4yx+QRHnMB6SUJbzMiwTww1iXVDDFXaukwuu02kjS9uZucLv3BhfA+0oahLoABrKPdHquIaxv0uq+IvA+Sy1YHLEgcLOSmCBXEyCeSh7gqwAgTyYgkMDQFgABD9QvJWQ71G9ekrZGMScnKiDBCVRwQfqEgHb3uUAI7PYS7xzuPwNAwN7kggIRbCADDJoABXgTghXgxAJ1CigAASgUAAEkBwEcmI0Sl8jEJjrxiVCMohSnSMUqWvGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkqxIQACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL9vwihQgPGDBQqKCBA4gUOFVcQGBgwIAAAggUOHBBp4sQHzZYmJDhQ4mZKUBQkMC9u3fuHEz+zLxwIACA8+jTny/QgKaLDQ4WJJhPn36EDStasuDwvX9/DCy81EEB6hVY4AAdwOSCBfLV56CDFqj0gnb+VfgdBy+spEJ5BnaoXgEqtPQBAw+W+OAGJ73An4UsdldBgCipQKCHNKInQIIpLWjijhCWxEIFLQbZ3QkndSBAjUiiBwFKLkTA45P0OeCCSC1sJ+SVwJFEgnlJdtlcSU1CKWYCD4T0AgZXpklBCyOpMECXcAZAQkkWjDlmBCCtmOaVFYw0I5xdCkDSBnba+YFHJ+ypqAghQQDoowiI5EKDhUK5QH4bnanonmt+5OajgMoZUgaV2jkBRyNsqqgHHykA6qP+BoC0AqWlQolpRkCquiebHR35KqBzekRorWOimBELuio6Qkcd/PqoAh85SayYDGgkQrJ7ctARAs4CWoBHk0475q0W5YqtkBRkuJGv3SYZQIgclSBusRi9cO6eWWakQruAfrnRsPM+aaxFyN57pXgaNctvl+1xBHDAO2aAUQoGX7msRhcs3CW0HE0A8ZN4XmRCxUIyqlEDGicZKUd1frzjqRclSnKLIGzkaMo1HtARqS6bGGHMM7eIcEY34+xhwxt90LOJEl/UQtAsEqkRCUbTuCRHISxd4qEX2Qt1hflitG/VHfqrkQtaP0huReZ+3V2n65JdoKgdOZB2lBqB4Pb+hR0dIPeHHz2sddMT7+2dyRtl/Dd6K3eE9t0JoLBR227zyhG7fwfrkcdp/6xRqoaz6pGri8cK0uNLX8rRC5TPTAGMHamAudEB4AiS0ksTvtHIbiPuEcpyNx6StB/D7JEGX2OgLkgEkD0AvCPZDbGUIGnqOuwgfYpz7Sa5IL24Dqzd0dMkUyD1SFSnHMDVJxFf6wNTjsSCledSMDRJHXDZbgBIoyT4mAvYQPxIYr1kmS8l2nPW+lriAs6JaQEREB9JPJAsDVguJQZwFgE01xIUZOB7JXJABiTHEhYgb08aOB9LOtA8QBGAfTJxwQcyYIEIRMACGdgACWFyAgq2iAKZGhjB8l4CgQzWKAAEUAD05HICEGhANxKgAAY44AETDJEmEDgAAY4DgAAMoAAGaMASZ0PGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKpwQEACH5BAkEADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPLJutiJ4sRHjRgoEABAwcPI17g7KDAAIEBAQIMKGBAgYqbKDI8cLBgQYIFDiJkQCHzhQgMEsL+ix8fnoOImSoQDADAvr179gUQyHSRwUGC+/jz32dgweULEBSQJyB5FJjwkgoHBPDegu8F0IBLLmxgnX4U6rfBSiZUMOCG5GGQAksNCMDgiO8NcMFKKNhX4Yr5McCdSf9xKCN5I6SEIIk4vqdASiVMyOKP94VQ0gsczGikeCCcpEIBOTbZ3gEnSQjklPd9MBKRR2YpgQclLenklwAYUNIHPlIJpJAhgaCllgaOdACYYD4oEgplmvnjAi96NMKaa34YkgJwwnkiSC6oaCeVC3z0QoB8ZllBSCooGOiXAoS0waGHWtmRCI2u2aZHCEwKp5wduVAnpnfWtlELjHZ6JAX+H5EgqahOBvBRBqgeOgFHnLqqZY0dhUormDt2xECudia6kYa+ZsmBRyIO+2UBHaGA7KEraMRCs2sKt1EH0sL53EaXXmvmhRn1yu2Rn2YkbLhOkppRBOaa+YBGRa575HkbMQmvk/Jt9EC9VDKgEbP6zvjsRtH+myO1Gx1LMJDKYoRwwhxqwFHDDpNIAEcTU6nqRRgbCetGHTdp60YhT5ntRS+UPOOjGqmQco6VbnRqy/lp1KrMAmq80aw3L/jxRobyrJ8DGoEH9IBcbrRe0QyKKbDSFd6bkQZPD5jkRgRQzSCUG+GKtX4ZaKRm1+SdwNGbYr8HAUcunK1fCdqyTV7+CxyBG7d7JBhr934cXdz1whv/DZ9HZtud9kZr683v24oDEDDdEmO9wMgZxaw3Bd5yZPPfAYzb0QeOe+SB5CAZ8PflHmXeMgOca+T50xWE3tHoVAtgukfW8rwA3h+Z8DQFbofUANUBzC1SCTxrCtLqMgMrkus3FztSuQT3NxLXGH9NUtgdk10S9OYuIL1IL4C/Lgi6i6QC+fAe8DtJKMh+6AJomhS5qxSw3kngNqwAaC8l3DPTAjZQu5LsCYDJUwmgaNW8l2RgZ/pZQAZexhLqaSmA8VMJ9sBkwPu1pATSyRx2HLAdmbAABIYbEAZEEEKWdOAAHBvRABBgQpo0kCaGKRCBBjSgIQpUwDciYAFOLoAAAhBARAEQwHIQ0IHZWPGKWMyiFrfIxS568YtgDKMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOelIqAQEAIfkECQMAMAAsAAAAAMgAyACHKSZkNTNtQj91Tk1/W1mIaGaRdXOZgX+jjYyslKnOmZm0mq3Qn7LSpaW9pbfVq7zXsrLGssDatczxt8Xcuc7xvNDxvcnfv77PwNTyw83hxNbyyNLiyNjyzMvYzNryzdbl0N3z09vn09/y1uHx2Nnh2t/p2uPy3uby4OTs4ujy5eXq5env5ury6u3z6+7x7fDz8fLz////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////CP4AYQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyrSp06dQo0qdSrWq1atYs2rdyrWr169gw4odS7as2bNo06pdy7at27dw48qdS7eu3bt48+rdy7ev37+AAwseTLiw4cOIEytezLix48eQI0ueTLmy5cuYM2vezLmz58+gQ4seTbq06dOoU6tezbq169ewY8sO62IFihAfSqzI+WIECA0YKFCowMHDiBY4VSg4QGBAgAACChhQQKJmiQwOEmjfrv2BBRQyX/6ICC6hvPnzEjSImKkCgXMA8OPLB0AAQcwS2bnr587gg0vxFKAnoIAgvNReAPMlmOABLeG334P6OeCfSilUMOCF6FFwAksXCKDgh/MFAEFKLlgA4Yn6WZDSCAFi6KJ566WkAIIg1hiffSa5EAGKPG73gEkvgPDikOZpcJIKB9ioZHwElOTCAz1GmYADLpAkJJFYGllSkkt22eRIO0oZ5Y8ijYDlmRKMQJICXbYJgAIiZSCmmBOEdEKLaBK5YUgQ0OjmkiN+FMKcc4bw0QsY5HkmBSGpMMCfbQYAEpSESsnAR2YqemaMHrEJaZs4cjRopWJOuNELeGo6JAXIdaSCn/6fKhlAdRzlR2qUC3SUqapYcrqRp7F2GWpGKNw6J3gbacDroh4REGykHMlprJQqavTCsmi2qpEKz7pJa0aUTttjrhrtiu2Qav7abZtwaiSumFVm5MG5WHLAkQHrdlmARsW+GyWyGHFAL5EYcFRAvksOoFEJ/kZZgkaJDvwioxs9irCNkmb0QcM9bqCRhRK/yJGHF9uo0agcn2jqRamGPCBHsJas4MIpo2hoRhG7PGAFHFkss4ICaLRCzScCfJGyOg9Y8EbO/qygwhm5QDSEu2V0ZdLoaakRl07P92VGDEwd4UYmYC2grxg10HWCw15kotjbVZvRtWaflwJH3K4t3/4FGw0Nt3Y3f1x3eUuPrDd8UG8UNtwLxKuRCINLUGBHCBwOAIMcbQx3Bh3RbTYFLHiU99oBdODR4kQz4PhGkJuN9kaVr932yWKvfCrIOlPwAkgqkPxzACqANAHRcnuUAtbpgnRB1+1OmjKVZeo8uUjAXox5SC7YKq7qJAksMQe7k3TwxQUEP9KT7z5QdfcDaxB+SeOvS4D5Jb19qwWrk9T6siC8b1LszzoA/UwSAtSJSUIrqZCqKGACDvnuTwFoQEs2YEAULWAD+VtRniggAv+tpHpLCgACBsiSEGSggttxwHcyuBLx5AxDGBCBtgzkHiUNAAHfkokLbvOBDZQABYksjAkLRsABDVhoOBjggAhCd5MOKKAABPAQdAZQAASYbjZYzKIWt8jFLnrxi2AMoxjHSMYymvGMaEyjGtfIxja68Y1wjKMc50jHOtrxjnjMox73yMc++vGPgAykIAdJyEIa8pCITKQiF8nIRjrykZCMpCQnSclKWvKSmMykJjfJyU568pOghEpAAAAh+QQJAwAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17BjywbrIsSHDRYmZPhQwgVOFiI8aMBAoQIGDh5MvMDZAYEBAgMCCBhQwEADFTNRbHCwIIH3798j/mRYIfOFCAwS0qtfn56DiJkqEAwAQL++ffoFELxcYaE7+P//TUBeSy+AQAF7CLJHgQkvqXBAAPdFeF8ADazkQn8AZgigBSyZcGCCIK6HAQssNQChhCjaN0AHKK3wgIYw/ufAgCYVGOKN7I2QkoMp9nifAiaVwECMRH63QAk1coDjkuq9Z5IKBfgoZX36jYSCf0VmieRILyjJ5JcglATllGQCcIBIK2CZZZELoDASCF/GKQGDIx1QZpkVfuTCi2v2yYBII8gpJ4khKXDnnSx6lEGfjCYwAUgvfCgokxWEpMKJh04pgEdpNsoojRyJMKmcdHqEQKZ35rnRBJ4yGoFH/i1IOuqSFHxEAqaoShkARyi02iioGcE5a5ylbmRnrnhutIGvjG7QUQXDxsmBRwIgW2YBG0XAbJ8LcMRCtHFSsNxGHVhbZgDYYeTCtp9uJCq4XxaL0anmkqmqRSWw2+cHG3kJ75JOahRlvVNWedEH+q7prEbQ/rvktBtVS7CU2GK0bMJFZrBRww7fqAFHEk/cIwEZsYoxka9q1PGSlW4kspSbYmTByUQ+mtELK+OIwUYqvOzjABktSjOMHGoka87sfbwRrj7fRzJGCA+t4cIZoYd0gh5wNF/TEhqQUQhSaxjCRhpcnWCYGxHAtYRnqht2hsBaJKzZ653A0bFr2weB/kZDvv2dAxylQPd6FLTA0QV52xcACRoJ7XcCGnPEMd0Qg5w4fhut+3gCW27kweDpBbyRAZcDYHBG2vqdMkctgF7BuByRcLkA6Wqk+dudc/Q53Tp+RHreQHZkstQ2e4Sz2a+D1PPatHvkggNSM+AbSCZcTYHdITXAdQB7f9TpyW2OtPvKvYv0+8vBg5QvxguMTZLVDqNN0tYTty3S+uy2X2PZ/4IA+0gqUBvBDlA7kaAAesxygJtQMrdZUaB8J8FbrgKQvpK4YHiMWkAEppeSQDkQeyox1AS7lxIUYLBID1ggS14wvi898H8qUcH5yETBAqoEBRlAoIYckAEVvoQFpSCYHIg0IAIYtqQDBwhZigiAABu6pDYZyMAEImCBDXzAhzRJgQg0oAFoFec4IiCUTS6AAAIQoFrSoQ4CEjWbNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJyla68pWwjKUsZ0nLWjolIAAh+QQJBAAwACwAAAAAyADIAIcpJmQ1M21CP3VOTX9bWYhoZpF1c5mBf6ONjKyUqc6ZmbSardCfstKlpb2lt9WrvNeyssaywNq1zPG3xdy5zvG80PG9yd+/vs/A1PLDzeHE1vLI0uLI2PLMy9jM2vLN1uXQ3fPT2+fT3/LW4fHY2eHa3+na4/Le5vLg5Ozi6PLl5erl6e/m6vLq7fPr7vHt8PPx8vP///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8I/gBhCBxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy/7qosQGCxMcRIhgIUMJnC9SiOCAoQKFCho4gEiBU8UFBAUGCAgggECBAxdk1p6wIIH37+AT/kz4MDOFBwoS0qtfn96DiZkXDAQAQL++ffoGGrgs4SC8//AOkOdSChqwZ6CBGrTw0gUE3OeggwSQoBIKEfxnIYAorPQCCOgd6OF6HrCkwgHzPWiifQagVEJ3F7b4XQgpsYDBhzSuh4GCKHUwwIk82jeAhCRtwKKLRG5wUgod1qgkBcyZdEGJPUYZQHYibUDkld8JOBILSSrpJQsldQBllGR2ENKKWKYJo0gvVODlm+lR8MJIKghA5p30BaDCRygMmSaRC2QI0gscwGkoBiKpUACejA7w0QN//ulASCMYaul7ICnA6Kb6cRRCpJGu2dELXVq6JEgqjLmplB1BCmqa/gt8JIKplorwEQKrborARp+++qeWGpFKq6EUeJRqrowGsFGFvqYZQUcnDHtpRxAgy6lGDDQLa0eFSgtniBwtai2eKWLUq7ZYAouRm96+WSxHdo57p7IYWYkulhls1EK7hoKpEQnyMmrmRRPci+UEG6XAL5xNZnRBwHhSaRGzBrv4rEYmLPwmphk1APGdnVrkasUtTqpRpRorOcJGmn4cpQIYjUyyhSZnNGvKNdqqEa4u97grwTO3+MBG0eJMI8cYVdszjyFXZEHQFyKskcJGf3jCRg8vfSIEGGUAtYUWbPRC1R/imJEKWp8IpEXnfg2eqBmVSrYEcnKkatoA6JmR/p9uL+ACRx7MzR4HHRmA930FaFSw299dTLTg613NkdKH08d1Rm27bSRHY0NO95wcoV153ntiy3gCDPzdUeCQ61z46D9rlHnQm48qd8p1G3u3y3pz1N/XDqjuEcpku+5Ry2nHvtEKXy/wW0gzVo0B6CDtqPUApXdUAtTqeiQszhSYjeru4wawtkf2Vhw2SSyAL/lIHfQcwOUheW2wBcKPRDW/FLxPUtYBm19JtqetBXRvS+ySFgb8dZIOxMtaAxhYSVCQrVc9AG4neQHraKUB6qFEBYZDFgGyd5IPVBBLC9hA/lTCggS+SQMNY4kDN0UAia1kAxSz0AIeoEKZnKBbpDSigAZWJhMIiItHASAAzGQSggxM4AEM0A1vPrDCmZwABBpgFwUwwAEPnMCDNIHAAQgQrwAMoAAGgAAJZ8PGNrrxjXCMoxznSMc62vGOeMyjHvfIxz768Y+ADKQgB0nIQhrykIhMpCIXychGOvKRkIykJCdJyUpa8pKYzKQmN8nJTnryk6AMpShHScpSmvKUqEylKlfJyla68pWwjKUsZ0nLpQQEACH5BAkDADAALAAAAADIAMgAhykmZDUzbUI/dU5Nf1tZiGhmkXVzmYF/o42MrJSpzpmZtJqt0J+y0qWlvaW31au817KyxrLA2rXM8bfF3LnO8bzQ8b3J37++z8DU8sPN4cTW8sjS4sjY8szL2Mza8s3W5dDd89Pb59Pf8tbh8djZ4drf6drj8t7m8uDk7OLo8uXl6uXp7+bq8urt8+vu8e3w8/Hy8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wj+AGEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuXMGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXcq0qdOnUKNKnUq1qtWrWLNq3cq1q9evYMOKHUu2rNmzaNOqXcu2rdu3cOPKnUu3rt27ePPq3cu3r9+/gAMLHky4sOHDiBMrXsy4sePHkCNLnky5suXLmDNr3sy5s+fPoEOLHk26tOnTqFOrXs26tevXsGPL/uoihIUHDhgweBBhwgYXOFmI8KABA4UKGDh4MPECZwcEBggMCCBgQAEDDVTEdJHBwYIE4MP+i08QIcPMFyIwSFjPvv16DiJmqkAwAID9+/jtF0DQ0sWG7+MFON4GL70AAgXuJegeBSa8pMIBAeQnYX4BNKBSCQ4IqOF4DJTAkgkIKihiexiwwFIDEU6oIn4DdHDSfxvGKB6BKBk44o3ujZDSgyv2mJ8CJLlggYxEhmfBSS9wgOOS7MVnkgoF+CjlffyJNGSRWEZQUpJMdikBCCVBOeWYABwQ0gZYppnABySB4KWXDY50AJlkWuhRCQCqWaSHIY3w5psmhqQAnXS6yJELGeqJ5QIhvRDin0xWEJIKKRI6pQAdoalomuZ9JAKkb8bpEQKW0mlnRi7kuSmRC6zgUQv+j4K6JAUfkVBpqVIGsJGmq2JJI0duygqnR3PiWqdGD/SaJqMdVSCslxx4JICxZBaAqrJquroRC896SUFzG3VALZkBaHfRB9im+WtGn3bbpagZkTrumKdWNEG6WXKkpLtMOqlRlPNOWaVFEeBbpJYbOcvvktFuNG3AUlp7UbIGy8gARwovfKMGHD0McY8EYMRAxTIyq5HGS0q60cdSYnqRqiQHaDJGL6CMIwYbqcCyjwNglGjMAjrAUaw2u8fxRrfunF/IEwOt4QMcqVe0gh5wVJ/SExqA0ZVOjzcBRxpMrSCYGxGA9YRmntt1gJ1qFKzY7Z3AUbFn4wcBRi6sPZ7+thqlAHd7FLTA0QV14xcACRmNrHcCQjf793vSFq6fRhksnkDbG3nwuAT+bmSA5AAMfFGqerf66uMVgMsRCZILYG5GvDqNOUeaw63jR5/XDSRHigO9AHAf1Sx26iDpfLbrHZXQNZshmTA1BXKH1ADWAdztUewGHzlS7SjfLlLuLO/+0b0Vf12S1AuTTdLVEKcdEtfYRgA8SS+EzS8Iqo+kgtkBH/B6SJXD1uxK8jZZUcB7J6EbrgIgPpKUoHd6WgDzVOInA0ZPJYNaoPVQgj1W/cYlL+Belw6YP5WoAHxjYuD/UrKBn23oARng20tYAIKMjUgDIihhSzpwAI+tiACVCFghS/wzgQeMbAEO6E0GUICTFIhAAxpw1nGSI4JA2eQCCCAAAaZFHesgwFCzCaMYx0jGMprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTGVTAgIAOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='); + background-size: 30px 30px; + } + .content-item { + display: inline-block; + padding: 10px; + line-height: 1.3em; + @include boxShadow(0, + 0, + 4px, + 0, + rgba(0, + 20, + 66, + 0.2)); + } + .widget-link, + .widget-content-link { + display: inline-block; + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: $blueDark; + background-color: #fff; + border: 1px solid $blueDark; + padding: 10px; + margin: 5px 10px 5px 0; + width: 100%; + text-align: center; + box-sizing: border-box; + @include borderRadius(20px); + @include boxShadow(0, + 2px, + 4px, + 0, + rgba(0, + 20, + 66, + 0.3)); + @include transitionEase(); + text-decoration: none; + &:hover { + background-color: $blueDark; + color: #fff; + } + } + .widget-content-img { + display: inline-block; + height: auto; + max-width: 80%; + } + &.widget-bubble { + justify-content: flex-start; + .content-item { + @include borderRadiusMulti(10px, + 10px, + 10px, + 0); + background-color: $blueLight; + color: #fff; + word-break: break-word; + } + } + &.user-bubble { + justify-content: flex-end; + .content-item { + @include borderRadiusMulti(10px, + 10px, + 0, + 10px); + background-color: #fff; + color: #333; + } + } + } + } + /* widget FOOTER */ + #widget-main-footer { + height: auto; + padding: 20px 15px 10px 15px; + background: $blueLight; + align-items: center; + justify-content: center; + position: relative; + @include boxShadow(0, + 0, + 4px, + 0, + rgba(0, + 20, + 66, + 0.2)); + #widget-mic-btn { + display: inline-block; + height: 30px; + width: 30px; + @include transitionEase(); + @include borderRadius(30px); + background-color: $blueDark; + .icon { + display: inline-block; + width: 24px; + height: 24px; + @include maskImage("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 30 30' style='enable-background:new 0 0 30 30;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23FFFFFF;%7D %3C/style%3E %3Cpath class='st0' d='M14.7,19.6h0.6c2.9,0,5.2-2.3,5.2-5.1V7.7c0-2.8-2.3-5.2-5.2-5.2h-0.6c-2.9,0-5.2,2.3-5.2,5.1v6.8 C9.5,17.3,11.8,19.6,14.7,19.6z M14.7,4h0.6c1.9,0,3.5,1.4,3.7,3.3h-1.2C17.4,7.3,17,7.6,17,8c0,0.4,0.3,0.7,0.7,0.7H19v1.7h-2 c-0.4,0-0.7,0.3-0.7,0.7c0,0.4,0.3,0.7,0.7,0.7h2v1.7h-1.2c-0.4,0-0.7,0.3-0.7,0.7s0.3,0.7,0.7,0.7H19c-0.2,1.8-1.8,3.2-3.7,3.2 h-0.6c-1.9,0-3.5-1.4-3.7-3.2h1.2c0.4,0,0.7-0.3,0.7-0.7s-0.3-0.7-0.7-0.7H11v-1.7h2c0.4,0,0.7-0.3,0.7-0.7c0-0.4-0.3-0.7-0.7-0.7 h-2V8.7h1.2C12.6,8.7,13,8.4,13,8c0-0.4-0.3-0.7-0.7-0.7H11C11.2,5.4,12.8,4,14.7,4z'/%3E %3Cpath class='st0' d='M23.7,14.2c0-0.4-0.3-0.7-0.7-0.7s-0.7,0.3-0.7,0.7c0,3.9-3.2,7.1-7.2,7.1s-7.2-3.2-7.2-7.1 c0-0.4-0.3-0.7-0.7-0.7c-0.4,0-0.7,0.3-0.7,0.7c0,4.4,3.5,8.1,8,8.5V26h-4c-0.4,0-0.7,0.3-0.7,0.7s0.3,0.7,0.7,0.7h9.6 c0.4,0,0.7-0.3,0.7-0.7S20.2,26,19.8,26h-4v-3.3C20.2,22.3,23.7,18.7,23.7,14.2z'/%3E %3C/svg%3E"); + background-color: #fff; + margin: 3px; + @include transitionEase(); + } + &:hover { + background-color: #fff; + .icon { + background-color: $blueDark; + } + } + &.recording { + @include blinkWhiteToRed(); + .icon { + @include blinkRedToWhite(); + } + } + } + #widget-msg-btn { + display: inline-block; + height: 30px; + width: 30px; + @include transitionEase(); + @include borderRadius(20px); + background-color: #fff; + position: absolute; + top: 50%; + margin-top: -10px; + .icon { + display: inline-block; + width: 20px; + height: 20px; + @include maskImage("data:image/svg+xml,%3C?xml version='1.0' encoding='utf-8'?%3E %3C!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg version='1.1' id='Calque_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 16 16' style='enable-background:new 0 0 16 16;' xml:space='preserve'%3E %3Cstyle type='text/css'%3E .st0%7Bfill:%23055E89;%7D %3C/style%3E %3Cpath class='st0' d='M1,15.5c-0.1,0-0.3-0.1-0.4-0.1c-0.1-0.1-0.2-0.3-0.1-0.5l0.9-3.5c0-0.1,0.1-0.2,0.1-0.2L11.7,1 C12.6,0,14.1,0,15,1C16,1.9,16,3.4,15,4.3L4.9,14.5c-0.1,0.1-0.1,0.1-0.2,0.1l-3.5,0.9C1.1,15.5,1,15.5,1,15.5z M2.3,11.7l-0.6,2.6 l2.6-0.6l10-10c0.5-0.5,0.5-1.4,0-1.9c-0.5-0.5-1.4-0.5-1.9,0L2.3,11.7z'/%3E %3Cpath class='st0' d='M14.9,15.6H6.4c-0.3,0-0.6-0.3-0.6-0.6s0.3-0.6,0.6-0.6h8.5c0.3,0,0.6,0.3,0.6,0.6S15.2,15.6,14.9,15.6z'/%3E %3C/svg%3E"); + background-color: $blueLight; + margin: 5px; + @include transitionEase(); + &:hover { + background-color: $blueDark; + } + } + } + #chabtot-msg-input { + border: none; + background-color: #fff; + padding: 5px 10px; + @include borderRadius(15px); + width: auto; + min-width: 0; + margin-left: 10px; + @include transitionEase(); + outline: none; + font-size: 14px; + } + &.mic-enabled { + padding: 15px; + #widget-mic-btn { + width: 50px; + height: 50px; + .icon { + width: 34px; + height: 34px; + margin: 8px; + } + } + #widget-msg-btn { + top: 50%; + left: 50%; + margin-top: -15px; + margin-left: 30px; + } + #chabtot-msg-input { + display: none; + } + } + &.mic-disabled { + #widget-mic-btn { + width: 30px; + height: 30px + } + #widget-msg-btn { + left: 100%; + margin-left: -45px; + } + #chabtot-msg-input { + display: inline-block; + height: auto; + max-height: 150px; + min-height: 20px; + line-height: 20px; + padding: 5px 30px 5px 10px; + overflow: auto; + } + } + } + /* widget msg error */ + #chatbot-msg-error { + padding: 0 10px 10px 10px; + background-color: $blueLight; + color: $redDark; + z-index: 2; + justify-content: center; + font-size: 14px; + } + /* widget Settings */ + #widget-settings { + background: #fff; + padding: 20px; + .widget-settings-title { + display: inline-block; + font-size: 18px; + font-weight: 700; + color: #454545; + margin: 10px 0; + } + .widget-settings-checkbox { + margin: 10px 0; + .widget-settings-label { + font-size: 14px; + line-height: 18px; + padding-left: 5px; + font-weight: 500; + color: #333; + } + } + button { + display: inline-block; + padding: 10px 15px 8px 15px; + margin: 10px 0; + text-align: center; + font-size: 14px; + font-weight: 400; + color: #fff; + height: auto !important; + @include borderRadius(25px); + @include transitionEase(); + @include boxShadow(0, + 2px, + 4px, + 0, + rgba(0, + 20, + 66, + 0.2)); + font-family: $spartan; + } + input[type="checkbox"] { + margin: 0; + } + } + input[type="checkbox"] { + -webkit-appearance: checkbox; + padding: 0; + height: auto !important; + width: auto; + margin: 5px; + } + .widget-settings-btn-container { + justify-content: space-evenly; + #widget-settings-cancel { + background-color: #777; + &:hover { + background-color: #333; + } + } + #widget-settings-save { + background-color: $blueLight; + &:hover { + background-color: $blueDark; + } + } + } + #widget-quit-btn { + width: 100%; + background-color: $redLight; + &:hover { + background-color: $redDark; + } + } + /* Widget Minimal-streaming */ + #widget-minimal-overlay { + display: flex; + position: fixed; + width: 100%; + bottom: 0%; + left: 0; + background-color: rgba(0, 0, 0, 0.8); + align-items: center; + justify-content: center; + z-index: 900; + &.visible { + padding: 20px 0; + @include transition(all 0.3s ease); + overflow: visible; + } + &.hidden { + height: 0px; + @include transition(all 0.3s ease); + overflow: hidden; + } + .widget-ms-container { + justify-content: center; + max-width: 1400px; + } + #widget-ms-close { + display: inline-block; + width: 30px; + height: 30px; + position: absolute; + top: 20px; + left: 100%; + margin-left: -50px; + background-color: transparent; + @include borderRadius(50px); + z-index: 998; + border: none; + &:after { + content: ''; + display: inline-block; + width: 30px; + height: 30px; + position: absolute; + top: 0; + left: 0%; + border: none; + @include maskImage("data:image/svg+xml,%3C?xml version='1.0' encoding='UTF-8' standalone='no'?%3E %3C!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E %3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' version='1.1' id='Calque_1' x='0px' y='0px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve' sodipodi:docname='close.svg' inkscape:version='0.92.4 (5da689c313, 2019-01-14)'%3E%3Cmetadata id='metadata9'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage' /%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cdefs id='defs7' /%3E%3Csodipodi:namedview pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1' objecttolerance='10' gridtolerance='10' guidetolerance='10' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:window-width='1920' inkscape:window-height='1016' id='namedview5' showgrid='false' inkscape:zoom='7.8666667' inkscape:cx='-13.983051' inkscape:cy='14.745763' inkscape:window-x='0' inkscape:window-y='27' inkscape:window-maximized='1' inkscape:current-layer='Calque_1' /%3E %3Cpath d='m 22.684485,21.147587 -6.147589,-6.147588 6.147589,-6.1475867 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 L 21.916036,7.3155152 c -0.219556,-0.2195567 -0.548892,-0.2195567 -0.768448,0 L 15,13.463103 8.8524123,7.3155152 c -0.2195568,-0.2195567 -0.5488919,-0.2195567 -0.7684486,0 L 7.3155152,8.0839633 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 6.1475878,6.1475867 -6.1475878,6.147588 c -0.2195567,0.219557 -0.2195567,0.548892 0,0.768449 l 0.7684485,0.768449 c 0.2195567,0.219556 0.5488918,0.219556 0.7684486,0 L 15,16.536896 l 6.147588,6.147589 c 0.219556,0.219556 0.548892,0.219556 0.768448,0 l 0.768449,-0.768449 c 0.219556,-0.219557 0.219556,-0.548892 0,-0.768449 z' id='path2' inkscape:connector-curvature='0' style='fill:%23ed1c24;stroke-width:1.09778357' /%3E %3C/svg%3E"); + background-color: rgba(255, 255, 255, 0.7); + z-index: 999; + } + &:hover { + &:after { + background-color: #fff; + } + } + } + &.minimal-audio { + &.visible { + @include boxShadow(0, + 2px, + 6px, + 0, + rgba(0, + 0, + 0, + 0.3)); + width: 100px; + height: 100px; + left: 50%; + bottom: 20px; + margin-left: -50px; + @include borderRadius(50px); + padding: 0; + } + #widget-ms-close { + top: -10px; + left: 100%; + margin: 0; + background-color: rgba(0, 0, 0, 0.8); + z-index: 901; + } + } + } + .widget-animation { + width: 100px; + height: 100px; + position: relative; + padding: 0; + margin: 0; + } + .widget-ms-content { + font-family: $spartan; + .widget-ms-content-current { + justify-content: center; + font-size: 25px; + font-weight: 500; + color: #fff; + height: 80px; + padding: 0 40px; + } + .widget-ms-content-previous { + display: inline-block; + font-size: 20px; + font-weight: 400; + color: #939393; + height: 20px; + padding: 0 40px; + } + } + .hidden { + display: none !important; + } +} + +#widget-error-message { + position: absolute; + top: 0; + left: -220px; + width: 200px; + text-align: left; + background: $redDark; + color: #fff; + padding: 10px; + font-size: 14px; + @include borderRadius(5px); +} \ No newline at end of file diff --git a/client/web/src/assets/scss/mixin.scss b/client/web/src/assets/scss/mixin.scss new file mode 100644 index 0000000..bee9513 --- /dev/null +++ b/client/web/src/assets/scss/mixin.scss @@ -0,0 +1,125 @@ +@mixin borderRadius($value) { + -webkit-border-radius: $value; + -moz-border-radius: $value; + border-radius: $value; +} + +@mixin borderRadiusMulti($topleft, $topright, $botright, $botleft) { + -webkit-border-top-left-radius: $topleft; + -moz-border-radius-topleft: $topleft; + border-top-left-radius: $topleft; + -webkit-border-top-right-radius: $topright; + -moz-border-radius-topright: $topright; + border-top-right-radius: $topright; + -webkit-border-bottom-right-radius: $botright; + -moz-border-radius-bottomright: $botright; + border-bottom-right-radius: $botright; + -webkit-border-bottom-left-radius: $botleft; + -moz-border-radius-bottomleft: $botleft; + border-bottom-left-radius: $botleft; +} + +@mixin boxShadow($offsetX, $offsetY, $blurRadius, $spreadRadius, $color) { + -moz-box-shadow: $offsetX $offsetY $blurRadius $spreadRadius $color; + -webkit-box-shadow: $offsetX $offsetY $blurRadius $spreadRadius $color; + box-shadow: $offsetX $offsetY $blurRadius $spreadRadius $color; +} + +@mixin noShadow() { + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +@mixin maskImage($path) { + mask-image: url($path); + -webkit-mask-image: url($path); + mask-size: cover; + -webkit-mask-size: cover; + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; +} + +@mixin transition($value) { + -webkit-transition: $value; + -moz-transition: $value; + -o-transition: $value; + transition: $value; +} + +@mixin transitionEase() { + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +@mixin transform($value) { + -webkit-transform: $value; + -o-transform: $value; + transform: $value; +} + +@-webkit-keyframes blinkRedWhite { + 0% { + background-color: #fff; + } + 50% { + background-color: #ec5a5a; + } + 100% { + background-color: #fff; + } +} + +@keyframes blinkRedWhite { + 0% { + background-color: #fff; + } + 50% { + background-color: #ec5a5a; + } + 100% { + background-color: #fff; + } +} + +@mixin blinkRedToWhite { + -webkit-animation: blinkRedWhite 2s infinite; + -moz-animation: blinkRedWhite 2s infinite; + -ms-animation: blinkRedWhite 2s infinite; + -o-animation: blinkRedWhite 2s infinite; + animation: blinkRedWhite 2s infinite; +} + +@-webkit-keyframes blinkWhiteRed { + 0% { + background-color: #ec5a5a; + } + 50% { + background-color: #fff; + } + 100% { + background-color: #ec5a5a; + } +} + +@keyframes blinkWhiteRed { + 0% { + background-color: #ec5a5a; + } + 50% { + background-color: #fff; + } + 100% { + background-color: #ec5a5a; + } +} + +@mixin blinkWhiteToRed { + -webkit-animation: blinkWhiteRed 2s infinite; + -moz-animation: blinkWhiteRed 2s infinite; + -ms-animation: blinkWhiteRed 2s infinite; + -o-animation: blinkWhiteRed 2s infinite; + animation: blinkWhiteRed 2s infinite; +} \ No newline at end of file diff --git a/client/web/src/assets/scss/styles.scss b/client/web/src/assets/scss/styles.scss new file mode 100644 index 0000000..49cb898 --- /dev/null +++ b/client/web/src/assets/scss/styles.scss @@ -0,0 +1,3 @@ +body { + background: #ccc; +} \ No newline at end of file diff --git a/client/web/src/assets/template/widget-default.html b/client/web/src/assets/template/widget-default.html new file mode 100644 index 0000000..3047834 --- /dev/null +++ b/client/web/src/assets/template/widget-default.html @@ -0,0 +1,98 @@ +
+
+ + + + +
+ + + +
\ No newline at end of file diff --git a/client/web/src/audio.js b/client/web/src/audio.js new file mode 100644 index 0000000..9a5d86e --- /dev/null +++ b/client/web/src/audio.js @@ -0,0 +1,98 @@ +import WebVoiceSDK from "@linto-ai/webvoicesdk" +import base64Js from "base64-js" + +export default class Audio extends EventTarget { + constructor( + isMobile, + useHotword = true, + hotwordModel = "linto", + threshold = 0.99, + mobileConstraintsOverrides = { + echoCancellation: false, + autoGainControl: false, + noiseSuppression: false, + } + ) { + super() + this.useHotword = useHotword + this.hotwordModel = hotwordModel + this.threshold = threshold + if (isMobile) { + this.mic = new webVoiceSDK.Mic({ + sampleRate: 44100, + frameSize: 4096, + constraints: mobileConstraintsOverrides, + }) + } else { + this.mic = new webVoiceSDK.Mic() // uses webVoiceSDK.Mic.defaultOptions + } + this.downSampler = new WebVoiceSDK.DownSampler() + this.vad = new WebVoiceSDK.Vad({ + numActivations: 10, + threshold: 0.85, + timeAfterStop: 2000, + }) + this.speechPreemphaser = new WebVoiceSDK.SpeechPreemphaser() + this.featuresExtractor = new WebVoiceSDK.FeaturesExtractor() + this.hotword = new WebVoiceSDK.Hotword() + this.recorder = new WebVoiceSDK.Recorder() + this.start() + } + + async start() { + try { + await this.mic.start() + await this.downSampler.start(this.mic) + await this.speechPreemphaser.start(this.downSampler) + await this.featuresExtractor.start(this.speechPreemphaser) + if (this.useHotword) { + await this.vad.start(this.mic) + await this.hotword.start( + this.featuresExtractor, + this.vad, + this.threshold + ) + await this.hotword.loadModel( + this.hotword.availableModels[this.hotwordModel] + ) + } + await this.recorder.start(this.downSampler) + } catch (e) { + console.log(e) + } + } + + async stop() { + await this.downSampler.stop() + await this.speechPreemphaser.stop() + await this.featuresExtractor.stop() + await this.recorder.stop() + if (this.useHotword) { + await this.hotword.stop() + await this.vad.stop() + } + await this.mic.stop() + } + + pause() { + this.mic.pause() + } + + resume() { + this.mic.resume() + } + + async listenCommand() { + this.recorder.punchIn() + } + + async getCommand() { + const audioBlob = this.recorder.punchOut() + const audioBuffer = await fetch(audioBlob, { + method: "GET", + }) + const audioArrayBuffer = await audioBuffer.arrayBuffer() + const vue = new Int8Array(audioArrayBuffer) + return base64Js.fromByteArray(vue) + } +} diff --git a/client/web/src/handlers/audio.js b/client/web/src/handlers/audio.js new file mode 100644 index 0000000..988de00 --- /dev/null +++ b/client/web/src/handlers/audio.js @@ -0,0 +1,3 @@ +export function streamingHandler(){ + +} \ No newline at end of file diff --git a/client/web/src/handlers/linto-ui.js b/client/web/src/handlers/linto-ui.js new file mode 100644 index 0000000..f736322 --- /dev/null +++ b/client/web/src/handlers/linto-ui.js @@ -0,0 +1,364 @@ +export function mqttConnectHandler(event) { + if (this.debug) { + console.log("MQTT: connected") + } +} +export function mqttConnectFailHandler(event) { + if (this.debug) { + console.log("MQTT: failed to connect") + console.log(event) + } +} +export function mqttErrorHandler(event) { + if (this.debug) { + console.log("MQTT: error") + console.log(event.detail) + } +} +export function mqttDisconnectHandler(event) { + if (this.debug) { + console.log("MQTT: Offline") + } +} +export function audioSpeakingOn(event) { + if (this.debug) { + console.log("Speaking") + } +} +export function audioSpeakingOff(event) { + if (this.debug) { + console.log("Not speaking") + } +} +export function commandAcquired(event) { + if (this.debug) { + console.log("Command acquired", event) + } +} +export function commandPublished(event) { + if (this.debug) { + console.log("Command published id :", event.detail) + } +} +export function hotword(event) { + if (this.debug) { + console.log("Hotword triggered : ", event.detail) + } + if (this.hotwordEnabled && this.widgetState === "waiting") { + this.widgetState = "listening" + if (this.widgetMode === "minimal-streaming") { + this.closeWidget() + this.openMinimalOverlay() + this.setMinimalOverlayAnimation("listening") + } else { + this.openWidget() + } + + const widgetFooter = document.getElementById("widget-main-footer") + const txtBtn = document.getElementById("widget-msg-btn") + if (widgetFooter.classList.contains("mic-disabled")) { + txtBtn.classList.remove("txt-enabled") + txtBtn.classList.add("txt-disabled") + widgetFooter.classList.remove("mic-disabled") + widgetFooter.classList.add("mic-enabled") + } + } +} +export function commandTimeout(event) { + if (this.debug) { + console.log("Command timeout, id : ", event.detail) + } +} +export async function sayFeedback(event) { + if (this.debug) { + console.log( + "Saying : ", + event.detail.behavior.say.text, + " ---> Answer to : ", + event.detail.transcript + ) + } + this.setWidgetBubbleContent(event.detail.behavior.say.text) + + const mainContent = document.getElementById("widget-ms-content-current") + this.setMinimalOverlaySecondaryContent(mainContent.innerHTML) + this.setMinimalOverlayMainContent(event.detail.behavior.say.text) + + await this.widgetSay(event.detail.behavior.say.text) +} + +export function streamingChunk(event) { + if (this.widgetState === "listening") { + // VAD + if (this.streamingMode === "vad") { + if (event.detail.behavior.streaming.partial) { + if (this.debug) { + console.log( + "Streaming chunk received : ", + event.detail.behavior.streaming.partial + ) + } + this.streamingContent = event.detail.behavior.streaming.partial + this.setUserBubbleContent(this.streamingContent) + if (this.widgetMode === "minimal-streaming") { + this.setMinimalOverlayMainContent(this.streamingContent) + } + this.widgetContentScrollBottom() + } + if ( + event.detail.behavior.streaming.text || + event.detail.behavior.streaming.text === "" + ) { + if (this.debug) { + console.log( + "Streaming utterance completed : ", + event.detail.behavior.streaming.text + ) + } + this.linto.stopStreaming() + if (this.streamingContent !== "") { + this.setUserBubbleContent(event.detail.behavior.streaming.text) + this.createWidgetBubble() + if (this.widgetMode === "minimal-streaming") { + this.setMinimalOverlayMainContent( + event.detail.behavior.streaming.text + ) + this.setMinimalOverlayAnimation("thinking") + } + //this.linto.sendCommandText(event.detail.behavior.streaming.text) + this.sendText(event.detail.behavior.streaming.text) + this.streamingContent = "" + this.widgetState = "treating" + } else { + if (this.widgetMode === "minimal-streaming") { + setTimeout(() => { + this.closeMinimalOverlay() + this.widgetState = "waiting" + }, 2000) + } + } + } + } + } else { + // VAD CUSTOM + if (this.streamingMode === "vad-custom" && this.writingTarget !== null) { + if (event.detail.behavior.streaming.partial) { + if (this.debug) { + console.log( + "Streaming chunk received : ", + event.detail.behavior.streaming.partial + ) + } + this.streamingContent = event.detail.behavior.streaming.partial + this.writingTarget.innerHTML = this.streamingContent + } + if (event.detail.behavior.streaming.text) { + if (this.debug) { + console.log( + "Streaming utterance completed : ", + event.detail.behavior.streaming.text + ) + } + this.streamingContent = event.detail.behavior.streaming.text + + this.writingTarget.innerHTML = this.streamingContent + this.linto.stopStreaming() + this.linto.startStreamingPipeline() + this.widgetContentScrollBottom() + this.streamingContent = "" + this.widgetState = "waiting" + } + } + // STREAMING + STOP WORD ("stop") + else if (this.streamingMode === "infinite" && this.writingTarget !== null) { + if (event.detail.behavior.streaming.partial) { + if (this.debug) { + console.log( + "Streaming chunk received : ", + event.detail.behavior.streaming.partial + ) + } + if ( + event.detail.behavior.streaming.partial !== this.streamingStopWord + ) { + this.writingTarget.innerHTML = + this.streamingContent + + (this.streamingContent.length > 0 ? "\n" : "") + + event.detail.behavior.streaming.partial + } + } + if (event.detail.behavior.streaming.text) { + if (this.debug) { + console.log( + "Streaming utterance completed : ", + event.detail.behavior.streaming.text + ) + } + if (event.detail.behavior.streaming.text === this.streamingStopWord) { + this.linto.stopStreaming() + this.linto.startStreamingPipeline() + this.streamingContent = "" + this.widgetState = "waiting" + } else { + this.streamingContent += + (this.streamingContent.length > 0 ? "\n" : "") + + event.detail.behavior.streaming.text + this.writingTarget.innerHTML = this.streamingContent + } + } + } + } +} + +export function streamingStart(event) { + this.beep.play() + if (this.debug) { + console.log("Streaming started with no errors") + } + const micBtn = document.getElementById("widget-mic-btn") + micBtn.classList.add("recording") + this.cleanUserBubble() + this.createUserBubble() +} + +export function streamingStop(event) { + if (this.debug) { + console.log("Streaming stop") + } + this.cleanUserBubble() + this.streamingMode = "vad" + this.writingTarget = null + const micBtn = document.getElementById("widget-mic-btn") + micBtn.classList.remove("recording") +} +export function streamingFinal(event) { + if (this.debug) { + console.log( + "Streaming ended, here's the final transcript : ", + event.detail.behavior.streaming.result + ) + } +} +export function streamingFail(event) { + if (this.debug) { + console.log("Streaming cannot start : ", event.detail) + } + if (event.detail.behavior.streaming.status === "chunk") { + this.linto.stopStreaming() + this.linto.stopStreamingPipeline() + } + this.cleanUserBubble() + + if (this.widgetMode === "multi-modal") this.closeWidget() + if (this.widgetMode === "minimal-streaming") this.closeMinimalOverlay() + + const micBtn = document.getElementById("widget-mic-btn") + if (micBtn.classList.contains("recording")) { + micBtn.classList.remove("recording") + } + + this.setWidgetRightCornerAnimation("error") + this.widgetRightCornerAnimation.onComplete = () => { + this.linto.startStreamingPipeline() + setTimeout(() => { + this.setWidgetRightCornerAnimation("awake") + }, 500) + } +} +export function textPublished(e) { + if (this.debug) { + console.log("textPublished", e) + } +} +export function chatbotAcquired(e) { + if (this.debug) { + console.log("chatbotAcquired", e) + } +} +export function chatbotPublished(e) { + if (this.debug) { + console.log("chatbotPublished", e) + } +} +export function actionPublished(e) { + if (this.debug) { + console.log("actionPublished", e) + } +} +export function actionFeedback(e) { + if (this.debug) { + console.log("actionFeedback", e) + } +} +export async function customHandler(e) { + if (this.debug) { + console.log("customHandler", e) + } + this.cleanWidgetBubble() + this.closeMinimalOverlay() + this.widgetState = "waiting" +} +export async function askFeedback(e) { + if (this.debug) { + console.log("Ask feedback", e) + } + if (!!e.detail && !!e.detail.behavior) { + let ask = e.detail.behavior?.ask + let answer = e.detail.behavior?.answer + if (answer?.say) { + this.setWidgetBubbleContent(answer.say.text) + } + if (answer?.data) { + this.setFeedbackData(answer.data) + } + + if (this.widgetMode === "minimal-streaming") { + this.setMinimalOverlaySecondaryContent(ask) + if (answer?.say) { + this.setMinimalOverlayMainContent(answer?.say.text) + } else { + setTimeout(() => { + this.closeMinimalOverlay() + }, 3000) + } + this.setMinimalOverlayAnimation("talking") + } + if (answer?.say) { + await this.widgetSay(answer.say.text) + } + + this.widgetState = "waiting" + } +} +export async function widgetFeedback(e) { + if (this.debug) { + console.log("chatbot feedback", e) + } + + let responseObj = e?.detail?.behavior?.chatbot + ? e.detail.behavior.chatbot + : e?.detail?.behavior + + let say = responseObj?.say?.text + let question = responseObj?.question + let data = responseObj?.data + + // Say response + if (say && !this.stringIsHTML(say) && !Array.isArray(data) && say !== "") { + this.setWidgetBubbleContent(say) + } + + if (this.widgetMode === "minimal-streaming") { + this.setMinimalOverlaySecondaryContent(question) + this.setMinimalOverlayMainContent(say) + this.setMinimalOverlayAnimation("talking") + } + + // Set Data + if (data) this.setFeedbackData(data) + + if (!this.stringIsHTML(say) && say !== "") { + await this.widgetSay(say) + } + this.widgetState = "waiting" +} diff --git a/client/web/src/handlers/linto.js b/client/web/src/handlers/linto.js new file mode 100644 index 0000000..0210cc3 --- /dev/null +++ b/client/web/src/handlers/linto.js @@ -0,0 +1,193 @@ +export function mqttConnect(event) { + this.dispatchEvent(new CustomEvent("mqtt_connect", event)) +} + +export function vadStatus(event) { + event.detail + ? this.dispatchEvent(new CustomEvent("speaking_on")) + : this.dispatchEvent(new CustomEvent("speaking_off")) +} + +export function hotwordCommandBuffer(hotWordEvent) { + this.dispatchEvent(new CustomEvent("hotword_on", hotWordEvent)) + const whenSpeakingOff = async () => { + await this.sendCommandBuffer() + this.removeEventListener("speaking_off", whenSpeakingOff) + this.audio.hotword.resume() + } + this.listenCommand() + this.audio.hotword.pause() + this.addEventListener("speaking_off", whenSpeakingOff) +} + +export function hotwordStreaming(hotWordEvent) { + this.dispatchEvent(new CustomEvent("hotword_on", hotWordEvent)) + this.startStreaming() + const whenSpeakingOff = async () => { + this.stopStreaming() + this.removeEventListener("speaking_off", whenSpeakingOff) + this.audio.hotword.resume() + } + this.listenCommand() + this.audio.hotword.pause() + this.addEventListener("speaking_off", whenSpeakingOff) +} + +export async function nlpAnswer(event) { + if (event.detail.behavior.chatbot) { + this.dispatchEvent( + new CustomEvent("chatbot_feedback_from_skill", { + detail: event.detail, + }) + ) + return // Might handle custom_action say or ask, so we just exit here. + } + + if (event.detail.behavior.customAction) { + this.dispatchEvent( + new CustomEvent("custom_action_from_skill", { + detail: event.detail, + }) + ) + return // Might handle custom_action say or ask, so we just exit here. + } + + if (event.detail.behavior.say) { + this.dispatchEvent( + new CustomEvent("say_feedback_from_skill", { + detail: event.detail, + }) + ) + return + } + + if (event.detail.behavior.ask) { + this.dispatchEvent( + new CustomEvent("ask_feedback_from_skill", { + detail: event.detail, + }) + ) + } +} + +export async function chatbotAnswer(event) { + if (event?.detail?.behavior?.chatbot) { + this.dispatchEvent( + new CustomEvent("chatbot_feedback", { + detail: event.detail, + }) + ) + } else { + this.dispatchEvent( + new CustomEvent("chatbot_error", { + detail: event.detail, + }) + ) + } + return +} + +export async function actionAnswer(event) { + if (event.detail.behavior) { + this.dispatchEvent( + new CustomEvent("action_feedback", { + detail: event.detail, + }) + ) + return + } else { + this.dispatchEvent( + new CustomEvent("action_error", { + detail: event.detail, + }) + ) + return + } +} + +// Might be an error +export function streamingStartAck(event) { + this.streamingPublishHandler = streamingPublish.bind(this) + if (event.detail.behavior.streaming.status == "started") { + this.audio.downSampler.addEventListener( + "downSamplerFrame", + this.streamingPublishHandler + ) + this.dispatchEvent( + new CustomEvent("streaming_start", { + detail: event.detail, + }) + ) + } else { + this.dispatchEvent( + new CustomEvent("streaming_fail", { + detail: event.detail, + }) + ) + } +} + +export function streamingStopAck(event) { + this.dispatchEvent( + new CustomEvent("streaming_stop", { + detail: event.detail, + }) + ) +} + +export function streamingChunk(event) { + this.dispatchEvent( + new CustomEvent("streaming_chunk", { + detail: event.detail, + }) + ) +} + +export function streamingFinal(event) { + this.dispatchEvent( + new CustomEvent("streaming_final", { + detail: event.detail, + }) + ) +} + +export function streamingFail(event) { + this.dispatchEvent( + new CustomEvent("streaming_fail", { + detail: event.detail, + }) + ) +} + +export function ttsLangAction(event) { + this.setTTSLang(event.detail.value) +} + +export function mqttConnectFail(event) { + this.dispatchEvent( + new CustomEvent("mqtt_connect_fail", { + detail: event.detail, + }) + ) +} + +export function mqttError(event) { + this.dispatchEvent( + new CustomEvent("mqtt_error", { + detail: event.detail, + }) + ) +} + +export function mqttDisconnect(event) { + this.dispatchEvent( + new CustomEvent("mqtt_disconnect", { + detail: event.detail, + }) + ) +} + +// Local +function streamingPublish(event) { + this.mqtt.publishStreamingChunk(event.detail) +} diff --git a/client/web/src/handlers/mqtt.js b/client/web/src/handlers/mqtt.js new file mode 100644 index 0000000..da9dc64 --- /dev/null +++ b/client/web/src/handlers/mqtt.js @@ -0,0 +1,150 @@ +export async function mqttConnect() { + //clear any previous subs + this.client.unsubscribe(this.ingress) + this.client.subscribe(this.ingress, async (e) => { + if (!e) { + let payload = { + connexion: "online", + on: new Date().toJSON(), + } + try { + await this.publish("status", payload, 2, false, true) + this.dispatchEvent(new CustomEvent("mqtt_connect")) + } catch (err) { + this.dispatchEvent( + new CustomEvent("mqtt_connect_fail", { + detail: err, + }) + ) + } + } else { + this.dispatchEvent( + new CustomEvent("mqtt_connect_fail", { + detail: e, + }) + ) + } + }) +} + +export function mqttMessage(topic, payload) { + try { + // exemple topic appa62499241959338bdba1e118d6988f4d/tolinto/WEB_c3dSEMd014aE/nlp/file/eiydaeji + const topicArray = topic.split("/") + const command = topicArray[3] // i.e nlp + const message = new Object() + message.payload = JSON.parse(payload.toString()) + switch (command) { + // Command pipeline answers for ${clientCode}/tolinto/${sessionId}/nlp/file/${fileId} + + case "nlp": + this.pendingCommandIds = this.pendingCommandIds.filter( + (element) => element !== topicArray[5] + ) //removes from array of files to process + // Say is the final step of a ask/ask/.../say transaction + if (message.payload.behavior.say) this.conversationData = {} + // otherwise sets local conversation data to the received value + else if (message.payload.behavior.ask) + this.conversationData = message.payload.behavior.conversationData + + this.dispatchEvent( + new CustomEvent(command, { + detail: message.payload, + }) + ) + break + case "chatbot": + this.pendingCommandIds = this.pendingCommandIds.filter( + (element) => element !== topicArray[4] + ) //removes from array of files to process + this.dispatchEvent( + new CustomEvent("chatbot_feedback", { + detail: message.payload, + }) + ) + break + case "customAction": + this.dispatchEvent( + new CustomEvent("action_feedback", { + detail: message.payload, + }) + ) + break + // Received on connection tolinto/${sessionId}/tts_lang/ + case "tts_lang": + this.dispatchEvent( + new CustomEvent(command, { + detail: message.payload, + }) + ) + break + case "streaming": + if (topicArray[4] == "start") { + this.dispatchEvent( + new CustomEvent("streaming_start_ack", { + detail: message.payload, + }) + ) + } + if (topicArray[4] == "stop") { + this.dispatchEvent( + new CustomEvent("streaming_stop_ack", { + detail: message.payload, + }) + ) + } + if (topicArray[4] == "chunk") { + this.dispatchEvent( + new CustomEvent("streaming_chunk", { + detail: message.payload, + }) + ) + } + if (topicArray[4] == "final") { + this.dispatchEvent( + new CustomEvent("streaming_final", { + detail: message.payload, + }) + ) + } + if (topicArray[4] == "error") { + this.dispatchEvent( + new CustomEvent("streaming_fail", { + detail: message.payload, + }) + ) + } + break + } + } catch (e) { + this.dispatchEvent( + new CustomEvent("mqtt_error", { + detail: e, + }) + ) + } +} + +export function mqttDisconnect(e) { + this.dispatchEvent( + new CustomEvent("mqtt_disconnect", { + detail: e, + }) + ) +} + +export function mqttOffline(e) { + this.dispatchEvent( + new CustomEvent("mqtt_disconnect", { + detail: e, + }) + ) +} + +export function mqttError(e) { + this.dispatchEvent( + new CustomEvent("mqtt_error", { + detail: e, + }) + ) +} diff --git a/client/web/src/lib/lottie.min.js b/client/web/src/lib/lottie.min.js new file mode 100644 index 0000000..18a90bf --- /dev/null +++ b/client/web/src/lib/lottie.min.js @@ -0,0 +1,15 @@ +(typeof navigator !== "undefined") && (function(root, factory) { + if (typeof define === "function" && define.amd) { + define(function() { + return factory(root); + }); + } else if (typeof module === "object" && module.exports) { + module.exports = factory(root); + } else { + root.lottie = factory(root); + root.bodymovin = root.lottie; + } +}((window || {}), function(window) { + "use strict";var svgNS="http://www.w3.org/2000/svg",locationHref="",initialDefaultFrame=-999999,subframeEnabled=!0,idPrefix="",expressionsPlugin,isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),cachedColors={},bmRnd,bmPow=Math.pow,bmSqrt=Math.sqrt,bmFloor=Math.floor,bmMax=Math.max,bmMin=Math.min,BMMath={};function ProjectInterface(){return{}}!function(){var t,e=["abs","acos","acosh","asin","asinh","atan","atanh","atan2","ceil","cbrt","expm1","clz32","cos","cosh","exp","floor","fround","hypot","imul","log","log1p","log2","log10","max","min","pow","random","round","sign","sin","sinh","sqrt","tan","tanh","trunc","E","LN10","LN2","LOG10E","LOG2E","PI","SQRT1_2","SQRT2"],r=e.length;for(t=0;t>>=1;return(t+r)/e};return n.int32=function(){return 0|a.g(4)},n.quick=function(){return a.g(4)/4294967296},n.double=n,P(E(a.S),o),(e.pass||r||function(t,e,r,i){return i&&(i.S&&b(i,a),t.state=function(){return b(a,{})}),r?(h[c]=t,e):t})(n,s,"global"in e?e.global:this==h,e.state)},P(h.random(),o)}([],BMMath);var BezierFactory=function(){var t={getBezierEasing:function(t,e,r,i,s){var a=s||("bez_"+t+"_"+e+"_"+r+"_"+i).replace(/\./g,"p");if(o[a])return o[a];var n=new h([t,e,r,i]);return o[a]=n}},o={};var l=11,p=1/(l-1),e="function"==typeof Float32Array;function i(t,e){return 1-3*e+3*t}function s(t,e){return 3*e-6*t}function a(t){return 3*t}function m(t,e,r){return((i(e,r)*t+s(e,r))*t+a(e))*t}function f(t,e,r){return 3*i(e,r)*t*t+2*s(e,r)*t+a(e)}function h(t){this._p=t,this._mSampleValues=e?new Float32Array(l):new Array(l),this._precomputed=!1,this.get=this.get.bind(this)}return h.prototype={get:function(t){var e=this._p[0],r=this._p[1],i=this._p[2],s=this._p[3];return this._precomputed||this._precompute(),e===r&&i===s?t:0===t?0:1===t?1:m(this._getTForX(t),r,s)},_precompute:function(){var t=this._p[0],e=this._p[1],r=this._p[2],i=this._p[3];this._precomputed=!0,t===e&&r===i||this._calcSampleValues()},_calcSampleValues:function(){for(var t=this._p[0],e=this._p[2],r=0;rn?-1:1,l=!0;l;)if(i[a]<=n&&i[a+1]>n?(o=(n-i[a])/(i[a+1]-i[a]),l=!1):a+=h,a<0||s-1<=a){if(a===s-1)return r[a];l=!1}return r[a]+(r[a+1]-r[a])*o}var F=createTypedArray("float32",8);return{getSegmentsLength:function(t){var e,r=segmentsLengthPool.newElement(),i=t.c,s=t.v,a=t.o,n=t.i,o=t._length,h=r.lengths,l=0;for(e=0;er[0]||!(r[0]>t[0])&&(t[1]>r[1]||!(r[1]>t[1])&&(t[2]>r[2]||!(r[2]>t[2])&&null))}var h,r=function(){var i=[4,4,14];function s(t){var e,r,i,s=t.length;for(e=0;e=a.t-i){s.h&&(s=a),f=0;break}if(a.t-i>t){f=c;break}c=r&&r<=t||this._caching.lastFrame=t&&(this._caching._lastKeyframeIndex=-1,this._caching.lastIndex=0);var i=this.interpolateValue(t,this._caching);this.pv=i}return this._caching.lastFrame=t,this.pv}function d(t){var e;if("unidimensional"===this.propType)e=t*this.mult,1e-5=this.p.keyframes[this.p.keyframes.length-1].t?(r=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/e,0),this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/e,0)):(r=this.p.pv,this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/e,this.p.offsetTime));else if(this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime){r=[],i=[];var s=this.px,a=this.py;s._caching.lastFrame+s.offsetTime<=s.keyframes[0].t?(r[0]=s.getValueAtTime((s.keyframes[0].t+.01)/e,0),r[1]=a.getValueAtTime((a.keyframes[0].t+.01)/e,0),i[0]=s.getValueAtTime(s.keyframes[0].t/e,0),i[1]=a.getValueAtTime(a.keyframes[0].t/e,0)):s._caching.lastFrame+s.offsetTime>=s.keyframes[s.keyframes.length-1].t?(r[0]=s.getValueAtTime(s.keyframes[s.keyframes.length-1].t/e,0),r[1]=a.getValueAtTime(a.keyframes[a.keyframes.length-1].t/e,0),i[0]=s.getValueAtTime((s.keyframes[s.keyframes.length-1].t-.01)/e,0),i[1]=a.getValueAtTime((a.keyframes[a.keyframes.length-1].t-.01)/e,0)):(r=[s.pv,a.pv],i[0]=s.getValueAtTime((s._caching.lastFrame+s.offsetTime-.01)/e,s.offsetTime),i[1]=a.getValueAtTime((a._caching.lastFrame+a.offsetTime-.01)/e,a.offsetTime))}else r=i=n;this.v.rotate(-Math.atan2(r[1]-i[1],r[0]-i[0]))}this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])}this.frameId=this.elem.globalData.frameId}},precalculateMatrix:function(){if(!this.a.k&&(this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1,!this.s.effectsSequence.length)){if(this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2,this.sk){if(this.sk.effectsSequence.length||this.sa.effectsSequence.length)return;this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3}this.r?this.r.effectsSequence.length||(this.pre.rotate(-this.r.v),this.appliedTransformations=4):this.rz.effectsSequence.length||this.ry.effectsSequence.length||this.rx.effectsSequence.length||this.or.effectsSequence.length||(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}},autoOrient:function(){}},extendPrototype([DynamicPropertyContainer],i),i.prototype.addDynamicProperty=function(t){this._addDynamicProperty(t),this.elem.addDynamicProperty(t),this._isDirty=!0},i.prototype._addDynamicProperty=DynamicPropertyContainer.prototype.addDynamicProperty,{getTransformProperty:function(t,e,r){return new i(t,e,r)}}}();function ShapePath(){this.c=!1,this._length=0,this._maxLength=8,this.v=createSizedArray(this._maxLength),this.o=createSizedArray(this._maxLength),this.i=createSizedArray(this._maxLength)}ShapePath.prototype.setPathData=function(t,e){this.c=t,this.setLength(e);for(var r=0;r=this._maxLength&&this.doubleArrayLength(),r){case"v":a=this.v;break;case"i":a=this.i;break;case"o":a=this.o;break;default:a=[]}(!a[i]||a[i]&&!s)&&(a[i]=pointPool.newElement()),a[i][0]=t,a[i][1]=e},ShapePath.prototype.setTripleAt=function(t,e,r,i,s,a,n,o){this.setXYAt(t,e,"v",n,o),this.setXYAt(r,i,"o",n,o),this.setXYAt(s,a,"i",n,o)},ShapePath.prototype.reverse=function(){var t=new ShapePath;t.setPathData(this.c,this._length);var e=this.v,r=this.o,i=this.i,s=0;this.c&&(t.setTripleAt(e[0][0],e[0][1],i[0][0],i[0][1],r[0][0],r[0][1],0,!1),s=1);var a,n=this._length-1,o=this._length;for(a=s;a=c[c.length-1].t-this.offsetTime)i=c[c.length-1].s?c[c.length-1].s[0]:c[c.length-2].e[0],a=!0;else{for(var d,u,y=f,g=c.length-1,v=!0;v&&(d=c[y],!((u=c[y+1]).t-this.offsetTime>t));)y=u.t-this.offsetTime)p=1;else if(ti+r))p=o.s*s<=i?0:(o.s*s-i)/r,m=o.e*s>=i+r?1:(o.e*s-i)/r,h.push([p,m])}return h.length||h.push([0,0]),h},TrimModifier.prototype.releasePathsData=function(t){var e,r=t.length;for(e=0;ee.e){r.c=!1;break}e.s<=d&&e.e>=d+n.addedLength?(this.addSegment(f[i].v[s-1],f[i].o[s-1],f[i].i[s],f[i].v[s],r,o,y),y=!1):(l=bez.getNewSegment(f[i].v[s-1],f[i].v[s],f[i].o[s-1],f[i].i[s],(e.s-d)/n.addedLength,(e.e-d)/n.addedLength,h[s-1]),this.addSegmentFromArray(l,r,o,y),y=!1,r.c=!1),d+=n.addedLength,o+=1}if(f[i].c&&h.length){if(n=h[s-1],d<=e.e){var g=h[s-1].addedLength;e.s<=d&&e.e>=d+g?(this.addSegment(f[i].v[s-1],f[i].o[s-1],f[i].i[0],f[i].v[0],r,o,y),y=!1):(l=bez.getNewSegment(f[i].v[s-1],f[i].v[0],f[i].o[s-1],f[i].i[0],(e.s-d)/g,(e.e-d)/g,h[s-1]),this.addSegmentFromArray(l,r,o,y),y=!1,r.c=!1)}else r.c=!1;d+=n.addedLength,o+=1}if(r._length&&(r.setXYAt(r.v[p][0],r.v[p][1],"i",p),r.setXYAt(r.v[r._length-1][0],r.v[r._length-1][1],"o",r._length-1)),d>e.e)break;i=d.length&&(m=0,d=u[f+=1]?u[f].points:P.v.c?u[f=m=0].points:(l-=h.partialLength,null)),d&&(c=h,y=(h=d[m]).partialLength));L=_[s].an/2-_[s].add,A.translate(-L,0,0)}else L=_[s].an/2-_[s].add,A.translate(-L,0,0),A.translate(-E[0]*_[s].an*.005,-E[1]*B*.01,0);for(F=0;Fe);)r+=1;return this.keysIndex!==r&&(this.keysIndex=r),this.data.d.k[this.keysIndex].s},TextProperty.prototype.buildFinalText=function(t){for(var e,r,i=[],s=0,a=t.length,n=!1;sthis.minimumFontSize&&k=u(o)&&(n=c(0,d(t-o<0?d(h,1)-(o-t):h-t,1))),a(n));return n*this.a.v},getValue:function(t){this.iterateDynamicProperties(),this._mdf=t||this._mdf,this._currentTextLength=this.elem.textProperty.currentData.l.length||0,t&&2===this.data.r&&(this.e.v=this._currentTextLength);var e=2===this.data.r?1:100/this.data.totalChars,r=this.o.v/e,i=this.s.v/e+r,s=this.e.v/e+r;if(st-this.layers[e].st&&this.buildItem(e),this.completeLayers=!!this.elements[e]&&this.completeLayers;this.checkPendingElements()},BaseRenderer.prototype.createItem=function(t){switch(t.ty){case 2:return this.createImage(t);case 0:return this.createComp(t);case 1:return this.createSolid(t);case 3:return this.createNull(t);case 4:return this.createShape(t);case 5:return this.createText(t);case 6:return this.createAudio(t);case 13:return this.createCamera(t);case 15:return this.createFootage(t);default:return this.createNull(t)}},BaseRenderer.prototype.createCamera=function(){throw new Error("You're using a 3d camera. Try the html renderer.")},BaseRenderer.prototype.createAudio=function(t){return new AudioElement(t,this.globalData,this)},BaseRenderer.prototype.createFootage=function(t){return new FootageElement(t,this.globalData,this)},BaseRenderer.prototype.buildAllItems=function(){var t,e=this.layers.length;for(t=0;t=t)return this.threeDElements[e].perspectiveElem;e+=1}return null},HybridRenderer.prototype.createThreeDContainer=function(t,e){var r,i,s=createTag("div");styleDiv(s);var a=createTag("div");if(styleDiv(a),"3d"===e){(r=s.style).width=this.globalData.compSize.w+"px",r.height=this.globalData.compSize.h+"px";var n="50% 50%";r.webkitTransformOrigin=n,r.mozTransformOrigin=n,r.transformOrigin=n;var o="matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)";(i=a.style).transform=o,i.webkitTransform=o}s.appendChild(a);var h={container:a,perspectiveElem:s,startPos:t,endPos:t,type:e};return this.threeDElements.push(h),h},HybridRenderer.prototype.build3dContainers=function(){var t,e,r=this.layers.length,i="";for(t=0;tt?!0!==this.isInRange&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):!1!==this.isInRange&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var t,e=this.renderableComponents.length;for(t=0;t=t.x+t.width&&this.currentBBox.height+this.currentBBox.y>=t.y+t.height},HShapeElement.prototype.renderInnerContent=function(){if(this._renderShapeFrame(),!this.hidden&&(this._isFirstFrame||this._mdf)){var t=this.tempBoundingBox,e=999999;if(t.x=e,t.xMax=-e,t.y=e,t.yMax=-e,this.calculateBoundingBox(this.itemsData,t),t.width=t.xMaxthis.animationData.op&&(this.animationData.op=t.op,this.totalFrames=Math.floor(t.op-this.animationData.ip));var e,r,i=this.animationData.layers,s=i.length,a=t.layers,n=a.length;for(r=0;rthis.timeCompleted&&(this.currentFrame=this.timeCompleted),this.trigger("enterFrame"),this.renderFrame()},AnimationItem.prototype.renderFrame=function(){if(!1!==this.isLoaded&&this.renderer)try{this.renderer.renderFrame(this.currentFrame+this.firstFrame)}catch(t){this.triggerRenderFrameError(t)}},AnimationItem.prototype.play=function(t){t&&this.name!==t||!0===this.isPaused&&(this.isPaused=!1,this.audioController.resume(),this._idle&&(this._idle=!1,this.trigger("_active")))},AnimationItem.prototype.pause=function(t){t&&this.name!==t||!1===this.isPaused&&(this.isPaused=!0,this._idle=!0,this.trigger("_idle"),this.audioController.pause())},AnimationItem.prototype.togglePause=function(t){t&&this.name!==t||(!0===this.isPaused?this.play():this.pause())},AnimationItem.prototype.stop=function(t){t&&this.name!==t||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},AnimationItem.prototype.getMarkerData=function(t){for(var e,r=0;r=this.totalFrames-1&&0=this.totalFrames?(this.playCount+=1,this.checkSegments(e%this.totalFrames)||(this.setCurrentRawFrameValue(e%this.totalFrames),this._completedLoop=!0,this.trigger("loopComplete"))):this.setCurrentRawFrameValue(e):this.checkSegments(e>this.totalFrames?e%this.totalFrames:0)||(r=!0,e=this.totalFrames-1):e<0?this.checkSegments(e%this.totalFrames)||(!this.loop||this.playCount--<=0&&!0!==this.loop?(r=!0,e=0):(this.setCurrentRawFrameValue(this.totalFrames+e%this.totalFrames),this._completedLoop?this.trigger("loopComplete"):this._completedLoop=!0)):this.setCurrentRawFrameValue(e),r&&(this.setCurrentRawFrameValue(e),this.pause(),this.trigger("complete"))}},AnimationItem.prototype.adjustSegment=function(t,e){this.playCount=0,t[1]t[0]&&(this.frameModifier<0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(1)),this.totalFrames=t[1]-t[0],this.timeCompleted=this.totalFrames,this.firstFrame=t[0],this.setCurrentRawFrameValue(.001+e)),this.trigger("segmentStart")},AnimationItem.prototype.setSegment=function(t,e){var r=-1;this.isPaused&&(this.currentRawFrame+this.firstFramee&&(r=e-t)),this.firstFrame=t,this.totalFrames=e-t,this.timeCompleted=this.totalFrames,-1!==r&&this.goToAndStop(r,!0)},AnimationItem.prototype.playSegments=function(t,e){if(e&&(this.segments.length=0),"object"==typeof t[0]){var r,i=t.length;for(r=0;rdata.k[e].t&&tdata.k[e+1].t-t?(r=e+2,data.k[e+1].t):(r=e+1,data.k[e].t);break}}-1===r&&(r=e+1,i=data.k[e].t)}else i=r=0;var a={};return a.index=r,a.time=i/elem.comp.globalData.frameRate,a}function key(t){var e,r,i;if(!data.k.length||"number"==typeof data.k[0])throw new Error("The property has no keyframe at index "+t);t-=1,e={time:data.k[t].t/elem.comp.globalData.frameRate,value:[]};var s=Object.prototype.hasOwnProperty.call(data.k[t],"s")?data.k[t].s:data.k[t-1].e;for(i=s.length,r=0;rl.length-1)&&(e=l.length-1),i=p-(s=l[l.length-1-e].t)),"pingpong"===t){if(Math.floor((h-s)/i)%2!=0)return this.getValueAtTime((i-(h-s)%i+s)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var m=this.getValueAtTime(s/this.comp.globalData.frameRate,0),f=this.getValueAtTime(p/this.comp.globalData.frameRate,0),c=this.getValueAtTime(((h-s)%i+s)/this.comp.globalData.frameRate,0),d=Math.floor((h-s)/i);if(this.pv.length){for(n=(o=new Array(m.length)).length,a=0;al.length-1)&&(e=l.length-1),i=(s=l[e].t)-p),"pingpong"===t){if(Math.floor((p-h)/i)%2==0)return this.getValueAtTime(((p-h)%i+p)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var m=this.getValueAtTime(p/this.comp.globalData.frameRate,0),f=this.getValueAtTime(s/this.comp.globalData.frameRate,0),c=this.getValueAtTime((i-(p-h)%i+p)/this.comp.globalData.frameRate,0),d=Math.floor((p-h)/i)+1;if(this.pv.length){for(n=(o=new Array(m.length)).length,a=0;an){var p=o,m=r.c&&o===h-1?0:o+1,f=(n-l)/a[o].addedLength;i=bez.getPointInSegment(r.v[p],r.v[m],r.o[p],r.i[m],f,a[o]);break}l+=a[o].addedLength,o+=1}return i||(i=r.c?[r.v[0][0],r.v[0][1]]:[r.v[r._length-1][0],r.v[r._length-1][1]]),i},vectorOnPath:function(t,e,r){1==t?t=this.v.c:0==t&&(t=.999);var i=this.pointOnPath(t,e),s=this.pointOnPath(t+.001,e),a=s[0]-i[0],n=s[1]-i[1],o=Math.sqrt(Math.pow(a,2)+Math.pow(n,2));return 0===o?[0,0]:"tangent"===r?[a/o,n/o]:[-n/o,a/o]},tangentOnPath:function(t,e){return this.vectorOnPath(t,e,"tangent")},normalOnPath:function(t,e){return this.vectorOnPath(t,e,"normal")},setGroupProperty:expressionHelpers.setGroupProperty,getValueAtTime:expressionHelpers.getStaticValueAtTime},extendPrototype([r],t),extendPrototype([r],e),e.prototype.getValueAtTime=function(t){return this._cachingAtTime||(this._cachingAtTime={shapeValue:shapePool.clone(this.pv),lastIndex:0,lastTime:initialDefaultFrame}),t*=this.elem.globalData.frameRate,(t-=this.offsetTime)!==this._cachingAtTime.lastTime&&(this._cachingAtTime.lastIndex=this._cachingAtTime.lastTime base64 +const audioFileBase64 = + "data:audio/ogg;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwK6AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAA" + +// HMTL Template file +const htmlTemplate = fs.readFileSync( + "./src/assets/template/widget-default.html", + "utf8" +) + +// Inserting CSS to DOM +const cssFile = fs.readFileSync("./src/assets/css/linto-ui.min.css", "utf8") + +export default class LintoUI { + constructor(data) { + /* REQUIRED */ + this.containerId = "" + this.lintoWebHost = "" + this.lintoWebToken = "" + + // GLOBAL + this.debug = false + this.linto = null + this.widgetEnabled = false + this.widgetMode = "mutli-modal" + this.widgetContainer = null + this.streamingStopWord = "stop" + + // STATES + this.streamingMode = "vad" + this.writingTarget = null + this.streamingContent = "" + this.widgetState = "sleeping" + + // SETTINGS + this.hotwordValue = "linto" + this.hotwordEnabled = true + this.audioResponse = true + this.transactionMode = "chatbot_only" + + // AUDIO + this.beep = null + + // CUSTOM EVENTS + this.lintoCustomEvents = [] + + // ANIMATIONS + this.widgetRightCornerAnimation = null + this.widgetminimalOverlayAnimation = null + this.widgetMicAnimation = micJson + this.widgetThinkAnimation = lintoThinkJson + this.widgetSleepAnimation = lintoSleepJson + this.widgetTalkAnimation = lintoTalkJson + this.widgetAwakeAnimation = lintoAwakeJson + this.widgetErrorAnimation = errorJson + this.widgetValidateAnimation = validationJson + + // HTML ELEMENTS + this.widgetStartBtn = null + this.widgetCloseInitFrameBtn = null + this.widgetCollapseBtn = null + this.widgetSettingsBtn = null + this.widgetQuitBtn = null + this.widgetCloseSettings = null + this.widgetSaveSettingsBtn = null + this.settingsHotword = null + this.settingsAudioResp = null + this.widgetShowMinimal = null + this.widgetFooter = null + this.inputContent = null + this.txtBtn = null + this.micBtn = null + this.inputError = null + this.closeMinimalOverlayBtn = null + this.contentWrapper = null + this.widgetShowBtn = null + this.widgetMultiModal = null + this.widgetInitFrame = null + this.widgetMain = null + this.widgetSettings = null + this.widgetBody = null + + if (this.widgetMode === "minimal-streaming") { + this.widgetFooter.classList.add("hidden") + } + + // CUSTOMIZATION + this.widgetTitle = "Linto Widget" + + /* INITIALIZATION */ + this.init(data) + } + + async init(data) { + // Set custom parameters + for (let key in data) { + this[key] = data[key] || this[key] + } + this.widgetContainer = document.getElementById(this.containerId) + + // Animations + if (data?.widgetMicAnimation) { + this.widgetMicAnimation = require(data.widgetMicAnimation) + } + if (data?.widgetThinkAnimation) { + this.widgetThinkAnimation = require(data.widgetThinkAnimation) + } + if (data?.widgetSleepAnimation) { + this.widgetSleepAnimation = require(data.widgetSleepAnimation) + } + if (data?.widgetTalkAnimation) { + this.widgetTalkAnimation = require(data.widgetTalkAnimation) + } + if (data?.widgetAwakeAnimation) { + this.widgetAwakeAnimation = require(data.widgetAwakeAnimation) + } + if (data?.widgetErrorAnimation) { + this.widgetErrorAnimation = require(data.widgetErrorAnimation) + } + if (data?.widgetValidateAnimation) { + this.widgetValidateAnimation = require(data.widgetValidateAnimation) + } + // CUSTO / CSS + let style = document.createElement("style") + let cssRewrite = cssFile + if (data?.cssPrimarycolor) { + cssRewrite = cssRewrite.replace(/#59bbeb/g, data.cssPrimarycolor) + } + if (data?.cssSecondaryColor) { + cssRewrite = cssRewrite.replace(/#055e89/g, data.cssSecondaryColor) + } + style.textContent = cssRewrite + document.getElementsByTagName("head")[0].appendChild(style) + + // First initialisation + if (!this.widgetEnabled) { + // HTML (right corner) + this.widgetContainer.innerHTML = htmlTemplate + if (data?.widgetTitle) { + setTimeout(() => { + this.updateWidgetTitle(data) + }, 200) + } + + // Set HTML elements + this.widgetStartBtn = document.getElementById("widget-init-btn-enable") + this.widgetCloseInitFrameBtn = document.getElementsByClassName( + "widgetCloseInitFrameBtn" + ) + this.widgetCollapseBtn = document.getElementById("widget-mm-collapse-btn") + this.widgetSettingsBtn = document.getElementById("widget-mm-settings-btn") + this.widgetQuitBtn = document.getElementById("widget-quit-btn") + this.widgetCloseSettings = document.getElementById( + "widget-settings-cancel" + ) + this.widgetSaveSettingsBtn = document.getElementById( + "widget-settings-save" + ) + this.settingsHotword = document.getElementById("widget-settings-hotword") + this.settingsAudioResp = document.getElementById( + "widget-settings-say-response" + ) + this.widgetShowMinimal = document.getElementById("widget-show-minimal") + this.widgetFooter = document.getElementById("widget-main-footer") + this.inputContent = document.getElementById("chabtot-msg-input") + this.txtBtn = document.getElementById("widget-msg-btn") + this.micBtn = document.getElementById("widget-mic-btn") + this.inputError = document.getElementById("chatbot-msg-error") + this.closeMinimalOverlayBtn = document.getElementById("widget-ms-close") + this.contentWrapper = document.getElementById("widget-main-content") + this.widgetShowBtn = document.getElementById("widget-show-btn") + this.widgetMultiModal = document.getElementById("widget-mm") + this.widgetInitFrame = document.getElementById("widget-init-wrapper") + this.widgetMain = document.getElementById("widget-mm-main") + this.widgetSettings = document.getElementById("widget-settings") + this.widgetBody = document.getElementById("widget-main-body") + + // Audio hotword sound + this.beep = new Audio(audioFileBase64) + this.beep.volume = 0.1 + + // Widget Show button (right corner animation) + if (this.widgetShowBtn.classList.contains("sleeping")) { + this.setWidgetRightCornerAnimation("sleep") + } + this.widgetShowBtn.onclick = () => { + if (this.widgetMode === "minimal-streaming" && this.widgetEnabled) { + this.openMinimalOverlay() + this.setMinimalOverlayAnimation("listening") + this.widgetState = "listening" + this.linto.startStreaming() + } else { + this.openWidget() + } + } + + // Widget close init frame buttons + for (let closeBtn of this.widgetCloseInitFrameBtn) { + closeBtn.onclick = () => { + this.closeWidget() + } + } + + // Start widget + this.widgetStartBtn.onclick = async () => { + let hotwordEnabledSettings = document.getElementById( + "widget-init-settings-hotword" + ) + let audioResponseEnabledSettings = document.getElementById( + "widget-init-settings-say-response" + ) + let options = { + hotwordEnabled: hotwordEnabledSettings.checked, + audioResponseEnabled: audioResponseEnabledSettings.checked, + } + await this.initLintoWeb(options) + } + + // Collapse widget + this.widgetCollapseBtn.onclick = () => { + this.closeWidget() + } + + // Show / Hide widget settings + this.widgetSettingsBtn.onclick = () => { + if (this.widgetSettingsBtn.classList.contains("closed")) { + this.showSettings() + } else if (this.widgetSettingsBtn.classList.contains("opened")) { + this.hideSettings() + } + } + + // Widget CLOSE BTN + this.widgetQuitBtn.onclick = async () => { + this.closeWidget() + this.stopWidget() + await this.stopAll() + } + + // Close Settings + this.widgetCloseSettings.onclick = () => { + this.hideSettings() + } + + // Save Settings + this.widgetSaveSettingsBtn.onclick = () => { + this.updateWidgetSettings() + this.hideSettings() + } + + // Widget MIC BTN + this.micBtn.onclick = async () => { + if (this.widgetFooter.classList.contains("mic-disabled")) { + this.txtBtn.classList.remove("txt-enabled") + this.txtBtn.classList.add("txt-disabled") + this.widgetFooter.classList.remove("mic-disabled") + this.widgetFooter.classList.add("mic-enabled") + } + if (this.micBtn.classList.contains("recording")) { + this.linto.stopStreaming() + this.cleanUserBubble() + } else { + if (this.widgetState !== "listening") { + this.linto.stopSpeech() + setTimeout(() => { + this.widgetState = "listening" + this.linto.startStreaming() + }, 100) + } else { + this.linto.startStreaming() + } + } + } + + // Widget SEND BTN + this.txtBtn.onclick = () => { + // Disable mic, enable text + if (this.txtBtn.classList.contains("txt-disabled")) { + this.txtBtn.classList.add("txt-enabled") + this.txtBtn.classList.remove("txt-disabled") + this.widgetFooter.classList.add("mic-disabled") + this.widgetFooter.classList.remove("mic-enabled") + this.inputContent.focus() + } else { + let text = this.inputContent.innerHTML.replace(/ /g, " ").trim() + if (this.stringAsSpecialChar(text)) { + this.inputError.innerHTML = "Caractères non autorisés" + return + } else if (text.length > 0) { + this.createUserBubble() + this.setUserBubbleContent(text) + this.sendText(text) + this.createWidgetBubble() + this.inputContent.innerHTML = "" + } + } + } + document.addEventListener("keypress", (e) => { + if (e.key == 13 || e.key === "Enter") { + e.preventDefault() + if ( + this.inputContent === document.activeElement && + this.inputContent.innerHTML !== "" + ) { + let text = this.inputContent.innerHTML + .replace(/ /g, " ") + .trim() + if (this.stringAsSpecialChar(text)) { + this.inputError.innerHTML = "Caractères non autorisés" + return + } else { + this.createUserBubble() + this.setUserBubbleContent(text) + this.sendText(text) + this.createWidgetBubble() + this.inputContent.innerHTML = "" + } + } + } + }) + this.inputContent.oninput = () => { + if (this.inputError.innerHTML.length > 0) { + this.inputError.innerHTML = "" + } + } + + // MINIMAL OVERLAY + this.closeMinimalOverlayBtn.onclick = () => { + this.closeMinimalOverlay() + this.linto.stopStreaming() + this.linto.stopSpeech() + } + + // MINIMAL SHOW WIDGET + this.widgetShowMinimal.onclick = () => { + this.openWidget() + } + + // Local Storage and settings + if (localStorage.getItem("lintoWidget") !== null) { + const storage = JSON.parse(localStorage.getItem("lintoWidget")) + + if (!!storage.hotwordEnabled) { + this.hotwordEnabled = storage.hotwordEnabled + } + if (!!storage.audioRespEnabled) { + this.audioResponse = storage.audioRespEnabled + } + let options = { + hotwordEnabled: storage.hotwordEnabled, + audioResponseEnabled: storage.audioRespEnabled, + } + + if ( + storage.widgetEnabled === true || + storage.widgetEnabled === "true" + ) { + await this.initLintoWeb(options) + } + } + this.hotwordEnabled === "false" + ? (this.settingsHotword.checked = false) + : (this.settingsHotword.checked = true) + this.audioResponse === "false" + ? (this.settingsAudioResp.checked = false) + : (this.settingsAudioResp.checked = true) + } + } + updateWidgetTitle(data) { + const widgetTitleMain = + document.getElementsByClassName("widget-mm-title")[0] + const widgetTitleInit = + document.getElementsByClassName("widget-init-title")[0] + widgetTitleMain.innerHTML = data.widgetTitle + widgetTitleInit.innerHTML = data.widgetTitle + } + + // ANIMATION RIGHT CORNER + setWidgetRightCornerAnimation(name, cb) { + // Lottie animations + let jsonPath = "" + // animation + if (name === "listening") { + jsonPath = this.widgetMicAnimation + } else if (name === "thinking") { + jsonPath = this.widgetThinkAnimation + } else if (name === "talking") { + jsonPath = this.widgetTalkAnimation + } else if (name === "sleep") { + jsonPath = this.widgetSleepAnimation + } else if (name === "awake") { + jsonPath = this.widgetAwakeAnimation + } else if (name === "error") { + jsonPath = this.widgetErrorAnimation + } else if (name === "validation") { + jsonPath = this.widgetValidateAnimation + } else if (name === "destroy") { + this.widgetRightCornerAnimation.destroy() + } + if (this.widgetRightCornerAnimation !== null && name !== "destroy") { + this.widgetRightCornerAnimation.destroy() + } + if (name !== "destroy") { + this.widgetRightCornerAnimation = lottie.loadAnimation({ + container: document.getElementById("widget-show-btn"), + renderer: "svg", + loop: !(name === "validation" || name === "error"), + autoplay: true, + animationData: jsonPath, + rendererSettings: { + className: "linto-animation", + }, + }) + if (!!cb) { + this.widgetRightCornerAnimation.onComplete = () => { + cb() + } + } + } + } + + // WIDGET MAIN + startWidget() { + this.widgetInitFrame.classList.add("hidden") + this.closeWidget() + this.widgetMain.classList.remove("hidden") + this.setWidgetRightCornerAnimation("validation", () => { + this.widgetShowBtn.classList.remove("sleeping") + this.widgetShowBtn.classList.add("awake") + this.setWidgetRightCornerAnimation("awake") + }) + if (this.widgetMode === "minimal-streaming") { + setTimeout(() => { + this.widgetShowMinimal.classList.remove("hidden") + this.widgetShowMinimal.classList.add("visible") + }, 2000) + } + } + + openWidget() { + if (this.widgetMode === "minimal-streaming") { + this.widgetShowMinimal.classList.remove("visible") + this.widgetShowMinimal.classList.add("hidden") + } + this.widgetShowBtn.classList.remove("visible") + this.widgetShowBtn.classList.add("hidden") + this.widgetMultiModal.classList.remove("hidden") + this.widgetMultiModal.classList.add("visible") + this.widgetContentScrollBottom() + } + closeWidget() { + if (this.widgetMode === "minimal-streaming") { + this.widgetShowMinimal.classList.add("visible") + this.widgetShowMinimal.classList.remove("hidden") + } + this.widgetMultiModal.classList.add("hidden") + this.widgetMultiModal.classList.remove("visible") + this.widgetShowBtn.classList.add("visible") + this.widgetShowBtn.classList.remove("hidden") + this.hideSettings() + if (this.widgetShowBtn.classList.contains("sleeping")) { + this.setWidgetRightCornerAnimation("sleep") + } else { + this.setWidgetRightCornerAnimation("awake") + } + } + + stopWidget() { + if (this.widgetMode === "minimal-streaming") { + this.widgetShowMinimal.classList.remove("visible") + this.widgetShowMinimal.classList.add("hidden") + } + this.widgetInitFrame.classList.remove("hidden") + this.widgetMain.classList.add("hidden") + if (this.widgetShowBtn.classList.contains("awake")) { + this.widgetShowBtn.classList.add("sleeping") + this.widgetShowBtn.classList.remove("awake") + this.setWidgetRightCornerAnimation("sleep") + } + } + + // WIDGET SETTINGS + showSettings() { + this.widgetSettingsBtn.classList.remove("closed") + this.widgetSettingsBtn.classList.add("opened") + this.widgetSettings.classList.remove("hidden") + this.widgetBody.classList.add("hidden") + + let enableHotwordInput = document.getElementById("widget-settings-hotword") + let enableSayRespInput = document.getElementById( + "widget-settings-say-response" + ) + if (!this.hotwordEnabled || this.hotwordEnabled === "false") { + enableHotwordInput.checked = false + } + if (!this.audioResponse || this.audioResponse === "false") { + enableSayRespInput.checked = false + } + } + hideSettings() { + this.widgetSettingsBtn.classList.remove("opened") + this.widgetSettingsBtn.classList.add("closed") + this.widgetBody.classList.remove("hidden") + this.widgetSettings.classList.add("hidden") + } + updateWidgetSettings() { + const hotwordCheckbox = document.getElementById("widget-settings-hotword") + const audioRespCheckbox = document.getElementById( + "widget-settings-say-response" + ) + // Disable Hotword + if (!hotwordCheckbox.checked && this.hotwordEnabled) { + this.hotwordEnabled = false + this.linto.stopStreamingPipeline() + this.linto.stopAudioAcquisition() + setTimeout(() => { + this.linto.startAudioAcquisition(false, this.hotwordValue, 0.99) + this.linto.startStreamingPipeline() + }, 200) + } + // Enable Hotword + else if (hotwordCheckbox.checked && !this.hotwordEnabled) { + this.hotwordEnabled = true + this.linto.stopStreamingPipeline() + this.linto.stopAudioAcquisition() + setTimeout(() => { + this.linto.startAudioAcquisition(true, this.hotwordValue, 0.99) + this.linto.startStreamingPipeline() + }, 200) + } + // Disable AudioResponse + if (!audioRespCheckbox.checked && this.audioResponse) { + this.audioResponse = false + } + // Enable AudioResponse + else if (audioRespCheckbox.checked && !this.audioResponse) { + this.audioResponse = true + } + let widgetStatus = { + widgetEnabled: this.widgetEnabled, + hotwordEnabled: this.hotwordEnabled, + audioRespEnabled: this.audioResponse, + } + localStorage.setItem("lintoWidget", JSON.stringify(widgetStatus)) + } + + // WIDGET CONTENT BUBBLES + cleanUserBubble() { + let userBubbles = document.getElementsByClassName("user-bubble") + for (let bubble of userBubbles) { + if (bubble.innerHTML.indexOf("loading") >= 0) { + bubble.remove() + } + } + } + createUserBubble() { + this.contentWrapper.innerHTML += ` +
+ +
` + } + setUserBubbleContent(text) { + let userBubbles = document.getElementsByClassName("user-bubble") + let current = userBubbles[userBubbles.length - 1] + current.innerHTML = `${text}` + this.widgetContentScrollBottom() + } + createWidgetBubble() { + this.contentWrapper.innerHTML += ` +
+ +
` + } + setWidgetBubbleContent(text) { + let widgetBubbles = document.getElementsByClassName("widget-bubble") + let current = widgetBubbles[widgetBubbles.length - 1] + if (current) { + current.innerHTML = `${text}` + this.widgetContentScrollBottom() + } else { + this.createWidgetBubble() + this.setWidgetBubbleContent(text) + } + } + cleanWidgetBubble() { + let widgetBubbles = document.getElementsByClassName("widget-bubble") + for (let bubble of widgetBubbles) { + if (bubble.innerHTML.indexOf("loading") >= 0) { + bubble.remove() + } + } + } + stringIsHTML(str) { + const regex = /[<>]/ + return regex.test(str) + } + stringAsSpecialChar(str) { + const regex = /[!@#$%^&*()"{}|<>]/g + return regex.test(str) + } + setFeedbackData(data) { + this.cleanWidgetBubble() + let jhtml = "" + if (!Array.isArray(data)) { + if (data?.button) { + jhtml = '
' + for (let item of data.button) { + jhtml += `` + } + jhtml += "
" + } + if (data?.html) { + jhtml = + '
' + + data.html + + "
" + } + } else { + jhtml = '
' + for (let item of data) { + switch (item.eventType) { + case "sentence": + if (this.stringIsHTML(item.text)) { + jhtml += item.text + } else { + jhtml += `${item.text}` + this.widgetSay(item.text) + } + break + case "choice": + jhtml += `` + break + case "attachment": + if (!!item.file && item.file.type === "image") { + jhtml += `` + } + case "default": + break + } + } + jhtml += "
" + } + + this.contentWrapper.innerHTML += jhtml + this.widgetContentScrollBottom() + if (this.transactionMode === "chatbot_only") { + this.bindWidgetButtons() + } else { + this.bindCommandButtons() + } + } + + bindWidgetButtons() { + let widgetEventsBtn = document.getElementsByClassName("widget-content-link") + for (let btn of widgetEventsBtn) { + btn.onclick = (e) => { + let value = e.target.innerHTML + this.createUserBubble() + this.setUserBubbleContent(value) + this.createWidgetBubble() + this.linto.sendChatbotText(value) + } + } + } + + bindCommandButtons() { + let widgetEventsBtn = document.getElementsByClassName("widget-content-link") + for (let btn of widgetEventsBtn) { + btn.onclick = (e) => { + let value = e.target.innerHTML + this.createUserBubble() + this.setUserBubbleContent(value) + this.createWidgetBubble() + this.linto.sendCommandText(value) + } + } + } + widgetContentScrollBottom() { + this.contentWrapper.scrollTo({ + top: this.contentWrapper.scrollHeight, + left: 0, + behavior: "smooth", + }) + } + + // Minimal streaming overlay + setMinimalOverlayAnimation(name, cb) { + let jsonPath = "" + // animation + + switch (name) { + case "listening": + jsonPath = this.widgetMicAnimation + break + case "thinking": + jsonPath = this.widgetThinkAnimation + break + case "talking": + jsonPath = this.widgetTalkAnimation + break + case "sleep": + jsonPath = this.widgetSleepAnimation + break + case "destroy": + jsonPath = this.widgetDestroyAnimation + this.widgetminimalOverlayAnimation.destroy() + case "default": + break + } + + if (this.widgetminimalOverlayAnimation !== null && name !== "destroy") { + this.widgetminimalOverlayAnimation.destroy() + } + if (name !== "destroy") { + this.widgetminimalOverlayAnimation = lottie.loadAnimation({ + container: document.getElementById("widget-ms-animation"), + renderer: "svg", + loop: !(name === "validation" || name === "error"), + autoplay: true, + animationData: jsonPath, + rendererSettings: { + className: "linto-animation", + }, + }) + this.widgetminimalOverlayAnimation.onComplete = () => { + cb() + } + } + } + openMinimalOverlay() { + const minOverlay = document.getElementById("widget-minimal-overlay") + this.closeWidget() + this.widgetShowMinimal.classList.remove("visible") + this.widgetShowMinimal.classList.add("hidden") + this.widgetShowBtn.classList.remove("visible") + this.widgetShowBtn.classList.add("hidden") + minOverlay.classList.remove("hidden") + minOverlay.classList.add("visible") + } + closeMinimalOverlay() { + const minOverlay = document.getElementById("widget-minimal-overlay") + + this.widgetShowMinimal.classList.add("visible") + this.widgetShowMinimal.classList.remove("hidden") + this.widgetShowBtn.classList.add("visible") + this.widgetShowBtn.classList.remove("hidden") + minOverlay.classList.add("hidden") + minOverlay.classList.remove("visible") + this.setMinimalOverlayAnimation("") + this.setMinimalOverlaySecondaryContent("") + this.setMinimalOverlayMainContent("") + } + setMinimalOverlayMainContent(txt) { + const mainContent = document.getElementById("widget-ms-content-current") + if (txt === "") { + mainContent.innerHTML = "" + } + if (txt) { + mainContent.innerHTML = txt + } + } + setMinimalOverlaySecondaryContent(txt) { + const secContent = document.getElementById("widget-ms-content-previous") + if (txt === "") { + secContent.innerHTML = "" + } + if (txt) { + secContent.innerHTML = txt + } + } + async widgetSay(answer) { + this.linto.stopSpeech() + let isLink = this.stringIsHTML(answer) + let sayResp = null + this.widgetState = "saying" + if (this.audioResponse && !isLink) { + sayResp = await this.linto.say("fr-FR", answer) + } + if (sayResp !== null) { + if (this.widgetMode === "minimal-streaming") { + this.setMinimalOverlaySecondaryContent("") + this.setMinimalOverlayMainContent("") + this.closeMinimalOverlay() + } + this.widgetState = "waiting" + } else { + if (this.widgetMode === "minimal-streaming") { + setTimeout(() => { + this.setMinimalOverlaySecondaryContent("") + this.setMinimalOverlayMainContent("") + this.closeMinimalOverlay() + }, 4000) + this.widgetState = "waiting" + } + } + } + async stopAll() { + this.linto.stopStreaming() + this.linto.stopStreamingPipeline() + this.linto.stopAudioAcquisition() + this.linto.stopSpeech() + await this.linto.logout + this.widgetEnabled = false + this.hideSettings() + localStorage.clear() + } + + customStreaming(streamingMode, target) { + this.beep.play() + this.streamingMode = streamingMode + this.writingTarget = document.getElementById(target) + this.linto.stopStreamingPipeline() + this.linto.startStreaming(0) + } + setHandler(label, func) { + this.linto.addEventListener(label, func) + } + sendText(text) { + if (this.transactionMode === "chatbot_only") { + this.linto.sendChatbotText(text) + } else if (this.transactionMode === "skills_and_chatbot") { + this.linto.sendCommandText(text) + } + } + initLintoWeb = async (options) => { + // Set chatbot + this.linto = new Linto(this.lintoWebHost, this.lintoWebToken) + + // Chatbot events + this.linto.addEventListener( + "mqtt_connect", + handlers.mqttConnectHandler.bind(this) + ) + this.linto.addEventListener( + "mqtt_connect_fail", + handlers.mqttConnectFailHandler.bind(this) + ) + this.linto.addEventListener( + "mqtt_error", + handlers.mqttErrorHandler.bind(this) + ) + this.linto.addEventListener( + "mqtt_disconnect", + handlers.mqttDisconnectHandler.bind(this) + ) + this.linto.addEventListener( + "command_acquired", + handlers.commandAcquired.bind(this) + ) + this.linto.addEventListener( + "command_published", + handlers.commandPublished.bind(this) + ) + this.linto.addEventListener( + "speaking_on", + handlers.audioSpeakingOn.bind(this) + ) + this.linto.addEventListener( + "speaking_off", + handlers.audioSpeakingOff.bind(this) + ) + this.linto.addEventListener( + "streaming_start", + handlers.streamingStart.bind(this) + ) + this.linto.addEventListener( + "streaming_stop", + handlers.streamingStop.bind(this) + ) + this.linto.addEventListener( + "streaming_chunk", + handlers.streamingChunk.bind(this) + ) + this.linto.addEventListener( + "streaming_final", + handlers.streamingFinal.bind(this) + ) + this.linto.addEventListener( + "streaming_fail", + handlers.streamingFail.bind(this) + ) + this.linto.addEventListener("hotword_on", handlers.hotword.bind(this)) + this.linto.addEventListener( + "ask_feedback_from_skill", + handlers.askFeedback.bind(this) + ) + this.linto.addEventListener( + "say_feedback_from_skill", + handlers.sayFeedback.bind(this) + ) + this.linto.addEventListener( + "custom_action_from_skill", + handlers.customHandler.bind(this) + ) + this.linto.addEventListener( + "startRecording", + handlers.textPublished.bind(this) + ) + this.linto.addEventListener( + "chatbot_acquired", + handlers.chatbotAcquired.bind(this) + ) + this.linto.addEventListener( + "chatbot_published", + handlers.chatbotPublished.bind(this) + ) + this.linto.addEventListener( + "action_published", + handlers.actionPublished.bind(this) + ) + this.linto.addEventListener( + "action_feedback", + handlers.actionFeedback.bind(this) + ) + this.linto.addEventListener( + "chatbot_feedback", + handlers.widgetFeedback.bind(this) + ) + this.linto.addEventListener("chatbot_error", (e) => { + // todo : handle error + console.log("chatbot error", e) + this.cleanWidgetBubble() + }) + this.linto.addEventListener( + "chatbot_feedback_from_skill", + handlers.widgetFeedback.bind(this) + ) + + // Widget login + try { + let login = await this.linto.login() + if (login === true) { + this.widgetEnabled = true + + // Bind custom events + if (this.lintoCustomEvents.length > 0) { + for (let event of this.lintoCustomEvents) { + this.setHandler(event.flag, event.func) + } + } + this.hotwordEnabled = options.hotwordEnabled + this.audioResponse = options.audioResponseEnabled + + if (!this.hotwordEnabled || this.hotwordEnabled === "false") { + this.linto.startAudioAcquisition(false, this.hotwordValue, 0.99) + } else { + this.linto.startAudioAcquisition(true, this.hotwordValue, 0.99) + } + this.linto.startStreamingPipeline() + this.widgetState = "waiting" + let widgetStatus = { + widgetEnabled: this.widgetEnabled, + hotwordEnabled: this.hotwordEnabled, + audioRespEnabled: this.audioResponse, + } + localStorage.setItem("lintoWidget", JSON.stringify(widgetStatus)) + this.startWidget() + } else { + throw login + } + } catch (error) { + this.closeWidget() + this.setWidgetRightCornerAnimation("error", () => { + if (!!error.message) { + let widgetErrorMsg = document.getElementById("widget-error-message") + widgetErrorMsg.classList.remove("hidden") + widgetErrorMsg.innerHTML = error.message + setTimeout(() => { + widgetErrorMsg.classList.add("hidden") + widgetErrorMsg.innerHTML = "" + this.setWidgetRightCornerAnimation("sleep") + let widgetStatus = { + widgetEnabled: false, + hotwordEnabled: false, + audioRespEnabled: false, + } + localStorage.setItem("lintoWidget", JSON.stringify(widgetStatus)) + }, 4000) + } + }) + return + } + } +} +window.LintoUI = LintoUI +module.exports = LintoUI diff --git a/client/web/src/linto.js b/client/web/src/linto.js new file mode 100644 index 0000000..8179597 --- /dev/null +++ b/client/web/src/linto.js @@ -0,0 +1,413 @@ +import ReTree from "re-tree" +import UaDeviceDetector from "ua-device-detector" +import MqttClient from "./mqtt.js" +import Audio from "./audio.js" +import * as handlers from "./handlers/linto.js" +import axios from "axios" + +export default class Linto extends EventTarget { + constructor(httpAuthServer, requestToken, commandTimeout = 10000) { + super() + this.browser = UaDeviceDetector.parseUserAgent(window.navigator.userAgent) + this.commandTimeout = commandTimeout + this.lang = "en-US" // default + // Status + this.commandPipeline = false + this.streamingPipeline = false + this.streaming = false + this.hotword = false + this.event = { + nlp: false, + } + // Server connexion + this.httpAuthServer = httpAuthServer + this.requestToken = requestToken + } + + /****************************** + * Application state management + ******************************/ + + setTTSLang(lang) { + this.lang = lang + } + + triggerHotword(dummyHotwordName = "dummy") { + this.audio.vad.dispatchEvent( + new CustomEvent("speaking", { + detail: true, + }) + ) + this.audio.hotword.dispatchEvent( + new CustomEvent("hotword", { + detail: dummyHotwordName, + }) + ) + } + + startAudioAcquisition( + useHotword = true, + hotwordModel = "linto", + threshold = 0.99, + mobileConstraintsOverrides = { + echoCancellation: false, + autoGainControl: false, + noiseSuppression: false, + } + ) { + if (!this.audio) { + this.audio = new Audio( + this.browser.isMobile(), + useHotword, + hotwordModel, + threshold, + mobileConstraintsOverrides + ) + if (useHotword) { + this.audio.vad.addEventListener( + "speakingStatus", + handlers.vadStatus.bind(this) + ) + } + } + } + + pauseAudioAcquisition() { + if (this.audio) { + this.audio.pause() + } + } + + resumeAudioAcquisition() { + if (this.audio) { + this.audio.resume() + } + } + + stopAudioAcquisition() { + if (this.audio) this.audio.stop() + this.stopCommandPipeline() + this.stopStreaming() + delete this.audio + } + + startStreamingPipeline(withHotWord = true) { + if (!this.streamingPipeline && this.audio) { + this.streamingPipeline = true + if (withHotWord) this.startHotword() + this.addEventNlp() + } + } + + stopStreamingPipeline() { + if (this.streamingPipeline && this.audio) { + this.streamingPipeline = false + if (this.hotword) this.stopHotword() + this.removeEventNlp() + } + } + + startCommandPipeline(withHotWord = true) { + if (!this.commandPipeline && this.audio) { + this.commandPipeline = true + if (withHotWord) this.startHotword() + this.addEventNlp() + } + } + + stopCommandPipeline() { + if (this.commandPipeline && this.audio) { + this.commandPipeline = false + if (this.hotword) this.stopHotword() + this.removeEventNlp() + } + } + + startHotword() { + if (!this.hotword && this.audio) { + this.hotword = true + if (this.commandPipeline) + this.hotwordHandler = handlers.hotwordCommandBuffer.bind(this) + if (this.streamingPipeline) + this.hotwordHandler = handlers.hotwordStreaming.bind(this) + this.audio.hotword.addEventListener("hotword", this.hotwordHandler) + } + } + + stopHotword() { + if (this.hotword && this.audio) { + this.hotword = false + this.audio.hotword.removeEventListener("hotword", this.hotwordHandler) + } + } + + startStreaming(metadata = 1) { + if (!this.streaming && this.mqtt && this.audio) { + this.streaming = true + this.mqtt.startStreaming( + this.audio.downSampler.options.targetSampleRate, + metadata + ) + // We wait start streaming acknowledgment returning from MQTT before actualy start to publish audio frames. + } + } + + stopStreaming() { + if (this.streaming) { + this.streaming = false + // We immediatly stop streaming audio without waiting stop streaming acknowledgment + this.audio.downSampler.removeEventListener( + "downSamplerFrame", + this.streamingPublishHandler + ) + this.mqtt.stopStreaming() + } + } + + addEventNlp() { + if (!this.event.nlp) { + this.nlpAnswerHandler = handlers.nlpAnswer.bind(this) + this.mqtt.addEventListener("nlp", this.nlpAnswerHandler) + this.event.nlp = true + } + } + + removeEventNlp() { + if (this.event.nlp) { + this.event.nlp = false + this.mqtt.removeEventListener("nlp", this.nlpAnswerHandler) + } + } + + printErrorMsg(message) { + let errorFrame = document.createElement("div") + let errorFrameStyle = ` + display: inline-block; + width: 400px; + height: auto; + padding: 10px; + position: fixed; + top: 100%; + left: 50%; + margin-top: -80px; + margin-left: -200px; + background-color: #ff3d3d; + color: #fff; + text-align: center; + font-family: arial, helvetica, verdana; + font-size: 14px; + ` + errorFrame.setAttribute("style", errorFrameStyle) + errorFrame.innerHTML = message + document.body.appendChild(errorFrame) + setTimeout(() => { + errorFrame.remove() + }, 4000) + } + + /********* + * Actions + *********/ + async login() { + return new Promise(async (resolve, reject) => { + let auth + try { + auth = await axios.post( + this.httpAuthServer, + { + requestToken: this.requestToken, + }, + { + headers: { + "Content-Type": "application/json", + }, + } + ) + } catch (authFail) { + if (authFail.response && authFail.response.data) { + this.printErrorMsg(authFail.response.data.message) + return reject(authFail.response.data) + } else return reject(authFail) + } + + try { + this.userInfo = auth.data.user + this.mqttInfo = auth.data.mqttConfig + this.mqtt = new MqttClient() + // Mqtt + + this.mqtt.addEventListener( + "tts_lang", + handlers.ttsLangAction.bind(this) + ) + this.mqtt.addEventListener( + "streaming_start_ack", + handlers.streamingStartAck.bind(this) + ) + this.mqtt.addEventListener( + "streaming_chunk", + handlers.streamingChunk.bind(this) + ) + this.mqtt.addEventListener( + "streaming_stop_ack", + handlers.streamingStopAck.bind(this) + ) + this.mqtt.addEventListener( + "streaming_final", + handlers.streamingFinal.bind(this) + ) + this.mqtt.addEventListener( + "streaming_fail", + handlers.streamingFail.bind(this) + ) + this.mqtt.addEventListener( + "chatbot_feedback", + handlers.chatbotAnswer.bind(this) + ) + this.mqtt.addEventListener( + "action_feedback", + handlers.actionAnswer.bind(this) + ) + this.mqtt.addEventListener( + "mqtt_connect", + handlers.mqttConnect.bind(this) + ) + this.mqtt.addEventListener( + "mqtt_connect_fail", + handlers.mqttConnectFail.bind(this) + ) + this.mqtt.addEventListener("mqtt_error", handlers.mqttError.bind(this)) + this.mqtt.addEventListener( + "mqtt_disconnect", + handlers.mqttDisconnect.bind(this) + ) + this.mqtt.connect(this.userInfo, this.mqttInfo) + } catch (mqttFail) { + return reject(mqttFail) + } + resolve(true) + }) + } + + async logout() { + this.stopCommandPipeline() + this.stopStreamingPipeline() + this.stopStreaming() + this.mqtt.disconnect() + delete this.mqtt + } + + listenCommand() { + this.audio.listenCommand() + } + + say(lang, text) { + return new Promise((resolve, reject) => { + const toSay = new SpeechSynthesisUtterance(text) + toSay.lang = lang + toSay.onend = resolve + toSay.onerror = reject + speechSynthesis.speak(toSay) + }) + } + + async ask(lang, text) { + await this.say(lang, text) + this.triggerHotword() + } + + stopSpeech() { + speechSynthesis.cancel() + } + + async sendCommandBuffer() { + try { + const b64Audio = await this.audio.getCommand() + this.dispatchEvent(new CustomEvent("command_acquired")) + const id = await this.mqtt.publishAudioCommand(b64Audio) + this.dispatchEvent( + new CustomEvent("command_published", { + detail: id, + }) + ) + setTimeout(() => { + // Check if id is still in the array of "to be processed commands" + // Mqtt handles itself the removal of received transcriptions + if (this.mqtt && this.mqtt.pendingCommandIds.includes(id)) { + this.dispatchEvent( + new CustomEvent("command_timeout", { + detail: id, + }) + ) + } + }, this.commandTimeout) + } catch (e) { + this.dispatchEvent( + new CustomEvent("command_error", { + detail: e, + }) + ) + } + } + + async sendCommandText(text) { + this.sendLintoText(text, { status: "text" }) + } + + async sendChatbotText(text) { + this.sendLintoText(text, { status: "chatbot" }) + } + + // detail : contains event information + async sendLintoText(text, detail) { + try { + this.dispatchEvent(new CustomEvent(`${detail.status}_acquired`)) + const id = await this.mqtt.publishText(text, detail) + this.dispatchEvent( + new CustomEvent(`${detail.status}_published`, { + detail: id, + }) + ) + setTimeout(() => { + // Check if id is still in the array of "to be processed commands" + // Mqtt handles itself the removal of received transcriptions + if (this.mqtt && this.mqtt.pendingCommandIds.includes(id)) { + this.dispatchEvent( + new CustomEvent("command_timeout", { + detail: id, + }) + ) + } + }, this.commandTimeout) + } catch (e) { + console.log(e) + this.dispatchEvent( + new CustomEvent("command_error", { + detail: e, + }) + ) + } + } + + async triggerAction(payload, skillName, eventName) { + try { + this.dispatchEvent(new CustomEvent("action_acquired")) + const id = await this.mqtt.publishAction(payload, skillName, eventName) + this.dispatchEvent( + new CustomEvent("action_published", { + detail: id, + }) + ) + } catch (e) { + console.log(e) + this.dispatchEvent( + new CustomEvent("action_error", { + detail: e, + }) + ) + } + } +} + +window.Linto = Linto +module.exports = Linto diff --git a/client/web/src/mqtt.js b/client/web/src/mqtt.js new file mode 100644 index 0000000..2fe6712 --- /dev/null +++ b/client/web/src/mqtt.js @@ -0,0 +1,193 @@ +import * as mqtt from "mqtt" +import * as handlers from "./handlers/mqtt" + +export default class MqttClient extends EventTarget { + constructor() { + super() + this.conversationData = {} // Context for long running transactions (interactive asks) + this.pendingCommandIds = new Array() // Currently being processed ids + } + + connect(userInfo, mqttInfo) { + this.userInfo = userInfo + this.ingress = `${userInfo.topic}/tolinto/${userInfo.session_id}/#` // Everything to receive by this instance + this.egress = `${userInfo.topic}/fromlinto/${userInfo.session_id}` // Base for sent messages + + const cnxParam = { + clean: true, + keepalive: 300, + reconnectPeriod: Math.floor(Math.random() * 1000) + 1000, // ms for reconnect + will: { + topic: `${this.egress}/status`, + retain: false, + payload: JSON.stringify({ + connexion: "offline", + }), + }, + qos: 2, + } + + if (mqttInfo.mqtt_use_login) { + cnxParam.username = mqttInfo.mqtt_login + cnxParam.password = mqttInfo.mqtt_password + } + this.client = mqtt.connect(mqttInfo.host, cnxParam) + // Listen events from this.client (mqtt client) + this.client.addListener("connect", handlers.mqttConnect.bind(this)) + this.client.addListener("disconnect", handlers.mqttDisconnect.bind(this)) + this.client.addListener("offline", handlers.mqttOffline.bind(this)) + this.client.addListener("close", handlers.mqttOffline.bind(this)) + this.client.addListener("error", handlers.mqttError.bind(this)) + this.client.addListener("message", handlers.mqttMessage.bind(this)) + } + + async disconnect() { + // Gracefuly disconnect from broker + const payload = { + connexion: "offline", + on: new Date().toJSON(), + } + await this.publish("status", payload, 0, false, true) + this.client.end() + } + + async publish(topic, value, qos = 2, retain = false, requireOnline = true) { + return new Promise((resolve, reject) => { + value.auth_token = `WebApplication ${this.userInfo.auth_token}` + const pubTopic = `${this.egress}/${topic}` + const pubOptions = { + qos: qos, + retain: retain, + } + if (requireOnline === true) { + if (this.client.connected !== true) return + this.client.publish( + pubTopic, + JSON.stringify(value), + pubOptions, + (err) => { + if (err) return reject(err) + return resolve() + } + ) + } + }) + } + + // app1cf2ee0f5a4ce4bcd668e734f2604018/fromlinto/DEV_5f3d383540cd1902084c6275/skills/transcribe/transcriber + async publishAction(payload, skillName, eventName) { + return new Promise((resolve, reject) => { + const pubOptions = { + qos: 0, + retain: false, + } + const transactionId = Math.random().toString(36).substring(4) + const pubTopic = `${this.egress}/skills/${skillName}/${eventName}` + + this.client.publish( + pubTopic, + JSON.stringify(payload), + pubOptions, + (err) => { + if (err) return reject(err) + this.pendingCommandIds.push(transactionId) + return resolve(transactionId) + } + ) + }) + } + + async publishText(text, detail) { + return new Promise((resolve, reject) => { + const pubOptions = { + qos: 0, + retain: false, + } + const transactionId = Math.random().toString(36).substring(4) + let pubTopic + if (detail.status === "chatbot") + pubTopic = `${this.egress}/chatbot/${transactionId}` + else pubTopic = `${this.egress}/nlp/text/${transactionId}` + + const payload = { + text: text, + conversationData: this.conversationData, + } + this.client.publish( + pubTopic, + JSON.stringify(payload), + pubOptions, + (err) => { + if (err) return reject(err) + this.pendingCommandIds.push(transactionId) + return resolve(transactionId) + } + ) + }) + } + + async publishAudioCommand(b64Audio) { + return new Promise((resolve, reject) => { + const pubOptions = { + qos: 0, + retain: false, + } + const fileId = Math.random().toString(36).substring(4) + const pubTopic = `${this.egress}/nlp/file/${fileId}` + const payload = { + audio: b64Audio, + auth_token: `WebApplication ${this.userInfo.auth_token}`, + conversationData: this.conversationData, + } + this.client.publish( + pubTopic, + JSON.stringify(payload), + pubOptions, + (err) => { + if (err) return reject(err) + this.pendingCommandIds.push(fileId) + return resolve(fileId) + } + ) + }) + } + + publishStreamingChunk(audioFrame) { + const pubOptions = { + qos: 0, + retain: false, + properties: { + payloadFormatIndicator: true, + }, + } + const pubTopic = `${this.egress}/streaming/chunk` + const frame = convertFloat32ToInt16(audioFrame) // Conversion can occur on a second downsampler being spawned + const vue = new Uint8Array(frame) + this.client.publish(pubTopic, vue, pubOptions, (err) => { + if (err) console.log(err) + }) + } + + startStreaming(sample_rate = 16000, metadata = 1) { + const streamingOptions = { + config: { + sample_rate, + metadata, + }, + } + this.publish(`streaming/start`, streamingOptions, 2, false, true) + } + + stopStreaming() { + this.publish(`streaming/stop`, {}, 2, false, true) + } +} + +function convertFloat32ToInt16(buffer) { + let l = buffer.length + let buf = new Int16Array(l) + while (l--) { + buf[l] = Math.min(1, buffer[l]) * 0x7fff + } + return buf.buffer +} diff --git a/client/web/tests/index.html b/client/web/tests/index.html new file mode 100644 index 0000000..0c0389a --- /dev/null +++ b/client/web/tests/index.html @@ -0,0 +1,15 @@ + + + + + + + LinTO Web Client tests + + + + + + + + \ No newline at end of file diff --git a/client/web/tests/index.js b/client/web/tests/index.js new file mode 100644 index 0000000..9b909c3 --- /dev/null +++ b/client/web/tests/index.js @@ -0,0 +1,167 @@ +// import Linto from '../dist/linto.min.js' +import Linto from '../src/linto.js' + +let mqttConnectHandler = function(event) { + console.log("mqtt up !") +} + +let mqttConnectFailHandler = function(event) { + console.log("Mqtt failed to connect : ") + console.log(event) +} + +let mqttErrorHandler = function(event) { + console.log("An MQTT error occured : ", event.detail) +} + +let mqttDisconnectHandler = function(event) { + console.log("MQTT Offline") +} + +let audioSpeakingOn = function(event) { + console.log("Speaking") +} + +let audioSpeakingOff = function(event) { + console.log("Not speaking") +} + +let commandAcquired = function(event) { + console.log("Command acquired") +} + +let commandPublished = function(event) { + console.log("Command published id :", event.detail) +} + +let actionAcquired = function(event) { + console.log("action acquired") +} + +let actionPublished = function(event) { + console.log("action published id :", event.detail) +} + +let actionFeedback = function(event) { + console.log("action feedback :", event) +} + +let actionError = function(event) { + console.log("action error :", event) +} + +let textAcquired = function(event) { + console.log("text acquired") +} + +let textPublished = function(event) { + console.log("text published id :", event.detail) +} + +let chatbotAcquired = function(event) { + console.log("chatbot text acquired") +} + +let chatbotPublished = function(event) { + console.log("chatbot text published id :", event.detail) +} + +let widgetFeedback = function(event) { + console.log("chatbot feedback :", event) +} + +let chatbotError = function(event) { + console.log("chatbot error :", event) +} + +let hotword = function(event) { + console.log("Hotword triggered : ", event.detail) +} + +let commandTimeout = function(event) { + console.log("Command timeout, id : ", event.detail) +} + +let sayFeedback = async function(event) { + console.log("Saying : ", event.detail.behavior.say.text, " ---> Answer to : ", event.detail.transcript) + await linto.say(linto.lang, event.detail.behavior.say.text) +} + +let askFeedback = async function(event) { + console.log("Asking : ", event.detail.behavior.ask.text, " ---> Answer to : ", event.detail.transcript) + await linto.ask(linto.lang, event.detail.behavior.ask.text) +} + +let streamingChunk = function(event) { + if (event.detail.behavior.streaming.partial) + console.log("Streaming chunk received : ", event.detail.behavior.streaming.partial) + if (event.detail.behavior.streaming.text) + console.log("Streaming utterance completed : ", event.detail.behavior.streaming.text) +} + +let streamingStart = function(event) { + console.log("Streaming started with no errors") +} + +let streamingStop = function(event) { + console.log("Streaming stoped with no errors") +} + +let streamingFinal = function(event) { + console.log("Streaming ended, here's the final transcript : ", event.detail.behavior.streaming.result) +} + +let streamingFail = function(event) { + console.log("Streaming error : ", event.detail) +} + +let customHandler = function(event) { + console.log(`${event.detail.behavior.customAction.kind} fired`) + console.log(event.detail.behavior) + console.log(event.detail.transcript) +} + + + +window.start = async function() { + try { + window.linto = new Linto("https://stage.linto.ai/overwatch/local/web/login", "v2lS299nR5Fv8k7Q", 10000) + // Some feedbacks for UX implementation + linto.addEventListener("mqtt_connect", mqttConnectHandler) + linto.addEventListener("mqtt_connect_fail", mqttConnectFailHandler) + linto.addEventListener("mqtt_error", mqttErrorHandler) + linto.addEventListener("mqtt_disconnect", mqttDisconnectHandler) + linto.addEventListener("speaking_on", audioSpeakingOn) + linto.addEventListener("speaking_off", audioSpeakingOff) + linto.addEventListener("command_acquired", commandAcquired) + linto.addEventListener("command_published", commandPublished) + linto.addEventListener("command_timeout", commandTimeout) + linto.addEventListener("hotword_on", hotword) + linto.addEventListener("say_feedback_from_skill", sayFeedback) + linto.addEventListener("ask_feedback_from_skill", askFeedback) + linto.addEventListener("custom_action_from_skill", customHandler) + linto.addEventListener("chatbot_feedback_from_skill", widgetFeedback) + linto.addEventListener("text_acquired", textAcquired) + linto.addEventListener("text_published", textPublished) + linto.addEventListener("chatbot_acquired", chatbotAcquired) + linto.addEventListener("chatbot_published", chatbotPublished) + linto.addEventListener("chatbot_feedback", widgetFeedback) + linto.addEventListener("chatbot_error", chatbotError) + linto.addEventListener("action_feedback", actionFeedback) + linto.addEventListener("action_error", actionError) + linto.addEventListener("streaming_start", streamingStart) + linto.addEventListener("streaming_stop", streamingStop) + linto.addEventListener("streaming_chunk", streamingChunk) + linto.addEventListener("streaming_final", streamingFinal) + linto.addEventListener("streaming_fail", streamingFail) + await linto.login() + linto.startAudioAcquisition(true, "linto", 0.99) // Uses hotword built in WebVoiceSDK by name / model / threshold (0.99 is fine enough) + linto.startCommandPipeline() + return true + } catch (e) { + return e.message + } + +} + +start() \ No newline at end of file diff --git a/client/web/tests/linto-ui/index.html b/client/web/tests/linto-ui/index.html new file mode 100644 index 0000000..a9a21bb --- /dev/null +++ b/client/web/tests/linto-ui/index.html @@ -0,0 +1,33 @@ + + + + + + + LinTO Chatbot test + +
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/client/web/tests/linto-ui/index.js b/client/web/tests/linto-ui/index.js new file mode 100644 index 0000000..0b85019 --- /dev/null +++ b/client/web/tests/linto-ui/index.js @@ -0,0 +1,16 @@ +import LintoUI from "../../src/linto-ui.js" + +window.LintoUI = new LintoUI({ + debug: true, + containerId: "chatbot-wrapper", + lintoWebToken: "wdyEXAlwSFY3WjvD", //linagora.com chatbot flow + lintoWebHost: "https://gamma.linto.ai/overwatch/local/web/login", + widgetMode: "multi-modal", + transactionMode: "chatbot_only", +}) + +const formNameBtn = document.getElementById("form-name-button") +formNameBtn.onclick = function () { + formNameBtn.classList.add("streaming-on") + window.LintoUI.customStreaming("vad-custom", "form-name") +} diff --git a/platform/business-logic-server/.dockerenv b/platform/business-logic-server/.dockerenv new file mode 100644 index 0000000..042d760 --- /dev/null +++ b/platform/business-logic-server/.dockerenv @@ -0,0 +1,27 @@ +LINTO_SHARED_MOUNT=~/linto_shared_mount/ +LINTO_STACK_DOMAIN=localhost +# LINTO_STACK_NPM_CUSTOM_REGISTRY=https://registry.npmjs.org/ +# NODE_ENV=production + +#LINTO_STACK Configuration +LINTO_STACK_USE_SSL=false + +#LINTO-RED Configuration +LINTO_STACK_BLS_HTTP_PORT=80 + +LINTO_STACK_BLS_SERVICE_UI_PATH=/redui +LINTO_STACK_BLS_SERVICE_API_PATH=/red + +LINTO_STACK_BLS_USE_LOGIN=false +LINTO_STACK_BLS_USER= +LINTO_STACK_BLS_PASSWORD= + +#STACK Configuration +LINTO_STACK_OVERWATCH_SERVICE=linto-platform-overwatch +LINTO_STACK_OVERWATCH_BASE_PATH=/overwatch + +LINTO_STACK_BLS_API_MAX_LENGTH=5mb + +LINTO_STACK_TOCK_BOT_API=linto-tock-bot-api +LINTO_STACK_TOCK_SERVICE=linto-tock-nlu-web +LINTO_STACK_TOCK_NLP_API=linto-tock-nlp-api \ No newline at end of file diff --git a/platform/business-logic-server/.dockerignore b/platform/business-logic-server/.dockerignore new file mode 100644 index 0000000..f0cf935 --- /dev/null +++ b/platform/business-logic-server/.dockerignore @@ -0,0 +1,11 @@ +Dockerfile +.env +.dockerenv +.dockerignore +.git +.gitignore +.gitlab-ci.yml +docker-compose.yml +node_modules/ +flow-storage/ +local-settings/ \ No newline at end of file diff --git a/platform/business-logic-server/.envdefault b/platform/business-logic-server/.envdefault new file mode 100644 index 0000000..cd81e1c --- /dev/null +++ b/platform/business-logic-server/.envdefault @@ -0,0 +1,24 @@ +LINTO_SHARED_MOUNT=~/linto_shared_mount/ + +#LINTO_STACK Configuration +LINTO_STACK_USE_SSL=false + +#LINTO-RED Configuration +LINTO_STACK_BLS_HTTP_PORT= + +LINTO_STACK_BLS_SERVICE_UI_PATH=/redui +LINTO_STACK_BLS_SERVICE_API_PATH=/red + +LINTO_STACK_BLS_USE_LOGIN=false +LINTO_STACK_BLS_USER= +LINTO_STACK_BLS_PASSWORD= + +#STACK Configuration +LINTO_STACK_OVERWATCH_SERVICE=linto-platform-overwatch +LINTO_STACK_OVERWATCH_BASE_PATH=/overwatch + +LINTO_STACK_BLS_API_MAX_LENGTH=5mb + +LINTO_STACK_TOCK_BOT_API=linto-tock-bot-api +LINTO_STACK_TOCK_SERVICE=linto-tock-nlu-web +LINTO_STACK_TOCK_NLP_API=linto-tock-nlp-api \ No newline at end of file diff --git a/platform/business-logic-server/.eslintrc.json b/platform/business-logic-server/.eslintrc.json new file mode 100644 index 0000000..6e4816b --- /dev/null +++ b/platform/business-logic-server/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "extends": "strongloop", + "env": { + "es6": true, + "mocha": true + }, + "parserOptions": { + "ecmaVersion": 2017 +}, + "rules" :{ + "no-unused-vars" : ["error", { "varsIgnorePattern": "debug" }], + "semi": ["error", "never"], + "max-len": ["error", { "code": 100 , "ignoreComments": true }], + "radix": ["error", "as-needed"] + } +} diff --git a/platform/business-logic-server/.github/workflows/dockerhub-description.yml b/platform/business-logic-server/.github/workflows/dockerhub-description.yml new file mode 100644 index 0000000..14857f8 --- /dev/null +++ b/platform/business-logic-server/.github/workflows/dockerhub-description.yml @@ -0,0 +1,20 @@ +name: Update Docker Hub Description +on: + push: + branches: + - master + paths: + - README.md + - .github/workflows/dockerhub-description.yml +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + repository: lintoai/linto-platform-business-logic-server + readme-filepath: ./README.md diff --git a/platform/business-logic-server/.gitignore b/platform/business-logic-server/.gitignore new file mode 100644 index 0000000..b327f00 --- /dev/null +++ b/platform/business-logic-server/.gitignore @@ -0,0 +1,28 @@ +.DS_store +.config.json +.dist +.jshintignore +.npm +.project +.sessions.json +.settings +.tern-project +*.backup +*_cred* +coverage +credentials.json +flows*.json +nodes/node-red-nodes/ +local-settings +linto-skill +node_modules +locales/zz-ZZ +nodes/core/locales/zz-ZZ +!packages/node_modules +packages/node_modules/@node-red/editor-client/public +!test/**/node_modules +docs +!packages/node_modules/**/docs +**/package-lock.json +**/.env +catalogues/catalogues.json diff --git a/platform/business-logic-server/Dockerfile b/platform/business-logic-server/Dockerfile new file mode 100644 index 0000000..33fd5b0 --- /dev/null +++ b/platform/business-logic-server/Dockerfile @@ -0,0 +1,40 @@ +FROM node:latest + +WORKDIR /usr/src/app/business-logic-server + +COPY . /usr/src/app/business-logic-server +RUN npm install && \ + npm install @linto-ai/node-red-linto-core && \ + npm install @linto-ai/node-red-linto-calendar && \ + npm install @linto-ai/node-red-linto-datetime && \ + npm install @linto-ai/node-red-linto-definition && \ + npm install @linto-ai/node-red-linto-meeting && \ + npm install @linto-ai/node-red-linto-memo && \ +# npm install @linto-ai/node-red-linto-news && \ + npm install @linto-ai/node-red-linto-pollution && \ + npm install @linto-ai/node-red-linto-weather && \ + npm install @linto-ai/node-red-linto-welcome && \ + npm install @linto-ai/linto-skill-room-control && \ + npm install @linto-ai/linto-skill-browser-control + +# RUN npm install +# RUN npm install @linto-ai/node-red-linto-core +# RUN npm install @linto-ai/node-red-linto-calendar +# RUN npm install @linto-ai/node-red-linto-datetime +# RUN npm install @linto-ai/node-red-linto-definition +# RUN npm install @linto-ai/node-red-linto-meeting +# RUN npm install @linto-ai/node-red-linto-memo +# RUN npm install @linto-ai/node-red-linto-news +# RUN npm install @linto-ai/node-red-linto-pollution +# RUN npm install @linto-ai/node-red-linto-weather +# RUN npm install @linto-ai/node-red-linto-welcome +# RUN npm install @linto-ai/linto-skill-room-control +# RUN npm install @linto-ai/linto-skill-browser-control + +HEALTHCHECK CMD node docker-healthcheck.js || exit 1 +EXPOSE 80 + +COPY ./docker-entrypoint.sh / + +ENTRYPOINT ["/docker-entrypoint.sh"] +# CMD ["node", "index.js"] diff --git a/platform/business-logic-server/README.md b/platform/business-logic-server/README.md new file mode 100644 index 0000000..322c288 --- /dev/null +++ b/platform/business-logic-server/README.md @@ -0,0 +1,51 @@ +# Linto-Platform-Business-Logic-Server +This services is mandatory in a complete LinTO platform stack as the main process that actualy executes a workflow defined as a collection of LinTO skills. This service itself mainly consists of a wrapper for a node-red runtime. Any user defined context on linto-admin (a given set of configured skills) is therefore backed by a node-red flow. + +## Define LinTO contexts as node-red flows +This service provides for a node-red web interface wich is meant to get embedded in the main [LinTO platform admin web interface](https://github.com/linto-ai/linto-platform-admin/). LinTO skills are node-red _nodes_ + +# Develop + +## Install project +``` +git clone https://github.com/linto-ai/Business-Logic-Server.git +cd Business-Logic-Server +npm install +``` + +### Configuration environement +`cp .envdefault .env` +Then update the `.env` to manage your personal configuration + +### Red Settings +Node-Red provide a configuration file `lib/node-red/settings/settings.js`. +More information can be found on node-red website : [Settings.js](https://nodered.org/docs/user-guide/runtime/settings-file) + +Custom catalogue can be setup on the `settings.js` +```json +editorTheme:{ + palette: { + catalogues: [ + 'https://my.custom.registry/catalogue.json' + ] + } +} +``` +*Note that the .npmrc need to be configured to be used with a custom registry* + +### Run project +Normal : `npm run start` +Debug : `DEBUG=* npm run start` + +### Interface connect +By default you can reach the user interface on [http://localhost:9000](http://localhost:9000) + +## Docker +### Install Docker and Docker Compose +You will need to have Docker and Docker Compose installed on your machine. If they are already installed, you can skip this part. +Otherwise, you can install them referring to [docs.docker.com/engine/installation/](https://docs.docker.com/engine/installation/ "Install Docker"), and to [docs.docker.com/compose/install/](https://docs.docker.com/compose/install/ "Install Docker Compose"). + +### Build +You can build the docker with `docker-compose build` +Then run it with `docker-compose run` +Then you can acces it on [localhost:9000](http://localhost:9000) diff --git a/platform/business-logic-server/RELEASE.md b/platform/business-logic-server/RELEASE.md new file mode 100644 index 0000000..d8c11b3 --- /dev/null +++ b/platform/business-logic-server/RELEASE.md @@ -0,0 +1,17 @@ +# 1.1.2 +- Added tock env settings +- Update css + +# 1.1.1 +- Update node-red version +- Add api route for install zip or tar module +- Disable catalogue feature + +# 1.1.0 +- Add custom node-red script and css +- Environement variable harmonization +- Clean for linto v2 + +# 1.0.0 +- Manage an express for RED +- Added express route (config/admin) \ No newline at end of file diff --git a/platform/business-logic-server/asset/linto.png b/platform/business-logic-server/asset/linto.png new file mode 100644 index 0000000000000000000000000000000000000000..0d746d882f949d4f8e7062eff3f251a6cf24ac0e GIT binary patch literal 53054 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 literal 0 HcmV?d00001 diff --git a/platform/business-logic-server/asset/linto_min.png b/platform/business-logic-server/asset/linto_min.png new file mode 100644 index 0000000000000000000000000000000000000000..108f00a2d4e47b9eba8e0df92043647a2c6c2f8d GIT binary patch literal 40118 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 { + if (error) { + throw error + } +}) diff --git a/platform/business-logic-server/index.js b/platform/business-logic-server/index.js new file mode 100644 index 0000000..a6c76ad --- /dev/null +++ b/platform/business-logic-server/index.js @@ -0,0 +1,19 @@ +const debug = require('debug')('linto-red:ctl') +require('./config') + +class Ctl { + constructor() { + this.init() + } + async init() { + try { + this.webServer = await require('./lib/webserver') + debug(`Application is started - Listening on ${process.env.LINTO_STACK_BLS_HTTP_PORT}`) + } catch (error) { + console.error(error) + process.exit(1) + } + } +} + +new Ctl() diff --git a/platform/business-logic-server/jenkins-deployment/Dockerfile b/platform/business-logic-server/jenkins-deployment/Dockerfile new file mode 100644 index 0000000..582a30f --- /dev/null +++ b/platform/business-logic-server/jenkins-deployment/Dockerfile @@ -0,0 +1,32 @@ +FROM node:latest + +WORKDIR /usr/src/app/business-logic-server + +COPY . /usr/src/app/business-logic-server + +ARG VERDACCIO_USR +ARG VERDACCIO_PSW +ARG VERDACCIO_REGISTRY_HOST + +RUN npm config set registry http://${VERDACCIO_USR}:${VERDACCIO_PSW}@${VERDACCIO_REGISTRY_HOST} && npm install && \ + npm install @linto-ai/node-red-linto-core && \ + npm install @linto-ai/node-red-linto-calendar && \ + npm install @linto-ai/node-red-linto-datetime && \ + npm install @linto-ai/node-red-linto-definition && \ + npm install @linto-ai/node-red-linto-meeting && \ + npm install @linto-ai/node-red-linto-memo && \ + npm install @linto-ai/node-red-linto-news && \ + npm install @linto-ai/node-red-linto-pollution && \ + npm install @linto-ai/node-red-linto-weather && \ + npm install @linto-ai/node-red-linto-welcome && \ + npm install @linto-ai/linto-skill-room-control && \ + npm install @linto-ai/linto-skill-browser-control && \ + npm config set registry https://registry.npmjs.org/ + +HEALTHCHECK CMD node docker-healthcheck.js || exit 1 +EXPOSE 80 + +COPY ./docker-entrypoint.sh / + +ENTRYPOINT ["/docker-entrypoint.sh"] +# CMD ["node", "index.js"] diff --git a/platform/business-logic-server/lib/node-red/css/nodered-custom.css b/platform/business-logic-server/lib/node-red/css/nodered-custom.css new file mode 100644 index 0000000..02c6e4c --- /dev/null +++ b/platform/business-logic-server/lib/node-red/css/nodered-custom.css @@ -0,0 +1,184 @@ +/*** HIDE ELEMENTS ***/ + + +/* Deploy */ + +.red-ui-deploy-button-group { + display: none !important; +} + + +/* Sidebar (right) */ + + +/*#red-ui-sidebar, +#red-ui-sidebar-separator { + display: none !important; +}*/ + +.red-ui-sidebar-info.show-tips .red-ui-sidebar-info-stack { + display: none !important; +} + + +/* Tabs */ + +.red-ui-tabs.red-ui-tabs-add.red-ui-tabs-search .red-ui-tab-scroll-right { + display: none !important; +} + +.red-ui-tabs.red-ui-tabs-add.red-ui-tabs-search.red-ui-tabs-scrollable { + padding-right: 0px !important; +} + +#red-ui-workspace-tabs { + width: 100%; +} + +.red-ui-tabs ul#red-ui-workspace-tabs li { + display: none !important; +} + +.red-ui-tabs ul#red-ui-workspace-tabs li.active { + display: inline-block !important; +} + + +/* Button add tabs */ + +.red-ui-tab-button.red-ui-tabs-add { + display: none !important; +} + + +/* user menu */ + +#red-ui-header-button-user { + display: none !important; +} + + +/*list palette */ + +.red-ui-tab-button.red-ui-tabs-search { + display: none !important; +} + + +/*** END HIDE ELEMENTS ***/ + + +/*** STYLE ELEMENTS ***/ + +#red-ui-header { + background-color: #6989aa; +} + +#red-ui-header span.red-ui-header-logo span { + color: #fff; + font-weight: 600; +} + + +/* Sidemenu buttons */ + +#red-ui-header .button { + border-color: #434C5F; + color: #fff; +} + +#red-ui-header-button-sidemenu.button { + color: #fff; +} + +#red-ui-header-button-sidemenu.button:hover { + color: #45baeb; +} + +#red-ui-header .button:hover, +#red-ui-header .button:focus { + background-color: #434C5F; + border-color: #434C5F; +} + + +/* Submenu */ + +#red-ui-header ul.red-ui-menu-dropdown { + border-color: #434C5F; + background-color: #434C5F; +} + + +/* Submenu divider */ + +#red-ui-header ul.red-ui-menu-dropdown li.red-ui-menu-divider { + background-color: #6989aa; +} + + +/* Submenu hover */ + +#red-ui-header ul.red-ui-menu-dropdown>li>a:hover, +#red-ui-header ul.red-ui-menu-dropdown>li>a:focus, +#red-ui-header ul.red-ui-menu-dropdown>li:hover>a, +#red-ui-header ul.red-ui-menu-dropdown>li:focus>a { + background-color: #45baeb !important; + color: #fff; +} + + +/* Deploy button */ + +#red-ui-header .button-group.red-ui-deploy-button-group { + display: none; +} + +#red-ui-header ul#red-ui-header-button-deploy-options-submenu li a:hover .red-ui-menu-sublabel { + color: #fff; +} + + +/*** EDITOR CONFIG **/ + +.red-ui-editor .form-row, +.red-ui-editor-dialog .form-row { + height: auto; +} + + + + +#confidence-helper{ + display: inline-block; + width: 16px; + height: 14px; + padding-top: 2px; + background: #333; + text-align: center; + position: relative; + color: #fff; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + } + #confidence-helper:hover{ + background-color: #ccc; + color: #555; + } + #confidence-helper:hover:after{ + display: inline-block; + width: 120px; + height: auto; + background-color: #ececec; + border: 1px solid #ccc; + color: #555; + padding: 10px; + position: absolute; + top: 0; + left: 30px; + text-align: left; + font-family: "Helvetica Neue",Arial,Helvetica,sans-serif; + font-size: 14px; + line-height: 18px; + } \ No newline at end of file diff --git a/platform/business-logic-server/lib/node-red/index.js b/platform/business-logic-server/lib/node-red/index.js new file mode 100644 index 0000000..eb85377 --- /dev/null +++ b/platform/business-logic-server/lib/node-red/index.js @@ -0,0 +1,161 @@ +const debug = require('debug')('linto-red:node-red') + +const fs = require('fs') +const path = require('path') +const decompress = require('decompress') +const tar = require('tar-fs') +const http = require('http') +const bcrypt = require('bcryptjs') + +let redSettings = require('./settings/settings.js') +let RED = require('node-red') + +const TRANSFORM_EXTENSION_SUPPORTED = ['.zip'] +const EXTENSION_SUPPORTED = ['.tar', '.tar.gz', '.gz'] + +function ifHas(element, defaultValue) { + if (!element) return defaultValue + return element +} + +class RedManager { + constructor(webServer) { + return this.init(webServer) + } + + async init(express) { + let server = http.createServer(express) + if (process.env.LINTO_STACK_BLS_HTTP_PORT) + if (process.env.LINTO_STACK_BLS_USE_LOGIN === 'false') { + delete redSettings.adminAuth + } else { + const hashPassword = bcrypt.hashSync(process.env.LINTO_STACK_BLS_PASSWORD, 8) + + redSettings.adminAuth = { + type: 'credentials', + users: [{ + username: process.env.LINTO_STACK_BLS_USER, + password: hashPassword, + permissions: '*', + }], + } + } + + redSettings.httpAdminRoot = ifHas(process.env.LINTO_STACK_BLS_SERVICE_UI_PATH, redSettings.httpAdminRoot) + redSettings.httpNodeRoot = ifHas(process.env.LINTO_STACK_BLS_SERVICE_API_PATH, redSettings.httpNodeRoot) + + //Load auth service + if (process.env.LINTO_STACK_OVERWATCH_SERVICE && process.env.LINTO_STACK_OVERWATCH_BASE_PATH) + redSettings.functionGlobalContext.authServerHost = process.env.LINTO_STACK_OVERWATCH_SERVICE + process.env.LINTO_STACK_OVERWATCH_BASE_PATH + + redSettings.functionGlobalContext.sslStack = "http://" + if (process.env.LINTO_STACK_USE_SSL === 'true') { + redSettings.functionGlobalContext.sslStack = "https://" + } + + // redSettings.editorTheme.palette.catalogues.push(redSettings.functionGlobalContext.sslStack + process.env.LINTO_STACK_DOMAIN + '/red/catalogue') + redSettings.apiMaxLength = process.env.LINTO_STACK_BLS_API_MAX_LENGTH + + RED.init(server, redSettings) + + express.use(ifHas(process.env.LINTO_STACK_BLS_SERVICE_UI_PATH, redSettings.httpAdminRoot), RED.httpAdmin) + express.use(ifHas(process.env.LINTO_STACK_BLS_SERVICE_API_PATH, redSettings.httpNodeRoot), RED.httpNode) + + server.listen(ifHas(process.env.LINTO_STACK_BLS_HTTP_PORT, redSettings.uiPort)) + server.timeout = 360000 + + const events = RED.events + events.once('flows:started', () => { + if (redSettings.disableList) { + for (let i in RED.nodes.getNodeList()) { + if (redSettings.disableList.indexOf(RED.nodes.getNodeList()[i].name) > -1) { + RED.nodes.disableNode(RED.nodes.getNodeList()[i].id) + } + } + } + }) + + + await RED.start() + + express.get('/red/print', function (req, res) { + res.send(RED.nodes.getNodeList()) + }) + + express.post('/red/node/module/:nodeName', async function (req, res) { + if (req.params.nodeNameModule) { + await RED.runtime.nodes.addModule({ + module: req.params.nodeNameModule + }) + return res.send('Node installed') + } + return res.status(500).send('nodeName is missing') + }) + + express.post('/red/node/file', async function (req, res) { + if (!req.files || Object.keys(req.files).length === 0) { + return res.status(400).send('No files were uploaded.') + } + + let sampleFile, extensionFile + sampleFile = req.files.files + extensionFile = path.extname(sampleFile.name) + + if (TRANSFORM_EXTENSION_SUPPORTED.includes(extensionFile)) { + try { + const pathExtract = '/tmp/skills/' + path.basename(sampleFile.name, extensionFile) + + decompress(sampleFile.data, pathExtract).then((files) => { + tar.pack(pathExtract, { + map: function (header) { + header.name = './' + header.name + return header + } + }).pipe(fs.createWriteStream(pathExtract + '.tar')) + .on('finish', async () => { + let tarFiles = fs.readFileSync(pathExtract + '.tar') + const moduleToAdd = { + tarball: { + name: path.basename(sampleFile.name, extensionFile), + size: tarFiles.length, + buffer: tarFiles + } + } + + await RED.runtime.nodes.addModule(moduleToAdd).then(node => { + return res.status(200).send(node) + }).catch((err) => { + if (err.code === 'module_already_loaded') return res.status(202).send({ error: 'module already loaded' }) + else return res.status(400).send({ error: err.message }) + }) + }) + }) + } catch (err) { return res.status(err.status).send({ error: err.code }) } + } + + else if (EXTENSION_SUPPORTED.includes(extensionFile)) { + try { + let moduleToAdd = { + tarball: { + name: sampleFile.name, + size: sampleFile.size, + buffer: sampleFile.data + } + } + + await RED.runtime.nodes.addModule(moduleToAdd).then(node => { + return res.status(200).send(node) + }).catch((err) => { + if (err.code === 'module_already_loaded') return res.status(202).send({ error: 'module already loaded' }) + else return res.status(400).send({ error: err.message }) + }) + + } catch (err) { return res.status(err.status).send({ error: err.code }) } + } + + else return res.status(400).send({ error: 'Wrong extension. Supported extension are : .zip, .tar and .tar.gz' }) + }) + } +} + +module.exports = RedManager \ No newline at end of file diff --git a/platform/business-logic-server/lib/node-red/js/nodered-custom.js b/platform/business-logic-server/lib/node-red/js/nodered-custom.js new file mode 100644 index 0000000..49588c2 --- /dev/null +++ b/platform/business-logic-server/lib/node-red/js/nodered-custom.js @@ -0,0 +1,144 @@ +window.onload = async() => { + let uriConfigAdmin = document.location.origin + '/red/config/admin' + let apiUri = await initApi(uriConfigAdmin) + + async function initApi(uriConfigAdmin) { + return new Promise((resolve, reject) => { + fetch(uriConfigAdmin, { + method: 'GET', + headers: {}, + }).then(response => { + return response.json() + }).then(data => { + console.log(data) + if (!!data.admin) + resolve(data.admin) + }).catch(err => { + reject(err) + }) + }) + } + + async function getFullFlow(workspaceId) { + const fullFlow = RED.nodes.createCompleteNodeSet() + let configNodeIds = [] + let formattedFlow = fullFlow + .filter(flow => flow.id === workspaceId || flow.z === workspaceId) + .map(flow => { + if (flow.type === 'linto-config') { + configNodeIds.push(flow.configMqtt) + configNodeIds.push(flow.configEvaluate) + configNodeIds.push(flow.configTranscribe) + } + return flow + }) + let configNodes = fullFlow.filter(flow => configNodeIds.indexOf(flow.id) >= 0) + formattedFlow.push(...configNodes) + return formattedFlow + } + + async function saveTmpFlow(flow) { + const payload = { + payload: flow, + workspaceId: window['workspace_active'], + } + + let updateTmp = await fetch(`${apiUri}/flow/tmp`, { + method: 'put', + headers: new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(payload), + }).then(function(response) { + return response.json() + }).then(function(data) { + return data + }) + + if (updateTmp.status === 'error') { + alert('an error has occured') + } + } + + // Save TMP Flow on change workspace + RED.events.on('workspace:change', async function(status) { + window['workspace_active'] = status.workspace + const fullFlow = await getFullFlow(window['workspace_active']) + await saveTmpFlow(fullFlow) + }) + + // Save TMP Flow on change nodes + RED.events.on('nodes:change', async function() { + try { + window['workspace_active'] = RED.workspaces.active() + const fullFlow = await getFullFlow(window['workspace_active']) + await saveTmpFlow(fullFlow) + } catch (err) { + console.log(err) + } + }) + + // Save TMP Flow on change nodes + RED.events.on('view:selection-changed', async function() { + try { + window['workspace_active'] = RED.workspaces.active() + const fullFlow = await getFullFlow(window['workspace_active']) + await saveTmpFlow(fullFlow) + } catch (err) { + console.log(err) + } + }) + + RED.events.on('editor:close', async function() { + try { + window['workspace_active'] = RED.workspaces.active() + const fullFlow = await getFullFlow(window['workspace_active']) + await saveTmpFlow(fullFlow) + } catch (err) { + console.log(err) + } + }) + + /* + RED.events.on > + const events = [ + 'view:selection-changed', + 'sidebar:resize', + 'workspace:change', + 'registry:node-type-added', + 'registry:node-type-removed', + 'registry:node-set-enabled', + 'registry:node-set-disabled', + 'registry:node-set-removed', + 'subflows:change', + 'registry:module-updated', + 'registry:node-set-added', + 'nodes:add', + 'nodes:remove', + 'projects:load', + 'flows:add', + 'flows:remove', + 'flows:change', + 'flows:reorder', + 'subflows:add', + 'subflows:remove', + 'nodes:change', + 'groups:add', + 'groups:remove', + 'groups:change', + 'workspace:clear', + 'editor:open', + 'editor:close', + 'search:open', + 'search:close', + 'actionList:open', + 'actionList:close', + 'type-search:open', + 'type-search:close', + 'workspace:dirty', + 'project:change', + 'editor:save', + 'layout:update' + ]*/ +} \ No newline at end of file diff --git a/platform/business-logic-server/lib/node-red/settings/settings.js b/platform/business-logic-server/lib/node-red/settings/settings.js new file mode 100644 index 0000000..074e908 --- /dev/null +++ b/platform/business-logic-server/lib/node-red/settings/settings.js @@ -0,0 +1,289 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +// The `https` setting requires the `fs` module. Uncomment the following +// to make it available: +// var fs = require("fs"); + +'use strict' + +module.exports = { + // the tcp port that the Node-RED web server is listening on + uiPort: 1880, + + // By default, the Node-RED UI accepts connections on all IPv4 interfaces. + // To listen on all IPv6 addresses, set uiHost to "::", + // The following property can be used to listen on a specific interface. For + // example, the following would only allow connections from the local machine. + // uiHost: "127.0.0.1", + + // Retry time in milliseconds for MQTT connections + mqttReconnectTime: 15000, + + // Retry time in milliseconds for Serial port connections + serialReconnectTime: 15000, + + // Disable node that will be remove + disableList: ['sentiment', 'link', 'exec', 'email', 'template', 'delay', 'trigger', 'rpi-gpio', + 'tls', 'websocket', 'watch', 'tcpin', 'udp', 'switch', 'change', 'range', 'sort', 'batch', + 'CSV', 'HTML', 'JSON', 'XML', 'YAML', 'tail', 'file', 'feedparse', 'rbe', 'twitter' + ], + + // Retry time in milliseconds for TCP socket connections + // socketReconnectTime: 10000, + + // Timeout in milliseconds for TCP server socket connections + // defaults to no timeout + // socketTimeout: 120000, + + // Maximum number of messages to wait in queue while attempting to connect to TCP socket + // defaults to 1000 + // tcpMsgQueueSize: 2000, + + // Timeout in milliseconds for HTTP request connections + // defaults to 120 seconds + // httpRequestTimeout: 120000, + + // The maximum length, in characters, of any message sent to the debug sidebar tab + debugMaxLength: 1000, + + // The maximum number of messages nodes will buffer internally as part of their + // operation. This applies across a range of nodes that operate on message sequences. + // defaults to no limit. A value of 0 also means no limit is applied. + // nodeMaxMessageBufferLength: 0, + + // To disable the option for using local files for storing keys and certificates in the TLS configuration + // node, set this to true + // tlsConfigDisableLocalFiles: true, + + // Colourise the console output of the debug node + // debugUseColors: true, + + // The file containing the flows. If not set, it defaults to flows_.json + flowFile: 'flowsStorage.json', + + // To enabled pretty-printing of the flow within the flow file, set the following + // property to true: + // flowFilePretty: true, + + // By default, credentials are encrypted in storage using a generated key. To + // specify your own secret, set the following property. + // If you want to disable encryption of credentials, set this property to false. + // Note: once you set this property, do not change it - doing so will prevent + // node-red from being able to decrypt your existing credentials and they will be + // lost. + // credentialSecret: "a-secret-key", + + // By default, all user data is stored in the Node-RED install directory. To + // use a different location, the following property can be used + userDir: process.env.HOME + '/.node-red/', + + // Node-RED scans the `nodes` directory in the install directory to find nodes. + // The following property can be used to specify an additional directory to scan. + // nodesDir: "node-user-dir/node_modules/", + + // By default, the Node-RED UI is available at http://localhost:1880/ + // The following property can be used to specify a different root path. + // If set to false, this is disabled. + // httpAdminRoot: '/redui', + + // Some nodes, such as HTTP In, can be used to listen for incoming http requests. + // By default, these are served relative to '/'. The following property + // can be used to specifiy a different root path. If set to false, this is + // disabled. + // httpNodeRoot: '/red-nodes', + + // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot', + // to apply the same root to both parts. + // httpRoot: '/red', + + // When httpAdminRoot is used to move the UI to a different root path, the + // following property can be used to identify a directory of static content + // that should be served at http://localhost:1880/. + // httpStatic: '/home/nol/node-red-static/', + + // The maximum size of HTTP request that will be accepted by the runtime api. + // Default: 5mb + // apiMaxLength: '5mb', + + // If you installed the optional node-red-dashboard you can set it's path + // relative to httpRoot + ui: { path: 'ui' }, + + // Securing Node-RED + // ----------------- + // To password protect the Node-RED editor and admin API, the following + // property can be used. See http://nodered.org/docs/security.html for details. + adminAuth: { + type: 'credentials', + users: [{ + username: 'admin', + password: '$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.', + permissions: '*', + }], + }, + + // To password protect the node-defined HTTP endpoints (httpNodeRoot), or + // the static content (httpStatic), the following properties can be used. + // The pass field is a bcrypt hash of the password. + // See http://nodered.org/docs/security.html#generating-the-password-hash + // httpNodeAuth: { user: "user", pass: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN." }, + // httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + + // The following property can be used to enable HTTPS + // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + // for details on its contents. + // See the comment at the top of this file on how to load the `fs` module used by + // this setting. + // + // https: { + // key: fs.readFileSync('privatekey.pem'), + // cert: fs.readFileSync('certificate.pem') + // }, + + // The following property can be used to cause insecure HTTP connections to + // be redirected to HTTPS. + // requireHttps: true, + + // The following property can be used to disable the editor. The admin API + // is not affected by this option. To disable both the editor and the admin + // API, use either the httpRoot or httpAdminRoot properties + // disableEditor: false, + + // The following property can be used to configure cross-origin resource sharing + // in the HTTP nodes. + // See https://github.com/troygoode/node-cors#configuration-options for + // details on its contents. The following is a basic permissive set of options: + // httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + // }, + + // If you need to set an http proxy please set an environment variable + // called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. + // For example - http_proxy=http://myproxy.com:8080 + // (Setting it here will have no effect) + // You may also specify no_proxy (or NO_PROXY) to supply a comma separated + // list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk + + // The following property can be used to add a custom middleware function + // in front of all http in nodes. This allows custom authentication to be + // applied to all http in nodes, or any other sort of common request processing. + // httpNodeMiddleware: function(req,res,next) { + // // Handle/reject the request, or pass it on to the http in node by calling next(); + // // Optionally skip our rawBodyParser by setting this to true; + // //req.skipRawBodyParser = true; + // next(); + // }, + + // The following property can be used to verify websocket connection attempts. + // This allows, for example, the HTTP request headers to be checked to ensure + // they include valid authentication information. + // webSocketNodeVerifyClient: function(info) { + // // 'info' has three properties: + // // - origin : the value in the Origin header + // // - req : the HTTP request + // // - secure : true if req.connection.authorized or req.connection.encrypted is set + // // + // // The function should return true if the connection should be accepted, false otherwise. + // // + // // Alternatively, if this function is defined to accept a second argument, callback, + // // it can be used to verify the client asynchronously. + // // The callback takes three arguments: + // // - result : boolean, whether to accept the connection or not + // // - code : if result is false, the HTTP error status to return + // // - reason: if result is false, the HTTP reason string to return + // }, + + // Anything in this hash is globally available to all functions. + // It is accessed as context.global. + // eg: + // functionGlobalContext: { os:require('os') } + // can be accessed in a function block as: + // context.global.os + + functionGlobalContext: { + // os:require('os'), + // jfive:require("johnny-five"), + // j5board:require("johnny-five").Board({repl:false}) + }, + + // Context Storage + // The following property can be used to enable context storage. The configuration + // provided here will enable file-based context that flushes to disk every 30 seconds. + // Refer to the documentation for further options: https://nodered.org/docs/api/context/ + // + // contextStorage: { + // default: { + // module:"localfilesystem" + // }, + // }, + + // The following property can be used to order the categories in the editor + // palette. If a node's category is not in the list, the category will get + // added to the end of the palette. + // If not set, the following default order is used: + paletteCategories: ['linto', 'settings', 'services', 'interface', 'skills', + 'dictionary', 'subflows', 'input', 'output', 'function', 'social', 'mobile', + 'storage', 'analysis', 'advanced' + ], + + // Configure the logging output + logging: { + // Only console logging is currently supported + console: { + // Level of logging to be recorded. Options are: + // fatal - only those errors which make the application unusable should be recorded + // error - record errors which are deemed fatal for a particular request + fatal errors + // warn - record problems which are non fatal + errors + fatal errors + // info - record information about the general running of the application + warn + error + fatal errors + // debug - record information which is more verbose than info + info + warn + error + fatal errors + // trace - record very detailed logging + debug + info + warn + error + fatal errors + // off - turn off all logging (doesn't affect metrics or audit) + level: 'info', + // Whether or not to include metric events in the log output + metrics: false, + // Whether or not to include audit events in the log output + audit: false, + }, + }, + + // Customising the editor + editorTheme: { + header: { + title: 'Linto', + image: process.cwd() + '/asset/linto_min.png', // or null to remove image + url: 'http://linto.ai', // optional url to make the header text/image a link to this url + }, + page: { + css: `${process.cwd()}/lib/node-red/css/nodered-custom.css`, + scripts: [`${process.cwd()}/lib/node-red/js/nodered-custom.js`], + }, + projects: { + // To enable the Projects feature, set this value to true + enabled: false, + }, + menu: { + 'menu-item-edit-palette': true, + }, + palette: { + editable: true, // Enable/disable the Palette Manager + catalogues: [ // Alternative palette manager catalogues + // 'https://catalogue.nodered.org/catalogue.json', + ], + }, + }, +} \ No newline at end of file diff --git a/platform/business-logic-server/lib/webserver/index.js b/platform/business-logic-server/lib/webserver/index.js new file mode 100644 index 0000000..21b3365 --- /dev/null +++ b/platform/business-logic-server/lib/webserver/index.js @@ -0,0 +1,31 @@ +'use strict' + +const debug = require('debug')('linto-red:webserver') +const express = require('express') +const fileUpload = require('express-fileupload'); + +const EventEmitter = require('eventemitter3') +const RedManager = require(process.cwd() + '/lib/node-red') +const bodyParser = require('body-parser') + +class WebServer extends EventEmitter { + constructor() { + super() + this.app = express() + + this.app.use(bodyParser.json({ limit: process.env.LINTO_STACK_BLS_API_MAX_LENGTH })) + + this.app.use('/', express.static('public')) + this.app.use(express.json()) + this.app.use(fileUpload()); + + require('./routes')(this) + return this.init() + } + + async init() { + await new RedManager(this.app) + return this + } +} +module.exports = new WebServer() diff --git a/platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/rawCatalogue.js b/platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/rawCatalogue.js new file mode 100644 index 0000000..effaba7 --- /dev/null +++ b/platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/rawCatalogue.js @@ -0,0 +1,17 @@ +const debug = require('debug')('linto-red:webserver:front:red:catalogue:raw') +const fetch = require("node-fetch") + +const fs = require('fs') + +module.exports = { + create: async function (jsonCatalogue, catalogueData) { + try { + let jsonStr = JSON.stringify(jsonCatalogue) + fs.mkdirSync(catalogueData.dirPath, { recursive: true }) + fs.writeFileSync(catalogueData.dirPath + catalogueData.fileName, jsonStr) + return jsonCatalogue + } catch (err) { + throw err + } + } +} \ No newline at end of file diff --git a/platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/verdaccio.js b/platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/verdaccio.js new file mode 100644 index 0000000..821c665 --- /dev/null +++ b/platform/business-logic-server/lib/webserver/routes/catalogue/catalogue/verdaccio.js @@ -0,0 +1,58 @@ +const debug = require('debug')('linto-red:webserver:front:red:catalogue:verdaccio') +const fetch = require("node-fetch") + +const fs = require('fs') + +async function getDataFromAPI(url) { + let response = await fetch(url) + let json = await response.json() + + if (response.ok) + return json + else { + throw json + } +} + +function writeFile(redCatalogue, catalogueData) { + let jsonStr = JSON.stringify(redCatalogue) + + fs.mkdirSync(catalogueData.dirPath, { recursive: true }) + fs.writeFileSync(catalogueData.dirPath + catalogueData.fileName, jsonStr) +} + +module.exports = { + create: async function (host, catalogueData) { + try { + let redCatalogue = { + name: "verdaccio-catalogue", + updated_at: new Date(), + modules: [] + } + + let url = host + "/-/verdaccio/packages" + let catalogue = await getDataFromAPI(url) + + host += "/-/web/detail/" + catalogue.map(mod_verdaccio => { + let catalogue_module = { + description: mod_verdaccio.description, + version: mod_verdaccio.version, + keywords: mod_verdaccio.keywords, + types: ["node-red"], + updated_at: mod_verdaccio.time, + id: mod_verdaccio.name, + url: host + mod_verdaccio.name, + pkg_url: mod_verdaccio.dist.tarball + + } + redCatalogue.modules.push(catalogue_module) + }) + + writeFile(redCatalogue, catalogueData) + return redCatalogue + } catch (err) { + throw err + } + } +} \ No newline at end of file diff --git a/platform/business-logic-server/lib/webserver/routes/catalogue/index.js b/platform/business-logic-server/lib/webserver/routes/catalogue/index.js new file mode 100644 index 0000000..5602add --- /dev/null +++ b/platform/business-logic-server/lib/webserver/routes/catalogue/index.js @@ -0,0 +1,51 @@ +'use strict' +const debug = require('debug')('linto-red:webserver:front:catalogue') +const fs = require('fs') +const verdaccio = require('./catalogue/verdaccio') +const catalogueJson = require('./catalogue/rawCatalogue') + +const catalogueData = { + dirPath: './catalogues/', + fileName: 'catalogues.json' +} + +module.exports = (webServer) => { + return { + '/catalogue/:type': { + method: 'post', + controller: async (req, res, next) => { + try { + if (req.params.type === 'raw') { + let catalogue = await catalogueJson.create(req.body, catalogueData) + res.status(200).json({ msg: "Catalogue generated : " + req.body.name, ...catalogue }) + return + } + + if (req.body.host === undefined) + res.status('409').json({ error: "Registry not defined" }) + + if (req.params.type === 'verdaccio') { + let catalogue = await verdaccio.create(req.body.host, catalogueData) + res.status(200).json({ msg: "Registry generated for : " + req.body.host, ...catalogue }) + } else { + res.status('409').json({ error: "Registry parser type not implemented" }) + } + } catch (err) { + res.status('404').json(err) + } + } + }, + '/catalogue': { + method: 'get', + controller: async (req, res, next) => { + try { + let jsonBuffer = fs.readFileSync(catalogueData.dirPath + catalogueData.fileName) + let json = JSON.parse(jsonBuffer) + res.status(200).json(json) + } catch (err) { + res.status('404').json({ error: "Catalogue not found" }) + } + } + } + } +} diff --git a/platform/business-logic-server/lib/webserver/routes/index.js b/platform/business-logic-server/lib/webserver/routes/index.js new file mode 100644 index 0000000..1609344 --- /dev/null +++ b/platform/business-logic-server/lib/webserver/routes/index.js @@ -0,0 +1,29 @@ +'use strict' + +const debug = require('debug')('linto-red:webserver:routes') + +const ifHasElse = (condition, ifHas, otherwise) => { + return !condition ? otherwise() : ifHas() +} + +class Route { + constructor(webServer) { + const routes = require('./routes.js')(webServer) + for (let level in routes) { + for (let path in routes[level]) { + const route = routes[level][path] + webServer.app[route.method]( + `${level}${path}`, + ifHasElse( + Array.isArray(route.controller), + () => Object.values(route.controller), + () => route.controller + ) + ) + } + } + } +} + +module.exports = webServer => new Route(webServer) + diff --git a/platform/business-logic-server/lib/webserver/routes/red/index.js b/platform/business-logic-server/lib/webserver/routes/red/index.js new file mode 100644 index 0000000..11f69bc --- /dev/null +++ b/platform/business-logic-server/lib/webserver/routes/red/index.js @@ -0,0 +1,23 @@ +'use strict' +const debug = require('debug')('linto-red:webserver:front:red') + +module.exports = (webServer) => { + return { + '/config/admin': { + method: 'get', + controller: async (req, res, next) => { + let baseRequest = "http://" + if (process.env.LINTO_STACK_USE_SSL === 'true') + baseRequest = "https://" + + res.status(200).json({ admin: baseRequest + process.env.LINTO_STACK_DOMAIN + '/api' }) + }, + }, + '/health': { + method: 'get', + controller: async (req, res, next) => { + res.sendStatus(200) + } + } + } +} diff --git a/platform/business-logic-server/lib/webserver/routes/routes.js b/platform/business-logic-server/lib/webserver/routes/routes.js new file mode 100644 index 0000000..4f6831c --- /dev/null +++ b/platform/business-logic-server/lib/webserver/routes/routes.js @@ -0,0 +1,16 @@ +const debug = require('debug')('linto-red:webserver:routes:routes') + +module.exports = (webServer) => { + + let routes = {} + + let redApi = require('./red')(webServer) + let catalogueApi = require('./catalogue')(webServer) + + routes[process.env.LINTO_STACK_BLS_SERVICE_API_PATH] = { + ...redApi, + ...catalogueApi + } + + return routes +} diff --git a/platform/business-logic-server/package.json b/platform/business-logic-server/package.json new file mode 100644 index 0000000..8263bc3 --- /dev/null +++ b/platform/business-logic-server/package.json @@ -0,0 +1,48 @@ +{ + "name": "business-logic-server", + "version": "1.1.2", + "description": "Connector to server functionality for linto (LinStt, OpenPaas, ...)", + "main": "index.js", + "scripts": { + "start": "node index.js", + "start-dev": "NODE_ENV=developpement DEBUG=* node index.js", + "pretest": "eslint --ignore-path .gitignore .", + "pretest-fix": "eslint --ignore-path .gitignore . --fix" + }, + "keywords": [ + "linto", + "node-red", + "docker" + ], + "author": "yhoupert@linagora.com", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@linto-ai/node-red-linto-core": "latest", + "body-parser": "^1.19.0", + "debug": "^4.1.0", + "decompress": "^4.2.1", + "dotenv": "^6.1.0", + "eventemitter3": "^3.1.0", + "express": "^4.16.4", + "express-fileupload": "^1.2.1", + "fs": "0.0.1-security", + "node-fetch": "^2.6.0", + "node-red": "^1.2.8", + "node-red-dashboard": "^2.19.0", + "path": "^0.12.7", + "tar-fs": "^2.1.1" + }, + "devDependencies": { + "eslint": "^5.16.0", + "eslint-config-strongloop": "^2.1.0", + "prettier": "^1.17.0" + }, + "homepage": "https://linto.ai/", + "repository": { + "type": "git", + "url": "https://github.com/linto-ai/Business-Logic-Server.git" + }, + "bugs": { + "url": "https://github.com/linto-ai/Business-Logic-Server/issues" + } +} diff --git a/platform/linto-admin/.docker_env b/platform/linto-admin/.docker_env new file mode 100644 index 0000000..8dfbeb0 --- /dev/null +++ b/platform/linto-admin/.docker_env @@ -0,0 +1,47 @@ +TZ=Europe/Paris + +LINTO_STACK_REDIS_SESSION_SERVICE=redis-admin +LINTO_STACK_REDIS_SESSION_SERVICE_PORT=6379 + +LINTO_STACK_TOCK_SERVICE=linto-stack-tock +LINTO_STACK_TOCK_SERVICE_PORT=8080 +LINTO_STACK_TOCK_USER=user +LINTO_STACK_TOCK_PASSWORD=password + +LINTO_STACK_STT_SERVICE_MANAGER_SERVICE=linto-stack-stt-service-manager + +LINTO_STACK_DOMAIN=127.0.0.1 +LINTO_STACK_ADMIN_HTTP_PORT=80 +LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS=http://127.0.0.1 +LINTO_STACK_ADMIN_COOKIE_SECRET=mysecretcookie + +LINTO_STACK_MONGODB_SERVICE=mongo-admin +LINTO_STACK_MONGODB_PORT=27017 +LINTO_STACK_MONGODB_DBNAME=lintoAdmin +LINTO_STACK_MONGODB_USE_LOGIN=true +LINTO_STACK_MONGODB_USER=root +LINTO_STACK_MONGODB_PASSWORD=example + +LINTO_STACK_MQTT_HOST=localhost +LINTO_STACK_MQTT_DEFAULT_HW_SCOPE=blk +LINTO_STACK_MQTT_PORT=1883 +LINTO_STACK_MQTT_USE_LOGIN=true +LINTO_STACK_MQTT_USER=user +LINTO_STACK_MQTT_PASSWORD=password + +LINTO_STACK_BLS_SERVICE=linto-stack-bls +LINTO_STACK_BLS_USE_LOGIN=true +LINTO_STACK_BLS_USER=LINTO_STACK_BLS_LOGIN +LINTO_STACK_BLS_PASSWORD=LINTO_STACK_BLS_PASSWORD +LINTO_STACK_BLS_SERVICE_UI_PATH=/redui +LINTO_STACK_BLS_SERVICE_API_PATH=/red-nodes + +LINTO_SHARED_MOUNT=~/linto_shared_mount +LINTO_STACK_USE_SSL=true +LINTO_STACK_USE_ACME=false +LINTO_STACK_ACME_EMAIL=contact@example.com +LINTO_STACK_HTTP_USE_AUTH=true +LINTO_STACK_HTTP_USER=user +LINTO_STACK_HTTP_PASSWORD=password + +VUE_APP_DEBUG=true \ No newline at end of file diff --git a/platform/linto-admin/.dockerignore b/platform/linto-admin/.dockerignore new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/platform/linto-admin/.dockerignore @@ -0,0 +1 @@ + diff --git a/platform/linto-admin/.github/workflows/dockerhub-description.yml b/platform/linto-admin/.github/workflows/dockerhub-description.yml new file mode 100644 index 0000000..56e6d7f --- /dev/null +++ b/platform/linto-admin/.github/workflows/dockerhub-description.yml @@ -0,0 +1,20 @@ +name: Update Docker Hub Description +on: + push: + branches: + - master + paths: + - README.md + - .github/workflows/dockerhub-description.yml +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + repository: lintoai/linto-platform-admin + readme-filepath: ./README.md diff --git a/platform/linto-admin/.gitignore b/platform/linto-admin/.gitignore new file mode 100644 index 0000000..877cd6b --- /dev/null +++ b/platform/linto-admin/.gitignore @@ -0,0 +1,13 @@ +**/node_modules +**/.env +**/settings.tmp.js +**/json_tmp +**/public/tockapp.json +**/public/tocksentences.json +**/package-lock.json +**/dist +**/.vscode +**/.local_cmd +**/data +**/dump.rdb +/webserver/model/mongodb/schemas \ No newline at end of file diff --git a/platform/linto-admin/Dockerfile b/platform/linto-admin/Dockerfile new file mode 100644 index 0000000..2c9fad7 --- /dev/null +++ b/platform/linto-admin/Dockerfile @@ -0,0 +1,22 @@ +FROM node:latest +# Gettext for envsubst being called form entrypoint script +RUN apt-get update -y && \ + apt-get install gettext -y + +COPY ./vue_app /usr/src/app/linto-admin/vue_app +COPY ./webserver /usr/src/app/linto-admin/webserver +COPY ./docker-entrypoint.sh / +COPY ./wait-for-it.sh / + +WORKDIR /usr/src/app/linto-admin/vue_app +RUN npm install && npm install -s node-sass + +WORKDIR /usr/src/app/linto-admin/webserver +RUN npm install + +HEALTHCHECK CMD node docker-healthcheck.js || exit 1 + +EXPOSE 80 +# Entrypoint handles the passed arguments +ENTRYPOINT ["/docker-entrypoint.sh"] +# CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/platform/linto-admin/README.md b/platform/linto-admin/README.md new file mode 100644 index 0000000..c6e4dab --- /dev/null +++ b/platform/linto-admin/README.md @@ -0,0 +1,115 @@ +# linto-platform-admin + +## Description +This web interface is used as a central manager for a given fleet of LinTO clients (voice-enabled apps or devices) + +You might : +- Create a "room context" (node-red workflow paired with a specific LinTO device) +- Create, edit, mock and template workflows for later usage +- Create an "application context" (node-red workflow paired with a dynamic number of connected LinTO clients) +- Install or uninstall LinTO skills (node-red nodes) +- Monitor LinTO clients (client devices or client applications) +- Edit/train a NLU model (natural language understanding) +- And many more + +## Usage + +See documentation : [https://doc.linto.ai](https://doc.linto.ai) + +# Deploy + +With our proposed stack [https://github.com/linto-ai/linto-platform-stack](https://github.com/linto-ai/linto-platform-stack) + +# Develop + +## Prerequisites +To lauch the application, you'll have to launch associated services : +- redis-server : [Installation guide](https://www.npmjs.com/package/redis-server) +- mongodb [installation guide](https://www.npmjs.com/package/mongodb) +- linto-platform-business-logic-server : [Documentation](https://github.com/linto-ai/linto-platform-business-logic-server) +- linto-platform-logic-mqtt-server : [LINK] +- linto-platform-nlu : [LINK] +- linto-platform-overwatch : [LINK] + +## Download and setup + +#### Download git repository +``` +cd YOUR/PROJECT/PATH/ +git clone git@github.com:linto-ai/linto-platform-admin.git +cd linto-platform-admin +``` + +#### Setup packages/depencies +``` +cd /webserver +npm install +cd ../vue_app +npm install +``` + +## Front-end settings +You will need to set some environment variables to connect services like "Business Logic Server", "NLU/Tock" + +### Set front-end variables +Go to the **/vue_app** folder and edit the following files: `.env.devlopment`, `.env.production` + +- `.env.devlopment` : if you want to set custom port or url, replace **VUE_APP_URL** and **VUE_APP_NLU_URL** values +``` +(example) +VUE_APP_URL=http://localhost:9000 +VUE_APP_NLU_URL=http://my-nlu-service.local +``` +- `.env.production` : set your "application url" and "Tock interface url" for production mode +``` +(example) +VUE_APP_URL=http://my-linto-platform-admin.com +VUE_APP_NLU_URL=http://my-nlu-service.com +``` + +## Back-end and services settings + +### Set global and webserver variables +Go to the **/webserver** folder, you'll see a `.env_default` file. +Rename this file as `.env` and edit the environment variables. + +``` +cd YOUR/PROJECT/PATH/linto-platform-admin/webserver +cp .env_default .env +``` + +#### Server settings + +- If you want to start linto-platform-admin as a stand-alone service: *Edit **/webserver/.env*** +- If you want to start linto-platform-admin with docker swarm mode: *Edit **/.docker_env*** + +| Env variable| Description | example | +|:---|:---|:---| +| TZ | Time-zone value | Europe/Paris | +| LINTO_STACK_DOMAIN | Linto admin host/url | localhost:9000, http://my-linto_admin.com | +| LINTO_STACK_ADMIN_HTTP_PORT | linto admin port | 9000 | +|LINTO_STACK_ADMIN_COOKIE_SECRET | linto admin cookie secret phrase | mysecretcookie | +| LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS | CORS auhtorized domains list (separator ',') | http://localhost:10000,http://my-domain.com | +| LINTO_STACK_REDIS_SESSION_SERVICE | Redis store service host/url | localhost, linto-platform-stack-redis | +| LINTO_STACK_REDIS_SESSION_SERVICE_PORT | Redis store service port | 6379 | +| LINTO_STACK_TOCK_SERVICE | Tock (nlu service) host/url | localhost, http://my-tock-service.com | +| LINTO_STACK_TOCK_USER | Tock (nlu service) user | admin@app.com | +| LINTO_STACK_TOCK_PASSWORD | Tock (nlu service) user | password | +| LINTO_STACK_STT_SERVICE_MANAGER_SERVICE | STT service host/url | localhost, http://my-s +tt-service.com | +| LINTO_STACK_MONGODB_SERVICE | MongoDb service host/url | localhost, linto-platform-stack-service | +| LINTO_STACK_MONGODB_PORT | MongoDb service port | 27017 | +| LINTO_STACK_MONGODB_DBNAME | MongoDb service database name | lintoAdmin | +| LINTO_STACK_MONGODB_USE_LOGIN | Enable/Disable MongoDb service authentication | true,false | +| LINTO_STACK_MONGODB_USER | MongoDb service username | user | +| LINTO_STACK_MONGODB_PASSWORD | MongoDb service username | password | +| LINTO_STACK_MQTT_HOST | MQTT broker host | localhost | +| LINTO_STACK_MQTT_PORT | MQTT broker port | 1883 | +| LINTO_STACK_MQTT_USE_LOGIN | Enable/Disable MQTT broker authentication | true,false | +| LINTO_STACK_MQTT_DEFAULT_HW_SCOPE | MQTT broker "hardware" scope | blk | +| LINTO_STACK_MQTT_USER | MQTT broker user | user | +| LINTO_STACK_MQTT_PASSWORD | MQTT broker user | password | +| LINTO_STACK_BLS_SERVICE | Business logic server (nodered instance) | localhost, http://my-bls.com | +| LINTO_STACK_BLS_USE_LOGIN | Enable/Disable Business logic server authentication | true,false | +| LINTO_STACK_BLS_USER | Business logic server user | user | +| LINTO_STACK_BLS_PASSWORD | Business logic server | password | diff --git a/platform/linto-admin/RELEASE.md b/platform/linto-admin/RELEASE.md new file mode 100644 index 0000000..99e2807 --- /dev/null +++ b/platform/linto-admin/RELEASE.md @@ -0,0 +1,50 @@ +# 0.3.0 +#### 2021/10/15 - Updates +- Remove "workflow templates" and Sandbox editor +- Update workflow creation forms (for device and multi-user applications) + - Add checkbox to pick feature(s) to be added on the workflow + - Remove workflow templates selection +- Update workflow settings forms + - Add checkbox to pick feature(s) to be added on the workflow + - Remove workflow templates selection + +# 0.2.5 +#### 2021/06/24 - Updates +- add environment variable LINTO_STACK_TOCK_BASEHREF to handle Tock version update +#### Updates 2021/03/16 +- Add "skills manager" + - Install or uninstall skills from http://registry.npmjs.com + - Install or uninstall local skills + - Skills are filtered by version number +- last commit : 20edf3f5f34520fb9ef712b7c1c0a2b3172de652 + +# 0.2.4 +#### 2021/04/12 - Updates +- Hotfix: Update removeUserFromApp function for deleting the good application +#### 2021/02/02 - Updates +- Hotfix: Update docker-entrypoint.sh for development mode +- App.vue : Update the "path" variable to be computed (url fullPath) + + +# 0.2.3 +#### 2021/02/01 - Updates +- Hotfix: update and fix tests on select fields for streaming services listing + +# 0.2.2 +#### Updates +- fix "webapp_hosts" model issue on deleting multi-user application +- fix flow formatting issue on "save and publish" +- comment code (wip) +- Add an "VUE_APP_DEBUG" environment variable on front to be able to log errors + +# 0.2.1 +#### Updates +- Add tests on "applications" views to handle applications using STT services in process of generating +- Add a notification modal on "applications" views to show state of STT services in process of generating +- Add 2 new collections to database: "mqtt_users" and "mqtt_acls" + +# 0.2.0 +- Replace "context" notion by "workflows". Works with DB_VERSION=2 + +# 0.1.0 +- First build of LinTO-Platform-Admin for our Docker Swarm Stack \ No newline at end of file diff --git a/platform/linto-admin/docker-compose.yml b/platform/linto-admin/docker-compose.yml new file mode 100644 index 0000000..2339700 --- /dev/null +++ b/platform/linto-admin/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3.7' + +services: + + mongo-admin: + image: mongo:latest + volumes: + - ./mongodb/seed:/docker-entrypoint-initdb.d + environment: + MONGO_INITDB_DATABASE: lintoAdmin + networks: + - internal + + linto-admin: + image: linto-admin:latest + deploy: + mode: replicated + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + healthcheck: + interval: 15s + timeout: 10s + retries: 4 + start_period: 50s + env_file: .docker_env # Remove when running from stack + volumes: + - "/etc/localtime:/etc/localtime:ro" + - "./vue_app:/usr/src/app/linto-admin/vue_app" + - "./webserver:/usr/src/app/linto-admin/webserver" + # You might bind mount here webserver and vue_app directories for development ;) + command: # Overrides CMD specified in dockerfile (none here, handled by entrypoint) + # - --reinstall-webserver + # - --rebuild-vue-app-dev + # - --run-cmd=DEBUG=* npm run start-dev + # - --rebuild-vue-app + - --run-cmd=npm run start + ports: + - 80:80 + networks: + - internal + + redis-admin: + image: redis:latest + networks: + - internal + +networks: + internal: diff --git a/platform/linto-admin/docker-entrypoint.sh b/platform/linto-admin/docker-entrypoint.sh new file mode 100755 index 0000000..0e68a30 --- /dev/null +++ b/platform/linto-admin/docker-entrypoint.sh @@ -0,0 +1,106 @@ +#!/bin/bash +set -e +[ -z "$LINTO_STACK_DOMAIN" ] && { + echo "Missing LINTO_STACK_DOMAIN" + exit 1 +} +[ -z "$LINTO_STACK_USE_SSL" ] && { + echo "Missing LINTO_STACK_USE_SSL" + exit 1 +} + +echo "Waiting redis, MQTT and mongo..." +/wait-for-it.sh $LINTO_STACK_REDIS_SESSION_SERVICE:$LINTO_STACK_REDIS_SESSION_SERVICE_PORT --timeout=20 --strict -- echo " $LINTO_STACK_REDIS_SESSION_SERVICE:$LINTO_STACK_REDIS_SESSION_SERVICE_PORT is up" +/wait-for-it.sh $LINTO_STACK_MONGODB_SERVICE:$LINTO_STACK_MONGODB_PORT --timeout=20 --strict -- echo " $LINTO_STACK_MONGODB_SERVICE:$LINTO_STACK_MONGODB_PORT is up" +/wait-for-it.sh $LINTO_STACK_MQTT_HOST:$LINTO_STACK_MQTT_PORT --timeout=20 --strict -- echo " $LINTO_STACK_MQTT_HOST:$LINTO_STACK_MQTT_PORT is up" + +while [ "$1" != "" ]; do + case $1 in + --rebuild-vue-app) + cd /usr/src/app/linto-admin/vue_app + echo "REBUILDING VUE APP" + if [[ "$LINTO_STACK_USE_SSL" == true ]]; then + echo "VUE_APP_URL= + VUE_APP_TOCK_URL=https://$LINTO_STACK_DOMAIN/tock/ + VUE_APP_TOCK_USER=$LINTO_STACK_TOCK_USER + VUE_APP_TOCK_PASSWORD=$LINTO_STACK_TOCK_PASSWORD + VUE_APP_NODERED_RED=https://$LINTO_STACK_DOMAIN/red + VUE_APP_NODERED=https://$LINTO_STACK_DOMAIN/redui + VUE_APP_NODERED_USER=$LINTO_STACK_BLS_USER + VUE_APP_NODERED_PASSWORD=$LINTO_STACK_BLS_PASSWORD + VUE_APP_DEBUG=$VUE_APP_DEBUG" >.env.production + else + echo "VUE_APP_URL= + VUE_APP_TOCK_URL=http://$LINTO_STACK_DOMAIN/tock/ + VUE_APP_TOCK_USER=$LINTO_STACK_TOCK_USER + VUE_APP_TOCK_PASSWORD=$LINTO_STACK_TOCK_PASSWORD + VUE_APP_NODERED_RED=http://$LINTO_STACK_DOMAIN/red + VUE_APP_NODERED=http://$LINTO_STACK_DOMAIN/redui + VUE_APP_NODERED_USER=$LINTO_STACK_BLS_USER + VUE_APP_NODERED_PASSWORD=$LINTO_STACK_BLS_PASSWORD + VUE_APP_DEBUG=$VUE_APP_DEBUG" >.env.production + fi + npm run build-app + ;; + --rebuild-vue-app-dev) + cd /usr/src/app/linto-admin/vue_app + echo "REBUILDING VUE APP IN DEVELOPMENT MODE" + if [[ "$LINTO_STACK_USE_SSL" == true ]]; then + echo "VUE_APP_URL= + VUE_APP_TOCK_URL=https://$LINTO_STACK_DOMAIN/tock/ + VUE_APP_TOCK_USER=$LINTO_STACK_TOCK_USER + VUE_APP_TOCK_PASSWORD=$LINTO_STACK_TOCK_PASSWORD + VUE_APP_NODERED_RED=https://$LINTO_STACK_DOMAIN/red + VUE_APP_NODERED=https://$LINTO_STACK_DOMAIN/redui + VUE_APP_NODERED_USER=$LINTO_STACK_BLS_USER + VUE_APP_NODERED_PASSWORD=$LINTO_STACK_BLS_PASSWORD + VUE_APP_DEBUG=$VUE_APP_DEBUG" >.env.development + else + echo "VUE_APP_URL= + VUE_APP_TOCK_URL=http://$LINTO_STACK_DOMAIN/tock/ + VUE_APP_TOCK_USER=$LINTO_STACK_TOCK_USER + VUE_APP_TOCK_PASSWORD=$LINTO_STACK_TOCK_PASSWORD + VUE_APP_NODERED_RED=http://$LINTO_STACK_DOMAIN/red + VUE_APP_NODERED=http://$LINTO_STACK_DOMAIN/redui + VUE_APP_NODERED_USER=$LINTO_STACK_BLS_USER + VUE_APP_NODERED_PASSWORD=$LINTO_STACK_BLS_PASSWORD + VUE_APP_DEBUG=$VUE_APP_DEBUG" >.env.development + fi + npm run build-dev + ;; + --reinstall-vue-app) + cd /usr/src/app/linto-admin/vue_app + echo "REINSTALL VUE APP" + npm install + ;; + --reinstall-webserver) + echo "REBUILDING WEBSERVER APP" + cd /usr/src/app/linto-admin/webserver + npm install + ;; + --run-cmd) + if [ "$2" ]; then + script=$2 + shift + else + die 'ERROR: "--run-cmd" requires a non-empty option argument.' + fi + ;; + --run-cmd?*) + script=${1#*=} # Deletes everything up to "=" and assigns the remainder. + ;; + --run-cmd=) # Handle the case of an empty --run-cmd= + die 'ERROR: "--run-cmd" requires a non-empty option argument.' + ;; + *) + echo "ERROR: Bad argument provided \"$1\"" + exit 1 + ;; + esac + shift +done + +echo "RUNNING : $script" +cd /usr/src/app/linto-admin/webserver + +eval "$script" \ No newline at end of file diff --git a/platform/linto-admin/vue_app/.browserslistrc b/platform/linto-admin/vue_app/.browserslistrc new file mode 100644 index 0000000..9dee646 --- /dev/null +++ b/platform/linto-admin/vue_app/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not ie <= 8 diff --git a/platform/linto-admin/vue_app/.env.development b/platform/linto-admin/vue_app/.env.development new file mode 100644 index 0000000..baa6f8a --- /dev/null +++ b/platform/linto-admin/vue_app/.env.development @@ -0,0 +1,9 @@ +VUE_APP_URL=http://dev.linto.local:9000 +VUE_APP_TOCK_URL=http://127.0.0.1:8880/tock +VUE_APP_TOCK_USER=admin@app.com +VUE_APP_TOCK_PASSWORD=password +VUE_APP_NODERED_RED=http://dev.linto.local:10000/red +VUE_APP_NODERED=http://dev.linto.local:10000/redui +VUE_APP_NODERED_USER=admin +VUE_APP_NODERED_PASSWORD=password +VUE_APP_DEBUG=true \ No newline at end of file diff --git a/platform/linto-admin/vue_app/.env.production b/platform/linto-admin/vue_app/.env.production new file mode 100644 index 0000000..2bc17a3 --- /dev/null +++ b/platform/linto-admin/vue_app/.env.production @@ -0,0 +1,9 @@ +VUE_APP_URL= +VUE_APP_TOCK_URL=http://dev.linto.local/tock +VUE_APP_TOCK_USER=admin@app.com +VUE_APP_TOCK_PASSWORD=password +VUE_APP_NODERED_RED=http://dev.linto.local/red +VUE_APP_NODERED=http://dev.linto.local/redui +VUE_APP_NODERED_USER=admin +VUE_APP_NODERED_PASSWORD=password +VUE_APP_DEBUG=false \ No newline at end of file diff --git a/platform/linto-admin/vue_app/.eslintrc.js b/platform/linto-admin/vue_app/.eslintrc.js new file mode 100644 index 0000000..11651c9 --- /dev/null +++ b/platform/linto-admin/vue_app/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + // no eslint +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/README.md b/platform/linto-admin/vue_app/README.md new file mode 100644 index 0000000..92bee21 --- /dev/null +++ b/platform/linto-admin/vue_app/README.md @@ -0,0 +1,34 @@ +# linto-admin Vue APP + +## Project setup +``` +npm install +``` +Open the `.env.production` or `.env.development` file and set your application URL +``` +VUE_APP_URL=http://localhost:9000 +``` +### Compiles and minifies for development +This will build static files into `../webserver/dist` folder +``` +npm run build-dev +``` + +### Compiles and minifies for production +This will build static files into `../webserver/dist` folder +``` +npm run build-app +``` + +### Run your tests +``` +npm run test +``` + +### Lints and fixes files +``` +npm run lint +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/platform/linto-admin/vue_app/babel.config.js b/platform/linto-admin/vue_app/babel.config.js new file mode 100644 index 0000000..ba17966 --- /dev/null +++ b/platform/linto-admin/vue_app/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/app' + ] +} diff --git a/platform/linto-admin/vue_app/package.json b/platform/linto-admin/vue_app/package.json new file mode 100644 index 0000000..9655ed6 --- /dev/null +++ b/platform/linto-admin/vue_app/package.json @@ -0,0 +1,27 @@ +{ + "name": "linto-admin", + "author": "Romain Lopez ", + "description": "This is the linto-platform-admin front-end web interface", + "version": "0.3.0", + "license": "GNU AFFERO GPLV3", + "scripts": { + "build-app": "npm run build:css && vue-cli-service build --mode production", + "build-dev": "npm run build:css && vue-cli-service build --mode development", + "build:css": "./node_modules/node-sass/bin/node-sass ./public/sass/styles.scss ./public/css/styles.css --output-style compressed" + }, + "dependencies": { + "@vue/cli-plugin-babel": "^4.2.3", + "@vue/cli-service": "^4.2.3", + "axios": "^0.19.2", + "babel-eslint": "^10.1.0", + "html-webpack-plugin": "^3.2.0", + "moment": "^2.24.0", + "node-sass": "^4.13.1", + "randomstring": "^1.1.5", + "socket.io-client": "^2.3.0", + "vue": "^2.6.11", + "vue-router": "^3.1.6", + "vue-template-compiler": "^2.6.11", + "vuex": "^3.1.3" + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/postcss.config.js b/platform/linto-admin/vue_app/postcss.config.js new file mode 100644 index 0000000..961986e --- /dev/null +++ b/platform/linto-admin/vue_app/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/platform/linto-admin/vue_app/public/404.html b/platform/linto-admin/vue_app/public/404.html new file mode 100644 index 0000000..05d4375 --- /dev/null +++ b/platform/linto-admin/vue_app/public/404.html @@ -0,0 +1,36 @@ + + + + + + + + + + + Linto Admin + + + + + + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/animations/error.json b/platform/linto-admin/vue_app/public/animations/error.json new file mode 100644 index 0000000..157ca8d --- /dev/null +++ b/platform/linto-admin/vue_app/public/animations/error.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":25,"ip":0,"op":50,"w":600,"h":600,"nm":"Composition 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[187.25,355.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.069,-168.569],[-9.069,51.069],[1.069,51.569],[1.069,-168.069]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.069,-168.569],[-9.069,51.069],[5.569,50.069],[5.569,-169.569]],"c":true}]},{"t":30,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.069,-168.532],[-9.069,59.25],[241.75,58.213],[241.75,-169.569]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"}],"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[152,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.752941176471,0.713725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.439215686275,0.439215686275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[112.75,-55.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[182.451,138.71],"ix":3},"r":{"a":0,"k":225,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[187.25,355.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-14.569,-167.569],[-14.569,52.069],[0.069,51.069],[0.069,-168.569]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.069,-168.569],[-9.069,51.069],[5.569,50.069],[5.569,-169.569]],"c":true}]},{"t":20,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-9.069,-168.532],[-9.069,59.25],[241.75,58.213],[241.75,-169.569]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"}],"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[152,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.752941176471,0.713725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.439215686275,0.439215686275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[112.75,-55.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[182.451,138.71],"ix":3},"r":{"a":0,"k":135,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Calque de forme 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[301.5,298.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[443,443],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.439215686275,0.439215686275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,1.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/animations/validation.json b/platform/linto-admin/vue_app/public/animations/validation.json new file mode 100644 index 0000000..104785b --- /dev/null +++ b/platform/linto-admin/vue_app/public/animations/validation.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":25,"ip":0,"op":50,"w":600,"h":600,"nm":"Composition 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[304.125,311.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-154.01,-205.367],[-154.01,95.01],[-136.625,95.01],[-136.625,-205.367]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-154.01,-205.367],[-154.01,95.01],[269.375,95.01],[269.375,-205.367]],"c":true}]},{"t":37,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-153.125,-239.5],[-153.125,124.5],[293.875,124.5],[293.875,-239.5]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Masque 1"}],"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[152,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.752941176471,0.713725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.752941176471,0.713725490196,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[112.75,-55.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[263.55,138.71],"ix":3},"r":{"a":0,"k":135,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[152,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.752941176471,0.713725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.752941176471,0.713725490196,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-62,28],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[107.042,143.487],"ix":3},"r":{"a":0,"k":45.882,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[443,443],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.752941176471,0.713725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":30,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,1.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/css/styles.css b/platform/linto-admin/vue_app/public/css/styles.css new file mode 100644 index 0000000..086539c --- /dev/null +++ b/platform/linto-admin/vue_app/public/css/styles.css @@ -0,0 +1 @@ +@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,800&display=swap");@-webkit-keyframes rotating{from{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotating{from{-ms-transform:rotate(0deg);-moz-transform:rotate(0deg);-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}to{-ms-transform:rotate(360deg);-moz-transform:rotate(360deg);-webkit-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes traceBorder{from{width:0%}to{width:100%}}@keyframes traceBorder{from{width:0%}to{width:100%}}body{font-family:'Open Sans', Arial, Helvetica, sans-serif;font-size:16px;color:#454545;font-weight:400;padding:0;margin:0}h1,h2,h3,h4{display:inline-block;width:100%;margin:0}h1{font-size:30px;font-weight:700;padding:0 0 10px 0;color:#434C5F}h2{font-size:24px;font-weight:600;padding:20px 0;color:#6989aa}h3{font-size:20px;font-weight:500;padding:0 0 5px 0}.flex{display:flex}.flex.col{flex-direction:column}.flex.row{flex-direction:row}.flex1{flex:1}.flex2{flex:2}.flex3{flex:3}.flex4{flex:4}img{display:inline-block}ul{margin:0;padding:0;list-style-type:none}ul li{padding:0}ul.checkbox-list{padding:20px;border:1px solid #E0F1FF;display:flex;flex-direction:column;padding:10px}ul.checkbox-list li{display:inline-block;flex:1;height:30px}ul.checkbox-list li input[type=checkbox]{display:inline-block;line-height:30px;vertical-align:top;margin:10px 5px}ul.checkbox-list li .checkbox__label{line-height:30px;vertical-align:top;font-size:16px;color:#434C5F}ul.checkbox-list li .none{display:inline-block;font-style:italic;color:#ccc}ul.checkbox-list.no-borders{border:none;padding:10px}ul.array-list{padding:0 10px;list-style-type:circle}ul.array-list li{padding:2px 0;font-size:15px;font-weight:600}.hidden{display:none}.divider{height:1px;width:100%;margin:40px 0 0 0}.divider.small{margin:20px 0 0 0}.block{padding:20px;background-color:#fff;-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin:0 0 20px 0}.block.block--transparent{-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none;background-color:transparent;padding:20px 0}.block.block--no-padding{padding:0 !important}.block.block--no-margin{margin:0 !important}.block.notice--important{border:3px dashed #ff7070}.block.notice--important .icon{display:inline-block;width:30px;height:30px;margin-right:10px;background-image:url("../img/warning@2x.png");background-position:center center;background-size:30px 30px}.block.notice--important .title{display:inline-block;padding:0 0 20px 0;font-size:22px;line-height:30px;font-weight:600;color:#ff7070}.block.notice--important .content{line-height:26px;font-weight:600}.block.notice--important .content strong{color:#ff7070}.block.notice--important .content a{color:#00C0B6;text-decoration:none;font-weight:600}.block.notice--important .content a:hover{text-decoration:underline}.table{border-collapse:separate;border-spacing:0;width:auto}.table.table--full{width:100%}.table thead tr th{font-size:14px;font-weight:600;padding:5px 20px;text-align:left;color:#454545}.table thead tr th.status{width:100px}.table tbody{-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.table tbody tr:first-child td:first-child{-webkit-border-top-left-radius:3px;-moz-border-radius-topleft:3px;border-top-left-radius:3px}.table tbody tr:first-child td:last-child{-webkit-border-top-right-radius:3px;-moz-border-radius-topright:3px;border-top-right-radius:3px}.table tbody tr:last-child td:first-child{-webkit-border-bottom-left-radius:3px;-moz-border-radius-bottomleft:3px;border-bottom-left-radius:3px}.table tbody tr:last-child td:last-child{-webkit-border-bottom-right-radius:3px;-moz-border-radius-bottomright:3px;border-bottom-right-radius:3px}.table tbody tr td{border-bottom:1px solid #ececec;font-size:16px;padding:10px 20px;background-color:#fefefe;font-size:14px;border-right:1px solid #f2f2f2}.table tbody tr td strong{display:inline-block;vertical-align:top;color:#6989aa;font-weight:600;padding-right:5px;line-height:32px}.table tbody tr td.important{font-weight:600;color:#545454}.table tbody tr td.center{text-align:center}.table tbody tr td.right{text-align:right}.table tbody tr td.status{width:100px}.table tbody tr td:last-child{border:none}.table tbody tr td.table--desc{font-size:14px;font-style:italic}.table tbody tr:hover td{background-color:#f8f8f8}.table tbody tr.active td,.table tbody tr.active:hover td{background-color:rgba(125,252,245,0.1);border:1x solid #00C0B6}.table tbody tr.center td{text-align:center}.no-content{font-style:italic;padding:20px;font-weight:600;color:#6989aa}.icon.icon--status{display:inline-block;width:12px;height:12px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.icon.icon--status.online{background-color:#00C0B6}.icon.icon--status.offline{background-color:#ff7070}details.description{padding:0 20px 20px 20px}details.description:focus{outline:none}details.description summary{font-weight:700;font-size:16px;color:#6989aa;cursor:pointer}details.description summary:focus{outline:none}details.description summary:hover{color:#00C0B6}details.description span{font-size:15px;color:#434C5F;font-style:italic;display:inline-block;width:100%}details.description span a{color:#00C0B6;text-decoration:none;font-weight:600}details.description span a:hover{text-decoration:underline}#app{width:100%;height:100%;position:fixed;top:0;left:0;margin:0;padding:0;overflow:hidden;z-index:1}#page-view{padding:0;margin:0;z-index:1;overflow:hidden}#page-view.fullscreen-child{z-index:10}#view{position:relative;overflow:auto;z-index:4;background-color:#f0f6f9;padding:40px}#view.fullscreen-child{position:inherit}#view-render{height:100%;padding:0}#view-render>.flex{padding:0 0 40px 0}#header{position:relative;height:40px;min-height:40px;padding:10px 0;background-color:#fff;margin:0;z-index:10;-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3)}#header.fullscreen-child{z-index:1}#header .header__logo{padding:0 20px}#header .header__logo img{display:inline-block;width:auto;height:100%}#header .header__nav{padding:0 20px;text-align:right;margin:5px 20px}#vertical-nav{position:relative;min-width:180px;max-width:260px;width:auto;padding:20px 0;background:#434C5F;overflow:hidden;z-index:5}#vertical-nav.fullscreen-child{z-index:1}#vertical-nav .nav-divider{width:100%;height:1px;background-color:#747e92;margin:10px 0}.vertical-nav-item{position:relative;padding:20px;height:auto}.vertical-nav-item .vertical-nav-item__link,.vertical-nav-item .vertical-nav-item__link--parent{position:relative;display:inline-block;font-size:14px;font-weight:400;color:#fff;text-decoration:none}.vertical-nav-item .vertical-nav-item__link .nav-link__icon,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon{display:inline-block;width:30px;height:30px;background-color:#fff;margin:0 5px;vertical-align:top}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--static,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--static{mask-image:url("../img/svg/cpu.svg");-webkit-mask-image:url("../img/svg/cpu.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--app,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--app{mask-image:url("../img/svg/app.svg");-webkit-mask-image:url("../img/svg/app.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--android,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--android{mask-image:url("../img/svg/android.svg");-webkit-mask-image:url("../img/svg/android.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--android-users,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--android-users{mask-image:url("../img/svg/android-users.svg");-webkit-mask-image:url("../img/svg/android-users.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--webapp,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--webapp{mask-image:url("../img/svg/webapp.svg");-webkit-mask-image:url("../img/svg/webapp.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--workflow,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--workflow{mask-image:url("../img/svg/workflow.svg");-webkit-mask-image:url("../img/svg/workflow.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--nlu,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--nlu{mask-image:url("../img/svg/nlu.svg");-webkit-mask-image:url("../img/svg/nlu.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--single-user,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--single-user{mask-image:url("../img/svg/single-user.svg");-webkit-mask-image:url("../img/svg/single-user.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--multi-user,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--multi-user{mask-image:url("../img/svg/multi-user.svg");-webkit-mask-image:url("../img/svg/multi-user.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--terminal,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--terminal{mask-image:url("../img/svg/terminal.svg");-webkit-mask-image:url("../img/svg/terminal.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--users,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--users{mask-image:url("../img/svg/users.svg");-webkit-mask-image:url("../img/svg/users.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__icon.nav-link__icon--skills-manager,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__icon.nav-link__icon--skills-manager{mask-image:url("../img/svg/skills-manager.svg");-webkit-mask-image:url("../img/svg/skills-manager.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.vertical-nav-item .vertical-nav-item__link .nav-link__label,.vertical-nav-item .vertical-nav-item__link--parent .nav-link__label{display:inline-block;height:30px;vertical-align:top;color:#fff;line-height:30px}.vertical-nav-item .vertical-nav-item__link--parent::after{content:'';display:inline-block;width:20px;height:20px;position:absolute;top:2px;left:100%;margin-left:-30px;background-image:url("../img/nav-arrows@2x.png");background-size:40px 40px;background-position:0 0}.vertical-nav-item .vertical-nav-item__link--parent:hover::after{background-position:0 -20px}.vertical-nav-item .vertical-nav-item__link--parent.opened::after{background-position:-20px 0}.vertical-nav-item .vertical-nav-item__link--parent.opened:hover::after{background-position:-20px -20px}.vertical-nav-item .vertical-nav-item--children{overflow:hidden;-webkit-transition:all 0.3s ease-in;-moz-transition:all 0.3s ease-in;-o-transition:all 0.3s ease-in;transition:all 0.3s ease-in;border-left:1px solid #ececec;margin:5px 0}.vertical-nav-item .vertical-nav-item--children.hidden{display:flex;height:0px;margin:0;padding:0}.vertical-nav-item .vertical-nav-item--children .vertical-nav-item__link--children{display:inline-block;font-size:14px;padding:8px 0 8px 15px;font-weight:400;text-decoration:none;color:#fff}.vertical-nav-item .vertical-nav-item--children .vertical-nav-item__link--children:hover{color:#45baeb}.vertical-nav-item .vertical-nav-item--children .vertical-nav-item__link--children.active,.vertical-nav-item .vertical-nav-item--children .vertical-nav-item__link--children.active:hover{background-color:#6989aa;font-weight:600}.vertical-nav-item.active{background-color:#6989aa}.vertical-nav-item.active .vertical-nav-item__link,.vertical-nav-item.active .vertical-nav-item__link--parent{font-weight:600}.vertical-nav-item.active .vertical-nav-item__link:hover,.vertical-nav-item.active .vertical-nav-item__link--parent:hover{color:#fff}.notif-wrapper{z-index:999;-moz-box-shadow:0 -2px 2px 0 rgba(0,0,0,0.2);-webkit-box-shadow:0 -2px 2px 0 rgba(0,0,0,0.2);box-shadow:0 -2px 2px 0 rgba(0,0,0,0.2);padding:0;overflow:hidden;border:none;position:relative;-webkit-transition:height 0.3s ease;-moz-transition:height 0.3s ease;-o-transition:height 0.3s ease;transition:height 0.3s ease;background-color:#fff}.notif-wrapper.closed{height:0}.notif-wrapper.success,.notif-wrapper.error{height:40px;padding:10px 0}.notif-wrapper.success::after{content:'';display:inline-block;position:absolute;height:3px;width:100%;top:0;left:0;background-color:#00C0B6;-webkit-animation:traceBorder 2s linear;-moz-animation:traceBorder 2s linear;-ms-animation:traceBorder 2s linear;-o-animation:traceBorder 2s linear;animation:traceBorder 2s linear}.notif-wrapper.error::after{content:'';display:inline-block;position:absolute;height:3px;width:100%;top:0;left:0;background-color:#ff7070;-webkit-animation:traceBorder 2s linear;-moz-animation:traceBorder 2s linear;-ms-animation:traceBorder 2s linear;-o-animation:traceBorder 2s linear;animation:traceBorder 2s linear}.notif-container{align-items:center;justify-content:center;position:relative}.notif-container>span{display:inline-block}.notif-container .icon{width:40px;height:40px;margin:0 10px}.notif-container .notif-msg{font-size:16px}.notif-container .notif-msg.success{color:#00C0B6}.notif-container .notif-msg.error{color:#ff7070}#top-notif{min-height:40px;background:#f2f2f2;border-bottom:1px solid #ccc;z-index:20}#top-notif .icon.state__icon{display:inline-block;width:20px;height:20px;background-image:url("../img/deploy-status-icons@2x.png");background-size:20px 60px;background-repeat:no-repeat;margin-right:10px}#top-notif .icon.state__icon.state__icon--loading{background-position:0 -40px;animation-name:rotating;-webkit-animation-name:rotating;animation-duration:1s;-webkit-animation-duration:1s;animation-iteration-count:infinite;-webkit-animation-iteration-count:infinite}#top-notif .icon.state__icon.state__icon--success{background-position:0 0}#top-notif .state-item{padding:10px 40px;justify-content:center;align-items:center}#top-notif .state-item .state-text{display:inline-block;font-size:14px;font-weight:500;color:#434C5F}#top-notif .state-item .state-text.success{color:#00C0B6}#top-notif .state-item .state-text strong{font-weight:600;color:#45baeb}.state-progress-container{height:15px;width:50%;max-width:220px;border:1px solid #6989aa;position:relative;margin-left:20px;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;overflow:hidden}.state-progress-container .state-progress{-webkit-transition:all 0.3 ease;-moz-transition:all 0.3 ease;-o-transition:all 0.3 ease;transition:all 0.3 ease;position:absolute;top:0;height:15px;background:#00C0B6;width:0;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px}#app-notif-top{background:#fff;-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);margin-bottom:20px;-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease;position:relative;overflow:hidden}#app-notif-top.closed{height:50px}#app-notif-top.closed #app-notif-top-data,#app-notif-top.closed #model-generating-refresh{display:none}#app-notif-top>div{align-items:center;justify-content:center;margin:10px}#app-notif-top h3{padding:0 20px;font-size:18px;font-weight:600;color:#6989aa}#app-notif-top #close-notif-top{position:absolute;top:10px;left:100%;margin-left:-40px}.model-generating-table{width:auto}.model-generating-table tr td{padding:5px}.model-generating-table tr td.model-generating__label{font-size:16px;font-weight:600}.model-generating__prct-wrapper{display:inline-block;height:20px;width:240px;border:1px solid #ccc;background-color:#fcfcfc;position:relative;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.model-generating__prct-wrapper .model-generating__prct-value{position:absolute;top:0;left:0;height:20px;background-color:#00C0B6;z-index:2}.model-generating__prct-wrapper .model-generating__prct-label{display:inline-block;width:100%;height:20px;line-height:20px;text-align:center;font-size:14px;color:#454545;position:absolute;top:0;left:0;z-index:3}.button{display:inline-block;border:1px solid #fff;padding:0;margin:0;height:32px;background-color:#fff;-webkit-transition:all 0.3s ease-in;-moz-transition:all 0.3s ease-in;-o-transition:all 0.3s ease-in;transition:all 0.3s ease-in;-moz-box-shadow:0 1px 2px 0 rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 2px 0 rgba(0,0,0,0.2);box-shadow:0 1px 2px 0 rgba(0,0,0,0.2);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;outline:none;cursor:pointer;position:relative;overflow:hidden}.button .button__label{display:inline-block;font-size:15px;font-weight:400;line-height:28px;color:#45baeb;vertical-align:top;color:#454545;padding:0 10px;border:1px solid transparent;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.button .button__icon{display:inline-block;width:30px;height:30px;vertical-align:top;margin:0;padding:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.button .button__icon.button__icon--monitoring{mask-image:url("../img/svg/levels.svg");-webkit-mask-image:url("../img/svg/levels.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--close{mask-image:url("../img/svg/close.svg");-webkit-mask-image:url("../img/svg/close.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--ping{mask-image:url("../img/svg/ping.svg");-webkit-mask-image:url("../img/svg/ping.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--talk{mask-image:url("../img/svg/chat.svg");-webkit-mask-image:url("../img/svg/chat.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--mute{mask-image:url("../img/svg/mute.svg");-webkit-mask-image:url("../img/svg/mute.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--unmute{mask-image:url("../img/svg/unmute.svg");-webkit-mask-image:url("../img/svg/unmute.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--workflow{mask-image:url("../img/svg/workflow.svg");-webkit-mask-image:url("../img/svg/workflow.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--fullscreen{mask-image:url("../img/svg/fullscreen.svg");-webkit-mask-image:url("../img/svg/fullscreen.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--leave-fullscreen{mask-image:url("../img/svg/leave-fullscreen.svg");-webkit-mask-image:url("../img/svg/leave-fullscreen.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--save{mask-image:url("../img/svg/save.svg");-webkit-mask-image:url("../img/svg/save.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--load{mask-image:url("../img/svg/upload.svg");-webkit-mask-image:url("../img/svg/upload.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--publish,.button .button__icon.button__icon--deploy{mask-image:url("../img/svg/rocket.svg");-webkit-mask-image:url("../img/svg/rocket.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--logout{mask-image:url("../img/svg/logout.svg");-webkit-mask-image:url("../img/svg/logout.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--settings{mask-image:url("../img/svg/settings.svg");-webkit-mask-image:url("../img/svg/settings.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--barcode{mask-image:url("../img/svg/barcode.svg");-webkit-mask-image:url("../img/svg/barcode.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--delete,.button .button__icon.button__icon--trash{mask-image:url("../img/svg/delete.svg");-webkit-mask-image:url("../img/svg/delete.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--cancel{mask-image:url("../img/svg/cancel.svg");-webkit-mask-image:url("../img/svg/cancel.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--apply{mask-image:url("../img/svg/apply.svg");-webkit-mask-image:url("../img/svg/apply.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--add{mask-image:url("../img/svg/add.svg");-webkit-mask-image:url("../img/svg/add.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--back{mask-image:url("../img/svg/back.svg");-webkit-mask-image:url("../img/svg/back.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--user-settings{mask-image:url("../img/svg/user-settings.svg");-webkit-mask-image:url("../img/svg/user-settings.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--android{mask-image:url("../img/svg/android.svg");-webkit-mask-image:url("../img/svg/android.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--webapp{mask-image:url("../img/svg/webapp.svg");-webkit-mask-image:url("../img/svg/webapp.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--reset{mask-image:url("../img/svg/reset.svg");-webkit-mask-image:url("../img/svg/reset.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--edit{mask-image:url("../img/svg/edit.svg");-webkit-mask-image:url("../img/svg/edit.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--say{mask-image:url("../img/svg/say.svg");-webkit-mask-image:url("../img/svg/say.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--goto{mask-image:url("../img/svg/goto.svg");-webkit-mask-image:url("../img/svg/goto.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--mutli-user{mask-image:url("../img/svg/multi-user.svg");-webkit-mask-image:url("../img/svg/multi-user.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--install{mask-image:url("../img/svg/install.svg");-webkit-mask-image:url("../img/svg/install.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat}.button .button__icon.button__icon--loading{mask-image:url("../img/svg/loading.svg");-webkit-mask-image:url("../img/svg/loading.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;animation-name:rotating;-webkit-animation-name:rotating;animation-duration:1s;-webkit-animation-duration:1s;animation-iteration-count:infinite;-webkit-animation-iteration-count:infinite}.button .button__icon.button__icon--arrow{mask-image:url("../img/svg/arrow.svg");-webkit-mask-image:url("../img/svg/arrow.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease}.button .button__icon.button__icon--arrow.opened{-ms-transform:rotate(0deg);-moz-transform:rotate(0deg);-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg)}.button .button__icon.button__icon--arrow.closed{-ms-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.button.button-icon-txt{min-width:120px}.button.button-icon-txt .button__icon{margin-left:5px}.button.button-icon-txt .button__label{padding:0 10px 0 5px}.button.button--full{width:100%;padding:0}.button.button--valid,.button.button--green{border-color:#00C0B6}.button.button--valid .button__icon,.button.button--green .button__icon{background-color:#00C0B6}.button.button--valid .button__label,.button.button--green .button__label{color:#00C0B6}.button.button--valid:hover,.button.button--green:hover{background-color:#00C0B6}.button.button--valid:hover .button__label,.button.button--green:hover .button__label{color:#fff}.button.button--important,.button.button--red{border-color:#ff7070}.button.button--important .button__icon,.button.button--red .button__icon{background-color:#ff7070}.button.button--important .button__label,.button.button--red .button__label{color:#ff7070}.button.button--important:hover,.button.button--red:hover{background-color:#ff7070}.button.button--important:hover .button__label,.button.button--red:hover .button__label{color:#fff}.button.button--cancel,.button.button--grey{border-color:#666}.button.button--cancel .button__icon,.button.button--grey .button__icon{background-color:#666}.button.button--cancel .button__label,.button.button--grey .button__label{color:#666}.button.button--cancel:hover,.button.button--grey:hover{background-color:#666}.button.button--cancel:hover .button__label,.button.button--grey:hover .button__label{color:#fff}.button.button--blue{border-color:#45baeb}.button.button--blue .button__icon{background-color:#45baeb}.button.button--blue .button__label{color:#45baeb}.button.button--blue:hover{background-color:#45baeb}.button.button--blue:hover .button__label{color:#fff}.button.button--bluemid{border-color:#6989aa}.button.button--bluemid .button__icon{background-color:#6989aa}.button.button--bluemid .button__label{color:#6989aa}.button.button--bluemid:hover{background-color:#6989aa}.button.button--bluemid:hover .button__label{color:#fff}.button.button--bluedark{border-color:#434C5F}.button.button--bluedark .button__icon{background-color:#434C5F}.button.button--bluedark .button__label{color:#434C5F}.button.button--bluedark:hover{background-color:#434C5F}.button.button--bluedark:hover .button__label{color:#fff}.button.button--orange{border-color:#ffa659}.button.button--orange .button__icon{background-color:#ffa659}.button.button--orange .button__label{color:#ffa659}.button.button--orange:hover{background-color:#ffa659}.button.button--orange:hover .button__label{color:#fff}.button:hover .button__icon{background-color:#fff}.button:hover.button--with-desc{overflow:visible}.button:hover.button--with-desc::after{content:attr(data-desc);position:absolute;font-size:14px;max-width:180px;min-width:80px;height:auto;top:2px;left:110%;padding:5px;color:#ffffff;background-color:inherit;font-style:italic;white-space:nowrap;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;z-index:10;white-space:break-spaces;text-align:center}.button:hover.button--with-desc.bottom::after{top:35px;left:0}a.button{height:30px}.button--toggle__container{padding-bottom:20px;border-bottom:1px solid #E0F1FF}.button--toggle__label{display:inline-block;font-size:18px;font-weight:600;color:#434C5F;line-height:25px;padding:0 10px 0 0}.button--toggle{display:inline-block;width:50px;height:24px;border:2px solid #6989aa;background-color:#fff;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;-moz-box-shadow:0 1px 2px 0 rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 2px 0 rgba(0,0,0,0.2);box-shadow:0 1px 2px 0 rgba(0,0,0,0.2);-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease;position:relative;outline:none !important}.button--toggle .button--toggle__disc{display:inline-block;-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease;width:18px;height:18px;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;border:1px solid #fff;position:absolute;top:0}.button--toggle.enabled{border-color:#00C0B6}.button--toggle.enabled .button--toggle__disc{background-color:#00C0B6;left:100%;margin-left:-20px}.button--toggle.disabled{border-color:#ff7070}.button--toggle.disabled .button--toggle__disc{background-color:#ff7070;left:0;margin-left:0}.button--toggle:hover{cursor:pointer;background-color:#f2f2f2;-moz-box-shadow:0 2px 4px 0 rgba(0,0,0,0.4);-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,0.4);box-shadow:0 2px 4px 0 rgba(0,0,0,0.4)}.form__input{display:inline-block;flex:1;padding:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-moz-box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);font-size:16px;background-color:#fff;color:#333;max-width:320px;min-width:160px;margin:5px 0;outline:none}.form__input.form__input--error{background-color:rgba(255,112,112,0.1);border-color:#ff7070}.form__input.form__input--valid{background-color:rgba(125,252,245,0.1);border-color:#00C0B6}.form__input.form__input--login{background-color:transparent;border:1px solid transparent;border-bottom:1px solid #fff;-webkit-border-radius:0px;-moz-border-radius:0px;border-radius:0px;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none;margin:10px 0;height:40px;line-height:40px;font-size:18px;padding:0 5px 0 45px;color:#fff;background-image:url("../img/login-icons@2x.png");background-size:40px 80px;background-repeat:no-repeat;max-width:330px}.form__input.form__input--login.name{background-position:0 0}.form__input.form__input--login.pswd{background-position:0 -40px}.form__input.form__input--login:focus,.form__input.form__input--login:active{outline:none !important;border:1px solid #fff;background-color:rgba(255,255,255,0.2)}.form__input.form__input--login.error{border:1px solid #ff7070;background-color:rgba(255,112,112,0.2)}.form__input[disabled="disabled"]{background-color:#ececec;border-color:#ccc;color:#454545}.form__input.input--number{max-width:100px;min-width:100px}.form__input::placeholder{color:#b9b9b9;font-style:italic}.form__input::-webkit-input-placeholder{color:#b9b9b9;font-style:italic}.form__input::-ms-input-placeholder{color:#b9b9b9;font-style:italic}.form__input::-moz-placeholder{color:#b9b9b9;font-style:italic}.form__input:-moz-placeholder{color:#b9b9b9;font-style:italic}.form__select{display:inline-block;flex:1;padding:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-moz-box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);font-size:15px;background-color:#fff;color:#333;max-width:320px;min-width:160px;margin:5px 0;outline:none}.form__select.form__select--error{background-color:rgba(255,112,112,0.1);border-color:#ff7070}.form__select.form__select--valid{background-color:rgba(125,252,245,0.1);border-color:#00C0B6}.form__select[disabled="disabled"]{background-color:#ececec;border-color:#ccc;color:#454545}.form__select.form__select--inarray{max-width:120px;min-width:80px}.form__checkbox-container{margin:10px 0;align-items:flex-start}.form__checkbox-container .form__select,.form__checkbox-container .form__input{margin-top:-7px}.form__checkbox-container input[type="checkbox"]{cursor:pointer}.form__checkbox-container .form__checkbox-label{display:inline-block;line-break:normal;font-size:15px;font-weight:600;padding:0 15px 0 5px;line-height:18px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer}.form__textarea{display:inline-block;flex:1;padding:5px;border:1px solid #fff;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-moz-box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);box-shadow:0 2px 4px 0 rgba(0,0,0,0.3);font-size:14px;background-color:#fff;color:#333;max-width:400px;min-width:220px;min-height:80px;height:auto;margin:5px 0;resize:vertical}.form__textarea.form__textarea--error{background-color:rgba(255,112,112,0.1);border-color:#ff7070}.form__textarea.form__textarea--valid{background-color:rgba(125,252,245,0.1);border-color:#00C0B6}.input-file-container{position:relative}.input-file-container .input__file{position:absolute;z-index:-1;top:5px;left:5px;width:1px;height:1px}.input-file-container .input__file:hover{cursor:pointer}.input-file-container .input__file-label-btn{display:inline-block;min-width:120px;text-align:center;padding:10px;border:1px solid #00C0B6;background-color:#00C0B6;color:#fff;z-index:5;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-moz-box-shadow:0 2px 2px 0 rgba(0,0,0,0.2);-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,0.2);box-shadow:0 2px 2px 0 rgba(0,0,0,0.2);margin-right:20px;-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease}.input-file-container .input__file-label-btn .input__file-icon{display:inline-block;width:30px;height:30px;mask-image:url("../img/svg/upload.svg");-webkit-mask-image:url("../img/svg/upload.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:#fff;vertical-align:top}.input-file-container .input__file-label-btn .input__file-label{vertical-align:top;display:inline-block;height:30px;line-height:30px;font-weight:700;color:#fff}.input-file-container .input__file-label-btn.error{background-color:#ff7070;border-color:#ff7070}.input-file-container .input__file-label-btn:hover{cursor:pointer;background-color:#fff;color:#00C0B6}.input-file-container .input__file-label-btn:hover .input__file-label{color:#00C0B6}.input-file-container .input__file-label-btn:hover .input__file-icon{background-color:#00C0B6}.input-file-container .input__file-label-btn:hover.error:hover .input__file-label{color:#ff7070}.input-file-container .input__file-label-btn:hover.error:hover .input__file-icon{background-color:#ff7070}.form__label{font-size:14px;font-weight:600;color:#6989aa}.form__label.form__label--sub{line-height:30px;font-weight:500}.form__label strong{font-size:16px;color:#ff7070}.form__error-field{font-size:14px;line-height:16px;height:16px;margin:0 0 4px 0;color:#ff7070;font-style:italic}.form__error-field.features-error{margin-top:10px;margin-left:-10px}.form__info{display:inline-block;font-size:14px;line-height:16px;color:#b2b2b2;font-style:italic}.stt-field{margin:0 10px}.application-features-container{padding-left:20px;border-left:3px solid #6989aa;margin:20px 10px}.application-features-container.error{border-color:#ff7070}.dictation-external{margin-left:100px;margin-top:-15px}.helper-btn{display:inline-block;width:20px;height:20px;background:#434C5F;cursor:pointer;color:#fff;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:0;margin-right:10px;border:1px solid #434C5F;-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease;font-weight:600}.helper-btn:hover{background-color:#fff;color:#434C5F}.helper-content{display:inline-block;max-width:640px;height:auto;padding:20px;position:absolute;top:25px;left:0;background:#fff;font-size:14px;z-index:20;border:1px solid #434C5F}.helper-content .close{-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease;display:inline-block;position:absolute;top:5px;left:100%;margin-left:-25px;width:22px;height:22px;background-color:transparent;border:1px solid #ff7070;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:0}.helper-content .close:after{-webkit-transition:all 0.3s ease;-moz-transition:all 0.3s ease;-o-transition:all 0.3s ease;transition:all 0.3s ease;content:'';display:inline-block;width:20px;height:20px;position:absolute;top:0;left:0;mask-image:url("../img/svg/close.svg");-webkit-mask-image:url("../img/svg/close.svg");mask-size:cover;-webkit-mask-size:cover;mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;background-color:#ff7070;margin:0;padding:0}.helper-content .close:hover{cursor:pointer;background-color:#ff7070}.helper-content .close:hover:after{background-color:#fff}.helper-content p{margin:0}#iframe-container.iframe--default{display:flex;-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);padding:5px;background-color:#fff}#iframe-container.iframe--fullscreen{position:fixed;top:0;left:0;width:100%;height:100%;z-index:100;padding:0;background-color:#fff}#iframe-container .iframe__controls{background-color:#fff;height:30px;padding:10px 20px;z-index:10}#iframe-container .iframe__controls .iframe__controls-right{justify-content:flex-end}#iframe-container .iframe__controls .iframe__controls-right .button{margin:0 5px}.iframe{border:none;padding:0;margin:0;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none}#login-wrapper{position:fixed;top:0;left:0;height:100%;width:100%;z-index:2;background-image:url("../img/bg-login.jpg");background-size:cover;background-position:center center;background-repeat:no-repeat}#login-wrapper{justify-content:center;align-items:center}#login-wrapper .login-logo{width:340px;height:auto;margin:0 auto 40px auto}#login-wrapper .login-form-container{max-width:800px;padding:40px;justify-content:center}#login-wrapper .login-form-container>div{justify-content:center}.setup-form-container{padding:40px;background-color:rgba(255,255,255,0.75);border:1px solid #fff;-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);color:#434C5F;justify-content:center;max-width:420px}.setup-form-container h1{font-size:22px;color:#45baeb;width:100%;text-align:center}.setup-form-container .info{font-size:16px;color:#434C5F;padding-bottom:20px}.setup-form-container .field-info{font-size:14px}.setup-form-container .field-info ul{font-size:14px;margin:0}.modal-wrapper{position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,0.5);z-index:990;align-items:center;justify-content:center;display:flex;flex-direction:column;align-items:center;justify-content:center}.modal-wrapper.hidden{display:none}.modal{max-width:880px;min-width:600px;max-height:600px;height:auto;background-color:#fff;display:flex;flex-direction:column;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);padding:20px}.modal .modal-header{min-height:30px;height:auto;border-bottom:1px solid #E0F1FF;padding-bottom:10px}.modal .modal-header .modal-header__tilte{min-height:30px;display:inline-block;font-size:18px;font-weight:600;color:#6989aa}.modal .modal-body{padding:20px 0;overflow:auto;flex:1}.modal .modal-body>.flex{padding:0 5px}.modal .modal-body .modal-body__content .subtitle{display:inline-block;width:100%;font-size:18px;font-weight:600;color:#434C5F;padding-bottom:20px}.modal .modal-body .modal-body__content strong{font-weight:600;color:#ff7070}.modal .modal-body .modal-body__content table tr td strong{display:inline-block;vertical-align:top;color:#6989aa;font-weight:600;padding-right:5px;line-height:32px}.modal .modal-footer{border-top:1px solid #E0F1FF;padding-top:20px}.modal .modal-footer .modal-footer-left{justify-content:flex-start}.modal .modal-footer .modal-footer-right{justify-content:flex-end}.modal .modal-footer .button{margin-left:10px}ul.deploy-status{padding:0 20px;margin:0;flex:1;display:flex;flex-direction:column}.deploy-status--item{display:flex;flex-direction:row;line-height:20px;padding:10px 0;border-bottom:1px solid #ececec}.deploy-status--item .icon{display:inline-block;width:20px;height:20px;background-color:transparent;margin-right:5px}.deploy-status--item .label{display:inline-block;flex:1;font-size:16px;color:#777}.deploy-status--item.deploy-status--item__updating .icon{background-image:url("../img/deploy-status-icons@2x.png");background-size:20px 60px;background-repeat:no-repeat;background-position:0 -40px;animation-name:rotating;-webkit-animation-name:rotating;animation-duration:1s;-webkit-animation-duration:1s;animation-iteration-count:infinite;-webkit-animation-iteration-count:infinite}.deploy-status--item.deploy-status--item__updating .label{color:#45baeb}.deploy-status--item.deploy-status--item__valid .icon{background-image:url("../img/deploy-status-icons@2x.png");background-size:20px 60px;background-repeat:no-repeat;background-position:0 0}.deploy-status--item.deploy-status--item__valid .label{color:#00C0B6}.deploy-status--item.deploy-status--item__error .icon{background-image:url("../img/deploy-status-icons@2x.png");background-size:20px 60px;background-repeat:no-repeat;background-position:0 -20px}.deploy-status--item.deploy-status--item__error .label{color:#ff7070}.healtcheck-overview{max-width:320px;margin:20px auto;background-color:#fcfcfc;-moz-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);-webkit-box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);box-shadow:0 2px 4px 0 rgba(67,76,95,0.3);padding:40px}table.healthcheck-table{border-collapse:collapse;margin-bottom:20px;width:100%}table.healthcheck-table thead th{text-align:left}table.healthcheck-table tr{border-bottom:1px solid #ccc}table.healthcheck-table td{padding:10px 5px}table.healthcheck-table td.status{text-align:center}table.healthcheck-table td.status span{display:inline-block;font-weight:600}table.healthcheck-table td.status span.connected{color:#00C0B6}table.healthcheck-table td.status span.disconnected{color:#ff7070}.linto-config-table{padding:20px;background-color:#fcfcfc;margin:0 20px 20px 20px;border:1px solid #6989aa;max-height:360px;overflow:auto}.linto-config-table .network-item{margin:10px 0}.table--config{border-collapse:collapse}.table--config tr td{padding:5px 10px;border:1px solid #ccc;text-align:left;width:50%;background-color:#fafafa;font-size:14px}.linto-settings-item{margin:0 0 20px 40px}.linto-settings-item .ping-status{margin-top:5px;font-size:14px;font-style:italic}.linto-settings-item .ping-status.success{color:#00C0B6}.linto-settings-item .ping-status.error{color:#ff7070}.button.button--ping{width:120px;border-color:#6989aa;background-color:#fff}.button.button--ping .label{color:#6989aa}.button.button--ping .icon{display:inline-block;width:30px;height:30px;margin-right:10px;background-image:url("../img/ping@2x.png");background-size:30px 60px;background-repeat:no-repeat;background-position:0 0}.button.button--ping:hover{background-color:#6989aa}.button.button--ping:hover .label{color:#fff}.button.button--ping:hover .icon{background-position:0 -30px}.button.button--ping.loading{background-color:#fff;border-color:#45baeb}.button.button--ping.loading .label{color:#45baeb}.button.button--ping.loading .icon{background-image:url("../img/loading@2x.png");background-size:30px 30px;animation-name:rotating;-webkit-animation-name:rotating;animation-duration:1s;-webkit-animation-duration:1s;animation-iteration-count:infinite;-webkit-animation-iteration-count:infinite}.button.button--say{border-color:#45baeb;margin:25px 0 0 10px}.button.button--say .label{color:#45baeb}.button.button--say .icon{display:inline-block;width:30px;height:30px;background-image:url("../img/say@2x.png");background-size:30px 60px;background-repeat:no-repeat;background-position:0 0}.button.button--say:hover{background-color:#45baeb}.button.button--say:hover .label{color:#fff}.button.button--say:hover .icon{background-position:0 -30px}.button.button--img{margin:0 10px;border-color:#6989aa}.button.button--img .button__icon{background-image:url("../img/mute-unmute@2x.png");background-size:60px 60px;background-repeat:no-repeat}.button.button--img .button__icon.button__icon--mute{background-position:-30px 0}.button.button--img .button__icon.button__icon--unmute{background-position:0 0}.button.button--img:hover{background-color:#6989aa}.button.button--img:hover .button__icon.button__icon--mute{background-position:-30px -30px}.button.button--img:hover .button__icon.button__icon--unmute{background-position:0 -30px}.icon.icon--status{position:relative;cursor:pointer}.icon.icon--status:after{display:none}.icon.icon--status.icon--status__with-desc:hover:after{content:attr(data-label);display:inline-block;width:200px;padding:5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;background-color:#ccc;position:absolute;top:-8px;left:20px;color:#fff;font-size:12px;font-weight:600;z-index:20}.icon.icon--status.icon--status__with-desc:hover.offline:after{background-color:#ff7070}.icon.icon--status.icon--status__with-desc:hover.online:after{background-color:#00C0B6}.icon--status__label{display:inline-block;line-height:20px;padding-left:5px}.icon--status__label.label--green{color:#00C0B6}.icon--status__label.label--red{color:#ff7070}.client-status__link{display:inline-block;text-decoration:none;color:#45baeb;padding-left:10px;font-style:italic}.client-status__link:hover{text-decoration:underline;color:#6989aa}.auth-status{display:inline-block;width:10px;height:10px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.auth-status.enabled{background-color:#00C0B6}.auth-status.disabled{background-color:#ff7070}.skills-list-container{max-height:480px;overflow-y:auto;overflow-x:hidden}.skills-list{border-collapse:collapse;width:100%}.skills-list thead{width:100%}.skills-list thead tr th{text-align:left}.skills-list tbody{width:100%}.skills-list tbody tr{border:5px solid #f0f6f9}.skills-list tbody tr td{padding:10px;background-color:rgba(255,255,255,0.8);position:relative}.skills-list tbody tr td.center{text-align:center}.skills-list tbody tr td.skill--id{min-width:220px}.skills-list tbody tr td.skill--id span{font-weight:600;color:#434C5F;font-size:15px} diff --git a/platform/linto-admin/vue_app/public/default.html b/platform/linto-admin/vue_app/public/default.html new file mode 100644 index 0000000..041b9e7 --- /dev/null +++ b/platform/linto-admin/vue_app/public/default.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/favicon.ico b/platform/linto-admin/vue_app/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c7b9a43c8cd16d0b434adaf513fcacb340809a11 GIT binary patch literal 1150 zcmchVOGsN$5QZm2NTI$erQpKHrdQX(jn+pVxKN`Ng)RzW5+8_2Xb@Y)Dkd6tq9V8u z3WAh^C@KZ1kA;tohzs}b3NC_*QmUXr$oP*rH(2mdT{z*(KX=aj=bX$9kqMvFRKj;Q zwI&d~A);J>5-PDega~WT5us%#Dc(Y}C4WpP?+fS;FaZ*z_CFzgiW=w{I02=q_TUz( z?=^H2uwoIK1n%|Ay21~QgjV1emYtWttJdz^L#=DjJ@Ex*9UPc*7<=rZo*_NAh4PxA zqkso~Ioa1y$e+3kIkXi29YNLi&lW}vY6C}ut4{8ou(7w=$_=$v{yJ$h?y!&bJfq*( zL_NQRF37$6e>%9erGV?p^lRFD?|5J_eupXaS;QluyrOmBT>PJhirMYb*i?(4Tf=j~?VvnUlY_ zDCVuuk3E&T9aP~Cr-0i-MaKUjf_|U!=R&t}_CfD=d${p~HH`BPaqb9aXT}UI$iGRg z>0^GlZ`vM4?;$*LhfI(RG|XK4GF+@-W*W}YJT5&2N_ZyZuaM_Ry=%PWx>r0P(Rc?> jRc4}SfGA>*agjwN{7E7DEm(*)%rSx{B0<6wBoglxJAy|R literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/admin-logo-dark@2x.png b/platform/linto-admin/vue_app/public/img/admin-logo-dark@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..5ac9a6686d8e72009039c69bbf7f13395b9054d4 GIT binary patch literal 2590 zcmaJ@c{~%08z1qK`UWQgg!ZeVa`rF4pynw%E@mKLT6M zB{4=KDU~BHL&mAol4bB>;!Rcn07D00xB&Nm!F2)l!fL#v1&Vlka3 zeR1{97bUee^`Z*Fb7z+?nuxL_hDe$w#ioC-WoC;I<;7ieHq_e)I{)Lsc}q>x`4{Q* z-yF^l@EAQGFXCyG7!Tkew(s3BYU$lss+-TkruY&D!K z^xn_}0^B5&(ux86Y=CQEKtwA5@aOR6)jqS9b0~tdaze{xP*mxrCd2&7nqv!5_?Jz6 zfH^ELJUH`SC>O2hO#XW&THQ-j?&!vjWmT{8M)|h~B-qW^kVhh{DJ{q?f`O%`)s*GW zjjZuz@1u|sjvQjBn0v@UFazcve0<|x_vfV})rntaP>u?}Xx#=iubzda=VN*q)Tu~V=KDar zB=@p{^xwoeKUvwLZ1!qXt6>*;*%FO4Q@6MGE*sC_Et~tIS&d20G&@DmGoStvSvfkX zW_?nzVHeW*yc9X-PL{w~L(JvrH=V}ZuM(bJj~|p)?F}Bme2!64fc@F+ndZ9K{x((% zt>ZiK5M!z$Hxkn}7x#&!9~&IY(KY%d!N!Z$;dG_a2DrCf2xUAGkE1y`0cnLS{Vpr* zO2as5amFA8io9Jl59ec8XcceV_vrV}gYu<5a+4ba2h^o3vCFDBf(HNlD%}I3qtJn8 zzE7&do;BlE1L2CylJplCf1FF<9`bSa<3quRb|PqUJ?yLl2%1=sq{7@W0c|C5+pwhv zmc^_cWWDli1h6LJl3qyi?F74#JI~(?-U3dKZp_F$lYplc&MP@{mvQJ{0eP;JL0D!+ zl`Ea&;$w{`*d3NmCZUYkexSkH_s3S6SA@Hpic@Y@wK+pH?H%?w=qT%Bk29!3Fl7UFF3Lm%{JigCqJ86QqvYjxSjjk z4i01uyQX~GQ51P-H{tj%;awuF7;oiGdD6_MA6McL+UC#&n+%Z?a`r$qu$p9Qpg~f( zR%B!x;W>oj_HhZv)|c}-;MsWIV!0XyWvxTqp-&Mz76*x;O461`l*3p)J11DEed&p= zaF=1S#+XfiGGi1+oPQO7F^a}?93SAQ{k$m;!b=d1eo$1;vF+4ii&r@coS7H1cCt>d_H z%mXrxEQg4($S7Iw&Gekq{z9lp|Atw$!KfCDm8ezJ_S#p}qYk0(8hsB|w#Uo;sXYUX z-RxTBj^5e$TD$DbbVb!Da&uAbZs>a6jz6%Db93@_eh;ByaaDplLm~NY9qG2RFdnsK?r55Bx&(;HLu&2m+4mvrfxvn#=;R0f}I2bK)Svj`V#!}9| z>ZxLj7Niv6s9vlcPK7fL#M7sG^oC{qWX7r!ymf&Y?a$19^ykSgs{J}Rb5eAYQ!{|Rz@|K7G^f3Xt>x)K_?6?8u#Yf$`n`xQMUS^0 z_2KLjM*xW^kEPsK8S^;0-=Hx8OO>mW^9zi&8dz3M|a8W@PGK@WBP0<^)u(uPquvAPWz#ft~0ol>*oEur42r{Y^@5f^V&Ss9@6v`|J7D0Tc5w7DAqc z-E*`$9;>3Z)8?EN16*cUAnOiM%`kq0WKr>Y&q0UUfKKlo+ArORbU83Cf2C6g(oG}I zf1?$!o+n231;)SJGTdfZ%Sh@hY0YZ_cxfmHae2q?>U!d;r>!QVmiG!8 zP3>2*+AL${OII1=z1V}evMAx}LF3XkZD{fa=aeQ7SH^Zs-=X%1#@>{We&Q5ga>Lfb zEW%JF(SKc_)s!VZw$E0T+%fC=g_EGK;QCnfrpe%GH6e!e8aN<1Ij_sXpt*&!NbD|o z-+3f*-)MX{b(d-8Dl`2 zn!VmfgXV#()oZUUtbH_HPQ4zO6VSHnp%0UWije^&rU81tKf~}plhhhocK2htGyD$`I2AYzu`uDrjSY<_>2DmPT%Wf literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/admin-logo-light@2x.png b/platform/linto-admin/vue_app/public/img/admin-logo-light@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..4493f64df70cb06580daf0c1ce2cfe2b8e829222 GIT binary patch literal 2537 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 SVV?AV=*BR(!CO7o-~SD|K(v+s literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/admin-logo@2x.png b/platform/linto-admin/vue_app/public/img/admin-logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1cc23411a03cad4b0eb87d0ef724fb0f212b49a0 GIT binary patch literal 11049 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 O;bbHf#mhyF{r?Zgn-28= literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/bg-login.jpg b/platform/linto-admin/vue_app/public/img/bg-login.jpg new file mode 100644 index 0000000000000000000000000000000000000000..67ff498d550d623c087b32d2b82841d6d0f63466 GIT binary patch literal 116925 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 literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/close-icon@2x.png b/platform/linto-admin/vue_app/public/img/close-icon@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..18882a2d88674023fb140dbe63b3f7a6b8026199 GIT binary patch literal 598 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j#!3HGPnNH*YQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsTns7=L)WIEGZ*dNa#Aiz!gRb^c6trgw^ua}Hj$IIX~4_~6Xd z9jwRSDGHomQ(7V*c_p zx6iIOTCnS%lgP8X_pG#*D!11@#?1Mzvk`IXPn*{y~{bQDZC!tI1<)2Miw<}}nuSl({6;rZwy>sQfa{0Y^ z8N7Ncl$hMSS3ke>F5%nE*_SWu{CeqK{zCU(>t?sV4%l>bsdH6W@yly+57ML{eu9z* z?7MC+&0pTWcW;dD_Sy2O4yAuvHO{o}Ee-H;zw-Sb^8+KF&Cjd3S533M{{2etkN$S3 m$5J64OBj3}JJ4y*$i`r>Pa^YrLB&pxH$7ebT-G@yGywoy!3Dwq literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/deploy-status-icons@2x.png b/platform/linto-admin/vue_app/public/img/deploy-status-icons@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..436f688f6286446cde78e8423a3b17ec08a1eaea GIT binary patch literal 1111 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 HT@(HR0(Nw{ literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/android-icon-192x192.png b/platform/linto-admin/vue_app/public/img/favicon/android-icon-192x192.png new file mode 100755 index 0000000000000000000000000000000000000000..97257fcbc9c0ddcd1d672d23e28c3a3650cac8c9 GIT binary patch literal 3833 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 A?*IS* literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/android-icon-72x72.png b/platform/linto-admin/vue_app/public/img/favicon/android-icon-72x72.png new file mode 100755 index 0000000000000000000000000000000000000000..c7d2bea25be43237f98c04757881276151cedd50 GIT binary patch literal 2429 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fHRz`&Fk z;1l8sRN%1t%_tfT0a}E>|Ns93wQTMI6WPgqT`h}bq|h@3o?=8VN;Fl-+lA- zm!H31-PqX^ro<|;@Xn9#bNmDo_J94fCzjo3*S9S$GQDqKG)tKO)xUOu@!@UX2bR|j zm(7~JeBV}%iI=*)`Iz}tCj4m)eE;uqk%DsROE>WYGt_m2GV{1A-bo%$W=R*0{+v_p z&$)UE*Zg_9kIvmn`IS``aYp*evBo}42Sq0-vyEP<)0RXPbfisPyw3S-{^K_`9@S!} zs@gk`%AU{8U1R)g)8vfjC50#2KE{a7JzQ&ay32L5n%=qk{A)l@F(!GtyPW#G#q1f7 z!&%@FSq!AX8J5BAWVRgx1M_B27srr@!*8d*jPD5)IX?H~rIfS)-(U^hjG0op(|F`m zHC1LAWoo$GbnErn6h2wx=Jcc%k=~GyAjRKh|F!qds!o4b`|ix+!_xEX^3T8jTzhWs z_c_JyjZ;r8QTxizYP5dk+Q%rdR+TR(rlmBV7r0Cu*sbIt87 zT#UFA8vQdOEQ*3$E*x_0cI3)t5|$M0mFT*7%*(-XPM#-=P+*PIqIPZ;kEmTdgrCvuDS9`Fnm@^=sQn^%WK{8wyIr}KM`22vmFS;m%m@5lBrrVW zvMG_!a6cz#oY>isx{SsBG{@xnovn#XR}D8+ZxPku4-pDp{Ax$*+%k_+n}=2hJvg)c zgnrk&+H#}w$sENXD{pz313N$82$-!Nr@YkV?6s$c4qtcptl@nW%3W~qg+ZHnkeflu zBmUT&M_&UAjLXgmY6KirmDzVD*CUd<>{nIb0_`1d&xZe;yEfQ*P2Keard77b1kdg} zIN#K@bVp8p(1M>k*5s#tbc~C-8H*jh7o^`WH+EyKIZ*X} zKkKAm1?hR2{4D0)*~j;ay!;f{;I>-%mq8zYklw-8D-Mlvp_Wbk-9HyNExK(Lvz_sr zZ6S;G?h1v5{n?MsgS zIhT5zkt_P1a(Jqgg!st{(aeiqj@2D_&8)cdUfvRm71KX5Fg$fn5B{_K)s{yVGZjv3 znzY+)daH7q+rACoHXV7Fbh*6F*7DEW+aDa(-fWq1=}uzo?GJYktE(&w3p?2uAgLiL zV%Gh0+r}G9Carn;CVW!royF~D3+3aKd_UaHo5W(&y+@U?Zr;jCE?MgZE2lIWU0XMi z!RYmoHok_WE%#qpdRB+j@NscPAL_X1Ji#zt)o!-Bm_@~kua73&T4lad*yy&-pKkZ7 zf3x$W5AWqGmy$mJb8~&=ugR0@_Njl3e`T)Zr^lUXmB0wh%c>==5hW>!C8<`)MX5lF z!N|bSQrFN>*T^iy(7?*X%*w!6+rYrez~F<_av2m2x%nxXX_dG&G`h<#0BUdp*-)IH zR#Ki=l*-_lo0y*Jo0y)NoULG{XRc?VYpDRV(nQz5RM*f*A<)oFA)}j_G(WPzgVhIl-A#sSE~APE!`yy#Y#vAxVV> zc`~GB=A;6>ub-P&l9QjVpO#pbnVg?jtdEEo{X_!`lVl5X6B9!dV<3@~YG|08WMpEV iY++`QXl7up2h^blbRT!~Enqu}fx*+&&t;ucLK6T^uV*v> literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/android-icon-96x96.png b/platform/linto-admin/vue_app/public/img/favicon/android-icon-96x96.png new file mode 100755 index 0000000000000000000000000000000000000000..7857e52de87e89843336fcb004e40ae6b936af87 GIT binary patch literal 2916 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 LZ7`K)UeW&o8J2jb literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-144x144.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-144x144.png new file mode 100755 index 0000000000000000000000000000000000000000..5b51b4bce1192268d28372e67d1f0d29e4690525 GIT binary patch literal 3889 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 HT@(HR0(Nw{ literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-152x152.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-152x152.png new file mode 100755 index 0000000000000000000000000000000000000000..e6b622333f2029ae94123378243698f17d63bb1b GIT binary patch literal 4039 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 KLAOrZDdr#Tf}}|R literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-180x180.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-180x180.png new file mode 100755 index 0000000000000000000000000000000000000000..b106f9290ca88099af5b241bcc2dac5570fe25a6 GIT binary patch literal 4671 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 literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-57x57.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-57x57.png new file mode 100755 index 0000000000000000000000000000000000000000..78e501e649b778e6a7b9c8265e7cf3740ce93f57 GIT binary patch literal 2180 zcmeAS@N?(olHy`uVBq!ia0vp^mLSZ*3?z3SE8Y&ISkfJR9T^xl_H+M9WCijWi-X*q z7}lMWc?skwBzpw;GB8xBF)%c=FfjZA3N^f7U???UV0e|lz+g3lfkC`r&aOZk1_maF z0G|+7paO^8Z${B*2#^s1|Ns9lURN^_n1=S11o;I6(;}C&j+v#Ax-cu7TI{qP7azZV z`||Gb)pgF|OuXK6fByS)s?mTidiRF~7A$(pzElV>2VI#jpRr5(-61W{r`u%C|9x?C zX%0!Sea9&a?yWM)J%f9XTzmLVgdSEDeyZF0nI{%8!?KaOHb}lMa zy>;o^oR16l6^LAwjf>cy@+>?g*J-xOqc{2*l73&`?_Fp9!z)taMA?(Gjyy-w#O|*> zkkxjc z?PRtcFyB1$ba4#vIR19p&+s5ek>h)(bTv(zBGLBLc>9z^E``~SF}FOpZ{=LRb81tN zx0i9|MkDXV3$C0x)g^Y@Ja_K?wdY=*eSFq{dCq0azfZm&|9S8Hnd!?)k>%j>h?Qi_9_`^(q(I=}0J~UQEi>nxwKs*YkWpYnr05 zcD3drm5ibyUV%pkQcw4GsGjYbdNAOG@l)l#Anr4NVj5Q&2o|b}h<*;ZAU!KN zH^#8_jt941JLJUJbvniI+NZ8lt4d#+=S)%exsj4QQFHcR#jB>#+iqRux?rtyd)xHi z*L$s*_*px?zn{0`@*0Lc`&O52->)b1i0P!Q=cK*m@`Y~`u6xcD{`Q=)HYL`!j9+7= zJ5#8$vf1oenVXi1RI@K;zQ@Yb9<_1p;_bF`)U4LJOU!KFws?k5mugN!@(s(p^tj0C z_7~GjCM|i(VrSGHslLFjbNf3brUR8;G4{C+MP{sw?s=goe818y=Ka$z1{S7CM4OwnBoZln# z=?vc_nFH@Dr@9}z_MogorO3L&`-N2cv4dX~4U>MbIqur}sOqNgx!QZ5It1@&H2Li8 ztuAlyT~)Q@-8xl|6^D+^^+~(i_rS?vadOJTzK5L&J?US&)*i6Wd^f4J`n_G*r`gQE zgX1-%Q$kK2-XUsz)3D_8GhUksz7AS{6Y4b1&gybMlFqO8tK;JT9lfvKs$V+b;2i6} zH2L6yH(yUUJ}D0BPpN-q9z1`4f&U9eho@3CH(NX^eMC4zLhdf$ZJ(1;6Z_@je0S%`*YvrL zspn?x+HbA$N;+{{Yxb{f_fn28(cH!Jnspbi`}Dj2p~`x-ZZfuMmcL6d3g3S# z_u&6EXLD7(#pOTqYiCaUXyZi#61~-rm#rbI^<%vb944%1(>8ZYn>6yvd z3TArddKS8t3P3AObPY^(4UH564b2oXN=gc>^!3Zj%k?rrs(~1&S}(sS{Z89kpn(h$ zAQM9}N^_H}tX%SwOLJ56O028`fJ%!Q4441kt&gS#Y^Rk~MrvkyMhSzVr4c_vQ7cf5 zIFdO~HQ|{lB^e+km)9tA0F_80De=wBO)aS`NM!)KO1~g4-F{P;ADTJ7AwX3OhNk8w z=4PgbX2$E7j`sqU@FSTMoLQC1VBq95Ws%(*pi~%=RA`VVLt17|D$x7-xp^fy`RV#; ziDj9|`FX|qh=|coG_Wv9wlFs_F*Gp-5=p6shRI1rCg#Z&W(J972IhJ|9eP0baW~%r RHYXSuJYD@<);T3K0RTqX1{DAR literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-60x60.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-60x60.png new file mode 100755 index 0000000000000000000000000000000000000000..176ddb8208a91aec6ab0c00c205b2753d8c6677b GIT binary patch literal 2227 zcmeH}doVufYFVfiGXaq-*dn z$YHuCq&qN>L%DpFg4_xzX*F6zc2Q+*wYcDf+h!ScM$W_Wg+HF(Sd)?eq|?Ou0ns^fYr?|{$Iyr$pVjiUeb)KIb+{o%E;tli4< zx8W33gMljD{SUUSy@GBTcF>W0qJBBs^FFy8C%ikJ5i1XW_{qYS}6gFwK@!5O% zJ4rPcJQDV3Ha6c!^=&u4VV}RHO>tLghGp)CAZPCB5A)M($E)41%mjs-qV!%EHl!Xi zMj?|~=51#~sh{W9b|QgnAk|ZVcei7#8u^eQRnodi4SFhoJ;RaI2Sd;KilPM z=8jFxXMc*|KkAaat6L}oKEVgg;_B9V)%uvfj)}8KQkFS!94ot5t!QFlMPBmBeah=^ zSv&CVz~a`y1N~r4kh2=aPNmNKYP%sS(N&hTDRa-=J!Ab7p#ShQopSjET7aSq)xlY} zh~6Rfi^9cPR28Es{NU?Sb%M@RV%wW+g%aNF<}NOjSX-abcuFm+qO#hvwBqfXfYz6_ zx{;#c0V(~YmF#?radDi|=tQnS+DUl~wJj*$ataKzKdXs3K`3`I1|zfk=*6^j4F&cf z*}}zzjsnrH==AyQ*diutGZml0ShvWm`!$6g@)Mk(^n5Hl+8jtj^|8epAYKiNXJ)m!(MMgS!0Q=EnQHyWkYP2Si7lB z|IE+rB-esdHPH&!viC(#>qYaQNTcJaFZ#~jRv$p|Hg?j^LGo9;>{bfQ>&N#qZF@U9 zO139wee5!bzZV;P8KfpBs?FplB$tQ}wd9E_JeMn*Xe4;`N$wX8AJvf)RV6LVvlAs2 zjul>L%?nz-QKh0seo9F>trQ!XHYE&k##~klj??D<8JgmLE}v( zNfQrWriD^<7{xv_Zpx3bj4=#B*PBMfTx|bRw_bu3gs5b zR$4pxb4Qp}>Z6i{JL@`}8L!aum&(+Z&2e*$!U_i>OQ%*(?|$|CX5+g3@B>dr^YuBI zFS^fIXSaDgEboO^&yA^LE~y7^uH4P`93_~BmbQcZKIioBv>z25b%A4q0C@5 zp!qX7Y7!xg|Bpw49VPFF-XPBVH6$%4MI9zCGD3-TKBZkmeelVve zeiRV|8x|E^xFP=DBoc+s3h`$L2uUO-1Qh^S^yMz@8|G_G5{b*^akxSNn;0m9;5dX) zS)%hdLOi4KDo&IS;foKavpf^@kp>2jshfG^LFjZ7csA0YJOb8x=QpLi^OfB^T; z#^9KcF`i{i#N%PSA@cKP!!XO+08eBY6Cfr5B4UvYEHb{;vE9h)0|1(p1Eq-U74sLl Ch!oxc literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-72x72.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-72x72.png new file mode 100755 index 0000000000000000000000000000000000000000..c7d2bea25be43237f98c04757881276151cedd50 GIT binary patch literal 2429 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fHRz`&Fk z;1l8sRN%1t%_tfT0a}E>|Ns93wQTMI6WPgqT`h}bq|h@3o?=8VN;Fl-+lA- zm!H31-PqX^ro<|;@Xn9#bNmDo_J94fCzjo3*S9S$GQDqKG)tKO)xUOu@!@UX2bR|j zm(7~JeBV}%iI=*)`Iz}tCj4m)eE;uqk%DsROE>WYGt_m2GV{1A-bo%$W=R*0{+v_p z&$)UE*Zg_9kIvmn`IS``aYp*evBo}42Sq0-vyEP<)0RXPbfisPyw3S-{^K_`9@S!} zs@gk`%AU{8U1R)g)8vfjC50#2KE{a7JzQ&ay32L5n%=qk{A)l@F(!GtyPW#G#q1f7 z!&%@FSq!AX8J5BAWVRgx1M_B27srr@!*8d*jPD5)IX?H~rIfS)-(U^hjG0op(|F`m zHC1LAWoo$GbnErn6h2wx=Jcc%k=~GyAjRKh|F!qds!o4b`|ix+!_xEX^3T8jTzhWs z_c_JyjZ;r8QTxizYP5dk+Q%rdR+TR(rlmBV7r0Cu*sbIt87 zT#UFA8vQdOEQ*3$E*x_0cI3)t5|$M0mFT*7%*(-XPM#-=P+*PIqIPZ;kEmTdgrCvuDS9`Fnm@^=sQn^%WK{8wyIr}KM`22vmFS;m%m@5lBrrVW zvMG_!a6cz#oY>isx{SsBG{@xnovn#XR}D8+ZxPku4-pDp{Ax$*+%k_+n}=2hJvg)c zgnrk&+H#}w$sENXD{pz313N$82$-!Nr@YkV?6s$c4qtcptl@nW%3W~qg+ZHnkeflu zBmUT&M_&UAjLXgmY6KirmDzVD*CUd<>{nIb0_`1d&xZe;yEfQ*P2Keard77b1kdg} zIN#K@bVp8p(1M>k*5s#tbc~C-8H*jh7o^`WH+EyKIZ*X} zKkKAm1?hR2{4D0)*~j;ay!;f{;I>-%mq8zYklw-8D-Mlvp_Wbk-9HyNExK(Lvz_sr zZ6S;G?h1v5{n?MsgS zIhT5zkt_P1a(Jqgg!st{(aeiqj@2D_&8)cdUfvRm71KX5Fg$fn5B{_K)s{yVGZjv3 znzY+)daH7q+rACoHXV7Fbh*6F*7DEW+aDa(-fWq1=}uzo?GJYktE(&w3p?2uAgLiL zV%Gh0+r}G9Carn;CVW!royF~D3+3aKd_UaHo5W(&y+@U?Zr;jCE?MgZE2lIWU0XMi z!RYmoHok_WE%#qpdRB+j@NscPAL_X1Ji#zt)o!-Bm_@~kua73&T4lad*yy&-pKkZ7 zf3x$W5AWqGmy$mJb8~&=ugR0@_Njl3e`T)Zr^lUXmB0wh%c>==5hW>!C8<`)MX5lF z!N|bSQrFN>*T^iy(7?*X%*w!6+rYrez~F<_av2m2x%nxXX_dG&G`h<#0BUdp*-)IH zR#Ki=l*-_lo0y*Jo0y)NoULG{XRc?VYpDRV(nQz5RM*f*A<)oFA)}j_G(WPzgVhIl-A#sSE~APE!`yy#Y#vAxVV> zc`~GB=A;6>ub-P&l9QjVpO#pbnVg?jtdEEo{X_!`lVl5X6B9!dV<3@~YG|08WMpEV iY++`QXl7up2h^blbRT!~Enqu}fx*+&&t;ucLK6T^uV*v> literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-76x76.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-76x76.png new file mode 100755 index 0000000000000000000000000000000000000000..82a9a89cdc057815a8f916fb15c855907eebe1fa GIT binary patch literal 2532 zcmeH}X;70%8poR}0uhlJoDdE{L68BGHxLqt$R+nBfh2$oCLtscBnbf@MRr_^6P1XDKQ{DZ0{!e$m-CdpH z?q)A1t1JrukaKjf@qoR{mPt#%<)ULA8g`O_R<2e6R9!-DFs0zSSh$D1HK=&1It5oW zSPmYp07Pm75PJrIHMl8u5&#hdz%&B@QWgNH(40E5835u@?k*Ht7_h9L{pf#-_uw?6D>xRZ5~g4wx`SSV6wR^-_^S#^hy>{Il)6car z&l7yIeTHtP4SM|T`!u7;v&QP9KmKy#`b@;`RNR}=VYikTs%Yt78Om1xakx|K8-3G}41eNJW>68|Ni*sJu{co4TP-&FF)bexG4V>f5npM(RCp!m z`?*s6=<^b~(RvEHT=u=)%31e$sOpJ~nnMl}kcC=tvaJLE`dP&40a3N|NX9K!iGCxg zNS`w{^MmnXv$_GN--h&tcbsxIym9y)l-FnYoRP7BtVT7slOHs@R8y$xC8f)qj~|t+ z=Tb8e9@qMQh?xu>jHxxEGqFwYtNV5~Tg?pv<)dV86}lWXvUTYFk;b;Sb%*3=6K&?E zH7TV*pzBsZM_f}YUAZOuqxQI_{Yi7E{9qEd0y6J;@pr!`UDT$jm@=u6d&E#)YO<@& zV99PCqfrHY7;$3e!-CD#VbAFiIYDzmPB~9iCtzH|xxO&tqT2r2gSBSf;-l;%#hg5E zLf%5tV}es`zu&;`<$V=({Pfg=jru*w)y zyzXq8NQ|joReAF5f6<{a*8KRDUp3dD^rfc3*Ty zTw>luyMQ~7a%J`m*{666nLs5S1wX!-@tN106$HuoUq1fyr|penjPo^xIqz(B6V*;= zn%-I4h|104j7Jy0O+aL({x;@@(>J{Qb&PT)vSi*QOmzSr<>2 zuT?6vl^WnuGn(Dd@qSI^y`Xi&cd7~_aks2p`Py#FwdjpY7NW5buXOZXdfxQCUt((O z_a5&!n^C_iC}Zr%+O~EzX*5|h*>;;Q={u1Bozo^zv6QWin2{pGSHV7^t+z0MDP)sa zVQkm|LkMSTfWsLW5-2!`geQ<7BRvQrLC~UdvFd*X@B{%|&dL8B5NEHR3kTS3We68= zgb~azHgM!I1KF<3KrZW;1_49Fm>8I9z$x(tkg);IP=kykXaos`p(HFeA|e9A-J)TG zX^bE&@I}%bEC`gg6e&R>ons9$R=alnuU&3LDA~w5X=J{Yl4_0#g8I zOvDoj#yEmeg?QHm7?J-<$CDe$29ULNdcOHA+(rAUi|XMBINT66Jbf&WFANa`VmZv? zT$X?zj{VP%!7?EeJj;ZL$K&uuaN*C!;aL8Lcp}S$05J&=5d%kH;PD|7M&aig0FJh9 KHWk*4_&)#^I<3M0 literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-precomposed.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-precomposed.png new file mode 100755 index 0000000000000000000000000000000000000000..767e6eb552daa8ba8da7e27b34f152ee6c52f2fd GIT binary patch literal 4405 zcmeH}hc{f`7RRq)bP_E=B!&bd!ptxRBSss&_Y%Qi^cDs|M3?9hC8D=zQKE&Ah)C2R zq=+6tv;;AFoj3Wd_tyHox7Pa$-n;AWv+us=v%lxwz0SRB$Li~g618mcJ)HA6frZQVnI?_a+tOb)bG;$>w3vs_hiN$vc$zO{FJdUCk) z`{$RkC}YWsG?e-wAAh`*W;7{UooWt-GizmTdx=v8P3K#1&|S})XuqJC{cAJ;s=V}p zAeu3^|CZZtyaq$9doUAvY4Pap`Lmw022u;!!<;uNh3MCw3wpN>$ZRy2E^B`8ug9em zCly>jx7>HPu?>Itv}<1O>n2`=jej@g-94r&j$w~q#nzsK8I_FW#y=6ZA45r~CS*N8 zC*H2QBZhdRt`k|@Yw0HYsZYbhyhFe&!l^0R$2ToU9(!x0+^X?%X-~D@&1)5E{#;&K zLL1~NP}pgyVULPjQB&lA&RgBWm+aO5GSxme3;@u-(!eSj-=0{z8E9qvm1B`HJg|Tt zbd`fk9SI*`RAQVhhsHtbwPPdr;IXx5NwLc#cN}S)z-FnrQ7rY@ezgVgmNzKHKbRI7F z*TpQ=F_qcTn}tS7^q$al^0A{sT687-o?!@r1#F-^CJ!3-cGhN>Ha5-=k2D)Cb=dgP zt#Z55x(xF3)I?(rjGNc>?nPg?tnFAQd@V(;s3?Esz+Kzjs&*9m=KISp#R2(wA(yep zxub)|wfy0JZY3X8UMbsx&?5Dy`-&QUkw%1S)rbZ4uY47eAA4VVVI7LRf>f2<^dW&> z@h5&gmyk13ojC_n!?+qxaNk|i-QP>1+v~C{d7pN{PelX(PSu0~{C4{-=>%>$Rqu3= z%R;*}Q9x#0)|N|FY)Igu#_CbvyiLZ>cFKcV!lt1OMMqRDxGIG7XxU)jHKmolGySLpI)c-)PPKk2-U8`?XX_@eeG|UA zP&2Y=1PHt7Xj@}zB)Ah@-04RjF-#pddzh=a&9rRpeelR?4}3pbePc9X^7+Z2{I7x} zh)?NnSJ0S+qWze$jO#vcfT@o9?c1fElTDLtI{9GOTH*J)U!wEH!1eR`jA6^oyWl}+ z@V3~j5XYCJZ+9Cyu)67OUHx68BFcBwUd*y%-7}qi4O<`hW{S^zAFAcGbH9`<7{*ev z*|zfIZA1UCR(T1Z*kZ1xA-ID2fWE*{vao5OmAW^ftTV?BFK~Bz=^k=H*B079Urr5H z)yi~Jq2;&r@__wMKdDUp6PQb!3pz8D~1#!Kd;vD(0z27t{{j9)1PfIG(+|K5+;r-*i@BG z*%a+BUQdA8FTyI{9`bsdnr_ou^o=nLMKGE&JiE}wAa_$t;*ff8eeBi9J(MZYa7sXX z-5~ec-bGcxoVU9laS2m}@rVHy-dimb5y_Nr$Djdx1E>KJP`;Qa$@9DovfVvam*a3_ zpz0pNcUg6DWT!l$DkQ~O861ie|Gaa(6t8zHu9Z)W=A?qCWkJ;e9?<#d>glBCjo|U9 zL`8>7F0f`1)Xp~BpOJcNGxA)VM&i!X+1G_?qn5f()I);H$=1ZdM_d@wYeB^VPVvMR zV5&48XT+2bF;kDv(Xo>l#pEWidw1n#8FJot82E7~-cBx(@p@5x1=m_WA2>(Cd*>Td z4gUvbqI}t@wQP!q>!Ivx;c}L?c7<^X-ZBoL?7`%ei4Cz`u`YxE-sXX;CWhh_JykzL zQ}&VFjI2Jj2(=NBE2en4vv%{fD$g!+|6E*r8sSoJ;&8Xni=z!eE47_MIVtE0@xhxh zopm|&Rb@mkP0k0y=Yc3!)9l@JRRf;~Eo_oN z8OOd;Zq&N`#MsK>vH_@71Rk$>8|0_&`a-!Y-G^3S_QH6N#h~5T&9srS@|Kqmg5MhA zsXjx%!%bm=3TzNF1vU1s{AP@HXo{F%XbrkXi1iFyAS)l(RubG=Z4k&&$-_<;PAIJQ z6t{SOq%L<@PQ0FQlA#$^&Dr%uiu?mu>2|iF1{AZ0IOj zH?T>dRRdTM@w_lg$_ZPp<)+NEfu$;E#QQrou|4DG=Tbmo(ZeB08wL({37Z#LqHZkr zh*S_1?*~)HUE8!d&W3DbK88+ol|j#{IJUnF3Y_7e?G0)S8zSfl+DZ^JFhWTIY_ud) zA$Z@Add@5*SyZH!h_QybDICEtYbt6jymA#DOE2>&H`+KUK5P`(UsHmKJkI&_Dk`Lf zh7c>6mcIZHPvz^+e8o@&n!!)wUu;2zHwt)ZX?`sQ4q6%~=+q`vmR-pWzx(yEe2Tp} zwModW(|DKn!Wugj&Ykw8?_a4#J@$K|SuQ{|qTMoaCOt&U#slh1l?Qg{HT2r#M5Tdq zcbl0E9iS|#5fU}oxX6g%>{e#mhd!Hz1kujK`VYUc(k@bZSL!Opb*^qq_;W1t=M8~5 z%v=2!o5fAXp;vM#q&W$K84vTSzaZn%iUm>PTE8y#VGAryc6q35`u@xe@55|=`oqBz zjr#8D&!zR~d37E6-bu4aPlfBTTg>JUD|F~cU6^F4i)mZ_>{T96`h}dr_Hoem5m-pD z8V@T0Ml<1{p3rV_*;mo;_pcouPn;ysL!$aaH|bAykZETf_$(h%O9+Rp-XQ#_9{utu z`wd3J&u+;5LK7A6fep*x%|^i8S7dCuGO%f5bnY=|So9!`02dQ;xJZNrBenlVB zyux!9Ni`7FdwqG*-znikEdL*TNKN?XB!31Xxk@sZU9#hdOyPV@`sGHl( zdcvR;RhsUv$Q$6$OZbNE%=OjE{Ez`*qPr)|l+8xD9t!vT*+o>f3YL{=`WD0-y)!^F zmh@>h`pP-qZVrhHEV|Y-K;&+MFn*Xf^mhsU9G9}f*`U-bs!zi_6PChlkD^*KB8s5e zwrr5DnXg={t3)+Bj$%N{`;0piJ*T0}B`7VX@2Ns1W9Fl}*JcFCO8G^xI^uZ6uU(7b%sJYa_K<4%^aZ2Gwl zjHRl(*ab|`ft2#|hCQq1D^;9t3L!McFnPU@);@-4>+9@cOVuXFZqL~GDn%cCO9agK z2N7R?Z5&tb8*@7zmW5yB7nBH{ii5Abcc|hcio4BxHz2?MfV_7Sa zSvZ@Vd(49@+3;^qrbHGY!oS<1+=@d&w)Mkjpvy9(aG;CzW7v=!} z-ZPS$N7sG%a3xo91yA`W2BN_}>%4)p!fY395fq@bz*e1mJw|fQARo39o~5aTR4{K(C;56)-T%7h9ub8}SdS|4=so literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon.png new file mode 100755 index 0000000000000000000000000000000000000000..767e6eb552daa8ba8da7e27b34f152ee6c52f2fd GIT binary patch literal 4405 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 literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/browserconfig.xml b/platform/linto-admin/vue_app/public/img/favicon/browserconfig.xml new file mode 100755 index 0000000..c554148 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/favicon/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/favicon/favicon-16x16.png b/platform/linto-admin/vue_app/public/img/favicon/favicon-16x16.png new file mode 100755 index 0000000000000000000000000000000000000000..37faacc2329d589179c68fbf7dd4ee9f1cf842f9 GIT binary patch literal 1190 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5)W;Oy z6XFU~;IRA6h(-VZ|9|j!6$>yFLP~=CfWgbiuc9WxBwc;t>GO;I+WLq8-SU<=@MBAU z#-jIk4S&^hv&6ie!@P6v6nUBJjUj(OSp@(2=CErw-=w0SoDpx*BoBSxkd-p`-87@) zS7gt;5>(gKQ)GC2A{S^rW0JSKOXy^l9mYToXMsm#F_88EW4Dvpc0k4kPZ!4!iOaeD zSA`Ba@VI(T6w?UX5)pmp?$RCBPyX+3z5DjVfdiFZensg*lV9>5n5i2k%|Bm^W%pst zU3cUd7JvVxvg8Nz83%)whaED#M-3+~VN3B0G;Ij6YQ8BW7$wJ@X7!+ZhQ`E2F$Y&o zTzggetp3HU@?9pE4*zR#ZCk;TVR&d`Ez^YAuR4r3aerpj3x9pK%-!#1Fk`!Nodu@} zbNW}MS3TKRcFfy-SH9vnS~e{Seckv85nCD7+4t?e2`i$gQ6ifKP5A*61Rp%cliZC4Q?PCiu2P-$`gxH z89Z|n(^GvD(=(H^70mR^^(=HP6@XTn=o*;n8X74C8k#9&l#~=$>Fbx5m+NJMR0AhKqiJ{l;$Q`S-IpVm*%GCl~`E?0F@Rq7%u<6TOUmg*iI{}jMU8Z zj1mSzOCx@UqE?_9aU^q~YQi&9N-{u7F0WDK04kA0QsSGLn_5y?kjemdm3~29y8Wgw zKQwcELx8Fn3{A~V%*{*<&5YME9q$Dy;YTtjII}91!NAFB$|AcrK&db!sn8%#hP2F_ zRG|0ubMs1a^3(Ox63a4^^Ye=J5fP)GXkcNIY+-I!lvI6;x#X;^) z4C~IxyacIC_6YK2V5m}KU}$JzVE6?TYIwoGP-?)y@G60U!D6!EHBSnG;0bNNm=`1s?eSC^LM%DQa$ z{PzMsN8$5dt5nz;zV1`c`SR`J+T{mcTR!;dBhJEO@~?71nf;lEhGsARR9oHJCRB7- zfN5!>%sX?wV|w!=C9jK4PM5fxnIOGI@a6CFo3mIKy|+DkXtj6KiG|yr#eRS4v87I;J!18EO1b~~AE2UJk&>Eak-aeD9M z>rsas1X^bLI zmFHT6FKz2!ElCSxnKsQbJ!T?rnrLdQaAPS;jt0Z*O(H3QTs#44QwqGfauzE^ZeMeD zZb{SVM zozh>oXWo(WU72=Cd(H0Nx!3w(erf;p)cT4P(Jx05oTSY)7|!eeJhvj;d)bT=Uh^1K z?r(a}CaxR$?6GhPbKk`H`$m5!w0zS375$ezm0Xkx zq!^403@vpH4RwvoLJSS8Ow6ndjI|96tPBi3NG+E^(U6;;l9^VCTSKF}`~si`H;@g* z`DrEPiAAXlp1FzXslJKnnaSA-W_sp&7P^)SKr2mj4NP?njT8b6%@i_9N(!v>^~=l4 z^)f-Kff%S-FTW`LPTO0cfeaEL6GJjebCayBT=J7kb5rw5tgHfnN{bl`m;c|bkERA} zr*C=uTl}I8f@y*OlEvYO>WdOTM zzaTH&ep8qqnmN89KvfKersgK*W~PQ_#_O1l_X3shBbgJNS(VCQ;N&!Ak=+}hR2Y&} zXpkpET4qiv(EIwic_lgd>H2AjWtqwOdBysOh|y0purNusFgGzVG%*GeNvVd0$w@{g j=E)Xj28m_{=6XOKdO-JaH{Sx5{0t19u6{1-oD!M|8D_=|GbRZQMwpl_rBcSs7~_5!Y?qaBJqO9H$R(nL zoja9cOCgeWE?ry-DfhcVTQ29Pb=Eqaz1I2v{IS>iJ@2!g-}8CC&-=c=cfIQuYfUv1 z6P6bS0En4W$TY}1Zkr$U#$hra*Ft-O_&sCSjN^i+Y&o2^HNw589)r|;`}qWZBpGAyF0tYRim7AKZQlM&#|&GwsJyhy6)n58YyMj)b|Gt+)f~V1K8KuE z^pT4h8T)Xn*%^A(IOlK8>IRFktMHo(UvlNdi$pVZLZ}PxgT9<{<#E223E3}nKJ7H2 zpS^LRoFLm|6`ziB*j<^OwNt>5NqODWg>}B5^sIN6PhxY(o*lfQ_Ne9qIjA+0yNXsT zYNDq-;fmD@G|2{1%ao6_IQ2nr*c>C!sVud7iNTWqqtZG_V`N|4RDN=#alAZKF@fdci$8YcuG= z>UpdN?8N=r`P}hT$?iTdEQk*p&Kh-ks>pFWs=GGp3qq^yPW~CvB_bRIH*AR~ zPnS%zA0B<5{@V-JhzAXO<*D@%U2m3hVckojh@SpO9s{2gR$s!1>=q57@+m>rBU7UQ z^hGT(f=izuH@tvqLb;T1tWTj;3|JmP$OXP3w&X?5CVa3OIq))-D(M|kW1O|r9h@&= zSYKsN?k!CgsHqXw^zAQtBP%@{V$wI0Bn+$7G|k&Gj4grJKRik4Mel;YeynOeA9`-- zE{x0-LOInB zo_OO*%_?lZ*?lZXctZ!#pzob%8!bVNa(?)DbU;k5E1)X_%^%JJNQjqu|`H|}} zhffREZe(2P0TPuIp9utmPwydutsA{A0R~&g^QcXN!_FV!p>d(s;qk|BG*_fmw!&-D zRSfT>idKlJ+{@qj_7KdJs`3I^!sOwkigm);L_2MjUvw@rd9M7l0Y{cVksIC{4FE-bfQFH%?I2ByAKaoRo-OGCmg2*lG!OH6-O?s1d6>ztNcXlsP@? zg+nw8=3)O;O`Y*1R?i7-!-V+af=syudl`Xjb-r2~yt%3gHa*Vuy@b;h+E}*Z`oTR6 zeN$lc>*WslLSRsC5*!~Q2a}RXQYil(K`Y-_G@)cN+mqb}Wg67AXdwA}<=~WsC9j4a z9=2H(+3hnnTuQ#2vLC@i`$h30lcgIwaA$nxPmA(m74yO_?I@rcuOBWcdaOL4^Q6*J zzxe^pS0qWfS5f+&-e=R+xx30+pt*1y{|nIj)xyeRX1T!e$uCz?hhYxam` zzy7xQ^1?xE~8l+*H1Y?3slH3l0=g)W-6Q8L5vIU$wQ>23>eK7f>3Weigfx-?{uYvEa<) zQ@5}^^6g@{r7B~&xlNTK^`_59+bA9LlKztTK&N^&Q6%MP`l*exUNFJ|wj||Mb%+r0 zwR)e0qto3wH?zF#tfav>;Iweh9vUutgtssT5mZnfZ7irVMqs%7SLWtZ?h!w|!#SQXi zfgiI%dwa5vRoPMhqGM$XQ2?t$z!P+Ju)4T1cxwWLAb-;Ng%ivI7-QqB`35sk%I>Gs zDVjNO=D4$<<)b}3x$a(Uv@<<`!|?L-L;vT)py?PQo{0aV=AEoCV4;X}-uO{Ds=C_f6SJOc`bpMM` z&pF!Q^(bN*AJA{hK+nX@M#;x<3(YS>&onsS$ax;BKVu!*Q}4(s?xC8lt@p&+TN}8X z*pEHvKGgf&8Ef8iCwkX*2c7%$PU-J|fJl@@q^fj5nt&pT zBGM6ph$!%aG$|r2hyn^5-`Ux9-)J90KkeX0HAUJK+r$`iS=m!0IgY>+ZoaU-PYCrU;m%L|NI0F4-Zpk zH!<`t{yW*m(iQ-ixp}yl7=#rJodc7uXWc8gdo!MNUSC$|C=1jv>Q2|6%NyVJe;({C zwHJn1s|XwsHH&#LwEAPP;tYsiH}Tp0*XhPE6j;WyYIbk8SR$z7^J70b##gkLBHLT+ zCTn@oAAI!c)N#~K`-H{GWAa7U{(yalKT<}tG(mV%zi@i5Oc}r1oOpq?vq|Y|=+4v- zW7WdvzCM1*i9@l{-(qh3)|=BO>Tjb;ehZq z9g{6S2l@;c+{}y(cFjE;H|PnQzsb2EdNldVjCy^v0f4m%ZJ=itHvT0q+}Uo7zkBh; zz-dIf1*17@0z?;t!}DcuN}f5YTr8J&;$G9N@?}u&<20xoNY70d+Rm&ejZcQFl)5daa7t5lY^gzxVV;>Q2X;Gcq_}riqvi_li_GC&<}LsH+*~c>2WA5x zdE`b}e|LbwCs|BRqYjwGG0PiQ$zQrL1Kw;1)1+F24 zUM}LfRh1-zl;YV6uI|cNHtBI521SLe-5NFViCO9MJn>Dw4lM$wU*^vXRur;6zEYYx zzOfM%{zlbO8tjkJE)}Lg8nbJy1W(pEco=(kchGjE>@gNXLl0pKGrRcb=KKcf%W9TR z;_*G2=b${*&g%9);)S8FY8*_M%F4&AYFwMM1(6LY8omoVa((MXY`wNd9uJ&PeTlv> zx<`GRzmhvXp1Yzqv}ehF@>OGvIhZGHTypbe-ourmyoZ~+oFj-9p|QF^^jz6%3md1t z9;y=iW!JA9!}o}MBW5m~zH0>nQ$Hu_f(M&nn@$0~h+f%-H9YuPzr*1Ptx(dvxgV6K zlIcOn@Sp8N;~tFdW;%kxcdZG}<=8z-FKA)CV>QeapOi|groMpbdPt>BeC_AmbOuS{ zM~E33>QQy}GSn-M22XbvO=Wn2t_EtbuSDcdA3Hb)^xo^@iD0T;Xz~FhgIkJb9TAQL4K6`qp zNe(W;q}`mNJ(yoL`Jd-UM(&q-N+qE-vvg^G@zjK<1}pcCHWGo#gH zmDO-w7cWf4lp6Bd^iVu|=LgW33TN=SrYtwVg4A0Ays=IdnZ_)Csubr{b<|p~4S%F` zA<8ZocJxo-V7(A6Hv~QIGxd3i11fXzD8+JY5j*g? zZW>MT9+_C$h;-^Y;If?wTh*4uq=&Rds2%axf7WM{FU_B4z8Z^9Z+z?0756TwVh&v9 zqXaXc6}hBjLhLG8)3l@Rx>L&TV$WXStP z&@ef;enPNrB6qPaX(tQ*K-b3{{Ho;oT~E?!waDaj*5FypzTMMYi-Y~Jt=?LtoAAN! z!0VD$VkN_U?AzmmP3Yst4@Ixt9%oeS3+I$jW#1n;#r7_Cm$gZ(!DP36qH_YtO{$Vc zzJ74VZ|tZ2#qfGT3~lM)ZZsi_(c^o z!a2Kdmy4$s@$^ObU$S;;i?;5KSM^7Z+v@7k zG>7}kMFM>~VQSU9pZR>o%{DVMldu;>y|SE;kD>%lzU;UJ_B6s`8sma@Y{uj$tXsr2 z!MPc+k`p40_1DdLAFlUQ+Mk)4`#S5j3!Xq5U#)JagakOB(~|}H^zo!R)c16jyphhg z^a`(=er)Jx4)m8s^j^Gls_3S(aER)nDs4Dala-c1!|zG3ltilJ1`-fsVFh1`!}Ood z->uq69EhvGfgDVu93$Z^`IntUS4QN(QgNeS%1sI=%osk5fA+%Fqn#?+Q$ELhr0S|S z?%xa6Hi~|u2YMr1CFT|*Q#&M#(@T#S*AW?vd7_z{)S=1o5hl6RrZClY0YWfx--->1 z-KGka=f~WYPcS*T^wD>vGV+5Innm?hDbwB_J2QAAPD^EiPN?XHrP#I<4^BUyFM7C3xVu zOH+E{O0C@8K$VrqPf`s7u>%WpaxRu#EVufKMc|O)KI(Ce@WXtala<&-o z#KsgF71_F)lCmfAm_7#Vd{D^NLeOx#1)D&!ylhbb{|)!XAGNoW7Rz)38nU+I??s6{ zu(FE=MbF(w2}YqL(>`+_vwxW^7d#&mvb%%&B8W6rtnb~qC{?w%`c40{fVWoun=|vx zemicnPabt$+pf|<3ROfqCG7|iF++)S)IB#Tn-y9JU+EvwdcRoN}AAF ze{0%)KB|}{*OXE6U%v@T(t9GS@lTqT(KPz82aqNko+W!=$vBjIAdVgYRR~l=2?|wG zh1)?PC>R_CQ9A{JpdgS9k#f=hDDWeB5In>GdqKRh#2tEp(XS3cBu{cEHV_A({jgp* z3#=Ex-A59xj8H}@X-Lvr!jvHDN>EivYbab2PbT}LR8&GkLzM}?Xu8p9Wm2HmWXc!1 zARzQh(GDN%=Y~QVklcg)a6~c+WkpAW0BHGPtIFS)zj~rjcpSkCPX?eGs@yisz zxLOUU`x+hL{!Pc05P<_A`udsov{&g%4!Li zRQ~;9RIm^v%pHkiR1LMF94toEe&e) HT@(HR0(Nw{ literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/favicon/ms-icon-150x150.png b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-150x150.png new file mode 100755 index 0000000000000000000000000000000000000000..26342bcf92a889cc5779707d535c2199d0511804 GIT binary patch literal 4016 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 literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/linto-say@2x.png b/platform/linto-admin/vue_app/public/img/linto-say@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..76ced6652ae4c504a74f40d4e9d3108aa4a924a7 GIT binary patch literal 1258 zcmVnCC*5ax{*P6*nKiuYx=x&9{ zWEX;0-Y5gFI=UMNyA&n^#Vheb*jnvI?bfw5I4v?#$0pK2*AF_@uh5p}ocz5=Ka%FW zCnwE0ame{m2%P6R=l%6f^1d(m11>HuE{-C0JfwA$L0Ma`1=IDqp4mVkFc(B{cBoyn z|4guTAk_iU-x1)L059#XGy69VNIyibj&?@IDk9iAkm~S=ejdOZCEhwg;l0fz%<0c10l1l_!^l#9iI*?@F|X zri%3z6>PnhYVuIK0g;#;a$695p!)*xVv-)SVtjZp=7BzFw;TWnESLr#nmz1DO?I%i zYIg>02zwFs0d-Y^{Z%`QLvzdr0C??bpQ&IKstgxw!@KnA@RWZ+y;Yt|uA({SL*Mkq z_O-t04eYP-V0Jx&*>#%s7C_9Xj%Mn0aA5H^L|#`v)kes#N4l#&GAfzvB$pi0DIf#y zx2l?2Fckd`z#}Jw@*omp-QgRmvQls)E-M+}Q)jHU(KO%#vj)?{!dO@I8vsAqs+0pE zXA<4f`({0+f&eg=j=W3Ys(o5nNL(20id-(%XX!+J^cVWl+R{8A)#a-}gYb)yj_7%a zuvl}^83cf#d5{pjTr&d00FI1wR`*(w0~8DOZrWnY9}Doz-lX^dlpl4E=c`0#c%sz% zi@wE&e_m?wde}=^n{5$UBbgMB=T1gPV(jh6-BP7oTwGjSTwGlKKiHC!kPHH-1yB!Q z1Hc@BS&{N{l5(UvNM0a0L1Qu`e<#^PvbEA;zbT~5NRm85($c|qFJcjrKkPwlH`hp- z{(80-xkDtUY>?aHFOtvOCD%mqv5j(DOj?mN{T7pq0k{X}qKNwdUJ()HZ*#>yu#V;= z02(&OP@uIfAq;1bq#(N+R4R%{h{$21f=aL! zS;0!NDk4uA6;y9=MRO7Wq0JSe0F|KWN`Ef7hWPbm^i8L?ul=#0;3G{ z)q-9H)^id7ra8&Y`E=eZ0Q|k8Bc%be>%>e>3^Bf-<-M9CSQ9oXJfC?k(2A}0Z$%t_34k}!aO;Otfhn5=<- z$qb6f3M!D3ph!+q6q5Z8e)c{r`(GrvkK`2xtzYsemS?;yK`xH*Z7i_4SDzks#T U`$u47NB{r;07*qoM6N<$f)mb5y#N3J literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/loading@2x.png b/platform/linto-admin/vue_app/public/img/loading@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..addf3f5b2f52c366c35dcf8904f74ca0b3c0f372 GIT binary patch literal 650 zcmV;50(Jd~P)UR%5R_`I*?Vfb0cb3#e!|o&C0S*YgxDYo8Az7`L}Cz)pr#ya zda93qq3{;7Nxcx^u<{bS0+Es%Zp+*xZWKX~+_I2$eashq@3L63=cq|Os5Z@py$%{j zkVOISRN8C;L7uzK;vk}ag4Zc6g>Xph6C0&(8>sJD>yqFVmN~ErGe~ig*pd`X8(XkC zj)MX6a(2p%{E`7evN;BFfe607LlJE5gIX$*m^&cY(@HpV1yP!-gnsKNP|GT2SX5h< zm0K|*Tet5eCTEn6#ej0T*EHaw#v&m{!79$J2X_dh34HThDAzfZB>~fldISZ9ToNc6 z*qn7Su?hT5R@Hl)5)0uQIPE}ZuV=hb%U0sVECs>}2c!Ea^dm`J* z+Xs~J>I8QO)eopxavNXpD})At-7VPZZUE9V?N%XRlZiog>ip^hxjEJDVbopB-z1=z kP>R(Zs#>&Y0sIOu0Q&jpzNnr4O8@`>07*qoM6N<$f+~a@$^ZZW literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/login-icons@2x.png b/platform/linto-admin/vue_app/public/img/login-icons@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..12f8e23417e243473c02586cc83a053e0b18b26c GIT binary patch literal 1378 zcmV-o1)chdP)(|QmEuU1wJQ6`WU@cokyIozRc5MeIy!dnPJT8LR-M&S zytkyOCHM?ajTa~x{IBa?!!t$iJSeHJW0h#C${a~cOSS@a=Co6)vO?|Xzq8oL=Bl(v zEsG&E1rgV6m$8tokTe<(?bm29n^?$tm4Kx|^RdX5h-^-*KLa(zWlphmAiaZc)??ai zP9WM6r=^DR-6AAmGq+X@`#w0o!hBs*RTi9?agJ#SvNe2gK?poJDxWe2a%KiG#9Z4# z$%vh*n9IJ8UuGd}HyfKU)P2=9PWu-eb%PbpGNM(@ zla9SPF~9|*antDUh@x84m#GiJQ?!bSV=@1>k}7=JZ%b8wGq1YZFjY;tzJeoRZNFQF z`s<+J!C`F8J9otTd$g-EC}9p#SM^6~N{yoaAc2fVI9m87oI2QWdX9vRTHbRAz+7(@ z56{HcuHd;HlmW*}L;VHE+gkDWhB{EL5gms>My#K6cPL;%&@#26J+;{AyDY4j1gNoh z?Jz*Ow{0KP_`+dzwIT*8EoZrhOu)~68j=`CSBT|mj1q~lM;vutv6WGbp0zFUMOyy09!MEAS?IYI?3S#VS}+Ely) zsy`?Af`-^;e5rWoogHOx&>fMK*pIkU@htVDCR=jMB(79EsNZldZ-b3DjQUZg;`xk) zpAt#MdmopK#!0DoppsLx8cG>AWnZsFsQTvpt%59&uIXJAPw`q-%iWe%h zj42h**k@l{pIDJ;qT{FHp>3TZaH#!Wsdy7d!rI|%z+v3jb1A4<*;CTX1MTYecF^vv z`h6KPOsrf^9(wwZs8aEAkKf5-O~tD{v|o;XkanU|l8Wb`z8@kLua;E2|2P#-Kev*K kClCk(0)gN|;jaJ#0FK~5*c4d6UH||907*qoM6N<$f{qZ48~^|S literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/monitoring@2x.png b/platform/linto-admin/vue_app/public/img/monitoring@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..1aa091b08023ab02e6fdee93a26dc63c1db8d0f4 GIT binary patch literal 692 zcmV;l0!#ggP)OYvMJB7 zYsB5Hxio?ijNoC@S(esYP|OO4y283pu&sJ{f<^37Zgb1Hs*avohDav2%vYD59;m*8 zbm=$8_^PGHj4hM6lT|Og5UgXh%Xr<`yhbKsA>Z{8VwQG=$h}WlTK&jnZD=j4k+=Lf zmFQ-sOScV?zK(}uV9=OQ(+US*V8xTb5PXcO7I5_f;QrJoG6`>*v??Lq#DgzYBSVQ(Y@>Ts1Q; zjbH>L7{LfeFqPIZ)LHeR_IK&9%Z&#B#hG>h0000000000007_%oi$}tRW*r>-orSK zw-&o3GI|Qdu54>}HPjER_Q@P}nPHclW7ibk9wHdQ2p$xjy%5)15WR=NuCQ&Nv=tsf zAYzx5ov%w36qj~sdJi?LmL6m6J(LhBhUorBU0TL|9U(FgHB0;7dnmL2ulG<~FmL&B zx7B;7Co1p5buQlIq4(yi?a$s;S`s3uT$|c89h~3;1xo7J!;{)Y@8J*x-&XITB&;l= z_i&)c!v=|KVjL<8S$TR7acKl27{LfeFoMIQ%l`l{pEQ<^H~S}5TGy_~000000AQa# a0R{lBvt#ox!^EQi0000NUOq literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/mute-unmute@2x.png b/platform/linto-admin/vue_app/public/img/mute-unmute@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..325b98ce3b5e7aa27a8e72f0c3b733b4b76c6c0f GIT binary patch literal 1764 zcmaJ?c~BAv7sn&QyFBo!bl1&HP2ARWHM~&qDAR-v9r=*EaI92i(i7UW za9gV;cAShgVi`^{%?3~hkq=g6;PT?h4xsaJep!8+j^1FOzfyj@5d6QgnX~5_4SwcpW4suS;Pb|C zPq%{vUiRML&yzPlh32D54+suU^T)EOlm!bcTu_up0MpyQYuB0-<9hM7GY~yvFrOWD z%gZ?w0ucb~AOBfn1)+?cUL3fB==6g+BY8 zG}D|p_e@X}ICYLWZ+ekJ9qai9qS@*taXMChoJ|-*dn9mnlq#Dc%^uGad8H z0^a&L>s^_&DmU5MDSnW%e^fvRH2uoRxOY(Zyz|55w-q}^{EKz77*j<=qANhZx#UHV z1w6}-=DG%s54IGl2xFwFRFui@e3Hnm)=qCmy;N)vO%;iuVO7m{n-yH_!Ugm98Ur=M z^-XMvQCF9d3XIb<%wVqh0l#7ZqQJR2?<6l~At$5`dc!n#;mB((W9%7RTHhQgLb}(I z1g+bkX>GUp-W~<^(FPLY-d!A!9VzLaj*ii6dAyC;Kh5i(ztwnOZ0721)VkRHEBLII z`4$C?_cYpc)L4eK(r*lVMjf=n48Lp!v*?9&&4o(o9uSjt_Ft-(sU9N4xAHM8n#@do;MrGn!giD z^d(oO!7}>#*F}BYP2K@c>M_#MAp0Zc2>}Capn3^9T7}bA%SsRFWu#`t^ zb2rhB%P|n=HBDnX0~}IK)!5zur;A~R7!?C46LIRN$8_iuRI%cWkhKfo0Z>=4Rc8K) z|DFP!Do^BNb8C`Mjk=E=1;_5^UwN4CF_c!3hMU_xDU^ACdrYgu?ECjE*bEuJs*aw9 zE|h0Y*bMb&$_}{SoI@SL8+cZ@bT-THsXFz+-@5IpL|$jMK|sT9a?DY zxw0T~4*dNqM2A&&{6p3XOmo;J?2h~CgsT6J^2rI#j;+vZ`M_ZA$jFF2mY(=ka{Vx$ z-1@|9BFKP9_+a#8%}&jz0p)|3h?c<6-hO$A3tLGN*z&5*)knQpC&tk6jibh$Jyh+2 zQ2ggim--xW&ML5AAUi<9{`(h*JR9qfLpSOIuj>|vO7Rjl9m&z* zK%@1!SK&Czr-p~emjfg)zU=L(uJ@sqMM~r4eDW8*=cq*#9D)yi6MrexVAFEme8ZP2 zWDqJ6O*`bY7t*woi&o#kR=B-U`rJ8+%g51Ja*2@pj_pE0e6q~?(O-v(8Hx{CSqm^}R-JkqRAfvWQ6x>S|dM1&>`}O^w*J zDwM8l$`AV&hvfx6`;tydV2tVuB1Y0!6^k2qUp}netbE;1ygPCNsnA__U*|UHZR>zR ni-@29kp$rXgrT85P1P0>;VRw^|yBQC2$l0 z000000001(I%QdIC}vQ?UO>&2LO~6C0S#x01gh8zXxLN40kjrZHc|o_a;g5Pzn5E! zEmtqOw@f4x5>Yt_s3c3cj-y0>YHYfV-m7}_Y(2dA9+%1%iiU? z9w%xR(t$oZ{SvO^nYYL-55)%tGl_JtHCGYK6|#+~$u(=pGm}axNF&{l@3F2ru#|JN zHAyG}j$$)`C$jsbhV88GImZ2&_(^dl5f$W%q6do+SXN$MDAJ$z=K}<^6VPyAAup1xg{#AbFS>?w z=n}3Y;j+#J`{R=0=5YLt&k;!oXvkPd?|)vTk)s7zJDg(8=MX93I!^kI`tFaEpMK^e z?s(XR@|^pch1>E%v8M~#Qp6rLOg$YlxPXQaV|#q`M;`HAM*YF1O7NVB<8vZD1l&=~ z=`JgqCT%$iwVf(je6(=2UQYvFhsY#SVa2|1)?iy!5=*&Oeu{*9m0Y3lXm1i|)kj72 zEnvarT0{ZBd7SF1qpuwMDq-=Z+&?Rnea(Bl;5q`eU<(p%5vX{^*6ga2TA!`Ob_Cvd z>7zzsYnJRsdmVk&yB=fQP-P>pB491x3_Ixn000000Dx)mC%^!>QqcRLrJ57~0000< KMNUMnLSTZp?j^|p literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/ping@2x.png b/platform/linto-admin/vue_app/public/img/ping@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9b908f9f2334694bacf931dcc88338b91509b107 GIT binary patch literal 630 zcmeAS@N?(olHy`uVBq!ia0vp^Hb7j#!3HGPnNH*YQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsTnsm?S)1978H@y_szvbSOZeU09v{0mnzSisp*u8xhO525Eb5 z-2x<5R{XIRONi0AsL153LM-NN& zw0_xqK2dz$`C^Dqze}Y&`?7iNJMLBO6tnG~7ngVLdYJN_vkoQNMVWEO1RlOW<8oc- z?e}}U*^g&b{o31e^~bF!um3KpQ6{-rVm!OA*iGipUweInafQCy@!WG~n0j6~UJuS_ zyT8Rzcek8C*TZsCE9)-{bQ-?w&5K^n@HbX@YR1#g>1V2pUb&rjk9{t*xvg|gvD@i@ zO$M(suB^=pU*sKo8z>k%_f_01b;e2C)&!?-`EccQ=H!Da*@3GkU%0gWbJdE?uQ%%5 zcAQ+c;^D%wXG!P9r_3_kle#D5aivF__5VkT%kHcydo-;;_EYY%FY}f?v#NMn-ST_h zQtt2HIigu10RbZq$i3P%ue)moPrgLwrx`rfVT-IUDu^AH7F%Sbc367ZqKEN&3j=?j zUg0}ycICGXnZd@N-z3gj=Kb3A&7!EI7q+~K^S3_Z=fkfM?eZOo=HQ}>jdA~0$4+iMY8Crt_D`LwFRwf{-FPu!lIhy3D$COOOTQX@ ze3Ll+irl|xx59d-+neg2zIdi z?yVSRBfD@4fktI18i4+g@ei!&(DgKCBf@5|#mMwb#sjgI=$;Scyad*{6L%g5*aSAZ z|53X;1H6MEMEBSy@?Hg=A=AOH#DlHbLgSiMy{#~14R@!<76kf0u9GR&J-b<>S~%Y? zVISpLQ3ZQr1rWM*nAe|UmTj)>Iav0#wzeGjbzGNbu-nGFKLBE-2~b?W1vphErCY1H z?Ub*3y6Z+_u^nVjj|Nxy+;aS=J!X+%xkx-7CGN`C)5dfc6{2({Ee@EzN{;QiO!~T` zb?HPCNdP&LW3hoNzo&(C{W41}1c)PSQ%?}(W6jMNI7+J{?-u}`OCoNY=(>NlNLJ|5 zS&Wr<$Xq!G*brV9me46V=R0|Q5!UT@g=MMM<1_%6^-86M^8L16i#(CAx3r0@_uf9h z)Ut1qBcI^4JjVX8Y)~V`!(a{efv>KwX(- zUI9$b74(y9*uKRBFYG@S3jLw!v2Wxv4?~pb*F9l*{rk{x@2=2!UH>|W#x8T|CM@qt z%%Pyz)&FLnO*QGE7`@Xv=y6b1OILOA;6W2l1?@GM)!(<7|?cb&0 zfrZ;(`P!|7!h)X@gWE@%Ue;j<*zUk)u&SO^Tl?oAw2C~~mds%fXMv}^(xC;y?NHY3 zx(};hH)pVNz+7R~+qx1PSTjrOhEmv%<)IEXfCwE4(SAL|e(N&vSk}R&*>rpVFALAD zuFE=`VZb|`C=?2XLZMJ76bgkxp-?Ck3WfT=$lGTRPp4B97(9D;b#=9yO)@EJM^$*~)_PNutAV?!-rLZt`+$BGu0 z+_t!GA$*5S@u)$IP9%^#dq~o~if%o@f9csnu;SUXhcvf^WMJ@T56O2O$8_b5D$O%+ zWv(|NkCp|tMDtynF<~Q3w2duuheU|xJg0tZykA}{ht@>iP&i&#LdTjdt+?oRq;Ri{ zyJipQ47?+PkzTxD01%~xz$Mcj&?ZX9v~H-F#Jq7chJr3-Dd^axohmsP0(u!-O0dpi zJ_$vQ>k@k9Ti%=*6bFU(>(&A|Hmrz>$-@fJsblEWH$dKqiw1z+_y}9dGM}E%#VgCQ3&;pyv`o@h`tgdxiOcC~M;+pdE1FTopwSiUqnlPOL zLDHa06`hbmRkteEY;p4OSHMOCAXR`Ax+1+%MwZ^}w=I!1R@{ulq$2^9)oL=o-lI?` j6bgkxp-|`gSAYQkn-x^@Y-;`Q00000NkvXXu0mjfg>cHw literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/say@2x.png b/platform/linto-admin/vue_app/public/img/say@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..888f954a0bd28f5d8578cc9a0958f5abb8c8af36 GIT binary patch literal 1280 zcmV+b1^@bqP)v>J#3lMeT23X?a$Be$IcQ4l7ielv;v7XfgoN%TLSrA zCkO*kL2!yhFM%Nb1jMT-2?Nm_UKRvIb;KWl_&yrKKz0N#rN%!6grI9^uR1@K{Cfax z2rbu)A^tOg|Goq#3<3+Sjx=2_=!=zuOWd2rgz+AN5jK$Bp*AXlSWs#L-bJKSVzP4( zWGT4hT_|5aQ21Ee^Q8`xi=eVq0#s{cYe0~82y=Q9@u8MaCw>)QlKTMbW@&y&PICTC=#A&j|kp+w5cH(Q)j zcA)}gBiHb~@_89>m$y748wp`qyZaE@RkXY}$aXnlDc^m^I)c5tEJF`vB-60!Bd-w< zemx^2DdAlNgm*54ZyOGtDL!QP5s_VJYZ^L+>F2Dn~#!egQfByr<@?u zoJzw^5wT@BtsJg|2N4h+v_=@sR~~Q}0pX#tO+8X-SU}F5bKDz;@?SA`ZjgS1rs}wldvVPB;v; zO~f+oSV>u}i}AK5J*e4)w@p?p$xdAEDB2R1HN5tbUZNSRWM5os;kTCNoaxLN^V*?g z0neSknWnjU*2l$vV930L2D*E@6a+amttu$kCtbTCj)m@Rpw&oN2-zbKHx9u{clE`U z{{=#&XzCRaQIW~@smR&{t)u4ie=`MP)|%v`!ur%>($&Y7ubUHOE@uX?m-4YLw>_ID`ZBNay#sBoWM6`gmIb9AhMiDd7 zoQSB7cpe+s5wwFr;5RhW0)6?De`8;XztusEJ&J|w!XV_r#Uzouam*OWZYg{ZiEpDJ zZ6I5=Fi#nrH2SQH(Y4H{i^d1vlWRyHyhLx!(o=f0EY-X>K9Y(L0V+W3Py<=vQ@cd~ z!i5(tV{RP9ny6VJ`0OFI@4Q8j;k~sMOEBLJ5}7R`HlIDDB;47vhfN88FqgVg44Bre z&h(Z3nQecK3>l@r$U>aX{8mL?*RM%iPmXiVsFx*Ie66#G8q+hLJ%r!xla|jOHZ|$7 z%M#GCOzKiOrqnQ>J&b^`)7ir>ms&cXJq%eS$GLNZY^0@~&TkL70AM-U>Z(gTnsjdE znXLCB-Y`gQI0;my)dY(y5LrJLTPeV=0-3R&6{60ZF|QR`SYay&<_mi>O=Hf5MDqG> zm)6|yK8Nnz5Z4speS-9n2hy{Luz@E(dk91Ju`m#{0B}G*8=lF%qkC*AwIPj_!8Kg* qo#h3){s+?-3GfS)h literal 0 HcmV?d00001 diff --git a/platform/linto-admin/vue_app/public/img/svg/add.svg b/platform/linto-admin/vue_app/public/img/svg/add.svg new file mode 100644 index 0000000..56e01f6 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/add.svg @@ -0,0 +1,129 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/android-users.svg b/platform/linto-admin/vue_app/public/img/svg/android-users.svg new file mode 100644 index 0000000..c586d67 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/android-users.svg @@ -0,0 +1,131 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/android.svg b/platform/linto-admin/vue_app/public/img/svg/android.svg new file mode 100644 index 0000000..9689708 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/android.svg @@ -0,0 +1,155 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/app.svg b/platform/linto-admin/vue_app/public/img/svg/app.svg new file mode 100644 index 0000000..aeea864 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/app.svg @@ -0,0 +1,82 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/apply.svg b/platform/linto-admin/vue_app/public/img/svg/apply.svg new file mode 100644 index 0000000..1dc76e0 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/apply.svg @@ -0,0 +1,58 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/arrow.svg b/platform/linto-admin/vue_app/public/img/svg/arrow.svg new file mode 100644 index 0000000..4fb3be9 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/arrow.svg @@ -0,0 +1,114 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/back.svg b/platform/linto-admin/vue_app/public/img/svg/back.svg new file mode 100644 index 0000000..02b687b --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/back.svg @@ -0,0 +1,116 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/barcode.svg b/platform/linto-admin/vue_app/public/img/svg/barcode.svg new file mode 100644 index 0000000..bea7904 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/barcode.svg @@ -0,0 +1,232 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/cancel.svg b/platform/linto-admin/vue_app/public/img/svg/cancel.svg new file mode 100644 index 0000000..cff2ecf --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/cancel.svg @@ -0,0 +1,116 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/chat.svg b/platform/linto-admin/vue_app/public/img/svg/chat.svg new file mode 100644 index 0000000..d2c8300 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/chat.svg @@ -0,0 +1,140 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/circuit.svg b/platform/linto-admin/vue_app/public/img/svg/circuit.svg new file mode 100644 index 0000000..14b5178 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/circuit.svg @@ -0,0 +1,108 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/close.svg b/platform/linto-admin/vue_app/public/img/svg/close.svg new file mode 100644 index 0000000..8f2839d --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/close.svg @@ -0,0 +1,76 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/cpu.svg b/platform/linto-admin/vue_app/public/img/svg/cpu.svg new file mode 100644 index 0000000..045788d --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/cpu.svg @@ -0,0 +1,299 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/delete.svg b/platform/linto-admin/vue_app/public/img/svg/delete.svg new file mode 100644 index 0000000..b3a4167 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/delete.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/edit.svg b/platform/linto-admin/vue_app/public/img/svg/edit.svg new file mode 100644 index 0000000..7815180 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/edit.svg @@ -0,0 +1,130 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/fullscreen.svg b/platform/linto-admin/vue_app/public/img/svg/fullscreen.svg new file mode 100644 index 0000000..630ace9 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/fullscreen.svg @@ -0,0 +1,155 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/goto.svg b/platform/linto-admin/vue_app/public/img/svg/goto.svg new file mode 100644 index 0000000..060a437 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/goto.svg @@ -0,0 +1,122 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/install.svg b/platform/linto-admin/vue_app/public/img/svg/install.svg new file mode 100644 index 0000000..8f57999 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/install.svg @@ -0,0 +1,132 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/leave-fullscreen.svg b/platform/linto-admin/vue_app/public/img/svg/leave-fullscreen.svg new file mode 100644 index 0000000..b5308f8 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/leave-fullscreen.svg @@ -0,0 +1,158 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/levels.svg b/platform/linto-admin/vue_app/public/img/svg/levels.svg new file mode 100644 index 0000000..0469b61 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/levels.svg @@ -0,0 +1,145 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/loading.svg b/platform/linto-admin/vue_app/public/img/svg/loading.svg new file mode 100644 index 0000000..51f4e18 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/loading.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/logout.svg b/platform/linto-admin/vue_app/public/img/svg/logout.svg new file mode 100644 index 0000000..d31484f --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/logout.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/multi-user.svg b/platform/linto-admin/vue_app/public/img/svg/multi-user.svg new file mode 100644 index 0000000..bb28ef8 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/multi-user.svg @@ -0,0 +1,141 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/mute.svg b/platform/linto-admin/vue_app/public/img/svg/mute.svg new file mode 100644 index 0000000..23fefd8 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/mute.svg @@ -0,0 +1,151 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/nlu.svg b/platform/linto-admin/vue_app/public/img/svg/nlu.svg new file mode 100644 index 0000000..41cc67d --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/nlu.svg @@ -0,0 +1,153 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/ping.svg b/platform/linto-admin/vue_app/public/img/svg/ping.svg new file mode 100644 index 0000000..e942c29 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/ping.svg @@ -0,0 +1,115 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/qr.svg b/platform/linto-admin/vue_app/public/img/svg/qr.svg new file mode 100644 index 0000000..05b3e3d --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/qr.svg @@ -0,0 +1,57 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/reset.svg b/platform/linto-admin/vue_app/public/img/svg/reset.svg new file mode 100644 index 0000000..10c32ac --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/reset.svg @@ -0,0 +1,123 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/rocket.svg b/platform/linto-admin/vue_app/public/img/svg/rocket.svg new file mode 100644 index 0000000..df47c55 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/rocket.svg @@ -0,0 +1,82 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/save.svg b/platform/linto-admin/vue_app/public/img/svg/save.svg new file mode 100644 index 0000000..e70ce85 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/save.svg @@ -0,0 +1,138 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/say.svg b/platform/linto-admin/vue_app/public/img/svg/say.svg new file mode 100644 index 0000000..4a51047 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/say.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/settings.svg b/platform/linto-admin/vue_app/public/img/svg/settings.svg new file mode 100644 index 0000000..1625ff0 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/settings.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/single-user.svg b/platform/linto-admin/vue_app/public/img/svg/single-user.svg new file mode 100644 index 0000000..618c52d --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/single-user.svg @@ -0,0 +1,109 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/single-user.svg.svg b/platform/linto-admin/vue_app/public/img/svg/single-user.svg.svg new file mode 100644 index 0000000..2d01a95 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/single-user.svg.svg @@ -0,0 +1,104 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/skills-manager.svg b/platform/linto-admin/vue_app/public/img/svg/skills-manager.svg new file mode 100644 index 0000000..44c6c5a --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/skills-manager.svg @@ -0,0 +1,129 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/terminal.svg b/platform/linto-admin/vue_app/public/img/svg/terminal.svg new file mode 100644 index 0000000..3f38b5f --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/terminal.svg @@ -0,0 +1,160 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/unmute.svg b/platform/linto-admin/vue_app/public/img/svg/unmute.svg new file mode 100644 index 0000000..45c99c8 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/unmute.svg @@ -0,0 +1,119 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/svg/upload.svg b/platform/linto-admin/vue_app/public/img/svg/upload.svg new file mode 100644 index 0000000..aedb72a --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/upload.svg @@ -0,0 +1,131 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/user-list.svg b/platform/linto-admin/vue_app/public/img/svg/user-list.svg new file mode 100644 index 0000000..25e4bbc --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/user-list.svg @@ -0,0 +1,139 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/user-settings.svg b/platform/linto-admin/vue_app/public/img/svg/user-settings.svg new file mode 100644 index 0000000..7de2dba --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/user-settings.svg @@ -0,0 +1,91 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/users.svg b/platform/linto-admin/vue_app/public/img/svg/users.svg new file mode 100644 index 0000000..8ed99b9 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/users.svg @@ -0,0 +1,107 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/webapp.svg b/platform/linto-admin/vue_app/public/img/svg/webapp.svg new file mode 100644 index 0000000..072061c --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/webapp.svg @@ -0,0 +1,112 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/svg/workflow.svg b/platform/linto-admin/vue_app/public/img/svg/workflow.svg new file mode 100644 index 0000000..ec7e2b7 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/svg/workflow.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/platform/linto-admin/vue_app/public/img/warning@2x.png b/platform/linto-admin/vue_app/public/img/warning@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..772ac03f1ac5dd5d865224f71a7f1df528d711d8 GIT binary patch literal 904 zcmV;319$w1P)E$6EsYaFhSD^!k?hoAjt&H24RG3@O!9N3DG-qHiqKJPeMf47#|Pr-7|@zfPjF2 zfPmRxG3CYnXITonXV?-vRKxafQB|kA3mAgUV7F(Qa|pV+n}8wMCG0+Wx;;YBm$B!f zCL;LNQJ?t-4D3F^E){f5v9I?GQ_R)4P0q3!nca0OBezlF)W z)_pQ=!fQd+?7o5Xn3`@K&WfF|rTzh6^!EVA-K9xW+F2ZB& zo6(F(JvD10yN|-~-hc^8t|Z5#WA8@|7>kTl>U9a6g~vwo%%%+6c-_9_B(0B~cxGk5 zF6VU%1M6lTFkYh=>Y0@=*jQ6!R0cQr(qZY=m$V3(B8w5JOi3Ab;dQ@RYV6_xGw;g6 z*bnl|3jw=HL@&7-dtHaCD@P`I_Ac#8{P4!$PD>`cY`q?ZEg}bK0(JPItT6G++#~(W$j~908E{S~VGOJ+QS6?___*Z=XCmJu zwmRI-ew)a1D2wG7UvM!AxV(daOL4&HN-B?o-Ej$a(W7xsnC)Uu0&QN!N&l=G5D*X$ e5HJ9r0t^5tfl?yFmPnHT0000S)YYlq)`THduK2j+Zr?*t@zJ7Rf<5Dn*y%DjoG$eZF zN-&ANpu=XsB^D&Qdg`QXWP#VcpI!{Sr-4e zNiaSAQhDn3R1H6iU`ewdGNowia37n;rrxQTMl_uJQyn8#DCHpqSAu1Z%oFcZN_MzP z>3rb5zLDtd-u1So`|v>S#31b8h`+WhxJg{P1&L(UAGj#mVGP0QK1_{m5?s)ZrIk@S zDy7*4QDh!um80p??2m6P&Bf7um>PetHR={IX)R@`sG0Ahom^&2Xf7tDjHuI!lT!(w z8Vhce*xs9yS{{}k!Q#Zq#Ok^BZOPavU69cFNKWpRX4)K6F!#Cnp7R>ww_D9gmflNq zgf{~sJri@}qYsM{TP9YoG-6Ffc<*h>m3AJc@>BB{2LJ#70000000000000000002C z=(_Gbl})GPk;;ln?p5rnOXHVEu@~Jit~K|`#uONiyZ4YkTdJNq={fB^G@rsHmHON% zZRWG3$z;-o^bgS2RKId1SVZ%8TAxDP<_{J+bC=TRVjBK4vV_R6t0{x-L(qoOwDCN$ z7Gme3T+kuwXFjE+(cHjT-&#mL6P#=BA!kPEtq%`fk<=vmpFVDW&>8^Gv7_%jj2*C2 zdE6hk%nw>Ly>N6E3HM;vw4nO__8zidrKq$)|HV2#q+9XHK8=SM3Abh>^b?sR|JTy>TNMBR0000005HKH0R{j&-Q6-oj#{e#0000 + + + + + + + + + Linto Admin + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/platform/linto-admin/vue_app/public/js/lottie.min.js b/platform/linto-admin/vue_app/public/js/lottie.min.js new file mode 100644 index 0000000..753a6a6 --- /dev/null +++ b/platform/linto-admin/vue_app/public/js/lottie.min.js @@ -0,0 +1 @@ +var a,b;"undefined"!=typeof navigator&&(a=window||{},b=function(window){"use strict";var svgNS="http://www.w3.org/2000/svg",locationHref="",initialDefaultFrame=-999999,subframeEnabled=!0,expressionsPlugin,isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),cachedColors={},bm_rounder=Math.round,bm_rnd,bm_pow=Math.pow,bm_sqrt=Math.sqrt,bm_abs=Math.abs,bm_floor=Math.floor,bm_max=Math.max,bm_min=Math.min,blitter=10,BMMath={};function ProjectInterface(){return{}}!function(){var t,e=["abs","acos","acosh","asin","asinh","atan","atanh","atan2","ceil","cbrt","expm1","clz32","cos","cosh","exp","floor","fround","hypot","imul","log","log1p","log2","log10","max","min","pow","random","round","sign","sin","sinh","sqrt","tan","tanh","trunc","E","LN10","LN2","LOG10E","LOG2E","PI","SQRT1_2","SQRT2"],r=e.length;for(t=0;t>>=1;return(t+r)/e};return n.int32=function(){return 0|a.g(4)},n.quick=function(){return a.g(4)/4294967296},n.double=n,E(x(a.S),o),(e.pass||r||function(t,e,r,i){return i&&(i.S&&b(i,a),t.state=function(){return b(a,{})}),r?(h[c]=t,e):t})(n,s,"global"in e?e.global:this==h,e.state)},E(h.random(),o)}([],BMMath);var BezierFactory=function(){var t={getBezierEasing:function(t,e,r,i,s){var a=s||("bez_"+t+"_"+e+"_"+r+"_"+i).replace(/\./g,"p");if(o[a])return o[a];var n=new h([t,e,r,i]);return o[a]=n}},o={};var l=11,p=1/(l-1),e="function"==typeof Float32Array;function i(t,e){return 1-3*e+3*t}function s(t,e){return 3*e-6*t}function a(t){return 3*t}function m(t,e,r){return((i(e,r)*t+s(e,r))*t+a(e))*t}function f(t,e,r){return 3*i(e,r)*t*t+2*s(e,r)*t+a(e)}function h(t){this._p=t,this._mSampleValues=e?new Float32Array(l):new Array(l),this._precomputed=!1,this.get=this.get.bind(this)}return h.prototype={get:function(t){var e=this._p[0],r=this._p[1],i=this._p[2],s=this._p[3];return this._precomputed||this._precompute(),e===r&&i===s?t:0===t?0:1===t?1:m(this._getTForX(t),r,s)},_precompute:function(){var t=this._p[0],e=this._p[1],r=this._p[2],i=this._p[3];this._precomputed=!0,t===e&&r===i||this._calcSampleValues()},_calcSampleValues:function(){for(var t=this._p[0],e=this._p[2],r=0;rn?-1:1,l=!0;l;)if(i[a]<=n&&i[a+1]>n?(o=(n-i[a])/(i[a+1]-i[a]),l=!1):a+=h,a<0||s-1<=a){if(a===s-1)return r[a];l=!1}return r[a]+(r[a+1]-r[a])*o}var D=createTypedArray("float32",8);return{getSegmentsLength:function(t){var e,r=segments_length_pool.newElement(),i=t.c,s=t.v,a=t.o,n=t.i,o=t._length,h=r.lengths,l=0;for(e=0;er[0]||!(r[0]>t[0])&&(t[1]>r[1]||!(r[1]>t[1])&&(t[2]>r[2]||!(r[2]>t[2])&&void 0))}var h,r=function(){var i=[4,4,14];function s(t){var e,r,i,s=t.length;for(e=0;e=a.t-i){s.h&&(s=a),f=0;break}if(a.t-i>t){f=c;break}c=r&&r<=t||this._caching.lastFrame=t&&(this._caching._lastKeyframeIndex=-1,this._caching.lastIndex=0);var i=this.interpolateValue(t,this._caching);this.pv=i}return this._caching.lastFrame=t,this.pv}function d(t){var e;if("unidimensional"===this.propType)e=t*this.mult,1e-5=this.p.keyframes[this.p.keyframes.length-1].t?(e=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/i,0),this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/i,0)):(e=this.p.pv,this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/i,this.p.offsetTime));else if(this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime){e=[],r=[];var s=this.px,a=this.py;s._caching.lastFrame+s.offsetTime<=s.keyframes[0].t?(e[0]=s.getValueAtTime((s.keyframes[0].t+.01)/i,0),e[1]=a.getValueAtTime((a.keyframes[0].t+.01)/i,0),r[0]=s.getValueAtTime(s.keyframes[0].t/i,0),r[1]=a.getValueAtTime(a.keyframes[0].t/i,0)):s._caching.lastFrame+s.offsetTime>=s.keyframes[s.keyframes.length-1].t?(e[0]=s.getValueAtTime(s.keyframes[s.keyframes.length-1].t/i,0),e[1]=a.getValueAtTime(a.keyframes[a.keyframes.length-1].t/i,0),r[0]=s.getValueAtTime((s.keyframes[s.keyframes.length-1].t-.01)/i,0),r[1]=a.getValueAtTime((a.keyframes[a.keyframes.length-1].t-.01)/i,0)):(e=[s.pv,a.pv],r[0]=s.getValueAtTime((s._caching.lastFrame+s.offsetTime-.01)/i,s.offsetTime),r[1]=a.getValueAtTime((a._caching.lastFrame+a.offsetTime-.01)/i,a.offsetTime))}this.v.rotate(-Math.atan2(e[1]-r[1],e[0]-r[0]))}this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])}this.frameId=this.elem.globalData.frameId}},precalculateMatrix:function(){if(!this.a.k&&(this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1,!this.s.effectsSequence.length)){if(this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2,this.sk){if(this.sk.effectsSequence.length||this.sa.effectsSequence.length)return;this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3}if(this.r){if(this.r.effectsSequence.length)return;this.pre.rotate(-this.r.v),this.appliedTransformations=4}else this.rz.effectsSequence.length||this.ry.effectsSequence.length||this.rx.effectsSequence.length||this.or.effectsSequence.length||(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}},autoOrient:function(){}},extendPrototype([DynamicPropertyContainer],i),i.prototype.addDynamicProperty=function(t){this._addDynamicProperty(t),this.elem.addDynamicProperty(t),this._isDirty=!0},i.prototype._addDynamicProperty=DynamicPropertyContainer.prototype.addDynamicProperty,{getTransformProperty:function(t,e,r){return new i(t,e,r)}}}();function ShapePath(){this.c=!1,this._length=0,this._maxLength=8,this.v=createSizedArray(this._maxLength),this.o=createSizedArray(this._maxLength),this.i=createSizedArray(this._maxLength)}ShapePath.prototype.setPathData=function(t,e){this.c=t,this.setLength(e);for(var r=0;r=this._maxLength&&this.doubleArrayLength(),r){case"v":a=this.v;break;case"i":a=this.i;break;case"o":a=this.o}(!a[i]||a[i]&&!s)&&(a[i]=point_pool.newElement()),a[i][0]=t,a[i][1]=e},ShapePath.prototype.setTripleAt=function(t,e,r,i,s,a,n,o){this.setXYAt(t,e,"v",n,o),this.setXYAt(r,i,"o",n,o),this.setXYAt(s,a,"i",n,o)},ShapePath.prototype.reverse=function(){var t=new ShapePath;t.setPathData(this.c,this._length);var e=this.v,r=this.o,i=this.i,s=0;this.c&&(t.setTripleAt(e[0][0],e[0][1],i[0][0],i[0][1],r[0][0],r[0][1],0,!1),s=1);var a,n=this._length-1,o=this._length;for(a=s;a=c[c.length-1].t-this.offsetTime)i=c[c.length-1].s?c[c.length-1].s[0]:c[c.length-2].e[0],a=!0;else{for(var d,u,y=f,g=c.length-1,v=!0;v&&(d=c[y],!((u=c[y+1]).t-this.offsetTime>t));)y=u.t-this.offsetTime)p=1;else if(ti+r);else p=o.s*s<=i?0:(o.s*s-i)/r,m=o.e*s>=i+r?1:(o.e*s-i)/r,h.push([p,m])}return h.length||h.push([0,0]),h},TrimModifier.prototype.releasePathsData=function(t){var e,r=t.length;for(e=0;ee.e){r.c=!1;break}e.s<=d&&e.e>=d+n.addedLength?(this.addSegment(f[i].v[s-1],f[i].o[s-1],f[i].i[s],f[i].v[s],r,o,y),y=!1):(l=bez.getNewSegment(f[i].v[s-1],f[i].v[s],f[i].o[s-1],f[i].i[s],(e.s-d)/n.addedLength,(e.e-d)/n.addedLength,h[s-1]),this.addSegmentFromArray(l,r,o,y),y=!1,r.c=!1),d+=n.addedLength,o+=1}if(f[i].c&&h.length){if(n=h[s-1],d<=e.e){var g=h[s-1].addedLength;e.s<=d&&e.e>=d+g?(this.addSegment(f[i].v[s-1],f[i].o[s-1],f[i].i[0],f[i].v[0],r,o,y),y=!1):(l=bez.getNewSegment(f[i].v[s-1],f[i].v[0],f[i].o[s-1],f[i].i[0],(e.s-d)/g,(e.e-d)/g,h[s-1]),this.addSegmentFromArray(l,r,o,y),y=!1,r.c=!1)}else r.c=!1;d+=n.addedLength,o+=1}if(r._length&&(r.setXYAt(r.v[p][0],r.v[p][1],"i",p),r.setXYAt(r.v[r._length-1][0],r.v[r._length-1][1],"o",r._length-1)),d>e.e)break;i=d.length&&(m=0,d=u[f+=1]?u[f].points:E.v.c?u[f=m=0].points:(l-=h.partialLength,null)),d&&(c=h,y=(h=d[m]).partialLength));L=T[s].an/2-T[s].add,_.translate(-L,0,0)}else L=T[s].an/2-T[s].add,_.translate(-L,0,0),_.translate(-x[0]*T[s].an/200,-x[1]*V/100,0);for(T[s].l/2,w=0;we));)r+=1;return this.keysIndex!==r&&(this.keysIndex=r),this.data.d.k[this.keysIndex].s},TextProperty.prototype.buildFinalText=function(t){for(var e,r=FontManager.getCombinedCharacterCodes(),i=[],s=0,a=t.length;sthis.minimumFontSize&&D=m(i)&&(r=t-i<0?1-(i-t):l(0,p(s-t,1))),e(r));return r*this.a.v},getValue:function(t){this.iterateDynamicProperties(),this._mdf=t||this._mdf,this._currentTextLength=this.elem.textProperty.currentData.l.length||0,t&&2===this.data.r&&(this.e.v=this._currentTextLength);var e=2===this.data.r?1:100/this.data.totalChars,r=this.o.v/e,i=this.s.v/e+r,s=this.e.v/e+r;if(st-this.layers[e].st&&this.buildItem(e),this.completeLayers=!!this.elements[e]&&this.completeLayers;this.checkPendingElements()},BaseRenderer.prototype.createItem=function(t){switch(t.ty){case 2:return this.createImage(t);case 0:return this.createComp(t);case 1:return this.createSolid(t);case 3:return this.createNull(t);case 4:return this.createShape(t);case 5:return this.createText(t);case 13:return this.createCamera(t)}return this.createNull(t)},BaseRenderer.prototype.createCamera=function(){throw new Error("You're using a 3d camera. Try the html renderer.")},BaseRenderer.prototype.buildAllItems=function(){var t,e=this.layers.length;for(t=0;t=t)return this.threeDElements[e].perspectiveElem;e+=1}},HybridRenderer.prototype.createThreeDContainer=function(t,e){var r=createTag("div");styleDiv(r);var i=createTag("div");styleDiv(i),"3d"===e&&(r.style.width=this.globalData.compSize.w+"px",r.style.height=this.globalData.compSize.h+"px",r.style.transformOrigin=r.style.mozTransformOrigin=r.style.webkitTransformOrigin="50% 50%",i.style.transform=i.style.webkitTransform="matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)"),r.appendChild(i);var s={container:i,perspectiveElem:r,startPos:t,endPos:t,type:e};return this.threeDElements.push(s),s},HybridRenderer.prototype.build3dContainers=function(){var t,e,r=this.layers.length,i="";for(t=0;tt?!0!==this.isInRange&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):!1!==this.isInRange&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var t,e=this.renderableComponents.length;for(t=0;t=t.x+t.width&&this.currentBBox.height+this.currentBBox.y>=t.y+t.height},HShapeElement.prototype.renderInnerContent=function(){if(this._renderShapeFrame(),!this.hidden&&(this._isFirstFrame||this._mdf)){var t=this.tempBoundingBox,e=999999;if(t.x=e,t.xMax=-e,t.y=e,t.yMax=-e,this.calculateBoundingBox(this.itemsData,t),t.width=t.xMaxthis.animationData.op&&(this.animationData.op=t.op,this.totalFrames=Math.floor(t.op-this.animationData.ip));var e,r,i=this.animationData.layers,s=i.length,a=t.layers,n=a.length;for(r=0;rthis.timeCompleted&&(this.currentFrame=this.timeCompleted),this.trigger("enterFrame"),this.renderFrame()},AnimationItem.prototype.renderFrame=function(){if(!1!==this.isLoaded)try{this.renderer.renderFrame(this.currentFrame+this.firstFrame)}catch(t){this.triggerRenderFrameError(t)}},AnimationItem.prototype.play=function(t){t&&this.name!=t||!0===this.isPaused&&(this.isPaused=!1,this._idle&&(this._idle=!1,this.trigger("_active")))},AnimationItem.prototype.pause=function(t){t&&this.name!=t||!1===this.isPaused&&(this.isPaused=!0,this._idle=!0,this.trigger("_idle"))},AnimationItem.prototype.togglePause=function(t){t&&this.name!=t||(!0===this.isPaused?this.play():this.pause())},AnimationItem.prototype.stop=function(t){t&&this.name!=t||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},AnimationItem.prototype.goToAndStop=function(t,e,r){r&&this.name!=r||(e?this.setCurrentRawFrameValue(t):this.setCurrentRawFrameValue(t*this.frameModifier),this.pause())},AnimationItem.prototype.goToAndPlay=function(t,e,r){this.goToAndStop(t,e,r),this.play()},AnimationItem.prototype.advanceTime=function(t){if(!0!==this.isPaused&&!1!==this.isLoaded){var e=this.currentRawFrame+t*this.frameModifier,r=!1;e>=this.totalFrames-1&&0=this.totalFrames?(this.playCount+=1,this.checkSegments(e%this.totalFrames)||(this.setCurrentRawFrameValue(e%this.totalFrames),this._completedLoop=!0,this.trigger("loopComplete"))):this.setCurrentRawFrameValue(e):this.checkSegments(e>this.totalFrames?e%this.totalFrames:0)||(r=!0,e=this.totalFrames-1):e<0?this.checkSegments(e%this.totalFrames)||(!this.loop||this.playCount--<=0&&!0!==this.loop?(r=!0,e=0):(this.setCurrentRawFrameValue(this.totalFrames+e%this.totalFrames),this._completedLoop?this.trigger("loopComplete"):this._completedLoop=!0)):this.setCurrentRawFrameValue(e),r&&(this.setCurrentRawFrameValue(e),this.pause(),this.trigger("complete"))}},AnimationItem.prototype.adjustSegment=function(t,e){this.playCount=0,t[1]t[0]&&(this.frameModifier<0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(1)),this.timeCompleted=this.totalFrames=t[1]-t[0],this.firstFrame=t[0],this.setCurrentRawFrameValue(.001+e)),this.trigger("segmentStart")},AnimationItem.prototype.setSegment=function(t,e){var r=-1;this.isPaused&&(this.currentRawFrame+this.firstFramee&&(r=e-t)),this.firstFrame=t,this.timeCompleted=this.totalFrames=e-t,-1!==r&&this.goToAndStop(r,!0)},AnimationItem.prototype.playSegments=function(t,e){if(e&&(this.segments.length=0),"object"==typeof t[0]){var r,i=t.length;for(r=0;rdata.k[e].t&&tdata.k[e+1].t-t?(r=e+2,data.k[e+1].t):(r=e+1,data.k[e].t);break}}-1===r&&(r=e+1,i=data.k[e].t)}else i=r=0;var a={};return a.index=r,a.time=i/elem.comp.globalData.frameRate,a}function key(t){var e,r,i;if(!data.k.length||"number"==typeof data.k[0])throw new Error("The property has no keyframe at index "+t);t-=1,e={time:data.k[t].t/elem.comp.globalData.frameRate,value:[]};var s=data.k[t].hasOwnProperty("s")?data.k[t].s:data.k[t-1].e;for(i=s.length,r=0;rl.length-1)&&(e=l.length-1),i=p-(s=l[l.length-1-e].t)),"pingpong"===t){if(Math.floor((h-s)/i)%2!=0)return this.getValueAtTime((i-(h-s)%i+s)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var m=this.getValueAtTime(s/this.comp.globalData.frameRate,0),f=this.getValueAtTime(p/this.comp.globalData.frameRate,0),c=this.getValueAtTime(((h-s)%i+s)/this.comp.globalData.frameRate,0),d=Math.floor((h-s)/i);if(this.pv.length){for(n=(o=new Array(m.length)).length,a=0;al.length-1)&&(e=l.length-1),i=(s=l[e].t)-p),"pingpong"===t){if(Math.floor((p-h)/i)%2==0)return this.getValueAtTime(((p-h)%i+p)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var m=this.getValueAtTime(p/this.comp.globalData.frameRate,0),f=this.getValueAtTime(s/this.comp.globalData.frameRate,0),c=this.getValueAtTime((i-(p-h)%i+p)/this.comp.globalData.frameRate,0),d=Math.floor((p-h)/i)+1;if(this.pv.length){for(n=(o=new Array(m.length)).length,a=0;an){var p=o,m=r.c&&o===h-1?0:o+1,f=(n-l)/a[o].addedLength;i=bez.getPointInSegment(r.v[p],r.v[m],r.o[p],r.i[m],f,a[o]);break}l+=a[o].addedLength,o+=1}return i||(i=r.c?[r.v[0][0],r.v[0][1]]:[r.v[r._length-1][0],r.v[r._length-1][1]]),i},vectorOnPath:function(t,e,r){t=1==t?this.v.c?0:.999:t;var i=this.pointOnPath(t,e),s=this.pointOnPath(t+.001,e),a=s[0]-i[0],n=s[1]-i[1],o=Math.sqrt(Math.pow(a,2)+Math.pow(n,2));return 0===o?[0,0]:"tangent"===r?[a/o,n/o]:[-n/o,a/o]},tangentOnPath:function(t,e){return this.vectorOnPath(t,e,"tangent")},normalOnPath:function(t,e){return this.vectorOnPath(t,e,"normal")},setGroupProperty:expressionHelpers.setGroupProperty,getValueAtTime:expressionHelpers.getStaticValueAtTime},extendPrototype([r],t),extendPrototype([r],e),e.prototype.getValueAtTime=function(t){return this._cachingAtTime||(this._cachingAtTime={shapeValue:shape_pool.clone(this.pv),lastIndex:0,lastTime:initialDefaultFrame}),t*=this.elem.globalData.frameRate,(t-=this.offsetTime)!==this._cachingAtTime.lastTime&&(this._cachingAtTime.lastIndex=this._cachingAtTime.lastTimediv { + align-items: center; + justify-content: center; + margin: 10px; + } + h3 { + padding: 0 20px; + font-size: 18px; + font-weight: 600; + color: $blueMid; + } + #close-notif-top { + position: absolute; + top: 10px; + left: 100%; + margin-left: -40px; + } +} + +.model-generating-table { + width: auto; + tr { + td { + padding: 5px; + &.model-generating__label { + font-size: 16px; + font-weight: 600; + } + } + } +} + +.model-generating__prct-wrapper { + display: inline-block; + height: 20px; + width: 240px; + border: 1px solid #ccc; + background-color: #fcfcfc; + position: relative; + @include borderRadius(5px); + .model-generating__prct-value { + position: absolute; + top: 0; + left: 0; + height: 20px; + background-color: $greenChart; + z-index: 2; + } + .model-generating__prct-label { + display: inline-block; + width: 100%; + height: 20px; + line-height: 20px; + text-align: center; + font-size: 14px; + color: $textColor; + position: absolute; + top: 0; + left: 0; + z-index: 3; + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/app-notify.scss b/platform/linto-admin/vue_app/public/sass/components/app-notify.scss new file mode 100644 index 0000000..c0ddc67 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/app-notify.scss @@ -0,0 +1,131 @@ +.notif-wrapper { + z-index: 999; + @include boxShadow(0, -2px, 2px, 0, rgba(0, 0, 0, 0.2)); + padding: 0; + overflow: hidden; + border: none; + position: relative; + @include transition(height 0.3s ease); + background-color: #fff; + &.closed { + height: 0; + } + &.success, + &.error { + height: 40px; + padding: 10px 0; + } + &.success { + &::after { + content: ''; + display: inline-block; + position: absolute; + height: 3px; + width: 100%; + top: 0; + left: 0; + background-color: $valid; + @include traceBorderTop(); + } + } + &.error { + &::after { + content: ''; + display: inline-block; + position: absolute; + height: 3px; + width: 100%; + top: 0; + left: 0; + background-color: $error; + @include traceBorderTop(); + } + } +} + +.notif-container { + align-items: center; + justify-content: center; + position: relative; + &>span { + display: inline-block; + } + .icon { + width: 40px; + height: 40px; + margin: 0 10px; + } + .notif-msg { + font-size: 16px; + &.success { + color: $valid; + } + &.error { + color: $error; + } + } +} + + +/*** TOP NOTIF ***/ + +#top-notif { + min-height: 40px; + background: #f2f2f2; + border-bottom: 1px solid #ccc; + z-index: 20; + .icon.state__icon { + display: inline-block; + width: 20px; + height: 20px; + background-image: url('../img/deploy-status-icons@2x.png'); + background-size: 20px 60px; + background-repeat: no-repeat; + margin-right: 10px; + &.state__icon--loading { + background-position: 0 -40px; + @include rotate(); + } + &.state__icon--success { + background-position: 0 0; + } + } + .state-item { + padding: 10px 40px; + justify-content: center; + align-items: center; + .state-text { + display: inline-block; + font-size: 14px; + font-weight: 500; + color: $blueDark; + &.success { + color: $greenChart; + } + strong { + font-weight: 600; + color: $blueLinto; + } + } + } +} + +.state-progress-container { + height: 15px; + width: 50%; + max-width: 220px; + border: 1px solid $blueMid; + position: relative; + margin-left: 20px; + @include borderRadius(10px); + overflow: hidden; + .state-progress { + @include transition(all 0.3 ease); + position: absolute; + top: 0; + height: 15px; + background: $greenChart; + width: 0; + @include borderRadius(10px); + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/app-vertical-nav.scss b/platform/linto-admin/vue_app/public/sass/components/app-vertical-nav.scss new file mode 100644 index 0000000..a6b1409 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/app-vertical-nav.scss @@ -0,0 +1,153 @@ +/*** VERTICAL NAVIGATION ***/ + +#vertical-nav { + position: relative; + min-width: 180px; + max-width: 260px; + width: auto; + padding: 20px 0; + background: $blueDark; + overflow: hidden; + z-index: 5; + &.fullscreen-child { + z-index: 1; + } + .nav-divider { + width: 100%; + height: 1px; + background-color: #747e92; + margin: 10px 0; + } +} + +.vertical-nav-item { + position: relative; + padding: 20px; + height: auto; + .vertical-nav-item__link, + .vertical-nav-item__link--parent { + position: relative; + display: inline-block; + font-size: 14px; + font-weight: 400; + color: #fff; + text-decoration: none; + .nav-link__icon { + display: inline-block; + width: 30px; + height: 30px; + background-color: #fff; + margin: 0 5px; + vertical-align: top; + &.nav-link__icon--static { + @include maskImage('../img/svg/cpu.svg'); + } + &.nav-link__icon--app { + @include maskImage('../img/svg/app.svg'); + } + &.nav-link__icon--android { + @include maskImage('../img/svg/android.svg'); + } + &.nav-link__icon--android-users { + @include maskImage('../img/svg/android-users.svg'); + } + &.nav-link__icon--webapp { + @include maskImage('../img/svg/webapp.svg'); + } + &.nav-link__icon--workflow { + @include maskImage('../img/svg/workflow.svg'); + } + &.nav-link__icon--nlu { + @include maskImage('../img/svg/nlu.svg'); + } + &.nav-link__icon--single-user { + @include maskImage('../img/svg/single-user.svg'); + } + &.nav-link__icon--multi-user { + @include maskImage('../img/svg/multi-user.svg'); + } + &.nav-link__icon--terminal { + @include maskImage('../img/svg/terminal.svg'); + } + &.nav-link__icon--users { + @include maskImage('../img/svg/users.svg'); + } + &.nav-link__icon--skills-manager { + @include maskImage('../img/svg/skills-manager.svg'); + } + } + .nav-link__label { + display: inline-block; + height: 30px; + vertical-align: top; + color: #fff; + line-height: 30px; + } + } + .vertical-nav-item__link--parent { + &::after { + content: ''; + display: inline-block; + width: 20px; + height: 20px; + position: absolute; + top: 2px; + left: 100%; + margin-left: -30px; + background-image: url('../img/nav-arrows@2x.png'); + background-size: 40px 40px; + background-position: 0 0; + } + &:hover::after { + background-position: 0 -20px; + } + &.opened { + &::after { + background-position: -20px 0; + } + &:hover::after { + background-position: -20px -20px; + } + } + } + .vertical-nav-item--children { + overflow: hidden; + @include transition(all 0.3s ease-in); + border-left: 1px solid #ececec; + margin: 5px 0; + &.hidden { + display: flex; + height: 0px; + margin: 0; + padding: 0; + } + .vertical-nav-item__link--children { + display: inline-block; + font-size: 14px; + padding: 8px 0 8px 15px; + font-weight: 400; + text-decoration: none; + color: #fff; + &:hover { + color: $blueLinto; + } + &.active { + &, + &:hover { + background-color: $blueMid; + font-weight: 600; + } + } + } + } + &.active { + background-color: $blueMid; + .vertical-nav-item__link, + .vertical-nav-item__link--parent { + font-weight: 600; + &:hover { + color: #fff; + } + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/app.scss b/platform/linto-admin/vue_app/public/sass/components/app.scss new file mode 100644 index 0000000..fe07061 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/app.scss @@ -0,0 +1,40 @@ +#app { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0; + overflow: hidden; + z-index: 1; +} + +#page-view { + padding: 0; + margin: 0; + z-index: 1; + overflow: hidden; + &.fullscreen-child { + z-index: 10; + } +} + +#view { + position: relative; + overflow: auto; + z-index: 4; + background-color: #f0f6f9; + padding: 40px; + &.fullscreen-child { + position: inherit; + } +} + +#view-render { + height: 100%; + padding: 0; + &>.flex { + padding: 0 0 40px 0; + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/buttons.scss b/platform/linto-admin/vue_app/public/sass/components/buttons.scss new file mode 100644 index 0000000..5dbc83b --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/buttons.scss @@ -0,0 +1,367 @@ +.button { + display: inline-block; + border: 1px solid #fff; + padding: 0; + margin: 0; + height: 32px; + background-color: #fff; + @include transition(all 0.3s ease-in); + @include buttonShadow(); + @include borderRadius(3px); + outline: none; + cursor: pointer; + position: relative; + overflow: hidden; + /* label & icon */ + .button__label { + display: inline-block; + font-size: 15px; + font-weight: 400; + line-height: 28px; + color: $blueLinto; + vertical-align: top; + color: $textColor; + padding: 0 10px; + border: 1px solid transparent; + @include borderRadius(3px); + } + .button__icon { + display: inline-block; + width: 30px; + height: 30px; + vertical-align: top; + margin: 0; + padding: 0; + @include borderRadius(3px); + &.button__icon--monitoring { + @include maskImage('../img/svg/levels.svg'); + } + &.button__icon--close { + @include maskImage('../img/svg/close.svg'); + } + &.button__icon--ping { + @include maskImage('../img/svg/ping.svg'); + } + &.button__icon--talk { + @include maskImage('../img/svg/chat.svg'); + } + &.button__icon--mute { + @include maskImage('../img/svg/mute.svg'); + } + &.button__icon--unmute { + @include maskImage('../img/svg/unmute.svg'); + } + &.button__icon--workflow { + @include maskImage('../img/svg/workflow.svg'); + } + &.button__icon--fullscreen { + @include maskImage('../img/svg/fullscreen.svg'); + } + &.button__icon--leave-fullscreen { + @include maskImage('../img/svg/leave-fullscreen.svg'); + } + &.button__icon--save { + @include maskImage('../img/svg/save.svg'); + } + &.button__icon--load { + @include maskImage('../img/svg/upload.svg'); + } + &.button__icon--publish, + &.button__icon--deploy { + @include maskImage('../img/svg/rocket.svg'); + } + &.button__icon--logout { + @include maskImage('../img/svg/logout.svg'); + } + &.button__icon--settings { + @include maskImage('../img/svg/settings.svg'); + } + &.button__icon--barcode { + @include maskImage('../img/svg/barcode.svg'); + } + &.button__icon--delete, + &.button__icon--trash { + @include maskImage('../img/svg/delete.svg'); + } + &.button__icon--cancel { + @include maskImage('../img/svg/cancel.svg'); + } + &.button__icon--apply { + @include maskImage('../img/svg/apply.svg'); + } + &.button__icon--add { + @include maskImage('../img/svg/add.svg'); + } + &.button__icon--back { + @include maskImage('../img/svg/back.svg'); + } + &.button__icon--user-settings { + @include maskImage('../img/svg/user-settings.svg'); + } + &.button__icon--android { + @include maskImage('../img/svg/android.svg'); + } + &.button__icon--webapp { + @include maskImage('../img/svg/webapp.svg'); + } + &.button__icon--reset { + @include maskImage('../img/svg/reset.svg'); + } + &.button__icon--edit { + @include maskImage('../img/svg/edit.svg'); + } + &.button__icon--say { + @include maskImage('../img/svg/say.svg'); + } + &.button__icon--goto { + @include maskImage('../img/svg/goto.svg'); + } + &.button__icon--mutli-user { + @include maskImage('../img/svg/multi-user.svg'); + } + &.button__icon--install { + @include maskImage('../img/svg/install.svg'); + } + &.button__icon--loading { + @include maskImage('../img/svg/loading.svg'); + @include rotate(); + } + &.button__icon--arrow { + @include maskImage('../img/svg/arrow.svg'); + @include transition(all 0.3s ease); + &.opened { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + &.closed { + -ms-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -webkit-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + transform: rotate(-90deg); + } + } + } + /* Type of button */ + &.button-icon-txt { + min-width: 120px; + .button__icon { + margin-left: 5px; + } + .button__label { + padding: 0 10px 0 5px; + } + } + &.button--full { + width: 100%; + padding: 0; + } + /* Button colors */ + &.button--valid, + &.button--green { + border-color: $valid; + .button__icon { + background-color: $valid; + } + .button__label { + color: $valid; + } + &:hover { + background-color: $valid; + .button__label { + color: #fff; + } + } + } + &.button--important, + &.button--red { + border-color: $error; + .button__icon { + background-color: $error; + } + .button__label { + color: $error; + } + &:hover { + background-color: $error; + .button__label { + color: #fff; + } + } + } + &.button--cancel, + &.button--grey { + border-color: #666; + .button__icon { + background-color: #666; + } + .button__label { + color: #666; + } + &:hover { + background-color: #666; + .button__label { + color: #fff; + } + } + } + &.button--blue { + border-color: $blueLinto; + .button__icon { + background-color: $blueLinto; + } + .button__label { + color: $blueLinto; + } + &:hover { + background-color: $blueLinto; + .button__label { + color: #fff; + } + } + } + &.button--bluemid { + border-color: $blueMid; + .button__icon { + background-color: $blueMid; + } + .button__label { + color: $blueMid; + } + &:hover { + background-color: $blueMid; + .button__label { + color: #fff; + } + } + } + &.button--bluedark { + border-color: $blueDark; + .button__icon { + background-color: $blueDark; + } + .button__label { + color: $blueDark; + } + &:hover { + background-color: $blueDark; + .button__label { + color: #fff; + } + } + } + &.button--orange { + border-color: $warning; + .button__icon { + background-color: $warning; + } + .button__label { + color: $warning; + } + &:hover { + background-color: $warning; + .button__label { + color: #fff; + } + } + } + &:hover { + /* Button icons HOVER */ + .button__icon { + background-color: #fff; + } + &.button--with-desc { + overflow: visible; + &::after { + content: attr(data-desc); + position: absolute; + font-size: 14px; + max-width: 180px; + min-width: 80px; + height: auto; + top: 2px; + left: 110%; + padding: 5px; + color: #ffffff; + background-color: inherit; + font-style: italic; + white-space: nowrap; + @include borderRadius(3px); + z-index: 10; + white-space: break-spaces; + text-align: center; + } + &.bottom { + &::after { + top: 35px; + left: 0; + } + } + } + } +} + +a.button { + height: 30px; +} + +.button--toggle__container { + padding-bottom: 20px; + border-bottom: 1px solid $blueLight; +} + +.button--toggle__label { + display: inline-block; + font-size: 18px; + font-weight: 600; + color: $blueDark; + line-height: 25px; + padding: 0 10px 0 0; +} + +.button--toggle { + display: inline-block; + width: 50px; + height: 24px; + border: 2px solid $blueMid; + background-color: #fff; + @include borderRadius(15px); + @include buttonShadow(); + @include transition(all 0.3s ease); + position: relative; + outline: none !important; + .button--toggle__disc { + display: inline-block; + @include transition(all 0.3s ease); + width: 18px; + height: 18px; + @include borderRadius(15px); + border: 1px solid #fff; + position: absolute; + top: 0; + } + &.enabled { + border-color: $valid; + .button--toggle__disc { + background-color: $valid; + left: 100%; + margin-left: -20px; + } + } + &.disabled { + border-color: $error; + .button--toggle__disc { + background-color: $error; + left: 0; + margin-left: 0; + } + } + &:hover { + cursor: pointer; + background-color: #f2f2f2; + @include buttonShadowHover(); + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/clients-overview.scss b/platform/linto-admin/vue_app/public/sass/components/clients-overview.scss new file mode 100644 index 0000000..0e076b3 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/clients-overview.scss @@ -0,0 +1,73 @@ +.icon.icon--status { + position: relative; + cursor: pointer; + &:after { + display: none; + } + &.icon--status__with-desc { + &:hover { + &:after { + content: attr(data-label); + display: inline-block; + width: 200px; + padding: 5px; + @include borderRadius(3px); + background-color: #ccc; + position: absolute; + top: -8px; + left: 20px; + color: #fff; + font-size: 12px; + font-weight: 600; + z-index: 20; + } + &.offline { + &:after { + background-color: $error; + } + } + &.online { + &:after { + background-color: $valid; + } + } + } + } +} + +.icon--status__label { + display: inline-block; + line-height: 20px; + padding-left: 5px; + &.label--green { + color: $valid; + } + &.label--red { + color: $error; + } +} + +.client-status__link { + display: inline-block; + text-decoration: none; + color: $blueLinto; + padding-left: 10px; + font-style: italic; + &:hover { + text-decoration: underline; + color: $blueMid; + } +} + +.auth-status { + display: inline-block; + width: 10px; + height: 10px; + @include borderRadius(5px); + &.enabled { + background-color: $valid; + } + &.disabled { + background-color: $error; + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/forms.scss b/platform/linto-admin/vue_app/public/sass/components/forms.scss new file mode 100644 index 0000000..f7eb174 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/forms.scss @@ -0,0 +1,401 @@ +/* Input */ + +.form__input { + display: inline-block; + flex: 1; + padding: 5px; + border: 1px solid #fff; + @include borderRadius(5px); + @include boxShadow(0, 2px, 4px, 0, rgba(0, 0, 0, 0.3)); + font-size: 16px; + background-color: #fff; + color: #333; + max-width: 320px; + min-width: 160px; + margin: 5px 0; + outline: none; + &.form__input--error { + background-color: rgba(255, 112, 112, 0.1); + border-color: $error; + } + &.form__input--valid { + background-color: rgba(125, 252, 245, 0.1); + border-color: $valid; + } + /* LOGIN */ + &.form__input--login { + background-color: transparent; + border: 1px solid transparent; + border-bottom: 1px solid #fff; + @include borderRadius(0px); + @include cancelBoxShadow(); + margin: 10px 0; + height: 40px; + line-height: 40px; + font-size: 18px; + padding: 0 5px 0 45px; + color: #fff; + background-image: url('../img/login-icons@2x.png'); + background-size: 40px 80px; + background-repeat: no-repeat; + max-width: 330px; + &.name { + background-position: 0 0; + } + &.pswd { + background-position: 0 -40px; + } + &:focus, + &:active { + outline: none !important; + border: 1px solid #fff; + background-color: rgba(255, 255, 255, 0.2) + } + &.error { + border: 1px solid $error; + background-color: rgba(255, 112, 112, 0.2); + } + } + &[disabled="disabled"] { + background-color: #ececec; + border-color: #ccc; + color: $textColor; + } + &.input--number { + max-width: 100px; + min-width: 100px; + } +} + +.form__input::placeholder { + color: #b9b9b9; + font-style: italic; +} + +.form__input::-webkit-input-placeholder { + color: #b9b9b9; + font-style: italic; +} + +.form__input::-ms-input-placeholder { + color: #b9b9b9; + font-style: italic; +} + +.form__input::-moz-placeholder { + color: #b9b9b9; + font-style: italic; +} + +.form__input:-moz-placeholder { + color: #b9b9b9; + font-style: italic; +} + + +/* Select */ + +.form__select { + display: inline-block; + flex: 1; + padding: 5px; + border: 1px solid #fff; + @include borderRadius(5px); + @include boxShadow(0, + 2px, + 4px, + 0, + rgba(0, 0, 0, 0.3)); + font-size: 15px; + background-color: #fff; + color: #333; + max-width: 320px; + min-width: 160px; + margin: 5px 0; + outline: none; + &.form__select--error { + background-color: rgba(255, 112, 112, 0.1); + border-color: $error; + } + &.form__select--valid { + background-color: rgba(125, 252, 245, 0.1); + border-color: $valid; + } + &[disabled="disabled"] { + background-color: #ececec; + border-color: #ccc; + color: $textColor; + } + &.form__select--inarray { + max-width: 120px; + min-width: 80px; + } +} + +.form__checkbox-container { + margin: 10px 0; + align-items: flex-start; + .form__select, + .form__input { + margin-top: -7px; + } + input[type="checkbox"] { + cursor: pointer; + } + .form__checkbox-label { + display: inline-block; + line-break: normal; + font-size: 15px; + font-weight: 600; + padding: 0 15px 0 5px; + line-height: 18px; + @include noSelection(); + cursor: pointer; + } +} + + +/* Textarea */ + +.form__textarea { + display: inline-block; + flex: 1; + padding: 5px; + border: 1px solid #fff; + @include borderRadius(5px); + @include boxShadow(0, + 2px, + 4px, + 0, + rgba(0, 0, 0, 0.3)); + font-size: 14px; + background-color: #fff; + color: #333; + max-width: 400px; + min-width: 220px; + min-height: 80px; + height: auto; + margin: 5px 0; + resize: vertical; + &.form__textarea--error { + background-color: rgba(255, 112, 112, 0.1); + border-color: $error; + } + &.form__textarea--valid { + background-color: rgba(125, 252, 245, 0.1); + border-color: $valid; + } +} + + +/* input file */ + +.input-file-container { + position: relative; + .input__file { + position: absolute; + z-index: -1; + top: 5px; + left: 5px; + width: 1px; + height: 1px; + &:hover { + cursor: pointer; + } + } + .input__file-label-btn { + display: inline-block; + min-width: 120px; + text-align: center; + padding: 10px; + border: 1px solid $greenChart; + background-color: $greenChart; + color: #fff; + z-index: 5; + @include borderRadius(3px); + @include boxShadow(0, + 2px, + 2px, + 0, + rgba(0, 0, 0, 0.2)); + margin-right: 20px; + .input__file-icon { + display: inline-block; + width: 30px; + height: 30px; + @include maskImage('../img/svg/upload.svg'); + background-color: #fff; + vertical-align: top; + } + .input__file-label { + vertical-align: top; + display: inline-block; + height: 30px; + line-height: 30px; + font-weight: 700; + color: #fff; + } + &.error { + background-color: $error; + border-color: $error; + } + &:hover { + cursor: pointer; + background-color: #fff; + color: $greenChart; + .input__file-label { + color: $greenChart; + } + .input__file-icon { + background-color: $greenChart; + } + &.error { + &:hover { + .input__file-label { + color: $error; + } + .input__file-icon { + background-color: $error; + } + } + } + } + @include transition(all 0.3s ease); + } +} + + +/* Label */ + +.form__label { + font-size: 14px; + font-weight: 600; + color: $blueMid; + &.form__label--sub { + line-height: 30px; + font-weight: 500; + } + strong { + font-size: 16px; + color: $error; + } +} + + +/* Error field */ + +.form__error-field { + font-size: 14px; + line-height: 16px; + height: 16px; + margin: 0 0 4px 0; + color: $error; + font-style: italic; + &.features-error { + margin-top: 10px; + margin-left: -10px; + } +} + +.form__info { + display: inline-block; + font-size: 14px; + line-height: 16px; + color: #b2b2b2; + font-style: italic; +} + +.stt-field { + margin: 0 10px; +} + + +/* Featurs (app deploy) */ + +.application-features-container { + padding-left: 20px; + border-left: 3px solid $blueMid; + margin: 20px 10px; + &.error { + border-color: $error; + } +} + +.dictation-external { + margin-left: 100px; + margin-top: -15px; +} + + +/* HELPER BUTTON */ + +.helper-btn { + display: inline-block; + width: 20px; + height: 20px; + background: $blueDark; + cursor: pointer; + color: #fff; + @include borderRadius(10px); + padding: 0; + margin-right: 10px; + border: 1px solid $blueDark; + @include transition(all 0.3s ease); + font-weight: 600; + &:hover { + background-color: #fff; + color: $blueDark; + } +} + +.helper-content { + display: inline-block; + max-width: 640px; + height: auto; + padding: 20px; + position: absolute; + top: 25px; + left: 0; + background: #fff; + font-size: 14px; + z-index: 20; + border: 1px solid $blueDark; + .close { + @include transition(all 0.3s ease); + display: inline-block; + position: absolute; + top: 5px; + left: 100%; + margin-left: -25px; + width: 22px; + height: 22px; + background-color: transparent; + border: 1px solid $error; + @include borderRadius(3px); + padding: 0; + &:after { + @include transition(all 0.3s ease); + content: ''; + display: inline-block; + width: 20px; + height: 20px; + position: absolute; + top: 0; + left: 0; + @include maskImage('../img/svg/close.svg'); + background-color: $error; + margin: 0; + padding: 0; + } + &:hover { + cursor: pointer; + background-color: $error; + &:after { + background-color: #fff; + } + } + } + p { + margin: 0; + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/healthcheck.scss b/platform/linto-admin/vue_app/public/sass/components/healthcheck.scss new file mode 100644 index 0000000..3a7331c --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/healthcheck.scss @@ -0,0 +1,37 @@ +.healtcheck-overview { + max-width: 320px; + margin: 20px auto; + background-color: #fcfcfc; + @include blockShadow(); + padding: 40px; +} + +table.healthcheck-table { + border-collapse: collapse; + margin-bottom: 20px; + width: 100%; + thead { + th { + text-align: left; + } + } + tr { + border-bottom: 1px solid #ccc; + } + td { + padding: 10px 5px; + &.status { + text-align: center; + span { + display: inline-block; + font-weight: 600; + &.connected { + color: $greenChart; + } + &.disconnected { + color: $error; + } + } + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/iframe.scss b/platform/linto-admin/vue_app/public/sass/components/iframe.scss new file mode 100644 index 0000000..a20422d --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/iframe.scss @@ -0,0 +1,39 @@ +#iframe-container { + &.iframe--default { + display: flex; + @include blockShadow(); + padding: 5px; + background-color: #fff; + } + &.iframe--fullscreen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; + padding: 0; + background-color: #fff; + } + + .iframe__controls { + background-color: #fff; + height: 30px; + padding: 10px 20px; + z-index: 10; + .iframe__controls-right { + justify-content: flex-end; + .button { + margin: 0 5px; + } + } + } +} + +.iframe { + border: none; + padding: 0; + margin: 0; + @include cancelBoxShadow(); +} + diff --git a/platform/linto-admin/vue_app/public/sass/components/linto-monitoring.scss b/platform/linto-admin/vue_app/public/sass/components/linto-monitoring.scss new file mode 100644 index 0000000..323c388 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/linto-monitoring.scss @@ -0,0 +1,134 @@ +.linto-config-table { + padding: 20px; + background-color: #fcfcfc; + margin: 0 20px 20px 20px; + border: 1px solid $blueMid; + max-height: 360px; + overflow: auto; + .network-item { + margin: 10px 0; + } +} + +.table--config { + border-collapse: collapse; + tr { + td { + padding: 5px 10px; + border: 1px solid #ccc; + text-align: left; + width: 50%; + background-color: #fafafa; + font-size: 14px; + } + } +} + +.linto-settings-item { + margin: 0 0 20px 40px; + .ping-status { + margin-top: 5px; + font-size: 14px; + font-style: italic; + &.success { + color: $greenChart; + } + &.error { + color: $error + } + } +} + +.button { + &.button--ping { + width: 120px; + border-color: $blueMid; + background-color: #fff; + .label { + color: $blueMid; + } + .icon { + display: inline-block; + width: 30px; + height: 30px; + margin-right: 10px; + background-image: url('../img/ping@2x.png'); + background-size: 30px 60px; + background-repeat: no-repeat; + background-position: 0 0; + } + &:hover { + background-color: $blueMid; + .label { + color: #fff; + } + .icon { + background-position: 0 -30px; + } + } + &.loading { + background-color: #fff; + border-color: $blueLinto; + .label { + color: $blueLinto; + } + .icon { + background-image: url('../img/loading@2x.png'); + background-size: 30px 30px; + @include rotate(); + } + } + } + &.button--say { + border-color: $blueLinto; + margin: 25px 0 0 10px; + .label { + color: $blueLinto; + } + .icon { + display: inline-block; + width: 30px; + height: 30px; + background-image: url('../img/say@2x.png'); + background-size: 30px 60px; + background-repeat: no-repeat; + background-position: 0 0; + } + &:hover { + background-color: $blueLinto; + .label { + color: #fff; + } + .icon { + background-position: 0 -30px; + } + } + } + /* Mute / unmute */ + &.button--img { + margin: 0 10px; + border-color: $blueMid; + .button__icon { + background-image: url('../img/mute-unmute@2x.png'); + background-size: 60px 60px; + background-repeat: no-repeat; + &.button__icon--mute { + background-position: -30px 0; + } + &.button__icon--unmute { + background-position: 0 0; + } + } + &:hover { + background-color: $blueMid; + .button__icon { + &.button__icon--mute { + background-position: -30px -30px; + } + &.button__icon--unmute { + background-position: 0 -30px; + } + } + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/login.scss b/platform/linto-admin/vue_app/public/sass/components/login.scss new file mode 100644 index 0000000..54990fe --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/login.scss @@ -0,0 +1,58 @@ +#login-wrapper { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 2; + background-image: url('../img/bg-login.jpg'); + background-size: cover; + background-position: center center; + background-repeat: no-repeat; +} + +#login-wrapper { + justify-content: center; + align-items: center; + .login-logo { + width: 340px; + height: auto; + margin: 0 auto 40px auto; + } + .login-form-container { + max-width: 800px; + padding: 40px; + justify-content: center; + &>div { + justify-content: center; + } + } +} + +.setup-form-container { + padding: 40px; + background-color: rgba(255, 255, 255, 0.75); + border: 1px solid #fff; + @include blockShadow(); + color: $blueDark; + justify-content: center; + max-width: 420px; + h1 { + font-size: 22px; + color: $blueLinto; + width: 100%; + text-align: center; + } + .info { + font-size: 16px; + color: $blueDark; + padding-bottom: 20px; + } + .field-info { + font-size: 14px; + ul { + font-size: 14px; + margin: 0; + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/modal.scss b/platform/linto-admin/vue_app/public/sass/components/modal.scss new file mode 100644 index 0000000..8494d35 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/modal.scss @@ -0,0 +1,156 @@ +.modal-wrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 990; + align-items: center; + justify-content: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + &.hidden { + display: none; + } +} + +.modal { + max-width: 880px; + min-width: 600px; + max-height: 600px; + height: auto; + background-color: #fff; + display: flex; + flex-direction: column; + @include borderRadius(5px); + @include blockShadow(); + padding: 20px; + .modal-header { + min-height: 30px; + height: auto; + border-bottom: 1px solid $blueLight; + padding-bottom: 10px; + .modal-header__tilte { + min-height: 30px; + display: inline-block; + font-size: 18px; + font-weight: 600; + color: $blueMid; + } + } + .modal-body { + padding: 20px 0; + overflow: auto; + flex: 1; + &>.flex { + padding: 0 5px; + } + .modal-body__content { + .subtitle { + display: inline-block; + width: 100%; + font-size: 18px; + font-weight: 600; + color: $blueDark; + padding-bottom: 20px; + } + strong { + font-weight: 600; + color: $error; + } + table { + tr { + td { + strong { + display: inline-block; + vertical-align: top; + color: $blueMid; + font-weight: 600; + padding-right: 5px; + line-height: 32px; + } + } + } + } + } + } + .modal-footer { + border-top: 1px solid $blueLight; + padding-top: 20px; + .modal-footer-left { + justify-content: flex-start; + } + .modal-footer-right { + justify-content: flex-end; + } + .button { + margin-left: 10px; + } + } +} + +ul.deploy-status { + padding: 0 20px; + margin: 0; + flex: 1; + display: flex; + flex-direction: column; +} + +.deploy-status--item { + display: flex; + flex-direction: row; + line-height: 20px; + padding: 10px 0; + border-bottom: 1px solid #ececec; + .icon { + display: inline-block; + width: 20px; + height: 20px; + background-color: transparent; + margin-right: 5px; + } + .label { + display: inline-block; + flex: 1; + font-size: 16px; + color: #777; + } + &.deploy-status--item__updating { + .icon { + background-image: url('../img/deploy-status-icons@2x.png'); + background-size: 20px 60px; + background-repeat: no-repeat; + background-position: 0 -40px; + @include rotate(); + } + .label { + color: $blueLinto; + } + } + &.deploy-status--item__valid { + .icon { + background-image: url('../img/deploy-status-icons@2x.png'); + background-size: 20px 60px; + background-repeat: no-repeat; + background-position: 0 0; + } + .label { + color: $valid; + } + } + &.deploy-status--item__error { + .icon { + background-image: url('../img/deploy-status-icons@2x.png'); + background-size: 20px 60px; + background-repeat: no-repeat; + background-position: 0 -20px; + } + .label { + color: $error; + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/components/skills.scss b/platform/linto-admin/vue_app/public/sass/components/skills.scss new file mode 100644 index 0000000..30c4f0d --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/components/skills.scss @@ -0,0 +1,40 @@ +.skills-list-container { + max-height: 480px; + overflow-y: auto; + overflow-x: hidden; +} + +.skills-list { + border-collapse: collapse; + width: 100%; + thead { + width: 100%; + tr { + th { + text-align: left; + } + } + } + tbody { + width: 100%; + tr { + border: 5px solid $blueExtraLight; + td { + padding: 10px; + background-color: rgba(255, 255, 255, 0.8); + position: relative; + &.center { + text-align: center; + } + &.skill--id { + min-width: 220px; + span { + font-weight: 600; + color: $blueDark; + font-size: 15px; + } + } + } + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/sass/styles.scss b/platform/linto-admin/vue_app/public/sass/styles.scss new file mode 100644 index 0000000..00ced66 --- /dev/null +++ b/platform/linto-admin/vue_app/public/sass/styles.scss @@ -0,0 +1,18 @@ +@charset "utf-8"; +@import "./_mixin.scss"; +@import "./_variables.scss"; +@import "./_global.scss"; +@import "./components/app.scss"; +@import "./components/app-header.scss"; +@import "./components/app-vertical-nav.scss"; +@import "./components/app-notify.scss"; +@import "./components/app-notify-top.scss"; +@import "./components/buttons.scss"; +@import "./components/forms.scss"; +@import "./components/iframe.scss"; +@import "./components/login.scss"; +@import "./components/modal.scss"; +@import "./components/healthcheck.scss"; +@import "./components/linto-monitoring.scss"; +@import "./components/clients-overview.scss"; +@import "./components/skills.scss"; \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/App.vue b/platform/linto-admin/vue_app/src/App.vue new file mode 100644 index 0000000..8061169 --- /dev/null +++ b/platform/linto-admin/vue_app/src/App.vue @@ -0,0 +1,122 @@ + + diff --git a/platform/linto-admin/vue_app/src/components/AppFormLabel.vue b/platform/linto-admin/vue_app/src/components/AppFormLabel.vue new file mode 100644 index 0000000..1d7d1a3 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppFormLabel.vue @@ -0,0 +1,26 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/AppHeader.vue b/platform/linto-admin/vue_app/src/components/AppHeader.vue new file mode 100644 index 0000000..1ad81a1 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppHeader.vue @@ -0,0 +1,22 @@ + + diff --git a/platform/linto-admin/vue_app/src/components/AppIframe.vue b/platform/linto-admin/vue_app/src/components/AppIframe.vue new file mode 100644 index 0000000..17df0ba --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppIframe.vue @@ -0,0 +1,18 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/AppInput.vue b/platform/linto-admin/vue_app/src/components/AppInput.vue new file mode 100644 index 0000000..c865762 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppInput.vue @@ -0,0 +1,178 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/AppNotif.vue b/platform/linto-admin/vue_app/src/components/AppNotif.vue new file mode 100644 index 0000000..bf2c54f --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppNotif.vue @@ -0,0 +1,83 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/AppNotifTop.vue b/platform/linto-admin/vue_app/src/components/AppNotifTop.vue new file mode 100644 index 0000000..b28a535 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppNotifTop.vue @@ -0,0 +1,139 @@ + + diff --git a/platform/linto-admin/vue_app/src/components/AppSelect.vue b/platform/linto-admin/vue_app/src/components/AppSelect.vue new file mode 100644 index 0000000..77f1a92 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppSelect.vue @@ -0,0 +1,144 @@ + + diff --git a/platform/linto-admin/vue_app/src/components/AppTextarea.vue b/platform/linto-admin/vue_app/src/components/AppTextarea.vue new file mode 100644 index 0000000..a43a130 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppTextarea.vue @@ -0,0 +1,25 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/AppVerticalNav.vue b/platform/linto-admin/vue_app/src/components/AppVerticalNav.vue new file mode 100644 index 0000000..a71d88b --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/AppVerticalNav.vue @@ -0,0 +1,91 @@ + + diff --git a/platform/linto-admin/vue_app/src/components/ModalAddDomain.vue b/platform/linto-admin/vue_app/src/components/ModalAddDomain.vue new file mode 100644 index 0000000..e012d5d --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalAddDomain.vue @@ -0,0 +1,155 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalAddTerminal.vue b/platform/linto-admin/vue_app/src/components/ModalAddTerminal.vue new file mode 100644 index 0000000..5cd2597 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalAddTerminal.vue @@ -0,0 +1,109 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalAddUsers.vue b/platform/linto-admin/vue_app/src/components/ModalAddUsers.vue new file mode 100644 index 0000000..e27bf44 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalAddUsers.vue @@ -0,0 +1,231 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalDeleteDomain.vue b/platform/linto-admin/vue_app/src/components/ModalDeleteDomain.vue new file mode 100644 index 0000000..2ad4e02 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalDeleteDomain.vue @@ -0,0 +1,100 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalDeleteMultiUserApp.vue b/platform/linto-admin/vue_app/src/components/ModalDeleteMultiUserApp.vue new file mode 100644 index 0000000..a290cf7 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalDeleteMultiUserApp.vue @@ -0,0 +1,185 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalDeleteTerminal.vue b/platform/linto-admin/vue_app/src/components/ModalDeleteTerminal.vue new file mode 100644 index 0000000..20ed576 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalDeleteTerminal.vue @@ -0,0 +1,85 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalDeleteUser.vue b/platform/linto-admin/vue_app/src/components/ModalDeleteUser.vue new file mode 100644 index 0000000..20ca9a7 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalDeleteUser.vue @@ -0,0 +1,101 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalDissociateTerminal.vue b/platform/linto-admin/vue_app/src/components/ModalDissociateTerminal.vue new file mode 100644 index 0000000..f98e457 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalDissociateTerminal.vue @@ -0,0 +1,90 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalEditDomain.vue b/platform/linto-admin/vue_app/src/components/ModalEditDomain.vue new file mode 100644 index 0000000..3387706 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalEditDomain.vue @@ -0,0 +1,186 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalEditDomainApplications.vue b/platform/linto-admin/vue_app/src/components/ModalEditDomainApplications.vue new file mode 100644 index 0000000..caa21a3 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalEditDomainApplications.vue @@ -0,0 +1,515 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalEditUser.vue b/platform/linto-admin/vue_app/src/components/ModalEditUser.vue new file mode 100644 index 0000000..d46c054 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalEditUser.vue @@ -0,0 +1,240 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalEditUserApps.vue b/platform/linto-admin/vue_app/src/components/ModalEditUserApps.vue new file mode 100644 index 0000000..efce160 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalEditUserApps.vue @@ -0,0 +1,270 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalManageDomains.vue b/platform/linto-admin/vue_app/src/components/ModalManageDomains.vue new file mode 100644 index 0000000..c7ecaff --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalManageDomains.vue @@ -0,0 +1,221 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalManageUsers.vue b/platform/linto-admin/vue_app/src/components/ModalManageUsers.vue new file mode 100644 index 0000000..1b18870 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalManageUsers.vue @@ -0,0 +1,262 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalReplaceTerminal.vue b/platform/linto-admin/vue_app/src/components/ModalReplaceTerminal.vue new file mode 100644 index 0000000..e5681da --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalReplaceTerminal.vue @@ -0,0 +1,127 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/ModalUpdateApplicationServices.vue b/platform/linto-admin/vue_app/src/components/ModalUpdateApplicationServices.vue new file mode 100644 index 0000000..dedfd0d --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/ModalUpdateApplicationServices.vue @@ -0,0 +1,584 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/components/NodeRedIframe.vue b/platform/linto-admin/vue_app/src/components/NodeRedIframe.vue new file mode 100644 index 0000000..21f6974 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/NodeRedIframe.vue @@ -0,0 +1,132 @@ + + diff --git a/platform/linto-admin/vue_app/src/components/TockIframe.vue b/platform/linto-admin/vue_app/src/components/TockIframe.vue new file mode 100644 index 0000000..b907997 --- /dev/null +++ b/platform/linto-admin/vue_app/src/components/TockIframe.vue @@ -0,0 +1,45 @@ + + diff --git a/platform/linto-admin/vue_app/src/filters/index.js b/platform/linto-admin/vue_app/src/filters/index.js new file mode 100644 index 0000000..60f9f48 --- /dev/null +++ b/platform/linto-admin/vue_app/src/filters/index.js @@ -0,0 +1,438 @@ +import Vue from 'vue' +import store from '../store.js' + + + +/** + * @desc dispatch store - execute an "action" on vuex store + * @param {string} action - vuex store action name + * @param {object} data - data to be passed to the dipsatch function (optional) + * @return {object} {status, msg} + */ +Vue.filter('dispatchStore', async function(action, data) { + try { + let req = null + if (!!data) { + req = await store.dispatch(action, data) + } else { + req = await store.dispatch(action) + } + if (!!req.error) { + throw req.error + } + if (typeof req !== 'undefined') { + return { + status: 'success', + msg: '' + } + } else { + throw 'an error has occured' + } + } catch (error) { + return ({ + status: 'error', + msg: error + }) + } +}) + +/** + * @desc global test on "select" fields base on an object format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testSelectField', function(obj) { + obj.error = null + obj.valid = false + if (typeof(obj.value) === 'undefined') { + obj.value = '' + } + if (obj.value === '' || obj.value.length === 0) { + obj.error = 'This field is required' + } else { + obj.valid = true + } +}) + + +/** + * @desc Test password format + * Conditions : + * - length > 6 + * - alphanumeric characters and/or special chars : "!","@","#","$","%","-","_" + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +const testPassword = (obj) => { + obj.valid = false + obj.error = null + const regex = /^[0-9A-Za-z\!\@\#\$\%\-\_\s]{4,}$/ + if (obj.value.length === 0) { + obj.error = 'This field is required' + } else if (obj.value.length < 6) { + obj.error = 'This field must contain at least 6 characters' + } else if (obj.value.match(regex)) { + obj.valid = true + } else { + obj.error = 'Invalid password' + } + return obj +} + +/** + * @desc Test email format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +const testEmail = (obj) => { + obj.valid = false + obj.error = null + const regex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/ + if (obj.value.match(regex)) { + obj.valid = true + } else { + obj.error = 'Invalid email' + } + return obj +} + +/** + * @desc Test url format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +const testUrl = (obj) => { + obj.valid = false + obj.error = null + if (obj.value.length === 0) { + obj.valid = false + obj.error = 'This field is required' + } else { + const regex = /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/g + if (obj.value.match(regex)) { + obj.valid = true + } else { + obj.error = 'Invalid content url format.' + } + } + return obj +} + +/** + * @desc Test device workflow name format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testDeviceWorkflowName', async function(obj) { + obj.error = null + obj.valid = false + await store.dispatch('getDeviceApplications') + const workflows = store.state.deviceApplications + if (workflows.length > 0 && workflows.filter(wf => wf.name === obj.value).length > 0) { // check if workflow name is not used + obj.error = 'This workflow name is already used' + obj.valid = false + } +}) + +/** + * @desc Test multi-user workflow name format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testMultiUserWorkflowName', async function(obj) { + obj.error = null + obj.valid = false + await store.dispatch('getMultiUserApplications') + const workflows = store.state.multiUserApplications + if (workflows.length > 0 && workflows.filter(wf => wf.name === obj.value).length > 0) { // check if workflow name is not used + obj.error = 'This workflow name is already used' + obj.valid = false + } +}) + +/** + * @desc Test serial number format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testStaticClientsSN', async function(obj) { + obj.error = null + obj.valid = false + await store.dispatch('getStaticClients') + const clients = store.state.staticClients + if (clients.length > 0 && clients.filter(wf => wf.sn === obj.value && wf.associated_workflow !== null).length > 0) { // check if serial number is not used + obj.error = 'This serial number is already used' + obj.valid = false + } +}) + +/** + * @desc Test name format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * Conditions : + * - Length > 5 + * - alphanumeric charcates or/and: "-", "_", " " + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testName', function(obj) { + const regex = /^[0-9A-Za-z\s\-\_]+$/ + obj.valid = false + obj.error = null + if (obj.value.length === 0) { + obj.error = 'This field is required' + } else if (obj.value.length < 5) { + obj.error = 'This field must contain at least 5 characters' + } else if (obj.value.match(regex)) { + obj.valid = true + } else { + obj.error = 'Invalid name' + } +}) + +/** + * @desc Test password format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testPassword', function(obj) { + obj = testPassword(obj) +}) + +/** + * @desc Test password confirmation format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testPasswordConfirm', function(obj, compareObj) { + obj = testPassword(obj) + if (obj.valid) { + if (obj.value === compareObj.value) { + obj.valid = true + } else { + obj.valid = false + obj.error = 'The confirmation password is different from password' + } + } +}) + +/** + * @desc Test email format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testEmail', function(obj) { + obj = testEmail(obj) +}) + +/** + * @desc Test android user email format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testAndroidUserEmail', async function(obj) { + obj.valid = false + obj.error = null + obj = testEmail(obj) + if (obj.valid) { + await store.dispatch('getAndroidUsers') + const users = store.state.androidUsers + const userExist = users.filter(user => user.email === obj.value) + if (userExist.length > 0) { // check if email address is not used + obj.valid = false + obj.error = 'This email address is already used' + } else { + obj.valid = true + obj.error = null + } + } +}) + +/** + * @desc Test content format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * Conditions : + * - Length > 0 + * - alphanumeric characters or/and : "?","!","@","#","$","%","-","_",".",",","(",")","[","]","=","+",":",";" + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testContent', function(obj) { + obj.valid = false + obj.error = null + if (obj.value.length === 0) { + obj.valid = true + } else { + const regex = /[0-9A-Za-z\?\!\@\#\$\%\-\_\.\/\,\:\;\(\)\[\]\=\+\s]+$/g + if (obj.value.match(regex)) { + obj.valid = true + } else { + obj.error = 'Invalid content. Unauthorized characters.' + } + } +}) + +/** + * @desc Test content format to be sayed by linto + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * Conditions : + * - Length > 0 + * - alphanumeric characters or/and : "?","!","-",".",",",":",";" + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testContentSay', function(obj) { + obj.valid = false + obj.error = null + if (obj.value.length === 0) { + obj.valid = true + } else { + const regex = /[0-9A-Za-z\?\!\-\.\:\,\;\s]+$/g + if (obj.value.match(regex)) { + obj.valid = true + } else { + obj.error = 'Unauthorized characters.' + } + } +}) + +/** + * @desc Test url format for domains + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ +Vue.filter('testUrl', async function(obj) { + obj.valid = false + obj.error = null + obj = testUrl(obj) + if (obj.valid) { + await store.dispatch('getWebappHosts') + const hosts = store.state.webappHosts + const hostExist = hosts.filter(host => host.originUrl === obj.value) + if (hostExist.length > 0) { // check if domain is not used + obj.valid = false + obj.error = 'This origin url is already used' + } else { + obj.error = null + obj.valid = true + } + } +}) +Vue.filter('testPath', async function(obj) { + obj.valid = false + obj.error = null + if (obj.value.length === 0) { + obj.valid = false + obj.error = 'This field is required' + } else { + const regex = /^((\/){1}[a-z0-9]+([\-\.]{1}[a-z0-9]+)*)*$/g + if (obj.value.match(regex)) { + obj.valid = true + } else { + obj.error = 'Invalid path format.' + } + } + return obj +}) + +/** + * @desc Test integer format + * @param {object} obj {value: "string", error: "null" OR "string", valid: "boolean"} + * @return {object} {value: "string", error: "null" OR "string", valid: "boolean"} + */ + +Vue.filter('testInteger', function(obj) { + obj.valid = false + obj.error = null + if (obj.value.length === 0) { + obj.valid = false + obj.error = 'This field is required' + } else { + const regex = /[0-9]+$/g + if (obj.value.toString().match(regex)) { + obj.valid = true + } else { + obj.valid = false + obj.error = 'This value must be an integer' + } + } +}) + +Vue.filter('notEmpty', function(obj) { + obj.valid = false + obj.error = null + if (obj.value.length === 0) { + obj.valid = false + obj.error = 'This field is required' + } else { + obj.valid = true + } +}) + +Vue.filter('getSettingsByApplication', function(data) { + let settings = { + language: '', + command: { + enabled: false, + value: '' + }, + chatbot: { + enabled: false, + value: '' + }, + streaming: { + enabled: false, + value: '', + internal: 'false' + }, + tock: { + value: '' + } + } + // get worlflow language + if (!!data.flow && !!data.flow.nodes && data.flow.nodes.length > 0) { + const nodeConfig = data.flow.nodes.filter(node => node.type === 'linto-config') + if (nodeConfig.length > 0) { + settings.language = nodeConfig[0].language + } + } + + if (!!data.flow && !!data.flow.configs && data.flow.configs.length > 0) { + const nodeConfigTock = data.flow.configs.filter(node => node.type === 'linto-config-evaluate') + const nodeConfigTranscribe = data.flow.configs.filter(node => node.type === 'linto-config-transcribe') + const nodeConfigChatbot = data.flow.configs.filter(node => node.type === 'linto-config-chatbot') + const nodeLintoChatbot = data.flow.nodes.filter(node => node.type === 'linto-chatbot') + const nodeLintoStreaming = data.flow.nodes.filter(node => node.type === 'linto-transcribe-streaming') + const nodeLintoEvaluate = data.flow.nodes.filter(node => node.type === 'linto-evaluate') + const nodeLintoTranscribe = data.flow.nodes.filter(node => node.type === 'linto-transcribe') + + // tock + if (nodeConfigTock.length > 0) { + if (nodeConfigTock[0].appname !== '') { + settings.tock.value = nodeConfigTock[0].appname + } + } + + if (nodeConfigTranscribe.length > 0) { + // streaming + settings.streaming.value = nodeConfigTranscribe[0].largeVocabStreaming + settings.streaming.internal = nodeConfigTranscribe[0].largeVocabStreamingInternal + if (nodeLintoStreaming.length > 0) { + settings.streaming.enabled = true + } + + // commands + if (nodeConfigTranscribe[0].commandOffline !== '') { + settings.command.value = nodeConfigTranscribe[0].commandOffline + if (nodeLintoEvaluate.length > 0 && nodeLintoTranscribe.length > 0) { + settings.command.enabled = true + } + } + } + // chatbot + if (nodeConfigChatbot.length > 0) { + settings.chatbot.value = nodeConfigChatbot[0].rest + if (nodeLintoChatbot.length > 0) { + settings.chatbot.enabled = true + } + } + } + return settings +}) \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/login.js b/platform/linto-admin/vue_app/src/login.js new file mode 100644 index 0000000..38bc347 --- /dev/null +++ b/platform/linto-admin/vue_app/src/login.js @@ -0,0 +1,8 @@ +import Vue from 'vue' +import Login from './views/Login.vue' +import router from './router/router-login.js' + +new Vue({ + router, + render: h => h(Login) +}).$mount('#app') \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/main.js b/platform/linto-admin/vue_app/src/main.js new file mode 100644 index 0000000..30f999a --- /dev/null +++ b/platform/linto-admin/vue_app/src/main.js @@ -0,0 +1,15 @@ +import Vue from 'vue' +import App from './App.vue' +import router from './router.js' +import store from './store.js' +export const bus = new Vue() + +import './filters/index.js' + +Vue.config.productionTip = false + +new Vue({ + router, + store, + render: h => h(App) +}).$mount('#app') \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/page404.js b/platform/linto-admin/vue_app/src/page404.js new file mode 100644 index 0000000..3821029 --- /dev/null +++ b/platform/linto-admin/vue_app/src/page404.js @@ -0,0 +1,8 @@ +import Vue from 'vue' +import page404 from './views/404.vue' +import router from './router/router-404.js' + +new Vue({ + router, + render: h => h(page404) +}).$mount('#app') \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/router.js b/platform/linto-admin/vue_app/src/router.js new file mode 100644 index 0000000..53e53ef --- /dev/null +++ b/platform/linto-admin/vue_app/src/router.js @@ -0,0 +1,284 @@ +import Vue from 'vue' +import Router from 'vue-router' +import axios from 'axios' + +// Views +import DeviceApps from './views/DeviceApps.vue' +import DeviceAppDeploy from './views/DeviceAppDeploy.vue' +import DeviceAppWorkflowEditor from './views/DeviceAppWorkflowEditor.vue' +import MultiUserApps from './views/MultiUserApps.vue' +import MultiUserAppDeploy from './views/MultiUserAppDeploy.vue' +import MultiUserAppWorkflowEditor from './views/MultiUserAppWorkflowEditor.vue' +import Terminals from './views/Terminals.vue' +import TerminalsMonitoring from './views/TerminalsMonitoring.vue' +import Users from './views/Users.vue' +import Domains from './views/Domains.vue' +import TockView from './views/TockView.vue' +import SkillsManager from './views/SkillsManager.vue' + +Vue.use(Router) +const router = new Router({ + mode: 'history', + routes: [{ + path: '/admin/applications/device', + name: 'Static devices overview', + component: DeviceApps, + meta: [{ + name: 'title', + content: 'LinTO Admin - Static clients' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ] + }, + { + path: '/admin/applications/device/workflow/:workflowId', + name: 'Static device flow editor', + component: DeviceAppWorkflowEditor, + meta: [{ + name: 'title', + content: 'LinTO Admin - Static clients workflow editor' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ], + beforeEnter: async(to, from, next) => { + try { + // Check if the targeted device application exists + const workflowId = to.params.workflowId + const getWorkflow = await axios(`${process.env.VUE_APP_URL}/api/workflows/static/${workflowId}`) + if (!!getWorkflow.data.error) { + next('/admin/applications/device') + } else { + next() + } + } catch (error) { + console.error(error) + next('/admin/applications/device') + + } + } + }, + { + path: '/admin/applications/device/deploy', + name: 'Static devices - deployment', + component: DeviceAppDeploy, + meta: [{ + name: 'title', + content: 'LinTO Admin - Static clients deployment' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ] + }, + { + path: '/admin/applications/device/deploy/:sn', + name: 'Static devices - deployment by id', + component: DeviceAppDeploy, + meta: [{ + name: 'title', + content: 'LinTO Admin - Static clients deployment' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ], + beforeEnter: async(to, from, next) => { + try { + // Check if the targeted device exists + const sn = to.params.sn + const getStaticDevice = await axios(`${process.env.VUE_APP_URL}/api/clients/static/${sn}`) + if (getStaticDevice.data.associated_workflow !== null) { + next('/admin/applications/device') + } else { + next() + } + } catch (error) { + console.error(error) + next('/admin/applications/device') + } + } + }, + { + path: '/admin/devices', + name: 'Devices - statice devices', + component: Terminals, + meta: [{ + name: 'title', + content: 'Devices and static devices' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ] + }, + { + path: '/admin/device/:sn/monitoring', + name: 'Static devices - monitoring', + component: TerminalsMonitoring, + meta: [{ + name: 'title', + content: 'LinTO Admin - Static clients monitoring' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ], + beforeEnter: async(to, from, next) => { + try { + // Check if the targeted device exists + const sn = to.params.sn + const getStaticDevice = await axios(`${process.env.VUE_APP_URL}/api/clients/static/${sn}`) + if (getStaticDevice.data.associated_workflow === null) { + next('/admin/applications/device') + } else { + next() + } + } catch (error) { + console.error(error) + next('/admin/applications/device') + } + } + }, + { + path: '/admin/applications/multi', + name: 'Applications overview', + component: MultiUserApps, + meta: [{ + name: 'title', + content: 'LinTO Admin - applications' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ] + }, { + path: '/admin/applications/multi/deploy', + name: 'Create new application', + component: MultiUserAppDeploy, + meta: [{ + name: 'title', + content: 'LinTO Admin - Create an application workflow' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ], + }, + { + path: '/admin/applications/multi/workflow/:workflowId', + name: 'Nodered application flow editor', + component: MultiUserAppWorkflowEditor, + meta: [{ + name: 'title', + content: 'LinTO Admin - Application flow editor' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ], + beforeEnter: async(to, from, next) => { + try { + // Check if the targeted mutli-user application exists + const workflowId = to.params.workflowId + const getWorkflow = await axios(`${process.env.VUE_APP_URL}/api/workflows/application/${workflowId}`) + if (!!getWorkflow.data.error) { + next('/admin/applications/multi') + } else { + next() + } + } catch (error) { + console.error(error) + next('/admin/applications/multi') + } + } + }, + { + path: '/admin/skills', + name: 'Nodered skills manager', + component: SkillsManager, + meta: [{ + name: 'title', + content: 'Nodered skills manager' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ], + }, + { + path: '/admin/nlu', + name: 'tock interface', + component: TockView, + meta: [{ + name: 'title', + content: 'Tock interface' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ] + }, + { + path: '/admin/users', + name: 'Android users interface', + component: Users, + meta: [{ + name: 'title', + content: 'LinTO admin - android users' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ] + }, + { + path: '/admin/domains', + name: 'Web app hosts interface', + component: Domains, + meta: [{ + name: 'title', + content: 'LinTO admin - Web app hosts' + }, + { + name: 'robots', + content: 'noindex, nofollow' + } + ] + } + ] +}) + +/* The following function parse the route.meta attribtue to set page "title" and "meta" before entering a route" */ +router.beforeEach(async(to, from, next) => { + if (to.meta.length > 0) { + to.meta.map(m => { + if (m.name === 'title') { + document.title = m.content + } else { + let meta = document.createElement('meta') + meta.setAttribute('name', m.name) + meta.setAttribute('content', m.content) + document.getElementsByTagName('head')[0].appendChild(meta) + } + }) + } + next() +}) + +export default router \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/router/router-404.js b/platform/linto-admin/vue_app/src/router/router-404.js new file mode 100644 index 0000000..1cc179a --- /dev/null +++ b/platform/linto-admin/vue_app/src/router/router-404.js @@ -0,0 +1,16 @@ +import Vue from 'vue' +import Router from 'vue-router' +// Views +import page404 from '../views/404.vue' + +Vue.use(Router) +const router = new Router({ + mode: 'history', + routes: [{ + path: '/', + name: 'page404', + component: page404 + }] +}) + +export default router \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/router/router-healthcheck.js b/platform/linto-admin/vue_app/src/router/router-healthcheck.js new file mode 100644 index 0000000..d78aa1b --- /dev/null +++ b/platform/linto-admin/vue_app/src/router/router-healthcheck.js @@ -0,0 +1,16 @@ +import Vue from 'vue' +import Router from 'vue-router' +// Views +import Healthcheck from '../views/Healthcheck.vue' + +Vue.use(Router) +const router = new Router({ + mode: 'history', + routes: [{ + path: '/healthcheck/overview', + name: 'Healthcheck', + component: Healthcheck + }] +}) + +export default router \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/router/router-login.js b/platform/linto-admin/vue_app/src/router/router-login.js new file mode 100644 index 0000000..e46a44f --- /dev/null +++ b/platform/linto-admin/vue_app/src/router/router-login.js @@ -0,0 +1,16 @@ +import Vue from 'vue' +import Router from 'vue-router' +// Views +import Login from '../views/Login.vue' + +Vue.use(Router) +const router = new Router({ + mode: 'history', + routes: [{ + path: '/login', + name: 'login', + component: Login + }] +}) + +export default router \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/router/router-setup.js b/platform/linto-admin/vue_app/src/router/router-setup.js new file mode 100644 index 0000000..1094eda --- /dev/null +++ b/platform/linto-admin/vue_app/src/router/router-setup.js @@ -0,0 +1,18 @@ +import Vue from 'vue' +import Router from 'vue-router' +// Views +import Setup from '../views/Setup.vue' + +import '../filters/index.js' + +Vue.use(Router) +const router = new Router({ + mode: 'history', + routes: [{ + path: '/setup', + name: 'setup', + component: Setup + }] +}) + +export default router \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/setup.js b/platform/linto-admin/vue_app/src/setup.js new file mode 100644 index 0000000..093d7be --- /dev/null +++ b/platform/linto-admin/vue_app/src/setup.js @@ -0,0 +1,8 @@ +import Vue from 'vue' +import Setup from './views/Setup.vue' +import router from './router/router-setup' + +new Vue({ + router, + render: h => h(Setup) +}).$mount('#app') \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/store.js b/platform/linto-admin/vue_app/src/store.js new file mode 100644 index 0000000..51f708f --- /dev/null +++ b/platform/linto-admin/vue_app/src/store.js @@ -0,0 +1,614 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import axios from 'axios' + +Vue.use(Vuex) + +export default new Vuex.Store({ + strict: false, + state: { + multiUserApplications: '', + deviceApplications: '', + androidUsers: '', + staticClients: '', + sttServices: '', + sttLanguageModels: '', + sttAcousticModels: '', + tockApplications: '', + webappHosts: '', + nodeRedCatalogue: '', + installedNodes: '', + localSkills: '' + }, + mutations: { + SET_MULTI_USER_APPLICATIONS: (state, data) => { + state.multiUserApplications = data + }, + SET_DEVICE_APPLICATIONS: (state, data) => { + state.deviceApplications = data + }, + SET_STATIC_CLIENTS: (state, data) => { + state.staticClients = data + }, + SET_STT_SERVICES: (state, data) => { + state.sttServices = data + }, + SET_STT_LANG_MODELS: (state, data) => { + state.sttLanguageModels = data + }, + SET_STT_AC_MODELS: (state, data) => { + state.sttAcousticModels = data + }, + SET_TOCK_APPS: (state, data) => { + state.tockApplications = data + }, + SET_ANDROID_USERS: (state, data) => { + state.androidUsers = data + }, + SET_WEB_APP_HOSTS: (state, data) => { + state.webappHosts = data + }, + SET_NODERED_CATALOGUE: (state, data) => { + state.nodeRedCatalogue = data + }, + SET_INSTALLED_NODES: (state, data) => { + state.installedNodes = data + }, + SET_LOCAL_SKILLS: (state, data) => { + state.localSkills = data + } + }, + actions: { + // Static clients + getStaticClients: async({ commit, state }) => { + try { + const getStaticClients = await axios.get(`${process.env.VUE_APP_URL}/api/clients/static`) + commit('SET_STATIC_CLIENTS', getStaticClients.data) + return state.staticClients + } catch (error) { + return { error: 'Error on getting Linto(s) static devices' } + } + }, + // Device applications + getDeviceApplications: async({ commit, state }) => { + try { + const getDeviceApplications = await axios.get(`${process.env.VUE_APP_URL}/api/workflows/static`) + let allDeviceApplications = getDeviceApplications.data + if (allDeviceApplications.length > 0) { + allDeviceApplications.map(sw => { + if (!!sw.flow && !!sw.flow.configs && sw.flow.configs.length > 0) { + // get STT service + let nodeSttConfig = sw.flow.configs.filter(node => node.type === 'linto-config-transcribe') + let nodeChatbotConfig = sw.flow.configs.filter(node => node.type === 'linto-config-chatbot') + if (nodeSttConfig.length > 0 && !!nodeSttConfig[0].commandOffline) { + sw.featureServices = { + cmd: nodeSttConfig[0].commandOffline, + lvOnline: nodeSttConfig[0].largeVocabStreaming, + streamingInternal: nodeSttConfig[0].largeVocabStreamingInternal, + chatbot: nodeChatbotConfig[0].rest + } + } else { + sw.featureServices = { + cmd: '', + lvOnline: '', + streamingInternal: '', + chatbot: '' + } + } + } + }) + } + commit('SET_DEVICE_APPLICATIONS', allDeviceApplications) + return state.deviceApplications + } catch (error) { + return { error: 'Error on getting static workflows' } + } + }, + // Multi-user applications + getMultiUserApplications: async({ commit, state }) => { + try { + const getMultiUserApplications = await axios.get(`${process.env.VUE_APP_URL}/api/workflows/application`) + let allMultiUserApplications = getMultiUserApplications.data + if (allMultiUserApplications.length > 0) { + allMultiUserApplications.map(sw => { + if (!!sw.flow && !!sw.flow.configs && sw.flow.configs.length > 0) { + // get STT service + let nodeSttConfig = sw.flow.configs.filter(node => node.type === 'linto-config-transcribe') + let nodeChatbotConfig = sw.flow.configs.filter(node => node.type === 'linto-config-chatbot') + if (nodeSttConfig.length > 0 && nodeChatbotConfig.length > 0) { + sw.featureServices = { + cmd: nodeSttConfig[0].commandOffline, + lvOnline: nodeSttConfig[0].largeVocabStreaming, + streamingInternal: nodeSttConfig[0].largeVocabStreamingInternal, + chatbot: nodeChatbotConfig[0].rest + } + } else { + sw.featureServices = { + cmd: '', + lvOnline: '', + streamingInternal: '', + chatbot: '' + } + } + } + }) + } + commit('SET_MULTI_USER_APPLICATIONS', allMultiUserApplications) + return state.multiUserApplications + } catch (error) { + return { error: 'Error on getting Linto(s) static devices' } + } + }, + // Android users + getAndroidUsers: async({ commit, state }) => { + try { + const getAndroidUsers = await axios.get(`${process.env.VUE_APP_URL}/api/androidusers`) + let nestedObj = [] + getAndroidUsers.data.map(user => { + nestedObj.push({ + _id: user._id, + email: user.email, + applications: user.applications + }) + }) + commit('SET_ANDROID_USERS', nestedObj) + return state.androidUsers + } catch (error) { + return { error: 'Error on getting android applications users' } + } + }, + // Web app hosts + getWebappHosts: async({ commit, state }) => { + try { + const getWebappHosts = await axios.get(`${process.env.VUE_APP_URL}/api/webapphosts`) + commit('SET_WEB_APP_HOSTS', getWebappHosts.data) + return state.webappHosts + } catch (error) { + return { error: 'Error on getting web app hosts' } + } + }, + // Stt services + getSttServices: async({ commit, state }) => { + try { + const getServices = await axios.get(`${process.env.VUE_APP_URL}/api/stt/services`) + if (!!getServices.data.status && getServices.data.status === 'error') { + throw getServices.datagetTockApplications.data.msg + } + commit('SET_STT_SERVICES', getServices.data) + return state.sttServices + } catch (error) { + return { error: 'Error on getting STT services' } + } + }, + // Stt language models + getSttLanguageModels: async({ commit, state }) => { + try { + const getSttLanguageModels = await axios.get(`${process.env.VUE_APP_URL}/api/stt/langmodels`) + if (!!getSttLanguageModels.data.status && getSttLanguageModels.data.status === 'error') { + throw getSttLanguageModels.data.msg + } + commit('SET_STT_LANG_MODELS', getSttLanguageModels.data) + return state.sttLanguageModels + } catch (error) { + return { error: 'Error on getting language models' } + } + }, + // Stt acoustic models + getSttAcousticModels: async({ commit, state }) => { + try { + const getSttAcousticModels = await axios.get(`${process.env.VUE_APP_URL}/api/stt/acmodels`) + if (!!getSttAcousticModels.data.status && getSttAcousticModels.data.status === 'error') { + throw getSttAcousticModels.data.msg + } + commit('SET_STT_AC_MODELS', getSttAcousticModels.data) + return state.sttAcousticModels + } catch (error) { + return { error: 'Error on getting acoustic models' } + } + }, + // Tock applications + getTockApplications: async({ commit, state }) => { + try { + const getApps = await axios.get(`${process.env.VUE_APP_URL}/api/tock/applications`) + if (getApps.data.status === 'error') { + throw getApps.data.msg + } + let applications = [] + if (getApps.data.length > 0) { + getApps.data.map(app => { + applications.push({ + name: app.name, + namespace: app.namespace + }) + }) + commit('SET_TOCK_APPS', applications) + return state.tockApplications + } else { + // If no service is created< + commit('SET_TOCK_APPS', []) + return state.tockApplications + } + } catch (error) { + return { error: 'Error on getting tock applications' } + } + }, + // Node red catalogue + getNodeRedCatalogue: async({ commit, state }) => { + try { + const getCatalogue = await axios.get('https://registry.npmjs.com/-/v1/search?text=linto-ai&size=500') + + let lintoNodes = [] + const unwantedSkills = '@linto-ai/node-red-linto-skill' + if (getCatalogue.status === 200 && !!getCatalogue.data && getCatalogue.data.objects.length > 0) { + lintoNodes = getCatalogue.data.objects.filter(node => (node.package.name.indexOf('@linto-ai/node-red-linto') >= 0 || node.package.name.indexOf('@linto-ai/linto-skill') >= 0) && node.package.name.indexOf(unwantedSkills) < 0) + } + commit('SET_NODERED_CATALOGUE', lintoNodes) + + return state.nodeRedCatalogue + } catch (error) { + return { error } + } + }, + getInstalledNodes: async({ commit, state }) => { + try { + const getNodes = await axios.get(`${process.env.VUE_APP_URL}/api/flow/nodes`) + if (getNodes.status === 200 && !!getNodes.data.nodes) { + commit('SET_INSTALLED_NODES', getNodes.data.nodes) + return state.installedNodes + } else { + return [] + } + } catch (error) { + console.error(error) + return { error } + } + }, + + getLocalSkills: async({ commit, state }) => { + try { + const getLocalSkills = await axios.get(`${process.env.VUE_APP_URL}/api/localskills`) + commit('SET_LOCAL_SKILLS', getLocalSkills.data) + return state.localSkills + } catch (error) { + return { error: 'Error on getting local skills' } + } + }, + }, + getters: { + STT_SERVICES_AVAILABLE: (state) => { + try { + let services = state.sttServices || [] + let languageModels = state.sttLanguageModels || [] + let servicesCMD = [] + let serviceLVOnline = [] + let serviceLVOffline =   [] + let generating = [] + generating['cmd'] = [] + generating['lvOffline'] = [] + generating['lvOnline'] = [] + let allServicesNames = [] + if (services.length > 0) { + services.map(s => { + allServicesNames.push(s.serviceId) + if (languageModels.length > 0) { + let lm = languageModels.filter(l => l.modelId === s.LModelId) + if (lm.length > 0) { + // in generation progress + if (lm[0].updateState > 0) { + if (lm[0].type === 'cmd') { + generating['cmd'].push({ + ...s, + langModel: lm[0] + }) + } else if (lm[0].type === 'lvcsr') { + if (s.tag === 'online') { + generating['lvOnline'].push({ + ...s, + langModel: lm[0] + }) + } else if (s.tag === 'offline') { + generating['lvOffline'].push({ + ...s, + langModel: lm[0] + }) + } + } + } + // Available services + else if (lm[0].isGenerated === 1 || lm[0].isDirty === 1 && lm[0].isGenerated === 0 && lm[0].updateState >= 0) { + if (lm[0].type === 'cmd') { + servicesCMD.push({ + ...s, + langModel: lm[0] + }) + } else if (lm[0].type === 'lvcsr') { + if (s.tag === 'online') { + serviceLVOnline.push({ + ...s, + langModel: lm[0] + }) + } else if (s.tag === 'offline') { + serviceLVOffline.push({ + ...s, + langModel: lm[0] + }) + } + } + } + } + } else  { + return [] + } + }) + const availableServices = { + cmd: servicesCMD, + lvOnline: serviceLVOnline, + lvOffline: serviceLVOffline, + generating, + allServicesNames + } + return availableServices + } else { + return [] + } + } catch (error) { + return { error } + } + }, + STATIC_CLIENTS_AVAILABLE: (state) => { + try { + if (!!state.staticClients && state.staticClients.length > 0) { + return state.staticClients.filter(sc => sc.associated_workflow === null) + } else { + return [] + } + } catch (error) { + return { error } + } + }, + STATIC_CLIENTS_ENROLLED: (state) => { + try { + if (!!state.staticClients && state.staticClients.length > 0) { + return state.staticClients.filter(sc => sc.associated_workflow !== null) + } else { + return [] + } + } catch (error) { + return { error } + } + }, + STATIC_CLIENT_BY_SN: (state) => (sn) => { + try { + if (!!state.staticClients && state.staticClients.length > 0) { + const client = state.staticClients.filter(sc => sc.sn === sn) + return client[0] + } else  { + return [] + } + } catch (error) { + return { error } + } + }, + STATIC_WORKFLOW_BY_ID: (state) => (id) => { + try { + if (!!state.deviceApplications && state.deviceApplications.length > 0) { + const workflow = state.deviceApplications.filter(sw => sw._id === id) + let resp = workflow[0] + let sttServices =   {} + if (!!resp.flow && !!resp.flow.configs && resp.flow.configs.length > 0) { + // get STT service + let nodeSttConfig = resp.flow.configs.filter(node => node.type === 'linto-config-transcribe') + if (nodeSttConfig.length > 0 && !!nodeSttConfig[0].commandOffline) { + sttServices = { + cmd: nodeSttConfig[0].commandOffline, + lvOnline: nodeSttConfig[0].largeVocabStreaming, + lvOffline: nodeSttConfig[0].largeVocabOffline + } + } + } + resp.sttServices = sttServices + return resp + } + return [] + } catch (error) { + return { error } + } + }, + STATIC_WORKFLOWS_BY_CLIENTS: (state) => { + try { + let wfByClients = [] + if (!!state.staticClients && state.staticClients.length > 0) { + const associatedClients = state.staticClients.filter(sc => sc.associated_workflow !== null) + + if (associatedClients.length > 0 && state.deviceApplications.length > 0) { + associatedClients.map(ac => { + if (!wfByClients[ac._id]) { + wfByClients[ac._id] = state.deviceApplications.filter(sw => sw._id === ac.associated_workflow._id)[0] + } + }) + } + } + return wfByClients + } catch (error) { + return { error } + } + }, + ANDROID_USERS_BY_APPS: (state) => { + try { + const users = state.androidUsers + let usersByApp = [] + + if (users.length > 0) { + users.map(user => { + user.applications.map(app => { + if (!usersByApp[app]) { + usersByApp[app] = [user.email] + } else { + usersByApp[app].push(user.email) + } + }) + }) + } + return usersByApp + } catch (error) { + return { error } + } + }, + ANDROID_USERS_BY_APP_ID: (state) => (workflowId) => { + try { + if (!!state.androidUsers && state.androidUsers.length > 0) { + const users = state.androidUsers + return users.filter(user => user.applications.indexOf(workflowId) >= 0) + } + return [] + } catch (error) { + return { error } + } + }, + ANDROID_USER_BY_ID: (state) => (userId) => { + try { + if (!!state.androidUsers && state.androidUsers.length > 0) { + const users = state.androidUsers + const user = users.filter(user => user._id.indexOf(userId) >= 0) + return user[0] + } + return [] + } catch (error) { + return { error } + } + }, + APP_WORKFLOW_BY_ID: (state) => (workflowId) => { + try { + if (!!state.multiUserApplications && state.multiUserApplications.length > 0) { + const workflows = state.multiUserApplications + const workflow = workflows.filter(wf => wf._id === workflowId) + if (workflow.length > 0) { + let resp = workflow[0] + let sttServices =   {} + if (!!resp.flow && !!resp.flow.configs && resp.flow.configs.length > 0) { + // get STT service + let nodeSttConfig = resp.flow.configs.filter(node => node.type === 'linto-config-transcribe') + if (nodeSttConfig.length > 0 && !!nodeSttConfig[0].commandOffline) { + sttServices = { + cmd: nodeSttConfig[0].commandOffline, + lvOnline: nodeSttConfig[0].largeVocabStreaming, + lvOffline: nodeSttConfig[0].largeVocabOffline + } + } + } + resp.sttServices = sttServices + return resp + } + return workflow[0] + } + return [] + } catch (error) { + return { error } + } + }, + WEB_APP_HOST_BY_ID: (state) => (id) => { + try { + if (!!state.webappHosts && state.webappHosts.length > 0) { + const webappHosts = state.webappHosts + const webappHost = webappHosts.filter(wh => wh._id === id) + return webappHost[0] + } + return [] + } catch (error) { + return { error } + } + }, + WEB_APP_HOST_BY_APP_ID: (state) => (workflowId) => { + try { + if (!!state.webappHosts && state.webappHosts.length > 0) { + let hosts = state.webappHosts + let webappHostsById = [] + hosts.map(host => { + host.applications.map(app => { + if (app.applicationId.indexOf(workflowId) >= 0) { + webappHostsById.push(host) + } + }) + }) + return webappHostsById + } + return [] + } catch (error) { + return { error } + } + }, + WEB_APP_HOST_BY_APPS: (state) => { + try { + let hostByApp = [] + if (!!state.webappHosts && state.webappHosts.length > 0) { + const webappHosts = state.webappHosts + if (webappHosts.length > 0) { + webappHosts.map(host => { + host.applications.map(app => { + if (!hostByApp[app.applicationId]) { + hostByApp[app.applicationId] = [host.originUrl] + } else { + hostByApp[app.applicationId].push(host.originUrl) + } + }) + }) + } + } + return hostByApp + } catch (error) { + return { error } + } + }, + APP_WORKFLOWS_NAME_BY_ID: (state) => { + try { + if (!!state.multiUserApplications && state.multiUserApplications.length > 0) { + const workflows = state.multiUserApplications + let workflowNames = [] + if (workflows.length > 0) { + workflows.map(wf => { + workflowNames[wf._id] = { + name: wf.name, + description: wf.description + } + }) + } + return workflowNames + } + return [] + } catch (error) { + return { error } + } + }, + LINTO_SKILLS_INSTALLED: (state) => { + try { + const allNodes = state.installedNodes + let lintoNodes = [] + let lintoModules = [] + lintoNodes = allNodes.filter(node => node.id.indexOf('@linto-ai/') >= 0 && (node.id !== '@linto-ai/node-red-linto-core' && node.version !== '0.0.6')) + if (lintoNodes.length > 0) { + lintoNodes.map(node => { + if (lintoModules.length > 0) { + let moduleExist = lintoModules.findIndex(mod => mod.module === node.module) + if (moduleExist < 0) { + lintoModules.push({ + module: node.module, + version: node.version, + local: node.local + }) + } + } else  { + lintoModules.push({ + module: node.module, + version: node.version, + local: node.local + }) + } + }) + } + return lintoModules + } catch (error) { + return { error } + } + } + } +}) \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/404.vue b/platform/linto-admin/vue_app/src/views/404.vue new file mode 100644 index 0000000..7a7fcba --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/404.vue @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/DeviceAppDeploy.vue b/platform/linto-admin/vue_app/src/views/DeviceAppDeploy.vue new file mode 100644 index 0000000..67c1129 --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/DeviceAppDeploy.vue @@ -0,0 +1,655 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/DeviceAppWorkflowEditor.vue b/platform/linto-admin/vue_app/src/views/DeviceAppWorkflowEditor.vue new file mode 100644 index 0000000..32d4fc4 --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/DeviceAppWorkflowEditor.vue @@ -0,0 +1,166 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/DeviceApps.vue b/platform/linto-admin/vue_app/src/views/DeviceApps.vue new file mode 100644 index 0000000..27b825a --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/DeviceApps.vue @@ -0,0 +1,218 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/Domains.vue b/platform/linto-admin/vue_app/src/views/Domains.vue new file mode 100644 index 0000000..9bc60fc --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/Domains.vue @@ -0,0 +1,150 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/Login.vue b/platform/linto-admin/vue_app/src/views/Login.vue new file mode 100644 index 0000000..f42fd8a --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/Login.vue @@ -0,0 +1,116 @@ + + diff --git a/platform/linto-admin/vue_app/src/views/MultiUserAppDeploy.vue b/platform/linto-admin/vue_app/src/views/MultiUserAppDeploy.vue new file mode 100644 index 0000000..ff66e54 --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/MultiUserAppDeploy.vue @@ -0,0 +1,575 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/MultiUserAppWorkflowEditor.vue b/platform/linto-admin/vue_app/src/views/MultiUserAppWorkflowEditor.vue new file mode 100644 index 0000000..df76950 --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/MultiUserAppWorkflowEditor.vue @@ -0,0 +1,166 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/MultiUserApps.vue b/platform/linto-admin/vue_app/src/views/MultiUserApps.vue new file mode 100644 index 0000000..c847440 --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/MultiUserApps.vue @@ -0,0 +1,242 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/Setup.vue b/platform/linto-admin/vue_app/src/views/Setup.vue new file mode 100644 index 0000000..33f2838 --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/Setup.vue @@ -0,0 +1,114 @@ + + diff --git a/platform/linto-admin/vue_app/src/views/SkillsManager.vue b/platform/linto-admin/vue_app/src/views/SkillsManager.vue new file mode 100644 index 0000000..baf282b --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/SkillsManager.vue @@ -0,0 +1,504 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/Terminals.vue b/platform/linto-admin/vue_app/src/views/Terminals.vue new file mode 100644 index 0000000..a2e7a7d --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/Terminals.vue @@ -0,0 +1,215 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/TerminalsMonitoring.vue b/platform/linto-admin/vue_app/src/views/TerminalsMonitoring.vue new file mode 100644 index 0000000..8dde42b --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/TerminalsMonitoring.vue @@ -0,0 +1,373 @@ + + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/src/views/TockView.vue b/platform/linto-admin/vue_app/src/views/TockView.vue new file mode 100644 index 0000000..68ce585 --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/TockView.vue @@ -0,0 +1,65 @@ + + diff --git a/platform/linto-admin/vue_app/src/views/Users.vue b/platform/linto-admin/vue_app/src/views/Users.vue new file mode 100644 index 0000000..16cfd4a --- /dev/null +++ b/platform/linto-admin/vue_app/src/views/Users.vue @@ -0,0 +1,154 @@ + + \ No newline at end of file diff --git a/platform/linto-admin/vue_app/vue.config.js b/platform/linto-admin/vue_app/vue.config.js new file mode 100644 index 0000000..3b106ce --- /dev/null +++ b/platform/linto-admin/vue_app/vue.config.js @@ -0,0 +1,47 @@ +const path = require('path') + +module.exports = { + configureWebpack: config => { + config.devtool = false, + config.optimization = { + splitChunks: false + } + }, + outputDir: path.resolve(__dirname, '../webserver/dist'), + publicPath: path.resolve(__dirname, '/assets'), + pages: { + setup: { + entry: 'src/setup.js', + template: 'public/default.html', + filename: 'setup.html', + title: 'setup' + + }, + login: { + entry: 'src/login.js', + template: 'public/default.html', + filename: 'login.html', + title: 'login' + + }, + admin: { + entry: 'src/main.js', + template: 'public/index.html', + filename: 'index.html', + title: 'admin' + }, + page404: { + entry: 'src/page404.js', + template: 'public/404.html', + filename: '404.html', + title: '404' + } + + }, + pluginOptions: { + 'style-resources-loader': { + preProcessor: 'scss', + patterns: [path.resolve(__dirname, './public/styles/sass/styles.scss')] + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/wait-for-it.sh b/platform/linto-admin/wait-for-it.sh new file mode 100755 index 0000000..92cbdbb --- /dev/null +++ b/platform/linto-admin/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file diff --git a/platform/linto-admin/webserver/.envdefault b/platform/linto-admin/webserver/.envdefault new file mode 100644 index 0000000..1b1def4 --- /dev/null +++ b/platform/linto-admin/webserver/.envdefault @@ -0,0 +1,44 @@ +TZ=Europe/Paris + +LINTO_STACK_REDIS_SESSION_SERVICE=LINTO_STACK_REDIS_SESSION_SERVICE +LINTO_STACK_REDIS_SESSION_SERVICE_PORT=6379 + +LINTO_STACK_TOCK_SERVICE=LINTO_STACK_TOCK_SERVICE +LINTO_STACK_TOCK_SERVICE_PORT=8080 +LINTO_STACK_TOCK_USER=admin@app.com +LINTO_STACK_TOCK_PASSWORD=password +LINTO_STACK_TOCK_NLP_API=LINTO_STACK_TOCK_NLP_API + +LINTO_STACK_STT_SERVICE_MANAGER_SERVICE=LINTO_STACK_STT_SERVICE_MANAGER_SERVICE +LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN=admin-linto +LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD=aroganil + +LINTO_STACK_DOMAIN=LINTO_STACK_STT_SERVICE_MANAGER_SERVICE +LINTO_STACK_ADMIN_HTTP_PORT=80 +LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS=Domain1,Domain2,Domain3 +LINTO_STACK_USE_SSL=false + +LINTO_STACK_MONGODB_SERVICE=LINTO_STACK_MONGODB_SERVICE +LINTO_STACK_MONGODB_PORT=27017 +LINTO_STACK_MONGODB_DBNAME=LINTO_STACK_MONGODB_DBNAME +LINTO_STACK_MONGODB_USE_LOGIN=true +LINTO_STACK_MONGODB_USER=LINTO_STACK_MONGODB_USER +LINTO_STACK_MONGODB_PASSWORD=LINTO_STACK_MONGODB_PASSWORD +LINTO_STACK_MONGODB_SHARED_SCHEMAS=/schemas +LINTO_STACK_MONGODB_TARGET_VERSION=1 + +LINTO_STACK_MQTT_HOST=LINTO_STACK_MQTT_HOST +LINTO_STACK_MQTT_DEFAULT_HW_SCOPE=blk +LINTO_STACK_MQTT_PORT=1883 +LINTO_STACK_MQTT_USE_LOGIN=true +LINTO_STACK_MQTT_USER=linto +LINTO_STACK_MQTT_PASSWORD=otnil + +LINTO_STACK_BLS_SERVICE=LINTO_STACK_BLS_SERVICE +LINTO_STACK_BLS_USE_LOGIN=true +LINTO_STACK_BLS_USER=LINTO_STACK_BLS_LOGIN +LINTO_STACK_BLS_PASSWORD=LINTO_STACK_BLS_PASSWORD +LINTO_STACK_BLS_SERVICE_UI_PATH=/redui +LINTO_STACK_BLS_SERVICE_API_PATH=/red-nodes + +LINTO_STACK_ADMIN_COOKIE_SECRET=mysecretcookie diff --git a/platform/linto-admin/webserver/app.js b/platform/linto-admin/webserver/app.js new file mode 100644 index 0000000..9d3c014 --- /dev/null +++ b/platform/linto-admin/webserver/app.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const debug = require('debug')('linto-admin:ctl') + +require('./config') + +class Ctl { + constructor() { + this.init() + } + async init() { + try { + this.webServer = await require('./lib/webserver') + this.mqttMonitor = require('./lib/mqtt-monitor')(process.env.LINTO_STACK_MQTT_DEFAULT_HW_SCOPE) + require('./controller/mqtt-http').call(this) + debug(`Application is started - Listening on ${process.env.LINTO_STACK_ADMIN_HTTP_PORT}`) + } catch (error) { + console.error(error) + process.exit(1) + } + } +} + +new Ctl() \ No newline at end of file diff --git a/platform/linto-admin/webserver/config.js b/platform/linto-admin/webserver/config.js new file mode 100644 index 0000000..d01288c --- /dev/null +++ b/platform/linto-admin/webserver/config.js @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2017 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const debug = require('debug')('linto-admin:config') +const dotenv = require('dotenv') +const fs = require('fs') + +function ifHasNotThrow(element, error) { + if (!element) throw error + return element +} + +function ifHas(element, defaultValue) { + if (!element) return defaultValue + return element +} + +function configureDefaults() { + try { + + dotenv.config() + const envdefault = dotenv.parse(fs.readFileSync('.envdefault')) + + // Global + process.env.NODE_ENV = ifHas(process.env.NODE_ENV, envdefault.NODE_ENV) + process.env.TZ = ifHas(process.env.TZ, envdefault.TZ) + + // Webserver + process.env.LINTO_STACK_ADMIN_HTTP_PORT = ifHas(process.env.LINTO_STACK_ADMIN_HTTP_PORT, envdefault.LINTO_STACK_ADMIN_HTTP_PORT) + process.env.LINTO_STACK_DOMAIN = ifHas(process.env.LINTO_STACK_DOMAIN, envdefault.LINTO_STACK_DOMAIN) + process.env.LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS = ifHas(process.env.LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS, envdefault.LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS) + process.env.LINTO_STACK_ADMIN_COOKIE_SECRET = ifHas(process.env.LINTO_STACK_ADMIN_COOKIE_SECRET, envdefault.LINTO_STACK_ADMIN_COOKIE_SECRET) + process.env.LINTO_STACK_USE_SSL = ifHas(process.env.LINTO_STACK_USE_SSL, envdefault.LINTO_STACK_USE_SSL) + + // BLS + process.env.LINTO_STACK_BLS_SERVICE = ifHas(process.env.LINTO_STACK_BLS_SERVICE, envdefault.LINTO_STACK_BLS_SERVICE) + process.env.LINTO_STACK_BLS_USE_LOGIN = ifHas(process.env.LINTO_STACK_BLS_USE_LOGIN, envdefault.LINTO_STACK_BLS_USE_LOGIN) + process.env.LINTO_STACK_BLS_USER = ifHas(process.env.LINTO_STACK_BLS_USER, envdefault.LINTO_STACK_BLS_USER) + process.env.LINTO_STACK_BLS_PASSWORD = ifHas(process.env.LINTO_STACK_BLS_PASSWORD, envdefault.LINTO_STACK_BLS_PASSWORD) + LINTO_STACK_BLS_SERVICE_UI_PATH = ifHas(process.env.LINTO_STACK_BLS_SERVICE_UI_PATH, envdefault.LINTO_STACK_BLS_SERVICE_UI_PATH) + LINTO_STACK_BLS_SERVICE_API_PATH = '/red' + + // Mqtt + process.env.LINTO_STACK_MQTT_HOST = ifHas(process.env.LINTO_STACK_MQTT_HOST, envdefault.LINTO_STACK_MQTT_HOST) + process.env.LINTO_STACK_MQTT_PORT = ifHas(process.env.LINTO_STACK_MQTT_PORT, envdefault.LINTO_STACK_MQTT_PORT) + process.env.LINTO_STACK_MQTT_USER = ifHas(process.env.LINTO_STACK_MQTT_USER, envdefault.LINTO_STACK_MQTT_USER) + process.env.LINTO_STACK_MQTT_PASSWORD = ifHas(process.env.LINTO_STACK_MQTT_PASSWORD, envdefault.LINTO_STACK_MQTT_PASSWORD) + process.env.LINTO_STACK_MQTT_USE_LOGIN = ifHas(process.env.LINTO_STACK_MQTT_USE_LOGIN, envdefault.LINTO_STACK_MQTT_USE_LOGIN) + process.env.LINTO_STACK_MQTT_DEFAULT_HW_SCOPE = ifHas(process.env.LINTO_STACK_MQTT_DEFAULT_HW_SCOPE, envdefault.LINTO_STACK_MQTT_DEFAULT_HW_SCOPE) + + // Database (mongodb) + process.env.LINTO_STACK_MONGODB_DBNAME = ifHas(process.env.LINTO_STACK_MONGODB_DBNAME, envdefault.LINTO_STACK_MONGODB_DBNAME) + process.env.LINTO_STACK_MONGODB_SERVICE = ifHas(process.env.LINTO_STACK_MONGODB_SERVICE, envdefault.LINTO_STACK_MONGODB_SERVICE) + process.env.LINTO_STACK_MONGODB_PORT = ifHas(process.env.LINTO_STACK_MONGODB_PORT, envdefault.LINTO_STACK_MONGODB_PORT) + process.env.LINTO_STACK_MONGODB_USE_LOGIN = ifHas(process.env.LINTO_STACK_MONGODB_USE_LOGIN, envdefault.LINTO_STACK_MONGODB_USE_LOGIN) + process.env.LINTO_STACK_MONGODB_USER = ifHas(process.env.LINTO_STACK_MONGODB_USER, envdefault.LINTO_STACK_MONGODB_USER) + process.env.LINTO_STACK_MONGODB_PASSWORD = ifHas(process.env.LINTO_STACK_MONGODB_PASSWORD, envdefault.LINTO_STACK_MONGODB_PASSWORD) + process.env.LINTO_STACK_MONGODB_TARGET_VERSION = ifHas(process.env.LINTO_STACK_MONGODB_TARGET_VERSION, envdefault.LINTO_STACK_MONGODB_TARGET_VERSION) + + // Redis + process.env.LINTO_STACK_REDIS_SESSION_SERVICE_PORT = ifHas(process.env.LINTO_STACK_REDIS_SESSION_SERVICE_PORT, envdefault.LINTO_STACK_REDIS_SESSION_SERVICE_PORT) + process.env.LINTO_STACK_REDIS_SESSION_SERVICE = ifHas(process.env.LINTO_STACK_REDIS_SESSION_SERVICE, envdefault.LINTO_STACK_REDIS_SESSION_SERVICE) + + // NLU - TOCK + process.env.LINTO_STACK_TOCK_SERVICE = ifHas(process.env.LINTO_STACK_TOCK_SERVICE, envdefault.LINTO_STACK_TOCK_SERVICE) + process.env.LINTO_STACK_TOCK_NLP_API = ifHas(process.env.LINTO_STACK_TOCK_NLP_API, envdefault.LINTO_STACK_TOCK_NLP_API) + process.env.LINTO_STACK_TOCK_SERVICE_PORT = ifHas(process.env.LINTO_STACK_TOCK_SERVICE_PORT, envdefault.LINTO_STACK_TOCK_SERVICE_PORT) + process.env.LINTO_STACK_TOCK_BASEHREF = ifHas(process.env.LINTO_STACK_TOCK_BASEHREF, envdefault.LINTO_STACK_TOCK_BASEHREF) + process.env.LINTO_STACK_TOCK_BASEHREF = process.env.LINTO_STACK_TOCK_BASEHREF === 'undefined' ? '' : process.env.LINTO_STACK_TOCK_BASEHREF + + process.env.LINTO_STACK_TOCK_LOGIN = ifHas(process.env.LINTO_STACK_TOCK_LOGIN, envdefault.LINTO_STACK_TOCK_LOGIN) + process.env.LINTO_STACK_TOCK_PASSWORD = ifHas(process.env.LINTO_STACK_TOCK_PASSWORD, envdefault.LINTO_STACK_TOCK_PASSWORD) + + // STT service-manager + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE) + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN) + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + + } catch (e) { + console.error(debug.namespace, e) + process.exit(1) + } +} +module.exports = configureDefaults() \ No newline at end of file diff --git a/platform/linto-admin/webserver/controller/mqtt-http/index.js b/platform/linto-admin/webserver/controller/mqtt-http/index.js new file mode 100644 index 0000000..409dd1a --- /dev/null +++ b/platform/linto-admin/webserver/controller/mqtt-http/index.js @@ -0,0 +1,48 @@ +const debug = require('debug')(`linto-admin:mqtt-http`) + +module.exports = function() { + this.mqttMonitor.client.on('mqtt-monitor::message', async(payload) => { + debug(payload) + const toNotify = ['pong', 'status', 'muteack', 'unmuteack', 'notify_app'] // Array of messages that are to be notified on front + const msgType = payload.topicArray[3] + + if (toNotify.indexOf(msgType) >= 0) { + this.webServer.ioHandler.notify(msgType, payload) + } + }) + + this.webServer.ioHandler.on('linto_subscribe', (data) => { + this.mqttMonitor.subscribe(data) + }) + + this.webServer.ioHandler.on('linto_subscribe_all', (data) => { + this.mqttMonitor.subscribe({}) + }) + + this.webServer.ioHandler.on('linto_unsubscribe_all', (data) => { + this.mqttMonitor.unsubscribe({}) + }) + + this.webServer.ioHandler.on('linto_ping', (data) => { + this.mqttMonitor.ping(data) + }) + + this.webServer.ioHandler.on('linto_say', (data) => { + this.mqttMonitor.lintoSay(data) + }) + + this.webServer.ioHandler.on('linto_volume', (data) => { + this.mqttMonitor.setVolume(data) + }) + + this.webServer.ioHandler.on('linto_volume_end', (data) => { + this.mqttMonitor.setVolumeEnd(data) + }) + this.webServer.ioHandler.on('linto_mute', (data) => { + this.mqttMonitor.mute(data) + }) + this.webServer.ioHandler.on('linto_unmute', (data) => { + this.mqttMonitor.unmute(data) + }) + +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/doc/swagger.json b/platform/linto-admin/webserver/doc/swagger.json new file mode 100644 index 0000000..e4dfa25 --- /dev/null +++ b/platform/linto-admin/webserver/doc/swagger.json @@ -0,0 +1,1990 @@ +{ + "swagger": "2.0", + "info": { + "description": "", + "version": "0.0.2", + "title": "Linto admin API documentation", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "contact@linto.ai" + } + }, + "host": "linto.ai/api", + "tags": [{ + "name": "android_users", + "description": "Operations on \"android_users\" collection" + }, + { + "name": "client_static", + "description": "Operations on \"client_static\" collection" + }, + { + "name": "flow", + "description": "Operations on nodered flows" + }, + { + "name": "stt", + "description": "Operations on STT service manager" + }, + { + "name": "tock", + "description": "Operations on TOCK service" + }, + { + "name": "workflows", + "description": "Operations on static and application workflows" + }, + { + "name": "workflows_applications", + "description": "Operations on application workflows" + }, + { + "name": "workflows_static", + "description": "Operations on static workflows" + }, + { + "name": "workflows_templates", + "description": "Operations on workflows templates" + } + ], + "schemes": [ + "https", + "http" + ], + "paths": { + "/api/androidusers": { + "get": { + "tags": [ + "android_users" + ], + "summary": "Get all andoird users", + "operationId": "getAllAndroidUsers", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "applications": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "post": { + "tags": [ + "android_users" + ], + "summary": "Add a new andoird users", + "operationId": "addAndroidUsers", + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "pswd": { + "type": "string" + }, + "applications": { + "type": "array", + "items": { + "type": "string" + } + } + } + + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/androidusers/applications": { + "patch": { + "tags": [ + "android_users" + ], + "summary": "Remove an application for all android users", + "operationId": "removeApplicationFromAndroidUsers", + "parameters": [{ + "name": "payload", + "in": "body", + "required": "true", + "schema": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + + } + } + }, + "/api/androidusers/{userId}/applications": { + "put": { + "tags": [ + "android_users" + ], + "summary": "Register an android user to an application", + "operationId": "AddApplicationToAndroidUser", + "parameters": [{ + "name": "userId", + "in": "path", + "description": "Android user id", + "required": true, + "type": "string" + }, { + "name": "payload", + "in": "body", + "description": "Array of application to add to android user", + "required": true, + "schema": { + "type": "object", + "properties": { + "applications": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["success", "error"], + "required": true + }, + "msg": { + "type": "string", + "required": true + }, + "error": { + "type": "object", + "required": false + } + } + } + } + } + } + }, + "/api/androidusers/{userId}/applications/{applicationId}/remove": { + "patch": { + "tags": [ + "android_users" + ], + "summary": "Dissociate an android user from an android application", + "operationId": "RemoveApplicationFromAndroidUser", + "parameters": [{ + "name": "userId", + "in": "path", + "description": "Android user id", + "required": true, + "type": "string" + }, { + "name": "applicationId", + "in": "path", + "description": "Application workflow id to remove", + "required": true, + "type": "string" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/androidusers/{userId}": { + "get": { + "tags": [ + "android_users" + ], + "summary": "Get an android user by its id", + "operationId": "GetAndroidUserById", + "parameters": [{ + "name": "userId", + "in": "path", + "description": "Android user id", + "required": true, + "type": "string" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "applications": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "android_users" + ], + "summary": "Delete an android user by its id", + "operationId": "deleteAndroidUser", + "parameters": [{ + "name": "userId", + "in": "path", + "description": "Android user id", + "required": true, + "type": "string" + }], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/clients/static": { + "get": { + "tags": [ + "client_static" + ], + "summary": "Get all static clients", + "operationId": "GetAllStaticClients", + "produces": [ + "application/json" + ], + + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/client_static" + } + } + } + } + }, + "post": { + "tags": [ + "client_static" + ], + "summary": "Create a new static device", + "operationId": "CreateStaticDevice", + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "payload", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "sn": { + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["success", "error"], + "required": true + }, + "msg": { + "type": "string", + "required": true + }, + "error": { + "type": "object", + "required": false + } + } + } + } + } + } + }, + "/api/clients/static/{serialNumber}": { + "get": { + "tags": [ + "client_static" + ], + "summary": "Get a static client by its Id", + "operationId": "GetStaticClientById", + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "serialNumber", + "in": "path", + "description": "Static client serial number", + "required": true, + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + }, + "patch": { + "tags": [ + "client_static" + ], + "summary": "Update a static client", + "operationId": "UpdateStaticClientById", + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "serialNumber", + "in": "path", + "description": "Static client serial number", + "required": true, + "type": "string" + }, + { + "name": "payload", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + }, + "delete": { + "tags": [ + "client_static" + ], + "summary": "Delete a static client", + "operationId": "deleteStaticClientById", + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "serialNumber", + "in": "path", + "description": "Static client serial number", + "required": true, + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/clients/static/replace": { + "post": { + "tags": [ + "client_static" + ], + "summary": "Replace a static device Serial Number by a target one", + "operationId": "ReplaceStaticDeviceInWorkflow", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "sn": { + "type": "string" + }, + "workflow": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "targetDevice": { + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + + } + }, + "/api/flow/{flowId}": { + "delete": { + "tags": [ + "flow" + ], + "summary": "Delete a flow from nodered api by its flowId", + "operationId": "DeleteFlowFromBLS", + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "flowId", + "in": "query", + "description": "nodered flow id", + "required": true, + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/flow/getAuth": { + "get": { + "tags": [ + "flow" + ], + "summary": "Get bearer token to require nodered API", + "operationId": "GetBLSAuth", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + } + } + } + } + }, + "/api/flow/postbls/static": { + "post": { + "tags": [ + "flow" + ], + "summary": "Publish a static workflow on BLS", + "operationId": "postStaticFlowOnBLS", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "sn": { + "description": "Static client serial number", + "type": "string" + }, + "workflow_name": { + "description": "static workflow name", + "type": "string" + }, + "workflowTemplate": { + "description": "Workflow template name that will be used", + "type": "string" + }, + "sttServiceLanguage": { + "description": "Language of the STT service that will be used", + "type": "string" + }, + "sttService": { + "description": "STT service name that will be used", + "type": "string" + }, + "tockApplicationName": { + "description": "Tock application name that will be used", + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/flow/postbls/application": { + "post": { + "tags": [ + "flow" + ], + "summary": "Publish an application workflow on BLS", + "operationId": "postApplicationFlowOnBLS", + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "payload", + "in": "body", + "description": "static workflow name", + "required": true, + "schema": { + "type": "object", + "properties": { + "workflow_name": { + "description": "static workflow name", + "type": "string" + }, + "workflowTemplate": { + "description": "Workflow template name that will be used", + "type": "string" + }, + "sttServiceLanguage": { + "description": "Language of the STT service that will be used", + "type": "string" + }, + "sttService": { + "description": "STT service name that will be used", + "type": "string" + }, + "tockApplicationName": { + "description": "Tock application name that will be used", + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/flow/sandbox": { + "get": { + "tags": [ + "flow" + ], + "summary": "Get the sandbox flowId from BLS", + "operationId": "GetBLSSandboxId", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "properties": { + "sandBoxId": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "flow" + ], + "summary": "Create a SandBox nodered workflow on BLS", + "operationId": "CreateBLSSandboxId", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/flow/sandbox/load": { + "put": { + "tags": [ + "flow" + ], + "summary": "Load a template in the sandbox flow on BLS", + "operationId": "LoadFlowFromTemplate", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "requried": true, + "schema": { + "type": "object", + "properties": { + "flow": { + "type": "array", + "items": { + "type": "object" + } + }, + "flowId": { + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "properties": { + "sandBoxId": { + "$ref": "#/definitions/server_response" + } + } + } + } + } + } + }, + "/api/flow/tmp": { + "get": { + "tags": [ + "flow" + ], + "summary": "Get the flow object of the sandbox workspace", + "operationId": "GetTmpFlow", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Return the flow object of the sandbox workspace", + "schema": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + }, + "put": { + "tags": [ + "flow" + ], + "summary": "Update the flow object of the sandbox workspace", + "operationId": "UpdateTmpFlow", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "type": "object", + "properties": { + "flow": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "id": { + "type": "string" + }, + "node": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "flowId": { + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "Successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/stt/services": { + "get": { + "tags": [ + "stt" + ], + "summary": "Get all STT services", + "operationId": "GetAllSttServices", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "serviceId": { + "type": "string" + }, + "replicas": { + "type": "integer" + }, + "LModelId": { + "type": "string" + }, + "AModelId": { + "type": "string" + }, + "isOn": { + "type": "integer", + "enum": [0, 1] + }, + "date": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } + } + }, + "/api/stt/langmodels": { + "get": { + "tags": [ + "stt" + ], + "summary": "Get all STT services language models", + "operationId": "GetAllSttServicesLModels", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "modelId": { + "type": "string" + }, + "type": { + "type": "string" + }, + "acmodelId": { + "type": "string" + }, + "entities": { + "type": "array", + "items": { + "type": "string" + } + }, + "intents": { + "type": "array", + "items": { + "type": "string" + } + }, + "lang": { + "type": "string" + }, + "isGenerated": { + "type": "integer", + "enum": [0, 1, -1] + }, + "isDirty": { + "type": "integer" + }, + "updateState": { + "type": "integer" + }, + "updateStatus": { + "type": "string" + }, + "oov": { + "type": "array", + "items": { + "type": "string" + } + }, + "dateGeneration": { + "type": "string", + "format": "date-time" + }, + "dateModification": { + "type": "string", + "format": "date-time" + } + + + } + } + } + } + } + } + }, + "/api/stt/acmodels": { + "get": { + "tags": [ + "stt" + ], + "summary": "Get all STT services acoustic models", + "operationId": "GetAllSttServicesACModels", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "modelId": { + "type": "string" + }, + "lang": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "date": { + "type": "string", + "format": "date-time" + } + } + } + } + } + } + } + }, + "/api/stt/lexicalseeding": { + "post": { + "tags": [ + "stt" + ], + "summary": "Trigger the process of lexical seeding on a STT service", + "operationId": "SttLexicalSeeding", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "flowId": { + "type": "string" + }, + "service_name": { + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/stt/generategraph": { + "post": { + "tags": [ + "stt" + ], + "summary": "Trigger the process of graph generation on a STT service", + "operationId": "SttGenerateGraph", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "serviceName", + "required": true, + "schema": { + "type": "string" + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/tock/applications": { + "get": { + "tags": [ + "tock" + ], + "summary": "Get all tock applications", + "operationId": "GetTockApplications", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "namespace": { + "type": "string", + "enum": ["app"] + }, + "intents": { + "type": "array", + "items": { + "type": "object" + } + }, + "supportedLocales": { + "type": "array" + }, + "nlpEngineType": { + "type": "object" + }, + "mergeEngineTypes": { + "type": "boolean" + }, + "useEntityModels": { + "type": "boolean" + }, + "supportedSubEntites": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/tock/lexicalseeding": { + "post": { + "tags": [ + "tock" + ], + "summary": "Trigger the process of lexical seeding on a Tock application", + "operationId": "SttLexicalSeeding", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "flowId", + "required": true, + "schema": { + "type": "string" + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/workflows/{id}/services": { + "patch": { + "tags": [ + "workflows" + ], + "summary": "Update a static or application workflows parameters (STT, NLU...)", + "operationId": "UpdateWorkflowServices", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "name": "id", + "required": true, + "type": "string", + "description": "Workflow id to be updated" + }, + { + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["static", "application"] + }, + "workflowName": { + "type": "string" + }, + "sttServiceLanguage": { + "type": "string", + "enum": ["fr-FR", "en-US"] + }, + "sttService": { + "type": "string" + }, + "tockApplicationName": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/workflows/saveandpublish": { + "post": { + "tags": [ + "workflows" + ], + "summary": "Save the current workspace object and post it to BLS", + "operationId": "WorkflowSaveAndPublish", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["static", "application"] + }, + "noderedFlowId": { + "type": "string", + "description": "Nodered targeted workspace ID" + }, + "workflowId": { + "type": "string", + "description": "Current workflow (static or application) ID" + }, + "workflowName": { + "type": "string" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/workflows/application": { + "get": { + "tags": [ + "workflows_applications" + ], + "summary": "Get all application workflows", + "operationId": "GetAllApplicationWorkflows", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/workflows_application" + } + + } + } + } + }, + "post": { + "tags": [ + "workflows_applications" + ], + "summary": "Create an application workflows", + "operationId": "CreateApplicationWorkflow", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "workflowName": { + "type": "string" + }, + "workflowDescription": { + "type": "string", + "description": "Description of the workflow" + }, + "workflowTemplate": { + "type": "string" + }, + "sttServiceLanguage": { + "type": "string" + }, + "sttService": { + "type": "string" + }, + "tockApplicationName": { + "type": "string" + } + } + + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/workflows_application" + } + + } + } + } + } + }, + "/api/workflows/application/{id}": { + "get": { + "tags": [ + "workflows_applications" + ], + "summary": "Get an application workflow by its id", + "operationId": "GetApplicationWorkflowById", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "required": true, + "name": "id", + "description": "Application workflow id", + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/workflows_application" + } + } + } + }, + "delete": { + "tags": [ + "workflows_applications" + ], + "summary": "Delete an application workflow by its id", + "operationId": "DeleteApplicationWorkflow", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "required": true, + "name": "workflowId", + "description": "Application workflow id", + "type": "string" + }, + { + "in": "query", + "name": "workflowName", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/workflows_application" + } + } + } + } + }, + "/api/workflows/application/{id}/androidusers": { + "get": { + "tags": [ + "workflows_applications" + ], + "summary": "Get a user list associated to application {id}", + "operationId": "GetAndroidUserByApplication", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "required": true, + "name": "id", + "description": "Application workflow id", + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "applications": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/workflows/static": { + "get": { + "tags": [ + "workflows_static" + ], + "summary": "Get all static worfklows", + "operationId": "GetAllStaticWorkflows", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/workflows_static" + } + } + } + } + }, + "post": { + "tags": [ + "workflows_static" + ], + "summary": "Create a static worfklows", + "operationId": "CreateStaticWorkflows", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "tyep": "object", + "properties": { + "sn": { + "type": "string", + "description": "Serial number of the static device to associate" + }, + "workflowName": { + "type": "string", + "description": "Workflow name and nodered workspace name" + }, + "workflowDescription": { + "type": "string", + "description": "Description of the workflow" + }, + "workflowTemplate": { + "type": "string", + "description": "Name of the workflow template to use" + }, + "sttServiceLanguage": { + "type": "string", + "description": "languague used with STT service", + "enum": ["fr-FR, en-US"] + }, + "sttService": { + "type": "string", + "description": "Name of the STT service to use" + }, + "tockApplicationName": { + "type": "string", + "description": "Tock application name to use" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/server_response" + } + } + } + } + } + }, + "/api/workflows/static/{id}": { + "get": { + "tags": [ + "workflows_static" + ], + "summary": "Get a static worfklow by its ID", + "operationId": "GetStaticWorkflowById", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "name": "id", + "description": "Static workflow id", + "type": "string", + "required": true + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/workflows_static" + } + } + } + }, + "delete": { + "tags": [ + "workflows_static" + ], + "summary": "Delete a static worfklow by its ID", + "operationId": "DeleteStaticWorkflowById", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "name": "id", + "description": "Static workflow id", + "type": "string", + "required": true + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + }, + "/api/workflows/static/name/{name}": { + "get": { + "tags": [ + "workflows_static" + ], + "summary": "Get a static worfklow by its name", + "operationId": "GetStaticWorkflowByName", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "name": "name", + "description": "Static workflow name", + "type": "string", + "required": true + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/workflows_static" + } + } + } + } + }, + "/api/workflows/templates": { + "get": { + "tags": [ + "workflows_templates" + ], + "summary": "Get all workflows templates", + "operationId": "getAllWorkflowTemplates", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/workflows_application" + } + } + } + } + } + }, + "/api/workflows/template/": { + "post": { + "tags": [ + "workflows_templates" + ], + "summary": "Create a new workflow template", + "operationId": "CreateWorkflowTemplate", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "workflowType": { + "type": "string", + "enum": ["static", "application"] + }, + "workflowName": { + "type": "string", + "description": "Name of the template to create" + } + } + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/workflows_static" + } + } + } + } + }, + "/api/workflows/template/{templateId}": { + "delete": { + "tags": [ + "workflows_templates" + ], + "summary": "Delete a workflow template by its ID", + "operationId": "DeleteWorkflowTemplate", + "produces": [ + "application/json" + ], + "parameters": [{ + "in": "path", + "name": "templateId", + "required": true, + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/server_response" + } + } + } + } + } + }, + "definitions": { + "dbversion": { + "type": "object", + "properties": { + "_id": { + "type": "integer" + }, + "id": { + "type": "string", + "required": true, + "enum": ["current_version"] + }, + "version": { + "type": "integer" + } + } + }, + "users": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "userName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "pswdHash": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "android_users": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "pswdHash": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "applications": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "client_static": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "enrolled": { + "type": "boolean" + }, + "connexion": { + "type": "string", + "enum": ["online, offline"] + }, + "last_up": { + "type": "string", + "format": "date-time" + }, + "last_down": { + "type": "string", + "format": "date-time" + }, + "associated_workflow": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "config": { + "type": "object", + "properties": {} + }, + "meetting": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "flow_tmp": { + "type": "object", + "properties": { + "_id": { + "type": "integer" + }, + "id": { + "type": "string", + "required": true, + "enum": ["tmp"] + }, + "flow": { + "type": "array", + "items": { + "type": "object" + } + }, + "workspaceId": { + "type": "string" + } + + } + }, + "workflows_application": { + "type": "object", + "properties": { + "_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + }, + "updated_date": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "nodes": { + "type": "array", + "items": { + "type": "object" + } + }, + "configs": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "workflows_static": { + "type": "object", + "properties": { + "_id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + }, + "updated_date": { + "type": "string", + "format": "date-time" + }, + "associated_device": { + "type": "string" + }, + "flow": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "nodes": { + "type": "array", + "items": { + "type": "object" + } + }, + "configs": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + }, + "workflows_templates": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "types": "string", + "enum": ["static", "application"] + }, + "flow": { + "type": "array", + "items": { + "type": "object" + } + }, + "created_date": { + "type": "string", + "format": "date-time" + } + } + }, + "server_response": { + + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["success", "error"], + "required": true + }, + "msg": { + "type": "string", + "required": true + }, + "error": { + "type": "object", + "required": false + } + } + } + } +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/docker-healthcheck.js b/platform/linto-admin/webserver/docker-healthcheck.js new file mode 100644 index 0000000..b0a6a74 --- /dev/null +++ b/platform/linto-admin/webserver/docker-healthcheck.js @@ -0,0 +1,8 @@ +const request = require('request') + +//La route de healthcheck peut faire des logs pour le service +request(`http://localhost/healthcheck`, error => { + if (error) { + throw error + } +}) \ No newline at end of file diff --git a/platform/linto-admin/webserver/lexicalseeding.js b/platform/linto-admin/webserver/lexicalseeding.js new file mode 100644 index 0000000..bd6f2d7 --- /dev/null +++ b/platform/linto-admin/webserver/lexicalseeding.js @@ -0,0 +1,517 @@ +const axios = require('axios') +const nodered = require('./nodered.js') +const middlewares = require('./index.js') +const fs = require('fs') +var FormData = require('form-data') + +/** + * @desc Execute STT lexical seeding on a flowID, by its service_name + * @param {string} flowId - Id of the flow used on nodered + * @param {string} service_name - Name of the targeted stt service + * @return {object} - {status, msg, error(optional)} + */ + +async function sttLexicalSeeding(flowId, service_name) { + try { + // Get stt service data + const accessToken = await nodered.getBLSAccessToken() + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getSttService = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/service/${service_name}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + const sttService = getSttService.data.data + + // Get lexical seeding data + const getSttLexicalSeeding = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/red/${flowId}/dataset/linstt`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + } + }) + + const sttLexicalSeedingData = getSttLexicalSeeding.data.data + const intents = sttLexicalSeedingData.intents + const entities = sttLexicalSeedingData.entities + let intentsUpdated = false + let entitiesUpdated = false + let updateInt = { success: '', errors: '' } + let updateEnt = { success: '', errors: '' } + + // Update model intents + const intentsToSend = await filterLMData('intent', sttService.LModelId, intents) + if (intentsToSend.data.length > 0) { + updateInt = await updateLangModel(intentsToSend, sttService.LModelId) + if (!!updateInt.success && !!updateInt.errors) { + intentsUpdated = true + } + } else { + intentsUpdated = true + } + + // Update model entities + const entitiesToSend = await filterLMData('entity', sttService.LModelId, entities) + + if (entitiesToSend.data.length > 0) { + updateEnt = await updateLangModel(entitiesToSend, sttService.LModelId) + if (!!updateEnt.success && !!updateEnt.errors) { + entitiesUpdated = true + } + } else { + entitiesUpdated = true + } + if (intentsToSend.data.length > 0 || entitiesToSend.data.length > 0) { + const getUpdatedSttLangModel = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${sttService.LModelId}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + // Generate Graph if model updated + if (getUpdatedSttLangModel.data.data.isDirty === 1) { + try { + await generateGraph(service_name) + } catch (error) { + console.error(error) + } + } + } + + // Result + if (intentsUpdated && entitiesUpdated) { + if (updateInt.errors.length === 0 && updateEnt.errors.length === 0) { + return ({ + status: 'success', + msg: 'Model language has been updated' + }) + } else { + errorMsg = 'Model updated BUT : ' + if (updateInt.errors.length > 0) { + updateInt.errors.map(e => { + errorMsg += `Warning: error on updating intent ${e.name}.` + }) + } + if (updateEnt.errors.length > 0) { + updateEnt.errors.map(e => { + errorMsg += `Warning: error on updating entity ${e.name}.` + }) + } + return ({ + status: 'success', + msg: errorMsg + }) + } + } else { + throw 'Error on updating language model' + } + } catch (error) { + console.error(error) + return ({ + status: 'error', + msg: !!error.msg ? error.msg : error, + error: error + }) + } +} + +/** + * @desc filter language model data/values for lexical seeding + * @param {string} type - "intents" or "entities" + * @param {string} modelId - id of the targeted language model + * @param {object} newData - data/values to be updated + * @return {object} - {type, data(filtered)} + */ +async function filterLMData(type, modelId, newData) { + let getDataroutePath = '' + if (type === 'intent') { + getDataroutePath = 'intents' + } else if (type === 'entity') { + getDataroutePath = 'entities' + } + + // Current Values of the langage model + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getData = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${modelId}/${getDataroutePath}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + let currentData = [] + if (!!getData.data.data) { + currentData = getData.data.data + } + let dataToSend = [] + if (newData.length > 0) { + newData.map(d => { + let toAdd = [] + let toSendMethod = '' + let toCompare = currentData.filter(c => c.name === d.name) + if (toCompare.length === 0) { + toAdd.push(...d.items) + toSendMethod = 'post' + } else { + toSendMethod = 'patch' + d.items.map(val => { + if (toCompare[0]['items'].indexOf(val) < 0) { + toAdd.push(val) + } + }) + } + if (toAdd.length > 0) { + dataToSend.push({ + name: d.name, + items: toAdd, + method: toSendMethod + }) + } + }) + } + return { + type, + data: dataToSend + } +} + +/** + * @desc Execute requests to start generating graph on a service_name language model + * @param {string} service_name - STT service name + */ +async function generateGraph(service_name) { + try { + // get stt service data + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getSttService = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/service/${service_name}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + const sttService = getSttService.data.data + + // Generate graph + const generateGraph = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${sttService.LModelId}/generate/graph`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + + return ({ + status: 'success', + msg: generateGraph.data.data + }) + } catch (error) { + console.error(error) + return ({ + status: 'error', + msg: 'error on generating graph' + }) + } +} + +/** + * @desc Update a langage model with intents/entities object to add/update + * @param {object} payload - data to be updated + * @param {string} modelId - Id of the targeted language model + * @return {object} {errors, success} + */ +async function updateLangModel(payload, modelId) { + try { + let success = [] + let errors = [] + const type = payload.type + for (let i in payload.data) { + const name = payload.data[i].name + const items = payload.data[i].items + const method = payload.data[i].method + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + + const req = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${modelId}/${type}/${name}`, { + method, + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + }, + data: items + }) + + if (req.status === 200 || req.status === '200') { + success.push(payload.data[i]) + } else { + errors.push(payload.data[i]) + } + if (success.length + errors.length === payload.data.length) { + return ({ + errors, + success + }) + } + } + } catch (error) { + console.error(error) + return ('an error has occured') + } +} + +/** + * @desc Execute requests to update NLU application + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ +async function nluLexicalSeedingApplications(flowId) { + try { + // Get lexical seeding object to send to TOCK + const accessToken = await nodered.getBLSAccessToken() + const getNluLexicalSeeding = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/red/${flowId}/dataset/tock`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + } + }) + + // get Tock auth token + const token = middlewares.basicAuthToken(process.env.LINTO_STACK_TOCK_USER, process.env.LINTO_STACK_TOCK_PASSWORD) + + // Tmp json file path + const jsonApplicationContent = `{"application": ${JSON.stringify(getNluLexicalSeeding.data.application)}}` + const appFilePath = process.cwd() + '/public/tockapp.json' + + let postApp = await new Promise((resolve, reject) => { + fs.writeFile(appFilePath, jsonApplicationContent, async(err) => { + if (err) { + console.error(err) + throw err + } else { + const formData = new FormData() + formData.append('file', fs.createReadStream(appFilePath)) + axios({ + url: `${middlewares.useSSL() + process.env.LINTO_STACK_TOCK_SERVICE}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}${process.env.LINTO_STACK_TOCK_BASEHREF}/rest/admin/dump/application`, + method: 'post', + data: formData, + headers: { + 'Authorization': token, + 'Content-Type': formData.getHeaders()['content-type'] + } + }).then((res) => { + //fs.unlinkSync(appFilePath) + if (res.status === 200) { + resolve({ + status: 'success', + msg: 'Tock application has been updated' + }) + } else { + reject({ + status: 'error', + msg: 'Error on updating Tock application' + }) + } + }).catch((error) => { + console.error(error) + reject(error) + }) + } + }) + }) + return postApp + } catch (error) { + return ({ + status: 'error', + msg: 'Error on updating Tock application', + error + }) + } +} + +/** + * @desc Execute requests to update NLU sentences + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ + +async function nluLexicalSeedingSentences(flowId) { + try { + // Get lexical seeding object to send to TOCK + const accessToken = await nodered.getBLSAccessToken() + const getNluLexicalSeeding = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/red/${flowId}/dataset/tock`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + } + }) + + // get Tock auth token + const token = middlewares.basicAuthToken(process.env.LINTO_STACK_TOCK_USER, process.env.LINTO_STACK_TOCK_PASSWORD) + + + // Tmp json file path + const jsonSentencesContent = JSON.stringify(getNluLexicalSeeding.data.sentences) + const sentencesFilePath = process.cwd() + '/public/tocksentences.json' + + let postSentences = await new Promise((resolve, reject) => { + fs.writeFile(sentencesFilePath, jsonSentencesContent, async(err) => { + if (err) { + console.error(err) + throw err + } else { + const formData = new FormData() + formData.append('file', fs.createReadStream(sentencesFilePath)) + axios({ + url: `${middlewares.useSSL() + process.env.LINTO_STACK_TOCK_SERVICE}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}${process.env.LINTO_STACK_TOCK_BASEHREF}/rest/admin/dump/sentences`, + method: 'post', + data: formData, + headers: { + 'Authorization': token, + 'Content-Type': formData.getHeaders()['content-type'] + } + }).then((res) => { + //fs.unlinkSync(sentencesFilePath) + if (res.status === 200) { + resolve({ + status: 'success', + msg: 'Tock application has been updated' + }) + } else { + reject({ + status: 'error', + msg: 'Error on updating Tock application' + }) + } + }).catch((error) => { + console.error(error) + reject(error) + }) + } + }) + }) + return postSentences + } catch (error) { + return ({ + status: 'error', + msg: 'Error on updating Tock application sentences', + error + }) + } +} + +/** + * @desc Tock application lexical seeding + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ +async function nluLexicalSeeding(flowId) { + try { + const postApp = await nluLexicalSeedingApplications(flowId) + const postSentences = await nluLexicalSeedingSentences(flowId) + let errors = [] + let status = 'success' + let postAppValid = true + let postSentencesValid = true + if (postApp.status !== 'success') { + postAppValid = false + status = 'error' + errors.push({ 'application': postApp }) + } + if (postSentences.status !== 'success') { + postSentencesValid = false + status = 'error' + errors.push({ 'sentences': postSentences }) + } + + if (postAppValid && postSentencesValid) { + return ({ + status, + msg: 'NLU updated' + }) + } else { + throw errors + } + } catch (error) { + console.error(error) + return ({ + status: 'error', + msg: 'Error on updating Tock application', + error + }) + } +} + +/** + * @desc Execute functions to strat process of lexical seeding for STT and NLU applications + * @param {string} sttServiceName - name of the targeted STT service + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ +async function doLexicalSeeding(sttServiceName, flowId) { + try { + + // NLU lexical seeding + const nluLexSeed = await nluLexicalSeeding(flowId) + if (nluLexSeed.status !== 'success') { + throw !!nluLexSeed.msg ? nluLexSeed.msg : nluLexSeed + } + // STT lexical seeding + const sttLexSeed = await sttLexicalSeeding(flowId, sttServiceName) + if (sttLexSeed.status !== 'success') { + throw !!sttLexSeed.msg ? sttLexSeed.msg : sttLexSeed + } + // Success + if (sttLexSeed.status === 'success' && nluLexSeed.status === 'success') { + return ({ + status: 'success', + msg: 'Tock application and STT service have been updated' + }) + } else { + throw { + stt: sttLexSeed, + nlu: nluLexSeed + } + } + } catch (error) { + console.error(error) + return ({ + status: 'error', + error, + msg: !!error.msg ? error.msg : 'Error on executing lexical seeding' + }) + } + +} + +module.exports = { + doLexicalSeeding, + nluLexicalSeeding, + sttLexicalSeeding, + generateGraph +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/mqtt-monitor/index.js b/platform/linto-admin/webserver/lib/mqtt-monitor/index.js new file mode 100644 index 0000000..80e4cff --- /dev/null +++ b/platform/linto-admin/webserver/lib/mqtt-monitor/index.js @@ -0,0 +1,215 @@ +const debug = require('debug')(`linto-admin:mqtt-monitor`) +const EventEmitter = require('eventemitter3') +const Mqtt = require('mqtt') + +class MqttMonitor extends EventEmitter { + constructor(scope) { + super() + + this.scope = scope + this.client = null + this.subscribtionTopics = [] + this.cnxParam = { + clean: true, + servers: [{ + host: process.env.LINTO_STACK_MQTT_HOST, + port: process.env.LINTO_STACK_MQTT_PORT + }], + qos: 2 + } + if (process.env.LINTO_STACK_MQTT_USE_LOGIN) { + this.cnxParam.username = process.env.LINTO_STACK_MQTT_USER + this.cnxParam.password = process.env.LINTO_STACK_MQTT_PASSWORD + } + this.isSubscribed = false + + this.init() + + return this + } + + async init() { + return new Promise((resolve, reject) => { + let cnxError = setTimeout(() => { + console.error('Logic MQTT Broker - Unable to connect') + }, 5000) + this.client = Mqtt.connect(this.cnxParam) + this.client.on('error', e => { + console.error('Logic MQTT Broker error : ' + e) + }) + this.client.on('connect', () => { + console.log('> Logic MQTT Broker: Connected') + this.unsubscribe() + }) + + this.client.once('connect', () => { + clearTimeout(cnxError) + this.client.on('offline', () => { + debug('Logic MQTT Broker connexion down') + }) + resolve(this) + }) + + this.client.on('message', (topics, payload) => { + try { + debug(topics, payload) + let topicArray = topics.split('/') + payload = payload.toString() + + payload = JSON.parse(payload) + payload = Object.assign(payload, { + topicArray + }) + this.client.emit(`mqtt-monitor::message`, payload) + } catch (err) { + debug(err) + } + }) + }) + } + subscribe(data) { + let range = '+' + if (!!data.sn) { + range = data.sn + } + // Unsubscribe current Topics + this.unsubscribe() + + // Set new topics + this.subscribtionTopics['status'] = `${this.scope}/fromlinto/${range}/status` + this.subscribtionTopics['pong'] = `${this.scope}/fromlinto/${range}/pong` + this.subscribtionTopics['muteack'] = `${this.scope}/fromlinto/${range}/muteack` + this.subscribtionTopics['unmuteack'] = `${this.scope}/fromlinto/${range}/unmuteack` + this.subscribtionTopics['tts_lang'] = `${this.scope}/fromlinto/${range}/tts_lang` + this.subscribtionTopics['say'] = `${this.scope}/fromlinto/${range}/say` + + // Subscribe to new topics + for (let index in this.subscribtionTopics) { + const topic = this.subscribtionTopics[index] + + //Subscribe to the client topics + this.client.subscribe(topic, (err) => { + if (!err) { + this.isSubscribed = true + debug(`subscribed successfully to ${topic}`) + } else { + console.error(err) + } + }) + } + } + + unsubscribe() { + if (this.isSubscribed) { + for (let index in this.subscribtionTopics) { + const topic = this.subscribtionTopics[index] + this.client.unsubscribe(topic, (err) => { + if (err) console.error('disconnecting while unsubscribing', err) + debug('Unsubscribe to : ', topic) + this.isSubscribed = false + }) + } + } + } + ping(payload) { + try { + this.client.publish(`${this.scope}/tolinto/${payload.sn}/ping`, '{}', (err) => { + if (err) { + throw err + } + }) + } catch (error) { + console.error(error) + this.client.emit('tolinto_debug', { + status: 'error', + message: 'error on pong response', + error + }) + } + } + + lintoSay(payload) { + try { + this.client.publish(`${this.scope}/tolinto/${payload.sn}/say`, `{"value":"${payload.value}"}`, (err) => { + if (err) { + throw err + } + }) + } catch (error) { + console.error(error) + this.client.emit('tolinto_debug', { + status: 'error', + message: 'error on linto say', + error + }) + } + } + mute(payload) { + try { + this.client.publish(`${this.scope}/tolinto/${payload.sn}/mute`, '{}', (err) => { + if (err) { + throw err + } + }) + } catch (error) { + console.error(error) + this.client.emit('tolinto_debug', { + status: 'error', + message: 'error on linto mute', + error + }) + } + } + + unmute(payload) { + try { + this.client.publish(`${this.scope}/tolinto/${payload.sn}/unmute`, '{}', (err) => { + if (err) { + throw err + } + }) + } catch (error) { + console.error(error) + this.client.emit('tolinto_debug', { + status: 'error', + message: 'error on unmute ack', + error + }) + } + } + + setVolume(payload) { + try { + this.client.publish(`${this.scope}/tolinto/${payload.sn}/volume`, `{"value":"${payload.value}"}`, (err) => { + if (err) { + throw err + } + }) + } catch (error) { + console.error(error) + this.client.emit('tolinto_debug', { + status: 'error', + message: 'error on setting volume', + error + }) + } + } + setVolumeEnd(payload) { + try { + this.client.publish(`${this.scope}/tolinto/${payload.sn}/endvolume`, `{"value":"${payload.value}"}`, (err) => { + if (err) { + throw err + } + }) + } catch (error) { + console.error(error) + this.client.emit('tolinto_debug', { + status: 'error', + message: 'error on setting volume', + error + }) + } + } +} + +module.exports = scope => new MqttMonitor(scope) \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/redis/index.js b/platform/linto-admin/webserver/lib/redis/index.js new file mode 100644 index 0000000..fe7b0c2 --- /dev/null +++ b/platform/linto-admin/webserver/lib/redis/index.js @@ -0,0 +1,70 @@ +const Session = require('express-session') +const redis = require('redis') +const redisStore = require('connect-redis')(Session) + +class redisClient { + constructor() { + this.settings = { + host: process.env.LINTO_STACK_REDIS_SESSION_SERVICE, + port: process.env.LINTO_STACK_REDIS_SESSION_SERVICE_PORT, + } + this.maxAttempt = 5 + this.client = null + this.redisStore = null + this.init() + } + + init() { + this.client = redis.createClient({ + host: this.settings.host, + port: this.settings.port, + retry_strategy: function(options) { + try { + if (options.error && options.error.code === "ECONNREFUSED" && options.attempt < this.maxAttempt) { + console.log('> Redis : try to reconnect') + } + if (options.total_retry_time > 1000 * 60 * 60) { + // End reconnecting after a specific timeout and flush all commands + // with a individual error + throw "Retry time exhausted" + } + if (options.attempt > 5) { + // End reconnecting with built in error + throw "Disconnected, to many attempts" + } + // reconnect after + return Math.min(options.attempt * 100, 3000); + } catch (error) { + console.error('> Redis error :', error) + return error + } + } + }) + + this.redisStore = new redisStore({ + host: process.env.LINTO_STACK_REDIS_SESSION_SERVICE, + port: process.env.LINTO_STACK_REDIS_SESSION_SERVICE_PORT, + client: this.client + }) + + this.client.on('connect', () => { + console.log('> Redis : Connected') + }) + this.client.on('reconnect', () => { + console.log('> Redis : reconnect') + }) + this.client.on('error', (e) => { + console.log('> Redis ERROR :') + console.error(e) + }) + this.client.on('end', (e) => { + console.log('> Redis : Disconnected') + }) + } + + checkConnection() { + return this.client.connected + } +} + +module.exports = redisClient \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/index.js b/platform/linto-admin/webserver/lib/webserver/index.js new file mode 100644 index 0000000..8fa783a --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/index.js @@ -0,0 +1,96 @@ +const debug = require('debug')(`linto-admin:webserver`) +const express = require('express') +const Session = require('express-session') +const bodyParser = require('body-parser') +const EventEmitter = require('eventemitter3') +const cookieParser = require('cookie-parser') +const path = require('path') +const IoHandler = require('./iohandler') +const CORS = require('cors') +const redisClient = require(`${process.cwd()}/lib/redis`) +const middlewares = require(`${process.cwd()}/lib/webserver/middlewares/index.js`) +let corsOptions = {} +let whitelistDomains = [`${middlewares.useSSL() + process.env.LINTO_STACK_DOMAIN}`] +const swaggerUi = require('swagger-ui-express'); +const swaggerDocument = require(`${process.cwd()}/doc/swagger.json`); + +if (process.env.LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS.length > 0) { + whitelistDomains.push(...process.env.LINTO_STACK_ADMIN_API_WHITELIST_DOMAINS.split(',')) + corsOptions = { + origin: function(origin, callback) { + if (!origin || whitelistDomains.indexOf(origin) !== -1 || origin === 'undefined') { + callback(null, true) + } else { + callback(new Error('Not allowed by CORS')) + } + } + } +} + +class WebServer extends EventEmitter { + + constructor() { + super() + this.app = express() + this.app.set('etag', false) + this.app.set('trust proxy', true) + this.app.use('/assets', express.static(path.resolve(__dirname, '../../dist'))) + this.app.use('/public', express.static(path.resolve(__dirname, '../../public'))) + this.app.use(bodyParser.json({ limit: '1000mb' })) + this.app.use(bodyParser.urlencoded({ + extended: false + })) + + // CORS + this.app.use(cookieParser()) + this.app.use(CORS(corsOptions)) + + // SESSION + let sessionConfig = { + resave: false, + saveUninitialized: false, + secret: process.env.LINTO_STACK_ADMIN_COOKIE_SECRET, + cookie: { + maxAge: 30240000000 // 1 year + } + } + this.app.redis = new redisClient() + sessionConfig.store = this.app.redis.redisStore + this.session = Session(sessionConfig) + this.app.use(this.session) + + // Server + this.httpServer = this.app.listen(process.env.LINTO_STACK_ADMIN_HTTP_PORT, "0.0.0.0", (err) => { + if (err) console.error(err) + }) + console.log('Webserver started on port : ', process.env.LINTO_STACK_ADMIN_HTTP_PORT) + return this.init() + } + async init() { + // Set ioHandler + this.ioHandler = new IoHandler(this) + + // Router + require('./routes')(this) + + // API Swagger + this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + + // 404 + this.app.use((req, res, next) => { + res.status(404) + res.setHeader("Content-Type", "text/html") + res.sendFile(process.cwd() + '/dist/404.html') + }) + + // 500 + this.app.use((err, req, res, next) => { + console.error(err) + res.status(500) + res.end() + }) + return this + } +} + +module.exports = new WebServer() \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/iohandler/index.js b/platform/linto-admin/webserver/lib/webserver/iohandler/index.js new file mode 100644 index 0000000..a1d842d --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/iohandler/index.js @@ -0,0 +1,65 @@ +const debug = require('debug')('linto-admin:ioevents') +const EventEmitter = require('eventemitter3') + +class IoHandler extends EventEmitter { + constructor(webServer) { + super() + this.webServer = webServer + + //Adds socket.io + webServer.io = require('socket.io').listen(webServer.httpServer) + + //http AND io uses same session middleware + webServer.io.use((socket, next) => { + if (socket) { + webServer.session(socket.request, socket.request.res, next) + } + }) + webServer.io.on('connection', (socket) => { + debug(webServer.io) + + //Secures websocket usage with session + if (process.env.NODE_ENV !== 'production') { + socket.request.session.logged = 'on' + socket.request.session.save() + } + if (!socket.request.session || socket.request.session.logged != 'on') return socket.disconnect() + debug('new Socket connected') + + socket.on('linto_subscribe', (data) => { + this.emit('linto_subscribe', data) + }) + socket.on('linto_subscribe_all', (data) => { + this.emit('linto_subscribe_all', data) + }) + socket.on('linto_unsubscribe_all', (data) => { + this.emit('linto_unsubscribe_all', data) + }) + socket.on('tolinto_ping', (data) => { + this.emit('linto_ping', data) + }) + socket.on('tolinto_say', (data) => { + this.emit('linto_say', data) + }) + socket.on('tolinto_volume', (data) => { + this.emit('linto_volume', data) + }) + socket.on('tolinto_volume_end', (data) => { + this.emit('linto_volume_end', data) + }) + socket.on('tolinto_mute', (data) => { + this.emit('linto_mute', data) + }) + socket.on('tolinto_unmute', (data) => { + this.emit('linto_unmute', data) + }) + }) + } + + //broadcasts to connected sockets + notify(msgType, payload) { + this.webServer.io.emit('linto_' + msgType, payload) + } +} + +module.exports = IoHandler \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/middlewares/index.js b/platform/linto-admin/webserver/lib/webserver/middlewares/index.js new file mode 100644 index 0000000..c4b713d --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/middlewares/index.js @@ -0,0 +1,81 @@ +const debug = require('debug')('linto-admin:middlewares') +const btoa = require('btoa') +const atob = require('atob') +const sha1 = require('sha1') +const UsersModel = require(`${process.cwd()}/model/mongodb/models/users.js`) + + +function isProduction() { + return process.env.NODE_ENV === 'production' +} + +function logger(req, res, next) { + debug(`[${Date.now()}] new user entry on ${req.url}`) + next() +} + +async function checkAuth(req, res, next) { + try { + if (!!req.session) { + if (!!req.session.logged) { + if (req.session.logged === 'on' && req.url === '/login') { + req.session.save((err) => { + if (err && err !== 'undefined') { + console.error('Err:', err) + } + }) + res.redirect('/admin/applications/device') + } else if (req.session.logged === 'on' && req.url !== '/login') { + next() + } else if (req.session.logged !== 'on' && req.url !== '/login') { + res.redirect('/login') + } else if (req.session.logged !== 'on' && req.url === '/login') { + next() + } + } else { + const users = await UsersModel.getUsers() + if (users.length === 0) { + res.redirect('/setup') + } else if (req.url != '/login') { + res.redirect('/login') + } else { + next() + } + } + } else { // session not foun + res.redirect('/login') + } + } catch (error) { + console.error(error) + res.json({ error }) + } + +} + +// Get a Basic Auth token from user and password +function basicAuthToken(user, password) { + var token = user + ":" + password; + var hash = btoa(token); + return "Basic " + hash; +} + +function useSSL() { + if (process.env.NODE_ENV === 'local') { + return '' + } else { + if (process.env.LINTO_STACK_USE_SSL === true) { + return 'https://' + } else { + return 'http://' + } + } + +} + +module.exports = { + basicAuthToken, + checkAuth, + isProduction, + logger, + useSSL +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/middlewares/json/device-workflow.json b/platform/linto-admin/webserver/lib/webserver/middlewares/json/device-workflow.json new file mode 100644 index 0000000..b70ea5a --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/middlewares/json/device-workflow.json @@ -0,0 +1,107 @@ +[{ + "id": "90c0fa5.2442c08", + "type": "linto-config", + "z": "a2f78166.cb2a", + "name": "", + "configMqtt": "7c1cf535.20a7ec", + "configEvaluate": "88912f7c.650c5", + "configChatbot": "b96485f2.704258-chatbot", + "configTranscribe": "84e1b781.57967", + "language": "fr-FR", + "x": 100, + "y": 40, + "wires": [] + }, + { + "id": "997c2744.9c80f8", + "type": "linto-pipeline-router", + "z": "a2f78166.cb2a", + "name": "", + "x": 350, + "y": 140, + "wires": [] + }, + { + "id": "dce00109.eb573", + "type": "linto-on-connect", + "z": "a2f78166.cb2a", + "name": "", + "x": 120, + "y": 80, + "wires": [] + }, + { + "id": "832ff221.5c4348", + "type": "linto-model-dataset", + "z": "a2f78166.cb2a", + "name": "", + "x": 350, + "y": 40, + "wires": [] + }, + { + "id": "4b562aa9.950974", + "type": "linto-red-event-emitter", + "z": "a2f78166.cb2a", + "name": "", + "x": 900, + "y": 140, + "wires": [] + }, + { + "id": "b7ec987e.cfaba8", + "type": "linto-out", + "z": "a2f78166.cb2a", + "name": "", + "x": 860, + "y": 40, + "wires": [] + }, + { + "id": "99c1c1fd.a414b", + "type": "linto-terminal-in", + "z": "4b8ed08f.331d9", + "name": "", + "sn": "", + "x": 110, + "y": 140, + "wires": [ + [ + "997c2744.9c80f8" + ] + ] + }, + { + "id": "7c1cf535.20a7ec", + "type": "linto-config-mqtt", + "host": "localhost", + "port": "1883", + "scope": "blk", + "login": "test", + "password": "test" + }, + { + "id": "88912f7c.650c5", + "type": "linto-config-evaluate", + "host": "localhost:8888", + "api": "tock", + "appname": "linto", + "namespace": "app" + }, + { + "id": "b96485f2.704258-chatbot", + "type": "linto-config-chatbot", + "host": "dev.linto.local:8080", + "rest": "/io/app/linto/web" + }, + { + "id": "84e1b781.57967", + "type": "linto-config-transcribe", + "host": "https://stage.linto.ai/stt", + "api": "linstt", + "commandOffline": "", + "largeVocabStreaming": "", + "largeVocabStreamingInternal": true, + "largeVocabOffline": "" + } +] \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/middlewares/json/multi-user-workflow.json b/platform/linto-admin/webserver/lib/webserver/middlewares/json/multi-user-workflow.json new file mode 100644 index 0000000..90ebfde --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/middlewares/json/multi-user-workflow.json @@ -0,0 +1,108 @@ +[{ + "id": "90c0fa5.2442c08", + "type": "linto-config", + "z": "a2f78166.cb2a", + "name": "", + "configMqtt": "7c1cf535.20a7ec", + "configEvaluate": "88912f7c.650c5", + "configChatbot": "b96485f2.704258-chatbot", + "configTranscribe": "84e1b781.57967", + "language": "fr-FR", + "x": 100, + "y": 40, + "wires": [] + }, + { + "id": "997c2744.9c80f8", + "type": "linto-pipeline-router", + "z": "a2f78166.cb2a", + "name": "", + "x": 350, + "y": 140, + "wires": [] + }, + { + "id": "dce00109.eb573", + "type": "linto-on-connect", + "z": "80776222.28916", + "name": "", + "x": 120, + "y": 80, + "wires": [] + }, + { + "id": "832ff221.5c4348", + "type": "linto-model-dataset", + "z": "a2f78166.cb2a", + "name": "", + "x": 350, + "y": 40, + "wires": [] + }, + { + "id": "4b562aa9.950974", + "type": "linto-red-event-emitter", + "z": "a2f78166.cb2a", + "name": "", + "x": 900, + "y": 140, + "wires": [] + }, + { + "id": "b7ec987e.cfaba8", + "type": "linto-out", + "z": "a2f78166.cb2a", + "name": "", + "x": 860, + "y": 40, + "wires": [] + }, + { + "id": "99c1c1fd.a414b", + "type": "linto-application-in", + "z": "a2f78166.cb2a", + "name": "", + "auth_android": false, + "auth_web": false, + "x": 130, + "y": 140, + "wires": [ + [ + "997c2744.9c80f8" + ] + ] + }, + { + "id": "7c1cf535.20a7ec", + "type": "linto-config-mqtt", + "host": "localhost", + "port": "1883", + "scope": "blk", + "login": "test", + "password": "test" + }, + { + "id": "88912f7c.650c5", + "type": "linto-config-evaluate", + "host": "localhost:8888", + "api": "tock", + "appname": "linto", + "namespace": "app" + }, + { + "id": "b96485f2.704258-chatbot", + "type": "linto-config-chatbot", + "host": "dev.linto.local:8080", + "rest": "/io/app/linto/web" + }, + { + "id": "84e1b781.57967", + "type": "linto-config-transcribe", + "host": "https://stage.linto.ai/stt", + "api": "linstt", + "commandOffline": "", + "largeVocabStreaming": "", + "largeVocabStreamingInternal": true, + "largeVocabOffline": "" + } +] \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/middlewares/lexicalseeding.js b/platform/linto-admin/webserver/lib/webserver/middlewares/lexicalseeding.js new file mode 100644 index 0000000..20c3296 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/middlewares/lexicalseeding.js @@ -0,0 +1,518 @@ +const axios = require('axios') +const nodered = require('./nodered.js') +const middlewares = require('./index.js') +const fs = require('fs') +var FormData = require('form-data') + +/** + * @desc Execute STT lexical seeding on a flowID, by its service_name + * @param {string} flowId - Id of the flow used on nodered + * @param {string} service_name - Name of the targeted stt service + * @return {object} - {status, msg, error(optional)} + */ + +async function sttLexicalSeeding(flowId, service_name) { + try { + // Get stt service data + const accessToken = await nodered.getBLSAccessToken() + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getSttService = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/service/${service_name}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + const sttService = getSttService.data.data + + // Get lexical seeding data + const getSttLexicalSeeding = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/red/${flowId}/dataset/linstt`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + } + }) + + const sttLexicalSeedingData = getSttLexicalSeeding.data.data + const intents = sttLexicalSeedingData.intents + const entities = sttLexicalSeedingData.entities + let intentsUpdated = false + let entitiesUpdated = false + let updateInt = { success: '', errors: '' } + let updateEnt = { success: '', errors: '' } + + // Update model intents + const intentsToSend = await filterLMData('intent', sttService.LModelId, intents) + if (intentsToSend.data.length > 0) { + updateInt = await updateLangModel(intentsToSend, sttService.LModelId) + if (!!updateInt.success && !!updateInt.errors) { + intentsUpdated = true + } + } else { + intentsUpdated = true + } + + // Update model entities + const entitiesToSend = await filterLMData('entity', sttService.LModelId, entities) + + if (entitiesToSend.data.length > 0) { + updateEnt = await updateLangModel(entitiesToSend, sttService.LModelId) + if (!!updateEnt.success && !!updateEnt.errors) { + entitiesUpdated = true + } + } else { + entitiesUpdated = true + } + if (intentsToSend.data.length > 0 || entitiesToSend.data.length > 0) { + const getUpdatedSttLangModel = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${sttService.LModelId}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + // Generate Graph if model updated + if (getUpdatedSttLangModel.data.data.isDirty === 1) { + try { + await generateGraph(service_name) + } catch (error) { + console.error(error) + } + } + } + + // Result + if (intentsUpdated && entitiesUpdated) { + if (updateInt.errors.length === 0 && updateEnt.errors.length === 0) { + return ({ + status: 'success', + msg: 'Model language has been updated' + }) + } else { + errorMsg = 'Model updated BUT : ' + if (updateInt.errors.length > 0) { + updateInt.errors.map(e => { + errorMsg += `Warning: error on updating intent ${e.name}.` + }) + } + if (updateEnt.errors.length > 0) { + updateEnt.errors.map(e => { + errorMsg += `Warning: error on updating entity ${e.name}.` + }) + } + return ({ + status: 'success', + msg: errorMsg + }) + } + } else { + throw 'Error on updating language model' + } + } catch (error) { + console.error(error) + return ({ + status: 'error', + msg: !!error.msg ? error.msg : error, + error: error + }) + } +} + +/** + * @desc filter language model data/values for lexical seeding + * @param {string} type - "intents" or "entities" + * @param {string} modelId - id of the targeted language model + * @param {object} newData - data/values to be updated + * @return {object} - {type, data(filtered)} + */ +async function filterLMData(type, modelId, newData) { + let getDataroutePath = '' + if (type === 'intent') { + getDataroutePath = 'intents' + } else if (type === 'entity') { + getDataroutePath = 'entities' + } + + // Current Values of the langage model + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getData = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${modelId}/${getDataroutePath}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + let currentData = [] + if (!!getData.data.data) { + currentData = getData.data.data + } + let dataToSend = [] + if (newData.length > 0) { + newData.map(d => { + let toAdd = [] + let toSendMethod = '' + let toCompare = currentData.filter(c => c.name === d.name) + if (toCompare.length === 0) { + toAdd.push(...d.items) + toSendMethod = 'post' + } else { + toSendMethod = 'patch' + d.items.map(val => { + if (toCompare[0]['items'].indexOf(val) < 0) { + toAdd.push(val) + } + }) + } + if (toAdd.length > 0) { + dataToSend.push({ + name: d.name, + items: toAdd, + method: toSendMethod + }) + } + }) + } + return { + type, + data: dataToSend + } +} + +/** + * @desc Execute requests to start generating graph on a service_name language model + * @param {string} service_name - STT service name + */ +async function generateGraph(service_name) { + try { + // get stt service data + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getSttService = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/service/${service_name}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + const sttService = getSttService.data.data + + // Generate graph + const generateGraph = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${sttService.LModelId}/generate/graph`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + + return ({ + status: 'success', + msg: generateGraph.data.data + }) + } catch (error) { + console.error(error) + return ({ + status: 'error', + msg: 'error on generating graph' + }) + } +} + +/** + * @desc Update a langage model with intents/entities object to add/update + * @param {object} payload - data to be updated + * @param {string} modelId - Id of the targeted language model + * @return {object} {errors, success} + */ +async function updateLangModel(payload, modelId) { + try { + let success = [] + let errors = [] + const type = payload.type + for (let i in payload.data) { + const name = payload.data[i].name + const items = payload.data[i].items + const method = payload.data[i].method + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + + const req = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodel/${modelId}/${type}/${name}`, { + method, + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + }, + data: items + }) + + if (req.status === 200 || req.status === '200') { + success.push(payload.data[i]) + } else { + errors.push(payload.data[i]) + } + if (success.length + errors.length === payload.data.length) { + return ({ + errors, + success + }) + } + } + } catch (error) { + console.error(error) + return ('an error has occured') + } +} + +/** + * @desc Execute requests to update NLU application + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ +async function nluLexicalSeedingApplications(flowId) { + try { + // Get lexical seeding object to send to TOCK + const accessToken = await nodered.getBLSAccessToken() + const getNluLexicalSeeding = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/red/${flowId}/dataset/tock`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + } + }) + + // get Tock auth token + const token = middlewares.basicAuthToken(process.env.LINTO_STACK_TOCK_USER, process.env.LINTO_STACK_TOCK_PASSWORD) + + // Tmp json file path + const jsonApplicationContent = `{"application": ${JSON.stringify(getNluLexicalSeeding.data.application)}}` + const appFilePath = process.cwd() + '/public/tockapp.json' + + let postApp = await new Promise((resolve, reject) => { + fs.writeFile(appFilePath, jsonApplicationContent, async(err) => { + if (err) { + console.error(err) + throw err + } else { + const formData = new FormData() + formData.append('file', fs.createReadStream(appFilePath)) + axios({ + url: `${middlewares.useSSL() + process.env.LINTO_STACK_TOCK_SERVICE}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}${process.env.LINTO_STACK_TOCK_BASEHREF}/rest/admin/dump/application`, + method: 'post', + data: formData, + headers: { + 'Authorization': token, + 'Content-Type': formData.getHeaders()['content-type'] + } + }).then((res) => { + //fs.unlinkSync(appFilePath) + if (res.status === 200) { + resolve({ + status: 'success', + msg: 'Tock application has been updated' + }) + } else { + reject({ + status: 'error', + msg: 'Error on updating Tock application' + }) + } + }).catch((error) => { + console.error(error) + reject(error) + }) + } + }) + }) + return postApp + } catch (error) { + console.error(error) + return ({ + status: 'error', + msg: 'Error on updating Tock application', + error + }) + } +} + +/** + * @desc Execute requests to update NLU sentences + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ + +async function nluLexicalSeedingSentences(flowId) { + try { + // Get lexical seeding object to send to TOCK + const accessToken = await nodered.getBLSAccessToken() + const getNluLexicalSeeding = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/red/${flowId}/dataset/tock`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + } + }) + + // get Tock auth token + const token = middlewares.basicAuthToken(process.env.LINTO_STACK_TOCK_USER, process.env.LINTO_STACK_TOCK_PASSWORD) + + + // Tmp json file path + const jsonSentencesContent = JSON.stringify(getNluLexicalSeeding.data.sentences) + const sentencesFilePath = process.cwd() + '/public/tocksentences.json' + + let postSentences = await new Promise((resolve, reject) => { + fs.writeFile(sentencesFilePath, jsonSentencesContent, async(err) => { + if (err) { + console.error(err) + throw err + } else { + const formData = new FormData() + formData.append('file', fs.createReadStream(sentencesFilePath)) + axios({ + url: `${middlewares.useSSL() + process.env.LINTO_STACK_TOCK_SERVICE}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}${process.env.LINTO_STACK_TOCK_BASEHREF}/rest/admin/dump/sentences`, + method: 'post', + data: formData, + headers: { + 'Authorization': token, + 'Content-Type': formData.getHeaders()['content-type'] + } + }).then((res) => { + //fs.unlinkSync(sentencesFilePath) + if (res.status === 200) { + resolve({ + status: 'success', + msg: 'Tock application has been updated' + }) + } else { + reject({ + status: 'error', + msg: 'Error on updating Tock application' + }) + } + }).catch((error) => { + console.error(error) + reject(error) + }) + } + }) + }) + return postSentences + } catch (error) { + return ({ + status: 'error', + msg: 'Error on updating Tock application sentences', + error + }) + } +} + +/** + * @desc Tock application lexical seeding + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ +async function nluLexicalSeeding(flowId) { + try { + const postApp = await nluLexicalSeedingApplications(flowId) + const postSentences = await nluLexicalSeedingSentences(flowId) + let errors = [] + let status = 'success' + let postAppValid = true + let postSentencesValid = true + if (postApp.status !== 'success') { + postAppValid = false + status = 'error' + errors.push({ 'application': postApp }) + } + if (postSentences.status !== 'success') { + postSentencesValid = false + status = 'error' + errors.push({ 'sentences': postSentences }) + } + + if (postAppValid && postSentencesValid) { + return ({ + status, + msg: 'NLU updated' + }) + } else { + throw errors + } + } catch (error) { + console.error(error) + return ({ + status: 'error', + msg: 'Error on updating Tock application', + error + }) + } +} + +/** + * @desc Execute functions to strat process of lexical seeding for STT and NLU applications + * @param {string} sttServiceName - name of the targeted STT service + * @param {string} flowId - Id of the application nodered flow + * @return {object} - {status, msg, error (optional)} + */ +async function doLexicalSeeding(sttServiceName, flowId) { + try { + + // NLU lexical seeding + const nluLexSeed = await nluLexicalSeeding(flowId) + if (nluLexSeed.status !== 'success') { + throw !!nluLexSeed.msg ? nluLexSeed.msg : nluLexSeed + } + // STT lexical seeding + const sttLexSeed = await sttLexicalSeeding(flowId, sttServiceName) + if (sttLexSeed.status !== 'success') { + throw !!sttLexSeed.msg ? sttLexSeed.msg : sttLexSeed + } + // Success + if (sttLexSeed.status === 'success' && nluLexSeed.status === 'success') { + return ({ + status: 'success', + msg: 'Tock application and STT service have been updated' + }) + } else { + throw { + stt: sttLexSeed, + nlu: nluLexSeed + } + } + } catch (error) { + console.error(error) + return ({ + status: 'error', + error, + msg: !!error.msg ? error.msg : 'Error on executing lexical seeding' + }) + } + +} + +module.exports = { + doLexicalSeeding, + nluLexicalSeeding, + sttLexicalSeeding, + generateGraph +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/middlewares/nodered.js b/platform/linto-admin/webserver/lib/webserver/middlewares/nodered.js new file mode 100644 index 0000000..d112fe5 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/middlewares/nodered.js @@ -0,0 +1,718 @@ +const axios = require('axios') +const uuid = require('uuid/v1') +const middlewares = require('./index.js') +const md5 = require('md5') +const baseApplicationFlow = require(`${process.cwd()}/lib/webserver/middlewares/json/multi-user-workflow.json`) +const baseDeviceFlow = require(`${process.cwd()}/lib/webserver/middlewares/json/device-workflow.json`) + +function updateMultiUserApplicationFlowSettings(flow, payload) { + try { + + const settings = payload.settings + let flowId = flow.id + let toRemove = [] + for (let i = 0; i < flow.configs.length; i++) { + if (flow.configs[i].type === 'linto-config-chatbot') { + // Config Chatbot + if (settings.chatbot.enabled === true) { + flow.configs[i].rest = settings.chatbot.value + } else { + flow.configs[i].rest = '' + } + } + if (flow.configs[i].type === 'linto-config-transcribe') { + // Config command + if (settings.command.enabled === true) { + flow.configs[i].commandOffline = settings.command.value + } else { + flow.configs[i].commandOffline = '' + } + // Config Streaming + if (settings.streaming.enabled === true) { + flow.configs[i].largeVocabStreamingInternal = settings.streaming.internal + flow.configs[i].largeVocabStreaming = settings.streaming.value + } else { + flow.configs[i].largeVocabStreamingInternal = true + flow.configs[i].largeVocabStreaming = '' + } + } + if (flow.configs[i].type === 'linto-config-evaluate') { + if (settings.tock.value !== flow.configs[i].appname) { + flow.configs[i].appname = settings.tock.value + } + } + } + let lintoChatbotIndex = flow.nodes.findIndex(node => node.type === 'linto-chatbot') + let lintoStreamingIndex = flow.nodes.findIndex(node => node.type === 'linto-transcribe-streaming') + let lintoEvaluateIndex = flow.nodes.findIndex(node => node.type === 'linto-evaluate') + let lintoTranscribeIndex = flow.nodes.findIndex(node => node.type === 'linto-transcribe') + + // Uppdate Chatbot nodes + if (lintoChatbotIndex >= 0 && !settings.chatbot.enabled) { + toRemove.push(lintoChatbotIndex) + } else if (lintoChatbotIndex < 0 && settings.chatbot.enabled) { + flow.nodes.push({ + id: uuid(), + type: "linto-chatbot", + z: flowId, + name: "", + x: 640, + y: 360, + wires: [] + }) + } + + //Update Streaming nodes + if (lintoStreamingIndex >= 0 && !settings.streaming.enabled) { + toRemove.push(lintoStreamingIndex) + } else if (lintoStreamingIndex < 0 && settings.streaming.enabled) { + flow.nodes.push({ + id: uuid(), + type: "linto-transcribe-streaming", + z: flowId, + name: "", + x: 670, + y: 160, + wires: [] + }) + } + + // update command nodes + // evaluate + if (lintoEvaluateIndex >= 0 && !settings.command.enabled) { + toRemove.push(lintoEvaluateIndex) + } else if (lintoEvaluateIndex < 0 && settings.command.enabled) { + flow.nodes.push({ + id: uuid(), + type: "linto-evaluate", + z: flowId, + name: "", + x: 640, + y: 240, + wires: [], + useConfidenceScore: false, + confidenceThreshold: "" + }) + } + // transcribe + if (lintoTranscribeIndex >= 0 && !settings.command.enabled) { + toRemove.push(lintoTranscribeIndex) + } else if (lintoTranscribeIndex < 0 && settings.command.enabled) { + flow.nodes.push({ + id: uuid(), + type: "linto-transcribe", + z: flowId, + name: "", + x: 640, + y: 300, + wires: [], + useConfidenceScore: false, + confidenceThreshold: 50 + }) + } + + flow.nodes = flow.nodes.filter(function(value, index) { + return toRemove.indexOf(index) == -1 + }) + + return flow + } catch (error) { + console.error(error) + } +} + +function generateMultiUserApplicationFromBaseTemplate(payload) { + try { + const flowId = uuid() + const mqttId = flowId + '-mqtt' + const nluId = flowId + '-nlu' + const sttId = flowId + '-stt' + const configId = flowId + '-config' + const applicationInId = flowId + '-appin' + const pipelineRouterId = flowId + '-pr' + const chatbotId = flowId + '-chatbot' + const datasetId = flowId + '-dataset' + + let flow = baseApplicationFlow + + let idMap = [] // ID correlation array + let nodesArray = [] + flow = cleanDuplicateNodes(flow) + + // Format "linto-config" and set IDs + flow.filter(node => node.type === 'linto-config').map(f => { + f.z = flowId + + // Update language + f.language = payload.language + + // Update linto-config node ID + idMap[f.id] = configId + f.id = configId + + // Update config-transcribe node ID + idMap[f.configTranscribe] = sttId + f.configTranscribe = sttId + + // Update config-mqtt node ID + idMap[f.configMqtt] = mqttId + f.configMqtt = mqttId + + // Update config-nlu node ID + idMap[f.configEvaluate] = nluId + f.configEvaluate = nluId + + // // Update configChatbot node ID + idMap[f.configChatbot] = chatbotId + f.configChatbot = chatbotId + + nodesArray.push(f) + }) + + // Format required nodes (existing in default template) + flow.filter(node => node.type !== 'tab' && node.type !== 'linto-config').map(f => { + f.z = flowId + + // uppdate STT node + if (f.type === 'linto-config-transcribe') { + f.z = flowId + f.id = sttId + f.host = process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE + f.api = 'linstt' + f.commandOffline = payload.smart_assistant + f.largeVocabStreaming = payload.streamingService + f.largeVocabStreamingInternal = payload.streamingServiceInternal === true ? 'true' : 'false' + } + // uppdate NLU node + else if (f.type === 'linto-config-evaluate') { + f.z = flowId + f.id = nluId + f.api = 'tock' + f.host = `${process.env.LINTO_STACK_TOCK_NLP_API}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}` + f.appname = payload.nluAppName + f.namespace = 'app' + } + // uppdate MQTT node + else if (f.type === 'linto-config-mqtt') { + f.z = flowId + f.id = mqttId + f.host = process.env.LINTO_STACK_MQTT_HOST + f.port = process.env.LINTO_STACK_MQTT_PORT + f.scope = 'app' + md5(payload.workflowName) + f.login = process.env.LINTO_STACK_MQTT_USER + f.password = process.env.LINTO_STACK_MQTT_PASSWORD + + } + // Application-in (required) + else if (f.type === 'linto-application-in') { + f.wires = [pipelineRouterId] + f.id = applicationInId + f.z = flowId + f.auth_android = false + f.auth_web = false + } + // Config Chatbot (required) + else if (f.type === 'linto-config-chatbot') { + f.id = chatbotId + f.z = flowId + f.host = process.env.LINTO_STACK_TOCK_BOT_API + ':' + process.env.LINTO_STACK_TOCK_SERVICE_PORT + f.rest = payload.chatbot + } + // Pipeline router (required) + else if (f.type === 'linto-pipeline-router') { + f.id = pipelineRouterId + f.z = flowId + } else if (f.type === 'linto-model-dataset') { + f.id = datasetId + f.z = flowId + } else { + if (typeof(idMap[f.id]) === 'undefined') { + idMap[f.id] = uuid() + } + f.id = idMap[f.id] + } + nodesArray.push(f) + }) + + // streamingService + if (payload.streamingService !== '') { + let transcribStreamingId = flowId + '-trans-streaming' + let transcribStreamingObj = { + id: transcribStreamingId, + type: "linto-transcribe-streaming", + z: flowId, + name: "", + x: 670, + y: 160, + wires: [] + } + nodesArray.push(transcribStreamingObj) + } + + // CHATBOT + if (payload.chatbot !== '') { + let chatbotObj = { + id: uuid(), + type: "linto-chatbot", + z: flowId, + name: "", + x: 640, + y: 360, + wires: [] + } + nodesArray.push(chatbotObj) + } + // SMART ASSISTANT + if (payload.smart_assistant !== '') { + let lintoEvaluateObj = { + id: uuid(), + type: "linto-evaluate", + z: flowId, + name: "", + x: 640, + y: 240, + wires: [], + useConfidenceScore: false, + confidenceThreshold: "" + } + let lintoTranscribeObj = { + id: uuid(), + type: "linto-transcribe", + z: flowId, + name: "", + x: 640, + y: 300, + wires: [], + useConfidenceScore: false, + confidenceThreshold: 50 + } + nodesArray.push(lintoEvaluateObj) + nodesArray.push(lintoTranscribeObj) + } + const formattedFlow = { + label: payload.workflowName, + configs: [], + nodes: nodesArray, + id: flowId + } + return formattedFlow + } catch (error) { + console.error(error) + return error + } +} + +function generateDeviceApplicationFromBaseTemplate(payload) { + try { + const flowId = uuid() + const mqttId = flowId + '-mqtt' + const nluId = flowId + '-nlu' + const sttId = flowId + '-stt' + const configId = flowId + '-config' + const terminalInId = flowId + '-appin' + const pipelineRouterId = flowId + '-pr' + const chatbotId = flowId + '-chatbot' + const datasetId = flowId + '-dataset' + + let flow = baseDeviceFlow + + let idMap = [] // ID correlation array + let nodesArray = [] + flow = cleanDuplicateNodes(flow) + + // Format "linto-config" and set IDs + flow.filter(node => node.type === 'linto-config').map(f => { + f.z = flowId + + // Update language + f.language = payload.language + + // Update linto-config node ID + idMap[f.id] = configId + f.id = configId + + // Update config-transcribe node ID + idMap[f.configTranscribe] = sttId + f.configTranscribe = sttId + + // Update config-mqtt node ID + idMap[f.configMqtt] = mqttId + f.configMqtt = mqttId + + // Update config-nlu node ID + idMap[f.configEvaluate] = nluId + f.configEvaluate = nluId + + // // Update configChatbot node ID + idMap[f.configChatbot] = chatbotId + f.configChatbot = chatbotId + + nodesArray.push(f) + }) + + // Format required nodes (existing in default template) + flow.filter(node => node.type !== 'tab' && node.type !== 'linto-config').map(f => { + f.z = flowId + + // uppdate STT node + if (f.type === 'linto-config-transcribe') { + f.z = flowId + f.id = sttId + f.host = process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE + f.api = 'linstt' + f.commandOffline = payload.smart_assistant + f.largeVocabStreaming = payload.streamingService + f.largeVocabStreamingInternal = payload.streamingServiceInternal === true ? 'true' : 'false' + } + // uppdate NLU node + else if (f.type === 'linto-config-evaluate') { + f.z = flowId + f.id = nluId + f.api = 'tock' + f.host = `${process.env.LINTO_STACK_TOCK_NLP_API}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}` + f.appname = payload.nluAppName + f.namespace = 'app' + } + // uppdate MQTT node + else if (f.type === 'linto-config-mqtt') { + f.z = flowId + f.id = mqttId + f.host = process.env.LINTO_STACK_MQTT_HOST + f.port = process.env.LINTO_STACK_MQTT_PORT + f.scope = 'app' + md5(payload.workflowName) + f.login = process.env.LINTO_STACK_MQTT_USER + f.password = process.env.LINTO_STACK_MQTT_PASSWORD + + } + // Terminal-in (required) + else if (f.type === 'linto-terminal-in') { + f.wires = [pipelineRouterId] + f.id = terminalInId + f.sn = payload.device + f.z = flowId + } + // Config Chatbot (required) + else if (f.type === 'linto-config-chatbot') { + f.id = chatbotId + f.z = flowId + f.host = process.env.LINTO_STACK_TOCK_BOT_API + ':' + process.env.LINTO_STACK_TOCK_SERVICE_PORT + f.rest = payload.chatbot + } + // Pipeline router (required) + else if (f.type === 'linto-pipeline-router') { + f.id = pipelineRouterId + f.z = flowId + } else if (f.type === 'linto-model-dataset') { + f.id = datasetId + f.z = flowId + } else { + if (typeof(idMap[f.id]) === 'undefined') { + idMap[f.id] = uuid() + } + f.id = idMap[f.id] + } + nodesArray.push(f) + }) + + // streamingService + if (payload.streamingService !== '') { + let transcribStreamingId = flowId + '-trans-streaming' + let transcribStreamingObj = { + id: transcribStreamingId, + type: "linto-transcribe-streaming", + z: flowId, + name: "", + x: 670, + y: 160, + wires: [] + } + nodesArray.push(transcribStreamingObj) + } + + // CHATBOT + if (payload.chatbot !== '') { + let chatbotObj = { + id: uuid(), + type: "linto-chatbot", + z: flowId, + name: "", + x: 640, + y: 360, + wires: [] + } + nodesArray.push(chatbotObj) + } + // SMART ASSISTANT + if (payload.smart_assistant !== '') { + let lintoEvaluateObj = { + id: uuid(), + type: "linto-evaluate", + z: flowId, + name: "", + x: 640, + y: 240, + wires: [], + useConfidenceScore: false, + confidenceThreshold: "" + } + let lintoTranscribeObj = { + id: uuid(), + type: "linto-transcribe", + z: flowId, + name: "", + x: 640, + y: 300, + wires: [], + useConfidenceScore: false, + confidenceThreshold: 50 + } + nodesArray.push(lintoEvaluateObj) + nodesArray.push(lintoTranscribeObj) + } + const formattedFlow = { + label: payload.workflowName, + configs: [], + nodes: nodesArray, + id: flowId + } + return formattedFlow + } catch (error) { + console.error(error) + return error + } +} + +/** + * @desc Get a business logic server bearer token + * @return {string} + */ + +async function getBLSAccessToken() { + if (!process.env.LINTO_STACK_BLS_USE_LOGIN || process.env.LINTO_STACK_BLS_USE_LOGIN === 'false') { + return '' + } + const login = process.env.LINTO_STACK_BLS_USER + const pswd = process.env.LINTO_STACK_BLS_PASSWORD + const request = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE + process.env.LINTO_STACK_BLS_SERVICE_UI_PATH}/auth/token`, { + method: 'post', + data: { + "client_id": "node-red-admin", + "grant_type": "password", + "scope": "*", + "username": login, + "password": pswd + } + }) + return 'Bearer ' + request.data.access_token +} +/** + * @desc PUT request on business-logic-server + * @return {object} {status, msg, error(optional)} + */ +async function putBLSFlow(flowId, workflow) { + try { + const accessToken = await getBLSAccessToken() + let blsUpdate = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE + process.env.LINTO_STACK_BLS_SERVICE_UI_PATH}/flow/${flowId}`, { + method: 'put', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + }, + data: workflow + }) + if (blsUpdate.status == 200) { + return { + status: 'success' + } + } else { + throw 'Error on updating flow on the Business Logic Server' + } + } catch (error) { + console.error(error) + return { + status: 'error', + msg: error, + error + } + } +} +/** + * @desc Format a nodered flow object to be send by POST/PUT + */ +function formatFlowGroupedNodes(flow) { + let formattedFlow = {} + let nodes = [] + let registeredIds = [] + flow.map(f => { + if (f.type === 'tab') { + formattedFlow.id = f.id + formattedFlow.label = f.label + formattedFlow.configs = [] + formattedFlow.nodes = [] + registeredIds.push(f.id) + } else { + if (registeredIds.indexOf(f.id) < 0) { + registeredIds.push(f.id) + nodes.push(f) + } + } + }) + formattedFlow.nodes = nodes + + if (formattedFlow.nodes[0].type !== 'tab') { + const configIndex = formattedFlow.nodes.findIndex(flow => flow.type === 'linto-config') + let tmpIndex0 = formattedFlow.nodes[0] + let tmpConfig = formattedFlow.nodes[configIndex] + formattedFlow.nodes[0] = tmpConfig + formattedFlow.nodes[configIndex] = tmpIndex0 + } + return formattedFlow +} + +/** + * @desc POST request on business-logic-server + * @param {object} flow - flow object to be send + * @return {object} {status, msg, error(optional)} + */ +async function postBLSFlow(flow) { + try { + const accessToken = await getBLSAccessToken() + let blsPost = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/redui/flow`, { + method: 'post', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + }, + data: flow + }) + + // Validtion + if (blsPost.status == 200 && blsPost.data) { + return { + status: 'success', + msg: 'The worfklow has been deployed', + flowId: blsPost.data.id + } + } else { + throw { + msg: 'Error on posting flow on the business logic server' + } + } + } catch (error) { + console.error(error) + return { + status: 'error', + msg: !!error.msg ? error.msg : 'Error on posting flow on business logic server', + error + } + } +} + +/** + * @desc DELETE request on business-logic-server + * @param {string} flowId - id of the nodered flow + * @return {object} {status, msg, error(optional)} + */ +async function deleteBLSFlow(flowId) { + try { + const accessToken = await getBLSAccessToken() + let blsDelete = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/redui/flow/${flowId}`, { + method: 'delete', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + }, + }) + // Validtion + if (blsDelete.status == 204) { + return { + status: 'success', + msg: 'The worfklow has been removed' + } + } else { + throw { + msg: 'Error on deleting flow on the business logic server' + } + } + } catch (error) { + console.error(error) + return { + status: 'error', + error + } + } +} + +/** + * @desc request on business-logic-server to get a worflow by its id + * @param {string} id - id of the nodered flow + * @return {object} {status, msg, error(optional)} + */ +async function getFlowById(id) { + try { + const accessToken = await getBLSAccessToken() + let getFlow = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE}/redui/flow/${id}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Node-RED-Deployment-Type': 'flows', + 'Authorization': accessToken + } + }) + return getFlow.data + } catch (error) { + return { + status: 'error', + msg: error, + error + } + } +} + +function cleanDuplicateNodes(flow) { + let checked = [] + let indexToRemove = [] + let lintoConfigIndex = null + for (let i = 0; i < flow.length; i++) { + let type = flow[i].type + if (type === 'linto-config') { + lintoConfigIndex = i + } + if (checked.indexOf(type) >= 0) { + indexToRemove.push(i) + } else { + checked.push(type) + } + } + + let cleanedFlow = flow.filter(function(value, index) { + return indexToRemove.indexOf(index) == -1 + }) + return cleanedFlow +} + + + +module.exports = { + cleanDuplicateNodes, + deleteBLSFlow, + formatFlowGroupedNodes, + getBLSAccessToken, + generateMultiUserApplicationFromBaseTemplate, + generateDeviceApplicationFromBaseTemplate, + getFlowById, + postBLSFlow, + putBLSFlow, + updateMultiUserApplicationFlowSettings +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/_root/index.js b/platform/linto-admin/webserver/lib/webserver/routes/_root/index.js new file mode 100644 index 0000000..d2c11ee --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/_root/index.js @@ -0,0 +1,16 @@ +const debug = require('debug')('linto-admin:login') + +module.exports = (webServer) => { + return [{ + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + res.redirect('/login') + } catch (err) { + console.error(err) + } + } + }] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/admin/index.js b/platform/linto-admin/webserver/lib/webserver/routes/admin/index.js new file mode 100644 index 0000000..9510557 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/admin/index.js @@ -0,0 +1,13 @@ +const debug = require('debug')('linto-admin:routes/admin') + +module.exports = (webServer) => { + return [{ + path: '/*', + method: 'get', + requireAuth: true, + controller: (req, res, next) => { + res.setHeader("Content-Type", "text/html") + res.sendFile(process.cwd() + '/dist/index.html') + } + }] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/androidusers/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/androidusers/index.js new file mode 100644 index 0000000..a38f28b --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/androidusers/index.js @@ -0,0 +1,335 @@ +const applicationWorkflowsModel = require(`${process.cwd()}/model/mongodb/models/workflows-application.js`) +const androidUsersModel = require(`${process.cwd()}/model/mongodb/models/android-users.js`) +const mqttdUsersModel = require(`${process.cwd()}/model/mongodb/models/mqtt-users.js`) +module.exports = (webServer) => { + return [{ + // Get all android users + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Request + const getAndroidUsers = await androidUsersModel.getAllAndroidUsers() + + // Response + res.json(getAndroidUsers) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, { + // Create a new android user + /* + payload = { + email: String (android user email) + pswd: String (android user password) + applications: Array (Array of workflow_id) + } + */ + path: '/', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + + // Request + const createUser = await androidUsersModel.createAndroidUsers(payload) + + // Response + if (createUser === 'success') { + res.json({ + status: 'success', + msg: `The user "${payload.email}" has been created".` + }) + } else { + throw `Error on creating user "${payload.email}"` + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Remove an application for all android users + /* + paylaod = { + _id: String (application workflow_id), + name: String (application worfklow name), + } + */ + path: '/applications', + method: 'patch', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + + // Request + const updateAndroidUsers = await androidUsersModel.removeApplicationFromAndroidUsers(payload._id) + + // Response + if (updateAndroidUsers === 'success') { + res.json({ + status: 'success', + msg: `All users have been removed from application ${payload.name}` + }) + } else { + throw updateAndroidUsers.msg + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Add an application to an android user + /* + payload = { + applications: Array (Array of application workflow_id) + } + */ + path: '/:userId/applications', + method: 'put', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + const userId = req.params.userId + const applicationsToAdd = payload.applications + + // Get android user data + const getAndroidUser = await androidUsersModel.getUserById(userId) + + // Format data for update + let user = getAndroidUser + user.applications.push(...applicationsToAdd) + + // Request + const updateUser = await androidUsersModel.updateAndroidUser(user) + + // Response + if (updateUser === 'success') { + res.json({ + status: 'success', + msg: `New applications have been attached to user "${user.email}"` + }) + } else { + throw 'Error on updating user' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Dissociate an android user from an android application + path: '/:userId/applications/:applicationId/remove', + method: 'patch', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const applicationId = req.params.applicationId + const userId = req.params.userId + + // get android user data + const user = await androidUsersModel.getUserById(userId) + + // get application workflow data + const applicationWorkflow = await applicationWorkflowsModel.getApplicationWorkflowById(applicationId) + + // Format data for update + let filteredApps = user.applications.filter(app => app !== applicationId) + + // Remove MQTT user if user is no longer attached to any application + if (filteredApps.length === 0 && !!user.email) { + await mqttdUsersModel.deleteMqttUserByEmail(user.email) + } + + // Request + const updateUser = await androidUsersModel.updateAndroidUser({ + _id: userId, + applications: filteredApps + }) + + // Response + if (updateUser === 'success') { + res.json({ + status: 'success', + msg: `The user "${user.email}" has been dissociated from application "${applicationWorkflow.name}"` + }) + } else { + throw 'Error on updating android application user' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Get an android user by its id + path: '/:userId', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const userId = req.params.userId + + // Request + const getAndroidUser = await androidUsersModel.getUserById(userId) + + // Response + res.json(getAndroidUser) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Update an android user + path: '/:userId', + method: 'put', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + + // Request + const updateAndroidUser = await androidUsersModel.updateAndroidUser(payload) + + if (updateAndroidUser === 'success') { + res.json({ + status: 'success', + msg: `User ${payload.email} has been updated` + }) + } + + // Response + res.json(getAndroidUser) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Update an android user + path: '/:userId/pswd', + method: 'put', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + + if (payload.newPswd === payload.newPswdConfirmation) { + const userPayload = { + _id: payload._id, + pswd: payload.newPswd + } + const updateUserPswd = await androidUsersModel.upadeAndroidUserPassword(userPayload) + + // Remove MQTT user on password change + if (payload.email) { + await mqttdUsersModel.deleteMqttUserByEmail(payload.email) + } + + if (updateUserPswd === 'success') { + res.json({ + status: 'success', + msg: `User ${payload.email} has been updated` + }) + } else { + throw `Error on updating user ${payload.email}` + } + } else { + throw 'Password and confirmation password don\'t match' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Delete an android user + /* + payload = { + email : String (android user email) + } + */ + path: '/:userId', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const userId = req.params.userId + const payload = req.body.payload + + const getUserById = await androidUsersModel.getUserById(userId) + + // Remove MQTT user + if (!!getUserById.email) { + await mqttdUsersModel.deleteMqttUserByEmail(getUserById.email) + } + // Request + const removeUser = await androidUsersModel.deleteAndroidUser(userId) + + // Response + if (removeUser === 'success') { + res.json({ + status: 'success', + msg: `Android user ${payload.email} has been removed` + }) + } else { + throw `Error on removing user ${payload.email}` + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/clients/static/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/clients/static/index.js new file mode 100644 index 0000000..0b00710 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/clients/static/index.js @@ -0,0 +1,190 @@ +const clientsStaticModel = require(`${process.cwd()}/model/mongodb/models/clients-static.js`) +const workflowsStaticModel = require(`${process.cwd()}/model/mongodb/models/workflows-static.js`) +const nodered = require(`${process.cwd()}/lib/webserver/middlewares/nodered.js`) + +module.exports = (webServer) => { + return [{ + // Get all static devices from database + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Request + const getStaticClients = await clientsStaticModel.getAllStaticClients() + + // Response + res.json(getStaticClients) + } catch (error) { + console.error(error) + res.json({ error }) + } + } + }, + { + // Get a static device by its serial number + path: '/:sn', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const sn = req.params.sn + + // Request + const getStaticClient = await clientsStaticModel.getStaticClientBySn(sn) + + // Response + res.json(getStaticClient) + } catch (error) { + console.error(error) + res.json({ error }) + } + } + }, + { + // Create a new static device + path: '/', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + const sn = payload.sn + const addStaticDevice = await clientsStaticModel.addStaticClient(sn) + if (addStaticDevice === 'success') { + res.json({ + status: 'success', + msg: `The device with serial number "${sn}" has been added.` + }) + } else { + throw addStaticDevice + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Replace a static device Serial Number by a target one (BLS + Database) + path: '/replace', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + + // get static workflow data + const getWorklfow = await workflowsStaticModel.getStaticWorkflowById(payload.workflow._id) + + // format data for update + let workflowPayload = getWorklfow + workflowPayload.associated_device = payload.targetDevice + workflowPayload.flow.nodes.map(node => { + if (node.type === 'linto-terminal-in') { + node.sn = payload.targetDevice + } + }) + + // Update static workflow in DB + const updateWorkflow = await workflowsStaticModel.updateStaticWorkflow(workflowPayload) + if (updateWorkflow === 'success') { + + // Update flow on BLS + const updateBLS = await nodered.putBLSFlow(workflowPayload.flowId, workflowPayload.flow) + if (updateBLS.status === 'success') { + + // Update static devices (orignal) + const updateCurrentDevice = await clientsStaticModel.updateStaticClient({ sn: payload.sn, associated_workflow: null }) + + // Update static devices (target) + const updateTargetDevice = await + clientsStaticModel.updateStaticClient({ sn: payload.targetDevice, associated_workflow: payload.workflow }) + + // Response + if (updateCurrentDevice === 'success' && updateTargetDevice === 'success')  { + res.json({ + status: 'success', + msg: `The device "${payload.sn}" has been replaced by device "${payload.targetDevice}"` + }) + } else { + throw 'Error on updating devices' + } + throw 'Error on updating workflow on Business logic server' + } + throw 'Error on updating workflow on database' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Update a static client + path: '/:sn', + method: 'patch', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + let payload = req.body.payload + const sn = req.params.sn + payload.sn = sn + + // Request + const updateStaticClient = await clientsStaticModel.updateStaticClient(payload) + + // Response + if (updateStaticClient === 'success') { + res.json({ + status: 'success', + msg: `The device "${payload.sn}" has been updated` + }) + } else { + throw `Error on updating device "${payload.sn}"` + } + } catch (error) { + console.error(error) + res.json({ error }) + } + } + }, + { + // Delete a LinTO static device by its serial number + path: '/:sn', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const sn = req.params.sn + + // Request + const deleteClient = await clientsStaticModel.deleteStaticDevice(sn) + + // Response + if (deleteClient === 'success') { + res.json({ + status: 'success', + msg: `The device with serial number "${sn}" has been deleted.` + }) + } else { + throw 'Error on deleting device' + } + } catch (error) { + console.error(error) + res.json({ error }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/flow/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/flow/index.js new file mode 100644 index 0000000..4e84350 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/flow/index.js @@ -0,0 +1,187 @@ +const axios = require('axios') +const middlewares = require(`${process.cwd()}/lib/webserver/middlewares/index.js`) +const nodered = require(`${process.cwd()}/lib/webserver/middlewares/nodered.js`) + +module.exports = (webServer) => { + return [{ + path: '/healthcheck', + method: 'get', + requireAuth: false, + controller: async(req, res, next) => { + try { + const accessToken = await nodered.getBLSAccessToken() + const getBls = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE + process.env.LINTO_STACK_BLS_SERVICE_UI_PATH}`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': accessToken + } + }) + if (getBls.status === 200) { + res.json({ + status: 'success', + msg: '' + }) + } else { + throw 'error on connecting' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'unable to connect Business logic server', + error + }) + } + } + }, + { + // Delete a flow from BLS by its flowId + path: '/:flowId', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const flowId = req.params.flowId + + // Request + const deleteFlow = await nodered.deleteBLSFlow(flowId) + + // Response + if (deleteFlow.status === 'success') { + res.json({ + status: 'success', + msg: `The workflow "${flowId}" has been removed` + }) + } else { + throw `Error on deleting flow ${flowId} on the Business Logic Server` + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: error, + error + }) + } + } + }, + + + { + // Get Business Logic Server credentials for requests + path: '/getauth', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Request + const accessToken = await nodered.getBLSAccessToken() + + // Response + res.json({ + token: accessToken + }) + } catch (error) { + res.json({ error }) + } + } + }, + { + // Post flow on BLS on context creation + path: '/postbls/device', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + let formattedFlow = null + formattedFlow = nodered.generateDeviceApplicationFromBaseTemplate(payload) + + // Request + if (formattedFlow !== null) { + const postFlowOnBLS = await nodered.postBLSFlow(formattedFlow) + + // Response + res.json(postFlowOnBLS) + + } else { + throw ('Error on formatting flow') + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + + { + // Post flow on BLS on context creation + path: '/postbls/application', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + let formattedFlow = null + formattedFlow = nodered.generateMultiUserApplicationFromBaseTemplate(payload) + + // Request + if (formattedFlow !== null) { + const postFlowOnBLS = await nodered.postBLSFlow(formattedFlow) + + // Response + res.json(postFlowOnBLS) + + } else { + throw ('Error on formatting flow') + } + + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Get all the installed nodes + path: '/nodes', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const accessToken = await nodered.getBLSAccessToken() + const getNodes = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE + process.env.LINTO_STACK_BLS_SERVICE_UI_PATH}/nodes`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': accessToken + } + }) + if (getNodes.status === 200) { + res.json({ nodes: getNodes.data }) + } else { + throw getNodes + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on getting installed nodes' + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/flow/node/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/flow/node/index.js new file mode 100644 index 0000000..f0f8a71 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/flow/node/index.js @@ -0,0 +1,94 @@ +const axios = require('axios') +const middlewares = require(`${process.cwd()}/lib/webserver/middlewares/index.js`) +const nodered = require(`${process.cwd()}/lib/webserver/middlewares/nodered.js`) + +module.exports = (webServer) => { + return [{ + path: '/', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const nodeId = req.body.module + const accessToken = await nodered.getBLSAccessToken() + const installNode = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE + process.env.LINTO_STACK_BLS_SERVICE_UI_PATH}/nodes`, { + method: 'post', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': accessToken + }, + data: { module: nodeId } + }) + if (installNode.status === 200) { + res.json({ + status: 'success', + msg: `The skill "${nodeId}" has been uninstalled` + }) + } else { + throw installNode + } + } catch (error) { + console.error(error) + + // If module is already loaded + if (!!error.response && !!error.response.status && error.response.status === 400 && !!error.response.data && !!error.response.data.message) { + res.json({ + status: 'error', + msg: error.response.data.message + }) + } else { + res.json({ + status: 'error', + msg: `error on installing node "${nodeId}"` + }) + } + } + } + }, + { + path: '/remove', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + const nodeId = req.body.nodeId + const accessToken = await nodered.getBLSAccessToken() + const uninstallNode = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_BLS_SERVICE + process.env.LINTO_STACK_BLS_SERVICE_UI_PATH}/nodes/${nodeId}`, { + method: 'delete', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': accessToken + } + }) + if (uninstallNode.status === 204) { + res.json({ + status: 'success', + msg: `The skill "${nodeId}" has been uninstalled` + }) + } else { + throw uninstallNode + } + } catch (error) { + console.error(error) + + // If module is already loaded + if (!!error.response && !!error.response.status && error.response.status === 400 && !!error.response.data && !!error.response.data.message) { + res.json({ + status: 'error', + msg: error.response.data.message + }) + } else { + res.json({ + status: 'error', + msg: `error on uninstalling node "${req.body.module}"` + }) + } + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/index.js new file mode 100644 index 0000000..74179c8 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/index.js @@ -0,0 +1,11 @@ +const debug = require('debug')('linto-admin:routes/api') + +module.exports = (webServer) => { + return [{ + path: '/', + method: 'get', + controller: (req, res, next) => { + res.redirect('/login') + } + }] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/localskills/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/localskills/index.js new file mode 100644 index 0000000..6243c30 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/localskills/index.js @@ -0,0 +1,80 @@ +const axios = require('axios') +const localSkillsModel = require(`${process.cwd()}/model/mongodb/models/local-skills.js`) +const moment = require('moment') + +module.exports = (webServer) => { + return [{ + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const localSkills = await localSkillsModel.getLocalSkills() + res.json(localSkills) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on joining STT service', + error + }) + } + } + }, + { + path: '/', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + let payload = req.body + payload.created_date = moment().format() + const addSkill = await localSkillsModel.addLocalSkill(payload) + + if (addSkill === 'success') { + res.json({ + status: 'success', + msg: `module ${payload.name} has been installed` + }) + } else { + throw addSkill + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on adding local skill to DB', + error + }) + } + } + }, + { + path: '/:skillId', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + const nodeId = req.params.skillId + const nodeName = req.body.name + const removeSkill = await localSkillsModel.removeLocalSkill(nodeId) + if (removeSkill === 'success') { + res.json({ + status: 'success', + msg: `module ${nodeName} has been uninstalled` + }) + } else { + throw removeSkill + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on removing local skill from DB', + error + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/stt/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/stt/index.js new file mode 100644 index 0000000..1f58a4f --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/stt/index.js @@ -0,0 +1,180 @@ +const axios = require('axios') +const multer = require('multer') +const moment = require('moment') +const lexSeed = require(`${process.cwd()}/lib/webserver/middlewares/lexicalseeding.js`) +const middlewares = require(`${process.cwd()}/lib/webserver/middlewares/index.js`) +const AMPath = `${process.cwd()}/acousticModels/` +const AMstorage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, AMPath) + }, + filename: (req, file, cb) => { + let filename = moment().format('x') + '-' + file.originalname + cb(null, filename) + } +}) + +module.exports = (webServer) => { + return [{ + // Get all services in stt-service-manager + path: '/services', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getServices = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/services`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + res.json(getServices.data.data) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on joining STT service', + error + }) + } + } + }, + { + // Get lang models + path: '/langmodels', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getLanguageModels = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/langmodels`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + res.json(getLanguageModels.data.data) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on getting STT language models', + error + }) + } + } + }, + { + path: '/acmodels', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + + const getACModels = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE}/acmodels`, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + res.json(getACModels.data.data) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on getting STT acoustic models', + error + }) + } + } + }, + { + path: '/lexicalseeding', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const flowId = req.body.payload.flowId + const service_name = req.body.payload.service_name + const lexicalseeding = await lexSeed.sttLexicalSeeding(flowId, service_name) + if (lexicalseeding.status === 'success') { + res.json({ + status: 'success', + msg: 'STT service has been updated' + }) + } else { + throw lexicalseeding + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: !!error.msg ? error.msg : 'Error on updating language model', + error: error + }) + } + } + }, + { + path: '/generategraph', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const serviceName = req.body.serviceName + const lexicalSeeding = await lexSeed.generateGraph(serviceName) + res.json(lexicalSeeding) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'error on generating graph' + }) + } + } + }, + { + path: '/healthcheck', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const sttAuthToken = middlewares.basicAuthToken(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_LOGIN, process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE_PASSWORD) + const getSttManager = await axios(middlewares.useSSL() + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE, { + method: 'get', + headers: { + 'charset': 'utf-8', + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': sttAuthToken + } + }) + if (getSttManager.status === 200) { + res.json({ + status: 'success', + msg: '' + }) + } else { + throw 'error on connecting' + } + } catch (error) { + res.json({ + status: 'error', + msg: 'unable to connect STT services' + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/swagger/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/swagger/index.js new file mode 100644 index 0000000..e7e9a34 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/swagger/index.js @@ -0,0 +1,15 @@ +const swaggerUi = require('swagger-ui-express'); +const swaggerDocument = require(`${process.cwd()}/doc/swagger.json`); + +module.exports = (webServer) => { + return [{ + // Get all android users + path: '/', + method: 'get', + requireAuth: false, + controller: async(req, res, next) => { + swaggerUi.serve + swaggerUi.setup(swaggerDocument) + } + }] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/tock/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/tock/index.js new file mode 100644 index 0000000..6f57282 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/tock/index.js @@ -0,0 +1,97 @@ +const debug = require('debug')(`linto-admin:/api/tock`) +const middlewares = require(`${process.cwd()}/lib/webserver/middlewares/index.js`) +const lexSeed = require(`${process.cwd()}/lib/webserver/middlewares/lexicalseeding.js`) +const axios = require('axios') +module.exports = (webServer) => { + return [{ + // Get all existing Tock applications + path: '/applications', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const tockToken = middlewares.basicAuthToken(process.env.LINTO_STACK_TOCK_USER, process.env.LINTO_STACK_TOCK_PASSWORD) + const getTockApplications = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_TOCK_SERVICE}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}${process.env.LINTO_STACK_TOCK_BASEHREF}/rest/admin/applications`, { + method: 'get', + headers: { + 'Authorization': tockToken + } + }) + if (!!getTockApplications.data && getTockApplications.data.length > 0) { + res.json(getTockApplications.data) + } else { + // If no application is created + res.json([]) + } + } catch (error) { + console.error(error) + if ((!!error.response && !!error.response === undefined) || (!!error.code && error.code === 'ECONNREFUSED')) { + res.json({ + status: 'error', + msg: 'Tock service unvavailable' + }) + } + res.json({ + status: 'error', + msg: 'Error on getting tock applications' + }) + } + } + }, + { + path: '/healthcheck', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const tockToken = middlewares.basicAuthToken(process.env.LINTO_STACK_TOCK_USER, process.env.LINTO_STACK_TOCK_PASSWORD) + const getTock = await axios(`${middlewares.useSSL() + process.env.LINTO_STACK_TOCK_SERVICE}:${process.env.LINTO_STACK_TOCK_SERVICE_PORT}${process.env.LINTO_STACK_TOCK_BASEHREF}/rest/admin/applications`, { + method: 'get', + headers: { + 'Authorization': tockToken + } + }) + if (getTock.status === 200) { + res.json({ + status: 'success', + msg: '' + }) + } else { + throw 'error on connecting' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'unable to connect tock services' + }) + } + } + }, + { + path: '/lexicalseeding', + method: 'post', + controller: async(req, res, next) => { + try { + const flowId = req.body.payload.flowId + const lexicalSeeding = await lexSeed.nluLexicalSeeding(flowId) + if (lexicalSeeding.status === 'success') { + res.json({ + status: 'success', + msg: 'Tock application updated' + }) + } else { + throw 'Error on updating tock application' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: error, + error + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/webapphosts/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/webapphosts/index.js new file mode 100644 index 0000000..e97aebd --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/webapphosts/index.js @@ -0,0 +1,277 @@ +const webappHostsModel = require(`${process.cwd()}/model/mongodb/models/webapp-hosts.js`) +const applicationWorkflowsModel = require(`${process.cwd()}/model/mongodb/models/workflows-application.js`) + +module.exports = (webServer) => { + return [{ + // Get all webapp hosts + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Request + const getWebAppHosts = await webappHostsModel.getAllWebAppHosts() + + // Response + res.json(getWebAppHosts) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Create a webapp host + path: '/', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + + // Request + const createWebappHost = await webappHostsModel.createWebAppHost(payload) + + // Response + if (createWebappHost === 'success') { + res.json({ + status: 'success', + msg: `The domain "${payload.originUrl}" has been created` + }) + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Create a webapp host + path: '/:id', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + const webappHostId = req.params.id + const payload = req.body.payload + + // Request + const removeWebappHost = await webappHostsModel.deleteWebAppHost(webappHostId) + + // Response + if (removeWebappHost === 'success') { + res.json({ + status: 'success', + msg: `The domain "${payload.originUrl}" has been removed` + }) + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Update a webapp host + path: '/:id', + method: 'put', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + + // Request + const updateWebappHost = await webappHostsModel.updateWebAppHost(payload) + + // Response + if (updateWebappHost === 'success') { + res.json({ + status: 'success', + msg: `The domain "${payload.originUrl}" has been updated` + }) + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Dissociate an application from a web app host + path: '/:webappHostId/applications/:applicationId', + method: 'patch', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + const applicationId = req.params.applicationId + const applicationWorkflow = await applicationWorkflowsModel.getApplicationWorkflowById(applicationId) + let webappHost = payload.webappHost + webappHost.applications.splice(webappHost.applications.findIndex(item => item.applicationId === applicationId), 1) + + // Request + const updateWebappHost = await webappHostsModel.updateWebappHost(webappHost) + + // Response + if (updateWebappHost === 'success') { + res.json({ + status: 'success', + msg: `The domain "${webappHost.originUrl}" has been dissociated from the application "${applicationWorkflow.name}"` + }) + } else { + throw 'Error on updating domain' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Add an application to an android user + /* + payload = { + applications: Array (Array of application workflow_id) + } + */ + path: '/:webappHostId/applications', + method: 'put', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + const webappHostId = req.params.webappHostId + const applicationsToAdd = payload.applications + + // get Webapp host data + const getWebappHost = await webappHostsModel.getWebappHostById(webappHostId) + + // Format data for update + let webappHost = getWebappHost + webappHost.applications.push(...applicationsToAdd) + + // Request + const updateWebappHost = await webappHostsModel.updateWebappHost(webappHost) + + // Response + if (updateWebappHost === 'success') { + res.json({ + status: 'success', + msg: `New applications have been attached to domain "${webappHost.originUrl}"` + }) + } else { + throw 'Error on updating domain applications' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Update a webappHost application + /* + payload = { + webappHostId: webappHost._id, + applicationId:app.applicationId, + maxSlots: { + value: app.maxSlots, + error: null, + valid: true + }, + requestToken: app.requestToken + } + */ + path: '/:webappHostId/applications', + method: 'patch', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + const webappHostId = req.params.webappHostId + let webappHost = await webappHostsModel.getWebappHostById(webappHostId) + + webappHost.applications[webappHost.applications.findIndex(item => item.applicationId === payload.applicationId)].requestToken = payload.requestToken + webappHost.applications[webappHost.applications.findIndex(item => item.applicationId === payload.applicationId)].maxSlots = parseInt(payload.maxSlots.value) + + // Request + const updateWebappHost = await webappHostsModel.updateWebappHost(webappHost) + + // Response + if (updateWebappHost === 'success') { + res.json({ + status: 'success', + msg: `New applications have been attached to domain "${webappHost.originUrl}"` + }) + } else { + throw 'Error on updating domain applications' + } + + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Remove an application for all web-application hosts + /* + paylaod = { + _id: String (application workflow_id), + name: String (application worfklow name), + } + */ + path: '/applications', + method: 'patch', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + + // Request + const updateWebappHost = await webappHostsModel.removeApplicationForAllHosts(payload._id) + + // Response + if (updateWebappHost === 'success') { + res.json({ + status: 'success', + msg: `The application ${payload.name} has been removed from all registered domains` + }) + } else { + throw updateWebappHost + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error: 'Error on deleting application from registered domains' + }) + } + } + }, + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/application/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/application/index.js new file mode 100644 index 0000000..9730521 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/application/index.js @@ -0,0 +1,225 @@ +const nodered = require(`${process.cwd()}/lib/webserver/middlewares/nodered.js`) +const moment = require('moment') +const applicationWorkflowsModel = require(`${process.cwd()}/model/mongodb/models/workflows-application.js`) +const androidUsersModel = require(`${process.cwd()}/model/mongodb/models/android-users.js`) + +module.exports = (webServer) => { + return [{ + // Get all application workflows from database + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const getApplicationWorkflows = await applicationWorkflowsModel.getAllApplicationWorkflows() + res.json(getApplicationWorkflows) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Create a new application workflow + path: '/', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + const getPostedFlow = await nodered.getFlowById(payload.flowId) + + // Create workflow + const workflowPayload = { + name: payload.workflowName, + description: payload.workflowDescription, + flowId: payload.flowId, + created_date: moment().format(), + updated_date: moment().format(), + flow: getPostedFlow + } + + const postWorkflow = await applicationWorkflowsModel.postApplicationWorkflow(workflowPayload) + if (postWorkflow === 'success') { + res.json({ + status: 'success', + msg: `The multi-user application "${payload.workFlowName} has been created` + }) + } else { + throw postWorkflow + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Get an application workflow by its id + path: '/:id', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const workflowId = req.params.id + + // Request + const getApplicationWorkflow = await applicationWorkflowsModel.getApplicationWorkflowById(workflowId) + + // Response + if (!!getApplicationWorkflow.error) { + throw getApplicationWorkflow.error + } else { + res.json(getApplicationWorkflow) + } + } catch (error) { + console.error(error) + res.json({ error }) + } + } + }, + { + // Delete a workflow application + path: '/:workflowId', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + const workflowId = req.params.workflowId + const workflowName = req.body.workflowName + const removeApplication = await applicationWorkflowsModel.deleteApplicationWorkflow(workflowId) + if (removeApplication === 'success') { + res.json({ + status: 'success', + msg: `The multi-user application "${workflowName}" has been removed.` + }) + } else { + throw removeApplication.msg + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Get android users list by workflow ID + path: '/:workflowId/androidusers', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const workflowId = req.params.workflowId + const getAndroidUsers = await androidUsersModel.getAllAndroidUsers() + + const users = getAndroidUsers.filter(user => workflowId.indexOf(user.applications) >= 0) + + res.json(users) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // Get android users list by workflow ID + path: '/:workflowId/androidAuth', + method: 'put', + requireAuth: true, + controller: async(req, res, next) => { + try { + const workflowId = req.params.workflowId + + const getApplicationWorkflow = await applicationWorkflowsModel.getApplicationWorkflowById(workflowId) + let applicationPayload = getApplicationWorkflow + + const nodeIndex = applicationPayload.flow.nodes.findIndex(node => node.type === 'linto-application-in') + + if (nodeIndex >= 0) { + applicationPayload.flow.nodes[nodeIndex].auth_android = !applicationPayload.flow.nodes[nodeIndex].auth_android + + const updateApplicationWorkflow = await applicationWorkflowsModel.updateApplicationWorkflow(applicationPayload) + + if (updateApplicationWorkflow === 'success') { + const putBls = await nodered.putBLSFlow(applicationPayload.flowId, applicationPayload.flow) + + if (putBls.status === 'success')  { + res.json({ + status: 'success', + msg: `The multi-user application ${applicationPayload.name} has been updated` + }) + } + } else  { + throw 'Error on updating multi-user application' + } + } else  { + throw 'Cannot find users authentication settings' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + // update application domains + path: '/:workflowId/webappAuth', + method: 'put', + requireAuth: true, + controller: async(req, res, next) => { + try { + const workflowId = req.params.workflowId + + const getApplicationWorkflow = await applicationWorkflowsModel.getApplicationWorkflowById(workflowId) + let applicationPayload = getApplicationWorkflow + + const nodeIndex = applicationPayload.flow.nodes.findIndex(node => node.type === 'linto-application-in') + + if (nodeIndex >= 0) { + applicationPayload.flow.nodes[nodeIndex].auth_web = !applicationPayload.flow.nodes[nodeIndex].auth_web + + const updateApplicationWorkflow = await applicationWorkflowsModel.updateApplicationWorkflow(applicationPayload) + + if (updateApplicationWorkflow === 'success') { + const putBls = await nodered.putBLSFlow(applicationPayload.flowId, applicationPayload.flow) + + if (putBls.status === 'success')  { + res.json({ + status: 'success', + msg: `The multi-user application ${applicationPayload.name} has been updated` + }) + } + + + } else  { + throw 'Error on updating multi-user application' + } + } else  { + throw 'Cannot find domains authentication settings' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/index.js new file mode 100644 index 0000000..dbe15af --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/index.js @@ -0,0 +1,137 @@ +const workflowsStaticModel = require(`${process.cwd()}/model/mongodb/models/workflows-static.js`) +const workflowsApplicationModel = require(`${process.cwd()}/model/mongodb/models/workflows-application.js`) +const tmpFlowModel = require(`${process.cwd()}/model/mongodb/models/flow-tmp.js`) +const nodered = require(`${process.cwd()}/lib/webserver/middlewares/nodered.js`) +const lexSeed = require(`${process.cwd()}/lib/webserver/middlewares/lexicalseeding.js`) +const moment = require('moment') + +module.exports = (webServer) => { + return [ + + { + path: '/:id/services/multiuser', + method: 'patch', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + const workflowId = req.params.id + let application = null + + if (payload.type === 'device') { + application = await workflowsStaticModel.getStaticWorkflowById(workflowId) + } else if (payload.type === 'application') { + application = await workflowsApplicationModel.getApplicationWorkflowById(workflowId) + } + + if (application.name !== payload.applicationName) { + application.name = payload.applicationName + } + if (application.description !== payload.applicationDescription) { + application.description = payload.applicationDescription + } + + let updatedFlow = nodered.updateMultiUserApplicationFlowSettings(application.flow, payload) + + const updateBls = await nodered.putBLSFlow(updatedFlow.id, updatedFlow) + if (updateBls.status === 'success') { + application.name = payload.applicationName + application.description = payload.applicationDescription + application.updated_date = moment().format() + application.flow = updatedFlow + let updateApplication = null + if (payload.type === 'device') { + updateApplication = await workflowsStaticModel.updateStaticWorkflow(application) + } else if (payload.type === 'application') { + updateApplication = await workflowsApplicationModel.updateApplicationWorkflow(application) + } + + if (updateApplication === 'success') { + res.json({ + status: 'success', + msg: 'Workflow updated' + }) + } else { + throw 'Error on updating application' + } + + } else { + throw 'Error on updating BLS' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error + }) + } + } + }, + { + path: '/saveandpublish', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + const payload = req.body.payload + + // Get tmp flow + const getTmpFlow = await tmpFlowModel.getTmpFlow() + const formattedFlow = nodered.formatFlowGroupedNodes(getTmpFlow) + + // Update BLS + const putBls = await nodered.putBLSFlow(payload.noderedFlowId, formattedFlow) + if (putBls.status === 'success') { + const getUdpatedFlow = await nodered.getFlowById(payload.noderedFlowId) + let updateWorkflow + + if (payload.type === 'static') { // Static + // update static workflow + updateWorkflow = await workflowsStaticModel.updateStaticWorkflow({ + _id: payload.workflowId, + flow: getUdpatedFlow, + updated_date: moment().format() + }) + } else if (payload.type === 'application') { // Application + // update application workflow + updateWorkflow = await workflowsApplicationModel.updateApplicationWorkflow({ + _id: payload.workflowId, + flow: getUdpatedFlow, + updated_date: moment().format() + }) + } + if (updateWorkflow === 'success') { + // Lexical Seeding + const sttService = formattedFlow.nodes.filter(f => f.type === 'linto-config-transcribe') + if (sttService.length > 0 && !!sttService[0].commandOffline) { + const lexicalSeeding = await lexSeed.doLexicalSeeding(sttService[0].commandOffline, payload.noderedFlowId) + if (lexicalSeeding.status === 'success') { + res.json({ + status: 'success', + msg: `The application "${payload.workflowName}" has been updated` + }) + } else { + throw { + msg: 'Workflow updated but error on lexical seeding', + lexicalSeeding + } + } + } + } else { + throw  `Error on updating application "${payload.workflowName}"` + } + } else { + throw 'Error on updating flow on Business Logic Server' + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: !!error.msg ? error.msg : 'Error on updating workflow', + error + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/static/index.js b/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/static/index.js new file mode 100644 index 0000000..ac8af4f --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/api/workflows/static/index.js @@ -0,0 +1,189 @@ +const workflowsStaticModel = require(`${process.cwd()}/model/mongodb/models/workflows-static.js`) +const clientsStaticModel = require(`${process.cwd()}/model/mongodb/models/clients-static.js`) +const nodered = require(`${process.cwd()}/lib/webserver/middlewares/nodered.js`) +const moment = require('moment') + +module.exports = (webServer) => { + return [{ + // Get all static workflows from database + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Request + const getStaticWorkflows = await workflowsStaticModel.getAllStaticWorkflows() + + // Response + res.json(getStaticWorkflows) + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on getting device workflows', + error + }) + } + } + }, + { + // Get a static workflow by its id + path: '/:id', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const workflowId = req.params.id + + // Request + const getStaticWorkflow = await workflowsStaticModel.getStaticWorkflowById(workflowId) + + // Response + if (!!getStaticWorkflow.error) { + throw getStaticWorkflow.error + } else { + res.json(getStaticWorkflow) + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on getting device workflow', + error + }) + } + } + }, + { + // Get a static workflow by its name + path: '/name/:name', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + const name = req.params.name + + // Request + const getStaticWorkflow = await workflowsStaticModel.getStaticWorkflowByName(name) + + // Response + if (!!getStaticWorkflow.error) { + throw getStaticWorkflow.error + } else { + res.json(getStaticWorkflow) + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on getting device workflow', + error + }) + } + } + }, + { + // Create a new static workflow + /* + payload : { + sn: String, + workflowName: String, + workflowTemplate: String, + sttServiceLanguage: String, + sttService: String, + tockApplicationName: String + } + */ + + path: '/', + method: 'post', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + + // get flow object + const getPostedFlow = await nodered.getFlowById(payload.flowId) + + // Create workflow + const workflowPayload = { + name: payload.workflowName, + flowId: payload.flowId, + description: payload.workflowDescription, + created_date: moment().format(), + updated_date: moment().format(), + associated_device: payload.device, + flow: getPostedFlow + } + + // Request + const postWorkflow = await workflowsStaticModel.postStaticWorkflow(workflowPayload) + + // Response + if (postWorkflow === 'success') { + res.json({ + status: 'success', + msg: `The device application "${payload.workFlowName} has been created` + }) + } else { + throw postWorkflow + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on creating device application', + error + }) + } + } + }, + { + // Remove a static workflow and dissociate Static device and workflow template + + path: '/:id', + method: 'delete', + requireAuth: true, + controller: async(req, res, next) => { + try { + // Set variables & values + const payload = req.body.payload + const workflowId = req.params.id + + // Get static workflow + const getWorkflow = await workflowsStaticModel.getStaticWorkflowById(workflowId) + const staticDeviceSn = payload.sn + + // Delete workflow from BLS + // "Success" is not required (if the workflow has been removed manually for exemple) + await nodered.deleteBLSFlow(getWorkflow.flowId) + + // Update static client in DB + const updateStaticDevice = await clientsStaticModel.updateStaticClient({ sn: staticDeviceSn, associated_workflow: null }) + if (updateStaticDevice === 'success') { + // Delete Static workflow from DB + const deleteStaticWorkflow = await workflowsStaticModel.deleteStaticWorkflowById({ _id: workflowId }) + if (deleteStaticWorkflow === 'success') { + res.json({ + status: 'success', + msg: `The device "${staticDeviceSn}" has been dissociated from device application "${getWorkflow.name}"` + }) + } else { + throw `Error on updating device "${staticDeviceSn}"` + } + } else { + throw `Error on deleting device application "${getWorkflow.name}"` + } + } catch (error) { + console.error(error) + res.json({ + status: 'error', + msg: 'Error on removing device application', + error + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/healthcheck/index.js b/platform/linto-admin/webserver/lib/webserver/routes/healthcheck/index.js new file mode 100644 index 0000000..4b75913 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/healthcheck/index.js @@ -0,0 +1,85 @@ +const debug = require('debug')('linto-admin:routes/api/healthcheck') +const MongoDriver = require(`${process.cwd()}/model/mongodb/driver.js`) +module.exports = (webServer) => { + return [{ + path: '/', + method: 'get', + requireAuth: false, + controller: async(req, res, next) => { + try { + const mongoUp = MongoDriver.constructor.checkConnection() + const redisUp = await webServer.app.redis.checkConnection() + res.json([{ + service: 'mongo', + connected: mongoUp + }, { + service: 'redis', + connected: redisUp + }]) + } catch (error) { + console.error(error) + res.json( + [{ + service: 'mongo', + connected: mongoUp + }, + { + service: 'redis', + connected: redisUp + }, + { + error + } + ]) + } + } + }, { + path: '/overview', + method: 'get', + requireAuth: false, + controller: async(req, res, next) => { + res.setHeader("Content-Type", "text/html") + res.sendFile(process.cwd() + '/dist/healthcheck.html') + } + }, { + path: '/mongo', + method: 'get', + controller: async(req, res, next) => { + try { + const mongoUp = MongoDriver.constructor.checkConnection() + res.json({ + service: 'mongo', + connected: mongoUp + }) + } catch (error) { + console.error(error) + res.json({ + service: 'mongo', + connected: 'undefined', + error: 'Cannot get Mongo connection status' + }) + } + } + }, + { + path: '/redis', + method: 'get', + controller: async(req, res, next) => { + try { + const redisUp = await webServer.app.redis.checkConnection() + res.json({ + service: 'redis', + connected: redisUp + }) + } catch (error) { + console.error(error) + res.json({ + service: 'redis', + connected: 'undefined', + error: 'Cannot get Mongo connection status' + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/index.js b/platform/linto-admin/webserver/lib/webserver/routes/index.js new file mode 100644 index 0000000..c2b08ad --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/index.js @@ -0,0 +1,41 @@ +const debug = require('debug')(`linto-admin:routes`) +const middlewares = require(`${process.cwd()}/lib/webserver/middlewares`) +const ifHasElse = (condition, ifHas, otherwise) => { + return !condition ? otherwise() : ifHas() +} + +class Route { + constructor(webServer) { + const routes = require('./routes.js')(webServer) + for (let level in routes) { + for (let path in routes[level]) { + const route = routes[level][path] + const method = route.method + if (route.requireAuth) { + webServer.app[method]( + level + route.path, + middlewares.logger, + middlewares.checkAuth, + ifHasElse( + Array.isArray(route.controller), + () => Object.values(route.controller), + () => route.controller + ) + ) + } else { + webServer.app[method]( + level + route.path, + middlewares.logger, + ifHasElse( + Array.isArray(route.controller), + () => Object.values(route.controller), + () => route.controller + ) + ) + } + } + } + } +} + +module.exports = webServer => new Route(webServer) \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/login/index.js b/platform/linto-admin/webserver/lib/webserver/routes/login/index.js new file mode 100644 index 0000000..37d1b7a --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/login/index.js @@ -0,0 +1,84 @@ +const debug = require('debug')('linto-admin:login') +const sha1 = require('sha1') +const UsersModel = require(`${process.cwd()}/model/mongodb/models/users.js`) + +module.exports = (webServer) => { + return [{ + path: '/', + method: 'get', + requireAuth: true, + controller: async(req, res, next) => { + try { + if (!!req.session && req.session.logged === 'on') { + res.redirect('/admin/applications/device') + } else { + const users = await UsersModel.getUsers() + if (users.length === 0) { + res.redirect('/setup') + } else { + res.setHeader("Content-Type", "text/html") + res.sendFile(process.cwd() + '/dist/login.html') + } + } + } catch (err) { + console.error(err) + } + } + }, + { + path: '/userAuth', + method: 'post', + requireAuth: false, + controller: async(req, res, next) => { + if (req.body.userName != "undefined" && req.body.password != "undefined") { // get post datas + const userName = req.body.userName + const password = req.body.password + try { + let user + let getUser = await UsersModel.getUserByName(userName) + if (getUser.length > 0) { + user = getUser[0] + } + if (typeof(user) === "undefined") { // User not found + throw 'User not found' + } else { // User found + const userPswdHash = user.pswdHash + const salt = user.salt + // Compare password with database + if (sha1(password + salt) == userPswdHash) { + req.session.logged = 'on' + req.session.user = userName + req.session.save((err) => { + if (err) { + throw "Error on saving session" + } else { + //Valid password + res.json({ + "status": "success", + "msg": "valid" + }) + } + }) + + } else { + // Invalid password + throw "Invalid password" + } + } + } catch (error) { + console.error(error) + res.json({ + status: "error", + msg: error + }) + } + } else { + res.json({ + status: "error", + msg: "An error has occured whent trying to connect to database" + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/logout/index.js b/platform/linto-admin/webserver/lib/webserver/routes/logout/index.js new file mode 100644 index 0000000..03285bd --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/logout/index.js @@ -0,0 +1,21 @@ +const debug = require('debug')('linto-admin:logout') + +module.exports = (webServer) => { + return [{ + path: '/', + method: 'get', + requireAuth: true, + controller: [ + async(req, res, next) => { + if (req.session.logged === 'on') { + req.session.destroy((err) => { + if (err) { + console.error('Destroy session Err', err) + } + res.redirect('/login') + }) + } + } + ] + }] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/routes.js b/platform/linto-admin/webserver/lib/webserver/routes/routes.js new file mode 100644 index 0000000..4edb555 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/routes.js @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2017 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const debug = require('debug')('linto-admin:routes') + +module.exports = (webServer) => { + return { + "/setup": require('./setup')(webServer), + "/login": require('./login')(webServer), + "/logout": require('./logout')(webServer), + "/admin": require('./admin')(webServer), + "/healthcheck": require('./healthcheck')(webServer), + "/api": require('./api')(webServer), + "/api/swagger": require('./api/swagger')(webServer), + "/api/clients/static": require('./api/clients/static')(webServer), + "/api/workflows": require('./api/workflows')(webServer), + "/api/workflows/application": require('./api/workflows/application')(webServer), + "/api/workflows/static": require('./api/workflows/static')(webServer), + // Android users + "/api/androidusers": require('./api/androidusers')(webServer), + // Webapp hosts + "/api/webapphosts": require('./api/webapphosts')(webServer), + // Flow + "/api/flow": require('./api/flow')(webServer), + "/api/flow/tmp": require('./api/flow/tmp')(webServer), + "/api/flow/node": require('./api/flow/node')(webServer), + "/api/tock": require('./api/tock')(webServer), + "/api/stt": require('./api/stt')(webServer), + "/api/localskills": require('./api/localskills')(webServer), + "/": require('./_root')(webServer) + } +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/lib/webserver/routes/setup/index.js b/platform/linto-admin/webserver/lib/webserver/routes/setup/index.js new file mode 100644 index 0000000..fec34f8 --- /dev/null +++ b/platform/linto-admin/webserver/lib/webserver/routes/setup/index.js @@ -0,0 +1,43 @@ +const debug = require('debug')('linto-admin:setup') +const UsersModel = require(`${process.cwd()}/model/mongodb/models/users.js`) +module.exports = (webServer) => { + return [{ + path: '/', + method: 'get', + controller: async(req, res, next) => { + const users = await UsersModel.getUsers() + if (users.length === 0) { + res.setHeader("Content-Type", "text/html") + res.sendFile(process.cwd() + '/dist/setup.html') + } else { + res.redirect('/login') + } + } + }, + { + path: '/createuser', + method: 'post', + controller: async(req, res, next) => { + try { + const payload = req.body + const createUser = await UsersModel.createUser(payload) + if (createUser === 'success') { + res.json({ + status: 'success', + msg: '' + }) + } else { + throw 'error on creating user' + } + + } catch (error) { + console.error(error) + res.json({ + status: 'error', + error: error + }) + } + } + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/package.json b/platform/linto-admin/webserver/package.json new file mode 100644 index 0000000..575c2be --- /dev/null +++ b/platform/linto-admin/webserver/package.json @@ -0,0 +1,73 @@ +{ + "name": "linto-admin", + "version": "0.3.0", + "description": "This is the linto-platform-admin webserver", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "NODE_ENV=production node app.js", + "start-dev": "NODE_ENV=development nodemon app.js", + "start-local": "DEBUG=linto-admin:mqtt-monitor NODE_ENV=local nodemon app.js", + "start-debug": "DEBUG=linto-admin:mqtt-monitor NODE_ENV=development nodemon --inspect app.js " + }, + "nodemonConfig": { + "ignore": [ + "*/tockapp.json", + "*/tocksentences.json" + ] + }, + "author": "Romain Lopez ", + "contributors": [ + "Romain Lopez ", + "Damien Laine ", + "Yoann Houpert " + ], + "license": "GNU AFFERO GPLV3", + "dependencies": { + "atob": "^2.1.2", + "axios": "^0.19.2", + "btoa": "^1.2.1", + "child_process": "^1.0.2", + "connect-redis": "^3.4.0", + "cookie-parser": "^1.4.4", + "cors": "^2.8.5", + "cross-env": "^5.1.1", + "debug": "^4.1.1", + "dotenv": "^6.0.0", + "eventemitter3": "^3.1.0", + "events": "^3.0.0", + "express": "^4.16.2", + "express-session": "^1.17.0", + "form-data": "^3.0.0", + "https": "^1.0.0", + "i": "^0.3.6", + "i18next": "^12.0.0", + "lru-cache": "^4.1.1", + "md5": "^2.2.1", + "moment": "^2.24.0", + "mongodb": "^3.1.13", + "mqtt": "^3.0.0", + "multer": "^1.4.2", + "node-sass": "^4.12.0", + "npm": "^6.10.1", + "path": "^0.12.7", + "pg": "^7.5.0", + "pm2": "^2.10.4", + "querystring": "^0.2.0", + "randomstring": "^1.1.5", + "redis": "^2.8.0", + "remove-accents": "^0.4.2", + "request": "^2.88.2", + "sha1": "^1.1.1", + "socket.io": "^2.3.0", + "swagger-ui-express": "^4.1.4", + "uuid": "^3.3.3", + "when": "^3.7.8", + "word-definition": "^2.1.6", + "xml2json": "^0.11.2", + "z-schema": "^4.2.3" + }, + "devDependencies": { + "nodemon": "^2.0.2" + } +} \ No newline at end of file diff --git a/platform/linto-admin/webserver/public/img/nodered-linto-logo.png b/platform/linto-admin/webserver/public/img/nodered-linto-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9acd74f6334f29486c7e468cab88ac0e91c27222 GIT binary patch literal 3645 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 P00000NkvXXu0mjf4MXil literal 0 HcmV?d00001 diff --git a/config/mosquitto/auth/.gitkeep b/platform/linto-admin/webserver/readme.md similarity index 100% rename from config/mosquitto/auth/.gitkeep rename to platform/linto-admin/webserver/readme.md diff --git a/platform/mongodb-migration/.docker_env b/platform/mongodb-migration/.docker_env new file mode 100644 index 0000000..800bc0c --- /dev/null +++ b/platform/mongodb-migration/.docker_env @@ -0,0 +1,12 @@ +TZ=Europe/Paris + +LINTO_SHARED_MOUNT=~/linto_shared_mount/ +LINTO_STACK_MONGODB_SHARED_SCHEMAS=mongodb/schemas + +LINTO_STACK_MONGODB_SERVICE=localhost +LINTO_STACK_MONGODB_PORT=27017 +LINTO_STACK_MONGODB_DBNAME=lintoAdmin +LINTO_STACK_MONGODB_USE_LOGIN=true +LINTO_STACK_MONGODB_USER=root +LINTO_STACK_MONGODB_PASSWORD=example +LINTO_STACK_MONGODB_TARGET_VERSION=1 \ No newline at end of file diff --git a/platform/mongodb-migration/.dockeringore b/platform/mongodb-migration/.dockeringore new file mode 100644 index 0000000..b589822 --- /dev/null +++ b/platform/mongodb-migration/.dockeringore @@ -0,0 +1,4 @@ +**/node_modules +**/.env +**/.envdefault +**/package-lock.json \ No newline at end of file diff --git a/platform/mongodb-migration/.envdefault b/platform/mongodb-migration/.envdefault new file mode 100644 index 0000000..420feaa --- /dev/null +++ b/platform/mongodb-migration/.envdefault @@ -0,0 +1,12 @@ +TZ=Europe/Paris + +LINTO_SHARED_MOUNT=~/linto_shared_mount/ +LINTO_STACK_MONGODB_SHARED_SCHEMAS=mongodb/schemas + +LINTO_STACK_MONGODB_SERVICE=localhost +LINTO_STACK_MONGODB_PORT=27017 +LINTO_STACK_MONGODB_DBNAME=dbname +LINTO_STACK_MONGODB_USE_LOGIN=true +LINTO_STACK_MONGODB_USER=root +LINTO_STACK_MONGODB_PASSWORD=example +LINTO_STACK_MONGODB_TARGET_VERSION=2 \ No newline at end of file diff --git a/platform/mongodb-migration/.github/workflows/dockerhub-description.yml b/platform/mongodb-migration/.github/workflows/dockerhub-description.yml new file mode 100644 index 0000000..ea2dba6 --- /dev/null +++ b/platform/mongodb-migration/.github/workflows/dockerhub-description.yml @@ -0,0 +1,20 @@ +name: Update Docker Hub Description +on: + push: + branches: + - master + paths: + - README.md + - .github/workflows/dockerhub-description.yml +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + repository: lintoai/linto-platform-mongodb-migration + readme-filepath: ./README.md diff --git a/config/mosquitto/conf.d/.gitkeep b/platform/mongodb-migration/.gitignore similarity index 100% rename from config/mosquitto/conf.d/.gitkeep rename to platform/mongodb-migration/.gitignore diff --git a/platform/mongodb-migration/Dockerfile b/platform/mongodb-migration/Dockerfile new file mode 100644 index 0000000..2609f0a --- /dev/null +++ b/platform/mongodb-migration/Dockerfile @@ -0,0 +1,17 @@ +FROM node:latest +# Gettext for envsubst being called form entrypoint script +RUN apt-get update -y + +COPY . /usr/src/app/linto-platform-mongodb-migration +COPY ./docker-entrypoint.sh / +COPY ./wait-for-it.sh / + +WORKDIR /usr/src/app/linto-platform-mongodb-migration +RUN npm install + +EXPOSE 80 + +# Entrypoint handles the passed arguments + +ENTRYPOINT ["/docker-entrypoint.sh"] +# CMD ["npm", "run", "migrate"] \ No newline at end of file diff --git a/platform/mongodb-migration/LICENSE b/platform/mongodb-migration/LICENSE new file mode 100644 index 0000000..020d69c --- /dev/null +++ b/platform/mongodb-migration/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users" freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work"s +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users" Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work"s +users, your or third parties" legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program"s source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation"s users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party"s predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor"s "contributor version". + + A contributor"s "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor"s essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient"s use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others" Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy"s +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/platform/mongodb-migration/README.md b/platform/mongodb-migration/README.md new file mode 100644 index 0000000..d3a5d8e --- /dev/null +++ b/platform/mongodb-migration/README.md @@ -0,0 +1,39 @@ + +# Linto-Platform-Mongodb-Migration +This services is a one shoot scripts that might migrate LinTO Platform databases content when needed (version bumps, rollbacks...) + +## Usage + +See documentation : [doc.linto.ai](https://doc.linto.ai) + +# Deploy + +With our proposed stack [linto-platform-stack](https://github.com/linto-ai/linto-platform-stack) + +# Develop + +## Install project +``` +git clone https://github.com/linto-ai/linto-platform-mongodb-migration.git +linto-platform-mongodb-migration +npm install +``` + +## Environment +`cp .envdefault .env` +Then update the `.env` to manage your personal configuration + +## User + +Based on your environment settings, an user may be require to be create +``` +db.createUser({ + user: "LINTO_STACK_MONGODB_USER", + pwd: "LINTO_STACK_MONGODB_PASSWORD", + roles: ["readWrite"] +}) +``` + +## RUN +Run de mongodb migration : `npm run migrate` +A database with a set of collection will be create if it's successful. diff --git a/platform/mongodb-migration/RELEASE.md b/platform/mongodb-migration/RELEASE.md new file mode 100644 index 0000000..081e78a --- /dev/null +++ b/platform/mongodb-migration/RELEASE.md @@ -0,0 +1,21 @@ +# 0.3.0 +- 2021-10-15 +- remove "workflows_templates" collection +- works with linto-platform-admin@0.3.0 + +# 0.2.5 +- 2021-02-16 +- Added local_skills collection +- Update migration tests (some where missing) + +# 0.2.4 +- Added mqtt_user clean on startup + +# 0.0.3 +- Added mqtt schemas (auth user and acl) + +# 0.0.2 +- Second version of linto-platform-mongodb-migration service + +# 0.0.1 +- First version of linto-platform-mongodb-migration service diff --git a/platform/mongodb-migration/config.js b/platform/mongodb-migration/config.js new file mode 100644 index 0000000..652d92a --- /dev/null +++ b/platform/mongodb-migration/config.js @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2017 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const debug = require('debug')('linto-admin:config') +const dotenv = require('dotenv') +const path = require('path'); +const fs = require('fs') + +function ifHasNotThrow(element, error) { + if (!element) throw error + return element +} + +function ifHas(element, defaultValue) { + if (!element) return defaultValue + return element +} + +function configureDefaults() { + try { + dotenv.config() + const envdefault = dotenv.parse(fs.readFileSync('.envdefault')) + + process.env.LINTO_SHARED_MOUNT = ifHas(process.env.LINTO_SHARED_MOUNT, envdefault.LINTO_SHARED_MOUNT) + process.env.LINTO_STACK_MONGODB_SHARED_SCHEMAS = ifHas(process.env.LINTO_STACK_MONGODB_SHARED_SCHEMAS, envdefault.LINTO_STACK_MONGODB_SHARED_SCHEMAS) + // Database (mongodb) + process.env.LINTO_STACK_MONGODB_DBNAME = ifHas(process.env.LINTO_STACK_MONGODB_DBNAME, envdefault.LINTO_STACK_MONGODB_DBNAME) + process.env.LINTO_STACK_MONGODB_SERVICE = ifHas(process.env.LINTO_STACK_MONGODB_SERVICE, envdefault.LINTO_STACK_MONGODB_SERVICE) + process.env.LINTO_STACK_MONGODB_PORT = ifHas(process.env.LINTO_STACK_MONGODB_PORT, envdefault.LINTO_STACK_MONGODB_PORT) + process.env.LINTO_STACK_MONGODB_USE_LOGIN = ifHas(process.env.LINTO_STACK_MONGODB_USE_LOGIN, envdefault.LINTO_STACK_MONGODB_USE_LOGIN) + process.env.LINTO_STACK_MONGODB_USER = ifHas(process.env.LINTO_STACK_MONGODB_USER, envdefault.LINTO_STACK_MONGODB_USER) + process.env.LINTO_STACK_MONGODB_PASSWORD = ifHas(process.env.LINTO_STACK_MONGODB_PASSWORD, envdefault.LINTO_STACK_MONGODB_PASSWORD) + process.env.LINTO_STACK_MONGODB_TARGET_VERSION = ifHas(process.env.LINTO_STACK_MONGODB_TARGET_VERSION, envdefault.LINTO_STACK_MONGODB_TARGET_VERSION) + + } catch (e) { + console.error(debug.namespace, e) + process.exit(1) + } +} +module.exports = configureDefaults() \ No newline at end of file diff --git a/platform/mongodb-migration/docker-compose.yml b/platform/mongodb-migration/docker-compose.yml new file mode 100644 index 0000000..df658e0 --- /dev/null +++ b/platform/mongodb-migration/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.7' + +services: + + linto-platform-mongodb-migration: + image: lintoai/linto-platform-mongodb-migration:latest + networks: + - internal + deploy: + mode: replicated + replicas: 1 + restart_policy: + condition: none + command: # Overrides CMD specified in dockerfile (none here, handled by entrypoint) + - --run-cmd=npm run migrate + env_file: .docker_env + ports: + - 80:80 + +networks: + internal: diff --git a/platform/mongodb-migration/docker-entrypoint.sh b/platform/mongodb-migration/docker-entrypoint.sh new file mode 100755 index 0000000..2a55e86 --- /dev/null +++ b/platform/mongodb-migration/docker-entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e +[ -z "$LINTO_STACK_MONGODB_TARGET_VERSION" ] && { + echo "Missing LINTO_STACK_MONGODB_TARGET_VERSION" + exit 1 +} +echo "Waiting mongo..." +/wait-for-it.sh $LINTO_STACK_MONGODB_SERVICE:$LINTO_STACK_MONGODB_PORT --timeout=20 --strict -- echo " $LINTO_STACK_MONGODB_SERVICE:$LINTO_STACK_MONGODB_PORT is up" + +while [ "$1" != "" ]; do + case $1 in + --migrate) + cd /usr/src/app/linto-platform-mongodb-migartion + npm install && npm run migrate + ;; + --run-cmd?*) + script=${1#*=} # Deletes everything up to "=" and assigns the remainder. + ;; + --run-cmd=) # Handle the case of an empty --run-cmd= + die 'ERROR: "--run-cmd" requires a non-empty option argument.' + ;; + *) + echo "ERROR: Bad argument provided \"$1\"" + exit 1 + ;; + esac + shift +done + +echo "RUNNING : $script" +cd /usr/src/app/linto-platform-mongodb-migration + +eval "$script" diff --git a/platform/mongodb-migration/index.js b/platform/mongodb-migration/index.js new file mode 100644 index 0000000..d22d60d --- /dev/null +++ b/platform/mongodb-migration/index.js @@ -0,0 +1,138 @@ +require('./config.js') +const MongoDriver = require('./model/driver') +const migration = require(`./migrations/${process.env.LINTO_STACK_MONGODB_TARGET_VERSION}/index.js`) +const path = './migrations/'; +const fs = require('fs'); + +function migrate() { + return new Promise((resolve, reject) => { + try { + setTimeout(async() => { + // Check if MongoDriver is connected + if (!MongoDriver.constructor.checkConnection()) { + console.log('MongoDb migrate : Not connected') + } else { + const getCurrentVersion = await migration.getCurrentVersion() + if (getCurrentVersion.length > 0 && !!getCurrentVersion[0].version) { + const currentVersion = getCurrentVersion[0].version + const versions = parseFolders() + const indexStart = versions.indexOf(currentVersion.toString()) + const indexEnd = versions.indexOf(process.env.LINTO_STACK_MONGODB_TARGET_VERSION.toString()) + + if (parseInt(currentVersion) > parseInt(process.env.LINTO_STACK_MONGODB_TARGET_VERSION)) { // MIGRATE DOWN + try { + console.log('> MIGRATE DOWN') + for (let iteration of generatorMigrateDown(versions, indexStart, indexEnd)) { + const res = await iteration + if (res !== true) { + reject(res) + } + } + } catch (error) { + reject(error) + } + } else if (parseInt(currentVersion) <= parseInt(process.env.LINTO_STACK_MONGODB_TARGET_VERSION)) { // MIGRATE UP + try { + if (parseInt(currentVersion) < parseInt(process.env.LINTO_STACK_MONGODB_TARGET_VERSION)) { + console.log('> MIGRATE UP') + } else if (parseInt(currentVersion) === parseInt(process.env.LINTO_STACK_MONGODB_TARGET_VERSION)) { + console.log('> MIGRATE control current version') + } + for (let iteration of generatorMigrateUp(versions, indexStart, indexEnd)) { + const res = await iteration + if (res !== true) { + reject(res) + } + } + process.exit(0) + } catch (error) { + reject(error) + } + } + } else { // If database version is not found, execute Version 1 mongoUP + const initDb = require(`./migrations/1/index.js`) + const mig = await initDb.migrateUp() + if (mig === true) { + migrate() + } + } + + } + }, 500) + } catch (error) { + reject(error) + process.exit(1) + } + }) +} + +// Generator function to chain promises +function* generatorMigrateUp(versions, indexStart, indexEnd) { + for (let i = indexStart; i <= indexEnd; i++) { + yield(new Promise(async(resolve, reject) => { + try { + console.log('> Migrate up to version :', versions[i]) + const migrationFile = require(`./migrations/${versions[i]}/index.js`) + const mig = await migrationFile.migrateUp() + if (mig === true) { + resolve(mig) + } else { + reject(mig) + } + } catch (err) { + console.error(err) + reject(err) + } + })) + } +} + +function* generatorMigrateDown(versions, indexStart, indexEnd) { + // Execute migrate down for each version that are higher than the wanted one + for (let i = indexStart; i > indexEnd; i--) { + yield(new Promise(async(resolve, reject) => { + try { + console.log('> Migrate down to version :', versions[i]) + const migrationFile = require(`./migrations/${versions[i]}/index.js`) + const mig = await migrationFile.migrateDown() + if (mig === true) { + resolve(mig) + } else { + reject(mig) + } + } catch (err) { + console.error(err) + reject(err) + } + })) + } + // Execute migrate up for the wanted version. + const wantedVersion = require(`./migrations/${versions[indexEnd]}/index.js`) + yield(new Promise(async(resolve, reject) => { + try { + console.log('> Migrate down to version :', versions[indexEnd]) + let migup = await wantedVersion.migrateUp() + if (migup === true) { + resolve(migup) + } else { + reject(migup) + } + } catch (error) { + console.error(err) + reject(err) + } + })) +} + +function parseFolders() { + try { + return fs.readdirSync(path).filter(function(file) { + return fs.statSync(path + '/' + file).isDirectory(); + }) + } catch (error) { + return error + } +} + + +migrate() \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/index.js b/platform/mongodb-migration/migrations/1/index.js new file mode 100644 index 0000000..14d07bd --- /dev/null +++ b/platform/mongodb-migration/migrations/1/index.js @@ -0,0 +1,265 @@ +const MongoMigration = require(`../../model/migration.js`) +const schemas = { + context: require('./schemas/context.json'), + context_types: require('./schemas/context_types.json'), + dbversion: require('./schemas/dbversion.json'), + flow_pattern: require('./schemas/flow_pattern.json'), + flow_pattern_tmp: require('./schemas/flow_pattern_tmp.json'), + lintos: require('./schemas/lintos.json'), + users: require('./schemas/users.json'), + linto_users: require('./schemas/linto_users.json') +} + +class Migrate extends MongoMigration { + constructor() { + super() + this.version = 1 + } + async migrateUp() { + try { + const collections = await this.listCollections() + const collectionNames = [] + let migrationErrors = [] + collections.map(col => { + collectionNames.push(col.name) + }) + + /*****************/ + /* CONTEXT_TYPES */ + /*****************/ + if (collectionNames.indexOf('context_types') >= 0) { // collection exist + const contextTypes = await this.mongoRequest('context_types', {}) + if (contextTypes.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(contextTypes, schemas.context_types) + if (schemaValid.valid) { // schema is valid + const fleetVal = contextTypes.filter(ct => ct.name === 'Fleet') + if (fleetVal.length === 0) { + await this.mongoInsert('context_types', { name: 'Fleet' }) + } + + const applicationVal = contextTypes.filter(ct => ct.name === 'Application') + if (applicationVal.length === 0) { + await this.mongoInsert('context_types', { name: 'Application' }) + } + } else { // schema is invalid + migrationErrors.push({ + collectionName: 'context_types', + errors: schemaValid.errors + }) + } + } else { // collection exist but empty + const payload = [ + { name: 'Fleet' }, + { name: 'Application' } + ] + this.mongoInsertMany('context_types', payload) + } + } else { // collection does not exist + const payload = [ + { name: 'Fleet' }, + { name: 'Application' } + ] + await this.mongoInsertMany('context_types', payload) + } + + + /********************/ + /* FLOW_PATTERN_TMP */ + /********************/ + if (collectionNames.indexOf('flow_pattern_tmp') >= 0) { // collection exist + const flowPatternTmp = await this.mongoRequest('flow_pattern_tmp', {}) + if (flowPatternTmp.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(flowPatternTmp, schemas.flow_pattern_tmp) + if (schemaValid.valid) { // schema is valid + const neededVal = flowPatternTmp.filter(ct => ct.id === 'tmp') + if (neededVal.length === 0) { + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + await this.mongoInsert('flow_pattern_tmp', payload) + } + } else { // Schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'flow_pattern_tmp', + errors: schemaValid.errors + }) + } + } else { // collection exist but empty + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + + // Insert default data + await this.mongoInsert('flow_pattern_tmp', payload) + } + } else { // collection does not exist + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + + // Create collection and insert default data + await this.mongoInsert('flow_pattern_tmp', payload) + } + + /****************/ + /* FLOW_PATTERN */ + /****************/ + const flowPatternPayload = require('./json/linto-fleet-default-flow.json') + if (collectionNames.indexOf('flow_pattern') >= 0) { // collection exist + const flowPattern = await this.mongoRequest('flow_pattern', {}) + if (flowPattern.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(flowPattern, schemas.flow_pattern) + if (schemaValid.valid) { // schema is invalid + const neededVal = flowPattern.filter(ct => ct.name === 'linto-fleet-default') + if (neededVal.length === 0) { // required value doesn't exist + await this.mongoInsert('flow_pattern', flowPatternPayload) + } + } else { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'flow_pattern', + errors: schemaValid.errors + }) + } + } else { //collection exist but empty + await this.mongoInsert('flow_pattern', flowPatternPayload) + } + } else { // collection doesn't exist + await this.mongoInsert('flow_pattern', flowPatternPayload) + } + + /*********/ + /* USERS */ + /*********/ + if (collectionNames.indexOf('users') >= 0) { // collection exist + const users = await this.mongoRequest('users', {}) + if (users.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(users, schemas.users) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'users', + errors: schemaValid.errors + }) + } + } + } + + /***********/ + /* CONTEXT */ + /***********/ + if (collectionNames.indexOf('context') >= 0) { // collection exist + const context = await this.mongoRequest('context', {}) + + if (context.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(context, schemas.context) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'context', + errors: schemaValid.errors + }) + } + } + } + + + + /**********/ + /* LINTOS */ + /**********/ + if (collectionNames.indexOf('lintos') >= 0) { // collection exist + const lintos = await this.mongoRequest('lintos', {}) + + if (lintos.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(lintos, schemas.lintos) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'lintos', + errors: schemaValid.errors + }) + } + } + } + + /*************/ + /* DBVERSION */ + /*************/ + if (collectionNames.indexOf('dbversion') >= 0) { // collection exist + const dbversion = await this.mongoRequest('dbversion', {}) + + const schemaValid = this.testSchema(dbversion, schemas.dbversion) + if (schemaValid.valid) { // schema valid + await this.mongoUpdate('dbversion', { id: 'current_version' }, { version: this.version }) + } else { // schema is invalid + migrationErrors.push({ + collectionName: 'dbversion', + errors: schemaValid.errors + }) + } + } else { // collection doesn't exist + await this.mongoInsert('dbversion', { + id: 'current_version', + version: this.version + }) + } + + /*****************/ + /* ANDROID_USERS */ + /*****************/ + if (collectionNames.indexOf('linto_users') >= 0) { // collection exist + const linto_users = await this.mongoRequest('linto_users', {}) + if (linto_users.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(linto_users, schemas.linto_users) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'linto_users', + errors: schemaValid.errors + }) + } + } + } + + // RETURN + if (migrationErrors.length > 0) { + throw migrationErrors + } else { + await this.mongoUpdate('dbversion', { id: 'current_version' }, { version: 1 }) + console.log(`> MongoDB migration to version "${this.version}": Success `) + return true + } + } catch (error) { + console.error(error) + if (typeof (error) === 'object') { + console.error('======== Migration ERROR ========') + error.map(err => { + if (!!err.collectionName && !!err.errors) { + console.error('> Collection: ', err.collectionName) + err.errors.map(e => { + console.error('Error: ', e) + }) + } + }) + console.error('=================================') + } + return error + } + } + async migrateDown() { + console.log('Miration to version 1. This is the lowest version you can migrate to') + return true + } +} + +module.exports = new Migrate() \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/json/linto-fleet-default-flow.json b/platform/mongodb-migration/migrations/1/json/linto-fleet-default-flow.json new file mode 100644 index 0000000..e1f71a6 --- /dev/null +++ b/platform/mongodb-migration/migrations/1/json/linto-fleet-default-flow.json @@ -0,0 +1,216 @@ +{ + "name": "linto-fleet-default", + "type": "Fleet", + "flow": [{ + "id": "2d78a86.a300558", + "type": "tab", + "label": "linto-fleet-default", + "disabled": false + }, + { + "id": "1c43e500-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-config-mqtt", + "z": "2d78a86.a300558", + "host": "localhost", + "port": "1883", + "scope": "blk" + }, + { + "id": "1c43e501-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-config-evaluate", + "z": "2d78a86.a300558", + "host": "https://stage.linto.ai/tockapi/rest/nlp/parse", + "api": "tock", + "appname": "linto", + "namespace": "app" + }, + { + "id": "1c43e502-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-config-transcribe", + "z": "2d78a86.a300558", + "host": "https://stage.linto.ai/stt/fr", + "api": "linstt" + }, + { + "id": "1c440c1b-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-config-mqtt", + "z": "2d78a86.a300558", + "host": "localhost", + "port": "1883", + "scope": "blk" + }, + { + "id": "1c440c1c-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-config-evaluate", + "z": "2d78a86.a300558", + "host": "https://stage.linto.ai/tockapi/rest/nlp/parse", + "api": "tock", + "appname": "linto", + "namespace": "app" + }, + { + "id": "1c440c1d-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-config-transcribe", + "z": "2d78a86.a300558", + "host": "https://stage.linto.ai/stt/fr", + "api": "linstt" + }, + { + "id": "1c43e503-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-config", + "z": "2d78a86.a300558", + "name": "", + "configMqtt": "b4daef64.04ab9", + "configEvaluate": "eb19bca.3dd5e4", + "configTranscribe": "12977d52.e61213", + "language": "fr-FR", + "x": 120, + "y": 40, + "wires": [] + }, + { + "id": "1c43e504-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-red-event-emitter", + "z": "2d78a86.a300558", + "name": "", + "x": 760, + "y": 100, + "wires": [] + }, + { + "id": "1c440c10-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-evaluate", + "z": "2d78a86.a300558", + "name": "", + "x": 540, + "y": 100, + "wires": [ + [ + "1c43e504-6ac3-11ea-af80-ff4f9bc85132" + ] + ] + }, + { + "id": "1c440c11-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-transcribe", + "z": "2d78a86.a300558", + "name": "", + "x": 340, + "y": 100, + "wires": [ + [ + "1c440c10-6ac3-11ea-af80-ff4f9bc85132" + ] + ] + }, + { + "id": "1c440c12-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-terminal-in", + "z": "2d78a86.a300558", + "name": "", + "sn": "blk01", + "x": 140, + "y": 100, + "wires": [ + [ + "1c440c11-6ac3-11ea-af80-ff4f9bc85132" + ] + ] + }, + { + "id": "1c440c13-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-out", + "z": "2d78a86.a300558", + "name": "", + "x": 1140, + "y": 120, + "wires": [] + }, + { + "id": "1c440c14-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-skill-weather", + "z": "2d78a86.a300558", + "name": "", + "defaultCity": "Toulouse", + "degreeType": "C", + "api": "microsoft", + "command": "##intent|weather|fr\n- quelle est la météo pour [demain](time)\n- quel temps fait il [demain](time)\n- quel temps fait il [demain](time) a [paris](location)\n- quel temps fait il [demain](time) a [toulouse](location)\n- quel temps fait il [demain](time) a [bordeaux](location)\n- quel temps fait il [demain](time) a [rennes](location)\n- quel temps fait il a [rennes](location)\n- quel temps fait il a [toulouse](location)\n- quel temps fait il a [paris](location)\n- temps a [paris](location)\n- temps a [lens](location)\n- quelle est la météo pour [demain](time)\n- météo a [paris](location)\n- météo a [lens](location)\n- donne moi la météo a [lens](location)\n- donne moi la météo à [lens](location)\n- il fait quel temps à [toulouse](location)\n- est ce qu'il pleut à [bordeaux](location)\n- quel temps fait il à [paris](location)\n- quel temps fait il à [toulouse](location)\n- quel temps fait il à [las vegas](location) [demain](time)\n- quel temps fait il à [hollywood](location)\n- quel temps fait il à [phoenix parc](location)\n- quel temps fait il à [phoenix](location) [demain](time)\n- quel temps fait il à [bollywood](location)\n- quel temps fait il à [bollywood parc](location)\n- quel temps fait il à [bollywood hotel](location)\n- quel temps fait il à [las vegas hotel](location) [demain](time)\n- quel temps fait il à [las venise hotel](location)\n- quel temps fait il à [center venise hotel](location)\n- la météo est elle bonne a [paris toulouse](location)\n- la météo est elle bonne a [saint etienne en cogles](location)\n- la météo est elle bonne a [saint brice en cogles](location)\n- donne moi la météo de [paris](location)\n- Quelle est la météo à [vegas](location)\n- Quel temps fait-il à [Las Vegas](location) [demain](time)\n- Quelle est la météo à [Strasbourg](location) pour [demain](time)\n- La météo est-elle bonne [demain](time) à [Toulouse](location)\n- quel temps fait il à [santa monica](location)\n- quel temps fait il à [roma](location) parc\n- quel temps fait il à [phoenix parc](location)\n- quel temps fait il à l'aéroport de [las vegas](location)\n- quel temps fait il à [lyon](location)\n- quel temps fait il à [rennes](location)\n- quel temps fait il à [madrid](location)\n- quel temps fait il à [londres](location)\n##intent|weather|en\n- what's the weather in [new york](location)\n- weather at [new work](location)\n- give me the weather at [madrid](location)\n- give me the weather at [london](location)\n- could you give me the weather of [london](location)\n- weather at [london](location)\n- weather at [venise](location)\n- can you give me the weather at [venise](location)\n- can you give me the weather at [phoenix](location)\n- what's the weather for [tomorrow](time)\n- what is the weather for [tomorrow](time)\n- is the weather good [tomorrow](time)\n- what's the weather like [tomorrow](time)\n- the weather for [tomorrow](time) please\n- [tomorrow](time) weather\n- what's the weather in [toulouse](location) [tomorrow](time)\n- what's the weather in [paris](location) [tomorrow](time)\n- what's the weather in [las vegas](location) [tomorrow](time)\n- what's the weather in [london](location) [tomorrow](time)\n- what's the weather in [madrid](location) [tomorrow](time)\n- what's the weather in [casa del mar](location) [tomorrow](time)\n- what is the weather in [casa del mar](location) [tomorrow](time)\n- what is the weather in [hollywood](location) [tomorrow](time)\n- what is the weather in [rennes](location) [tomorrow](time)\n- what is the weather in [las vegas](location) [tomorrow](time)\n- what is the weather in [lyon](location) [tomorrow](time)\n- what is the weather in [toulouse](location) [tomorrow](time)\n- is the weather good [tomorrow](time) in [paris](location)\n- is the weather good [tomorrow](time) in [toulouse](location)\n- is the weather good [tomorrow](time) in [las vegas](location)\n- is the weather good [tomorrow](time) in [hollywood](location)\n- is the weather good [tomorrow](time) in [casa del mar](location)\n- is the weather good [tomorrow](time) in [nantes](location)\n- what's the weather for [tomorrow](time) in [toulouse](location)\n- what's the weather for [tomorrow](time) in [paris](location)\n- what's the weather for [tomorrow](time) in [las vegas](location)\n- what's the weather for [tomorrow](time) in [hollywood](location)\n- what's the weather for [tomorrow](time) in [santa monica](location)\n- what's the weather for [tomorrow](time) in [los angeles](location)\n- what's the weather for [tomorrow](time) in [new york](location)\n- what's the weather in [toulouse](location) for [tomorrow](time)\n- what's the weather in [phoenix](location) for [tomorrow](time)\n- what's the weather in [montauban](location) for [tomorrow](time)\n- what's the weather in [las vegas](location) for [tomorrow](time)\n- what's the weather in [venice beach](location) for [tomorrow](time)\n- what's the weather in [new york](location) for [tomorrow](time)\n- what's the weather in [london](location) for [tomorrow](time)\n- the weather at [las vegas](location)\n- the weather at [roma](location)\n- the weather like in [paris](location) for [tomorrow](time)\n- the weather like in [hollywood](location) for [tomorrow](time)\n- the weather like in [london](location) for [tomorrow](time)\n- what is the weather in [venice beach](location) for [tomorrow](time)\n- what is the weather in [hollywood](location) for [tomorrow](time)\n- what is the weather in [las vegas](location) for [tomorrow](time)\n- what is the weather in [casa del mar](location) for [tomorrow](time)\n- what is the weather in [paris](location) for [tomorrow](time)\n- what is the weather in [toulouse](location) for [tomorrow](time)\n- what's the weather in [paris](location)\n- what's the weather in [messages](location)\n- what's the weather in [las vegas airport](location)", + "x": 140, + "y": 180, + "wires": [ + [] + ] + }, + { + "id": "1c440c15-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-model-dataset", + "z": "2d78a86.a300558", + "name": "", + "x": 530, + "y": 200, + "wires": [] + }, + { + "id": "1c440c16-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-skill-welcome", + "z": "2d78a86.a300558", + "name": "", + "command": "##intent|goodbye|fr\n- salut\n- tchao\n- adieu\n- ciao\n- bye bye\n- au plaisir\n- à bientôt\n- a bientôt\n- a la prochaine\n- au revoir linto\n- au revoir\n- au plaisir linto\n- a bientôt linto\n- a bientôt\n- bye\n- a plus\n##intent|goodbye|en\n- well thank\n- see you soon\n- farewell\n- goodbye\n- good bye \n- good bye linto\n- thank you \n- thank you linto\n- see ya\n- see you\n- good bye\n- bye\n- bye bye\n- goodbye LinTo\n- thank you LinTo\n- thanks\n##intent|greeting|fr\n- bonjour\n- coucou\n- donne moi ton nom\n- bonjour, comment tu t'appelles\n- comment tu t'appelles\n- yo\n- bonsoir\n- salut\n- salut linto\n- bienvenue\n- salutation\n- hey\n##intent|greeting|en\n- greeting\n- hiya\n- hey\n- good morning\n- good afternoon\n- good evening\n- howdy\n- hello\n- hi\n- what’s your name ?\n- what's your name\n##intent|howareyou|fr\n- bonjour linto comment vas-tu\n- comment ça va\n- comment vas tu\n- comment tu vas\n- ca roule ?\n- est ce que ça va\n- linto comment ça va\n- je me sens [bien](isok)\n- tout va [bien](isok)\n- tout va [bien](isok) merci\n- je vais [bien](isok)\n- je [vais bien](isok)\n- ca va [bien](isok)\n- [ca va](isok)\n- [ca va](isok) merci\n- ça va [bien](isok)\n- [ça va](isok)\n- [oui](isok)\n- [tranquille](isok)\n- je vais [pas très bien](isko)\n- ca va pas [très bien](isko)\n- je [vais pas bien](isko)\n- je me sens [pas bien](isko)\n- je ne me sens [pas bien](isko) aujourd'hui\n- [pas très](isko) en forme\n- je suis de [mauvaise](isko) humeur\n- ça [ne va pas](isko) trop\n##intent|howareyou|en\n- are you ok\n- are you okay\n- how are you doing\n- how are you\n- how are you doing ?\n- how are you?\n- how do you do\n- are you fine?\n- are you feeling good?\n- are you ok ?\n- howdy\n- everything is [good](isok)\n- i'm [happy](isok)\n- i'm feel [great](isok)\n- i'm [right](isok)\n- [well](isok) thanks\n- [ok](isok)\n- i am [fine](isok)\n- [fine](isok)\n- [good](isok) thanks\n- i'm [well](isok) thank you\n- i'm [fine](isok)\n- i'm [ok](isok)\n- i'm in a [good mood](isok)\n- everything is [alright](isok)\n- i feel [good](isok)\n- i'm [not well](isko) thank you\n- i'm [not fine](isko)\n- i'm [not ok](isko)\n- i feel [no good](isko)\n- i'm [not feeling good](isko) today\n- [not good](isko)\n- i'm in a [bad mood](isko)\n- [not fine](isko)\n- [not ok](isko)\n- [not good](isko) thanks\n- [not well](isko) thanks\n- i am [sad](isko)", + "x": 140, + "y": 220, + "wires": [ + [] + ] + }, + { + "id": "1c440c17-6ac3-11ea-af80-ff4f9bc85132", + "type": "linto-ui", + "z": "2d78a86.a300558", + "name": "", + "group": "e7bb73f7.537b8", + "width": "9", + "height": "5", + "x": 1150, + "y": 60, + "wires": [] + }, + { + "id": "1c440c18-6ac3-11ea-af80-ff4f9bc85132", + "type": "to-linto-ui", + "z": "2d78a86.a300558", + "name": "", + "x": 1020, + "y": 60, + "wires": [ + [ + "1c440c17-6ac3-11ea-af80-ff4f9bc85132" + ] + ] + }, + { + "id": "b4daef64.04ab9", + "type": "linto-config-mqtt", + "z": "2d78a86.a300558", + "host": "localhost", + "port": "1883", + "scope": "blk" + }, + { + "id": "eb19bca.3dd5e4", + "type": "linto-config-evaluate", + "z": "2d78a86.a300558", + "host": "https://stage.linto.ai/tockapi/rest/nlp/parse", + "api": "tock", + "appname": "linto", + "namespace": "app" + }, + { + "id": "12977d52.e61213", + "type": "linto-config-transcribe", + "z": "2d78a86.a300558", + "host": "https://stage.linto.ai/stt/fr", + "api": "linstt" + } + ], + "created_date": "2020-04-22T10:43:49+02:00" +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/context.json b/platform/mongodb-migration/migrations/1/schemas/context.json new file mode 100644 index 0000000..c2a2ba3 --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/context.json @@ -0,0 +1,45 @@ +{ + "title": "context", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Fleet", "Application"] + }, + "associated_linto": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + }, + "updated_date": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "nodes": { + "type": "array" + } + } + } + }, + "required": ["name", "flowId", "type", "associated_linto", "created_date", "updated_date", "flow"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/context_types.json b/platform/mongodb-migration/migrations/1/schemas/context_types.json new file mode 100644 index 0000000..102227b --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/context_types.json @@ -0,0 +1,13 @@ +{ + "title": "context_types", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "enum": ["Fleet", "Application"] + } + }, + "required": ["name"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/dbversion.json b/platform/mongodb-migration/migrations/1/schemas/dbversion.json new file mode 100644 index 0000000..235cc76 --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/dbversion.json @@ -0,0 +1,16 @@ +{ + "title": "dbversion", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": ["id", "version"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/flow_pattern.json b/platform/mongodb-migration/migrations/1/schemas/flow_pattern.json new file mode 100644 index 0000000..6bd1278 --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/flow_pattern.json @@ -0,0 +1,24 @@ +{ + "title": "flow_pattern_tmp", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Fleet", "Application"] + }, + "flow": { + "type": "array" + }, + "created_date": { + "type": "string", + "format": "date-time" + } + }, + "required": ["name", "type", "flow", "created_date"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/flow_pattern_tmp.json b/platform/mongodb-migration/migrations/1/schemas/flow_pattern_tmp.json new file mode 100644 index 0000000..9c7ae09 --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/flow_pattern_tmp.json @@ -0,0 +1,19 @@ +{ + "title": "flow_pattern_tmp", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "flow": { + "type": "array" + }, + "workspaceId": { + "type": "string" + } + }, + "required": ["id", "flow", "workspaceId"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/linto_users.json b/platform/mongodb-migration/migrations/1/schemas/linto_users.json new file mode 100644 index 0000000..cb1a7d5 --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/linto_users.json @@ -0,0 +1,20 @@ +{ + "title": "linto_users", + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "hash": { + "type": "string" + }, + "salt": { + "type": "string" + } + }, + "required": ["email", "hash", "salt"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/lintos.json b/platform/mongodb-migration/migrations/1/schemas/lintos.json new file mode 100644 index 0000000..0e78995 --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/lintos.json @@ -0,0 +1,41 @@ +{ + "title": "lintos", + "type": "array", + "items": { + "type": "object", + "properties": { + "enrolled": { + "type": "boolean" + }, + "connexion": { + "type": "string", + "enum": ["offline", "online"] + }, + "last_up": { + "type": ["string", "null"], + "format": "date-time" + }, + "last_down": { + "type": ["string", "null"], + "format": "date-time" + }, + "associated_context": { + "type": ["string", "null"] + }, + "type": { + "type": "string", + "enum": ["fleet", "application"] + }, + "sn": { + "type": "string" + }, + "config": { + "type": "object" + }, + "meeting": { + "type": "array" + } + }, + "required": ["enrolled", "connexion", "last_up", "last_down", "type", "sn", "config"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/1/schemas/users.json b/platform/mongodb-migration/migrations/1/schemas/users.json new file mode 100644 index 0000000..df1a0da --- /dev/null +++ b/platform/mongodb-migration/migrations/1/schemas/users.json @@ -0,0 +1,26 @@ +{ + "title": "users", + "type": "array", + "items": { + "type": "object", + "properties": { + "userName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "pswdHash": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "role": { + "type": "string" + } + }, + "required": ["userName", "email", "pswdHash", "salt", "role"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/index.js b/platform/mongodb-migration/migrations/2/index.js new file mode 100644 index 0000000..e532f98 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/index.js @@ -0,0 +1,378 @@ +const MongoMigration = require(`../../model/migration.js`) +const schemas = { + clientsStatic: require('./schemas/clients_static.json'), + dbVersion: require('./schemas/db_version.json'), + flowTmp: require('./schemas/flow_tmp.json'), + users: require('./schemas/users.json'), + workflowsStatic: require('./schemas/workflows_static.json'), + workflowsTemplates: require('./schemas/workflows_templates.json'), + mqtt_acls: require('./schemas/mqtt_acls.json'), + mqtt_users: require('./schemas/mqtt_users.json'), + androidUsers: require('./schemas/android_users.json'), + webappHosts: require('./schemas/webapp_hosts.json'), + localSkills: require('./schemas/local_skills.json'), +} + +class Migrate extends MongoMigration { + constructor() { + super() + this.version = 2 + } + async migrateUp() { + try { + const collections = await this.listCollections() + const collectionNames = [] + let migrationErrors = [] + collections.map(col => { + collectionNames.push(col.name) + }) + + + /************/ + /* FLOW_TMP */ + /************/ + if (collectionNames.indexOf('flow_tmp') >= 0) { // collection exist + const flowTmp = await this.mongoRequest('flow_tmp', {}) + if (flowTmp.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(flowTmp, schemas.flowTmp) + if (schemaValid.valid) { // schema is valid + const neededVal = flowTmp.filter(ct => ct.id === 'tmp') + if (neededVal.length === 0) { + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + await this.mongoInsert('flow_tmp', payload) + } + } else { // Schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'flow_tmp', + errors: schemaValid.errors + }) + } + } else { // collection exist but empty + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + + // Insert default data + await this.mongoInsert('flow_tmp', payload) + } + } else { // collection does not exist + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + + // Create collection and insert default data + await this.mongoInsert('flow_tmp', payload) + } + + /***********************/ + /* WORKFLOWS_TEMAPLTES */ + /***********************/ + + const DeviceWorkflowTemplateValid = require('./json/device-default-flow.json') + const MultiUserWorkflowTemplate = require('./json/multi-user-default-flow.json') + + const DeviceWorkflowTemplateValidValid = this.testSchema([DeviceWorkflowTemplateValid], schemas.workflowsTemplates) + const MultiUserWorkflowTemplateValid = this.testSchema([MultiUserWorkflowTemplate], schemas.workflowsTemplates) + + if (collectionNames.indexOf('workflows_templates') >= 0) { // collection exist + const workflowsTemplates = await this.mongoRequest('workflows_templates', {}) + + if (workflowsTemplates.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(workflowsTemplates, schemas.workflowsTemplates) + if (schemaValid.valid) { // schema is valid + const neededValStatic = workflowsTemplates.filter(ct => ct.name === 'device-default-workflow') + const needValApplication = workflowsTemplates.filter(ct => ct.name === 'multi-user-default-workflow') + + // Insert Static template and application template if they don't exist + if (neededValStatic.length === 0) { // required value doesn't exist + await this.mongoInsert('workflows_templates', DeviceWorkflowTemplateValid) + } + + if (needValApplication.length === 0) { // required value doesn't exist + await this.mongoInsert('workflows_templates', MultiUserWorkflowTemplate) + } + + + } else { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'workflows_templates', + errors: schemaValid.errors + }) + } + } else { //collection exist but empty + if (DeviceWorkflowTemplateValidValid.valid) { + await this.mongoInsert('workflows_templates', DeviceWorkflowTemplateValid) + } else { + migrationErrors.push({ + collectionName: 'workflows_templates', + errors: DeviceWorkflowTemplateValidValid.errors + }) + } + + if (MultiUserWorkflowTemplateValid.valid) { + await this.mongoInsert('workflows_templates', MultiUserWorkflowTemplate) + } else { + migrationErrors.push({ + collectionName: 'workflows_templates', + errors: MultiUserWorkflowTemplate.errors + }) + } + } + } else { // collection doesn't exist + if (DeviceWorkflowTemplateValidValid.valid) { + await this.mongoInsert('workflows_templates', DeviceWorkflowTemplateValid) + await this.mongoInsert('workflows_templates', MultiUserWorkflowTemplate) + } else { + migrationErrors.push({ + collectionName: 'workflows_templates', + errors: DeviceWorkflowTemplateValidValid.errors + }) + } + } + + /*********/ + /* USERS */ + /*********/ + if (collectionNames.indexOf('users') >= 0) { // collection exist + const users = await this.mongoRequest('users', {}) + if (users.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(users, schemas.users) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'users', + errors: schemaValid.errors + }) + } + } + } + + /******************/ + /* CLIENTS_STATIC */ + /******************/ + if (collectionNames.indexOf('clients_static') >= 0) { // collection exist + const clientsStatic = await this.mongoRequest('clients_static', {}) + + if (clientsStatic.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(clientsStatic, schemas.clientsStatic) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'clients_static', + errors: schemaValid.errors + }) + } + } + } + + /********************/ + /* WORKFLOWS_STATIC */ + /********************/ + if (collectionNames.indexOf('workflows_static') >= 0) { // collection exist + const workflowsStatic = await this.mongoRequest('workflows_static', {}) + if (workflowsStatic.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(workflowsStatic, schemas.workflowsStatic) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'workflows_static', + errors: schemaValid.errors + }) + } + } + } + + /******************/ + /* ANDROID_USERS */ + /*****************/ + if (collectionNames.indexOf('android_users') >= 0) { // collection exist + const androidUsers = await this.mongoRequest('android_users', {}) + if (androidUsers.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(androidUsers, schemas.androidUsers) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'android_users', + errors: schemaValid.errors + }) + } + } + } + + /****************/ + /* WEBAPP_HOSTS */ + /***************/ + if (collectionNames.indexOf('webapp_hosts') >= 0) { // collection exist + const webappHosts = await this.mongoRequest('webapp_hosts', {}) + if (webappHosts.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(webappHosts, schemas.webappHosts) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'webapp_hosts', + errors: schemaValid.errors + }) + } + } + } + + /*****************/ + /* LOCAL_SKILLS */ + /****************/ + if (collectionNames.indexOf('local_skills') >= 0) { // collection exist + const localSkills = await this.mongoRequest('local_skills', {}) + if (localSkills.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(localSkills, schemas.localSkills) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'local_skills', + errors: schemaValid.errors + }) + } + } + } + + /*************/ + /* DBVERSION */ + /*************/ + if (collectionNames.indexOf('dbversion') >= 0) { // collection exist + const dbversion = await this.mongoRequest('dbversion', {}) + const schemaValid = this.testSchema(dbversion, schemas.dbVersion) + if (schemaValid.valid) { // schema valid + await this.mongoUpdate('dbversion', { id: 'current_version' }, { version: this.version }) + } else { // schema is invalid + migrationErrors.push({ + collectionName: 'dbversion', + errors: schemaValid.errors + }) + } + } else { // collection doesn't exist + await this.mongoInsert('dbversion', { + id: 'current_version', + version: this.version + }) + } + + + /************************/ + /* MQTT AUTH COLLECTION */ + /************************/ + // Allways remove mqtt_user at the start + if (collectionNames.indexOf('mqtt_users') >= 0) await this.mongoDrop('mqtt_users') + + if (collectionNames.indexOf('mqtt_users') >= 0) { // collection exist + const mqtt_users = await this.mongoRequest('mqtt_users', {}) + if (mqtt_users.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(mqtt_users, schemas.mqtt_users) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'mqtt_users', + errors: schemaValid.errors + }) + } + } + } + + if (collectionNames.indexOf('mqtt_acls') >= 0) { // collection exist + const mqtt_acls = await this.mongoRequest('mqtt_acls', {}) + if (mqtt_acls.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(mqtt_acls, schemas.mqtt_acls) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'mqtt_acls', + errors: schemaValid.errors + }) + } + } + } else { + await this.mongoInsert('mqtt_acls', { topic: '+/tolinto/%u/#', acc: 3 }) + await this.mongoInsert('mqtt_acls', { topic: '+/fromlinto/%u/#', acc: 3 }) + } + + + /**************************/ + /* REMOVE OLD COLLECTIONS */ + /**************************/ + // Remove if collection exist + if (collectionNames.indexOf('context_types') >= 0) await this.mongoDrop('context_types') + if (collectionNames.indexOf('context') >= 0) await this.mongoDrop('context') + if (collectionNames.indexOf('flow_pattern_tmp') >= 0) await this.mongoDrop('flow_pattern_tmp') + if (collectionNames.indexOf('flow_pattern') >= 0) await this.mongoDrop('flow_pattern') + if (collectionNames.indexOf('lintos') >= 0) await this.mongoDrop('lintos') + if (collectionNames.indexOf('linto_users') >= 0) await this.mongoDrop('linto_users') + + // RETURN + if (migrationErrors.length > 0) { + throw migrationErrors + } else { + await this.mongoUpdate('dbversion', { id: 'current_version' }, { version: this.version }) + console.log(`> MongoDB migration to version "${this.version}": Success `) + return true + } + } catch (error) { + console.error(error) + if (typeof(error) === 'object' && error.length > 0) { + console.error('======== Migration ERROR ========') + error.map(err => { + if (!!err.collectionName && !!err.errors) { + console.error('> Collection: ', err.collectionName) + err.errors.map(e => { + console.error('Error: ', e) + }) + } + }) + console.error('=================================') + } + return error + } + } + async migrateDown() { + + try { + const collections = await this.listCollections() + const collectionNames = [] + collections.map(col => { + collectionNames.push(col.name) + }) + + // Remove if collection exist + if (collectionNames.indexOf('clients_static') >= 0) await this.mongoDrop('clients_static') + if (collectionNames.indexOf('db_version') >= 0) await this.mongoDrop('db_version') + if (collectionNames.indexOf('flow_tmp') >= 0) await this.mongoDrop('flow_tmp') + if (collectionNames.indexOf('workflows_static') >= 0) await this.mongoDrop('workflows_static') + if (collectionNames.indexOf('workflows_application') >= 0) await this.mongoDrop('workflows_application') + if (collectionNames.indexOf('workflows_templates') >= 0) await this.mongoDrop('workflows_templates') + if (collectionNames.indexOf('local_skills') >= 0) await this.mongoDrop('local_skills') + if (collectionNames.indexOf('mqtt_acls') >= 0) await this.mongoDrop('mqtt_acls') + if (collectionNames.indexOf('mqtt_users') >= 0) await this.mongoDrop('mqtt_users') + if (collectionNames.indexOf('android_users') >= 0) await this.mongoDrop('android_users') + if (collectionNames.indexOf('webapp_hosts') >= 0) await this.mongoDrop('webapp_hosts') + + return true + } catch (error) { + console.error(error) + return false + } + + } +} + +module.exports = new Migrate() \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/json/device-default-flow.json b/platform/mongodb-migration/migrations/2/json/device-default-flow.json new file mode 100644 index 0000000..77f6a31 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/json/device-default-flow.json @@ -0,0 +1,202 @@ +{ + "name": "device-default-workflow", + "type": "static", + "flow": [{ + "id": "be6fd44c.33822", + "type": "tab", + "label": "SandBox", + "disabled": false, + "info": "" + }, + { + "id": "48d297d1.2ebde8", + "type": "linto-config", + "z": "be6fd44c.33822", + "name": "", + "configMqtt": "355cc7bb.9e486", + "configEvaluate": "e5538090.0da508", + "configTranscribe": "887e4dc7.82a26", + "language": "fr-FR", + "x": 120, + "y": 40, + "wires": [] + }, + { + "id": "e810d232.bc33c8", + "type": "linto-red-event-emitter", + "z": "be6fd44c.33822", + "name": "", + "x": 920, + "y": 100, + "wires": [] + }, + { + "id": "856638f0.e6cfd8", + "type": "linto-evaluate", + "z": "be6fd44c.33822", + "name": "", + "x": 730, + "y": 100, + "wires": [ + [ + "e810d232.bc33c8" + ] + ] + }, + { + "id": "84e9565b.798a2", + "type": "linto-transcribe", + "z": "be6fd44c.33822", + "name": "", + "x": 560, + "y": 100, + "wires": [ + [ + "856638f0.e6cfd8" + ] + ] + }, + { + "id": "89b8e953.d83bb", + "type": "linto-out", + "z": "be6fd44c.33822", + "name": "", + "x": 1140, + "y": 120, + "wires": [] + }, + { + "id": "f999e18c.924b9", + "type": "linto-model-dataset", + "z": "be6fd44c.33822", + "name": "", + "x": 530, + "y": 200, + "wires": [] + }, + { + "id": "3d4602b9.4a7b2e", + "type": "linto-ui", + "z": "be6fd44c.33822", + "name": "", + "group": "e7bb73f7.537b8", + "width": "9", + "height": "5", + "x": 1150, + "y": 60, + "wires": [] + }, + { + "id": "bca109ee.43a938", + "type": "to-linto-ui", + "z": "be6fd44c.33822", + "name": "", + "x": 1020, + "y": 60, + "wires": [ + [ + "3d4602b9.4a7b2e" + ] + ] + }, + { + "id": "226fea8b.9e09fe", + "type": "linto-terminal-in", + "z": "be6fd44c.33822", + "name": "", + "x": 110, + "y": 100, + "wires": [ + [ + "127dd3c9.86da64" + ] + ] + }, + { + "id": "5a769f16.1826b", + "type": "linto-on-connect", + "z": "be6fd44c.33822", + "name": "", + "x": 300, + "y": 40, + "wires": [] + }, + { + "id": "127dd3c9.86da64", + "type": "linto-pipeline-router", + "z": "be6fd44c.33822", + "name": "", + "x": 310, + "y": 100, + "wires": [ + [ + "84e9565b.798a2" + ], + [], + [] + ] + }, + { + "id": "2274858c.4c712a", + "type": "linto-skill-welcome", + "z": "be6fd44c.33822", + "name": "", + "command": "##intent|capabilities|fr\n- que peux tu faire\n- que sais tu faire\n- dis moi [tous](all) ce que tu sais faire\n- donne moi [toutes](all) tes commandes\n\n##intent|capabilities|en\n- what do you do\n- give me [all](all) you'r voice commands\n\n##intent|goodbye|fr\n- ciao\n- à bientôt\n- à la prochaine\n- au revoir\n- à plus\n\n##intent|goodbye|en\n- see you soon\n- goodbye\n- goodbye linto\n- thank you \n- see you\n- bye\n- thanks\n\n##intent|greeting|fr\n- bonjour\n- bonjour, comment tu t'appelles\n- comment tu t'appelles\n- bonsoir\n- salut\n\n##intent|greeting|en\n- greeting\n- hey\n- good morning\n- good afternoon\n- good evening\n- hello\n- hi\n- what's your name\n\n##intent|howareyou|fr\n- comment ça va\n- comment vas-tu\n- comment tu vas\n- est-ce que ça va\n- tout va [bien](isok)\n- je vais [bien](isok)\n- je [vais bien](isok)\n- ça va [bien](isok)\n- [oui](isok)\n- je ne vais [pas très bien](isko)\n- ça ne va pas [très bien](isko)\n- je ne [vais pas bien](isko)\n- je ne me sens [pas bien](isko)\n- ça [ne va pas](isko) trop\n\n##intent|howareyou|en\n- are you okay\n- how are you doing\n- how do you do\n- everything is [good](isok)\n- i am [happy](isok)\n- [ok](isok)\n- i am [fine](isok)\n- [good](isok) thanks\n- i'm [fine](isok)\n- i'm [ok](isok)\n- everything is [alright](isok)\n- i'm [not well](isko) thank you\n- i'm [not fine](isko)\n- i'm [not ok](isko)\n- i'm [not feeling good](isko) today\n- i'm in a [bad mood](isko)\n- [not fine](isko)\n- [not ok](isko)\n- [not good](isko) thanks\n- [not well](isko) thanks\n- i am [sad](isko)\n", + "description": { + "en-US": "greeting someone", + "fr-FR": "souhaiter la bienvenue" + }, + "x": 130, + "y": 180, + "wires": [ + [] + ] + }, + { + "id": "f82b7c7c.4abe4", + "type": "linto-skill-weather", + "z": "be6fd44c.33822", + "name": "", + "defaultCity": "", + "degreeType": "C", + "api": "microsoft", + "command": "##intent|weather|fr\n- quelle est la météo de [demain](time)\n- le temps est-il clément [demain](time)\n- quel temps fait-il [demain](time)\n- quel temps fait-il [demain](time) à [toulouse](location)\n- quelle est la météo pour [demain](time)\n- c'est quoi la météo à [lens](location)\n- donne-moi la météo à [bordeaux](location)\n- quel temps fait-il à [nantes](location) [demain](time)\n- donne-moi la météo de [nice](location)\n\n##intent|weather|en\n- what's the weather in [new york](location)\n- give me the weather at [london](location)\n- could you give me the weather of [london](location)\n- weather at [london](location)\n- can you give me the weather at [phoenix](location)\n- what's the weather of [tomorrow](time)\n- what is the weather for [tomorrow](time)\n- is the weather good [tomorrow](time)\n- the weather for [tomorrow](time) please\n- what's the weather in [toulouse](location) [tomorrow](time)\n- is the weather good [tomorrow](time) in [toulouse](location)\n- what's the weather for [tomorrow](time) in [paris](location)\n- the weather at [roma](location)\n", + "description": { + "en-US": "get weather information", + "fr-FR": "donnez la météo d'une ville" + }, + "x": 120, + "y": 220, + "wires": [ + [] + ] + }, + { + "id": "355cc7bb.9e486", + "type": "linto-config-mqtt", + "z": "be6fd44c.33822", + "host": "localhost", + "port": "1883", + "scope": "blk", + "login": "login", + "password": "password" + }, + { + "id": "e5538090.0da508", + "type": "linto-config-evaluate", + "z": "be6fd44c.33822", + "host": "host", + "api": "tock", + "appname": "appname", + "namespace": "app" + }, + { + "id": "887e4dc7.82a26", + "type": "linto-config-transcribe", + "z": "be6fd44c.33822", + "host": "host", + "api": "linstt" + } + ], + "created_date": "2020-07-30T10:43:49+02:00" +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/json/multi-user-default-flow.json b/platform/mongodb-migration/migrations/2/json/multi-user-default-flow.json new file mode 100644 index 0000000..23c4ba1 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/json/multi-user-default-flow.json @@ -0,0 +1,204 @@ +{ + "name": "multi-user-default-workflow", + "type": "application", + "flow": [{ + "id": "a0fb8820.c9d0f", + "type": "tab", + "label": "SandBox", + "disabled": false, + "info": "" + }, + { + "id": "dd6c64b3.07c618", + "type": "linto-config", + "z": "a0fb8820.c9d0f", + "name": "", + "configMqtt": "f2f95b61.f37c4", + "configEvaluate": "5f9f9b8f.6eec74", + "configTranscribe": "fea697ce.7f8ea", + "language": "fr-FR", + "x": 120, + "y": 40, + "wires": [] + }, + { + "id": "3fdec229.3afc4e", + "type": "linto-red-event-emitter", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 960, + "y": 100, + "wires": [] + }, + { + "id": "8c05b152.806bb8", + "type": "linto-evaluate", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 770, + "y": 100, + "wires": [ + [ + "3fdec229.3afc4e" + ] + ] + }, + { + "id": "6094cfae.9224a", + "type": "linto-transcribe", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 600, + "y": 100, + "wires": [ + [ + "8c05b152.806bb8" + ] + ] + }, + { + "id": "6b7205f0.5de0cc", + "type": "linto-out", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 1140, + "y": 120, + "wires": [] + }, + { + "id": "7f1cdb68.d79d8c", + "type": "linto-model-dataset", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 530, + "y": 200, + "wires": [] + }, + { + "id": "9219532c.5a171", + "type": "linto-ui", + "z": "a0fb8820.c9d0f", + "name": "", + "group": "e7bb73f7.537b8", + "width": "9", + "height": "5", + "x": 1150, + "y": 60, + "wires": [] + }, + { + "id": "50e16ed8.709d28", + "type": "to-linto-ui", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 1020, + "y": 60, + "wires": [ + [ + "9219532c.5a171" + ] + ] + }, + { + "id": "d2017fc2.910f58", + "type": "linto-application-in", + "z": "a0fb8820.c9d0f", + "name": "", + "auth_android": false, + "auth_web": false, + "x": 110, + "y": 100, + "wires": [ + [ + "6485c2e.c35d2bc" + ] + ] + }, + { + "id": "dce00109.eb573", + "type": "linto-on-connect", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 300, + "y": 40, + "wires": [] + }, + { + "id": "29091bdb.ff15b4", + "type": "linto-skill-weather", + "z": "a0fb8820.c9d0f", + "name": "", + "defaultCity": "", + "degreeType": "C", + "api": "microsoft", + "command": "##intent|weather|fr\n- quelle est la météo de [demain](time)\n- le temps est-il clément [demain](time)\n- quel temps fait-il [demain](time)\n- quel temps fait-il [demain](time) à [toulouse](location)\n- quelle est la météo pour [demain](time)\n- c'est quoi la météo à [lens](location)\n- donne-moi la météo à [bordeaux](location)\n- quel temps fait-il à [nantes](location) [demain](time)\n- donne-moi la météo de [nice](location)\n\n##intent|weather|en\n- what's the weather in [new york](location)\n- give me the weather at [london](location)\n- could you give me the weather of [london](location)\n- weather at [london](location)\n- can you give me the weather at [phoenix](location)\n- what's the weather of [tomorrow](time)\n- what is the weather for [tomorrow](time)\n- is the weather good [tomorrow](time)\n- the weather for [tomorrow](time) please\n- what's the weather in [toulouse](location) [tomorrow](time)\n- is the weather good [tomorrow](time) in [toulouse](location)\n- what's the weather for [tomorrow](time) in [paris](location)\n- the weather at [roma](location)\n", + "description": { + "en-US": "get weather information", + "fr-FR": "donnez la météo d'une ville" + }, + "x": 120, + "y": 240, + "wires": [ + [] + ] + }, + { + "id": "268b9be.a0a0964", + "type": "linto-skill-welcome", + "z": "a0fb8820.c9d0f", + "name": "", + "command": "##intent|capabilities|fr\n- que peux tu faire\n- que sais tu faire\n- dis moi [tous](all) ce que tu sais faire\n- donne moi [toutes](all) tes commandes\n\n##intent|capabilities|en\n- what do you do\n- give me [all](all) you'r voice commands\n\n##intent|goodbye|fr\n- ciao\n- à bientôt\n- à la prochaine\n- au revoir\n- à plus\n\n##intent|goodbye|en\n- see you soon\n- goodbye\n- goodbye linto\n- thank you \n- see you\n- bye\n- thanks\n\n##intent|greeting|fr\n- bonjour\n- bonjour, comment tu t'appelles\n- comment tu t'appelles\n- bonsoir\n- salut\n\n##intent|greeting|en\n- greeting\n- hey\n- good morning\n- good afternoon\n- good evening\n- hello\n- hi\n- what's your name\n\n##intent|howareyou|fr\n- comment ça va\n- comment vas-tu\n- comment tu vas\n- est-ce que ça va\n- tout va [bien](isok)\n- je vais [bien](isok)\n- je [vais bien](isok)\n- ça va [bien](isok)\n- [oui](isok)\n- je ne vais [pas très bien](isko)\n- ça ne va pas [très bien](isko)\n- je ne [vais pas bien](isko)\n- je ne me sens [pas bien](isko)\n- ça [ne va pas](isko) trop\n\n##intent|howareyou|en\n- are you okay\n- how are you doing\n- how do you do\n- everything is [good](isok)\n- i am [happy](isok)\n- [ok](isok)\n- i am [fine](isok)\n- [good](isok) thanks\n- i'm [fine](isok)\n- i'm [ok](isok)\n- everything is [alright](isok)\n- i'm [not well](isko) thank you\n- i'm [not fine](isko)\n- i'm [not ok](isko)\n- i'm [not feeling good](isko) today\n- i'm in a [bad mood](isko)\n- [not fine](isko)\n- [not ok](isko)\n- [not good](isko) thanks\n- [not well](isko) thanks\n- i am [sad](isko)\n", + "description": { + "en-US": "greeting someone", + "fr-FR": "souhaiter la bienvenue" + }, + "x": 130, + "y": 200, + "wires": [ + [] + ] + }, + { + "id": "6485c2e.c35d2bc", + "type": "linto-pipeline-router", + "z": "a0fb8820.c9d0f", + "name": "", + "x": 350, + "y": 100, + "wires": [ + [ + "6094cfae.9224a" + ], + [], + [] + ] + }, + { + "id": "f2f95b61.f37c4", + "type": "linto-config-mqtt", + "z": "a0fb8820.c9d0f", + "host": "localhost", + "port": "1883", + "scope": "blk", + "login": "login", + "password": "password" + }, + { + "id": "5f9f9b8f.6eec74", + "type": "linto-config-evaluate", + "z": "a0fb8820.c9d0f", + "host": "host", + "api": "tock", + "appname": "appname", + "namespace": "app" + }, + { + "id": "fea697ce.7f8ea", + "type": "linto-config-transcribe", + "z": "a0fb8820.c9d0f", + "host": "host", + "api": "linstt" + } + ], + "created_date": "2020-07-30T10:43:49+02:00" +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/android_users.json b/platform/mongodb-migration/migrations/2/schemas/android_users.json new file mode 100644 index 0000000..e685665 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/android_users.json @@ -0,0 +1,25 @@ +{ + "title": "android_users", + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "pswdHash": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "applications": { + "type": "array" + }, + "keyToken": { + "type": "string" + } + } + }, + "required": ["email", "pswdHash", "salt", "applications"] +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/clients_static.json b/platform/mongodb-migration/migrations/2/schemas/clients_static.json new file mode 100644 index 0000000..f070b37 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/clients_static.json @@ -0,0 +1,51 @@ +{ + "title": "clients_static", + "type": "array", + "items": { + "type": "object", + "properties": { + "enrolled": { + "type": "boolean" + }, + "connexion": { + "type": "string", + "enum": ["offline", "online"] + }, + "last_up": { + "anyOf": [{ + "type": "string", + "format": "date-time" + }, { + "type": "string" + }] + }, + "last_down": { + "anyOf": [{ + "type": "string", + "format": "date-time" + }, { + "type": "string" + }] + }, + "associated_workflow": { + "anyOf": [{ + "type": "object" + }, + { + "type": "null" + } + ] + }, + "sn": { + "type": "string" + }, + "config": { + "type": "object" + }, + "meeting": { + "type": "array" + } + }, + "required": ["enrolled", "connexion", "last_up", "last_down", "sn", "config", "associated_workflow"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/db_version.json b/platform/mongodb-migration/migrations/2/schemas/db_version.json new file mode 100644 index 0000000..235cc76 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/db_version.json @@ -0,0 +1,16 @@ +{ + "title": "dbversion", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": ["id", "version"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/flow_tmp.json b/platform/mongodb-migration/migrations/2/schemas/flow_tmp.json new file mode 100644 index 0000000..0acda35 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/flow_tmp.json @@ -0,0 +1,19 @@ +{ + "title": "flow_tmp", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "flow": { + "type": "array" + }, + "workspaceId": { + "type": "string" + } + }, + "required": ["id", "flow", "workspaceId"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/local_skills.json b/platform/mongodb-migration/migrations/2/schemas/local_skills.json new file mode 100644 index 0000000..0250270 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/local_skills.json @@ -0,0 +1,20 @@ +{ + "title": "local_skills", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + } + }, + "required": ["name", "version", "created_date"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/mqtt_acls.json b/platform/mongodb-migration/migrations/2/schemas/mqtt_acls.json new file mode 100644 index 0000000..cbbd151 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/mqtt_acls.json @@ -0,0 +1,16 @@ +{ + "title": "mqtt_acls", + "type": "array", + "items": { + "type": "object", + "properties": { + "topic": { + "type": "string" + }, + "acc": { + "type": "integer" + } + }, + "required": ["topic", "acc"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/mqtt_users.json b/platform/mongodb-migration/migrations/2/schemas/mqtt_users.json new file mode 100644 index 0000000..8522f53 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/mqtt_users.json @@ -0,0 +1,25 @@ +{ + "title": "users", + "type": "array", + "items": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "superuser": { + "type": "boolean" + }, + "acls": { + "type": "array" + }, + "email": { + "type": "string" + } + }, + "required": ["username", "password", "superuser"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/users.json b/platform/mongodb-migration/migrations/2/schemas/users.json new file mode 100644 index 0000000..df1a0da --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/users.json @@ -0,0 +1,26 @@ +{ + "title": "users", + "type": "array", + "items": { + "type": "object", + "properties": { + "userName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "pswdHash": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "role": { + "type": "string" + } + }, + "required": ["userName", "email", "pswdHash", "salt", "role"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/webapp_hosts.json b/platform/mongodb-migration/migrations/2/schemas/webapp_hosts.json new file mode 100644 index 0000000..dacd491 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/webapp_hosts.json @@ -0,0 +1,37 @@ +{ + "title": "webapp_hosts", + "type": "array", + "items": { + "type": "object", + "properties": { + "originUrl": { + "type": "string", + "format": "uri" + }, + "applications": { + "type": "array", + "items": { + "type": "object", + "properties": { + "applicationId": { + "type": "string" + }, + "requestToken": { + "type": "string" + }, + "slots": { + "type": "array", + "items": { + "type": "object" + } + }, + "maxSlots": { + "type": "integer" + } + } + } + } + }, + "required": ["originUrl", "applications"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/workflows_application.json b/platform/mongodb-migration/migrations/2/schemas/workflows_application.json new file mode 100644 index 0000000..e9ecf40 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/workflows_application.json @@ -0,0 +1,38 @@ +{ + "title": "workflows_application", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + }, + "updated_date": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "nodes": { + "type": "array" + } + } + } + }, + "required": ["name", "flowId", "created_date", "updated_date", "flow"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/workflows_static.json b/platform/mongodb-migration/migrations/2/schemas/workflows_static.json new file mode 100644 index 0000000..bb20775 --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/workflows_static.json @@ -0,0 +1,41 @@ +{ + "title": "workflows_static", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "associated_device": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + }, + "updated_date": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "nodes": { + "type": "array" + } + } + } + }, + "required": ["name", "flowId", "associated_device", "created_date", "updated_date", "flow"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/2/schemas/workflows_templates.json b/platform/mongodb-migration/migrations/2/schemas/workflows_templates.json new file mode 100644 index 0000000..13bcf8e --- /dev/null +++ b/platform/mongodb-migration/migrations/2/schemas/workflows_templates.json @@ -0,0 +1,23 @@ +{ + "title": "workflows_templates", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "flow": { + "type": "array" + }, + "created_date": { + "type": "string", + "format": "date-time" + } + }, + "required": ["name", "type", "flow", "created_date"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/index.js b/platform/mongodb-migration/migrations/3/index.js new file mode 100644 index 0000000..f09a39d --- /dev/null +++ b/platform/mongodb-migration/migrations/3/index.js @@ -0,0 +1,312 @@ +const MongoMigration = require(`../../model/migration.js`) +const schemas = { + clientsStatic: require('./schemas/clients_static.json'), + dbVersion: require('./schemas/db_version.json'), + flowTmp: require('./schemas/flow_tmp.json'), + users: require('./schemas/users.json'), + workflowsStatic: require('./schemas/workflows_static.json'), + workflowsApplication: require('./schemas/workflows_application.json'), + mqtt_acls: require('./schemas/mqtt_acls.json'), + mqtt_users: require('./schemas/mqtt_users.json'), + androidUsers: require('./schemas/android_users.json'), + webappHosts: require('./schemas/webapp_hosts.json'), + localSkills: require('./schemas/local_skills.json'), +} + +class Migrate extends MongoMigration { + constructor() { + super() + this.version = 3 + } + async migrateUp() { + try { + const collections = await this.listCollections() + const collectionNames = [] + let migrationErrors = [] + collections.map(col => { + collectionNames.push(col.name) + }) + + + /************/ + /* FLOW_TMP */ + /************/ + if (collectionNames.indexOf('flow_tmp') >= 0) { // collection exist + const flowTmp = await this.mongoRequest('flow_tmp', {}) + if (flowTmp.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(flowTmp, schemas.flowTmp) + if (schemaValid.valid) { // schema is valid + const neededVal = flowTmp.filter(ct => ct.id === 'tmp') + if (neededVal.length === 0) { + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + await this.mongoInsert('flow_tmp', payload) + } + } else { // Schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'flow_tmp', + errors: schemaValid.errors + }) + } + } else { // collection exist but empty + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + + // Insert default data + await this.mongoInsert('flow_tmp', payload) + } + } else { // collection does not exist + const payload = { + id: "tmp", + flow: [], + workspaceId: "" + } + + // Create collection and insert default data + await this.mongoInsert('flow_tmp', payload) + } + + /*********/ + /* USERS */ + /*********/ + if (collectionNames.indexOf('users') >= 0) { // collection exist + const users = await this.mongoRequest('users', {}) + if (users.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(users, schemas.users) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'users', + errors: schemaValid.errors + }) + } + } + } + + /******************/ + /* CLIENTS_STATIC */ + /******************/ + if (collectionNames.indexOf('clients_static') >= 0) { // collection exist + const clientsStatic = await this.mongoRequest('clients_static', {}) + + if (clientsStatic.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(clientsStatic, schemas.clientsStatic) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'clients_static', + errors: schemaValid.errors + }) + } + } + } + + /********************/ + /* WORKFLOWS_STATIC */ + /********************/ + if (collectionNames.indexOf('workflows_static') >= 0) { // collection exist + const workflowsStatic = await this.mongoRequest('workflows_static', {}) + if (workflowsStatic.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(workflowsStatic, schemas.workflowsStatic) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'workflows_static', + errors: schemaValid.errors + }) + } + } + } + + /*************************/ + /* WORKFLOWS_APPLICATION */ + /*************************/ + if (collectionNames.indexOf('workflows_application') >= 0) { // collection exist + const workflowsApplication = await this.mongoRequest('workflows_application', {}) + if (workflowsApplication.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(workflowsApplication, schemas.workflowsApplication) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'workflows_application', + errors: schemaValid.errors + }) + } + } + } + + /******************/ + /* ANDROID_USERS */ + /*****************/ + if (collectionNames.indexOf('android_users') >= 0) { // collection exist + const androidUsers = await this.mongoRequest('android_users', {}) + if (androidUsers.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(androidUsers, schemas.androidUsers) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'android_users', + errors: schemaValid.errors + }) + } + } + } + + /****************/ + /* WEBAPP_HOSTS */ + /***************/ + if (collectionNames.indexOf('webapp_hosts') >= 0) { // collection exist + const webappHosts = await this.mongoRequest('webapp_hosts', {}) + if (webappHosts.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(webappHosts, schemas.webappHosts) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'webapp_hosts', + errors: schemaValid.errors + }) + } + } + } + + /*****************/ + /* LOCAL_SKILLS */ + /****************/ + if (collectionNames.indexOf('local_skills') >= 0) { // collection exist + const localSkills = await this.mongoRequest('local_skills', {}) + if (localSkills.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(localSkills, schemas.localSkills) + + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'local_skills', + errors: schemaValid.errors + }) + } + } + } + + /*************/ + /* DBVERSION */ + /*************/ + if (collectionNames.indexOf('dbversion') >= 0) { // collection exist + const dbversion = await this.mongoRequest('dbversion', {}) + const schemaValid = this.testSchema(dbversion, schemas.dbVersion) + if (schemaValid.valid) { // schema valid + await this.mongoUpdate('dbversion', { id: 'current_version' }, { version: this.version }) + } else { // schema is invalid + migrationErrors.push({ + collectionName: 'dbversion', + errors: schemaValid.errors + }) + } + } else { // collection doesn't exist + await this.mongoInsert('dbversion', { + id: 'current_version', + version: this.version + }) + } + + + /************************/ + /* MQTT AUTH COLLECTION */ + /************************/ + // Allways remove mqtt_user at the start + if (collectionNames.indexOf('mqtt_users') >= 0) await this.mongoDrop('mqtt_users') + + if (collectionNames.indexOf('mqtt_users') >= 0) { // collection exist + const mqtt_users = await this.mongoRequest('mqtt_users', {}) + if (mqtt_users.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(mqtt_users, schemas.mqtt_users) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'mqtt_users', + errors: schemaValid.errors + }) + } + } + } + + if (collectionNames.indexOf('mqtt_acls') >= 0) { // collection exist + const mqtt_acls = await this.mongoRequest('mqtt_acls', {}) + if (mqtt_acls.length > 0) { // collection exist and not empty + const schemaValid = this.testSchema(mqtt_acls, schemas.mqtt_acls) + if (!schemaValid.valid) { // schema is invalid + // Add errors to migrationErrors array + migrationErrors.push({ + collectionName: 'mqtt_acls', + errors: schemaValid.errors + }) + } + } + } else { + await this.mongoInsert('mqtt_acls', { topic: '+/tolinto/%u/#', acc: 3 }) + await this.mongoInsert('mqtt_acls', { topic: '+/fromlinto/%u/#', acc: 3 }) + } + + + /**************************/ + /* REMOVE OLD COLLECTIONS */ + /**************************/ + // Remove if collection exist + if (collectionNames.indexOf('workflows_templates') >= 0) await this.mongoDrop('workflows_templates') + console.log('collectionNames', collectionNames) + + + // RETURN + if (migrationErrors.length > 0) { + throw migrationErrors + } else { + await this.mongoUpdate('dbversion', { id: 'current_version' }, { version: this.version }) + console.log(`> MongoDB migration to version "${this.version}": Success `) + return true + } + } catch (error) { + console.error(error) + if (typeof(error) === 'object' && error.length > 0) { + console.error('======== Migration ERROR ========') + error.map(err => { + if (!!err.collectionName && !!err.errors) { + console.error('> Collection: ', err.collectionName) + err.errors.map(e => { + console.error('Error: ', e) + }) + } + }) + console.error('=================================') + } + return error + } + } + async migrateDown() { + + try { + const collections = await this.listCollections() + const collectionNames = [] + collections.map(col => { + collectionNames.push(col.name) + }) + + return true + } catch (error) { + console.error(error) + return false + } + + } +} + +module.exports = new Migrate() \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/android_users.json b/platform/mongodb-migration/migrations/3/schemas/android_users.json new file mode 100644 index 0000000..e685665 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/android_users.json @@ -0,0 +1,25 @@ +{ + "title": "android_users", + "type": "array", + "items": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "pswdHash": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "applications": { + "type": "array" + }, + "keyToken": { + "type": "string" + } + } + }, + "required": ["email", "pswdHash", "salt", "applications"] +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/clients_static.json b/platform/mongodb-migration/migrations/3/schemas/clients_static.json new file mode 100644 index 0000000..f070b37 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/clients_static.json @@ -0,0 +1,51 @@ +{ + "title": "clients_static", + "type": "array", + "items": { + "type": "object", + "properties": { + "enrolled": { + "type": "boolean" + }, + "connexion": { + "type": "string", + "enum": ["offline", "online"] + }, + "last_up": { + "anyOf": [{ + "type": "string", + "format": "date-time" + }, { + "type": "string" + }] + }, + "last_down": { + "anyOf": [{ + "type": "string", + "format": "date-time" + }, { + "type": "string" + }] + }, + "associated_workflow": { + "anyOf": [{ + "type": "object" + }, + { + "type": "null" + } + ] + }, + "sn": { + "type": "string" + }, + "config": { + "type": "object" + }, + "meeting": { + "type": "array" + } + }, + "required": ["enrolled", "connexion", "last_up", "last_down", "sn", "config", "associated_workflow"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/db_version.json b/platform/mongodb-migration/migrations/3/schemas/db_version.json new file mode 100644 index 0000000..235cc76 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/db_version.json @@ -0,0 +1,16 @@ +{ + "title": "dbversion", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "required": ["id", "version"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/flow_tmp.json b/platform/mongodb-migration/migrations/3/schemas/flow_tmp.json new file mode 100644 index 0000000..0acda35 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/flow_tmp.json @@ -0,0 +1,19 @@ +{ + "title": "flow_tmp", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "flow": { + "type": "array" + }, + "workspaceId": { + "type": "string" + } + }, + "required": ["id", "flow", "workspaceId"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/local_skills.json b/platform/mongodb-migration/migrations/3/schemas/local_skills.json new file mode 100644 index 0000000..0250270 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/local_skills.json @@ -0,0 +1,20 @@ +{ + "title": "local_skills", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + } + }, + "required": ["name", "version", "created_date"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/mqtt_acls.json b/platform/mongodb-migration/migrations/3/schemas/mqtt_acls.json new file mode 100644 index 0000000..cbbd151 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/mqtt_acls.json @@ -0,0 +1,16 @@ +{ + "title": "mqtt_acls", + "type": "array", + "items": { + "type": "object", + "properties": { + "topic": { + "type": "string" + }, + "acc": { + "type": "integer" + } + }, + "required": ["topic", "acc"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/mqtt_users.json b/platform/mongodb-migration/migrations/3/schemas/mqtt_users.json new file mode 100644 index 0000000..8522f53 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/mqtt_users.json @@ -0,0 +1,25 @@ +{ + "title": "users", + "type": "array", + "items": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "superuser": { + "type": "boolean" + }, + "acls": { + "type": "array" + }, + "email": { + "type": "string" + } + }, + "required": ["username", "password", "superuser"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/users.json b/platform/mongodb-migration/migrations/3/schemas/users.json new file mode 100644 index 0000000..df1a0da --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/users.json @@ -0,0 +1,26 @@ +{ + "title": "users", + "type": "array", + "items": { + "type": "object", + "properties": { + "userName": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "pswdHash": { + "type": "string" + }, + "salt": { + "type": "string" + }, + "role": { + "type": "string" + } + }, + "required": ["userName", "email", "pswdHash", "salt", "role"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/webapp_hosts.json b/platform/mongodb-migration/migrations/3/schemas/webapp_hosts.json new file mode 100644 index 0000000..dacd491 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/webapp_hosts.json @@ -0,0 +1,37 @@ +{ + "title": "webapp_hosts", + "type": "array", + "items": { + "type": "object", + "properties": { + "originUrl": { + "type": "string", + "format": "uri" + }, + "applications": { + "type": "array", + "items": { + "type": "object", + "properties": { + "applicationId": { + "type": "string" + }, + "requestToken": { + "type": "string" + }, + "slots": { + "type": "array", + "items": { + "type": "object" + } + }, + "maxSlots": { + "type": "integer" + } + } + } + } + }, + "required": ["originUrl", "applications"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/workflows_application.json b/platform/mongodb-migration/migrations/3/schemas/workflows_application.json new file mode 100644 index 0000000..e9ecf40 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/workflows_application.json @@ -0,0 +1,38 @@ +{ + "title": "workflows_application", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + }, + "updated_date": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "nodes": { + "type": "array" + } + } + } + }, + "required": ["name", "flowId", "created_date", "updated_date", "flow"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/migrations/3/schemas/workflows_static.json b/platform/mongodb-migration/migrations/3/schemas/workflows_static.json new file mode 100644 index 0000000..bb20775 --- /dev/null +++ b/platform/mongodb-migration/migrations/3/schemas/workflows_static.json @@ -0,0 +1,41 @@ +{ + "title": "workflows_static", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "associated_device": { + "type": "string" + }, + "created_date": { + "type": "string", + "format": "date-time" + }, + "updated_date": { + "type": "string", + "format": "date-time" + }, + "flow": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "nodes": { + "type": "array" + } + } + } + }, + "required": ["name", "flowId", "associated_device", "created_date", "updated_date", "flow"] + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/package.json b/platform/mongodb-migration/package.json new file mode 100644 index 0000000..380d0ac --- /dev/null +++ b/platform/mongodb-migration/package.json @@ -0,0 +1,23 @@ +{ + "name": "mongodb-migration", + "version": "0.3.0", + "description": "linto-platform-admin migration service", + "main": "index.js", + "scripts": { + "migrate": "node index.js" + }, + "author": "Romain Lopez ", + "contributors": [ + "Romain Lopez ", + "Damien Laine ", + "Yoann Houpert " + ], + "license": "GNU AFFERO GPLV3", + "dependencies": { + "debug": "^4.1.1", + "dotenv": "^6.0.0", + "mongodb": "^3.1.13", + "path": "^0.12.7", + "z-schema": "^4.2.2" + } +} \ No newline at end of file diff --git a/platform/mongodb-migration/wait-for-it.sh b/platform/mongodb-migration/wait-for-it.sh new file mode 100755 index 0000000..92cbdbb --- /dev/null +++ b/platform/mongodb-migration/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file diff --git a/platform/overwatch/.dockerignore b/platform/overwatch/.dockerignore new file mode 100644 index 0000000..7ccd5ad --- /dev/null +++ b/platform/overwatch/.dockerignore @@ -0,0 +1,8 @@ +Dockerfile +.env +.dockerignore +.git +.gitignore +.gitlab-ci.yml +docker-compose.yml +node_modules/ \ No newline at end of file diff --git a/platform/overwatch/.envdefault b/platform/overwatch/.envdefault new file mode 100644 index 0000000..61b59a9 --- /dev/null +++ b/platform/overwatch/.envdefault @@ -0,0 +1,42 @@ +# Overwatch settings +# Default 80 +LINTO_STACK_OVERWATCH_HTTP_PORT= +LINTO_STACK_OVERWATCH_LOGS_MONGODB=false + +# Overwatch auth settings +LINTO_STACK_OVERWATCH_JWT_SECRET=secret +LINTO_STACK_OVERWATCH_REFRESH_SECRET=refresh +LINTO_STACK_OVERWATCH_AUTH_TYPE=local + +LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_URL= +LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_BASE= +LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_FILTER= + +LINTO_STACK_OVERWATCH_BASE_PATH=/overwatch + +# MQTT settings +LINTO_STACK_MQTT_HOST=localhost +LINTO_STACK_MQTT_PORT=1883 +LINTO_STACK_MQTT_KEEP_ALIVE=2 +LINTO_STACK_MQTT_USE_LOGIN=false +LINTO_STACK_MQTT_USER= +LINTO_STACK_MQTT_PASSWORD= + +LINTO_STACK_OVERWATCH_DEVICE_TOPIC_KEY=DEV_ +LINTO_STACK_OVERWATCH_WEB_TOPIC_KEY=WEB_ + +# MQTT WS settings +LINTO_STACK_WSS=false +LINTO_STACK_MQTT_OVER_WS=false +LINTO_STACK_MQTT_OVER_WS_ENDPOINT=/mqtt + +# Mongo settings +LINTO_STACK_MONGODB_SERVICE=127.0.0.1 +LINTO_STACK_MONGODB_PORT=27017 +LINTO_STACK_MONGODB_USE_LOGIN=false +LINTO_STACK_MONGODB_USER= +LINTO_STACK_MONGODB_PASSWORD= + +LINTO_STACK_MONGODB_DBNAME=linto +LINTO_STACK_MONGODB_COLLECTION_LINTOS=lintos +LINTO_STACK_MONGODB_COLLECTION_LOG=statusLog diff --git a/platform/overwatch/.github/workflows/dockerhub-description.yml b/platform/overwatch/.github/workflows/dockerhub-description.yml new file mode 100644 index 0000000..1dee926 --- /dev/null +++ b/platform/overwatch/.github/workflows/dockerhub-description.yml @@ -0,0 +1,20 @@ +name: Update Docker Hub Description +on: + push: + branches: + - master + paths: + - README.md + - .github/workflows/dockerhub-description.yml +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + repository: lintoai/linto-platform-overwatch + readme-filepath: ./README.md diff --git a/platform/overwatch/Dockerfile b/platform/overwatch/Dockerfile new file mode 100644 index 0000000..830b549 --- /dev/null +++ b/platform/overwatch/Dockerfile @@ -0,0 +1,17 @@ +FROM node + +WORKDIR /usr/src/app/linto-platform-overwatch + +COPY . /usr/src/app/linto-platform-overwatch + +RUN npm install + +HEALTHCHECK CMD node docker-healthcheck.js || exit 1 + +EXPOSE 80 + +COPY ./wait-for-it.sh / +COPY ./docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] + +#CMD ["node", "index.js"] \ No newline at end of file diff --git a/platform/overwatch/README.md b/platform/overwatch/README.md new file mode 100644 index 0000000..f41610f --- /dev/null +++ b/platform/overwatch/README.md @@ -0,0 +1,63 @@ +# LinTO-Platform-Overwatch + +This service is mandatory in a complete LinTO platform stack. +These covered process are : +- MQTT global subscriber for a fleet of LinTO clients +- Register every events in a persistent storage +- Enable different authentication system + +## Usage +See documentation : [doc.linto.ai](https://doc.linto.ai) + +## API +Overwatch API has the following feature +- Customizable api base path (setting the environment variable : `LINTO_STACK_OVERWATCH_BASE_PATH`) +- Authentication module can be enable or disable (environement `LINTO_STACK_OVERWATCH_AUTH_TYPE`, note that only local authentication method is supported for the moment) + +More information about there API can be found : + +**Default API** : +- [Overwatch API](doc/api/default.md) + +**Authentication APIC**: +- [Local](doc/api/auth/local.md) + +# Deploy + +With our proposed stack [linto-platform-stack](https://github.com/linto-ai/linto-platform-stack) + +# Develop + +## Getting Started +These instructions will get you a copy of the project up and running on your local machine for development. Thise module require at least for working : +* Mqtt server +* Mongodb + +Nodejs shall be installed `sudo apt-get install nodejs`, also npm shall be installed `sudo apt-get install npm` + +## Install project +``` +git clone https://github.com/linto-ai/linto-platform-overwatch.git +cd linto-platform-overwatch +npm install +``` + +### Configuration environement +`cp .envdefault .env` +Then update the `.env` to manage your personal configuration + +### Run project +Normal : `npm run start` +Debug : `DEBUG=* npm run start` + +# Docker +## Install Docker +You will need to have Docker installed on your machine. If they are already installed, you can skip this part. +Otherwise, you can install them referring to [https://docs.docker.com/engine/installation/](https://docs.docker.com/engine/installation/ "Install Docker") + +## Build +Next step is to build the docker image with the following command `docker build -t linto-overwatch .` +Then you just need to run bls image`docker run -d -it linto-overwatch` + +## Stack +You will find the full process to deploy the LinTO platform here : [LinTO-Platform-Stack](https://github.com/linto-ai/linto-platform-stack) or on the website [doc.linto.ai](https://doc.linto.ai/#/services/nlu?id=installation) \ No newline at end of file diff --git a/platform/overwatch/RELEASE.md b/platform/overwatch/RELEASE.md new file mode 100644 index 0000000..e3196e1 --- /dev/null +++ b/platform/overwatch/RELEASE.md @@ -0,0 +1,35 @@ +# 1.2.3 +- Add special check when max_slot is 0 +- Handle false requestToken for created app orgin + +# 1.2.2 +- clean console + +# 1.2.1 +- Added Wait-for-it for mongo +- Handle web user with unique password + +# 1.2.0 +- Added MQTT_USER session management +- Added Web slot manager +- Added WSS env configuration +- Added an Handler error request + +# 1.1.0 +- Added Android auth +- Added WebToken auth + +# 1.0.3 +- Reworking of local auth +- Added support mongodb with linto-mongodb + +# 1.0.2 +- Added passthrough auth mode +- Refactoring mongodb connector + +# 1.0.1 +- Added authentification system +- Reworking of environement settings for linto-stack + +# 1.0.0 +- Release of LinTO-Overwatch v1 diff --git a/platform/overwatch/config.js b/platform/overwatch/config.js new file mode 100644 index 0000000..442b069 --- /dev/null +++ b/platform/overwatch/config.js @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2017 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +require('dotenv').config() +const debug = require('debug')('linto-overwatch:config') +const dotenv = require('dotenv') +const fs = require('fs') + + +function ifHasNotThrow(element, error) { + if (!element) throw error + return element +} +function ifHas(element, defaultValue) { + if (!element) return defaultValue + return element +} + + +function configureDefaults() { + try { + const envdefault = dotenv.parse(fs.readFileSync('.envdefault')) + + process.env.LINTO_STACK_DOMAIN = ifHas(process.env.LINTO_STACK_DOMAIN, envdefault.LINTO_STACK_DOMAIN) + process.env.LINTO_STACK_OVERWATCH_BASE_PATH = ifHas(process.env.LINTO_STACK_OVERWATCH_BASE_PATH, envdefault.LINTO_STACK_OVERWATCH_BASE_PATH) + + //MQTT Configuration + + process.env.LINTO_STACK_MQTT_HOST = ifHas(process.env.LINTO_STACK_MQTT_HOST, envdefault.LINTO_STACK_MQTT_HOST) + process.env.LINTO_STACK_MQTT_PORT = ifHas(process.env.LINTO_STACK_MQTT_PORT, envdefault.LINTO_STACK_MQTT_PORT) + process.env.LINTO_STACK_MQTT_KEEP_ALIVE = ifHas(process.env.LINTO_STACK_MQTT_KEEP_ALIVE, envdefault.LINTO_STACK_MQTT_KEEP_ALIVE) + + if (process.env.LINTO_STACK_DOMAIN === 'undefined') + process.env.LINTO_STACK_DOMAIN = process.env.LINTO_STACK_MQTT_HOST + + process.env.LINTO_STACK_MQTT_USE_LOGIN = ifHas(process.env.LINTO_STACK_MQTT_USE_LOGIN, envdefault.LINTO_STACK_MQTT_USE_LOGIN) + if (process.env.LINTO_STACK_MQTT_USE_LOGIN === 'true') { + process.env.LINTO_STACK_MQTT_USER = ifHas(process.env.LINTO_STACK_MQTT_USER, envdefault.LINTO_STACK_MQTT_USER) + process.env.LINTO_STACK_MQTT_PASSWORD = ifHas(process.env.LINTO_STACK_MQTT_PASSWORD, envdefault.LINTO_STACK_MQTT_PASSWORD) + } + + //MQTT WS configuration + process.env.LINTO_STACK_WSS = ifHas(process.env.LINTO_STACK_WSS, envdefault.LINTO_STACK_WSS) + process.env.LINTO_STACK_MQTT_OVER_WS = ifHas(process.env.LINTO_STACK_MQTT_OVER_WS, envdefault.LINTO_STACK_MQTT_OVER_WS) + process.env.LINTO_STACK_MQTT_OVER_WS_ENDPOINT = ifHas(process.env.LINTO_STACK_MQTT_OVER_WS_ENDPOINT, envdefault.LINTO_STACK_MQTT_OVER_WS_ENDPOINT) + + process.env.LINTO_STACK_OVERWATCH_DEVICE_TOPIC_KEY = ifHas(process.env.LINTO_STACK_OVERWATCH_DEVICE_TOPIC_KEY, envdefault.LINTO_STACK_OVERWATCH_DEVICE_TOPIC_KEY) + process.env.LINTO_STACK_OVERWATCH_WEB_TOPIC_KEY = ifHas(process.env.LINTO_STACK_OVERWATCH_WEB_TOPIC_KEY, envdefault.LINTO_STACK_OVERWATCH_WEB_TOPIC_KEY) + + //Mongo Configuration + process.env.LINTO_STACK_MONGODB_SERVICE = ifHas(process.env.LINTO_STACK_MONGODB_SERVICE, envdefault.LINTO_STACK_MONGODB_SERVICE) + process.env.LINTO_STACK_MONGODB_PORT = ifHas(process.env.LINTO_STACK_MONGODB_PORT, envdefault.LINTO_STACK_MONGODB_PORT) + process.env.LINTO_STACK_MONGODB_DBNAME = ifHas(process.env.LINTO_STACK_MONGODB_DBNAME, envdefault.LINTO_STACK_MONGODB_DBNAME) + + //Mongo collection + process.env.LINTO_STACK_MONGODB_COLLECTION_LINTOS = ifHas(process.env.LINTO_STACK_MONGODB_COLLECTION_LINTOS, envdefault.LINTO_STACK_MONGODB_COLLECTION_LINTOS) + process.env.LINTO_STACK_MONGODB_COLLECTION_LOG = ifHas(process.env.LINTO_STACK_MONGODB_COLLECTION_LOG, envdefault.LINTO_STACK_MONGODB_COLLECTION_LOG) + process.env.LINTO_STACK_MONGODB_COLLECTION_ANDROID_USER = ifHas(process.env.LINTO_STACK_MONGODB_COLLECTION_ANDROID_USER, envdefault.LINTO_STACK_MONGODB_COLLECTION_ANDROID_USER) + + process.env.LINTO_STACK_MONGODB_USE_LOGIN = ifHas(process.env.LINTO_STACK_MONGODB_USE_LOGIN, envdefault.LINTO_STACK_MONGODB_USE_LOGIN) + if (process.env.LINTO_STACK_MONGODB_USE_LOGIN === 'true') { + process.env.LINTO_STACK_MONGODB_USER = ifHas(process.env.LINTO_STACK_MONGODB_USER, envdefault.LINTO_STACK_MONGODB_USER) + process.env.LINTO_STACK_MONGODB_PASSWORD = ifHas(process.env.LINTO_STACK_MONGODB_PASSWORD, envdefault.LINTO_STACK_MONGODB_PASSWORD) + } + + process.env.LINTO_STACK_OVERWATCH_LOG_MONGODB = ifHas(process.env.LINTO_STACK_OVERWATCH_LOG_MONGODB, envdefault.LINTO_STACK_OVERWATCH_LOG_MONGODB) + process.env.LINTO_STACK_OVERWATCH_HTTP_PORT = ifHas(process.env.LINTO_STACK_OVERWATCH_HTTP_PORT, 80) + process.env.LINTO_STACK_OVERWATCH_AUTH_TYPE = ifHas(process.env.LINTO_STACK_OVERWATCH_AUTH_TYPE, envdefault.LINTO_STACK_OVERWATCH_AUTH_TYPE) + + process.env.LINTO_STACK_OVERWATCH_AUTH_TYPE.split(',').map(auth => { + //TODO: Check if particular auth settings + if (auth === 'ldap') { + process.env.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_URL = ifHas(process.env.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_URL, envdefault.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_URL) + process.env.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_BASE = ifHas(process.env.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_BASE, envdefault.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_BASE) + process.env.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_FILTER = ifHas(process.env.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_FILTER, envdefault.LINTO_STACK_OVERWATCH_AUTH_LDAP_SERVER_SEARCH_FILTER) + } + if (auth === 'local') { + process.env.LINTO_STACK_OVERWATCH_JWT_SECRET = ifHas(process.env.LINTO_STACK_OVERWATCH_JWT_SECRET, envdefault.LINTO_STACK_OVERWATCH_JWT_SECRET) + process.env.LINTO_STACK_OVERWATCH_REFRESH_SECRET = ifHas(process.env.LINTO_STACK_OVERWATCH_REFRESH_SECRET, envdefault.LINTO_STACK_OVERWATCH_REFRESH_SECRET) + } + }) + + } catch (e) { + console.error(e) + process.exit(1) + } +} +module.exports = configureDefaults() \ No newline at end of file diff --git a/platform/overwatch/doc/api/auth/ldap.md b/platform/overwatch/doc/api/auth/ldap.md new file mode 100644 index 0000000..b688432 --- /dev/null +++ b/platform/overwatch/doc/api/auth/ldap.md @@ -0,0 +1,2 @@ +# LDAP +These authentication system is WIP \ No newline at end of file diff --git a/platform/overwatch/doc/api/auth/local.md b/platform/overwatch/doc/api/auth/local.md new file mode 100644 index 0000000..4ebfa9e --- /dev/null +++ b/platform/overwatch/doc/api/auth/local.md @@ -0,0 +1,238 @@ +# Local +Authentication module using an username and password. This authentication method is based on JWTs (Json Web Tokens) +User are based on linto-admin component. + +## Android Login +Used to collect information when android authentication is successful (authentication Token, refresh Token and mqtt information of the current stack). + +**URL** : `/:LINTO_STACK_OVERWATCH_BASE_PATH/local/android/login/` + +**Method** : `POST` + +**Auth required** : NO + +**Data constraints** +```json +{ + "username": "[valid email address]", + "password": "[password in plain text]" +} +``` + +**Data example** +```json +{ + "username": "user@linto.ai", + "password": "abcd1234" +} +``` + +### Success Response + +**Code** : `202 Accepted` + +**Info** Generate an `Android` token type + +**Body Content** +```json +{ + "user": { + "auth_token": "YYYYYYYYYYYYYYYYYYYYYYYYYYY", + "refresh_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXX", + "expiration_date": 1597502813, + "session_id": "5ee881b36915343d3dz16b13" + }, + "mqtt": { + "mqtt_host": "localhost", + "mqtt_port": "1883", + "mqtt_use_login": true, + "mqtt_password": "password", + "mqtt_login": "user" + } +} +``` + +### Error Response + +**Condition** : If 'username' and 'password' combination is wrong. + +**Code** : `401 Unauthorized` + +**Body Content** : +``` +Unauthorized +``` + +## Android Refresh +Used to refresh the user token when expired (authentication Token, refresh Token and mqtt information of the current stack). + +**URL** : `/:LINTO_STACK_OVERWATCH_BASE_PATH/local/android/refresh/` + +**Method** : `POST` + +**Auth required** : YES + +**Data header constraints** + +``` +Authorization : Android auth_user_refresh_token +``` +**Data header example** +``` +Authorization : Android XXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +### Success Response + +**Code** : `202 Accepted` + +**Info** Generate an new `Android` token type + +**Body Content** +```json +{ + "auth_token": "YYYYYYYYYYYYYYYYYYYYYYYYYYY", + "refresh_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXX", + "expiration_date": 1597502813, + "session_id": "5ee881b36915343d3dz16b13" + } +} +``` + +### Error Response + +**Condition** : If token is wrong + +**Code** : `401 Unauthorized` + +**Body Content** : +``` +{ + "message": "The token is malformed" +} +``` + +## Web Login +Used to collect information when authentication is successful (authentication Token). + +**URL** : `/:LINTO_STACK_OVERWATCH_BASE_PATH/local/web/login/` + +**Method** : `POST` + +**Auth required** : NO + +**Data constraints** +```json +{ + "requestToken": "[Token generated by linto admin]", +} +``` + +**Data example** +```json +{ + "requestToken": "XXXXXXXXXXXXXXXX", +} +``` + +### Success Response + +**Code** : `202 Accepted` + +**Info** Generate an `WebApplication` token type + +**Body Content** +```json +{ + "user": { + "auth_token": "YYYYYYYYYYYYYYYYYYYYYYYYYYY" + } +} +``` + +### Error Response + +**Condition** : If 'requestToken' and 'url' combination is wrong. + +**Code** : `401 Unauthorized` + +**Body Content** : +``` +Unauthorized +``` + +## Applications +Used to collect scopes for a registered User. + +**URL** : `/:LINTO_STACK_OVERWATCH_BASE_PATH/local/applications/` + +**Method** : `GET` + +**Auth required** : YES + +**Data header constraints** + +``` +Authorization : Android auth_user_token +``` +**Data header example** +``` +Authorization : Android XXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +### Success Response + +**Code** : `200 OK` + +**Body Content** +```json +[ + { + "topic": "blk", + "name": "Default", + "description": "Default scope" + }, + { + "topic": "LNG", + "name": "Linagora", + "description": "A small description of the scope" + } +] +``` + +### Error Response +#### Missing token +**Condition** : When token is missing + +**Code** : `401 Unauthorized` + +**Body Content** : + +```json +{ + "message": "No authorization token was found" +} +``` + +#### Token invalid +**Condition** : If invalid token is send + +**Code** : `401 Unauthorized` + +**Body Content** : +```json +{ + "message": "Unexpected token x in JSON at position x" +} +``` +#### Token expired +**Condition** : If token is expired + +**Code** : `401 Unauthorized` + +**Body Content** : +```json +{ + "message": "invalid exp value" +} +``` diff --git a/platform/overwatch/doc/api/default.md b/platform/overwatch/doc/api/default.md new file mode 100644 index 0000000..695590b --- /dev/null +++ b/platform/overwatch/doc/api/default.md @@ -0,0 +1,48 @@ +# Default +Default overwatch API, allow to get basic information of the service running, authentication methods running, ... + +## Healths + +Health checks for overwatch service + +**URL** : `/:LINTO_STACK_OVERWATCH_BASE_PATH/healthcheck` + +**Method** : `GET` + +**Auth required** : NO + +### Success Response + +**Code** : `200 OK` + +**Body Content** +``` +OK +``` + +## Authentication methods +List of enable authentication services + +**URL** : `/:LINTO_STACK_OVERWATCH_BASE_PATH/auths` + +**Method** : `GET` + +**Auth required** : NO + +### Success Response + +**Code** : `200 OK` + +**Body Content** +```json +[ + { + "type": "local", + "basePath": "/local" + }, + { + "type": "ldap", + "basePath": "/ldap" + } +] +``` \ No newline at end of file diff --git a/platform/overwatch/docker-compose.yml b/platform/overwatch/docker-compose.yml new file mode 100644 index 0000000..6a477a6 --- /dev/null +++ b/platform/overwatch/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.5' + +services: + linto-overwatch: + build: . + container_name: linto-platform-overwatch + env_file: .env diff --git a/platform/overwatch/docker-entrypoint.sh b/platform/overwatch/docker-entrypoint.sh new file mode 100755 index 0000000..fb28d31 --- /dev/null +++ b/platform/overwatch/docker-entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +echo "Waiting MQTT and mongo..." +/wait-for-it.sh $LINTO_STACK_MONGODB_SERVICE:$LINTO_STACK_MONGODB_PORT --timeout=20 --strict -- echo " $LINTO_STACK_MONGODB_SERVICE:$LINTO_STACK_MONGODB_PORT is up" +/wait-for-it.sh $LINTO_STACK_MQTT_HOST:$LINTO_STACK_MQTT_PORT --timeout=20 --strict -- echo " $LINTO_STACK_MQTT_HOST:$LINTO_STACK_MQTT_PORT is up" + +while [ "$1" != "" ]; do + case $1 in + --run-cmd?*) + script=${1#*=} # Deletes everything up to "=" and assigns the remainder. + ;; + --run-cmd=) # Handle the case of an empty --run-cmd= + die 'ERROR: "--run-cmd" requires a non-empty option argument.' + ;; + *) + echo "ERROR: Bad argument provided \"$1\"" + exit 1 + ;; + esac + shift +done + +echo "RUNNING : $script" +cd /usr/src/app/linto-platform-overwatch + +eval "$script" diff --git a/platform/overwatch/docker-healthcheck.js b/platform/overwatch/docker-healthcheck.js new file mode 100644 index 0000000..3f16ee5 --- /dev/null +++ b/platform/overwatch/docker-healthcheck.js @@ -0,0 +1,7 @@ +const request = require('request') + +request(`http://localhost`, error => { + if (error) { + throw error + } +}) diff --git a/platform/overwatch/index.js b/platform/overwatch/index.js new file mode 100644 index 0000000..cda75dc --- /dev/null +++ b/platform/overwatch/index.js @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const debug = require('debug')('linto-overwatch:ctl') +require('./config') + +class Ctl { + constructor() { + this.init() + } + async init() { + try { + const LintoOverwatch = await require('./lib/overwatch/overwatch') // will sequence actions on LinTO's MQTT payloads + this.lintoWatcher = await new LintoOverwatch() + + this.webServer = await require('./webserver') + } catch (error) { + console.error(error) + process.exit(1) + } + } +} + +new Ctl() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/driver.js b/platform/overwatch/lib/overwatch/mongodb/driver.js new file mode 100644 index 0000000..6be5221 --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/driver.js @@ -0,0 +1,70 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:driver') +const mongoDb = require('mongodb') + +let urlMongo = 'mongodb://' +if (process.env.LINTO_STACK_MONGODB_USE_LOGIN === 'true') + urlMongo += process.env.LINTO_STACK_MONGODB_USER + ':' + process.env.LINTO_STACK_MONGODB_PASSWORD + '@' +urlMongo += process.env.LINTO_STACK_MONGODB_SERVICE + ':' + process.env.LINTO_STACK_MONGODB_PORT + '/' + process.env.LINTO_STACK_MONGODB_DBNAME + +if (process.env.LINTO_STACK_MONGODB_USE_LOGIN === 'true') + urlMongo += '?authSource=' + process.env.LINTO_STACK_MONGODB_DBNAME + +class MongoDriver { + static mongoDb = mongoDb + static urlMongo = urlMongo + static client = mongoDb.MongoClient + static db = null + + // Check mongo database connection status + static checkConnection() { + try { + if (!!MongoDriver.db && MongoDriver.db.serverConfig) { + return MongoDriver.db.serverConfig.isConnected() + } else { + return false + } + } catch (error) { + console.error(error) + return false + } + } + + constructor() { + this.poolOptions = { + numberOfRetries: 5, + auto_reconnect: true, + poolSize: 40, + connectTimeoutMS: 5000, + useNewUrlParser: true, + useUnifiedTopology: true + } + // if connexion exists + if (MongoDriver.checkConnection()) { + return this + } + + // Otherwise, inits connexions and binds event handling + + MongoDriver.client.connect(MongoDriver.urlMongo, this.poolOptions, (err, client) => { + if (err) { + console.error('> MongoDB ERROR unable to connect:', err.toString()) + } else { + console.log('> MongoDB : Connected') + MongoDriver.db = client.db(process.env.LINTO_STACK_MONGODB_DBNAME) + const mongoEvent = client.topology + + mongoEvent.on('close', () => { + console.error('> MongoDb : Connection lost ') + }) + mongoEvent.on('error', (e) => { + console.error('> MongoDb ERROR: ', e) + }) + mongoEvent.on('reconnect', () => { + console.error('> MongoDb : reconnect') + }) + } + }) + } +} + +module.exports = new MongoDriver() // Exports a singleton \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/model.js b/platform/overwatch/lib/overwatch/mongodb/model.js new file mode 100644 index 0000000..3e880bd --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/model.js @@ -0,0 +1,107 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:model') +const MongoDriver = require('./driver.js') + +class MongoModel { + constructor(collection) { + this.collection = collection + } + + getObjectId(id) { + return MongoDriver.constructor.mongoDb.ObjectID(id) + } + + /* ========================= */ + /* ===== MONGO METHODS ===== */ + /* ========================= */ + /** + * Request function for mongoDB. This function will make a request on the "collection", filtered by the "query" passed in parameters. + * @param {string} collection + * @param {Object} query + * @returns {Pomise} + */ + async mongoRequest(query) { + return new Promise((resolve, reject) => { + try { + MongoDriver.constructor.db.collection(this.collection).find(query).toArray((error, result) => { + if (error) reject(error) + resolve(result) + }) + } catch (error) { + console.error(error) + reject(error) + } + }) + } + + /** + * Insert/Create function for mongoDB. This function will create an entry based on the "collection", the "query" and the "values" passed in parmaters. + * @param {Object} query + * @param {Object} values + * @returns {Pomise} + */ + async mongoInsert(payload) { + return new Promise((resolve, reject) => { + try { + MongoDriver.constructor.db.collection(this.collection).insertOne(payload, function (error, result) { + if (error) { + reject(error) + } + resolve('success') + }) + } catch (error) { + console.error(error) + reject(error) + } + }) + } + + /** + * Update function for mongoDB. This function will update an entry based on the "collection", the "query" and the "values" passed in parmaters. + * @param {Object} query + * @param {Object} values + * @returns {Pomise} + */ + async mongoUpdate(query, values) { + if (values._id) { + delete values._id + } + return new Promise((resolve, reject) => { + try { + MongoDriver.constructor.db.collection(this.collection).updateOne(query, { + $set: values + }, function (error, result) { + if (error) { + reject(error) + } + resolve('success') + }) + } catch (error) { + console.error(error) + reject(error) + } + }) + } + + /** + * Delete function for mongoDB. This function will create an entry based on the "collection", the "query" passed in parmaters. + * @param {Object} query + * @returns {Pomise} + */ + async mongoDelete(query) { + return new Promise((resolve, reject) => { + try { + MongoDriver.constructor.db.collection(this.collection).deleteOne(query, function (error, result) { + if (error) { + reject(error) + } + resolve("success") + }) + } catch (error) { + console.error(error) + reject(error) + } + }) + } +} + +module.exports = MongoModel \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/android_users.js b/platform/overwatch/lib/overwatch/mongodb/models/android_users.js new file mode 100644 index 0000000..8c6aee5 --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/android_users.js @@ -0,0 +1,51 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:android_users') + +const sha1 = require('sha1') +const MongoModel = require('../model.js') + +class LintoUsersModel extends MongoModel { + constructor() { + super('android_users') + } + + async findById(id) { + try { + return await this.mongoRequest({ _id: id }) + } catch (err) { + console.error(err) + return err + } + } + + async findOne(json) { + try { + let user = await this.mongoRequest(json) + return user[0] + } catch (err) { + console.error(err) + return err + } + } + + async update(payload) { + const query = { + _id: payload._id + } + delete payload._id + let mutableElements = payload + return await this.mongoUpdate(query, mutableElements) + } + + async logout(id) { + const query = { + _id: id + } + return await this.mongoUpdate(query, { keyToken: 'a' }) + } + + validatePassword(password, user) { + return user.pswdHash === sha1(password + user.salt) + } +} + +module.exports = new LintoUsersModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/linto_users.js b/platform/overwatch/lib/overwatch/mongodb/models/linto_users.js new file mode 100644 index 0000000..9b3ed6d --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/linto_users.js @@ -0,0 +1,41 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:linto_users') + +const crypto = require('crypto') +const jwt = require('jsonwebtoken') + +const TOKEN_DAYS_TIME = 10 +const REFRESH_TOKEN_DAYS_TIME = 14 + +const MongoModel = require('../model.js') + +class LintoUsersModel extends MongoModel { + constructor() { + super('linto_users') + } + + async findById(id) { + try { + return await this.mongoRequest({ id }) + } catch (err) { + console.error(err) + return err + } + } + + async findOne(username) { + try { + let user = await this.mongoRequest(username) + return user[0] + } catch (err) { + console.error(err) + return err + } + } + + validatePassword(password, user) { + const hash = crypto.pbkdf2Sync(password, user.salt, 10000, 512, 'sha512').toString('hex') + return user.hash === hash + } +} + +module.exports = new LintoUsersModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/lintos.js b/platform/overwatch/lib/overwatch/mongodb/models/lintos.js new file mode 100644 index 0000000..4c0714b --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/lintos.js @@ -0,0 +1,33 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:lintos') + +const MongoModel = require('../model.js') + +class LintosModel extends MongoModel { + constructor() { + super('clients_static') + } + + // Get a linto by its "sn" (serial number) + async getLintoBySn(sn) { + try { + return await this.mongoRequest({ sn }) + } catch (err) { + console.error(err) + return err + } + } + + async updateLinto(payload) { + try { + const query = { sn: payload.sn } + let mutableElements = payload + delete mutableElements.sn + + return await this.mongoUpdate(query, mutableElements) + } catch (err) { + return err + } + } +} + +module.exports = new LintosModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/logs.js b/platform/overwatch/lib/overwatch/mongodb/models/logs.js new file mode 100644 index 0000000..a225bbd --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/logs.js @@ -0,0 +1,21 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:logs') + +const MongoModel = require('../model.js') + +class LogsModel extends MongoModel { + constructor() { + super('logs') + } + + // Create a new linto that have "fleet" type + async insertLog(logs) { + try { + return await this.mongoInsert(logs) + } catch (err) { + console.error(err) + return err + } + } +} + +module.exports = new LogsModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/mqtt_users.js b/platform/overwatch/lib/overwatch/mongodb/models/mqtt_users.js new file mode 100644 index 0000000..cd1e4b0 --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/mqtt_users.js @@ -0,0 +1,58 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:mqtt_users') + +const MongoModel = require('../model.js') + +const bcrypt = require('bcrypt') +const SALT_ROUND = 10 + +class LogsModel extends MongoModel { + constructor() { + super('mqtt_users') + } + + async findById(id) { + try { + return await this.mongoRequest({ id }) + } catch (err) { + console.error(err) + return err + } + } + + async findByUsername(json) { + try { + return await this.mongoRequest(json) + } catch (err) { + console.error(err) + return err + } + } + + async insertMqttUsers(user) { + try { + let mqttUser = { ...user, superuser: false, acls: [] } + + mqttUser = await bcrypt.hash(user.password, SALT_ROUND).then(hash => { + mqttUser.password = hash + return mqttUser + }) + + await this.mongoInsert(mqttUser) + return mqttUser + } catch (err) { + console.error(err) + return err + } + } + + async deleteMqttUser(username) { + try { + await this.mongoDelete({ username }) + } catch (err) { + console.log(err) + return err + } + } +} + +module.exports = new LogsModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/scopes.js b/platform/overwatch/lib/overwatch/mongodb/models/scopes.js new file mode 100644 index 0000000..1f865e8 --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/scopes.js @@ -0,0 +1,29 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:lintos') + +const MongoModel = require('../model.js') + +class ScopesModel extends MongoModel { + constructor() { + super('scopes') + } + + async getScopesByUser(userId) { + try { + return await this.mongoRequest({ userId }) + } catch (err) { + console.error(err) + return err + } + } + + async getScopesById(id) { + try { + return await this.mongoRequest({ id }) + } catch (err) { + console.error(err) + return err + } + } +} + +module.exports = new ScopesModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/webapp_hosts.js b/platform/overwatch/lib/overwatch/mongodb/models/webapp_hosts.js new file mode 100644 index 0000000..d426007 --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/webapp_hosts.js @@ -0,0 +1,49 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:webapp_host') + +const MongoModel = require('../model.js') +const SlotsManager = new require('../../slotsManager/slotsManager') + +class LintoWebUsersModel extends MongoModel { + constructor() { + super('webapp_hosts') + } + + async findById(id) { + try { + return await this.mongoRequest(id) + } catch (err) { + console.error(err) + return err + } + } + + async findOne(url) { + try { + let user = await this.mongoRequest(url) + return user[0] + } catch (err) { + console.error(err) + return err + } + } + + async update(payload) { + const query = { + _id: payload._id + } + delete payload._id + let mutableElements = payload + + return await this.mongoUpdate(query, mutableElements) + } + + validApplicationAuth(webapp, requestToken) { + return webapp.applications.find(app => (app.requestToken === requestToken)) + } + + async deleteSlot(sn) { + slotsManager.removeSlot(sn) + } +} + +module.exports = new LintoWebUsersModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/mongodb/models/workflows_application.js b/platform/overwatch/lib/overwatch/mongodb/models/workflows_application.js new file mode 100644 index 0000000..004cbc5 --- /dev/null +++ b/platform/overwatch/lib/overwatch/mongodb/models/workflows_application.js @@ -0,0 +1,39 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher:logic:mongodb:models:workflows_application') + +const MongoModel = require('../model.js') + +class ScopesModel extends MongoModel { + constructor() { + super('workflows_application') + } + + async getScopesById(id) { + try { + let workflowRaw = await this.mongoRequest({ _id: this.getObjectId(id) }) + if (workflowRaw.length !== 0) { + for (let node of workflowRaw[0].flow.configs) { + if (node.type === 'linto-config-mqtt') return node.scope + } + } + } catch (err) { + console.error(err) + return err + } + } + + async getScopesByListId(idList) { + try { + let listWorkflow = [] + for (let id of idList) { + let workflowRaw = await this.mongoRequest({ _id: this.getObjectId(id) }) + if (workflowRaw.length !== 0 && workflowRaw[0]) listWorkflow.push(workflowRaw[0]) + } + return listWorkflow + } catch (err) { + console.error(err) + return err + } + } +} + +module.exports = new ScopesModel() \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/overwatch.js b/platform/overwatch/lib/overwatch/overwatch.js new file mode 100644 index 0000000..8bef805 --- /dev/null +++ b/platform/overwatch/lib/overwatch/overwatch.js @@ -0,0 +1,11 @@ +const debug = require('debug')('linto-overwatch:overwatch') + +class LintoOverwatch { + constructor() { + const Watcher = require('./watcher/watcher') + this.clientBrokerInit = new Watcher() + return this + } +} + +module.exports = LintoOverwatch \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/slotsManager/slotsManager.js b/platform/overwatch/lib/overwatch/slotsManager/slotsManager.js new file mode 100644 index 0000000..f738dc4 --- /dev/null +++ b/platform/overwatch/lib/overwatch/slotsManager/slotsManager.js @@ -0,0 +1,60 @@ +const debug = require('debug')('linto-overwatch:overwatch:slotManager') +const jwtDecode = require('jwt-decode') + +const SLOT_ALIVE_TIMEOUT = 1200000 // 20min + +const MqttUsers = require(process.cwd() + '/lib/overwatch/mongodb/models/mqtt_users') + +let slots = {} +let timesoutSlotManager = {} + +let self = module.exports = { + createSlotTimeout: sn => { + timesoutSlotManager[sn] = setTimeout(function () { + debug(`User slots ${sn} has been expired`) + delete self.removeSlot(sn) + }, SLOT_ALIVE_TIMEOUT) + }, + refreshTimeout: sn => { + debug(`User slots ${sn} has been refresh`) + clearTimeout(timesoutSlotManager[sn]) + self.createSlotTimeout(sn) + }, + get: () => slots, + getSn: (sn) => { + self.refreshTimeout(sn) + return slots[sn] + }, + getSnByToken: (sn, token) => { + let decodedToken = jwtDecode(token.split('WebApplication')[1]) + if (decodedToken && decodedToken.data && decodedToken.data.sessionId === sn) + return self.getSn(decodedToken.data.sessionId) + return undefined + }, + takeSlot: async (sn, data) => { + await MqttUsers.insertMqttUsers({ username: sn, password: data.password }) + + self.createSlotTimeout(sn) + slots[sn] = data + return data + }, + takeSlotIfAvailable: (sn, app, originUrl) => { + if (app.maxSlots > self.countSlotsApplication(app.applicationId)) { + return self.takeSlot(sn, { originUrl, applicationId: app.applicationId, password: app.password }) + } else return undefined + }, + countSlotsApplication: (appId) => { + let count = 0 + Object.keys(slots).forEach(key => { + if (slots[key].applicationId === appId) count++ + }) + return count + }, + removeSlot: async (sn) => { + if (slots[sn]) { + await MqttUsers.deleteMqttUser(sn) + clearTimeout(timesoutSlotManager[sn]) + delete slots[sn] + } + } +} diff --git a/platform/overwatch/lib/overwatch/watcher/mqttController/status.js b/platform/overwatch/lib/overwatch/watcher/mqttController/status.js new file mode 100644 index 0000000..508bd66 --- /dev/null +++ b/platform/overwatch/lib/overwatch/watcher/mqttController/status.js @@ -0,0 +1,37 @@ +const MongoLogsCollection = require(process.cwd() + '/lib/overwatch/mongodb/models/logs') +const MongoLintoCollection = require(process.cwd() + '/lib/overwatch/mongodb/models/lintos') + +const SlotsManager = require(process.cwd() + '/lib/overwatch/slotsManager/slotsManager') + +module.exports = function (topic, payload) { + const [_clientCode, _channel, _sn, _etat, _type, _id] = topic.split('/') + const jsonPayload = JSON.parse(payload) + + if (_sn.indexOf(process.env.LINTO_STACK_OVERWATCH_WEB_TOPIC_KEY) !== -1) { // Web client only + if (jsonPayload.connexion === "offline") { + SlotsManager.removeSlot(_sn) + } else if (jsonPayload.connexion === "online" && SlotsManager.getSnByToken(_sn, jsonPayload.auth_token)) { + console.log('User has taken a slot') + } + } else { // Other client : Static or Android + const lastModified = new Date(Date.now()) + const connexionStatus = jsonPayload.connexion + + if (process.env.LINTO_STACK_OVERWATCH_LOG_MONGODB === 'true') { + MongoLogsCollection.insertLog({ + sn: _sn, + status: connexionStatus, + date: lastModified + }) + } + + let dataLinto = { + sn: _sn, + connexion: connexionStatus, + } + connexionStatus === 'offline' ? dataLinto.last_down = lastModified : dataLinto.last_up = lastModified + MongoLintoCollection.updateLinto(dataLinto) + + } + +} \ No newline at end of file diff --git a/platform/overwatch/lib/overwatch/watcher/watcher.js b/platform/overwatch/lib/overwatch/watcher/watcher.js new file mode 100644 index 0000000..255adb4 --- /dev/null +++ b/platform/overwatch/lib/overwatch/watcher/watcher.js @@ -0,0 +1,85 @@ +const debug = require('debug')('linto-overwatch:overwatch:watcher') +const Mqtt = require('mqtt') + +const mqttControllerStatus = require('./mqttController/status') + +class WatcherMqtt { + constructor() { + this.register = [] + this.subTopic = '#' + + this.configMqtt = { + clean: true, + servers: [{ + host: process.env.LINTO_STACK_MQTT_HOST, + port: process.env.LINTO_STACK_MQTT_PORT + }], + keepalive: parseInt(process.env.LINTO_STACK_MQTT_KEEP_ALIVE), //can live for LOCAL_LINTO_STACK_MQTT_KEEP_ALIVE seconds without a single message sent on broker + reconnectPeriod: Math.floor(Math.random() * 1000) + 1000, // ms for reconnect, + qos: 2 + } + + if (process.env.LINTO_STACK_MQTT_USE_LOGIN === 'true') { + this.configMqtt.username = process.env.LINTO_STACK_MQTT_USER + this.configMqtt.password = process.env.LINTO_STACK_MQTT_PASSWORD + } + + return this.init() + } + + async init() { + return new Promise((resolve, reject) => { + let cnxError = setTimeout(() => { + debug('Timeout') + console.error('Unable to connect to Broker') + return reject('Unable to connect') + }, 2000) + + this.client = Mqtt.connect(this.configMqtt) + this.client.on('error', e => { + console.error('broker error : ' + e) + }) + + this.client.on('connect', () => { + //clear any previous subsciptions + this.client.unsubscribe(this.subTopic, (err) => { + if (err) debug('disconnecting while unsubscribing', err) + //Subscribe to the client topics + debug(`subscribing topics...`) + this.client.subscribe(this.subTopic, (err) => { + if (!err) { + debug(`subscribed successfully to ${this.subTopic}`) + } else { + console.error(err) + } + }) + }) + }) + + this.client.once('connect', () => { + clearTimeout(cnxError) + this.client.on('offline', () => { + console.error('broker connexion down') + }) + resolve(this) + }) + + this.client.on('message', async (topic, payload) => { + try { + const [_clientCode, _channel, _sn, _etat, _type, _id] = topic.split('/') + switch (_etat) { + case 'status': + mqttControllerStatus(topic, payload) + break + default: + break + } + } catch (err) { + console.error(err) + } + }) + }) + } +} + +module.exports = WatcherMqtt \ No newline at end of file diff --git a/platform/overwatch/package.json b/platform/overwatch/package.json new file mode 100644 index 0000000..c584439 --- /dev/null +++ b/platform/overwatch/package.json @@ -0,0 +1,52 @@ +{ + "name": "linto-overwatch", + "version": "1.2.3", + "description": "Overwatch module of linto", + "main": "index.js", + "directories": { + "lib": "lib" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/linto-ai/linto-skills-optional.git" + }, + "bugs": { + "url": "https://github.com/linto-ai/linto-skills-optional/issues" + }, + "homepage": "https://linto.ai/", + "keywords": [ + "linto", + "overwatch", + "mqtt", + "mongodb" + ], + "author": "yhoupert@linagora.com", + "license": "AGPL-3.0-or-later", + "dependencies": { + "bcrypt": "^5.0.0", + "body-parser": "^1.19.0", + "debug": "^4.1.1", + "dotenv": "^8.0.0", + "eventemitter3": "^4.0.0", + "express": "^4.17.1", + "express-jwt": "^5.3.1", + "fs": "0.0.1-security", + "jsonwebtoken": "^8.5.1", + "jwt-decode": "^3.0.0-beta.2", + "mongodb": "^3.2.5", + "mqtt": "^2.18.8", + "passport": "^0.4.1", + "passport-http": "^0.3.0", + "passport-http-bearer": "^1.0.1", + "passport-ldapauth": "^2.1.3", + "passport-local": "^1.0.0", + "passport-oauth2": "^1.5.0", + "randomstring": "^1.1.5", + "request": "^2.88.2", + "sha1": "^1.1.1" + } +} diff --git a/platform/overwatch/wait-for-it.sh b/platform/overwatch/wait-for-it.sh new file mode 100755 index 0000000..92cbdbb --- /dev/null +++ b/platform/overwatch/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file diff --git a/platform/overwatch/webserver/config/auth/local.js b/platform/overwatch/webserver/config/auth/local.js new file mode 100644 index 0000000..d638c89 --- /dev/null +++ b/platform/overwatch/webserver/config/auth/local.js @@ -0,0 +1,81 @@ +const debug = require('debug')('linto-overwatch:webserver:config:auth:local') + +require('../passport/local') +const passport = require('passport') +const jwt = require('express-jwt') + +const UsersAndroid = require(process.cwd() + '/lib/overwatch/mongodb/models/android_users') +const SlotsManager = require(process.cwd() + '/lib/overwatch/slotsManager/slotsManager') + +const { UnreservedSlot, MalformedToken } = require('../error/exception/auth') + +const refreshToken = require('./refresh') + +module.exports = { + authType: 'local', + authenticate_android: passport.authenticate('local-android', { session: false }), + authenticate_web: passport.authenticate('local-web', { session: false }), + isAuthenticate: [ + jwt({ + secret: generateSecretFromHeaders, + userProperty: 'payload', + getToken: getTokenFromHeaders, + }), + (req, res, next) => { + next() + } + ], + refresh_android: [ + jwt({ + secret: generateRefreshSecretFromHeaders, + userProperty: 'payload', + getToken: getTokenFromHeaders, + }), + async (req, res, next) => { + const { headers: { authorization } } = req + let token = await refreshToken(authorization) + res.local = token + next() + } + ] +} + +function getTokenFromHeaders(req, res, next) { + const { headers: { authorization } } = req + if (authorization && authorization.split(' ')[0] === 'Android') return authorization.split(' ')[1] + else if (authorization && authorization.split(' ')[0] === 'WebApplication') return authorization.split(' ')[1] + else return null +} + +function generateSecretFromHeaders(req, payload, done) { + if (!payload || !payload.data) { + done(new MalformedToken()) + } else { + const { headers: { authorization } } = req + if (authorization.split(' ')[0] === 'Android') { + UsersAndroid.findOne({ email: payload.data.email }) + .then(user => done(null, user.keyToken + authorization.split(' ')[0] + process.env.LINTO_STACK_OVERWATCH_JWT_SECRET)) + } else if (authorization.split(' ')[0] === 'WebApplication') { + if (SlotsManager.getSn(payload.data.sessionId)) { + done(null, payload.data.salt + authorization.split(' ')[0] + process.env.LINTO_STACK_OVERWATCH_JWT_SECRET) + } else { + done(new UnreservedSlot()) + } + } + } +} + +function generateRefreshSecretFromHeaders(req, payload, done) { + if (!payload || !payload.data) { + done(new MalformedToken()) + } else { + + const { headers: { authorization } } = req + if (authorization.split(' ')[0] === 'Android') { + UsersAndroid.findOne({ email: payload.data.email }) + .then(user => { + done(null, user.keyToken + authorization.split(' ')[0] + process.env.LINTO_STACK_OVERWATCH_REFRESH_SECRET + process.env.LINTO_STACK_OVERWATCH_JWT_SECRET) + }) + } + } +} \ No newline at end of file diff --git a/platform/overwatch/webserver/config/auth/refresh/index.js b/platform/overwatch/webserver/config/auth/refresh/index.js new file mode 100644 index 0000000..4197034 --- /dev/null +++ b/platform/overwatch/webserver/config/auth/refresh/index.js @@ -0,0 +1,24 @@ +const jwtDecode = require('jwt-decode') + +const TokenGenerator = require('../../passport/tokenGenerator') +const MongoAndroidUsers = require(process.cwd() + '/lib/overwatch/mongodb/models/android_users') + +const ANDROID_TOKEN = 'Android' +const randomstring = require('randomstring') + +const { UnableToGenerateKeyToken } = require('../../error/exception/auth') + +module.exports = async function (refreshToken) { + let decodedToken = jwtDecode(refreshToken) + let user = await MongoAndroidUsers.findOne({ email: decodedToken.data.email }) + if (user === undefined) + return undefined + + decodedToken.data.salt = randomstring.generate(12) + MongoAndroidUsers.update({ _id: user._id, keyToken: decodedToken.data.salt }) + .then(user => { + if (!user) return done(new UnableToGenerateKeyToken()) + }) + + return TokenGenerator(decodedToken.data, ANDROID_TOKEN).token +} \ No newline at end of file diff --git a/platform/overwatch/webserver/config/error/exception/auth.js b/platform/overwatch/webserver/config/error/exception/auth.js new file mode 100644 index 0000000..504fb91 --- /dev/null +++ b/platform/overwatch/webserver/config/error/exception/auth.js @@ -0,0 +1,127 @@ +/**************** +***Android******* +****************/ + +class InvalidCredential extends Error { + constructor(message) { + super() + this.name = 'InvalidCredential' + this.type = 'auth_android' + this.status = '401' + if (message) this.message = message + else this.message = 'Wrong user credential' + } +} + +class UnableToGenerateKeyToken extends Error { + constructor(message) { + super() + this.name = 'UnableToGenerateKeyToken' + this.type = 'auth_android' + this.status = '401' + if (message) this.message = message + else this.message = 'Overwatch was not able to generate the keyToken' + } +} + +class UserNotFound extends Error { + constructor(message) { + super() + this.name = 'UserNotFound' + this.type = 'auth_android' + this.status = '401' + if (message) this.message = message + else this.message = 'Unable to find the user' + } +} + + +/**************** +*******Web******* +****************/ + +class NoSecretFound extends Error { + constructor(message) { + super() + this.name = 'NoSecretFound' + this.type = 'auth_web' + this.status = '404' + if (message) this.message = message + else this.message = 'Secret token is missing' + } +} + +class NoSlotAvailable extends Error { + constructor(message) { + super() + this.name = 'NoSlotAvailable' + this.type = 'auth_web' + this.status = '401' + if (message) this.message = message + else this.message = 'No slot available for the requested website' + } +} + +class NoTokenApplicationFound extends Error { + constructor(message) { + super() + this.name = 'NoTokenApplicationFound' + this.type = 'token_not_found' + this.status = '404' + if (message) this.message = message + else this.message = 'Requested token application not found for the origin' + } +} + +class UnreservedSlot extends Error { + constructor(message) { + super() + this.name = 'UnreservedSlot' + this.type = 'auth_web' + this.status = '401' + if (message) this.message = message + else this.message = 'No slot has been reserved for these user' + } +} + +class NoWebAppFound extends Error { + constructor(message) { + super() + this.name = 'NoWebAppFound' + this.type = 'auth_web' + this.status = '401' + if (message) this.message = message + else this.message = 'No registred webapp has been found for the host' + } +} + +/**************** +***Passport****** +****************/ + +class MalformedToken extends Error { + constructor(message) { + super() + this.name = 'MalformedToken' + this.type = 'auth' + this.status = '401' + if (message) this.message = message + else this.message = 'The token is malformed' + } +} + + +module.exports = { + //Android Exception + InvalidCredential, + UnableToGenerateKeyToken, + UserNotFound, + //Web Exception + NoSecretFound, + NoSlotAvailable, + NoTokenApplicationFound, + NoWebAppFound, + UnreservedSlot, + //Passport Exception + MalformedToken, +} \ No newline at end of file diff --git a/platform/overwatch/webserver/config/error/handler.js b/platform/overwatch/webserver/config/error/handler.js new file mode 100644 index 0000000..257e934 --- /dev/null +++ b/platform/overwatch/webserver/config/error/handler.js @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const AuthsException = require('./exception/auth') +let customException = ['UnauthorizedError'] // Default JWT exception + +let initByAuthType = function (webserver) { + Object.keys(AuthsException).forEach(key => customException.push(key)) + process.env.LINTO_STACK_OVERWATCH_AUTH_TYPE.split(',').map(auth => { + if (auth === 'local') { + webserver.app.use(function (err, req, res, next) { + + if (customException.indexOf(err.name) > -1) { + res.status(err.status).send({ message: err.message }) + console.error(err) + return + } + + next() + }) + } + }) +} + +module.exports = { + initByAuthType +} \ No newline at end of file diff --git a/platform/overwatch/webserver/config/index.js b/platform/overwatch/webserver/config/index.js new file mode 100644 index 0000000..337532e --- /dev/null +++ b/platform/overwatch/webserver/config/index.js @@ -0,0 +1,13 @@ +const debug = require('debug')('linto-overwatch:webserver:config') + +module.exports.loadAuth = () => { + if (process.env.LINTO_STACK_OVERWATCH_AUTH_TYPE === '') + return undefined + + return process.env.LINTO_STACK_OVERWATCH_AUTH_TYPE.split(',').map(auth => { + debug(`LOADED STRATEGY ${auth}`) + return { + ...require(`./auth/${auth}`), + } + }) +} \ No newline at end of file diff --git a/platform/overwatch/webserver/config/passport/local.js b/platform/overwatch/webserver/config/passport/local.js new file mode 100644 index 0000000..08bc609 --- /dev/null +++ b/platform/overwatch/webserver/config/passport/local.js @@ -0,0 +1,102 @@ +const debug = require('debug')('linto-overwatch:webserver:config:passport:local') + +const passport = require('passport') +const LocalStrategy = require('passport-local') + +const MongoAndroidUsers = require(process.cwd() + '/lib/overwatch/mongodb/models/android_users') +const MqttUsers = require(process.cwd() + '/lib/overwatch/mongodb/models/mqtt_users') + +const MongoWebappHosts = require(process.cwd() + '/lib/overwatch/mongodb/models/webapp_hosts') +const MongoWorkflowApplication = require(process.cwd() + '/lib/overwatch/mongodb/models/workflows_application') + +const SlotsManager = require(process.cwd() + '/lib/overwatch/slotsManager/slotsManager') +const TokenGenerator = require('./tokenGenerator') + +const { NoSlotAvailable, NoWebAppFound, NoTokenApplicationFound,InvalidCredential, UnableToGenerateKeyToken } = require('../error/exception/auth') + +const randomstring = require('randomstring') + +const ANDROID_TOKEN = 'Android' +const WEB_TOKEN = 'WebApplication' + +const STRATEGY_ANDROID = new LocalStrategy({ + usernameField: 'email', + passwordField: 'password', +}, (email, password, done) => generateUserTokenAndroid(email, password, done)) +passport.use('local-android', STRATEGY_ANDROID) + +function generateUserTokenAndroid(username, password, done) { + MongoAndroidUsers.findOne({ email: username }) + .then(user => { + if (!user || !MongoAndroidUsers.validatePassword(password, user)) return done(new InvalidCredential()) + + let tokenData = { + salt: randomstring.generate(12), + sessionId: process.env.LINTO_STACK_OVERWATCH_DEVICE_TOPIC_KEY + user._id, + email: user.email + } + + MongoAndroidUsers.update({ _id: user._id, keyToken: tokenData.salt }) + .then(user => { + if (!user) return done(new UnableToGenerateKeyToken()) + }) + + MqttUsers.findByUsername({ username: tokenData.sessionId }).then(user => { + if (user.length === 0) mqttuser = MqttUsers.insertMqttUsers({ email: username, username: tokenData.sessionId, password }) + else { mqttuser = user[0] } + + return done(null, { + mqtt: { + mqtt_login: tokenData.sessionId, + mqtt_password: password + }, + token: TokenGenerator(tokenData, ANDROID_TOKEN).token, + }) + }).catch(done) + }).catch(done) +} + +const STRATEGY_WEB = new LocalStrategy({ + usernameField: 'originurl', + passwordField: 'requestToken' +}, (url, requestToken, done) => generateUserTokenWeb(url, requestToken, done)) +passport.use('local-web', STRATEGY_WEB) + + +function generateUserTokenWeb(url, requestToken, done) { + MongoWebappHosts.findOne({ originUrl: url }) + .then((webapp) => { + if (webapp === undefined) + return done(new NoWebAppFound()) + + let app = MongoWebappHosts.validApplicationAuth(webapp, requestToken) + if(!app){ + return done (new NoTokenApplicationFound()) + }else if(app.slots.length > app.maxSlots){ + return done(new NoSlotAvailable()) + } + + MongoWorkflowApplication.getScopesById(app.applicationId).then(topic => { + let tokenData = { + _id: webapp._id, + originUrl: url, + application: app.applicationId, + topic: topic, + sessionId: process.env.LINTO_STACK_OVERWATCH_WEB_TOPIC_KEY + randomstring.generate(12), + salt: randomstring.generate(12) + } + app.password = randomstring.generate(12) + if (SlotsManager.takeSlotIfAvailable(tokenData.sessionId, app, url)) { + return done(null, { + _id: webapp._id, + url: url, + mqtt: { + mqtt_login: tokenData.sessionId, + mqtt_password: app.password + }, + token: TokenGenerator(tokenData, WEB_TOKEN).token + }) + } else return done(new NoSlotAvailable()) + }).catch(done) + }).catch(done) +} \ No newline at end of file diff --git a/platform/overwatch/webserver/config/passport/tokenGenerator/index.js b/platform/overwatch/webserver/config/passport/tokenGenerator/index.js new file mode 100644 index 0000000..624e8fb --- /dev/null +++ b/platform/overwatch/webserver/config/passport/tokenGenerator/index.js @@ -0,0 +1,50 @@ +const jwt = require('jsonwebtoken') + +const TOKEN_DAYS_TIME = 10 +const REFRESH_TOKEN_DAYS_TIME = 20 + +const ANDROID_TOKEN = 'Android' +const WEB_TOKEN = 'WebApplication' + +module.exports = function (tokenData, type) { + let expiration_time_days = 60 + const authSecret = tokenData.salt + type + + if (type === WEB_TOKEN) expiration_time_days = 1 + else delete tokenData.salt + + return { + _id: tokenData._id, + token: generateJWT(tokenData, authSecret, expiration_time_days, type) + } +} + +function generateJWT(data, authSecret, days = 10, type) { + const today = new Date() + const expirationDate = new Date(today) + expirationDate.setDate(today.getDate() + days) + + let auth_token = jwt.sign({ + data, + exp: parseInt(expirationDate.getTime() / 1000, TOKEN_DAYS_TIME), + }, authSecret + process.env.LINTO_STACK_OVERWATCH_JWT_SECRET) + + if (type === ANDROID_TOKEN) { + return { + auth_token: auth_token, + refresh_token: jwt.sign({ + data, + exp: parseInt(expirationDate.getTime() / 1000, REFRESH_TOKEN_DAYS_TIME), + }, authSecret + process.env.LINTO_STACK_OVERWATCH_REFRESH_SECRET + process.env.LINTO_STACK_OVERWATCH_JWT_SECRET), + + expiration_date: parseInt(expirationDate.getTime() / 1000, 10), + session_id: data.sessionId + } + } else { + return { + auth_token: auth_token, + topic: data.topic, + session_id: data.sessionId + } + } +} diff --git a/platform/overwatch/webserver/index.js b/platform/overwatch/webserver/index.js new file mode 100644 index 0000000..7a3d43a --- /dev/null +++ b/platform/overwatch/webserver/index.js @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +'use strict' + +const debug = require('debug')('linto-overwatch:webserver') +const express = require('express') +const bodyParser = require('body-parser') +const EventEmitter = require('eventemitter3') +const passport = require('passport') + +const WebServerErrorHandler = require('./config/error/handler') + +class WebServer extends EventEmitter { + constructor() { + super() + this.app = express() + this.app.use(function (req, res, next) { + res.header("Access-Control-Allow-Origin", "*") + res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") + next() + }) + + this.app.use(bodyParser.urlencoded({ extended: true })) + this.app.use(bodyParser.json()) + + require('./routes')(this) + this.app.use('/', express.static('public')) + + this.app.set('trust proxy', true); + this.app.use(passport.initialize()) + this.app.use(passport.session()) // Optional + + WebServerErrorHandler.initByAuthType(this) + + return this.init() + } + + async init() { + this.app.listen(process.env.LINTO_STACK_OVERWATCH_HTTP_PORT, function () { + debug(`Express launch on ${process.env.LINTO_STACK_OVERWATCH_HTTP_PORT}`) + }) + return this + } +} +module.exports = new WebServer() diff --git a/platform/overwatch/webserver/lib/authWrapper.js b/platform/overwatch/webserver/lib/authWrapper.js new file mode 100644 index 0000000..143a5a5 --- /dev/null +++ b/platform/overwatch/webserver/lib/authWrapper.js @@ -0,0 +1,47 @@ +const debug = require('debug')('linto-overwatch:overwatch:webserver:lib:authWrapper') + +class AuthWrapper { + formatAuthAndroid(user) { + let mqttConfig = { + mqtt_host: process.env.LINTO_STACK_DOMAIN, + mqtt_port: process.env.LINTO_STACK_MQTT_PORT, + mqtt_use_login: false + } + + if (process.env.LINTO_STACK_MQTT_USE_LOGIN === 'true') { + mqttConfig.mqtt_use_login = true + mqttConfig.mqtt_login = user.mqtt.mqtt_login + mqttConfig.mqtt_password = user.mqtt.mqtt_password + } + + return { + user: { ...user.token }, + mqtt: mqttConfig + } + } + + formatAuthWeb(user) { + let mqttConfig = { + host: 'ws://', + mqtt_use_login: false + } + + if (process.env.LINTO_STACK_WSS === 'true') mqttConfig.host = 'wss://' + + mqttConfig.host += process.env.LINTO_STACK_DOMAIN + mqttConfig.host += process.env.LINTO_STACK_MQTT_OVER_WS_ENDPOINT + + if (process.env.LINTO_STACK_MQTT_USE_LOGIN === 'true') { + mqttConfig.mqtt_use_login = true + mqttConfig.mqtt_login = user.mqtt.mqtt_login + mqttConfig.mqtt_password = user.mqtt.mqtt_password + } + + return { + user: { ...user.token }, + mqttConfig + } + } +} + +module.exports = new AuthWrapper() \ No newline at end of file diff --git a/platform/overwatch/webserver/lib/user.js b/platform/overwatch/webserver/lib/user.js new file mode 100644 index 0000000..1ecb7d2 --- /dev/null +++ b/platform/overwatch/webserver/lib/user.js @@ -0,0 +1,24 @@ +const debug = require('debug')('linto-overwatch:overwatch:webserver:lib:workflow') + +const UsersAndroid = require(process.cwd() + '/lib/overwatch/mongodb/models/android_users') + +class WorkflowsApplicationApi { + constructor() { + } + + async logout(user) { + UsersAndroid.findOne({ email: user.email }) + .then(user => { + UsersAndroid.update({ + _id: user._id, + keyToken: '' + }).then(user => { + if (!user) + return done(null, false, { errors: 'Unable to generate keyToken' }) + }) + }).catch('ok') + } +} + + +module.exports = new WorkflowsApplicationApi() \ No newline at end of file diff --git a/platform/overwatch/webserver/lib/workflowApplication.js b/platform/overwatch/webserver/lib/workflowApplication.js new file mode 100644 index 0000000..2dd9f91 --- /dev/null +++ b/platform/overwatch/webserver/lib/workflowApplication.js @@ -0,0 +1,51 @@ +const debug = require('debug')('linto-overwatch:overwatch:webserver:lib:workflow') + +const UsersAndroid = require(process.cwd() + '/lib/overwatch/mongodb/models/android_users') +const UsersWeb = require(process.cwd() + '/lib/overwatch/mongodb/models/webapp_hosts') + +const Workflow = require(process.cwd() + '/lib/overwatch/mongodb/models/workflows_application') + +const LINTO_SKILL_PREFIX = 'linto-skill-' + +class WorkflowsApplicationApi { + constructor() { + } + + async getWorkflowApp(userData) { + if (userData.email) { + let user = await UsersAndroid.findOne({ email: userData.email }) + let application = await Workflow.getScopesByListId(user.applications) + return formatApplication(application) + } else return {} + } +} + +module.exports = new WorkflowsApplicationApi() + +function formatApplication(applications) { + let scopes = [] + let scope + applications.map(app => { + scope = { + name: app.name, + description: app.description, + services: {} + } + + for (let node of app.flow.configs) { + if (node.type === 'linto-config-mqtt') scope.topic = node.scope + } + let skills = [] + for (let node of app.flow.nodes) { + if (node.type === 'linto-transcribe-streaming') scope.services.streaming = true + if (node.type.includes(LINTO_SKILL_PREFIX)) { + let skill = { name: node.type.split(LINTO_SKILL_PREFIX)[1] } + if (node.description) skill.description = node.description + skills.push(skill) + } + } + scope.skills = skills + scopes.push(scope) + }) + return scopes +} \ No newline at end of file diff --git a/platform/overwatch/webserver/routes/auth/index.js b/platform/overwatch/webserver/routes/auth/index.js new file mode 100644 index 0000000..c528952 --- /dev/null +++ b/platform/overwatch/webserver/routes/auth/index.js @@ -0,0 +1,122 @@ + +/* + * Copyright (c) 2018 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict' +const debug = require('debug')('linto-overwatch:webserver:routes:auth') + +const WorkflowApplication = require(process.cwd() + '/webserver/lib/workflowApplication') +const User = require(process.cwd() + '/webserver/lib/user') +const authWrapper = require(process.cwd() + '/webserver/lib/authWrapper') + +const { MalformedToken } = require('../../config/error/exception/auth') + +module.exports = (webServer, auth) => { + return [ + { + name: 'login', + path: '/android/login', + method: 'post', + controller: [ + auth.authenticate_android, + (req, res, next) => { + let output = authWrapper.formatAuthAndroid(req.user) + res.status(202).json(output) + } + ], + }, + { + name: 'logout', + path: '/android/logout', + method: 'get', + controller: [ + (auth.isAuthenticate) ? auth.isAuthenticate : undefined, + (req, res, next) => { + User.logout(req.payload.data) + res.status(200).send('Ok') + } + ] + }, { + name: 'refresh', + path: '/android/refresh', + method: 'get', + controller: [ + (auth.refresh_android) ? auth.refresh_android : undefined, + (req, res, next) => { + if (res.local === undefined) + res.status(401).send(new MalformedToken().message) + else + res.status(202).json(res.local) + } + ], + }, + { + name: 'login', + path: '/web/login', + method: 'post', + controller: [ + (req, res, next) => { + if (req.headers.origin) { + req.body.originurl = extractHostname(req.headers.origin) + next() + } else res.status(400).json('Origin headers is require') + }, + auth.authenticate_web, + (req, res, next) => { + let output = authWrapper.formatAuthWeb(req.user) + res.status(202).json(output) + } + ], + }, + { + name: 'isAuth', + path: '/isAuth', + method: 'get', + controller: [ + (auth.isAuthenticate) ? auth.isAuthenticate : undefined, + (req, res, next) => { + res.status(200).send('Ok') + } + ] + }, + { + name: 'scopes', + path: '/scopes', + method: 'get', + controller: [ + (auth.isAuthenticate) ? auth.isAuthenticate : undefined, + (req, res, next) => { + WorkflowApplication.getWorkflowApp(req.payload.data) + .then(scopes => res.status(200).json(scopes)) + .catch(err => res.status(500).send('Can\'t retrieve scope')) + } + ] + } + ] +} + +function extractHostname(url) { + let hostname + + if (url.indexOf("//") > -1) hostname = url.split('/')[2] + else hostname = url.split('/')[0] + + hostname = hostname.split(':')[0].split('?')[0] + return hostname +} \ No newline at end of file diff --git a/platform/overwatch/webserver/routes/index.js b/platform/overwatch/webserver/routes/index.js new file mode 100644 index 0000000..54491bf --- /dev/null +++ b/platform/overwatch/webserver/routes/index.js @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +'use strict' + +const debug = require('debug')('linto-overwatch:webserver:routes') +const authMiddleware = require('../config/').loadAuth() + +const ifHasElse = (condition, ifHas, otherwise) => { + return !condition ? otherwise() : ifHas() +} + +class Route { + constructor(webServer) { + const routes = require('./routes.js')(webServer, authMiddleware) + + for (let level in routes) { + routes[level].map(route => { + let controller = ifHasElse( + Array.isArray(route.controller), + () => Object.values(route.controller), + () => route.controller + ) + + debug(`CREATE ${route.method} with path : ${level}${route.path}`) + webServer.app[route.method]( + `${level}${route.path}`, + controller + ) + }) + } + } +} + +module.exports = webServer => new Route(webServer) + diff --git a/platform/overwatch/webserver/routes/overwatch/index.js b/platform/overwatch/webserver/routes/overwatch/index.js new file mode 100644 index 0000000..d42d5b8 --- /dev/null +++ b/platform/overwatch/webserver/routes/overwatch/index.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict' +const debug = require('debug')('linto-overwatch:webserver:overwatch') + +module.exports = (webServer) => { + return [ + { + name: 'healthcheck', + path: '/healthcheck', + method: 'get', + controller: async (req, res, next) => { + res.sendStatus(200) + } + }, + { + name: 'auths', + path: '/auths', + method: 'get', + controller: async (req, res, next) => { + let authMethods = [] + process.env.LINTO_STACK_OVERWATCH_AUTH_TYPE.split(',').map(auth => { + authMethods.push({type : auth, basePath : `/${auth}`}) + }) + res.status(200).json(authMethods) + } + } + ] +} \ No newline at end of file diff --git a/platform/overwatch/webserver/routes/routes.js b/platform/overwatch/webserver/routes/routes.js new file mode 100644 index 0000000..c4f7771 --- /dev/null +++ b/platform/overwatch/webserver/routes/routes.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2018 Linagora. + * + * This file is part of Business-Logic-Server + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const debug = require('debug')('linto-overwatch:webserver:routes:routes') + +module.exports = (webServer, authMiddleware) => { + let basePath = process.env.LINTO_STACK_OVERWATCH_BASE_PATH + let routes = {} + + routes[`${basePath}`] = require('./overwatch')(webServer) + + if (authMiddleware !== undefined) { + authMiddleware.map(auth => { + routes[`${basePath}/${auth.authType}`] = require('./auth')(webServer, auth) + }) + } + return routes +} diff --git a/platform/service-broker/Dockerfile b/platform/service-broker/Dockerfile new file mode 100644 index 0000000..6a4083d --- /dev/null +++ b/platform/service-broker/Dockerfile @@ -0,0 +1,3 @@ +FROM redis/redis-stack-server:latest +COPY redis_conf/redis.conf /usr/local/etc/redis/redis.conf +CMD [ "redis-stack-server", "/usr/local/etc/redis/redis.conf" ] diff --git a/platform/service-broker/README.md b/platform/service-broker/README.md new file mode 100644 index 0000000..6c4a67d --- /dev/null +++ b/platform/service-broker/README.md @@ -0,0 +1,59 @@ +# LinTO Platform Services Broker +The service broker is the heart of the LinTO micro-service architecture. + +Based on redis-stack-server, the service broker is the communication pipeline between services and subservices. + +Its purposes are: +* Provide communication channels between services using dedicated message queues to submit tasks and provide results. +* Allows stack-wide service discovery. + +# DBs +By convention within the LinTO-stack, 3 redis dbs are used: +* db=0: Is assigned to celery task. +* db=1: Is assigned to celery result's backend. +* db=2: Is reserved for service registration and discovery. + +# Build +```bash +git clone +cd linto-platform-services-broker +docker build -t lintoai/linto_services_broker:latest . +``` +or +```bash +docker pull registry.linto.ai/lintoai/linto_services_broker:latest +``` + +# Run +As a container: +```bash +docker run \ +-p $MY_BROKER_PORT:6379 \ +--name services_broker \ +linto_services_broker:latest \ +redis-stack-server /usr/local/etc/redis/redis.conf \ +--requirepass $SERVICE_BROKER_PASSWORD +``` + +As a service: +```yml +version: '3.7' + +services: + services-broker: + image: linto_service_broker:stack + deploy: + replicas: 1 + ports: + - 6379:6379 + networks: + - $LINTO_STACK_NETWORK + command: /bin/sh -c "redis-stack-server /usr/local/etc/redis/redis.conf --requirepass $SERVICE_BROKER_PASSWORD" + +networks: + $LINTO_STACK_NETWORK: + external: true +``` + +# Broker configuration file +The broker default configuration file can be overided by mounting a config file on /usr/local/etc/redis/redis.conf. \ No newline at end of file diff --git a/platform/service-broker/RELEASE.md b/platform/service-broker/RELEASE.md new file mode 100644 index 0000000..d558f11 --- /dev/null +++ b/platform/service-broker/RELEASE.md @@ -0,0 +1,4 @@ +# 1.1.0 +- Implemented linto_services_broker image from redis/redis-stack-server:latest +- Added configuration file +- Added README \ No newline at end of file diff --git a/platform/service-broker/docker-compose.yml b/platform/service-broker/docker-compose.yml new file mode 100644 index 0000000..d5f4489 --- /dev/null +++ b/platform/service-broker/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.7' + +services: + linto-platform-services-broker: + image: linto_service_broker:stack + volumes: + - ./redis_conf/redis.conf:/usr/local/etc/redis/redis.conf + ports: + - 6379:6379 + expose: + - "6379" + networks: + - linto-net + command: /bin/sh -c "redis-server-stack --requirepass $LINTO_STACK_BROKER_PASSWORD" + +networks: + linto-net: + external: true \ No newline at end of file diff --git a/platform/service-broker/redis_conf/redis.conf b/platform/service-broker/redis_conf/redis.conf new file mode 100644 index 0000000..b93903b --- /dev/null +++ b/platform/service-broker/redis_conf/redis.conf @@ -0,0 +1,54 @@ +daemonize no +pidfile /var/run/redis.pid +port 6379 +tcp-backlog 511 +timeout 0 +tcp-keepalive 0 +loglevel notice +logfile "" +databases 2 +#save 900 1 +#save 300 10 +#save 60 10000 +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir ./ +slave-serve-stale-data yes +slave-read-only yes +repl-diskless-sync no +repl-diskless-sync-delay 5 +repl-disable-tcp-nodelay no +slave-priority 100 +requirepass password +#maxclients 10000 +#maxmemory +#maxmemory-policy volatile-lru +#maxmemory-samples 3 +appendonly no +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +aof-load-truncated yes +lua-time-limit 5000 +slowlog-log-slower-than 10000 +slowlog-max-len 128 +latency-monitor-threshold 0 +notify-keyspace-events "" +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 +list-max-ziplist-entries 512 +list-max-ziplist-value 64 +set-max-intset-entries 512 +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 +hll-sparse-max-bytes 3000 +activerehashing yes +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 0 0 0 +client-output-buffer-limit pubsub 0 0 0 +hz 10 +aof-rewrite-incremental-fsync yes diff --git a/platform/stt-service-manager/.defaultparam b/platform/stt-service-manager/.defaultparam new file mode 100644 index 0000000..df567a7 --- /dev/null +++ b/platform/stt-service-manager/.defaultparam @@ -0,0 +1,46 @@ +# Global service parameters +SAVE_MODELS_PATH=/opt/model +TEMP_FOLDER_NAME=tmp +LM_FOLDER_NAME=LMs +AM_FOLDER_NAME=AMs +DICT_DELIMITER=| +LANGUAGE="af-ZA, am-ET, ar-AE, ar-BH, ar-DZ, ar-EG, ar-IQ, ar-JO, ar-KW, ar-LB, ar-LY, ar-MA, arn-CL, ar-OM, ar-QA, ar-SA, ar-SY, ar-TN, ar-YE, as-IN, az-Cyrl-AZ, az-Latn-AZ, ba-RU, be-BY, bg-BG, bn-BD, bn-IN, bo-CN, br-FR, bs-Cyrl-BA, bs-Latn-BA, ca-ES, co-FR, cs-CZ, cy-GB, da-DK, de-AT, de-CH, de-DE, de-LI, de-LU, dsb-DE, dv-MV, el-GR, en-029, en-AU, en-BZ, en-CA, en-GB, en-IE, en-IN, en-JM, en-MY, en-NZ, en-PH, en-SG, en-TT, en-US, en-ZA, en-ZW, es-AR, es-BO, es-CL, es-CO, es-CR, es-DO, es-EC, es-ES, es-GT, es-HN, es-MX, es-NI, es-PA, es-PE, es-PR, es-PY, es-SV, es-US, es-UY, es-VE, et-EE, eu-ES, fa-IR, fi-FI, fil-PH, fo-FO, fr-BE, fr-CA, fr-CH, fr-FR, fr-LU, fr-MC, fy-NL, ga-IE, gd-GB, gl-ES, gsw-FR, gu-IN, ha-Latn-NG, he-IL, hi-IN, hr-BA, hr-HR, hsb-DE, hu-HU, hy-AM, id-ID, ig-NG, ii-CN, is-IS, it-CH, it-IT, iu-Cans-CA, iu-Latn-CA, ja-JP, ka-GE, kk-KZ, kl-GL, km-KH, kn-IN, kok-IN, ko-KR, ky-KG, lb-LU, lo-LA, lt-LT, lv-LV, mi-NZ, mk-MK, ml-IN, mn-MN, mn-Mong-CN, moh-CA, mr-IN, ms-BN, ms-MY, mt-MT, nb-NO, ne-NP, nl-BE, nl-NL, nn-NO, nso-ZA, oc-FR, or-IN, pa-IN, pl-PL, prs-AF, ps-AF, pt-BR, pt-PT, qut-GT, quz-BO, quz-EC, quz-PE, rm-CH, ro-RO, ru-RU, rw-RW, sah-RU, sa-IN, se-FIse-NO, se-SE, si-LK, sk-SK, sl-SI, sma-NO, sma-SE, smj-NO, smj-SE, smn-FI, sms-FI, sq-AL, sr-Cyrl-BA, sr-Cyrl-CS, sr-Cyrl-ME, sr-Cyrl-RS, sr-Latn-BA, sr-Latn-CS, sr-Latn-ME, sr-Latn-RS, sv-FI, sv-SE, sw-KE, syr-SY, ta-IN, te-IN, tg-Cyrl-TJ, th-TH, tk-TM, tn-ZA, tr-TR, tt-RU, tzm-Latn-DZ, ug-CN, uk-UA, ur-PK, uz-Cyrl-UZ, uz-Latn-UZ, vi-VN, wo-SN, xh-ZA, yo-NG, zh-CN, zh-HK, zh-MO, zh-SG, zh-TW, zu-ZA" +NGRAM=3 +### CHECK_SERVICE_TIMEOUT (in seconds) +CHECK_SERVICE_TIMEOUT=10 + +# Service Components and PORT +LINTO_STACK_STT_SERVICE_MANAGER_COMPONENTS=WebServer,ServiceManager,LinSTT,ClusterManager,IngressController +LINTO_STACK_STT_SERVICE_MANAGER_HTTP_PORT=80 +LINTO_STACK_STT_SERVICE_MANAGER_SWAGGER_PATH=/opt/swagger.yml + +# Service module +LINTO_STACK_STT_SERVICE_MANAGER_CLUSTER_MANAGER=DockerSwarm +LINTO_STACK_STT_SERVICE_MANAGER_INGRESS_CONTROLLER=nginx +LINTO_STACK_STT_SERVICE_MANAGER_LINSTT_TOOLKIT=kaldi + +# NGINX +LINTO_STACK_STT_SERVICE_MANAGER_NGINX_CONF=/opt/nginx/nginx.conf +LINTO_STACK_STT_SERVICE_MANAGER_NGINX_HOST=nginx-stt-service-manager + +# TRAEFIK +LINTO_STACK_DOMAIN=dev.local + +# Docker socket +LINTO_STACK_STT_SERVICE_MANAGER_DOCKER_SOCKET=/var/run/docker.sock + +# Mongodb settings +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_HOST=mongodb-stt-service-manager +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PORT=27017 +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_DBNAME=linSTTAdmin +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_REQUIRE_LOGIN=true +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_USER=root +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PSWD=root + +# LinSTT settings +LINTO_STACK_LINSTT_OFFLINE_IMAGE=lintoai/linto-platform-stt-standalone-worker +LINTO_STACK_LINSTT_STREAMING_IMAGE=lintoai/linto-platform-stt-standalone-worker-streaming +LINTO_STACK_LINSTT_NETWORK=linto-net +LINTO_STACK_LINSTT_PREFIX=stt +LINTO_STACK_IMAGE_TAG=latest +LINTO_STACK_LINSTT_NAME=stt \ No newline at end of file diff --git a/platform/stt-service-manager/.envdefault b/platform/stt-service-manager/.envdefault new file mode 100644 index 0000000..8ecec17 --- /dev/null +++ b/platform/stt-service-manager/.envdefault @@ -0,0 +1,28 @@ +# Service manager settings +LINTO_STACK_DOMAIN=dev.linto.local +LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY=/path/to/save/models +LINTO_STACK_STT_SERVICE_MANAGER_CLUSTER_MANAGER=DockerSwarm +LINTO_STACK_STT_SERVICE_MANAGER_INGRESS_CONTROLLER=nginx +LINTO_STACK_STT_SERVICE_MANAGER_LINSTT_TOOLKIT=kaldi + +# Ingress controler settings +LINTO_STACK_STT_SERVICE_MANAGER_NGINX_HOST=nginx-stt-service-manager + +# Mongodb settings +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_HOST=mongodb-stt-service-manager +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PORT=27017 +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_DBNAME=linSTTAdmin +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_REQUIRE_LOGIN=true +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_USER=root +LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PSWD=root + +# LinSTT settings +LINTO_STACK_LINSTT_NETWORK=linto-net +LINTO_STACK_LINSTT_PREFIX=stt +LINTO_STACK_LINSTT_NAME=stt + +LINTO_STACK_SPEAKER_DIARIZATION_HOST= +LINTO_STACK_SPEAKER_DIARIZATION_PORT= +LINTO_STACK_PUCTUATION_HOST= +LINTO_STACK_PUCTUATION_PORT= +LINTO_STACK_PUCTUATION_ROUTE= \ No newline at end of file diff --git a/platform/stt-service-manager/.github/workflows/dockerhub-description.yml b/platform/stt-service-manager/.github/workflows/dockerhub-description.yml new file mode 100644 index 0000000..c0c3bc8 --- /dev/null +++ b/platform/stt-service-manager/.github/workflows/dockerhub-description.yml @@ -0,0 +1,20 @@ +name: Update Docker Hub Description +on: + push: + branches: + - master + paths: + - README.md + - .github/workflows/dockerhub-description.yml +jobs: + dockerHubDescription: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + repository: lintoai/linto-platform-service-manager + readme-filepath: ./README.md diff --git a/platform/stt-service-manager/Dockerfile b/platform/stt-service-manager/Dockerfile new file mode 100644 index 0000000..b7ebcc9 --- /dev/null +++ b/platform/stt-service-manager/Dockerfile @@ -0,0 +1,74 @@ +FROM node:12 +LABEL maintainer="irebai@linagora.com" + +RUN apt-get update &&\ + apt-get install -y \ + python-dev \ + python-pip \ + automake wget sox unzip swig build-essential libtool zlib1g-dev locales libatlas-base-dev nano ca-certificates gfortran subversion &&\ + apt-get clean + + +## Build kaldi and Clean installation (intel, openfst, src/*) +RUN git clone --depth 1 https://github.com/kaldi-asr/kaldi.git /opt/kaldi && \ + cd /opt/kaldi && \ + cd /opt/kaldi/tools && \ + ./extras/install_mkl.sh && \ + make -j $(nproc) && \ + cd /opt/kaldi/src && \ + ./configure --shared && \ + make depend -j $(nproc) && \ + make -j $(nproc) && \ + mkdir -p /opt/kaldi/src_/lib && \ + mv /opt/kaldi/src/base/libkaldi-base.so \ + /opt/kaldi/src/chain/libkaldi-chain.so \ + /opt/kaldi/src/decoder/libkaldi-decoder.so \ + /opt/kaldi/src/feat/libkaldi-feat.so \ + /opt/kaldi/src/fstext/libkaldi-fstext.so \ + /opt/kaldi/src/gmm/libkaldi-gmm.so \ + /opt/kaldi/src/hmm/libkaldi-hmm.so \ + /opt/kaldi/src/lat/libkaldi-lat.so \ + /opt/kaldi/src/lm/libkaldi-lm.so \ + /opt/kaldi/src/matrix/libkaldi-matrix.so \ + /opt/kaldi/src/transform/libkaldi-transform.so \ + /opt/kaldi/src/tree/libkaldi-tree.so \ + /opt/kaldi/src/util/libkaldi-util.so \ + /opt/kaldi/src_/lib && \ + mv /opt/kaldi/src/lmbin /opt/kaldi/src/fstbin /opt/kaldi/src/bin /opt/kaldi/src_ && \ + rm -rf /opt/kaldi/src && mv /opt/kaldi/src_ /opt/kaldi/src && \ + cd /opt/kaldi/src && rm -f lmbin/*.cc lmbin/*.o lmbin/Makefile fstbin/*.cc fstbin/*.o fstbin/Makefile bin/*.cc bin/*.o bin/Makefile && \ + cd /opt/intel/mkl/lib && rm -f intel64/*.a intel64_lin/*.a && \ + cd /opt/kaldi/tools && mkdir openfsttmp && mv openfst-*/lib openfst-*/include openfst-*/bin openfsttmp && rm openfsttmp/lib/*.a openfsttmp/lib/*.la && \ + rm -r openfst-*/* && mv openfsttmp/* openfst-*/ && rm -r openfsttmp + + +## Install NLP packages +RUN cd /opt/kaldi/tools && \ + extras/install_phonetisaurus.sh && \ + extras/install_irstlm.sh && \ + pip install numpy && \ + pip install git+https://github.com/sequitur-g2p/sequitur-g2p && git clone https://github.com/sequitur-g2p/sequitur-g2p + +## Install npm modules +WORKDIR /usr/src/app +COPY ./package.json ./ +RUN npm install + +## Prepare work directories +COPY ./components ./components +COPY ./lib ./lib +COPY ./models /usr/src/app/models +COPY ./app.js ./config.js ./.defaultparam ./docker-healthcheck.js ./docker-entrypoint.sh ./wait-for-it.sh ./ +RUN mkdir /opt/model /opt/nginx && cp -r /opt/kaldi/egs/wsj/s5/utils ./components/LinSTT/Kaldi/scripts/ + +ENV LD_LIBRARY_PATH $LD_LIBRARY_PATH:/opt/kaldi/tools/openfst/lib +ENV PATH /opt/kaldi/egs/wsj/s5/utils:/opt/kaldi/tools/openfst/bin:/opt/kaldi/src/fstbin:/opt/kaldi/src/lmbin:/opt/kaldi/src/bin:/opt/kaldi/tools/phonetisaurus-g2p/src/scripts:/opt/kaldi/tools/phonetisaurus-g2p:/opt/kaldi/tools/sequitur-g2p/g2p.py:/opt/kaldi/tools/irstlm/bin:$PATH + +EXPOSE 80 + +HEALTHCHECK CMD node docker-healthcheck.js || exit 1 + +# Entrypoint handles the passed arguments +ENTRYPOINT ["./docker-entrypoint.sh"] + +#CMD [ "npm", "start" ] diff --git a/platform/stt-service-manager/README.md b/platform/stt-service-manager/README.md new file mode 100644 index 0000000..420dbc9 --- /dev/null +++ b/platform/stt-service-manager/README.md @@ -0,0 +1,97 @@ +# Linto-Platform-STT-Service-Manager + +This service is mandatory in a LinTO platform stack as the main process for speech to text toolkit. +It is used with [stt-standalone-worker](https://github.com/linto-ai/linto-platform-stt-standalone-worker) to run an API with docker swarm to manage STT services. + +## Usage +See documentation : [doc.linto.ai](https://doc.linto.ai/#/services/stt_manager) + +# Deploy + +With our proposed stack [linto-platform-stack](https://github.com/linto-ai/linto-platform-stack) + +# Develop + +## Prerequisites +To use the STT-manager service, you'll have to make sure that dependent services are installed and launched: + +- mongodb: `docker pull mongo` +- nginx: `docker pull nginx` +- traefik: `docker pull traefik` + +## Download and Install + +To install STT Service Manager you will need to download the source code : + +```bash +git clone https://github.com/linto-ai/linto-platform-stt-service-manager.git +cd linto-platform-stt-service-manager +``` + +You will need to have Docker and Docker Compose installed on your machine. Then, to build the docker image, execute: + +```bash +docker build -t lintoai/linto-platform-stt-standalone-worker . +``` + +Or using docker-compose: +```bash +docker-compose build +``` + +Otherwise, you can download the pre-built image from docker-hub: + +```bash +docker pull lintoai/linto-platform-stt-standalone-worker:latest +``` + +NOTE: To install the service without docker, please follow the instructions defined in the `Dockerfile` (Build kaldi, Install NLP packages, Install npm modules). + +## Configuration +Once all the services are build, you need to manage your environment variables. A default file `.envdefault` is provided to allow a default setup. Please adapt it to your configurations and needs. + +```bash +cp .envdefault .env +nano .env +``` + +| Env variable| Description | example | +|:---|:---|:---| +|LINTO_STACK_DOMAIN|Deployed domain. It is required when traefik controller is used|dev.linto.local| +|LINTO_STACK_STT_SERVICE_MANAGER_HTTP_PORT|STT-manager service port|80| +|LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY|Folder path where to save the created models|~/linto_shared_memory/| +|LINTO_STACK_STT_SERVICE_MANAGER_CLUSTER_MANAGER|A container orchestration tool (accepted values: DockerSwarm)|DockerSwarm| +|LINTO_STACK_STT_SERVICE_MANAGER_INGRESS_CONTROLLER|Controller ingress used (accepted values: nginx\|traefik)|nginx| +|LINTO_STACK_STT_SERVICE_MANAGER_LINSTT_TOOLKIT|ASR engine used (accepted values: kaldi)|kaldi| +|LINTO_STACK_STT_SERVICE_MANAGER_NGINX_HOST|STT-manager nginx host|localhost| +|LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_HOST|STT-manager mongodb host|localhost| +|LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PORT|MongoDb service port|27017| +|LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_DBNAME|MongoDb service database name|linSTTAdmin| +|LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_REQUIRE_LOGIN|Enable/Disable MongoDb service authentication|true| +|LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_USER|MongoDb service username|root| +|LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PSWD|MongoDb service password user|root| +|LINTO_STACK_LINSTT_OFFLINE_IMAGE|LinSTT docker image to use for offline decoding mode|lintoai/linto-platform-stt-standalone-worker| +|LINTO_STACK_LINSTT_STREAMING_IMAGE|LinSTT docker image to use for online decoding mode|lintoai/linto-platform-stt-standalone-worker-streaming| +|LINTO_STACK_LINSTT_NETWORK|LinSTT docker network to connect|linto-net| +|LINTO_STACK_LINSTT_PREFIX|LinSTT service prefix to use with controller ingress|stt| +|LINTO_STACK_IMAGE_TAG|Docker image tag to use|latest| +|LINTO_STACK_LINSTT_NAME|Docker stack name|stt| + +If you run STT-manager without docker, you need to change the following environment variables: + +| Env variable| Description | example | +|:---|:---|:---| +|SAVE_MODELS_PATH|Saved model path. Set it to the same path as LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY|~/linto_shared_memory/| +|LINTO_STACK_STT_SERVICE_MANAGER_SWAGGER_PATH|STT-manager swagger file path|~/linto-platform-stt-service-manager/config/swagger.yml| +|LINTO_STACK_STT_SERVICE_MANAGER_NGINX_CONF|STT-manager nginx config file path|~/linto-platform-stt-service-manager/config/nginx.conf| + +NOTE: if you want to use the user interface, you need also to configure the swagger file `~/linto-platform-stt-service-manager/config/swagger.yml`. Specifically, in the section `host`, specify the host and the address of the machine in which the service is deployed. + +## Execute +In order to run the service alone, you have first to run the ingress controller service (`LINTO_STACK_STT_SERVICE_MANAGER_INGRESS_CONTROLLER`). Then, you only need to execute: + +```bash +cd linto-platform-stt-service-manager +docker-compose up +``` +Then you can acces it on [localhost:8000](localhost:8000). You can use the user interface on [localhost:8000/api-doc/](localhost:8000/api-doc/) \ No newline at end of file diff --git a/platform/stt-service-manager/RELEASE.md b/platform/stt-service-manager/RELEASE.md new file mode 100644 index 0000000..47c6cc8 --- /dev/null +++ b/platform/stt-service-manager/RELEASE.md @@ -0,0 +1,26 @@ +# 1.2.0 +- New feature: allow or deny external access to a running service + +# 1.1.6 +- Update the parameters and fix download from link issue + +# 1.1.5 +- Change LinSTT service reload and Traefik Label parameters + +# 1.1.4 +- Fix minor bugs and update the environment variables for better usability + +# 1.1.3 +- Update Swagger file and nginx Component scripts to the latest version of LinSTT + +# 1.1.2 +- Update Dockerfile and fix minor bugs + +# 1.1.1 +- Update Traefik component: add extras labels such as ssl and basicAuth + +# 1.1.0 +- New Feature: Add Traefik component as an ingress controller + +# 1.0.0 +- First build of LinTO-Platform-stt-service-manager \ No newline at end of file diff --git a/platform/stt-service-manager/app.js b/platform/stt-service-manager/app.js new file mode 100644 index 0000000..c798166 --- /dev/null +++ b/platform/stt-service-manager/app.js @@ -0,0 +1,51 @@ +const debug = require('debug')('app:main') +const ora = require('ora') + +class App { + constructor() { + try { + // Load env variables + require('./config') + // Check mongo driver + //const MongoDriver = require(`${process.cwd()}/models/driver.js`) + //if( ! (MongoDriver.constructor.db && MongoDriver.constructor.db.serverConfig.isConnected()) ) throw "Failed to connect to MongoDB server" + // Auto-loads components based on process.env.COMPONENTS list + this.components = {} + this.db = {} + process.env.COMPONENTS.split(',').reduce((prev, componentFolderName) => { + return prev.then(async () => { await this.use(componentFolderName) }) + }, Promise.resolve()).then(() => { + // Do some stuff after all components being loaded + if (this.components['ClusterManager'] !== undefined) { + this.components['ClusterManager'].emit('verifServices') + this.components['ClusterManager'].emit('cleanServices') + } + }) + } catch (e) { + console.error(debug.namespace, e) + } + } + + + async use(componentFolderName) { + let spinner = ora(`Registering component : ${componentFolderName} \n`).start() + try { + // Component dependency injections with inversion of control based on events emitted between components + // Component is an async singleton - requiring it returns a reference to an instance + //console.log(this) + const component = await require(`${__dirname}/components/${componentFolderName}`)(this) + this.components[component.id] = component // We register the instancied component reference in app.components object + //console.log(component) + spinner.succeed(`Registered component : ${component.id}`) + } catch (e) { + if (e.name == "COMPONENT_MISSING") { + return spinner.warn(`Skipping ${componentFolderName} - this component depends on : ${e.missingComponents}`) + } + spinner.fail(`Error in component loading : ${componentFolderName}`) + console.error(debug.namespace, e) + process.exit(1) + } + } +} + +module.exports = new App() \ No newline at end of file diff --git a/platform/stt-service-manager/components/ClusterManager/DockerSwarm/index.js b/platform/stt-service-manager/components/ClusterManager/DockerSwarm/index.js new file mode 100644 index 0000000..b9dd777 --- /dev/null +++ b/platform/stt-service-manager/components/ClusterManager/DockerSwarm/index.js @@ -0,0 +1,205 @@ +const debug = require('debug')(`app:clustermanager:dockerswarm`) +const Docker = require('dockerode'); +const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET_PATH }); +const sleep = require('util').promisify(setTimeout) + +class DockerSwarm { + constructor() { + this.checkSwarm() + } + + serviceOption(params) { + return { + "Name": params.serviceId, + "Labels": { + "com.docker.stack.image" : `${params.image}:${process.env.LINSTT_IMAGE_TAG}`, + "com.docker.stack.namespace" : `${process.env.LINSTT_STACK_NAME}` + }, + "TaskTemplate": { + "ContainerSpec": { + "Image": `${params.image}:${process.env.LINSTT_IMAGE_TAG}`, + "Env": [ + `PUCTUATION_HOST=${process.env.PUCTUATION_HOST}`, + `PUCTUATION_PORT=${process.env.PUCTUATION_PORT}`, + `PUCTUATION_ROUTE=${process.env.PUCTUATION_ROUTE}`, + `SPEAKER_DIARIZATION_HOST=${process.env.SPEAKER_DIARIZATION_HOST}`, + `SPEAKER_DIARIZATION_PORT=${process.env.SPEAKER_DIARIZATION_PORT}` + ], + "Mounts": [ + { + "ReadOnly": false, + "Source": `${process.env.FILESYSTEM}/${process.env.LM_FOLDER_NAME}/${params.LModelId}`, + "Target": "/opt/models/LM", + "Type": "bind" + }, + { + "ReadOnly": true, + "Source": `${process.env.FILESYSTEM}/${process.env.AM_FOLDER_NAME}/${params.AModelId}`, + "Target": "/opt/models/AM", + "Type": "bind" + } + ], + "DNSConfig": {} + }, + "Networks": [ + { + "Target": process.env.LINSTT_NETWORK + } + ] + }, + "Mode": { + "Replicated": { + "Replicas": params.replicas + } + }, + "EndpointSpec": { + "mode": "dnsrr" + } + } + } + + checkSwarm() { + docker.swarmInspect(function (err) { + if (err) throw err + }) + } + + async checkServiceOn(params) { //check if service is correctly started + try { + const time = 0.5 //in seconds + let retries = process.env.CHECK_SERVICE_TIMEOUT / time + let status = {} + + while (retries > 0) { + await sleep(time * 1000) + const service = await docker.listContainers({ + "filters": { "label": [`com.docker.swarm.service.name=${params.serviceId}`] } + }) + if (service.length == 0) + retries = retries - 1 + debug(service.length, params.replicas) + if (service.length == params.replicas) { + status = 1 + break + } else if (retries === 0) { + status = 0 + const serviceLog = await docker.getService(params.serviceId) + var logOpts = { + stdout: 1, + stderr: 1, + tail:100, + follow:0 + }; + serviceLog.logs(logOpts, (logs, err)=>{ + console.log(err) + }) + break + } + } + return status + } catch (err) { + debug(err) + return 0 + } + } + + async checkServiceOff(serviceId) { //check if service is correctly stopped + try { + const time = 0.5 //in seconds + while (true) { + await sleep(time * 1000) + const service = await docker.listContainers({ + "filters": { "label": [`com.docker.swarm.service.name=${serviceId}`] } + }) + debug(service.length) + if (service.length === 0) break + } + return 1 + } catch (err) { + debug(err) + return -1 + } + } + + async listDockerServices() { + return new Promise((resolve, reject) => { + try { + docker.listServices(function (err, services) { + if (err) reject(err) + resolve(services) + }) + } catch (err) { + reject(err) + } + }) + } + + startService(params) { + return new Promise((resolve, reject) => { + try { + switch (params.tag) { + case 'offline': params["image"] = process.env.LINSTT_OFFLINE_IMAGE; break + case 'online': params["image"] = process.env.LINSTT_STREAMING_IMAGE; break + default: throw 'Undefined service tag' + } + const options = this.serviceOption(params) + docker.createService(options, function (err) { + if (err) reject(err) + resolve() + }) + } catch (err) { + reject(err) + } + }) + } + + async updateService(serviceId,replicas=null) { + return new Promise(async (resolve, reject) => { + try { + const service = await docker.getService(serviceId) + const spec = await service.inspect() + const newSpec = spec.Spec + newSpec.version = parseInt(spec.Version.Index) // version number of the service object being updated. This is required to avoid conflicting writes + newSpec.TaskTemplate.ForceUpdate = parseInt(spec.Spec.TaskTemplate.ForceUpdate) + 1 // counter that forces an update even if no relevant parameters have been changed + if (replicas != null) + newSpec.Mode.Replicated.Replicas = replicas + await service.update(newSpec) + resolve() + } catch (err) { + debug(err) + reject(err) + } + }) + } + + async stopService(serviceId) { + return new Promise(async (resolve, reject) => { + try { + const service = await docker.getService(serviceId) + await service.remove() + resolve() + } catch (err) { + reject(err) + } + }) + } + + getServiceInfo(serviceId) { + return docker.getService(serviceId) + } + + async serviceIsOn(serviceId) { + try { + const info = await docker.listContainers({ + "filters": { "label": [`com.docker.swarm.service.name=${serviceId}`] } + }) + return info.length + } catch (err) { + debug(err) + return 0 + } + } + +} + +module.exports = new DockerSwarm() \ No newline at end of file diff --git a/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/LinSTT.js b/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/LinSTT.js new file mode 100644 index 0000000..8815ee8 --- /dev/null +++ b/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/LinSTT.js @@ -0,0 +1,18 @@ +const debug = require('debug')(`app:dockerswarm:eventsFrom:LinSTT`) + +// this is bound to the component +module.exports = function () { + if (!this.app.components['LinSTT']) return + + this.app.components['LinSTT'].on('serviceReload', async (modelId) => { + try { + const services = await this.db.service.findServices({ isOn: 1, LModelId: modelId }) + services.forEach(async (service) => { + debug(`Reload running service ${service.serviceId} using the model ${modelId}`) + await this.cluster.updateService(service.serviceId) + }) + } catch (err) { + console.error(err) + } + }) +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/WebServer.js b/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/WebServer.js new file mode 100644 index 0000000..f89c568 --- /dev/null +++ b/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/WebServer.js @@ -0,0 +1,80 @@ +const debug = require('debug')(`app:dockerswarm:eventsFrom:WebServer`) + +// this is bound to the component +module.exports = function () { + if (!this.app.components['WebServer']) return + + this.app.components['WebServer'].on('startService', async (cb, payload) => { + /** + * Create a docker service by service Object + * @param {Object} payload: {serviceId, externalAccess} + * @returns {Object} + */ + try { + const service = await this.db.service.findService(payload.serviceId) + if (!service) throw `Service '${payload.serviceId}' does not exist` + if (service.isOn) throw `Service '${payload.serviceId}' is already started` + const lmodel = await this.db.lm.findModel(service.LModelId) + if (!lmodel) throw `Language Model used by this service has been removed` + if (!lmodel.isGenerated) throw `Service '${payload.serviceId}' could not be started (Language Model '${service.LModelId}' has not been generated yet)` + + await this.cluster.startService(service) + //const check = await this.cluster.checkServiceOn(service) + const check = true + if (check) { + if (service.externalAccess) + this.emit("serviceStarted", { service: payload.serviceId }) + await this.db.service.updateService(payload.serviceId, { isOn: 1 }) + } + else { + await this.cluster.stopService(payload.serviceId) + throw `Something went wrong. Service '${payload.serviceId}' is not started` + } + return cb({ bool: true, msg: `Service '${payload.serviceId}' is successfully started` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('stopService', async (cb, serviceId) => { + /** + * delete a docker service by service Object + * @param serviceId + * @returns {Object} + */ + try { + const service = await this.db.service.findService(serviceId) + if (service === -1) throw `Service '${serviceId}' does not exist` + if (!service.isOn) throw `Service '${serviceId}' is not running` + await this.cluster.stopService(serviceId) + //await this.cluster.checkServiceOff(serviceId) + await this.db.service.updateService(serviceId, { isOn: 0 }) + if (service.externalAccess) + this.emit("serviceStopped", serviceId) + return cb({ bool: true, msg: `Service '${serviceId}' is successfully stopped` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('scaleService', async (cb, payload) => { + /** + * Update a docker service by service Object + * @param {Object}: {serviceId, replicas} + * @returns {Object} + */ + try { + payload.replicas = parseInt(payload.replicas) + const service = await this.db.service.findService(payload.serviceId) + if (!service) throw `Service '${payload.serviceId}' does not exist` + if (payload.replicas < 1) throw 'The scale must be greater or equal to 1' + await this.cluster.updateService(payload.serviceId, payload.replicas) + //await this.cluster.checkServiceOn(payload) + await this.db.service.updateService(payload.serviceId, { replicas: payload.replicas }) + this.emit("serviceScaled") + return cb({ bool: true, msg: `Service '${payload.serviceId}' is successfully scaled` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) +} diff --git a/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/myself.js b/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/myself.js new file mode 100644 index 0000000..f01f417 --- /dev/null +++ b/platform/stt-service-manager/components/ClusterManager/controllers/eventsFrom/myself.js @@ -0,0 +1,68 @@ +const debug = require('debug')(`app:dockerswarm:eventsFrom:clusterManager:myself`) +const fs = require('fs') +const rimraf = require("rimraf"); + +// this is bound to the component +module.exports = function () { + this.on('verifServices', async () => { + try { + debug('Start service verification') + const services = await this.db.service.findServices() + if (services !== -1) { + services.forEach(async service => { + if (service.isOn) { //check if the service is running + const replicas = await this.cluster.serviceIsOn(service.serviceId) + if (replicas !== service.replicas) { + await this.cluster.stopService(service.serviceId).catch(err => { }) + await this.cluster.startService(service) + //const check = await this.cluster.checkServiceOn(service) + const check = true + if (check && service.externalAccess) { + this.emit("serviceStarted", { service: service.serviceId }) + } + debug(`**** Service ${service.serviceId} is restarted`) + } else { + if (service.externalAccess) + this.emit("serviceStarted", { service: service.serviceId }) + } + } else { // + const replicas = await this.cluster.serviceIsOn(service.serviceId) + if (replicas > 0) { + debug(`**** Service ${service.serviceId} is stopped`) + await this.cluster.stopService(service.serviceId) + } + } + }) + } + } catch (err) { + console.error(err) + } + }) + this.on('cleanServices', async () => { + try { + debug('Start service cleaning') + const models = await this.db.lm.findModels() + if (models !== -1) { + models.forEach(async model => { + if (model.updateState > 0) { //check if service crashed during model generation + let update = {} + update['updateState'] = 0 + update['updateStatus'] = '' + await this.db.lm.updateModel(model.modelId, update) + debug(`**** Language model ${model.modelId} is updated due to a service's crash`) + } + }) + } + //remove crashed files if exists + fs.readdir(process.env.TEMP_FILE_PATH, (err, files) => { + files.forEach(file => { + const path = `${process.env.TEMP_FILE_PATH}/${file}` + debug(`**** remove tmp file ${path} after a service's crash`); + rimraf(path, async (err) => { if (err) throw err }) + }); + }); + } catch (err) { + console.error(err) + } + }) +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/ClusterManager/index.js b/platform/stt-service-manager/components/ClusterManager/index.js new file mode 100644 index 0000000..08da809 --- /dev/null +++ b/platform/stt-service-manager/components/ClusterManager/index.js @@ -0,0 +1,23 @@ +const Component = require(`../component.js`) +const debug = require('debug')(`app:clustermanager`) +const service = require(`${process.cwd()}/models/models/ServiceUpdates`) +const lm = require(`${process.cwd()}/models/models/LMUpdates`) +const am = require(`${process.cwd()}/models/models/AMUpdates`) + +class ClusterManager extends Component { + constructor(app) { + super(app) + this.id = this.constructor.name + this.app = app + this.db = { service: service, lm: lm, am: am } + switch (process.env.CLUSTER_TYPE) { + case 'DockerSwarm': this.cluster = require(`./DockerSwarm`); break + case 'Kubernetes': this.cluster = ''; break + default: throw 'Undefined CLUSTER type' + } + return this.init() + } + +} + +module.exports = app => new ClusterManager(app) diff --git a/platform/stt-service-manager/components/IngressController/Nginx/index.js b/platform/stt-service-manager/components/IngressController/Nginx/index.js new file mode 100644 index 0000000..1a0ecc3 --- /dev/null +++ b/platform/stt-service-manager/components/IngressController/Nginx/index.js @@ -0,0 +1,175 @@ +const debug = require('debug')(`app:ingresscontroller:nginx`) +const fs = require('fs') +const NginxConfFile = require('nginx-conf').NginxConfFile; +const Docker = require('dockerode'); +const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET_PATH }); +const sleep = require('util').promisify(setTimeout) + +class Nginx { + constructor() { + try { + fs.copyFileSync(`${process.cwd()}/components/IngressController/Nginx/nginx.conf`, process.env.NGINX_CONF_PATH) + this.createConf().then(res => { this.conf = res }) + } catch (err) { + throw err + } + } + + async createConf() { + return new Promise((resolve, reject) => { + try { + NginxConfFile.create(process.env.NGINX_CONF_PATH, function (err, conf) { + if (err) reject(err) + resolve(conf) + }) + } catch (err) { + reject(err) + } + }) + } + + addUpStream(config) { + let idx = 0 + // Add upstream + try { + if (this.conf.nginx.upstream === undefined) { + this.conf.nginx._add('upstream', config.service); + this.conf.nginx.upstream._add('server', `${config.service}:80`); + this.conf.nginx.upstream._add('least_conn', ''); + } else { + this.conf.nginx._add('upstream', config.service); + idx = this.conf.nginx.upstream.length - 1 + this.conf.nginx.upstream[idx]._add('server', `${config.service}:80`); + this.conf.nginx.upstream[idx]._add('least_conn', ''); + } + + // Add location + const prefix = `/${process.env.LINSTT_PREFIX}/${config.service}` + + if (this.conf.nginx.server.location === undefined) { + this.conf.nginx.server._add('location', `${prefix}/`); + this.conf.nginx.server.location._add('rewrite', `${prefix}/(.*) /$1 break`); + this.conf.nginx.server.location._add('client_max_body_size', `200M`); + this.conf.nginx.server.location._add('keepalive_timeout', `600s`); + this.conf.nginx.server.location._add('proxy_connect_timeout', `600s`); + this.conf.nginx.server.location._add('proxy_send_timeout', `600s`); + this.conf.nginx.server.location._add('proxy_read_timeout', `600s`); + this.conf.nginx.server.location._add('send_timeout', `600s`); + this.conf.nginx.server.location._add('proxy_pass', `http://${config.service}`); + } else { + this.conf.nginx.server._add('location', `${prefix}/`); + idx = this.conf.nginx.server.location.length - 1 + this.conf.nginx.server.location[idx]._add('rewrite', `${prefix}/(.*) /$1 break`); + this.conf.nginx.server.location[idx]._add('client_max_body_size', `200M`); + this.conf.nginx.server.location[idx]._add('keepalive_timeout', `600s`); + this.conf.nginx.server.location[idx]._add('proxy_connect_timeout', `600s`); + this.conf.nginx.server.location[idx]._add('proxy_send_timeout', `600s`); + this.conf.nginx.server.location[idx]._add('proxy_read_timeout', `600s`); + this.conf.nginx.server.location[idx]._add('send_timeout', `600s`); + this.conf.nginx.server.location[idx]._add('proxy_pass', `http://${config.service}`); + } + } catch (err) { + console.log(err) + } + } + + removeUpStream(serviceId) { + try { + //Remove upstream + let findService = false + if (this.conf.nginx.upstream != undefined) { + if (Array.isArray(this.conf.nginx.upstream)) { + this.conf.nginx.upstream.forEach((upstream, idx) => { + if (upstream._getString().indexOf(serviceId) != -1) { + this.conf.nginx._remove('upstream', idx) + findService = true + } + }); + } else { + if (this.conf.nginx.upstream._getString().indexOf(serviceId) != -1) { + this.conf.nginx._remove('upstream') + findService = true + } + } + } + + //Remove location + if (this.conf.nginx.server.location != undefined) { + if (Array.isArray(this.conf.nginx.server.location)) { + this.conf.nginx.server.location.forEach((location, idx) => { + if (location._getString().indexOf(serviceId) != -1) { + this.conf.nginx.server._remove('location', idx) + } + }); + } else { + if (this.conf.nginx.server.location._getString().indexOf(serviceId) != -1) + this.conf.nginx.server._remove('location') + } + } + + return findService + } catch (err) { + console.error(err) + } + } + + async reloadNginx() { + return new Promise(async (resolve, reject) => { + try { + const service = await docker.getService(process.env.NGINX_SERVICE_ID) + while (true) { + try { + const spec = await service.inspect() + const newSpec = spec.Spec + newSpec.version = parseInt(spec.Version.Index) // version number of the service object being updated. This is required to avoid conflicting writes + newSpec.TaskTemplate.ForceUpdate = parseInt(spec.Spec.TaskTemplate.ForceUpdate) + 1 // counter that forces an update even if no relevant parameters have been changed + const time = 0.5 + await sleep(time * 1000) + await service.update(newSpec) + break + } catch (err) { + debug("Service nginx update stopped due to another update process! Retry...") + } + } + debug("Reload done") + resolve() + } catch (err) { + debug(err) + reject(err) + } + }) + } + + async reloadNginx_deprecated() { + return new Promise(async (resolve, reject) => { + try { + const nginx = await docker.listContainers({ + "filters": { + "name": [`/*${process.env.NGINX_SERVICE_ID}*`] + //"label":[`com.docker.swarm.service.name=${process.env.NGINX_SERVICE_ID}`] + } + }) + if (nginx.length == 0) throw ('Service Nginx is not running!!!') + const nginx_id = nginx[0].Names[0].replace('/', '') + const container = await docker.getContainer(nginx_id) + container.exec({ + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "Cmd": ["bash", "-c", "nginx -s reload 2> /etc/nginx/conf.d/.status"] + }, function (err, exec) { + if (err) reject(err) + exec.start(function (err, stream) { + if (err) reject(err) + }) + }) + resolve() + } catch (err) { + reject(err) + } + }) + } +} + + +module.exports = new Nginx() diff --git a/config/servicemanager/nginx.conf b/platform/stt-service-manager/components/IngressController/Nginx/nginx.conf similarity index 100% rename from config/servicemanager/nginx.conf rename to platform/stt-service-manager/components/IngressController/Nginx/nginx.conf diff --git a/platform/stt-service-manager/components/IngressController/Traefik/index.js b/platform/stt-service-manager/components/IngressController/Traefik/index.js new file mode 100644 index 0000000..05dbc7d --- /dev/null +++ b/platform/stt-service-manager/components/IngressController/Traefik/index.js @@ -0,0 +1,69 @@ +const debug = require('debug')(`app:ingresscontroller:traefik`) +const Docker = require('dockerode'); +const docker = new Docker({ socketPath: process.env.DOCKER_SOCKET_PATH }); + +class Traefik { + constructor() { + } + async addLabels(serviceId) { + return new Promise(async (resolve, reject) => { + try { + const service = await docker.getService(serviceId) + const spec = await service.inspect() + const newSpec = spec.Spec + newSpec.version = spec.Version.Index + + //Service Prefix + const prefix= `/${process.env.LINSTT_PREFIX}/${serviceId}` + + //services & routers + const enableLable = `traefik.enable` + const portLable = `traefik.http.services.${serviceId}.loadbalancer.server.port` + const entrypointLable = `traefik.http.routers.${serviceId}.entrypoints` + const ruleLable = `traefik.http.routers.${serviceId}.rule` + newSpec.Labels[enableLable] = 'true' + newSpec.Labels[portLable] = '80' + newSpec.Labels[entrypointLable] = 'http' + newSpec.Labels[ruleLable] = `Host(\`${process.env.LINTO_STACK_DOMAIN}\`) && PathPrefix(\`${prefix}\`)` + + //middlewares + const prefixLabel = `traefik.http.middlewares.${serviceId}-prefix.stripprefix.prefixes` + const middlewareLabel = `traefik.http.routers.${serviceId}.middlewares` + newSpec.Labels[prefixLabel] = prefix + newSpec.Labels[middlewareLabel] = `${serviceId}-prefix@docker` + + //ssl + const secureentrypoints = `traefik.http.routers.${serviceId}-secure.entrypoints` + const securetls = `traefik.http.routers.${serviceId}-secure.tls` + const securemiddleware = `traefik.http.routers.${serviceId}-secure.middlewares` + const securerule = `traefik.http.routers.${serviceId}-secure.rule` + + if (process.env.LINTO_STACK_USE_SSL != undefined && process.env.LINTO_STACK_USE_SSL == 'true') { + newSpec.Labels[secureentrypoints] = "https" + newSpec.Labels[securetls] = "true" + newSpec.Labels[securemiddleware] = `${serviceId}-prefix@docker` + newSpec.Labels[securerule] = `Host(\`${process.env.LINTO_STACK_DOMAIN}\`) && PathPrefix(\`${prefix}\`)` + newSpec.Labels[middlewareLabel] = `ssl-redirect@file, ${serviceId}-prefix@docker` + } + + //basicAuth + if (process.env.LINTO_STACK_HTTP_USE_AUTH != undefined && process.env.LINTO_STACK_HTTP_USE_AUTH == 'true') { + if (process.env.LINTO_STACK_USE_SSL != undefined && process.env.LINTO_STACK_USE_SSL == 'true') + newSpec.Labels[securemiddleware] = `basic-auth@file, ${serviceId}-prefix@docker` + else + newSpec.Labels[middlewareLabel] = `basic-auth@file, ${serviceId}-prefix@docker` + } + + await service.update(newSpec) + resolve() + } catch (err) { + debug(err) + reject(err) + } + }) + } + +} + + +module.exports = new Traefik() diff --git a/platform/stt-service-manager/components/IngressController/controllers/eventsFrom/ClusterManager.js b/platform/stt-service-manager/components/IngressController/controllers/eventsFrom/ClusterManager.js new file mode 100644 index 0000000..f23558a --- /dev/null +++ b/platform/stt-service-manager/components/IngressController/controllers/eventsFrom/ClusterManager.js @@ -0,0 +1,50 @@ +const debug = require('debug')(`app:ingresscontroller:eventsFrom:ClusterManager`) + +// this is bound to the component +module.exports = function () { + if (!this.app.components['ClusterManager']) return + + if (process.env.INGRESS_CONTROLLER == "nginx") { + this.app.components['ClusterManager'].on('serviceStarted', async (info) => { + try { + this.ingress.addUpStream(info) + debug(`Reload nginx service ${process.env.NGINX_SERVICE_ID}`) + await this.ingress.reloadNginx() + } catch (err) { + console.error(err) + } + }) + this.app.components['ClusterManager'].on('serviceStopped', async (serviceId) => { + try { + if (this.ingress.removeUpStream(serviceId)) { + debug(`Reload nginx service ${process.env.NGINX_SERVICE_ID}`) + await this.ingress.reloadNginx() + } + } catch (err) { + console.error(err) + } + }) + this.app.components['ClusterManager'].on('serviceScaled', async () => { + try { + debug(`Reload nginx service ${process.env.NGINX_SERVICE_ID}`) + await this.ingress.reloadNginx() + } catch (err) { + console.error(err) + } + }) + } + + if (process.env.INGRESS_CONTROLLER == "traefik") { + this.app.components['ClusterManager'].on('serviceStarted', async (info) => { + try { + await this.ingress.addLabels(info.service) + } catch (err) { + console.error(err) + } + }) + this.app.components['ClusterManager'].on('serviceStopped', async (serviceId) => { + }) + this.app.components['ClusterManager'].on('serviceScaled', async () => { + }) + } +} diff --git a/platform/stt-service-manager/components/IngressController/index.js b/platform/stt-service-manager/components/IngressController/index.js new file mode 100644 index 0000000..fbccc91 --- /dev/null +++ b/platform/stt-service-manager/components/IngressController/index.js @@ -0,0 +1,20 @@ +const Component = require(`../component.js`) +const debug = require('debug')(`app:ingresscontroller`) + +class IngressController extends Component { + constructor(app) { + super(app) + this.id = this.constructor.name + this.app = app + switch (process.env.INGRESS_CONTROLLER) { + case 'nginx': + this.ingress = require(`./Nginx`) + break + case 'traefik': this.ingress = require(`./Traefik`); break + default: throw 'Undefined INGRESS controller' + } + return this.init() + } +} + +module.exports = app => new IngressController(app) diff --git a/platform/stt-service-manager/components/LinSTT/Kaldi/index.js b/platform/stt-service-manager/components/LinSTT/Kaldi/index.js new file mode 100644 index 0000000..b560fbb --- /dev/null +++ b/platform/stt-service-manager/components/LinSTT/Kaldi/index.js @@ -0,0 +1,492 @@ +const debug = require('debug')(`app:linstt:kaldi`) +const fs = require('fs').promises +const rimraf = require("rimraf"); +const ini = require('ini') +const exec = require('child_process').exec; +const ncp = require('ncp').ncp; +const ncpPromise = require('util').promisify(ncp) +const datetime = require('node-datetime') +const sleep = require('util').promisify(setTimeout) + +Array.prototype.diff = function (a) { + return this.filter(function (i) { return a.indexOf(i) < 0; }); +}; + +/** + * Execute simple shell command (async wrapper). + * @param {String} cmd + * @return {Object} { stdout: String, stderr: String } + */ +async function sh(cmd) { + return new Promise(function (resolve, reject) { + try { + exec(cmd, (err, stdout, stderr) => { + if (err) { + reject(err); + } else { + resolve(stdout); + } + }) + } catch (err) { + reject(err) + } + }); +} + + + +function uuidv4() { + const date = datetime.create().format('mdY-HMS') + return date + '-yxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +class Kaldi { + constructor() { + this.lang = process.env.LANGUAGE.split(',') + this.lang = this.lang.map(s => s.trim()) + } + + async getAMParams(acmodelId) { + const content = await fs.readFile(`${process.env.AM_PATH}/${acmodelId}/decode.cfg`, 'utf-8') + const config = ini.parse(content.replace(/:/g, '='), 'utf-8') + const lmGenPath = `${process.env.AM_PATH}/${acmodelId}/${config.decoder_params.lmPath}` + const lmGenOrder = config.decoder_params.lmOrder + return { lmGenPath: lmGenPath, lmOrder: lmGenOrder } + } + + async checkModel(modelId, type) { + try { + const AM = ['conf/', 'ivector_extractor/', 'decode.cfg', 'final.mdl', 'tree'] + const LMGen = ['g2p/.tool', 'g2p/model', 'dict/lexicon.txt', 'dict/extra_questions.txt', 'dict/nonsilence_phones.txt', 'dict/optional_silence.txt', 'dict/silence_phones.txt'] + const LM = ['HCLG.fst', 'words.txt'] + switch (type) { + case 'am': + for (let i = 0; i < AM.length; i++) + await fs.stat(`${process.env.AM_PATH}/${modelId}/${AM[i]}`) + const params = await this.getAMParams(modelId) + for (let i = 0; i < LMGen.length; i++) + await fs.stat(`${params.lmGenPath}/${LMGen[i]}`) + break + case 'lm': + for (let i = 0; i < LM.length; i++) + await fs.stat(`${process.env.LM_PATH}/${modelId}/${LM[i]}`) + break + } + return true + } catch (err) { + return false + } + } + + async phonetisation(g2ptool, g2pmodel, oovFile) { + try { + let lex = {} + switch (g2ptool) { + case "phonetisaurus": + lex = await sh(`phonetisaurus-apply --model ${g2pmodel} --word_list ${oovFile}`) + break + case 'sequitur': + lex = await sh(`g2p.py --encoding=utf-8 --model=${g2pmodel} --apply ${oovFile}`) + break + default: + debug('undefined g2p tool') + throw 'Error during language model generation' + } + lex = lex.split('\n').filter(function (el) { return el; }) + lex = lex.map(s => s.split('\t')) + return lex + } catch (err) { + throw err + } + } + + prepareIntent(intent, words) { + /** + * Apply a set of transformations + * convert to lowercase + * remove multiple spaces + * split commands based on comma character + * split commands based on point character + * remove the begin-end white spaces + */ + let newIntent = intent.items.map(elem => elem.toLowerCase().trim().split(/,|\./)) + newIntent = newIntent.flat() + newIntent = newIntent.filter(function (el) { return el; }) //remove empty element from list + newIntent = newIntent.map(s => s.replace(/^##.*/g, '')) //remove starting character (markdown format) + newIntent = newIntent.map(s => s.replace(/^ *- */g, '')) //remove starting character (markdown format) + newIntent = newIntent.map(s => s.replace(/\[[^\[]*\]/g, '')) //remove entity values + newIntent = newIntent.map(s => s.replace(/\(/g, '#')) //add entity identifier + newIntent = newIntent.map(s => s.replace(/\)/g, ' ')) //remove parenthesis + newIntent = newIntent.map(s => s.replace(/’/g, '\'')) //replace special character + newIntent = newIntent.map(s => s.replace(/'/g, '\' ')) //add space after quote symbol + newIntent = newIntent.map(s => s.replace(/æ/g, 'ae')) //replace special character + newIntent = newIntent.map(s => s.replace(/œ/g, 'oe')) //replace special character + newIntent = newIntent.map(s => s.replace(/[^a-z àâäçèéêëîïôùûü_'\-#]/g, '')) //remove other symbols + newIntent = newIntent.map(s => s.replace(/ +/g, ' ')) //remove double space + newIntent = newIntent.map(s => s.trim()) //remove the begin-end white spaces + newIntent = newIntent.filter(function (el) { return el; }) //remove empty element from list + newIntent = newIntent.sort() + + + // intent.items.forEach((item) => { + // const subCmds = item.toLowerCase().replace(/ +/g, ' ').split(/,|\./).map(elem => elem.trim()) + // const filtered = subCmds.filter(function (el) { return el; }) //remove empty element from list + // let newCmds = filtered.map(s => s.replace(/^ *- */, '')) //remove starting character (markdown format) + // newCmds = newCmds.map(s => s.replace(/\[[^\[]*\]/g, '')) //remove entity values + // newCmds = newCmds.map(s => s.replace(/\(/g, '#')) //add entity identifier + // newCmds = newCmds.map(s => s.replace(/\)/g, ' ')) //remove parenthesis + // newCmds = newCmds.map(s => s.replace(/’/g, '\'')) //replace special character + // newCmds = newCmds.map(s => s.replace(/'/g, '\' ')) //add space after quote symbol + // newCmds = newCmds.map(s => s.replace(/æ/g, 'ae')) //replace special character + // newCmds = newCmds.map(s => s.replace(/œ/g, 'oe')) //replace special character + // newCmds = newCmds.map(s => s.replace(/[^a-z àâäçèéêëîïôùûü_'\-#]/g, '')) //remove other symbols + // newCmds = newCmds.map(s => s.replace(/ +/g, ' ')) //remove double space + // newCmds = newCmds.map(s => s.trim()) //remove the begin-end white spaces + // newCmds = newCmds.filter(function (el) { return el; }) //remove empty element from list + // newIntent.push(newCmds) + // }) + // + // newIntent = newIntent.sort() + + + /** + * match the commands vocab with the defined lexicon + * use the initialized word list and find each sequence of words in the commande + */ + let cmd = newIntent.flat().join(' \n ') + cmd = ` ${cmd} ` + words.forEach(word => { + if (cmd.indexOf(word.seq) !== -1) + cmd = cmd.replace(` ${word.seq} `, ` ${word.org} `) + }) + newIntent = cmd.trim().split(' \n ').map(elem => elem.trim()) //re-build list + + /** + * remove sub-commands + */ + for (let i = 0; i < newIntent.length; i++) + for (let j = 0; j < newIntent.length; j++) + if (i !== j && ` ${newIntent[i]} `.indexOf(` ${newIntent[j]} `) !== -1) { + newIntent[i] = "" + break + } + newIntent = newIntent.filter(function (el) { return el; }) //remove empty element from list + newIntent = newIntent.sort() + return newIntent + } + + prepareEntity(entity) { + /** + * Apply a set of transformations + * convert to lowercase + * remove duplicates + * select entities with multiple pronunciations + */ + let newEntity = entity.items.map(elem => elem.toLowerCase().trim()) + newEntity = [...new Set(newEntity)] //remove duplicates from list + newEntity = newEntity.filter(function (el) { return el; }) //remove empty element from list + const pronunciations = newEntity.map(e => { if (e.indexOf(process.env.DICT_DELIMITER) !== -1) return e; else return '' }) + newEntity = newEntity.map(e => { if (e.indexOf(process.env.DICT_DELIMITER) !== -1) return e.split(process.env.DICT_DELIMITER)[0]; else return e }) + + newEntity = newEntity.filter(function (el) { return el; }) //remove empty element from list + newEntity = newEntity.map(s => s.replace(/^##.*/, '')) //remove starting character (markdown format) + newEntity = newEntity.map(s => s.replace(/^ *- */, '')) //remove starting character (markdown format) + newEntity = newEntity.map(s => s.replace(/\[/g, ' ')) //remove special characters + newEntity = newEntity.map(s => s.replace(/\]/g, ' ')) //remove special characters + newEntity = newEntity.map(s => s.replace(/\(/g, ' ')) //remove parenthesis + newEntity = newEntity.map(s => s.replace(/\)/g, ' ')) //remove parenthesis + newEntity = newEntity.map(s => s.replace(/’/g, '\'')) //replace special character + newEntity = newEntity.map(s => s.replace(/'/g, '\' ')) //add space after quote symbol + newEntity = newEntity.map(s => s.replace(/æ/g, 'ae')) //replace special character + newEntity = newEntity.map(s => s.replace(/œ/g, 'oe')) //replace special character + newEntity = newEntity.map(s => s.replace(/[^a-z àâäçèéêëîïôùûü_'\-]/g, '')) //remove other symbols + newEntity = newEntity.map(s => s.replace(/ +/g, ' ')) //remove double space + newEntity = newEntity.map(s => s.trim()) //remove the begin-end white spaces + newEntity = newEntity.filter(function (el) { return el; }) //remove empty element from list + newEntity = newEntity.sort() + + return { entity: newEntity, pron: pronunciations } + } + + + async prepareParam(acmodelId, lgmodelId) { + //get acoustic model parameters + const params = await this.getAMParams(acmodelId) + const tmpfoldername = lgmodelId + '_' + uuidv4() + /** Configuration Section */ + /** ****************** */ + this.tmplmpath = `${process.env.TEMP_FILE_PATH}/${tmpfoldername}` //temporary LM path + this.entitypath = `${this.tmplmpath}/fst` //folder where the normalized entities will be saved + const lexiconpath = `${this.tmplmpath}/lexicon` //folder where to save the new lexicon including the oov + this.dictpath = `${this.tmplmpath}/dict` //folder to save the new dictionary + this.langpath = `${this.tmplmpath}/lang` //folder to save the new lang files + this.graph = `${this.tmplmpath}/graph` //folder to save the graph files + this.langextraspath = `${this.tmplmpath}/lang_new` //folder to save the new lang files + this.intentsFile = `${this.tmplmpath}/text` //LM training file path + this.lexiconFile = `${this.dictpath}/lexicon.txt` //new lexicon file + this.nonterminalsFile = `${this.dictpath}/nonterminals.txt` //nonterminals file + this.pronunciationFile = `${this.tmplmpath}/pronunciations` //words with different pronunciations + this.arpa = `${this.tmplmpath}/arpa` //arpa file path + this.g2ptool = await fs.readFile(`${params.lmGenPath}/g2p/.tool`, 'utf-8') + this.g2pmodel = `${params.lmGenPath}/g2p/model` + const dictgenpath = `${params.lmGenPath}/dict` + this.lexicongenfile = `${params.lmGenPath}/dict/lexicon.txt` + this.oovFile = `${lexiconpath}/oov` + this.graphError = false + this.graphMsg = "" + /** ****************** */ + + let exist = await fs.stat(this.tmplmpath).then(async () => { return true }).catch(async () => { return false }) + if (exist) { await new Promise((resolve, reject) => { rimraf(this.tmplmpath, async (err) => { if (err) reject(err); resolve() }); }) } + await fs.mkdir(this.tmplmpath) //create the temporary LM folder + await fs.mkdir(this.entitypath) //create the fst folder + await ncpPromise(dictgenpath, this.dictpath) //copy the dict folder + //await fs.mkdir(dictpath) //create the dict folder + await fs.mkdir(this.langpath) //create the langprep folder + await fs.mkdir(lexiconpath) //create the lexicon folder + await fs.mkdir(this.langextraspath) //create the langextras folder + } + + + async prepare_lex_vocab() { + this.lexicon = [] //lexicon (words + pronunciation) + this.words = [] //all list of words + this.specwords = [] //words with symbol '-' + //prepare lexicon and words + const content = await fs.readFile(this.lexicongenfile, 'utf-8') + const lexiconFile = content.split('\n') + lexiconFile.forEach((curr) => { + const e = curr.trim().replace('\t', ' ').split(' ') + const filtered = e.filter(function (el) { return el; }) + const item = filtered[0] + if (item !== undefined) { + filtered.shift() + this.lexicon.push([item, filtered.join(' ')]) + this.words.push(item) + if (item.indexOf('-') !== -1) { + this.specwords.push({ seq: [item.replace(/-/g, " ")], org: item }) + } + } + }) + } + + + async prepare_intents(intents) { + this.newIntent = [] + this.fullVocab = [] + //prepare intents + intents.forEach((intent) => { + this.newIntent.push(this.prepareIntent(intent, this.specwords)) + }) + this.newIntent = this.newIntent.flat() + if (this.newIntent.length === 0) + throw 'No command found' + this.fullVocab.push(this.newIntent.join(' ').split(' ')) + await fs.writeFile(this.intentsFile, this.newIntent.join('\n').replace(/#/g, '#nonterm:'), 'utf-8', (err) => { throw err }) + } + + + async prepare_entities(entities) { + this.entityname = [] + this.pronunciations = [] + //prepare entities + entities.forEach((entity) => { + this.entityname.push(entity.name) + const newEntity = this.prepareEntity(entity) + if (newEntity.entity.length === 0) + throw `The entity ${entity.name} is empty (either remove it or update it)` + this.pronunciations.push(newEntity.pron) + this.fullVocab.push(newEntity.entity.join(' ').split(' ')) + fs.writeFile(`${this.entitypath}/${entity.name}`, newEntity.entity.join('\n') + '\n', 'utf-8', (err) => { throw err }) + }) + this.pronunciations = this.pronunciations.flat().filter(function (el) { return el; }) + } + + + check_entities() { + //check entities + this.listentities = this.newIntent.join(' ').split(' ').filter(word => word.indexOf('#') !== -1) + this.listentities = this.listentities.map(w => w.replace(/#/, '')) + this.listentities = this.listentities.filter((v, i, a) => a.indexOf(v) === i) + const diff = this.listentities.diff(this.entityname) + if (diff.length !== 0) throw `This list of entities are not yet created: [${diff}]` + } + + + async prepare_new_lexicon() { + // Prepare OOV words + this.fullVocab = [...new Set(this.fullVocab.flat())] //remove duplicates from list + this.fullVocab = this.fullVocab.map(s => { if (s.indexOf('#') === -1) return s }) + this.fullVocab = this.fullVocab.filter(function (el) { return el; }) + this.fullVocab = this.fullVocab.sort() + this.oov = this.fullVocab.diff(this.words) + if (this.oov.length !== 0) { + await fs.writeFile(this.oovFile, this.oov.join('\n'), 'utf-8', (err) => { throw err }) + const oov_lex = await this.phonetisation(this.g2ptool.trim(), this.g2pmodel, this.oovFile) + const oov1 = oov_lex.map(s => s[0]) + const diff = this.oov.diff(oov1) + if (diff.length !== 0) { + throw `Error during language model generation: the phonetisation of some words were not generated [${diff}]` + } + this.lexicon = this.lexicon.concat(oov_lex) + } + + // Prepare multiple pronunciations + if (this.pronunciations.length !== 0) { + for (let i = 0; i < this.pronunciations.length; i++) { + const words = this.pronunciations[i].split(process.env.DICT_DELIMITER) + const org = words[0] + words.shift() + await fs.writeFile(this.pronunciationFile, words.join('\n'), { encoding: 'utf-8', flag: 'w' }) + let pronon_lex = await this.phonetisation(this.g2ptool.trim(), this.g2pmodel, this.pronunciationFile) + pronon_lex = pronon_lex.map(s => { s[0] = org; return s }) + this.lexicon = this.lexicon.concat(pronon_lex) + } + } + this.lexicon = this.lexicon.map(s => { return `${s[0]}\t${s[1]}` }) + this.lexicon = [...new Set(this.lexicon)] + this.lexicon = this.lexicon.sort() + + // save the new lexicon + await fs.writeFile(this.lexiconFile, `${this.lexicon.join('\n')}\n`, { encoding: 'utf-8', flag: 'w' }) + // create nonterminals file + if (this.listentities.length !== 0) + await fs.writeFile(this.nonterminalsFile, this.listentities.map(s => { return `#nonterm:${s}` }).join('\n'), { encoding: 'utf-8', flag: 'w' }) + // remove lexiconp.txt if exist + try { + await fs.stat(`${this.dictpath}/lexiconp.txt`).then(async () => { return true }).catch(async () => { return false }) + await fs.unlink(`${this.dictpath}/lexiconp.txt`) + } catch (err) {} + } + + async prepare_lang() { + try { + const scriptShellPath = `${process.cwd()}/components/LinSTT/Kaldi/scripts` + await sh(`cd ${scriptShellPath}; prepare_lang.sh ${this.dictpath} "" ${this.langpath}/tmp ${this.langpath}`) + } catch (err) { + debug(err) + throw 'Error during language model preparation' + } + } + + async generate_arpa() { + const ngram = process.env.NGRAM + try { + await sh(`add-start-end.sh < ${this.tmplmpath}/text > ${this.tmplmpath}/text.s`) + await sh(`ngt -i=${this.tmplmpath}/text.s -n=${ngram} -o=${this.tmplmpath}/irstlm.${ngram}.ngt -b=yes`) + await sh(`tlm -tr=${this.tmplmpath}/irstlm.${ngram}.ngt -n=${ngram} -lm=wb -o=${this.arpa}`) + } catch (err) { + debug(err) + throw 'Error during NGRAM language model generation' + } + } + + generate_main_and_entities_HCLG(acmodelId) { + const mainG = `${this.langextraspath}/main` + ncpPromise(this.langpath, mainG).then(async () => { + try { + await sh(`arpa2fst --disambig-symbol=#0 --read-symbol-table=${this.langpath}/words.txt ${this.arpa} ${mainG}/G.fst`) + await sh(`mkgraph.sh --self-loop-scale 1.0 ${mainG} ${process.env.AM_PATH}/${acmodelId} ${this.graph}_main`) + } catch (err) { + this.graphError = true + this.graphMsg = 'Error during main HCLG graph generation' + debug('Error during main HCLG graph generation') + debug(err) + } + }).catch(err => { + this.graphError = true + this.graphMsg = 'Error during main lang copy' + debug('Error during main lang copy') + }) + + this.listentities.forEach((entity) => { + const langG = `${this.langextraspath}/${entity}` + const scriptShellPath = `${process.cwd()}/components/LinSTT/Kaldi/scripts` + ncpPromise(this.langpath, langG).then(async () => { + try { + await sh(`awk -f ${scriptShellPath}/fst.awk ${this.langpath}/words.txt ${this.entitypath}/${entity} > ${this.entitypath}/${entity}.int`) + await sh(`fstcompile ${this.entitypath}/${entity}.int | fstarcsort --sort_type=ilabel > ${langG}/G.fst`) + await sh(`mkgraph.sh --self-loop-scale 1.0 ${langG} ${process.env.AM_PATH}/${acmodelId} ${this.graph}/${entity}`) + } catch (err) { + this.graphError = true + this.graphMsg = 'Error during entities HCLG graph generation' + debug(`Error during entities HCLG graph generation: ${this.graph}/${entity}`) + debug(err) + } + }).catch(err => { + this.graphError = true + this.graphMsg = 'Error during entities HCLG graph generation' + debug(`Error during entities HCLG graph generation: ${this.graph}/${entity}`) + }) + }) + } + + async check_previous_HCLG_creation() { + let retry = true + const time = 1 //in seconds + while (retry) { + debug('check_previous_HCLG_creation') + retry = false + try { + await fs.stat(`${this.graph}_main/HCLG.fst`) + } catch (err) { + retry = true + } + this.listentities.forEach(async (entity) => { + try { + await fs.stat(`${this.graph}/${entity}/HCLG.fst`) + } catch (err) { + retry = true + } + }) + await sleep(time * 1000) + if (this.graphError) + throw this.graphMsg + } + debug('wait until all files will be created on disk') + await sleep(1000) + debug('all files are successfully generated') + } + + async generate_final_HCLG(lgmodelId) { + if (this.listentities.length == 0) { + try { + await fs.copyFile(`${this.graph}_main/HCLG.fst`, `${process.env.LM_PATH}/${lgmodelId}/HCLG.fst`) + await fs.copyFile(`${this.graph}_main/words.txt`, `${process.env.LM_PATH}/${lgmodelId}/words.txt`) + } catch (err) { + debug(err) + throw 'Error while copying the new decoding graph' + } + } else { + try { + let cmd = '' + const content = await fs.readFile(`${this.langpath}/phones.txt`, 'utf-8') + let id = [] + let list = content.split('\n') + list = list.map(s => s.replace(/^(?!#nonterm.*$).*/g, '')) + list = list.filter(function (el) { return el; }) //remove empty element from list + list = list.map(s => { const a = s.split(' '); id.push(a[1]); return a[0]; }) + this.listentities.forEach(async (entity) => { + const idx = list.indexOf(`#nonterm:${entity}`) + cmd += `${id[idx]} ${this.graph}/${entity}/HCLG.fst ` + }) + const offset = list.indexOf(`#nonterm_bos`) + await sh(`make-grammar-fst --write-as-grammar=false --nonterm-phones-offset=${id[offset]} ${this.graph}_main/HCLG.fst ${cmd} ${this.graph}/HCLG.fst`) + await fs.copyFile(`${this.graph}/HCLG.fst`, `${process.env.LM_PATH}/${lgmodelId}/HCLG.fst`) + await fs.copyFile(`${this.langpath}/words.txt`, `${process.env.LM_PATH}/${lgmodelId}/words.txt`) + } catch (err) { + debug(err) + throw 'Error while generating the decoding graph' + } + } + } + + removeTmpFolder() { + rimraf(this.tmplmpath, async (err) => { if (err) throw err }) + } +} + +module.exports = new Kaldi() \ No newline at end of file diff --git a/platform/stt-service-manager/components/LinSTT/Kaldi/scripts/fst.awk b/platform/stt-service-manager/components/LinSTT/Kaldi/scripts/fst.awk new file mode 100755 index 0000000..947480c --- /dev/null +++ b/platform/stt-service-manager/components/LinSTT/Kaldi/scripts/fst.awk @@ -0,0 +1,34 @@ +#!/usr/bin/awk -f + +BEGIN { + # ARGV[0] is the filename of the script itself. + # Set ARGV length. + file=ARGV[2] #file to proceed + word=ARGV[1] #word dictionary + n=split(file,e,"/") + entity=e[n] + inode=1 + onode=2 + tnode=3 + while(( getline line< word) > 0 ) { + split(line,a," ") + words[a[1]]=a[2] + } + print("0 1 "words["#nonterm_begin"]" "words[""]) +} +{ + while(( getline line< file) > 0 ) { + n=split(line,a," ") + inode=1 + for(i=1;i"]) + print("3") +} diff --git a/devcerts/.gitkeep b/platform/stt-service-manager/components/LinSTT/Kaldi/scripts/path.sh old mode 100644 new mode 100755 similarity index 100% rename from devcerts/.gitkeep rename to platform/stt-service-manager/components/LinSTT/Kaldi/scripts/path.sh diff --git a/platform/stt-service-manager/components/LinSTT/Kaldi/scripts/prepare_HCLG.sh b/platform/stt-service-manager/components/LinSTT/Kaldi/scripts/prepare_HCLG.sh new file mode 100755 index 0000000..fe74b68 --- /dev/null +++ b/platform/stt-service-manager/components/LinSTT/Kaldi/scripts/prepare_HCLG.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Copyright 2018 Linagora (author: Ilyes Rebai; email: irebai@linagora.com) +# LinSTT project + +# Param +order=3 +. parse_options.sh || exit 1; +# End + +# Begin configuration section. +model=$1 # the path of the acoustic model +lmodel=$2 # the path to the decoding graph +lmgen=$3 # the path to the language generation directory +out=$4 # the output folder where to save the generated files + +## Working param +dictgen=$lmgen/dict +g2p_model=$lmgen/g2p/model +g2p_tool=$(cat $lmgen/g2p/.tool) + +## Create Output folders +dict=$out/dict +lex=$out/lexicon +lang=$out/lang +graph=$out/graph +fst=$out/fst +arpa=$out/arpa +# End configuration section. + +################################################## GENERATE THE LANG DIR ###################################### +#create lang dir +prepare_lang.sh $dict "" $lang/tmp $lang +if [ $? -eq 1 ]; then exit 1; fi +############################################################################################################### + +################################################## GENERATE THE ARPA FILE ##################################### +#create arpa using irstlm +sed -i "s/#/#nonterm:/g" $out/text +add-start-end.sh < $out/text > $out/text.s +ngt -i=$out/text.s -n=$order -o=$out/irstlm.${order}.ngt -b=yes 2>/dev/null +tlm -tr=$out/irstlm.${order}.ngt -n=$order -lm=wb -o=$arpa 2>/dev/null +############################################################################################################### + +################################################## GENERATE THE GRAMMAR FILE ################################## +#create G +langm=${lang}_new/main +mkdir -p $langm +cp -r $lang/* $langm +arpa2fst --disambig-symbol=#0 --read-symbol-table=$lang/words.txt $arpa $langm/G.fst +mkgraph.sh --self-loop-scale 1.0 $langm $model $graph/main + +cmd="" +for e in $(cat $fst/.entities); do + echo "Preparing the graph of the entity $e" + lange=${lang}_new/$e + mkdir -p $lange + cp -r $lang/* $lange + awk -f fst.awk $lang/words.txt $fst/$e > $lange/$e.int + fstcompile $lange/$e.int | fstarcsort --sort_type=ilabel > $lange/G.fst + mkgraph.sh --self-loop-scale 1.0 $lange $model $graph/$e + id=$(grep "#nonterm:"$e" " $lang/phones.txt | awk '{print $2}') + cmd="$cmd $id $graph/$e/HCLG.fst" +done +############################################################################################################### + +################################################## GENERATE THE HCLG FILE ##################################### +#create HCLG +if [ "$cmd" == "" ]; then + mkdir -p $graph + cp $graph/main/HCLG.fst $graph + cp $graph/main/words.txt $graph +else + echo "Preparing the main graph" + offset=$(grep nonterm_bos $lang/phones.txt | awk '{print $2}') + make-grammar-fst --write-as-grammar=false --nonterm-phones-offset=$offset $graph/main/HCLG.fst \ + $cmd $graph/HCLG.fst + cp $lang/words.txt $graph +fi +if [ ! -f $graph/HCLG.fst ]; then + echo "Error occured during generating the new decoding graph" + exit 1 +fi +############################################################################################################### + +################################################## SAVE THE GENERATED FILES ################################### +#copy new HCLG to model dir +cp $graph/HCLG.fst $lmodel +cp $graph/words.txt $lmodel +#return the oov if exists +if [ -s $lex/oov_vocab ]; then + oov=$(cat $lex/oov_vocab | tr '\n' ',') +fi +############################################################################################################### + + diff --git a/platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/WebServer.js b/platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/WebServer.js new file mode 100644 index 0000000..a3868b7 --- /dev/null +++ b/platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/WebServer.js @@ -0,0 +1,399 @@ +const debug = require('debug')(`app:linstt:eventsFrom:WebServer`) +const fs = require('fs').promises +const rimraf = require("rimraf"); +const ncp = require('ncp').ncp; +const ncpPromise = require('util').promisify(ncp) +const datetime = require('node-datetime') + +/** + * apiAModel.js + * +const debug = require('debug')(`app:linstt:apiamodel`) +const fs = require('fs').promises +const rimraf = require("rimraf"); +const compressing = require('compressing'); +const download = require('download'); + * + * +*/ + +/** + * apiLModel.js + * +const debug = require('debug')(`app:linstt:apilmodel`) +const fs = require('fs').promises +const rimraf = require("rimraf"); +const compressing = require('compressing'); +const download = require('download'); +const ncp = require('ncp').ncp; +const ncpPromise = require('util').promisify(ncp) + * + * +*/ + +/** + * apiElement.js + * +const debug = require('debug')(`app:linstt:apielement`) +const fs = require('fs').promises + * + * +*/ + + + +// this is bound to the component +module.exports = function () { + if (!this.app.components['WebServer']) return + + + /** + * Language Model events from WebServer + * createLModel + * deleteLModel + * getLModel + * getLModels + */ + this.app.components['WebServer'].on('createLModel', async (cb, payload) => { + try { + const destPath = `${process.env.LM_PATH}/${payload.modelId}` + const res = await this.db.lm.findModel(payload.modelId) + let amodel = {} + if (res) throw `Language Model '${payload.modelId}' exists` + if (payload.type == undefined || !this.verifType(payload.type)) throw `'type' parameter is required. Supported types are: ${this.type}` + + + /** Create a copy of an existing model */ + if (payload.lmodelId != undefined) { + const copy = await this.db.lm.findModel(payload.lmodelId) + if (!copy) throw `Language Model to copy '${payload.lmodelId}' does not exist` + await ncpPromise(`${process.env.LM_PATH}/${payload.lmodelId}`, destPath, async (err) => { if (err) throw err }) + await this.db.lm.createModel(payload.modelId, copy.acousticModel, copy.lang, copy.type, copy.isGenerated, copy.isDirty, copy.entities, copy.intents, copy.oov, copy.dateGen) + return cb({ bool: true, msg: `Language Model '${payload.modelId}' is successfully created` }) + } + + + /** check parameters */ + if (payload.acousticModel == undefined && payload.lang == undefined) throw `'acousticModel' or 'lang' parameter is required` + if (payload.acousticModel == undefined && payload.lang != undefined && this.stt.lang.indexOf(payload.lang) === -1) throw `${payload.lang} is not a valid language` + if (payload.acousticModel != undefined) { + amodel = await this.db.am.findModel(payload.acousticModel) + if (!amodel) throw `Acoustic Model '${payload.acousticModel}' does not exist` + } else if (payload.lang != undefined) { + amodel = await this.db.am.findModels({ lang: payload.lang }) + if (amodel.length == 0) throw `No Acoustic Model is found for the given language '${payload.lang}'` + amodel = amodel[amodel.length - 1] + } + + + /** Create a Model from a precompiled one using a file or link */ + if (payload.file != undefined || payload.link != undefined) { + if (payload.file != undefined) { + await this.uncompressFile(payload.file.mimetype, payload.file.path, destPath) + await fs.unlink(payload.file.path) + } else { + const fileparams = await this.downloadLink(payload.link) + await this.uncompressFile(fileparams["type"], fileparams["path"], destPath) + await fs.unlink(fileparams["path"]) + } + const check = await this.stt.checkModel(payload.modelId, 'lm') + if (check) { + await this.db.lm.createModel(payload.modelId, amodel.modelId, amodel.lang, payload.type, 1) + return cb({ bool: true, msg: `Language Model '${payload.modelId}' is successfully created` }) + } else { + rimraf(destPath, async (err) => { if (err) throw err; }) //remove folder + return cb({ bool: false, msg: 'This is not a valid model' }) + } + } + + { + let intents = [] + let entities = [] + /** Add data to Model if they exist */ + if (payload.data != undefined) { + /** Prepare intents if they exist */ + if (payload.data.intents != undefined) + payload.data.intents.forEach(intent => { + if (intent.name != undefined && intent.items != undefined && intent.items.length != 0) { + intent.items = [...new Set(intent.items)] + intents.push({ 'name': intent.name, 'items': intent.items }) + } else + throw 'The data intents are invalid' + }) + const namesI = intents.map(obj => { return obj.name }) + const uniqnamesI = [...new Set(intents.map(obj => { return obj.name }))] + if (namesI.length != uniqnamesI.length) throw 'The data intents are invalid (duplicated intents!!)' + + /** Prepare entities if they exist */ + if (payload.data.entities != undefined) + payload.data.entities.forEach(entity => { + if (entity.name != undefined && entity.items != undefined && entity.items.length != 0) { + entity.items = [...new Set(entity.items)] + entities.push({ 'name': entity.name, 'items': entity.items }) + } else + throw 'The data entities are invalid' + }) + const namesE = entities.map(obj => { return obj.name }) + const uniqnamesE = [...new Set(entities.map(obj => { return obj.name }))] + if (namesE.length != uniqnamesE.length) throw 'The data entities are invalid (duplicated intents!!)' + } + /** Create the Model */ + await this.db.lm.createModel(payload.modelId, amodel.modelId, amodel.lang, payload.type, 0, 1, entities, intents) + await fs.mkdir(destPath) + return cb({ bool: true, msg: `The Language Model '${payload.modelId}' is successfully created` }) + } + } catch (err) { + if (payload.file != undefined) await fs.unlink(payload.file.path) + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('deleteLModel', async (cb, modelId) => { + try { + const res = await this.db.lm.findModel(modelId) + if (!res) throw `Language Model '${modelId}' does not exist` + const services = await this.db.service.findServices({ LModelId:modelId, isOn: 1 }) + if (services.length != 0) throw `Language Model '${modelId}' is actually used by a running service` + + await this.db.lm.deleteModel(modelId) + rimraf(`${process.env.LM_PATH}/${modelId}`, async (err) => { if (err) throw err; }) //remove folder + return cb({ bool: true, msg: `Language Model '${modelId}' is successfully removed` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('getLModel', async (cb, modelId, param = '') => { + try { + const res = await this.db.lm.findModel(modelId) + if (!res) throw `Language Model '${modelId}' does not exist` + if (param == '') + return cb({ bool: true, msg: res }) + else + return cb({ bool: true, msg: res[param] }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('getLModels', async (cb) => { + try { + const res = await this.db.lm.findModels() + let models = [] + res.forEach((model) => { + let intents = model.intents.map(obj => { return obj.name }) + let entities = model.entities.map(obj => { return obj.name }) + model.intents = intents + model.entities = entities + models.push(model) + }) + return cb({ bool: true, msg: models }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('generateLModel', async (cb, modelId) => { + try { + const model = await this.db.lm.findModel(modelId) + if (!model) throw `Language Model '${modelId}' does not exist` + if (model.isDirty == 0 && model.isGenerated == 1) + throw `Language Model '${modelId}' is already generated and is up-to-date` + if (model.updateState > 0) + throw `Language Model '${modelId}' is in generation process` + + await this.db.lm.generationState(modelId, 1, 'In generation process') + this.generateModel(model, this.db.lm) + return cb({ bool: true, msg: `The process of generation of the Language Model '${modelId}' is successfully started.` }) + } catch (err) { + debug(err) + return cb({ bool: false, msg: err }) + } + }) + + + /** + * Acoustic Model events from WebServer + * createAModel + * deleteAModel + * getAModel + * getAModels + */ + this.app.components['WebServer'].on('createAModel', async (cb, payload) => { + try { + const destPath = `${process.env.AM_PATH}/${payload.modelId}` + const res = await this.db.am.findModel(payload.modelId) + if (res) throw `Acoustic Model '${payload.modelId}' exists` + if (payload.lang === undefined) throw `'lang' parameter is required` + if (this.stt.lang.indexOf(payload.lang) === -1) throw `${payload.lang} is not a valid language` + if (payload.file == undefined && payload.link == undefined) throw `'link' or 'file' parameter is required` + + if (payload.file != undefined) { + await this.uncompressFile(payload.file.mimetype, payload.file.path, destPath) + await fs.unlink(payload.file.path) + } else if (payload.link != undefined) { + const fileparams = await this.downloadLink(payload.link) + await this.uncompressFile(fileparams["type"], fileparams["path"], destPath) + await fs.unlink(fileparams["path"]) + } + const check = await this.stt.checkModel(payload.modelId, 'am') + if (check) { + await this.db.am.createModel(payload.modelId, payload.lang, payload.desc) + return cb({ bool: true, msg: `Acoustic Model '${payload.modelId}' is successfully created` }) + } else { + rimraf(destPath, async (err) => { if (err) throw err; }) //remove folder + throw 'This is not a valid model' + } + } catch (err) { + if (payload.file != undefined) fs.unlink(payload.file.path).catch(err => { }) + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('deleteAModel', async (cb, modelId) => { + try { + const res = await this.db.am.findModel(modelId) + if (!res) throw `Acoustic Model '${modelId}' does not exist` + const check = await this.db.lm.findModels({ acmodelId: modelId }) + if (check.length > 0) throw `There are language models (${check.length}) that use the acoustic model '${modelId}'` + await this.db.am.deleteModel(modelId) + rimraf(`${process.env.AM_PATH}/${modelId}`, async (err) => { if (err) throw err; }) //remove folder + return cb({ bool: true, msg: `Acoustic Model '${modelId}' is successfully removed` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('getAModel', async (cb, modelId) => { + try { + const res = await this.db.am.findModel(modelId) + if (!res) throw `Acoustic Model '${modelId}' does not exist` + return cb({ bool: true, msg: res }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('getAModels', async (cb) => { + try { + const res = await this.db.am.findModels() + return cb({ bool: true, msg: res }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + + + /** + * Entity/Intent events from WebServer + * addType + * deleteType + * updateType + * getType + * getTypes + */ + this.app.components['WebServer'].on('createType', async (cb, payload, type) => { + /** + this.emit('create', payload, type, async (err) => { + if (err) { + if (payload.file != undefined) await fs.unlink(payload.file.path) + return cb({ bool: false, msg: err }) + } + return cb({ bool: true, msg: `${payload.name} is successfully added to language model '${payload.modelId}'` }) + }) + */ + try { + const data = {} + const model = await this.db.lm.findModel(payload.modelId) + if (!model) throw `Language Model '${payload.modelId}' does not exist` + if (payload.file == undefined && payload.content.length == undefined) throw `'file' parameter or a JSON body (liste of values) is required` + const exist = await this.checkExists(model, payload.name, type, false) + if (exist) throw `${payload.name} already exists` + + if (payload.file != undefined) { + const content = await fs.readFile(payload.file.path, 'utf-8') + data.name = payload.name + data.items = content.split('\n') + await fs.unlink(payload.file.path) + } else if (payload.content.length != undefined && payload.content.length != 0) { + data.name = payload.name + data.items = payload.content + } + /** check the data before save */ + if (data.items.length == 0) throw `${payload.name} is empty` + + await this.db.lm.addElemInList(payload.modelId, type, data) + return cb({ bool: true, msg: `${payload.name} is successfully added` }) + } catch (err) { + debug(err) + if (payload.file != undefined) await fs.unlink(payload.file.path) + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('deleteType', async (cb, payload, type) => { + try { + const model = await this.db.lm.findModel(payload.modelId) + if (!model) throw `Language Model '${payload.modelId}' does not exists` + const exist = await this.checkExists(model, payload.name, type, true) + if (!exist) throw `${payload.name} does not exist` + + await this.db.lm.removeElemFromList(payload.modelId, type, payload.name) + return cb({ bool: true, msg: `${payload.name} is successfully removed` }) + } catch (err) { + if (payload.file != undefined) await fs.unlink(payload.file.path) + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('updateType', async (cb, payload, type, update) => { + try { + const model = await this.db.lm.findModel(payload.modelId) + if (!model) throw `Language Model '${payload.modelId}' does not exist` + if (payload.file == undefined && payload.content.length == undefined) throw `'file' parameter or a JSON body (liste of values) is required` + + /** get data */ + const obj = await this.checkExists(model, payload.name, type, true) + if (!obj) throw `${payload.name} does not exist` + + if (payload.file != undefined) { + const content = await fs.readFile(payload.file.path, 'utf-8') + tmp = content.split('\n') + await fs.unlink(payload.file.path) + } else if (payload.content.length != undefined && payload.content.length != 0) { + tmp = payload.content + } + + /** check the data before save */ + if (tmp.length == 0) throw `${payload.name} is empty` + + switch (update) { + case 'put': + break + case 'patch': + tmp = obj.items.concat(tmp) + tmp = [...new Set(tmp)] + break + default: throw `Undefined update parameter from 'updateType' eventEmitter` + } + await this.db.lm.updateElemFromList(payload.modelId, `${type}.${obj.idx}.items`, tmp) + return cb({ bool: true, msg: `${payload.name} is successfully updated` }) + } catch (err) { + if (payload.file != undefined) await fs.unlink(payload.file.path) + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('getType', async (cb, payload, type) => { + try { + const model = await this.db.lm.findModel(payload.modelId) + if (!model) throw `Language Model '${payload.modelId}' does not exist` + const obj = await this.checkExists(model, payload.name, type, true) + if (!obj) throw `${payload.name} does not exist` + return cb({ bool: true, msg: obj.items }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + this.app.components['WebServer'].on('getTypes', async (cb, modelId, type) => { + try { + const model = await this.db.lm.findModel(payload.modelId) + if (!model) throw `Language Model '${payload.modelId}' does not exist` + return cb({ bool: true, msg: model[type] }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + +} diff --git a/platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/myself.js b/platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/myself.js new file mode 100644 index 0000000..75977c6 --- /dev/null +++ b/platform/stt-service-manager/components/LinSTT/controllers/eventsFrom/myself.js @@ -0,0 +1,15 @@ +const debug = require('debug')(`app:linstt:events`) +const fs = require('fs').promises + +// this is bound to the component +module.exports = function () { + this.on('create', async (payload, type, cb) => { + }) + this.on('update', async (payload, type, update, cb) => { + + }) + this.on('delete', async (payload, type, cb) => { + + }) + +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/LinSTT/index.js b/platform/stt-service-manager/components/LinSTT/index.js new file mode 100644 index 0000000..2a1220e --- /dev/null +++ b/platform/stt-service-manager/components/LinSTT/index.js @@ -0,0 +1,167 @@ +const Component = require(`../component.js`) +const debug = require('debug')(`app:linstt`) +const compressing = require('compressing'); +const fetch = require('node-fetch'); +const mime = require('mime-types') +const fs = require('fs') +const am = require(`${process.cwd()}/models/models/AMUpdates`) +const lm = require(`${process.cwd()}/models/models/LMUpdates`) +const service = require(`${process.cwd()}/models/models/ServiceUpdates`) + +class LinSTT extends Component { + constructor(app) { + super(app) + this.id = this.constructor.name + this.app = app + this.db = { am: am, lm: lm, service: service } + this.type = ['lvcsr', 'cmd'] + switch (process.env.LINSTT_SYS) { + case 'kaldi': + this.stt = require(`./Kaldi`) + break + case '': this.stt = ''; break + default: throw 'Undefined LinSTT system' + } + return this.init() + } + + /** + * Other functions used by Acoustic and Language Model events + */ + verifType(type) { + if (this.type.indexOf(type) != -1) return 1 + else return 0 + } + + async downloadLink(link) { + return new Promise(async (resolve, rejection) => { + try { + const filename = link.split('/').pop() + const filepath = `${process.env.TEMP_FILE_PATH}/${filename}` + const res = await fetch(link, {method:"GET"}) + if(res.status != 200){ + if (res.statusText == "Not Found") + throw `${link} ${res.statusText}` + else throw res.statusText + } + const file=fs.createWriteStream(filepath,{'emitClose':true}) + res.body.pipe(file) + res.body.on('end',() => { + const filetype = mime.lookup(filepath) + resolve({ 'path': filepath, 'type': filetype }) + }) + res.body.on('error',(err) => {throw err}) + } catch (err) { + console.error("ERROR: " + err) + rejection(err) + } + }) + } + + async uncompressFile(type, src, dest) { + return new Promise(async (resolve, rejection) => { + try { + if (type == 'application/zip') + compressing.zip.uncompress(src, dest).then(() => { + resolve('uncompressed') + }).catch(err => { + rejection(err) + }) + else if (type == 'application/gzip') + compressing.tar.uncompress(src, dest).then(() => { + resolve('uncompressed') + }).catch(err => { + rejection(err) + }) + else if (type == 'application/x-gzip') + compressing.tgz.uncompress(src, dest).then(() => { + resolve('uncompressed') + }).catch(err => { + rejection(err) + }) + else rejection('Undefined file format. Please use one of the following format: zip or tar.gz') + } catch (err) { + console.error("ERROR: " + err) + rejection(err) + } + }) + } + + async checkExists(model, name, type, isTrue) { + let exist = 0 + if (isTrue) { + model[type].forEach(async (obj, idx) => { + obj.idx = idx + if (obj.name == name) + exist = obj + }) + } else { + model[type].forEach((obj) => { + if (obj.name == name) + exist = 1 + }) + } + return exist + } + + async generateModel(res, db) { + try { + await this.stt.prepareParam(res.acmodelId, res.modelId).then(async () => { + debug(`done prepareParam (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 1, 'In generation process') + }) + await this.stt.prepare_lex_vocab().then(async () => { + debug(`done prepare_lex_vocab (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 3, 'In generation process') + }) + await this.stt.prepare_intents(res.intents).then(async () => { + debug(`done prepare_intents (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 8, 'In generation process') + }) + await this.stt.prepare_entities(res.entities).then(async () => { + debug(`done prepare_entities (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 13, 'In generation process') + }) + + this.stt.check_entities() + debug(`done check_entities (${this.stt.tmplmpath})`) + + await this.stt.prepare_new_lexicon().then(async () => { + debug(`done prepare_new_lexicon (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 15, 'In generation process') + }) + await this.stt.generate_arpa().then(async () => { + debug(`done generate_arpa (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 20, 'In generation process') + }) + await this.stt.prepare_lang().then(async () => { + debug(`done prepare_lang (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 60, 'In generation process') + }) + this.stt.generate_main_and_entities_HCLG(res.acmodelId) + await this.stt.check_previous_HCLG_creation().then(async () => { + debug(`done generate_main_and_entities_HCLG (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 90, 'In generation process') + }) + await this.stt.generate_final_HCLG(res.modelId).then(async () => { + debug(`done generate_final_HCLG (${this.stt.tmplmpath})`) + await db.generationState(res.modelId, 100, 'In generation process') + }) + this.stt.removeTmpFolder() + + let data = {} + data.isGenerated = 1 + data.updateState = 0 + data.isDirty = 0 + data.oov = this.stt.oov + data.updateStatus = `Language model is successfully generated` + await db.updateModel(res.modelId, data) + this.emit('serviceReload', res.modelId) + } catch (err) { + this.stt.removeTmpFolder() + await db.generationState(res.modelId, -1, `ERROR: Not generated. ${err}`) + } + } +} + +module.exports = app => new LinSTT(app) \ No newline at end of file diff --git a/platform/stt-service-manager/components/ServiceManager/controllers/eventsFrom/WebServer.js b/platform/stt-service-manager/components/ServiceManager/controllers/eventsFrom/WebServer.js new file mode 100644 index 0000000..a28484a --- /dev/null +++ b/platform/stt-service-manager/components/ServiceManager/controllers/eventsFrom/WebServer.js @@ -0,0 +1,178 @@ +const debug = require('debug')(`app:servicemanager:eventsFrom:WebServer`) + +// this is bound to the component +module.exports = function () { + if (!this.app.components['WebServer']) return + + this.app.components['WebServer'].on('createService', async (cb, payload) => { + /** + * Create a service by its "serviceId" + * @param {Object} payload: {serviceId, replicas, tag} + * @returns {Object} + */ + try { + const res = await this.db.service.findService(payload.serviceId) + if (res) throw `Service '${payload.serviceId}' exists` + if (payload.replicas == undefined) throw 'Undefined field \'replicas\' (required)' + if (payload.replicas < 1) throw '\'replicas\' must be greater or equal to 1' + if (payload.tag == undefined) throw 'Undefined field \'tag\' (required)' + if (!this.verifTag(payload.tag)) throw `Unrecognized \'tag\'. Supported tags are: ${this.tag}` + if (payload.languageModel == undefined) throw 'Undefined field \'languageModel\' (required)' + const lmodel = await this.db.lm.findModel(payload.languageModel) + if (!lmodel) throw `Language Model '${payload.languageModel}' does not exist` + + if (payload.externalAccess == undefined) throw 'Undefined field \'externalAccess\' (required)' + if (payload.externalAccess.toLowerCase() != "yes" && payload.externalAccess.toLowerCase() != "no") throw 'Unrecognized \'externalAccess\'. Supported values are: yes|no' + + let externalAccess = 0 + if (payload.externalAccess.toLowerCase() == "yes") + externalAccess = 1 + + + const request = { + serviceId: payload.serviceId, + tag: payload.tag, + replicas: parseInt(payload.replicas), + LModelId: lmodel.modelId, + AModelId: lmodel.acmodelId, + externalAccess: externalAccess, + lang: lmodel.lang + } + await this.db.service.createService(request) + return cb({ bool: true, msg: `Service '${payload.serviceId}' is successfully created` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('updateService', async (cb, payload) => { + /** + * Update a service by its "serviceId" + * @param {Object} payload: {serviceId, replicas, tag} + * @returns {Object} + */ + try { + let update = {} + const service = await this.db.service.findService(payload.serviceId) + if (!service) throw `Service '${payload.serviceId}' does not exist` + if (service.isOn) throw `Service '${payload.serviceId}' is running` + + if (payload.replicas != undefined) { + if (payload.replicas < 1) throw '\'replicas\' must be greater or equal to 1' + update.replicas = parseInt(payload.replicas) + } + if (payload.tag != undefined) { + if (!this.verifTag(payload.tag)) throw `Unrecognized 'tag'. Supported tags are: ${this.tag}` + update.tag = payload.tag + } + if (payload.languageModel != undefined) { + const lmodel = await this.db.lm.findModel(payload.languageModel) + if (!lmodel) throw `Language Model '${payload.languageModel}' does not exist` + update.LModelId = lmodel.modelId + update.AModelId = lmodel.acmodelId + } + + if (payload.externalAccess != undefined) { + if (payload.externalAccess.toLowerCase() != "yes" && payload.externalAccess.toLowerCase() != "no") throw 'Unrecognized \'externalAccess\'. Supported values are: yes|no' + update.externalAccess = 0 + if (payload.externalAccess.toLowerCase() == "yes") + update.externalAccess = 1 + } + + await this.db.service.updateService(payload.serviceId, update) + return cb({ bool: true, msg: `Service '${payload.serviceId}' is successfully updated` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('deleteService', async (cb, serviceId) => { + /** + * Remove a service by its "serviceId" + * @param serviceId + * @returns {Object} + */ + try { + const service = await this.db.service.findService(serviceId) + if (!service) throw `Service '${serviceId}' does not exist` + if (service.isOn) throw `Service '${serviceId}' is running` + await this.db.service.deleteService(serviceId) + return cb({ bool: true, msg: `Service '${serviceId}' is successfully removed` }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('getService', async (cb, serviceId) => { + /** + * Find a service by its "serviceId" + * @param serviceId + * @returns {Object} + */ + try { + const res = await this.db.service.findService(serviceId) + if (!res) throw `Service '${serviceId}' does not exist` + return cb({ bool: true, msg: res }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('getReplicasService', async (cb, serviceId) => { + /** + * get number of replicas for a giving service + * @param serviceId + * @returns {Object} + */ + try { + const service = await this.db.service.findService(serviceId) + if (!service) throw `Service '${serviceId}' does not exist` + return cb({ bool: true, msg: service.replicas }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('getModeService', async (cb, serviceId) => { + /** + * get number of replicas for a giving service + * @param serviceId + * @returns {Object} + */ + try { + const service = await this.db.service.findService(serviceId) + if (!service) throw `Service '${serviceId}' does not exist` + return cb({ bool: true, msg: service.tag }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('getServices', async (cb) => { + /** + * Find all created services + * @param None + * @returns {Object} + */ + try { + const res = await this.db.service.findServices() + return cb({ bool: true, msg: res }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) + + this.app.components['WebServer'].on('getRunningServices', async (cb) => { + /** + * Find running docker services + * @param None + * @returns {Object} + */ + try { + const res = await this.db.service.findServices({ isOn: 1 }) + return cb({ bool: true, msg: res }) + } catch (err) { + return cb({ bool: false, msg: err }) + } + }) +} diff --git a/platform/stt-service-manager/components/ServiceManager/index.js b/platform/stt-service-manager/components/ServiceManager/index.js new file mode 100644 index 0000000..232a707 --- /dev/null +++ b/platform/stt-service-manager/components/ServiceManager/index.js @@ -0,0 +1,24 @@ +const Component = require(`../component.js`) +const debug = require('debug')(`app:servicemanager`) +const service = require(`${process.cwd()}/models/models/ServiceUpdates`) +const lm = require(`${process.cwd()}/models/models/LMUpdates`) +const am = require(`${process.cwd()}/models/models/AMUpdates`) + +class ServiceManager extends Component { + constructor(app) { + super(app) + this.id = this.constructor.name + this.app = app + this.db = { service: service, lm: lm, am: am } + this.tag = ['offline', 'online'] + return this.init() + } + + verifTag(tag) { + if (this.tag.indexOf(tag) != -1) return 1 + else return 0 + } + +} + +module.exports = app => new ServiceManager(app) diff --git a/platform/stt-service-manager/components/WebServer/controllers/.gitkeep b/platform/stt-service-manager/components/WebServer/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/platform/stt-service-manager/components/WebServer/index.js b/platform/stt-service-manager/components/WebServer/index.js new file mode 100644 index 0000000..16bcd8a --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/index.js @@ -0,0 +1,75 @@ +const Component = require(`../component.js`) +const path = require("path") +const debug = require('debug')(`app:webserver`) +const express = require('express') +const Session = require('express-session') +const bodyParser = require('body-parser') +const cookieParser = require('cookie-parser') +const swaggerUi = require('swagger-ui-express'); +const YAML = require('yamljs'); +const swaggerDocument = YAML.load(process.env.SWAGGER_PATH); + +/* +const CORS = require('cors') +const whitelistDomains = process.env.WHITELIST_DOMAINS.split(',') +const corsOptions = { + origin: function (origin, callback) { + if (!origin || whitelistDomains.indexOf(origin) !== -1) { + callback(null, true) + } else { + callback(new Error('Not allowed by CORS')) + } + } +} +*/ + +class WebServer extends Component { + constructor(app) { + super(app) + this.id = this.constructor.name + this.app = app + this.express = express() + //this.express.use(CORS(corsOptions)) + this.express.set('etag', false) + this.express.set('trust proxy', true) + this.express.use(bodyParser.json()) + this.express.use(bodyParser.urlencoded({ + extended: true + })) + this.express.use(cookieParser()) + + let sessionConfig = { + resave: false, + saveUninitialized: true, + secret: 'supersecret', + cookie: { + secure: false, + maxAge: 604800 // 7 days + } + } + this.session = Session(sessionConfig) + this.express.use(this.session) + this.httpServer = this.express.listen(process.env.WEBSERVER_HTTP_PORT, "0.0.0.0", (err) => { + debug(` WebServer listening on : ${process.env.WEBSERVER_HTTP_PORT}`) + if (err) throw (err) + }) + + require('./routes/router.js')(this) // Loads all defined routes + this.express.use('/api-doc', function(req, res, next){ debug('swagger API'); next()}, swaggerUi.serve, swaggerUi.setup(swaggerDocument)) + this.express.use((req, res, next) => { + res.status(404) + res.end() + }) + + + this.express.use((err, req, res, next) => { + console.error(err) + res.status(500) + res.end() + }) + + return this.init() + } +} + +module.exports = app => new WebServer(app) \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/middlewares/index.js b/platform/stt-service-manager/components/WebServer/middlewares/index.js new file mode 100644 index 0000000..240843d --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/middlewares/index.js @@ -0,0 +1,24 @@ +const debug = require('debug')('app:webserver:middlewares') + +function logger(req, res, next) { + debug(`[${Date.now()}] new user entry on ${req.url}`) + next() +} + +function checkAuth(req, res, next) { + // gotta check session here + next() +} + +function answer(out, res) { + res.json({ + status: out.bool ? 'success' : 'error', + data: out.msg + }) +} + +module.exports = { + answer, + checkAuth, + logger +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/healthcheck.js b/platform/stt-service-manager/components/WebServer/routes/healthcheck.js new file mode 100644 index 0000000..61528bd --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/healthcheck.js @@ -0,0 +1,18 @@ +module.exports = (webserver) => { + return [{ + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + webserver.emit("getServices", (ans) => { + //test if the connection with mongo is always maintained + if(ans.bool) res.status(200).end() + else { + console.error(`ERROR: ${ans.msg}`) + res.status(500).end() + } + }) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/modelManager/amodel.js b/platform/stt-service-manager/components/WebServer/routes/modelManager/amodel.js new file mode 100644 index 0000000..a09fc7e --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/modelManager/amodel.js @@ -0,0 +1,47 @@ +const debug = require('debug')('app:router:amodel') +const multer = require('multer') +const form = multer({ dest: process.env.TEMP_FILE_PATH }).single('file') +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver) => { + return [{ + path: '/', + method: 'post', + requireAuth: false, + controller: + [function (req, res, next) { + form(req, res, function (err) { + if (err instanceof multer.MulterError) { res.status(400); res.json({ status: err }) } + else next() + }) + }, (req, res, next) => { + req.body.modelId = req.params.modelId + req.body.file = req.file + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("createAModel", (ans) => { answer(ans, res) }, req.body) + }] + }, + { + path: '/', + method: 'delete', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("deleteAModel", (ans) => { answer(ans, res) }, req.params.modelId) + } + }, + { + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getAModel", (ans) => { answer(ans, res) }, req.params.modelId) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/modelManager/amodels.js b/platform/stt-service-manager/components/WebServer/routes/modelManager/amodels.js new file mode 100644 index 0000000..0f89da5 --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/modelManager/amodels.js @@ -0,0 +1,19 @@ +const debug = require('debug')('app:router:amodels') + +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver) => { + return [{ + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getAModels", (ans) => { answer(ans, res) }) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/modelManager/element.js b/platform/stt-service-manager/components/WebServer/routes/modelManager/element.js new file mode 100644 index 0000000..8d52e2e --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/modelManager/element.js @@ -0,0 +1,91 @@ +const debug = require('debug')('app:router:element') +const multer = require('multer') +const uploads = multer({ dest: process.env.TEMP_FILE_PATH }).single('file') +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver,type) => { + return [{ + path: '/', + method: 'post', + requireAuth: false, + controller: + [function (req, res, next) { + uploads(req, res, function (err) { + if (err instanceof multer.MulterError) { res.status(400); res.json({ status: err }) } + else next() + }) + }, + (req, res, next) => { + const data = {} + data.content = req.body + data.modelId = req.params.modelId + data.name = req.params.name + data.file = req.file + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit(`createType`, (ans) => { answer(ans, res) }, data, type) + }] + }, + { + path: '/', + method: 'delete', + requireAuth: false, + controller: (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit(`deleteType`, (ans) => { answer(ans, res) }, req.params, type) + } + }, + { + path: '/', + method: 'put', + requireAuth: false, + controller: + [function (req, res, next) { + uploads(req, res, function (err) { + if (err instanceof multer.MulterError) { res.status(400); res.json({ status: err }) } + else next() + }) + }, + (req, res, next) => { + const data = {} + data.content = req.body + data.modelId = req.params.modelId + data.name = req.params.name + data.file = req.file + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit(`updateType`, (ans) => { answer(ans, res) }, data, type, 'put') + }] + }, + { + path: '/', + method: 'patch', + requireAuth: false, + controller: + [function (req, res, next) { + uploads(req, res, function (err) { + if (err instanceof multer.MulterError) { res.status(400); res.json({ status: err }) } + else next() + }) + }, + (req, res, next) => { + const data = {} + data.content = req.body + data.modelId = req.params.modelId + data.name = req.params.name + data.file = req.file + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit(`updateType`, (ans) => { answer(ans, res) }, data, type, 'patch') + }] + }, + { + path: '/', + method: 'get', + requireAuth: false, + controller: (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit(`getType`, (ans) => { answer(ans, res) }, req.params, type) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/modelManager/elements.js b/platform/stt-service-manager/components/WebServer/routes/modelManager/elements.js new file mode 100644 index 0000000..bd39222 --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/modelManager/elements.js @@ -0,0 +1,19 @@ +const debug = require('debug')('app:router:elements') + +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver,type) => { + return [{ + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit(`getTypes`, (ans) => { answer(ans, res) }, req.params.modelId, type) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/modelManager/lmodel.js b/platform/stt-service-manager/components/WebServer/routes/modelManager/lmodel.js new file mode 100644 index 0000000..8eab495 --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/modelManager/lmodel.js @@ -0,0 +1,72 @@ +const debug = require('debug')('app:router:lmodel') +const multer = require('multer') +const form = multer({ dest: process.env.TEMP_FILE_PATH }).single('file') + +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver) => { + return [{ + path: '/', + method: 'post', + requireAuth: false, + controller: + [function (req, res, next) { + form(req, res, function (err) { + if (err instanceof multer.MulterError) { res.status(400); res.json({ status: err }) } + else next() + }) + }, (req, res, next) => { + req.body.modelId = req.params.modelId + req.body.file = req.file + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("createLModel", (ans) => { answer(ans, res) }, req.body) + }] + }, + { + path: '/generate/graph', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + try { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("generateLModel", (ans) => { answer(ans, res) }, req.params.modelId) + } catch (error) { + console.error(error) + } + } + }, + { + path: '/', + method: 'delete', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("deleteLModel", (ans) => { answer(ans, res) }, req.params.modelId) + } + }, + { + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getLModel", (ans) => { answer(ans, res) }, req.params.modelId) + } + }, + { + path: '/:param', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getLModel", (ans) => { answer(ans, res) }, req.params.modelId, req.params.param) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/modelManager/lmodels.js b/platform/stt-service-manager/components/WebServer/routes/modelManager/lmodels.js new file mode 100644 index 0000000..7841283 --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/modelManager/lmodels.js @@ -0,0 +1,19 @@ +const debug = require('debug')('app:router:lmodels') + +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver) => { + return [{ + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['LinSTT']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getLModels", (ans) => { answer(ans, res) }) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/router.js b/platform/stt-service-manager/components/WebServer/routes/router.js new file mode 100644 index 0000000..ff5669b --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/router.js @@ -0,0 +1,41 @@ +const debug = require('debug')(`app:webserver:router`) +const path = require('path') +const middlewares = require(path.join(__dirname, "../middlewares")) +const ifHasElse = (condition, ifHas, otherwise) => { + return !condition ? otherwise() : ifHas() +} +class Router { + constructor(webServer) { + const routes = require('./routes.js')(webServer) + for (let level in routes) { + for (let path in routes[level]) { + const route = routes[level][path] + const method = route.method + if (route.requireAuth) { + webServer.express[method]( + level + route.path, + middlewares.logger, + middlewares.checkAuth, + ifHasElse( + Array.isArray(route.controller), + () => Object.values(route.controller), + () => route.controller + ) + ) + } else { + webServer.express[method]( + level + route.path, + middlewares.logger, + ifHasElse( + Array.isArray(route.controller), + () => Object.values(route.controller), + () => route.controller + ) + ) + } + } + } + } +} + +module.exports = webServer => new Router(webServer) \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/routes.js b/platform/stt-service-manager/components/WebServer/routes/routes.js new file mode 100644 index 0000000..5f84b04 --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/routes.js @@ -0,0 +1,17 @@ +const debug = require('debug')('app:webserver:routes') + +module.exports = webServer => { + return { + '/': require(`./healthcheck`)(webServer), + '/service/:serviceId' : require('./serviceManager/service')(webServer), + '/services' : require('./serviceManager/services')(webServer), + '/acmodel/:modelId' : require('./modelManager/amodel')(webServer), + '/acmodels' : require('./modelManager/amodels')(webServer), + '/langmodel/:modelId' : require('./modelManager/lmodel')(webServer), + '/langmodels' : require('./modelManager/lmodels')(webServer), + '/langmodel/:modelId/entity/:name' : require('./modelManager/element')(webServer,'entities'), + '/langmodel/:modelId/entities' : require('./modelManager/elements')(webServer,'entities'), + '/langmodel/:modelId/intent/:name' : require('./modelManager/element')(webServer,'intents'), + '/langmodel/:modelId/intents' : require('./modelManager/elements')(webServer,'intents'), + } +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/serviceManager/service.js b/platform/stt-service-manager/components/WebServer/routes/serviceManager/service.js new file mode 100644 index 0000000..2ead74e --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/serviceManager/service.js @@ -0,0 +1,114 @@ +const debug = require('debug')('app:router:servicemanager') +const multer = require('multer') +const form = multer({ dest: process.env.TEMP_FILE_PATH }).none() +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver) => { + return [{ + path: '/', + method: 'post', + requireAuth: false, + controller: + [function (req, res, next) { + form(req, res, function (err) { + if (err instanceof multer.MulterError) { res.status(400); res.json({ status: err }) } + else next() + }) + }, async (req, res, next) => { + req.body.serviceId = req.params.serviceId + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("createService", (ans) => { answer(ans, res) }, req.body) + }] + }, + { + path: '/', + method: 'put', + requireAuth: false, + controller: + [function (req, res, next) { + form(req, res, function (err) { + if (err instanceof multer.MulterError) { res.status(400); res.json({ status: err }) } + else next() + }) + }, (req, res, next) => { + req.body.serviceId = req.params.serviceId + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("updateService", (ans) => { answer(ans, res) }, req.body) + }] + }, + { + path: '/', + method: 'delete', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("deleteService", (ans) => { answer(ans, res) }, req.params.serviceId) + } + }, + { + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getService", (ans) => { answer(ans, res) }, req.params.serviceId) + } + }, + { + path: '/replicas', + method: 'get', + requireAuth: false, + controller: + async (req, res, next) => { + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getReplicasService", (ans) => { answer(ans, res) }, req.params.serviceId) + } + }, + { + path: '/mode', + method: 'get', + requireAuth: false, + controller: + async (req, res, next) => { + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getModeService", (ans) => { answer(ans, res) }, req.params.serviceId) + } + }, + + { + path: '/start', + method: 'post', + requireAuth: false, + controller: + (req, res, next) => { + req.body.serviceId = req.params.serviceId + if (! webserver.app.components['ClusterManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("startService", (ans) => { answer(ans, res) }, req.body) + } + }, + { + path: '/scale/:replicas', + method: 'post', + requireAuth: false, + controller: + async (req, res, next) => { + if (! webserver.app.components['ClusterManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("scaleService", (ans) => { answer(ans, res) }, req.params) + } + }, + { + path: '/stop', + method: 'post', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['ClusterManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("stopService", (ans) => { answer(ans, res) }, req.params.serviceId) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/WebServer/routes/serviceManager/services.js b/platform/stt-service-manager/components/WebServer/routes/serviceManager/services.js new file mode 100644 index 0000000..d35af2c --- /dev/null +++ b/platform/stt-service-manager/components/WebServer/routes/serviceManager/services.js @@ -0,0 +1,30 @@ +const debug = require('debug')('app:router:servicemanager') +const multer = require('multer') +const form = multer({ dest: process.env.TEMP_FILE_PATH }).none() +const middlewares = require(`${process.cwd()}/components/WebServer/middlewares/index.js`) +const answer = (ans, req) => { + middlewares.answer(ans, req) +} + +module.exports = (webserver) => { + return [{ + path: '/', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getServices", (ans) => { answer(ans, res) }) + } + }, + { + path: '/running', + method: 'get', + requireAuth: false, + controller: + (req, res, next) => { + if (! webserver.app.components['ServiceManager']) answer({bool: false, msg: 'Component not found'}, res) + webserver.emit("getRunningServices", (ans) => { answer(ans, res) }) + } + }] +} \ No newline at end of file diff --git a/platform/stt-service-manager/components/component.js b/platform/stt-service-manager/components/component.js new file mode 100644 index 0000000..0c3f479 --- /dev/null +++ b/platform/stt-service-manager/components/component.js @@ -0,0 +1,55 @@ +const fsPromises = require('fs').promises +const path = require('path') +const EventEmitter = require('eventemitter3') +const { componentMissingError } = require(`${process.cwd()}/lib/customErrors.js`) + +class Component extends EventEmitter { + constructor(app, ...requiredComponents) { + super() + let missingComponents = [] + requiredComponents.every((component) => { + if (app.components.hasOwnProperty(component)) { + return true + } else { + return missingComponents.push(component) + } + }) + if (missingComponents.length > 0) { + throw new componentMissingError(missingComponents) + } + + } + + // recursively requires .js files by crawling filesystem for controllersDir ( shall be ../controllers/) + // for each required file, calls exported function by binding "this" Component context + async loadEventControllers(controllersDir) { + try { + const currentDir = await fsPromises.readdir(controllersDir) + for (let item of currentDir) { + let itemPath = path.join(controllersDir, item) + let stat = await fsPromises.lstat(itemPath) + if (stat.isDirectory()) { + await this.loadEventControllers(itemPath) + } else if (item.toLocaleLowerCase().indexOf('.js')) { + let controller = require(itemPath) + if (typeof controller === "function") controller.call(this) + } + } + } catch (e) { + throw e + } + } + + async init() { + return new Promise(async (resolve, reject) => { + try { + await this.loadEventControllers(path.join(__dirname, this.constructor.name, "/controllers")) + resolve(this) + } catch (e) { + reject(e) + } + }) + } +} + +module.exports = Component \ No newline at end of file diff --git a/platform/stt-service-manager/config.js b/platform/stt-service-manager/config.js new file mode 100644 index 0000000..0ee0b19 --- /dev/null +++ b/platform/stt-service-manager/config.js @@ -0,0 +1,96 @@ +const debug = require('debug')('app:config') +const dotenv = require('dotenv') +const fs = require('fs') + +function ifHasNotThrow(element, error) { + if (!element) throw error + return element +} + +function ifHas(element, defaultValue) { + if (!element) return defaultValue + return element +} + +function configureDefaults() { + try { + dotenv.config() // loads process.env from .env file (if not specified by the system) + const envdefault = dotenv.parse(fs.readFileSync('.defaultparam')) // default usable values + process.env.COMPONENTS = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_COMPONENTS, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_COMPONENTS) + process.env.WEBSERVER_HTTP_PORT = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_HTTP_PORT, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_HTTP_PORT) + process.env.SWAGGER_PATH = ifHasNotThrow(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SWAGGER_PATH, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_SWAGGER_PATH) + process.env.SAVE_MODELS_PATH = ifHas(process.env.SAVE_MODELS_PATH, envdefault.SAVE_MODELS_PATH) + process.env.LM_FOLDER_NAME = ifHas(process.env.LM_FOLDER_NAME, envdefault.LM_FOLDER_NAME) + process.env.LM_PATH = `${process.env.SAVE_MODELS_PATH}/${process.env.LM_FOLDER_NAME}` + process.env.AM_FOLDER_NAME = ifHas(process.env.AM_FOLDER_NAME, envdefault.AM_FOLDER_NAME) + process.env.AM_PATH = `${process.env.SAVE_MODELS_PATH}/${process.env.AM_FOLDER_NAME}` + process.env.TEMP_FOLDER_NAME = ifHas(process.env.TEMP_FOLDER_NAME, envdefault.TEMP_FOLDER_NAME) + process.env.TEMP_FILE_PATH = `${process.env.SAVE_MODELS_PATH}/${process.env.TEMP_FOLDER_NAME}` + process.env.FILESYSTEM = ifHasNotThrow(process.env.LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY, 'No LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY found. Please edit ".env" file') + + //Dictionary parameters + process.env.DICT_DELIMITER = ifHas(process.env.DICT_DELIMITER, envdefault.DICT_DELIMITER) + process.env.LANGUAGE = ifHas(process.env.LANGUAGE, envdefault.LANGUAGE) + process.env.NGRAM = ifHas(process.env.NGRAM, envdefault.NGRAM) + + //Cluster Manager + process.env.CLUSTER_TYPE = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_CLUSTER_MANAGER, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_CLUSTER_MANAGER) + //Ingress Controller + process.env.INGRESS_CONTROLLER = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_INGRESS_CONTROLLER, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_INGRESS_CONTROLLER) + //LinSTT Toolkit + process.env.LINSTT_SYS = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_LINSTT_TOOLKIT, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_LINSTT_TOOLKIT) + + //DOCKER settings + process.env.DOCKER_SOCKET_PATH = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_DOCKER_SOCKET, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_DOCKER_SOCKET) + process.env.CHECK_SERVICE_TIMEOUT = ifHas(process.env.CHECK_SERVICE_TIMEOUT, envdefault.CHECK_SERVICE_TIMEOUT) + + //NGINX + process.env.NGINX_CONF_PATH = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_NGINX_CONF, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_NGINX_CONF) + process.env.NGINX_SERVICE_ID = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_NGINX_HOST, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_NGINX_HOST) + + //MongoDB + process.env.MONGODB_HOST = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_HOST, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_HOST) + process.env.MONGODB_PORT = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PORT, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PORT) + process.env.MONGODB_DBNAME_SMANAGER = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_DBNAME, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_DBNAME) + process.env.MONGODB_REQUIRE_LOGIN = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_REQUIRE_LOGIN, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_REQUIRE_LOGIN) + process.env.MONGODB_USER = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_USER, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_USER) + process.env.MONGODB_PSWD = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PSWD, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PSWD) + + //LINSTT + process.env.LINSTT_OFFLINE_IMAGE = ifHas(process.env.LINTO_STACK_LINSTT_OFFLINE_IMAGE, envdefault.LINTO_STACK_LINSTT_OFFLINE_IMAGE) + process.env.LINSTT_STREAMING_IMAGE = ifHas(process.env.LINTO_STACK_LINSTT_STREAMING_IMAGE, envdefault.LINTO_STACK_LINSTT_STREAMING_IMAGE) + process.env.LINSTT_NETWORK = ifHas(process.env.LINTO_STACK_LINSTT_NETWORK, envdefault.LINTO_STACK_LINSTT_NETWORK) + process.env.LINSTT_PREFIX = ifHas(process.env.LINTO_STACK_LINSTT_PREFIX, envdefault.LINTO_STACK_LINSTT_PREFIX) + process.env.LINSTT_PREFIX = process.env.LINSTT_PREFIX.replace(/\//g,"") + process.env.LINSTT_IMAGE_TAG = ifHas(process.env.LINTO_STACK_IMAGE_TAG, envdefault.LINTO_STACK_IMAGE_TAG) + process.env.LINSTT_STACK_NAME = ifHas(process.env.LINTO_STACK_LINSTT_NAME, envdefault.LINTO_STACK_LINSTT_NAME) + + process.env.SPEAKER_DIARIZATION_HOST=ifHas(process.env.LINTO_STACK_SPEAKER_DIARIZATION_HOST, '') + process.env.SPEAKER_DIARIZATION_PORT=ifHas(process.env.LINTO_STACK_SPEAKER_DIARIZATION_PORT, 80) + process.env.PUCTUATION_HOST=ifHas(process.env.LINTO_STACK_PUCTUATION_HOST, '') + process.env.PUCTUATION_PORT=ifHas(process.env.LINTO_STACK_PUCTUATION_PORT, 80) + process.env.PUCTUATION_ROUTE=ifHas(process.env.LINTO_STACK_PUCTUATION_ROUTE, '') + + //parameter used when traefik is activated + process.env.LINTO_STACK_DOMAIN = ifHas(process.env.LINTO_STACK_DOMAIN, envdefault.LINTO_STACK_DOMAIN) + + //create the AM folder if it does not exist + if (!fs.existsSync(process.env.AM_PATH)) + fs.mkdirSync(process.env.AM_PATH) + + //create the LM folder if it does not exist + if (!fs.existsSync(process.env.LM_PATH)) + fs.mkdirSync(process.env.LM_PATH) + + //create the TMP folder if it does not exist + if (!fs.existsSync(process.env.TEMP_FILE_PATH)) + fs.mkdirSync(process.env.TEMP_FILE_PATH) + + //process.env.WHITELIST_DOMAINS = ifHasNotThrow(process.env.WHITELIST_DOMAINS, 'No whitelist found. Please edit ".env" file') + //process.env.COMPONENTS = ifHasNotThrow(process.env.COMPONENTS, Error("No COMPONENTS env_var specified")) + } catch (e) { + console.error(debug.namespace, e) + process.exit(1) + } +} +module.exports = configureDefaults() diff --git a/platform/stt-service-manager/config/nginx.conf b/platform/stt-service-manager/config/nginx.conf new file mode 100644 index 0000000..f310f61 --- /dev/null +++ b/platform/stt-service-manager/config/nginx.conf @@ -0,0 +1,4 @@ +server { + server_name ''; + port_in_redirect off; +} diff --git a/config/servicemanager/init.js b/platform/stt-service-manager/config/seed/init.js similarity index 100% rename from config/servicemanager/init.js rename to platform/stt-service-manager/config/seed/init.js diff --git a/platform/stt-service-manager/config/seed/user.js b/platform/stt-service-manager/config/seed/user.js new file mode 100644 index 0000000..5a5702a --- /dev/null +++ b/platform/stt-service-manager/config/seed/user.js @@ -0,0 +1,8 @@ +db.createUser({ + user: "root", + pwd: "root", + roles: [{ + role: "readWrite", + db: "linSTTAdmin" + }] +}) diff --git a/platform/stt-service-manager/config/swagger.yml b/platform/stt-service-manager/config/swagger.yml new file mode 100644 index 0000000..990272b --- /dev/null +++ b/platform/stt-service-manager/config/swagger.yml @@ -0,0 +1,892 @@ +swagger: "2.0" + +info: + version: 1.0.0 + title: STT Service Manager + description: A simple way to use the STT Service Manager APIs + +schemes: + - http +host: localhost:8000 +basePath: / + +definitions: + Entity: + title: Entity items + description: An example of to create an array of items + items: + type: string + type: array + example: ['item1', 'item2', 'item3'] + Intent: + title: Intent items + description: An example of to create an array of commands + items: + type: string + type: array + example: ['cmd1', 'cmd2', 'cmd3'] + Data: + title: Language Model data + description: An example of how to create the data + type: object + properties: + lang: + type: string + acousticModel: + type: string + type: + type: string + data: + type: object + properties: + intents: + type: array + items: + type: object + properties: + name: + type: string + items: + type: array + items: + type: string + entities: + type: array + items: + type: object + properties: + name: + type: string + items: + type: array + items: + type: string + +parameters: + service: + serviceId-req: + name: "serviceId" + in: "path" + description: "Service Name: must be unique and must finish by a character or number - Allowed characters: 'a-z', 'A-Z', '0-9', '_', and '-'" + required: true + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]*[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9]$' + + api-access-req: + name: "externalAccess" + in: "formData" + description: "Allow external access to the service API" + required: true + type: "string" + enum: [ "", "yes", "no" ] + + replicas: + name: "replicas" + in: formData + description: "Number of replicas" + type: integer + minimum: 1 + default: 1 + replicas-req: + name: "replicas" + in: formData + description: "Number of replicas" + required: true + type: integer + minimum: 1 + default: 1 + replicas-path-req: + name: "replicas" + in: path + description: "Number of replicas" + required: true + type: integer + minimum: 1 + default: 1 + tag: + name: "tag" + in: "formData" + description: "Service transcription mode" + type: "string" + enum: [ "", "offline", "online" ] + tag-req: + name: "tag" + in: "formData" + description: "Service transcription mode" + required: true + type: "string" + enum: [ "", "offline", "online" ] + + LM: + name: "languageModel" + in: formData + description: "Language Model 'modelId' that will be use by the current service" + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + LM-req: + name: "languageModel" + in: formData + description: "Language Model 'modelId' that will be used by the current service" + required: true + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + + model: + modelId-req: + name: "modelId" + in: "path" + description: "Model Name: must be unique - Allowed characters: 'a-z', 'A-Z', '0-9', '_', and '-'" + required: true + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + + modelID-req: + name: "modelID" + in: "path" + description: "Model Name: must be unique - Allowed characters: 'a-z', 'A-Z', '0-9', '_', and '-'" + required: true + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + + file: + name: "file" + in: "formData" + description: "Local Reference - Allowed file format: zip, tar.gz" + type: "file" + + file-txt-req: + name: "file" + in: "formData" + description: "Local Reference - Text file required" + required: true + type: "file" + + link: + name: "link" + in: "formData" + description: "URL Reference - Allowed file format: zip, tar.gz" + type: "string" + + lang: + name: "lang" + in: "formData" + description: "Transcription Language - ISO Language Code Table: ar-SA, de-DE, en-GB, en-US, es-ES, fr-FR, ..." + type: "string" + + lang-req: + name: "lang" + in: "formData" + description: "Transcription Language - ISO Language Code Table: ar-SA, de-DE, en-GB, en-US, es-ES, fr-FR, ..." + required: true + type: "string" + + description: + name: "desc" + in: "formData" + description: "Description" + type: "string" + + data: + name: "data" + in: "body" + description: "Language model content - A JSON object" + required: false + schema: + $ref: '#/definitions/Data' + + lmodel: + name: "lmodelId" + in: "formData" + description: "'modelId' of an existing Language Model" + required: false + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + + amodel: + name: "acousticModel" + in: "formData" + description: "'modelId' of an existing Acoustic Model" + required: false + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + + type: + name: "type" + in: "formData" + description: "Language model type: large vocabulary (lvcsr) or command (cmd)" + required: true + type: "string" + enum: [ "", "lvcsr", "cmd" ] + + entity-req: + name: "name" + in: "path" + description: "Entity Name: must be unique - Allowed characters: 'a-z', 'A-Z', '0-9', '_', and '-'" + required: true + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + + intent-req: + name: "name" + in: "path" + description: "Intent Name: must be unique - Allowed characters: 'a-z', 'A-Z', '0-9', '_', and '-'" + required: true + type: "string" + pattern: '[a-zA-ZàâçèéêîôùûÀÂÇÈÉÊÎÔÙÛ0-9_\-]+$' + + entity-data-req: + name: "entity" + in: "body" + description: "Entity items - A JSON object" + required: true + schema: + $ref: '#/definitions/Entity' + + intent-data-req: + name: "intent" + in: "body" + description: "Intent items - A JSON object" + required: true + schema: + $ref: '#/definitions/Intent' + +paths: + /service/{serviceId}: + post: + tags: + - "Service Manager APIs" + summary: Create the service by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + - $ref: '#/parameters/service/replicas-req' + - $ref: '#/parameters/service/tag-req' + - $ref: '#/parameters/service/LM-req' + - $ref: '#/parameters/service/api-access-req' + responses: + 200: + description: success + 400: + description: error + put: + tags: + - "Service Manager APIs" + summary: Update the service by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + - $ref: '#/parameters/service/replicas' + - $ref: '#/parameters/service/tag' + - $ref: '#/parameters/service/LM' + - $ref: '#/parameters/service/api-access-req' + responses: + 200: + description: success + 400: + description: error + delete: + tags: + - "Service Manager APIs" + summary: delete the service by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + responses: + 200: + description: success + 400: + description: error + get: + tags: + - "Service Manager APIs" + summary: get the service information by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + responses: + 200: + description: success + 400: + description: error + /service/{serviceId}/replicas: + get: + tags: + - "Service Manager APIs" + summary: get the number of replicas by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + responses: + 200: + description: success + 400: + description: error + /service/{serviceId}/mode: + get: + tags: + - "Service Manager APIs" + summary: get the decoding mode by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + responses: + 200: + description: success + 400: + description: error + /services: + get: + tags: + - "Service Manager APIs" + summary: get all created services + produces: + - "application/json" + responses: + 200: + description: success + + + /service/{serviceId}/start: + post: + tags: + - "Service Runtime APIs" + summary: start service by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + responses: + 200: + description: success + 400: + description: error + /service/{serviceId}/stop: + post: + tags: + - "Service Runtime APIs" + summary: stop service by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + responses: + 200: + description: success + 400: + description: error + /service/{serviceId}/scale/{replicas}: + post: + tags: + - "Service Runtime APIs" + summary: start service by serviceId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/service/serviceId-req' + - $ref: '#/parameters/service/replicas-path-req' + responses: + 200: + description: success + 400: + description: error + /services/running: + get: + tags: + - "Service Runtime APIs" + summary: get all started services + produces: + - "application/json" + responses: + 200: + description: success + + + /acmodel/{modelId}: + post: + tags: + - "Acoustic Model Management" + summary: "Create the acoustic model by modelId" + description: | +

The 'file' and 'link' parameters can be used to upload the desired acoustic model.
+ One parameter should be specified. If both parameters are provided, the 'file' parameter will be considered.

+ consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/file' + - $ref: '#/parameters/model/link' + - $ref: '#/parameters/model/lang-req' + - $ref: '#/parameters/model/description' + responses: + 200: + description: success + 400: + description: error + delete: + tags: + - "Acoustic Model Management" + summary: Delete the acoustic model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + responses: + 200: + description: success + 400: + description: error + get: + tags: + - "Acoustic Model Management" + summary: get the acoustic model information by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + responses: + 200: + description: success + 400: + description: error + /acmodels: + get: + tags: + - "Acoustic Model Management" + summary: get all created acoustic models + produces: + - "application/json" + responses: + 200: + description: success + + + /langmodel/{modelId}: + post: + tags: + - "Language Model Management" + summary: Create the language model by modelId + description: | +
To create the model, one of the following parameters should be used:
+
    +
  • To upload a pretrained model, use the parameters 'file' or 'link' for local and remove references respecitvely.
  • +
  • To create a copy of an already created, use the parameter 'lmodelId' and put the model ID.
  • +
+
NB: If no parameter is specified, an empty language model will be created.
+

+
To specify the acoustic model that will be used with the current language model, one of the following parameters can be used:
+
    +
  • 'lang': the ISO language of existing acoustic models. It will select automatically the most recent model according to this passed language.
  • +
  • 'acousticModel': the modelId of an existing acoustic model.
  • +
+ consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/file' + - $ref: '#/parameters/model/link' + - $ref: '#/parameters/model/lmodel' + - $ref: '#/parameters/model/lang' + - $ref: '#/parameters/model/amodel' + - $ref: '#/parameters/model/type' + responses: + 200: + description: success + 400: + description: error + delete: + tags: + - "Language Model Management" + summary: Delete the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + responses: + 200: + description: success + 400: + description: error + get: + tags: + - "Language Model Management" + summary: get the language model information by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + responses: + 200: + description: success + 400: + description: error + /langmodel/{modelId}/generate/graph: + get: + tags: + - "Language Model Management" + summary: Generate the language model by modelId + description: | +
After executing this API, you can get the language model information using 'GET /langmodel/{modelId}' API and check the following parameters:
+
    +
  • 'updateState': the update percentage. If generation succeeded, the value will be 0 – -1, if not.
  • +
  • 'updateStatus': the update message.
  • +
+ + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + responses: + 200: + description: success + /langmodels: + get: + tags: + - "Language Model Management" + summary: get all created language models + produces: + - "application/json" + responses: + 200: + description: success + /langmodel/{modelID}: + post: + tags: + - "Language Model Management (JSON format)" + summary: Create the language model by modelId + description: | +
To specify the acoustic model that will be used with the current language model, one of the following parameters can be used:
+
    +
  • 'lang': the ISO language of existing acoustic models. It will select automatically the most recent model according to this passed language.
  • +
  • 'acousticModel': the modelId of an existing acoustic model.
  • +
+ consumes: + - "application/json" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelID-req' + - $ref: '#/parameters/model/data' + responses: + 200: + description: success + 400: + description: error + + + /langmodel/{modelId}/entity/{name}: + post: + tags: + - "Language Model 'Entity' Management" + summary: Add entity to the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/entity-req' + - $ref: '#/parameters/model/file-txt-req' + responses: + 200: + description: success + 400: + description: error + put: + tags: + - "Language Model 'Entity' Management" + summary: Reset entity of the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/entity-req' + - $ref: '#/parameters/model/file-txt-req' + responses: + 200: + description: success + 400: + description: error + patch: + tags: + - "Language Model 'Entity' Management" + summary: Update entity of the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/entity-req' + - $ref: '#/parameters/model/file-txt-req' + responses: + 200: + description: success + 400: + description: error + delete: + tags: + - "Language Model 'Entity' Management" + summary: delete entity of the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/entity-req' + responses: + 200: + description: success + 400: + description: error + get: + tags: + - "Language Model 'Entity' Management" + summary: get the entity information of the language model by modelId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/entity-req' + responses: + 200: + description: success + 400: + description: error + /langmodel/{modelId}/entities: + get: + tags: + - "Language Model 'Entity' Management" + summary: get all entities of the language model by modelId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + responses: + 200: + description: success + + /langmodel/{modelID}/entity/{name}: + post: + tags: + - "Language Model 'Entity' Management (JSON format)" + summary: Add entity to the language model by modelId + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelID-req' + - $ref: '#/parameters/model/entity-req' + - $ref: '#/parameters/model/entity-data-req' + responses: + 200: + description: success + 400: + description: error + put: + tags: + - "Language Model 'Entity' Management (JSON format)" + summary: Reset entity of the language model by modelId + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelID-req' + - $ref: '#/parameters/model/entity-req' + - $ref: '#/parameters/model/entity-data-req' + responses: + 200: + description: success + 400: + description: error + patch: + tags: + - "Language Model 'Entity' Management (JSON format)" + summary: Update entity of the language model by modelId + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelID-req' + - $ref: '#/parameters/model/entity-req' + - $ref: '#/parameters/model/entity-data-req' + responses: + 200: + description: success + 400: + description: error + + + + /langmodel/{modelId}/intent/{name}: + post: + tags: + - "Language Model 'Intent' Management" + summary: Add intent to the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/intent-req' + - $ref: '#/parameters/model/file-txt-req' + responses: + 200: + description: success + 400: + description: error + put: + tags: + - "Language Model 'Intent' Management" + summary: Reset intent of the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/intent-req' + - $ref: '#/parameters/model/file-txt-req' + responses: + 200: + description: success + 400: + description: error + patch: + tags: + - "Language Model 'Intent' Management" + summary: Update intent of the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/intent-req' + - $ref: '#/parameters/model/file-txt-req' + responses: + 200: + description: success + 400: + description: error + delete: + tags: + - "Language Model 'Intent' Management" + summary: delete intent of the language model by modelId + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/intent-req' + responses: + 200: + description: success + 400: + description: error + get: + tags: + - "Language Model 'Intent' Management" + summary: get the intent information of the language model by modelId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + - $ref: '#/parameters/model/intent-req' + responses: + 200: + description: success + 400: + description: error + /langmodel/{modelId}/intents: + get: + tags: + - "Language Model 'Intent' Management" + summary: get all intents of the language model by modelId + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelId-req' + responses: + 200: + description: success + + /langmodel/{modelID}/intent/{name}: + post: + tags: + - "Language Model 'Intent' Management (JSON format)" + summary: Add intent to the language model by modelId + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelID-req' + - $ref: '#/parameters/model/intent-req' + - $ref: '#/parameters/model/intent-data-req' + responses: + 200: + description: success + 400: + description: error + put: + tags: + - "Language Model 'Intent' Management (JSON format)" + summary: Reset intent of the language model by modelId + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelID-req' + - $ref: '#/parameters/model/intent-req' + - $ref: '#/parameters/model/intent-data-req' + responses: + 200: + description: success + 400: + description: error + patch: + tags: + - "Language Model 'Intent' Management (JSON format)" + summary: Update intent of the language model by modelId + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - $ref: '#/parameters/model/modelID-req' + - $ref: '#/parameters/model/intent-req' + - $ref: '#/parameters/model/intent-data-req' + responses: + 200: + description: success + 400: + description: error diff --git a/platform/stt-service-manager/docker-compose.yml b/platform/stt-service-manager/docker-compose.yml new file mode 100755 index 0000000..2907b31 --- /dev/null +++ b/platform/stt-service-manager/docker-compose.yml @@ -0,0 +1,55 @@ +version: '3.5' + +services: + + stt-service-manager: + image: lintoai/linto-platform-stt-server-manager:latest-unstable + depends_on: + - mongodb-stt-service-manager + volumes: + - ${LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY}:/opt/model + - ./config/nginx.conf:/opt/nginx/nginx.conf + - /var/run/docker.sock:/var/run/docker.sock + - /etc/localtime:/etc/localtime:ro + - ./config/swagger.yml:/opt/swagger.yml + ports: + - target: 80 + published: 8000 + deploy: + mode: replicated + replicas: 1 + placement: + constraints: + - node.role == manager + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + healthcheck: + interval: 15s + timeout: 10s + retries: 4 + start_period: 50s + env_file: .env + command: # Overrides CMD specified in dockerfile (none here, handled by entrypoint) + - --run-cmd=npm run start + environment: + LINTO_STACK_STT_SERVICE_MANAGER_SWAGGER_PATH: /opt/swagger.yml + networks: + - linto-net + + mongodb-stt-service-manager: + image: mongo:latest + volumes: + - ${LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY}/dbdata:/data/db + - ${LINTO_STACK_STT_SERVICE_MANAGER_DIRECTORY}/dbbackup:/data/backup + - ./config/seed:/docker-entrypoint-initdb.d + environment: + MONGO_INITDB_DATABASE: linSTTAdmin + networks: + - linto-net + +networks: + internal: + linto-net: + external: true diff --git a/platform/stt-service-manager/docker-entrypoint.sh b/platform/stt-service-manager/docker-entrypoint.sh new file mode 100755 index 0000000..efba50a --- /dev/null +++ b/platform/stt-service-manager/docker-entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e + +echo "Waiting mongo..." +./wait-for-it.sh $LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_HOST:$LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PORT --timeout=20 --strict -- echo " $LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_HOST:$LINTO_STACK_STT_SERVICE_MANAGER_MONGODB_PORT is up" + +if [ ${LINTO_STACK_STT_SERVICE_MANAGER_INGRESS_CONTROLLER} == "nginx" ]; then + echo "Waiting nginx..." + ./wait-for-it.sh $LINTO_STACK_STT_SERVICE_MANAGER_NGINX_HOST:80 --timeout=20 --strict -- echo " $LINTO_STACK_STT_SERVICE_MANAGER_NGINX_HOST:80 is up" +fi + +while [ "$1" != "" ]; do + case $1 in + --run-cmd) + if [ "$2" ]; then + script=$2 + shift + else + die 'ERROR: "--run-cmd" requires a non-empty option argument.' + fi + ;; + --run-cmd?*) + script=${1#*=} # Deletes everything up to "=" and assigns the remainder. + ;; + --run-cmd=) # Handle the case of an empty --run-cmd= + die 'ERROR: "--run-cmd" requires a non-empty option argument.' + ;; + *) + echo "ERROR: Bad argument provided \"$1\"" + exit 1 + ;; + esac + shift +done + +echo "RUNNING : $script" +cd /usr/src/app + +eval "$script" \ No newline at end of file diff --git a/platform/stt-service-manager/docker-healthcheck.js b/platform/stt-service-manager/docker-healthcheck.js new file mode 100755 index 0000000..9991e4e --- /dev/null +++ b/platform/stt-service-manager/docker-healthcheck.js @@ -0,0 +1,5 @@ +const fetch = require('node-fetch') + +fetch(`http://localhost:${process.env.LINTO_STACK_STT_SERVICE_MANAGER_HTTP_PORT}`).catch(err => { + throw err.message +}) \ No newline at end of file diff --git a/platform/stt-service-manager/lib/customErrors.js b/platform/stt-service-manager/lib/customErrors.js new file mode 100644 index 0000000..58a5ae6 --- /dev/null +++ b/platform/stt-service-manager/lib/customErrors.js @@ -0,0 +1,11 @@ +class componentMissingError extends Error { + constructor(missingComponents) { + super() + this.name = 'COMPONENT_MISSING'; + this.missingComponents = missingComponents + } +} + +module.exports = { + componentMissingError +} \ No newline at end of file diff --git a/platform/stt-service-manager/models/.gitkeep b/platform/stt-service-manager/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/platform/stt-service-manager/models/driver.js b/platform/stt-service-manager/models/driver.js new file mode 100644 index 0000000..7413f7f --- /dev/null +++ b/platform/stt-service-manager/models/driver.js @@ -0,0 +1,110 @@ +const mongoDb = require('mongodb') +let urlMongo = 'mongodb://' +if (process.env.MONGODB_REQUIRE_LOGIN) { + urlMongo += process.env.MONGODB_USER + ':' + process.env.MONGODB_PSWD + '@' +} +urlMongo += process.env.MONGODB_HOST + ':' + process.env.MONGODB_PORT + '/' +if (process.env.MONGODB_REQUIRE_LOGIN) { + urlMongo += '?authSource=' + process.env.MONGODB_DBNAME_SMANAGER +} + + +// Create an instance of Mongodb Client. Handle connexion, closeConnection, reconnect and error +class MongoDriver { + static mongoDb = mongoDb + static urlMongo = urlMongo + static client = mongoDb.MongoClient + static db = null + + // Check mongo database connection status + static checkConnection() { + try { + if (MongoDriver.db && MongoDriver.db.serverConfig) { + return MongoDriver.db.serverConfig.isConnected() + } else { + return false + } + } catch (error) { + console.error(error) + return false + } + } + + constructor() { + this.poolOptions = { + numberOfRetries: 5, + auto_reconnect: true, + poolSize: 40, + connectTimeoutMS: 5000, + useNewUrlParser: true, + useUnifiedTopology: true + } + // if connexion exists + if (MongoDriver.checkConnection()) { + return this + } + + // Otherwise, inits connexions and binds event handling + MongoDriver.client.connect(MongoDriver.urlMongo, this.poolOptions, (err, client) => { + if (err) { + console.error('> MongoDB ERROR unable to connect:', err.message) + } else { + console.log('> MongoDB : Connected') + MongoDriver.db = client.db(process.env.LINTO_STACK_MONGODB_DBNAME) + const mongoEvent = client.topology + + mongoEvent.on('close', () => { + console.error('> MongoDb : Connection lost ') + }) + mongoEvent.on('error', (e) => { + console.error('> MongoDb ERROR: ', e) + }) + mongoEvent.on('reconnect', () => { + console.error('> MongoDb : reconnect') + }) + + /* ALL EVENTS */ + /* + commandStarted: [Function (anonymous)], + commandSucceeded: [Function (anonymous)], + commandFailed: [Function (anonymous)], + serverOpening: [Function (anonymous)], + serverClosed: [Function (anonymous)], + serverDescriptionChanged: [Function (anonymous)], + serverHeartbeatStarted: [Function (anonymous)], + serverHeartbeatSucceeded: [Function (anonymous)], + serverHeartbeatFailed: [Function (anonymous)], + topologyOpening: [Function (anonymous)], + topologyClosed: [Function (anonymous)], + topologyDescriptionChanged: [Function (anonymous)], + joined: [Function (anonymous)], + left: [Function (anonymous)], + ping: [Function (anonymous)], + ha: [Function (anonymous)], + connectionPoolCreated: [Function (anonymous)], + connectionPoolClosed: [Function (anonymous)], + connectionCreated: [Function (anonymous)], + connectionReady: [Function (anonymous)], + connectionClosed: [Function (anonymous)], + connectionCheckOutStarted: [Function (anonymous)], + connectionCheckOutFailed: [Function (anonymous)], + connectionCheckedOut: [Function (anonymous)], + connectionCheckedIn: [Function (anonymous)], + connectionPoolCleared: [Function (anonymous)], + authenticated: [Function (anonymous)], + error: [ [Function (anonymous)], [Function: listener] ], + timeout: [ [Function (anonymous)], [Function: listener] ], + close: [ [Function (anonymous)], [Function: listener] ], + parseError: [ [Function (anonymous)], [Function: listener] ], + open: [ [Function], [Function] ], + fullsetup: [ [Function], [Function] ], + all: [ [Function], [Function] ], + reconnect: [ [Function (anonymous)], [Function: listener] ] + */ + } + }) + } + +} + +module.exports = new MongoDriver() // Exports a singleton \ No newline at end of file diff --git a/platform/stt-service-manager/models/model.js b/platform/stt-service-manager/models/model.js new file mode 100644 index 0000000..69949de --- /dev/null +++ b/platform/stt-service-manager/models/model.js @@ -0,0 +1,136 @@ +const MongoDriver = require(`${process.cwd()}/models/driver.js`) + +class MongoModel { + constructor(collection) { + this.collection = collection + } + checkConnection() { + if (MongoDriver.constructor.db && MongoDriver.constructor.db.serverConfig.isConnected()) return true + else return false + } + /* ========================= */ + /* ===== MONGO METHODS ===== */ + /* ========================= */ + /** + * Request function for mongoDB. This function will make a request on the "collection", filtered by the "query" passed in parameters. + * @param {string} collection + * @param {Object} query + * @returns {Pomise} + */ + async mongoRequest(query) { + return new Promise((resolve, reject) => { + try { + if (!this.checkConnection()) throw 'failed to connect to MongoDB server' + MongoDriver.constructor.db.collection(this.collection).find(query).toArray((error, result) => { + if (error) { + reject(error) + } + resolve(result) + }) + } catch (error) { + reject(error) + } + }) + } + + /** + * Insert/Create function for mongoDB. This function will create an entry based on the "collection", the "query" and the "values" passed in parmaters. + * @param {Object} query + * @param {Object} values + * @returns {Pomise} + */ + async mongoInsert(payload) { + return new Promise((resolve, reject) => { + try { + if (!this.checkConnection()) throw 'failed to connect to MongoDB server' + MongoDriver.constructor.db.collection(this.collection).insertOne(payload, function (error, result) { + if (error) { + reject(error) + } + resolve('success') + }) + } catch (error) { + reject(error) + } + }) + } + + /** + * Update function for mongoDB. This function will update an entry based on the "collection", the "query" and the "values" passed in parmaters. + * @param {Object} query + * @param {Object} values + * @returns {Pomise} + */ + async mongoUpdate(query, values) { + if (values._id) { + delete values._id + } + return new Promise((resolve, reject) => { + try { + if (!this.checkConnection()) throw 'failed to connect to MongoDB server' + MongoDriver.constructor.db.collection(this.collection).updateOne(query, { + $set: values + }, function (error, result) { + if (error) { + reject(error) + } + resolve('success') + }) + } catch (error) { + reject(error) + } + }) + } + + /** + * Update function for mongoDB. This function will update an entry based on the "collection", the "query" and the "values" and the "modes" passed in parmaters. + * @param {Object} query + * @param {Object} values + * @returns {Pomise} + */ + async mongoUpdateModes(query, ...modesAndvalues) { + return new Promise((resolve, reject) => { + try { + if (!this.checkConnection()) throw 'failed to connect to MongoDB server' + let operators = {} + modesAndvalues.forEach((component) => { + switch (component.mode) { + case '$set': operators.$set = component.value; break + case '$push': operators.$push = component.value; break + case '$pull': operators.$pull = component.value; break + default: console.log('updateQ switch mode error'); break + } + }) + MongoDriver.constructor.db.collection(this.collection).updateOne(query, operators, (error, result) => { + if (error) reject(error) + resolve("success") + }) + } catch (error) { + reject(error) + } + }) + } + + /** + * Delete function for mongoDB. This function will create an entry based on the "collection", the "query" passed in parmaters. + * @param {Object} query + * @returns {Pomise} + */ + async mongoDelete(query) { + return new Promise((resolve, reject) => { + try { + if (!this.checkConnection()) throw 'failed to connect to MongoDB server' + MongoDriver.constructor.db.collection(this.collection).deleteOne(query, function (error, result) { + if (error) { + reject(error) + } + resolve("success") + }) + } catch (error) { + reject(error) + } + }) + } +} + +module.exports = MongoModel \ No newline at end of file diff --git a/platform/stt-service-manager/models/models/AMUpdates.js b/platform/stt-service-manager/models/models/AMUpdates.js new file mode 100644 index 0000000..f9c6584 --- /dev/null +++ b/platform/stt-service-manager/models/models/AMUpdates.js @@ -0,0 +1,56 @@ +const debug = require('debug')('app:model:AMupdate') +const datetime = require('node-datetime') +const MongoModel = require(`${process.cwd()}/models/model.js`) + +class AMUpdates extends MongoModel { + constructor() { + super('AcModels') // "context" est le nom de ma collection + } + + //create a new instance + async createModel(modelName, lang = "", desc = "") { + try { + let newModel = { + modelId: modelName, + lang: lang, + desc: desc, + date: datetime.create().format('m/d/Y-H:M:S') + } + return await this.mongoInsert(newModel) + } catch (err) { + throw "> AMCollection ERROR: " + err + } + } + + // delete acoustic model by name + async deleteModel(modelName) { + try { + return await this.mongoDelete({ modelId: modelName }) + } catch (err) { + throw "> AMCollection ERROR: " + err + } + } + + // find acoustic model by name + async findModel(modelName) { + try { + const model = await this.mongoRequest({ modelId: modelName }) + if (model.length == 0) return false + else return model[0] + } catch (err) { + throw "> AMCollection ERROR: " + err + } + } + + // find all acoustic models + async findModels(request = {}) { + try { + return await this.mongoRequest(request) + } catch (err) { + throw "> AMCollection ERROR: " + err + } + } + +} + +module.exports = new AMUpdates() \ No newline at end of file diff --git a/platform/stt-service-manager/models/models/LMUpdates.js b/platform/stt-service-manager/models/models/LMUpdates.js new file mode 100644 index 0000000..4fc3671 --- /dev/null +++ b/platform/stt-service-manager/models/models/LMUpdates.js @@ -0,0 +1,118 @@ +const debug = require('debug')('app:model:LMupdate') +const datetime = require('node-datetime') +const MongoModel = require(`${process.cwd()}/models/model.js`) + +class LMUpdates extends MongoModel { + constructor() { + super('LangModels') // "context" est le nom de ma collection + } + + //create a new instance + async createModel(modelName, acName = "", lang = "", type, isGenerated = 0, isDirty = 0, entities = [], intents = [], oov = [], dateGen = null) { + try { + let newModel = { + modelId: modelName, + type: type, + acmodelId: acName, + entities: entities, + intents: intents, + lang: lang, + isGenerated: isGenerated, + isDirty: isDirty, + updateState: 0, + updateStatus: '', + oov: oov, + dateGeneration: dateGen, + dateModification: datetime.create().format('m/d/Y-H:M:S') + } + return await this.mongoInsert(newModel) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // delete language model by name + async deleteModel(modelName) { + try { + return await this.mongoDelete({ modelId: modelName }) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // find language model by name + async findModel(modelName) { + try { + const model = await this.mongoRequest({ modelId: modelName }) + if (model.length == 0) return false + else return model[0] + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // find all language models + async findModels(request = {}) { + try { + return await this.mongoRequest(request) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // update one or multiple parameters by modelId + async updateModel(modelName, obj) { + try { + return await this.mongoUpdateModes({ modelId: modelName }, { mode: '$set', value: obj }) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // update model generation parameters (updateState, updateStatus, dateGeneration) by modelId + async generationState(modelName, value, msg) { + try { + const obj = { 'updateState': value, 'updateStatus': msg, 'dateGeneration': datetime.create().format('m/d/Y-H:M:S') } + return await this.mongoUpdateModes({ modelId: modelName }, { mode: '$set', value: obj }) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // add a single entity/intent by modelId + async addElemInList(modelName, element, value) { + try { + const set = { dateModification: datetime.create().format('m/d/Y-H:M:S'), isDirty: 1 } + const push = {} + push[element] = value //intent or entity + return await this.mongoUpdateModes({ modelId: modelName }, { mode: '$set', value: set }, { mode: '$push', value: push }) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // update a single entity/intent by modelId + async updateElemFromList(modelName, element, value) { + try { + let set = { dateModification: datetime.create().format('m/d/Y-H:M:S'), isDirty: 1 } + set[element] = value + return await this.mongoUpdateModes({ modelId: modelName }, { mode: '$set', value: set }) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } + + // remove a single entity/intent by modelId + async removeElemFromList(modelName, element, name) { + try { + const set = { dateModification: datetime.create().format('m/d/Y-H:M:S'), isDirty: 1 } + const pull = {} + pull[element] = { name: name } //intent or entity + return await this.mongoUpdateModes({ modelId: modelName }, { mode: '$set', value: set }, { mode: '$pull', value: pull }) + } catch (err) { + throw "> LMCollection ERROR: " + err + } + } +} + +module.exports = new LMUpdates() \ No newline at end of file diff --git a/platform/stt-service-manager/models/models/ServiceUpdates.js b/platform/stt-service-manager/models/models/ServiceUpdates.js new file mode 100644 index 0000000..063e1e3 --- /dev/null +++ b/platform/stt-service-manager/models/models/ServiceUpdates.js @@ -0,0 +1,70 @@ +const debug = require('debug')('app:model:serviceupdates') +const datetime = require('node-datetime') +const MongoModel = require(`${process.cwd()}/models/model.js`) + +class ServiceUpdates extends MongoModel { + constructor() { + super('Services') // "context" est le nom de ma collection + } + + //create a new instance + async createService(obj) { + try { + let newService = { + serviceId: obj.serviceId, + tag: obj.tag, + replicas: obj.replicas, + LModelId: obj.LModelId, + AModelId: obj.AModelId, + externalAccess: obj.externalAccess, + lang: obj.lang, + isOn: 0, + date: datetime.create().format('m/d/Y-H:M:S') + } + return await this.mongoInsert(newService) + } catch (err) { + throw "> ServiceCollection ERROR: " + err + } + } + + // find service by name + async findService(serviceId) { + try { + const service = await this.mongoRequest({ serviceId: serviceId }) + if (service.length == 0) return false + else return service[0] + } catch (err) { + throw "> ServiceCollection ERROR: " + err + } + } + + // find all services + async findServices(request = {}) { + try { + return await this.mongoRequest(request) + } catch (err) { + throw "> ServiceCollection ERROR: " + err + } + } + + // update service by name + async updateService(id, obj) { + try { + obj.date = datetime.create().format('m/d/Y-H:M:S') + return await this.mongoUpdate({ serviceId: id }, obj) + } catch (err) { + throw "> ServiceCollection ERROR: " + err + } + } + + // delete service by name + async deleteService(serviceId) { + try { + return await this.mongoDelete({ serviceId: serviceId }) + } catch (err) { + throw "> ServiceCollection ERROR: " + err + } + } +} + +module.exports = new ServiceUpdates() diff --git a/platform/stt-service-manager/package.json b/platform/stt-service-manager/package.json new file mode 100644 index 0000000..3d42290 --- /dev/null +++ b/platform/stt-service-manager/package.json @@ -0,0 +1,40 @@ +{ + "name": "vasistas", + "version": "1.0.0", + "description": "What is that ? A structure proposal & guidelines for bootstrapping a Node.js project that implements observer design pattern with Inversion of Control (IoC) and Dependency-Injection (DI)", + "main": "app.js", + "scripts": { + "start": "DEBUG=app:* node app.js", + "start-debug": "DEBUG=* node app.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "body-parser": "^1.19.0", + "compressing": "^1.4.0", + "configparser": "^0.3.6", + "cookie-parser": "^1.4.4", + "cors": "^2.8.5", + "debug": "^4.1.1", + "dockerode": "^2.5.8", + "dotenv": "^8.0.0", + "eventemitter3": "^4.0.0", + "express": "^4.17.1", + "express-session": "^1.16.2", + "ini": "^1.3.5", + "mongodb": "^3.3.1", + "multer": "^1.4.2", + "ncp": "^2.0.0", + "nginx-conf": "^1.5.0", + "node-datetime": "^2.1.2", + "node-fetch": "2.6.1", + "ora": "^3.4.0", + "rimraf": "^3.0.0", + "saslprep": "^1.0.3", + "socket.io": "^2.3.0", + "socket.io-client": "^2.3.0", + "swagger-ui-express": "^4.0.0", + "vorpal": "^1.12.0", + "yamljs": "^0.3.0" + } +} diff --git a/platform/stt-service-manager/wait-for-it.sh b/platform/stt-service-manager/wait-for-it.sh new file mode 100755 index 0000000..92cbdbb --- /dev/null +++ b/platform/stt-service-manager/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file diff --git a/stack/.gitignore b/stack/.gitignore new file mode 100644 index 0000000..85902e2 --- /dev/null +++ b/stack/.gitignore @@ -0,0 +1 @@ +devcerts/*.pem diff --git a/stack/LICENSE b/stack/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/stack/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/config/bls/flowsStorage.json b/stack/config/bls/flowsStorage.json similarity index 100% rename from config/bls/flowsStorage.json rename to stack/config/bls/flowsStorage.json diff --git a/config/jitsi/env/jitsienv_template b/stack/config/jitsi/env/jitsienv_template similarity index 100% rename from config/jitsi/env/jitsienv_template rename to stack/config/jitsi/env/jitsienv_template diff --git a/config/jitsi/jigasi/sip-communicator.properties b/stack/config/jitsi/jigasi/sip-communicator.properties similarity index 100% rename from config/jitsi/jigasi/sip-communicator.properties rename to stack/config/jitsi/jigasi/sip-communicator.properties diff --git a/config/jitsi/prosody/conf.d/jitsi-meet.cfg.lua b/stack/config/jitsi/prosody/conf.d/jitsi-meet.cfg.lua similarity index 100% rename from config/jitsi/prosody/conf.d/jitsi-meet.cfg.lua rename to stack/config/jitsi/prosody/conf.d/jitsi-meet.cfg.lua diff --git a/config/jitsi/prosody/user/prosody-user.dat b/stack/config/jitsi/prosody/user/prosody-user.dat similarity index 100% rename from config/jitsi/prosody/user/prosody-user.dat rename to stack/config/jitsi/prosody/user/prosody-user.dat diff --git a/config/jitsi/traefik/upd-jvb.toml b/stack/config/jitsi/traefik/upd-jvb.toml similarity index 100% rename from config/jitsi/traefik/upd-jvb.toml rename to stack/config/jitsi/traefik/upd-jvb.toml diff --git a/config/jitsi/web/custom-config.js b/stack/config/jitsi/web/custom-config.js similarity index 100% rename from config/jitsi/web/custom-config.js rename to stack/config/jitsi/web/custom-config.js diff --git a/config/mongoseeds/admin-users.js b/stack/config/mongoseeds/admin-users.js similarity index 100% rename from config/mongoseeds/admin-users.js rename to stack/config/mongoseeds/admin-users.js diff --git a/stack/config/mosquitto/auth/.gitkeep b/stack/config/mosquitto/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/mosquitto/auth/acls b/stack/config/mosquitto/auth/acls similarity index 100% rename from config/mosquitto/auth/acls rename to stack/config/mosquitto/auth/acls diff --git a/config/mosquitto/conf-tempalte/go-auth-template.conf b/stack/config/mosquitto/conf-tempalte/go-auth-template.conf similarity index 100% rename from config/mosquitto/conf-tempalte/go-auth-template.conf rename to stack/config/mosquitto/conf-tempalte/go-auth-template.conf diff --git a/stack/config/mosquitto/conf.d/.gitkeep b/stack/config/mosquitto/conf.d/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/mosquitto/mosquitto.conf b/stack/config/mosquitto/mosquitto.conf similarity index 100% rename from config/mosquitto/mosquitto.conf rename to stack/config/mosquitto/mosquitto.conf diff --git a/stack/config/servicemanager/init.js b/stack/config/servicemanager/init.js new file mode 100644 index 0000000..678a17b --- /dev/null +++ b/stack/config/servicemanager/init.js @@ -0,0 +1,11 @@ +//Create the language models collection +db.createCollection("LangModels") + +//Create the acoustic models collection +db.createCollection("AcModels") + +//this an example of the acousticModel collection information +db.createCollection("Services") + + + diff --git a/stack/config/servicemanager/nginx.conf b/stack/config/servicemanager/nginx.conf new file mode 100644 index 0000000..f310f61 --- /dev/null +++ b/stack/config/servicemanager/nginx.conf @@ -0,0 +1,4 @@ +server { + server_name ''; + port_in_redirect off; +} diff --git a/config/servicemanager/user.js b/stack/config/servicemanager/user.js similarity index 100% rename from config/servicemanager/user.js rename to stack/config/servicemanager/user.js diff --git a/config/tock/scripts/admin-web-entrypoint.sh b/stack/config/tock/scripts/admin-web-entrypoint.sh similarity index 100% rename from config/tock/scripts/admin-web-entrypoint.sh rename to stack/config/tock/scripts/admin-web-entrypoint.sh diff --git a/config/tock/scripts/setup.sh b/stack/config/tock/scripts/setup.sh similarity index 100% rename from config/tock/scripts/setup.sh rename to stack/config/tock/scripts/setup.sh diff --git a/config/traefik/http-auth.toml b/stack/config/traefik/http-auth.toml similarity index 100% rename from config/traefik/http-auth.toml rename to stack/config/traefik/http-auth.toml diff --git a/config/traefik/ssl-redirect.toml b/stack/config/traefik/ssl-redirect.toml similarity index 100% rename from config/traefik/ssl-redirect.toml rename to stack/config/traefik/ssl-redirect.toml diff --git a/config/traefik/stt-manager-path.toml b/stack/config/traefik/stt-manager-path.toml similarity index 100% rename from config/traefik/stt-manager-path.toml rename to stack/config/traefik/stt-manager-path.toml diff --git a/config/traefik/tock-path.toml b/stack/config/traefik/tock-path.toml similarity index 100% rename from config/traefik/tock-path.toml rename to stack/config/traefik/tock-path.toml diff --git a/stack/devcerts/.gitkeep b/stack/devcerts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dockerenv_template b/stack/dockerenv_template similarity index 100% rename from dockerenv_template rename to stack/dockerenv_template diff --git a/docs/Jitsi.md b/stack/docs/Jitsi.md similarity index 100% rename from docs/Jitsi.md rename to stack/docs/Jitsi.md diff --git a/docs/README.md b/stack/docs/README.md similarity index 100% rename from docs/README.md rename to stack/docs/README.md diff --git a/optional-stack-files/grafana.yml b/stack/optional-stack-files/grafana.yml similarity index 100% rename from optional-stack-files/grafana.yml rename to stack/optional-stack-files/grafana.yml diff --git a/optional-stack-files/linto-platform-jitsi.yml b/stack/optional-stack-files/linto-platform-jitsi.yml similarity index 100% rename from optional-stack-files/linto-platform-jitsi.yml rename to stack/optional-stack-files/linto-platform-jitsi.yml diff --git a/optional-stack-files/linto-platform-tasks-monitor.yml b/stack/optional-stack-files/linto-platform-tasks-monitor.yml similarity index 100% rename from optional-stack-files/linto-platform-tasks-monitor.yml rename to stack/optional-stack-files/linto-platform-tasks-monitor.yml diff --git a/optional-stack-files/network_tool.yml b/stack/optional-stack-files/network_tool.yml similarity index 100% rename from optional-stack-files/network_tool.yml rename to stack/optional-stack-files/network_tool.yml diff --git a/scripts/README.md b/stack/scripts/README.md similarity index 100% rename from scripts/README.md rename to stack/scripts/README.md diff --git a/scripts/bls_backup.sh b/stack/scripts/bls_backup.sh similarity index 100% rename from scripts/bls_backup.sh rename to stack/scripts/bls_backup.sh diff --git a/scripts/bls_restore.sh b/stack/scripts/bls_restore.sh similarity index 100% rename from scripts/bls_restore.sh rename to stack/scripts/bls_restore.sh diff --git a/scripts/db_backup.sh b/stack/scripts/db_backup.sh similarity index 100% rename from scripts/db_backup.sh rename to stack/scripts/db_backup.sh diff --git a/scripts/db_restore.sh b/stack/scripts/db_restore.sh similarity index 100% rename from scripts/db_restore.sh rename to stack/scripts/db_restore.sh diff --git a/scripts/start-jitsi.sh b/stack/scripts/start-jitsi.sh similarity index 100% rename from scripts/start-jitsi.sh rename to stack/scripts/start-jitsi.sh diff --git a/scripts/start-optional.sh b/stack/scripts/start-optional.sh similarity index 100% rename from scripts/start-optional.sh rename to stack/scripts/start-optional.sh diff --git a/stack-files/linto-docker-visualizer.yml b/stack/stack-files/linto-docker-visualizer.yml similarity index 100% rename from stack-files/linto-docker-visualizer.yml rename to stack/stack-files/linto-docker-visualizer.yml diff --git a/stack-files/linto-edge-router.yml b/stack/stack-files/linto-edge-router.yml similarity index 100% rename from stack-files/linto-edge-router.yml rename to stack/stack-files/linto-edge-router.yml diff --git a/stack-files/linto-mongo-migration.yml b/stack/stack-files/linto-mongo-migration.yml similarity index 100% rename from stack-files/linto-mongo-migration.yml rename to stack/stack-files/linto-mongo-migration.yml diff --git a/stack-files/linto-mqtt-broker.yml b/stack/stack-files/linto-mqtt-broker.yml similarity index 100% rename from stack-files/linto-mqtt-broker.yml rename to stack/stack-files/linto-mqtt-broker.yml diff --git a/stack-files/linto-platform-admin.yml b/stack/stack-files/linto-platform-admin.yml similarity index 100% rename from stack-files/linto-platform-admin.yml rename to stack/stack-files/linto-platform-admin.yml diff --git a/stack-files/linto-platform-bls.yml b/stack/stack-files/linto-platform-bls.yml similarity index 100% rename from stack-files/linto-platform-bls.yml rename to stack/stack-files/linto-platform-bls.yml diff --git a/stack-files/linto-platform-mongo.yml b/stack/stack-files/linto-platform-mongo.yml similarity index 100% rename from stack-files/linto-platform-mongo.yml rename to stack/stack-files/linto-platform-mongo.yml diff --git a/stack-files/linto-platform-overwatch.yml b/stack/stack-files/linto-platform-overwatch.yml similarity index 100% rename from stack-files/linto-platform-overwatch.yml rename to stack/stack-files/linto-platform-overwatch.yml diff --git a/stack-files/linto-platform-redis.yml b/stack/stack-files/linto-platform-redis.yml similarity index 100% rename from stack-files/linto-platform-redis.yml rename to stack/stack-files/linto-platform-redis.yml diff --git a/stack-files/linto-platform-stt-service-manager-nginx.yml b/stack/stack-files/linto-platform-stt-service-manager-nginx.yml similarity index 100% rename from stack-files/linto-platform-stt-service-manager-nginx.yml rename to stack/stack-files/linto-platform-stt-service-manager-nginx.yml diff --git a/stack-files/linto-platform-stt-service-manager.yml b/stack/stack-files/linto-platform-stt-service-manager.yml similarity index 100% rename from stack-files/linto-platform-stt-service-manager.yml rename to stack/stack-files/linto-platform-stt-service-manager.yml diff --git a/stack-files/linto-platform-tock.yml b/stack/stack-files/linto-platform-tock.yml similarity index 100% rename from stack-files/linto-platform-tock.yml rename to stack/stack-files/linto-platform-tock.yml diff --git a/start.sh b/stack/start.sh similarity index 100% rename from start.sh rename to stack/start.sh