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 0000000..8e4c9e3 Binary files /dev/null and b/client/web/src/assets/audio/beep.mp3 differ 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 0000000..ec94f43 Binary files /dev/null and b/client/web/src/assets/img/widget/writing.gif differ 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,SUQzAwAAAAAAJlRQRTEAAAAcAAAAU291bmRKYXkuY29tIFNvdW5kIEVmZmVjdHMA//uSwAAAAAABLBQAAALww+e/OUAAABAAADAGAHAIAwSAAAGI8/+ZHBGQYYZf/YvG2TqZMA3/b0YxGJEE/AzKEA5n+BhgwG/DgYQ5/gagMBslgKKAAhf/hQAEwAGSAAZ0SF+v/eLgFkCEgeoLEJD/1m5uhEAwwWBlKQHsaAd1mDA4ep//+XSXIIOYRBioZm//2939FOThTNxO5PuRMw////d/HAF9Ab3AaBhdwYUDVYDBcLGBBgAoMAYeNscZQ/////////2Fxm6aCzcPgD5CQHAAAAAAQAUAgAkDgAAAAAMSUCP3IxYGbXmA0w3EHEy/HIwKCMaAOgGgAqmQhuGEQZhYA8AcCWY6LjTQ2mmH0bKh4niETmbQnfxjO6lgEHMBGAzoKE86mb+3rkafiZbKIQgYVCaNyCKzDanMmqdeFyJfgtVuLRwUH1TAwF3uwCuGJ1MOPon0718GhowiIjI5OO+0kxoXDCIiAyEAAHfkkAJiQ3BgTjAcHzAYCfoUARjI4g4GtLGAAMA0cAgNALDYZg53WAN46FMPAKg/t+RW4XSrov/7ksBoACyKH03Z3hADPi3vd7DwB1/OZ0lO+FI7a4YosBLtPtHqGn7ucwpJb23uP2e1Z7VyY3eps1pmCQCyVDoBhsiCBAQYEDEFqaFA4RqhSgEqhhgmDVCsBSx2kDdxuDoeyv2JO3PJw0i6dbaNcnbKptat8s0jpTnZ23SOkp6UxP///////////ghkk9hEJ6aY06uUA///////////9PQxnsYt21Em83UrLLm390qBbM0sMwIVMPKU0BTGwz9PEJ2hc2XrvkeLxWIQboWyfPVyNBD2eG5qtnpuKwOBhoJXLZ8KyP83zqh+vJZsSPJoK3CVk9Jme19K28+NZzXJLY//97KQOp48iv53mmOqGHGvD0GQvmmaczw5ydlzUceArFYyP349ZCy3qNEF0E0BzhG0ebghAKQWBfJ2WAc4KcWMzA1ByibqoSd8EcJoN9CiWHuY5b0kSwzzzUB/qZCS8DyRSpOtsgqRNRGb3xrPpp9ThhbKQ/82nfJs8Ketu20jRCEWdm5PymMspXSgKSOOENOW/Bw8ChzEgRM0+3t093rxPIn/+5LAFIAVeVeFrT2Nurmq7zWnpbJga0PcEm3KhD1UciVVxoEMd3L4Xw4IFq794nzS35e+0pW+duZSnTszmda8/P71q2o9dnmrs1ZW1aXTVatatVatWrVpyfH1nrWXPfZpq1vqtozWWntW0swSkYgkQRjsfTpSSh7HUQiWaE4qrzUShSQDciloxqiDll1VVWahapHZEYkznvkUaVl1tttFaXPm89ahjMF0zdDSYDoRAC5NgLbNa1q6/xnT7OIUd4YCGGIWh1l9C+CSkEAxEmVzMXH1xFbp48aOylFmRPBNmBgXXDIm40imJjDYpjBkmMueTCQjJEJ5KQoXxjicgQpkBCIwpqicBWiuDMTci9HS6hIpjHIDKFFpIw1eCsVm9fUZsT0yX2KjyAgHeskogtI99bgbNz//oxd4XeoQDScEBpAuBDIHDgCHeDEAZ3uutkg5Zwl8saWDIhzviVBcreu/8zP1mhfEMG4NyAHwNQmH6rsvfPXuvP/OJ6OK1609rfa1/JrU2tRs6j4ltWPXdaWtumYUKskCb1t/S0GnmtWLXFIN//uSwC2AFZlZi+0x7brDKu+1tj2yYO81vFmrmHDm3Fa/JDa5ZctklmB8vJRthuUifQ44zgU60TA4CdkvUwuY/0QEMBUBgDADUFgDVgqwTYDG0kwAOCWEbpnqcDnVAxKjcoNjNLiwMttIVsttttsGtWIbFAoxwiNQtDKoFpvf/n/qwmCGFYSD2ps1C6+xRhQzGsP12urZ13rLP+dvS959sxsVzjGN1tvFPCtmmr+l9X1WlsUzE8eG3z3h2gUb39mB0pjoaj5J2xEEQ0v5C3MT8FOHOEfAWxCxxjkEMAzlABcFJAhkuBAAmxNgaQZADmHQEjBiCJiAgJxzHOMUvyOIKsF+JMpi3EKZtpE6UNL6hTlWCcqGrKeOoNqArJbbbIyjlZgdHQzFIL75D//+VevNAsOJO11shWY5ZOOlhlqIjnTIN6eyx6jTaJXPet487T61WvY7t2613Kk5nNQxLYftQxXnI3XdCEMzeWINBo0xGUDIYcmFDgGOrYFTSUgRCgYoVTLxAEMiTCxIgCL1CEUBIs5C4QCGR5Lk3qsRpYzDMO1YzP/7ksBFABWtRYGtIy2yeibvfYYxs7aanh2pflPdfjvDLvcbP75/5Z97Z3l3n1GhAiI/9FdTlE6hsMIETIqNtvo3Anq3J6xARkQBEySWb+xnVhl1ajiPAqAITOtOWNi9cmEuM5SLzbIDmBQpVPV9xP0a7VZ8+eHawbwOsQoTo4kdvap0rrr0D5ZuSjLeZ7etaBam+zry0JT1KSRBEUGoik0TgSNQbIIUioSyYNx0HQKxoHEfwkIo5h/CE4jmZbuVz8zJChOZvLK+wrXv6+wsm6yQ6sjnsRyC1hLcckkssTDipzQyyJD0AJGCgOygKNCiUtez17m9bIsylRomQpCnNORVxM2exT9VLc4XZIbGr2NT2rjcd7LqsGMrnBmjKOGzLiLBiNytf3zDjx2/0zpkkba4bWS7bD8V9KsSt9Viu5Vllzanhq5zeq1iVzeXGw+jrkZo7pmunWJ9DOVDTRLihqwnSUoSvEKXxwm8hYNIIaFCFnBYCTh+BrkqFWFYI+KIJAgQVZ5jkBjoWexCBbAbgmghAxL3tAiZB44T6vq/zDm0lt3/+5LAZYAYqWNvrL3tkxez7vWGMbO2zLYmtv8w3JcrxF73JVKfRtkdWLToqw8WnznVYMjIRhqFY/6Sz9aytpe0EnrrolIR+YVbLa9VT7mBWfeQyO2fRHx4PZVWpDGxoqLf4cJizZxneaOICghlpcyRSwZHZVQmSvRWVrIxBcZLuusWJ5kmZJ6pEUh8XHRoERaPRFNDNtQIB+KgPCWflVKrptbY9RVyVRdK0JTYkllkqg1PTFxm5zAuXWZW4uhVLYE6xesbYaiX4eNr/YXv3++3u+qPMuoAh3cCJHht///8ID9risb0v4EjmEATBJxj219JfcfWFxoq1q8+VQah6BYUjMwLaYiGJ8jOmI8sVDo/Sk4cjsyEktA0UnZsuJBkdYW33B4LHFQREVV2NJzM0Ox0MDFHCT3ikTKiWS0ZMOCwWGYFZIMCgdB4nQhHoYRCBBcnDAmwCGdJR4ENauDc1bFZ4IZfJLjoToC84Dk4aUNRvGKK6wusMu+zEXjf7qTonGyVbBP//7rR6DSED91cQ7KAErtddtvwEcZyQM5JkilB6kNJ//uSwGYAFtVZg+exjZK8qu+897GyzI+LsStCzjp2JlUdi0LHoyIaPVz1m1mrejTmCuM8OQWcpy4MiRez0izwcuVJCT+n5MOh6HMGzalDHI8D8nqiBY1jKokxqTk5KzCDQyK10sCM6PuY4yQ3kw5Kkx8vPCkUFicxjow8f3YI4lBKhg0OlcGiKeEFWeLhFHJzz4+LFoFxdYVeJN2hc5So5GZcJ4knq0QVsrS3hANR1Jh3ZgEkX3/f/gHETstgdUQ2Fo7oSoH8GptDQ0JMasY641fXNc1zFrpFRWxdsLfBmbn6Gq1h01Rm5mY6ratUkMQz1MqgNViZO0iJtlxOupTPFVQZOc6ZF1orVLJ9AhmDa7SQ2TLEg/Sk1fSKiEbLmly7z5nVLwsPCUgpU+FZw7ITSWsSx0kdCgI4nw8etFEePxrCLMK5KW3SUtdiKZseugxbPLlJ+Ih3ZAAAu/2tgDunxhZY1ZubMopAfRKHUYS////iHOdcrw58HyFMU5L6RsDpNJpSQ30I8THVniqPlrJ3aqTlErZKo6NN1gqhRaqFKlCghv/7ksB5gBSRV3nnvY2Sj6rvvJWxt4iQmz1cVUh8OsWHqc9juuk9LpqPJZRtPwGbSqSYXlrJKeTCUvmVDxOPktGUh47fHCnAxctrzOSoeJRDJaocwTBMG4Nz+1lP+AOidPEDzyKSpQMqqqhgl2Z2UAAHbbrECUrgcpLB6lEW4uR3radQ1lqf2aIcfADcARB1NtN////Ot4w5srC2LVL0bmNkVUzNWFvtyibXhfjqQ5Dl5XpU6sHUwzKZwZlM0uO2pRTxk6qRzDyNFWxGtjVMsVtJyeh5N0zasxm5aisSRQrCisxXYUNVs6dpfXrIzZpduY3i7VifV6EIMsJchgl0JuOlhbFUnXiHN6hXkNbDSdn6aLipi3GiqZE8aRpE6J0Tp69e1rh8+jYhRsvdQtwbvcBMoNDVupAAAFlllIzsNQ0dQl+L8eqSTKYSJ7GaVB2KNwvi+JW5uh0vr4gwoUOl6ZxnGP/9WgwpJcZc1IpIIqKzR7Lp0/MTExMsura5qSibH1bXKzExxMT/05pKKSkwVURNhtDIFQpDAdY+GZaTiSSiCKz/+5LAm4AZIYFZ573tmnS24eA3rbnQ+x9XETF/zETH//dOa41JRAkCUG5x7HsmYv+XTMTExMTLLpzXIpJJsHP5Siq+vYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//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 0000000..0d746d8 Binary files /dev/null and b/platform/business-logic-server/asset/linto.png differ 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 0000000..108f00a Binary files /dev/null and b/platform/business-logic-server/asset/linto_min.png differ diff --git a/platform/business-logic-server/config.js b/platform/business-logic-server/config.js new file mode 100644 index 0000000..f64772a --- /dev/null +++ b/platform/business-logic-server/config.js @@ -0,0 +1,52 @@ +const debug = require('debug')('linto-red:config') +const dotenv = require('dotenv') +const fs = require('fs') + +function ifHas(element, defaultValue) { + if (!element) return defaultValue + return element +} + +function configureDefaults() { + try { + dotenv.config() + const envdefault = dotenv.parse(fs.readFileSync('.envdefault')) + + // Shared folder + process.env.LINTO_SHARED_MOUNT = ifHas(process.env.LINTO_SHARED_MOUNT, envdefault.LINTO_SHARED_MOUNT) + process.env.LINTO_STACK_NPM_CUSTOM_REGISTRY = ifHas(process.env.LINTO_STACK_NPM_CUSTOM_REGISTRY) + + // Node environment + process.env.LINTO_STACK_NODE_ENV = ifHas(process.env.NODE_ENV, envdefault.NODE_ENV) + process.env.LINTO_STACK_USE_SSL = ifHas(process.env.LINTO_STACK_USE_SSL, envdefault.LINTO_STACK_USE_SSL) + + // Server RED properties + process.env.LINTO_STACK_BLS_HTTP_PORT = ifHas(process.env.LINTO_STACK_BLS_HTTP_PORT, 80) + process.env.LINTO_STACK_BLS_SERVICE_UI_PATH = ifHas(process.env.LINTO_STACK_BLS_SERVICE_UI_PATH, envdefault.LINTO_STACK_BLS_SERVICE_UI_PATH) + process.env.LINTO_STACK_BLS_SERVICE_API_PATH = '/red' + + 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) + process.env.LINTO_STACK_BLS_API_MAX_LENGTH = ifHas(process.env.LINTO_STACK_BLS_API_MAX_LENGTH, envdefault.LINTO_STACK_BLS_API_MAX_LENGTH) + + // STT properties + process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE = ifHas(process.env.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE, envdefault.LINTO_STACK_STT_SERVICE_MANAGER_SERVICE) + // Admin properties + process.env.LINTO_STACK_ADMIN_URI = ifHas(process.env.LINTO_STACK_ADMIN_URI, envdefault.LINTO_STACK_ADMIN_URI) + + // Overwatch properties + process.env.LINTO_STACK_OVERWATCH_SERVICE = ifHas(process.env.LINTO_STACK_OVERWATCH_SERVICE, envdefault.LINTO_STACK_OVERWATCH_SERVICE) + process.env.LINTO_STACK_OVERWATCH_BASE_PATH = ifHas(process.env.LINTO_STACK_OVERWATCH_BASE_PATH, envdefault.LINTO_STACK_OVERWATCH_BASE_PATH) + + // TOCK properties + process.env.LINTO_STACK_TOCK_BOT_API = ifHas(process.env.LINTO_STACK_TOCK_BOT_API, envdefault.LINTO_STACK_TOCK_BOT_API) + 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) + + } catch (e) { + console.error(debug.namespace, e) + process.exit(1) + } +} +module.exports = configureDefaults() diff --git a/platform/business-logic-server/docker-entrypoint.sh b/platform/business-logic-server/docker-entrypoint.sh new file mode 100755 index 0000000..0ecd5a7 --- /dev/null +++ b/platform/business-logic-server/docker-entrypoint.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -e + +install_linto_node_module(){ + npm install @linto-ai/node-red-linto-core + npm install @linto-ai/node-red-linto-calendar @linto-ai/node-red-linto-datetime @linto-ai/node-red-linto-definition @linto-ai/node-red-linto-meeting @linto-ai/node-red-linto-memo @linto-ai/node-red-linto-news @linto-ai/node-red-linto-pollution @linto-ai/node-red-linto-weather @linto-ai/node-red-linto-welcome +} + +install_by_node_registry(){ + npm set registry $1 + install_linto_node_module +} + +set_node_registry(){ + npm set registry $1 +} + +[ -z "$LINTO_STACK_DOMAIN" ] && { + echo "Missing LINTO_STACK_DOMAIN" + exit 1 +} +[ -z "$LINTO_STACK_BLS_SERVICE_UI_PATH" ] && { + echo "Missing LINTO_STACK_BLS_SERVICE_UI_PATH" + exit 1 +} + + + +while [ "$1" != "" ]; do + case $1 in + --default-registry-npmrc) + install_by_node_registry https://registry.npmjs.com/ + ;; + --set-custom-registry-npmrc) + [ -z "$LINTO_STACK_NPM_CUSTOM_REGISTRY" ] && { + echo "Missing LINTO_STACK_NPM_CUSTOM_REGISTRY" + exit 1 + } + set_node_registry $LINTO_STACK_NPM_CUSTOM_REGISTRY + ;; + --custom-registry-npmrc) + [ -z "$LINTO_STACK_NPM_CUSTOM_REGISTRY" ] && { + echo "Missing LINTO_STACK_NPM_CUSTOM_REGISTRY" + exit 1 + } + install_by_node_registry $LINTO_STACK_NPM_CUSTOM_REGISTRY + ;; + --reinstall) + install_linto_node_module + ;; + --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/business-logic-server +eval "$script" \ No newline at end of file diff --git a/platform/business-logic-server/docker-healthcheck.js b/platform/business-logic-server/docker-healthcheck.js new file mode 100644 index 0000000..3f16ee5 --- /dev/null +++ b/platform/business-logic-server/docker-healthcheck.js @@ -0,0 +1,7 @@ +const request = require('request') + +request(`http://localhost`, error => { + 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 0000000..c7b9a43 Binary files /dev/null and b/platform/linto-admin/vue_app/public/favicon.ico differ 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 0000000..5ac9a66 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/admin-logo-dark@2x.png differ 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 0000000..4493f64 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/admin-logo-light@2x.png differ 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 0000000..1cc2341 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/admin-logo@2x.png differ 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 0000000..67ff498 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/bg-login.jpg differ diff --git a/platform/linto-admin/vue_app/public/img/btn-icons@2x.png b/platform/linto-admin/vue_app/public/img/btn-icons@2x.png new file mode 100755 index 0000000..5ce29ea Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/btn-icons@2x.png differ 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 0000000..18882a2 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/close-icon@2x.png differ 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 0000000..436f688 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/deploy-status-icons@2x.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/android-icon-144x144.png b/platform/linto-admin/vue_app/public/img/favicon/android-icon-144x144.png new file mode 100755 index 0000000..5b51b4b Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/android-icon-144x144.png differ 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 0000000..97257fc Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/android-icon-192x192.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/android-icon-36x36.png b/platform/linto-admin/vue_app/public/img/favicon/android-icon-36x36.png new file mode 100755 index 0000000..e2ce8fa Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/android-icon-36x36.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/android-icon-48x48.png b/platform/linto-admin/vue_app/public/img/favicon/android-icon-48x48.png new file mode 100755 index 0000000..3ea86e0 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/android-icon-48x48.png differ 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 0000000..c7d2bea Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/android-icon-72x72.png differ 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 0000000..7857e52 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/android-icon-96x96.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-114x114.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-114x114.png new file mode 100755 index 0000000..342b0c9 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-114x114.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/apple-icon-120x120.png b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-120x120.png new file mode 100755 index 0000000..86378ee Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-120x120.png differ 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 0000000..5b51b4b Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-144x144.png differ 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 0000000..e6b6223 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-152x152.png differ 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 0000000..b106f92 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-180x180.png differ 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 0000000..78e501e Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-57x57.png differ 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 0000000..176ddb8 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-60x60.png differ 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 0000000..c7d2bea Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-72x72.png differ 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 0000000..82a9a89 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-76x76.png differ 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 0000000..767e6eb Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon-precomposed.png differ 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 0000000..767e6eb Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/apple-icon.png differ 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 0000000..37faacc Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/favicon-16x16.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/favicon-32x32.png b/platform/linto-admin/vue_app/public/img/favicon/favicon-32x32.png new file mode 100755 index 0000000..4a65c0b Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/favicon-32x32.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/favicon-96x96.png b/platform/linto-admin/vue_app/public/img/favicon/favicon-96x96.png new file mode 100755 index 0000000..7857e52 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/favicon-96x96.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/favicon.ico b/platform/linto-admin/vue_app/public/img/favicon/favicon.ico new file mode 100755 index 0000000..e19e33d Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/favicon.ico differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/manifest.json b/platform/linto-admin/vue_app/public/img/favicon/manifest.json new file mode 100755 index 0000000..013d4a6 --- /dev/null +++ b/platform/linto-admin/vue_app/public/img/favicon/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/platform/linto-admin/vue_app/public/img/favicon/ms-icon-144x144.png b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-144x144.png new file mode 100755 index 0000000..5b51b4b Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-144x144.png differ 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 0000000..26342bc Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-150x150.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/ms-icon-310x310.png b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-310x310.png new file mode 100755 index 0000000..602b036 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-310x310.png differ diff --git a/platform/linto-admin/vue_app/public/img/favicon/ms-icon-70x70.png b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-70x70.png new file mode 100755 index 0000000..c03fd3a Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/favicon/ms-icon-70x70.png differ diff --git a/platform/linto-admin/vue_app/public/img/full-screen-icons@2x.png b/platform/linto-admin/vue_app/public/img/full-screen-icons@2x.png new file mode 100755 index 0000000..d0863e5 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/full-screen-icons@2x.png differ 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 0000000..76ced66 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/linto-say@2x.png differ 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 0000000..addf3f5 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/loading@2x.png differ 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 0000000..12f8e23 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/login-icons@2x.png differ 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 0000000..1aa091b Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/monitoring@2x.png differ 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 0000000..325b98c Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/mute-unmute@2x.png differ diff --git a/platform/linto-admin/vue_app/public/img/nav-arrows@2x.png b/platform/linto-admin/vue_app/public/img/nav-arrows@2x.png new file mode 100755 index 0000000..7cd41d4 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/nav-arrows@2x.png differ 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 0000000..9b908f9 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/ping@2x.png differ diff --git a/platform/linto-admin/vue_app/public/img/publish-icon@2x.png b/platform/linto-admin/vue_app/public/img/publish-icon@2x.png new file mode 100755 index 0000000..227047b Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/publish-icon@2x.png differ 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 0000000..888f954 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/say@2x.png differ 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 0000000..772ac03 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/warning@2x.png differ diff --git a/platform/linto-admin/vue_app/public/img/workflows@2x.png b/platform/linto-admin/vue_app/public/img/workflows@2x.png new file mode 100755 index 0000000..cbdd419 Binary files /dev/null and b/platform/linto-admin/vue_app/public/img/workflows@2x.png differ diff --git a/platform/linto-admin/vue_app/public/index.html b/platform/linto-admin/vue_app/public/index.html new file mode 100644 index 0000000..8eca186 --- /dev/null +++ b/platform/linto-admin/vue_app/public/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + 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 0000000..9acd74f Binary files /dev/null and b/platform/linto-admin/webserver/public/img/nodered-linto-logo.png differ 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