├── .github └── FUNDING.yml ├── .gitignore ├── config ├── app.json ├── bot.json └── hub.json ├── disclaimer.txt ├── docker ├── Dockerfile ├── Dockerfile.dockerignore ├── db-init.js └── docker-compose.yml ├── docs └── README.md ├── libs ├── ai │ └── OllamaClient.js ├── app │ ├── Common.js │ ├── Hub │ │ ├── Hub.js │ │ ├── Main.js │ │ └── Worker.js │ ├── Queue.js │ └── System.js ├── mongodb │ ├── DCABotSchema.js │ ├── ServerSchema.js │ ├── Signals3CQSSchema.js │ └── index.js ├── signals │ └── 3CQS │ │ ├── 3cqs-signals-client.js │ │ └── signals.json ├── strategies │ └── DCABot │ │ ├── DCABot.js │ │ ├── DCABotManager.js │ │ └── telegram │ │ └── dealComplete.json ├── telegram │ ├── help.txt │ └── index.js └── webserver │ ├── Hub │ ├── index.js │ └── routes.js │ ├── index.js │ ├── public │ ├── css │ │ ├── style-news.css │ │ ├── style.css │ │ └── vendor │ │ │ ├── jquery-confirm │ │ │ └── jquery-confirm.min.css │ │ │ ├── jquery-ui-timepicker-addon │ │ │ └── jquery-ui-timepicker-addon.min.css │ │ │ ├── jquery-ui │ │ │ ├── images │ │ │ │ ├── ui-icons_444444_256x240.png │ │ │ │ ├── ui-icons_555555_256x240.png │ │ │ │ ├── ui-icons_777620_256x240.png │ │ │ │ ├── ui-icons_777777_256x240.png │ │ │ │ ├── ui-icons_cc0000_256x240.png │ │ │ │ └── ui-icons_ffffff_256x240.png │ │ │ └── jquery-ui.min.css │ │ │ ├── select2 │ │ │ └── select2.min.css │ │ │ ├── simple-switch │ │ │ └── simple-switch.css │ │ │ └── tablesorter │ │ │ └── filter.formatter.min.css │ ├── data │ │ └── tradingViewData.json │ ├── images │ │ └── SymBot-Logo.png │ ├── js │ │ └── vendor │ │ │ ├── chart.js │ │ │ ├── LICENSE.txt │ │ │ └── chart.js │ │ │ ├── ejs │ │ │ ├── LICENSE │ │ │ └── ejs.min.js │ │ │ ├── jquery-confirm │ │ │ ├── LICENSE │ │ │ └── jquery-confirm.min.js │ │ │ ├── jquery-ui-timepicker-addon │ │ │ ├── LICENSE │ │ │ └── jquery-ui-timepicker-addon.min.js │ │ │ ├── jquery-ui │ │ │ ├── LICENSE.txt │ │ │ └── jquery-ui.min.js │ │ │ ├── jquery │ │ │ ├── LICENSE.txt │ │ │ └── jquery.min.js │ │ │ ├── marked │ │ │ ├── LICENSE.md │ │ │ └── marked.min.js │ │ │ ├── select2 │ │ │ ├── LICENSE.md │ │ │ └── select2.full.min.js │ │ │ ├── simple-switch │ │ │ ├── LICENSE │ │ │ └── jquery.simpleswitch.min.js │ │ │ ├── socket.io │ │ │ ├── LICENSE │ │ │ └── socket.io.min.js │ │ │ └── tablesorter │ │ │ ├── LICENSE.txt │ │ │ ├── jquery.tablesorter.combined.min.js │ │ │ ├── parsers │ │ │ ├── parser-duration.min.js │ │ │ └── parser-named-numbers.min.js │ │ │ └── widgets │ │ │ ├── widget-columnSelector.min.js │ │ │ ├── widget-filter-formatter-jui.min.js │ │ │ └── widget-filter-formatter-select2.min.js │ └── views │ │ ├── Hub │ │ ├── configView.ejs │ │ ├── homeView.ejs │ │ ├── manageView.ejs │ │ └── newsView.ejs │ │ ├── backupsView.ejs │ │ ├── configView.ejs │ │ ├── dashboardView.ejs │ │ ├── homeView.ejs │ │ ├── loginView.ejs │ │ ├── logsLiveView.ejs │ │ ├── logsView.ejs │ │ ├── partialsFooterView.ejs │ │ ├── partialsHeaderView.ejs │ │ ├── partialsSocketView.ejs │ │ ├── strategies │ │ └── DCABot │ │ │ ├── DCABotCreateUpdateView.ejs │ │ │ ├── DCABotDealsActiveView.ejs │ │ │ ├── DCABotDealsHistoryView.ejs │ │ │ ├── DCABotsView.ejs │ │ │ └── ai │ │ │ ├── aiChatView.ejs │ │ │ └── aiDealAnalyzeView.ejs │ │ ├── systemView.ejs │ │ └── tradingView.ejs │ └── routes.js ├── license.txt ├── package.json ├── symbot-hub.js └── symbot.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 3cqs-coder 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | node_modules 3 | *.log 4 | .vscode 5 | -------------------------------------------------------------------------------- /config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "password": "", 3 | "mongo_db_url": "", 4 | "max_log_days": 10, 5 | "web_server": { 6 | "port": 3000 7 | }, 8 | "api": { 9 | "enabled": true, 10 | "key": "" 11 | }, 12 | "webhook": { 13 | "enabled": true 14 | }, 15 | "telegram": { 16 | "enabled": true, 17 | "token_id": "", 18 | "notify_user_id": "" 19 | }, 20 | "cron_backup": { 21 | "enabled": false, 22 | "schedule": "", 23 | "password": "", 24 | "max": "" 25 | }, 26 | "bots": { 27 | "start_conditions": { 28 | "asap": { 29 | "description": "Open new trade asap" 30 | }, 31 | "api": { 32 | "description": "Manually / API" 33 | } 34 | }, 35 | "exchange": { 36 | "default": { 37 | "account_balance_currencies": [ "USDC", "USDT", "USD" ], 38 | "orders": { 39 | "buy": { 40 | "slippage_percent": 0 41 | }, 42 | "sell": { 43 | "slippage_percent": 0 44 | } 45 | } 46 | } 47 | }, 48 | "pair_buttons": [ "USD", "USDT", "USDC", "BUSD" ], 49 | "pair_blacklist": [ "BTC/*", "WBTC/*", "ETH/*", "CBETH/*", "UST/*", "USD/*", "USDC/*", "USDD/*", "USDT/*", "BUSD/*", "GUSD/*", "TUSD/*", "PYUSD/*", "DAI/*", "EURC/*", "EUROC/*", "PAX/*" ] 50 | }, 51 | "signals": { 52 | "3CQS": { 53 | "enabled": true, 54 | "api_key": "" 55 | } 56 | }, 57 | "ai": { 58 | "ollama": { 59 | "enabled": true, 60 | "host": "", 61 | "model": "" 62 | } 63 | }, 64 | "verbose_log": true 65 | } 66 | -------------------------------------------------------------------------------- /config/bot.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiKey": "", 3 | "apiPassphrase": null, 4 | "apiPassword": null, 5 | "apiSecret": "", 6 | "dcaMaxOrder": 46, 7 | "dcaOrderAmount": 45, 8 | "dcaOrderSizeMultiplier": 1.08, 9 | "dcaOrderStartDistance": 1.3, 10 | "dcaOrderStepPercent": 1.3, 11 | "dcaOrderStepPercentMultiplier": 1.0, 12 | "dcaTakeProfitPercent": 1.5, 13 | "profitCurrency": "quote", 14 | "exchange": "coinbasepro", 15 | "exchangeFee": 0.45, 16 | "exchangeOptions": { "defaultType": "spot" }, 17 | "firstOrderAmount": 20, 18 | "firstOrderLimitPrice": 1300, 19 | "firstOrderType": "MARKET", 20 | "pair": "", 21 | "sandBox": true, 22 | "sandBoxWallet": 100 23 | } 24 | -------------------------------------------------------------------------------- /config/hub.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3100, 3 | "password": "", 4 | "instances": [] 5 | } -------------------------------------------------------------------------------- /disclaimer.txt: -------------------------------------------------------------------------------- 1 | All investment strategies and investments involve risk of loss. All information found here, including any ideas, opinions, views, predictions, forecasts, or suggestions, expressed or implied herein, are for informational, entertainment or educational purposes only and should not be construed as personal investment advice. Conduct your own due diligence, or consult a licensed financial advisor or broker before making any and all investment decisions. Any investments, trades, speculations, or discussions made on the basis of any information found here, expressed or implied herein, are committed at your own risk, financial or otherwise. 2 | 3 | By using the software, you acknowledge that you should only invest money you are prepared to lose. The authors and affiliates are not responsible for any trading results, and you use the software at your own risk. 4 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build - Use full node.js parent image 2 | FROM node:22 AS builder 3 | 4 | # Set working directory 5 | WORKDIR /usr/src/app 6 | 7 | # Process package.json / *lock.json first (leverage Docker cache) 8 | COPY package*.json ./ 9 | 10 | # Install only PROD dependencies 11 | RUN npm install --omit=dev 12 | 13 | # Copy app contents into working directory 14 | COPY . . 15 | 16 | 17 | # Install - Use slimmer Alpine image 18 | FROM node:22-alpine 19 | 20 | # Set working directory 21 | WORKDIR /usr/src/app 22 | 23 | # Copy application and node_modules from builder stage 24 | COPY --from=builder /usr/src/app . 25 | 26 | # Add curl to Alpine image 27 | RUN apk add --no-cache curl 28 | 29 | # Create non-root container user | prepare dir structure | grant ownership 30 | RUN addgroup -S symbot && adduser -S symbot -G symbot && \ 31 | mkdir -p /usr/src/app/logs && chown -R symbot:symbot /usr/src/app 32 | 33 | # Switch to non-root user 34 | USER symbot:symbot 35 | 36 | # Environment variables 37 | ENV DOCKER_RUNNING=true \ 38 | MONGODB_URI='mongodb://localhost:27017/symbot' 39 | 40 | # Expose the application port 41 | EXPOSE 3000 42 | 43 | # Start application 44 | CMD ["sh", "-c", "echo 'Warming up' && sleep 5 && exec npm start"] -------------------------------------------------------------------------------- /docker/Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .git/ 3 | node_modules/ 4 | config/server.json 5 | -------------------------------------------------------------------------------- /docker/db-init.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: "symbot", 3 | pwd: "symbot123", 4 | roles: [ { role: "dbOwner", db: "symbot" } ] 5 | }) 6 | 7 | db.users.insert({ 8 | name: "symbot" 9 | }) 10 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | database: 4 | image: mongo 5 | restart: always 6 | environment: 7 | MONGO_INITDB_ROOT_USERNAME: root 8 | MONGO_INITDB_ROOT_PASSWORD: symbotmongodb123 9 | MONGO_INITDB_DATABASE: symbot 10 | volumes: 11 | - ./db-init.js:/docker-entrypoint-initdb.d/init.js:ro 12 | 13 | mongo-express: 14 | image: mongo-express 15 | restart: always 16 | ports: 17 | - 3010:8081 18 | environment: 19 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 20 | ME_CONFIG_MONGODB_ADMINPASSWORD: symbotmongodb123 21 | ME_CONFIG_MONGODB_SERVER: database 22 | depends_on: 23 | - database 24 | 25 | web: 26 | image: symbot 27 | container_name: symbot 28 | restart: always 29 | build: 30 | context: .. 31 | dockerfile: ./docker/Dockerfile 32 | environment: 33 | # Use the username and password found in the db-init.js file instead of the root username. 34 | MONGODB_URI: mongodb://symbot:symbot123@database/symbot 35 | depends_on: 36 | - database 37 | ports: 38 | - 3000:3000 39 | healthcheck: 40 | test: ["CMD", "curl", "-f", "http://127.0.0.1:3000"] 41 | interval: 30s 42 | timeout: 5s 43 | retries: 3 44 | -------------------------------------------------------------------------------- /libs/ai/OllamaClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Ollama } = require('ollama'); 4 | 5 | let ollama; 6 | let modelCurrent; 7 | let shareData; 8 | 9 | 10 | const modelDefault = 'llama3.2'; 11 | const TIMEOUT_MS = 75000; 12 | const maxHistory = 25; 13 | const maxMessageAge = 2 * (60 * 60 * 1000); 14 | const hoursInterval = 1; 15 | 16 | // Map to store conversation history for each room 17 | const conversationHistory = new Map(); 18 | 19 | 20 | setInterval(() => { 21 | 22 | cleanupRooms(); 23 | 24 | }, (hoursInterval * (60 * 60 * 1000))); 25 | 26 | 27 | const streamChatResponse = async ({ room, model, message, abortSignal, reset }) => { 28 | 29 | let fullResponse = ''; 30 | 31 | // Reset conversation context if requested 32 | if (reset) { 33 | 34 | const resetMessage = { 35 | role: 'system', 36 | content: 'Reset the conversation context.', 37 | timestamp: Date.now(), 38 | }; 39 | 40 | // Reset the history for the room 41 | conversationHistory.set(room, [resetMessage]); 42 | //console.log('History reset for room:', room); 43 | } 44 | 45 | // Get the current conversation history for the room 46 | const history = conversationHistory.get(room) || []; 47 | 48 | // Append user message to history 49 | const userMessage = { 50 | role: 'user', 51 | content: message.content, 52 | timestamp: Date.now(), 53 | }; 54 | 55 | history.push(userMessage); 56 | 57 | // Ensure the history is not more than maxHistory 58 | if (history.length > maxHistory) { 59 | 60 | history.shift(); 61 | } 62 | 63 | // Proceed with chat response generation 64 | try { 65 | const response = await ollama.chat({ 66 | model: model, 67 | stream: true, 68 | messages: history, 69 | }); 70 | 71 | for await (const part of response) { 72 | 73 | if (abortSignal.aborted) { 74 | 75 | throw new Error('Stream aborted due to timeout'); 76 | } 77 | 78 | const content = part.message.content; 79 | 80 | // Accumulate content for the full response 81 | fullResponse += content; 82 | 83 | sendMessage(room, content); 84 | } 85 | 86 | // Add the assistant's response to the conversation history 87 | const assistantMessage = { 88 | role: 'assistant', 89 | content: fullResponse, 90 | timestamp: Date.now(), 91 | }; 92 | 93 | history.push(assistantMessage); 94 | 95 | // Ensure the history is not more than maxHistory after adding the assistant's message 96 | if (history.length > maxHistory) { 97 | 98 | history.shift(); 99 | } 100 | 101 | // Signal end of chat for the current conversation 102 | sendMessage(room, 'END_OF_CHAT'); 103 | 104 | const logObj = { 105 | room, 106 | message, 107 | response: fullResponse, 108 | }; 109 | 110 | shareData.Common.logger('Ollama Request: ' + JSON.stringify(logObj)); 111 | 112 | // Update the conversation history for the room 113 | conversationHistory.set(room, history); 114 | } 115 | catch (err) { 116 | 117 | if (abortSignal.aborted) { 118 | 119 | sendMessage(room, 'Stream aborted due to timeout'); 120 | } 121 | else { 122 | 123 | throw err; 124 | } 125 | } 126 | }; 127 | 128 | 129 | const streamChatResponseWithTimeout = async ({ room, model, message, reset }) => { 130 | 131 | const abortController = new AbortController(); 132 | 133 | const timeout = setTimeout(() => { abortController.abort(); }, TIMEOUT_MS); 134 | 135 | try { 136 | await streamChatResponse({ 137 | room, 138 | model, 139 | message, 140 | abortSignal: abortController.signal, 141 | reset, 142 | }); 143 | } 144 | finally { 145 | 146 | clearTimeout(timeout); 147 | } 148 | }; 149 | 150 | 151 | async function streamChat(data) { 152 | 153 | let room; 154 | let model = modelCurrent; 155 | 156 | try { 157 | const parsedData = JSON.parse(data); 158 | 159 | room = parsedData.message.room; 160 | 161 | if (parsedData.message.model) { 162 | 163 | model = parsedData.message.model; 164 | } 165 | 166 | const message = { 167 | role: 'user', 168 | content: parsedData.message.content, 169 | }; 170 | 171 | const reset = parsedData.message.reset || false; // Check for reset flag 172 | 173 | await streamChatResponseWithTimeout({ 174 | room, 175 | model, 176 | message, 177 | reset, 178 | }); 179 | } 180 | catch (err) { 181 | 182 | sendError(room, err.message); 183 | } 184 | } 185 | 186 | 187 | async function sendMessage(room, msg) { 188 | 189 | shareData.Common.sendSocketMsg({ 190 | room, 191 | type: 'message', 192 | message: msg, 193 | }); 194 | } 195 | 196 | 197 | async function sendError(room, msg) { 198 | 199 | const logData = 'Ollama Error: ' + msg; 200 | 201 | shareData.Common.logger(logData); 202 | sendMessage(room, logData); 203 | } 204 | 205 | 206 | function start(host, model) { 207 | 208 | if (model != undefined && model != null && model != '') { 209 | 210 | modelCurrent = model; 211 | } 212 | else { 213 | 214 | modelCurrent = modelDefault; 215 | } 216 | 217 | try { 218 | ollama = new Ollama({ 219 | 'host': host, 220 | }); 221 | } 222 | catch (err) { 223 | 224 | sendError('', err.message); 225 | } 226 | } 227 | 228 | 229 | function stop() { 230 | 231 | if (ollama) { 232 | 233 | try { 234 | ollama.abort(); 235 | ollama = null; 236 | } 237 | catch (e) {} 238 | } 239 | } 240 | 241 | 242 | function cleanupRooms() { 243 | 244 | const now = Date.now(); 245 | 246 | conversationHistory.forEach((history, room) => { 247 | // Remove messages older than maxMessageAge 248 | const filteredHistory = history.filter(msg => (now - msg.timestamp) <= maxMessageAge); 249 | 250 | // If the history becomes empty after filtering, delete the room's history 251 | if (filteredHistory.length === 0) { 252 | 253 | conversationHistory.delete(room); 254 | //console.log('Removed empty history for room:', room); 255 | } 256 | else { 257 | 258 | conversationHistory.set(room, filteredHistory); 259 | //console.log('History cleaned and updated for room:', room, filteredHistory); 260 | } 261 | }); 262 | } 263 | 264 | 265 | module.exports = { 266 | start, 267 | stop, 268 | streamChat, 269 | 270 | init: function(obj) { 271 | shareData = obj; 272 | } 273 | }; -------------------------------------------------------------------------------- /libs/app/Hub/Main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const colors = require('colors'); 6 | 7 | const pathRoot = path.resolve(__dirname, '..', '..', '..'); 8 | 9 | let Worker; 10 | let shutDownFunction; 11 | let shareData; 12 | 13 | 14 | 15 | function processWorkerMessage(workerId, instanceName) { 16 | 17 | // Messsages received from worker 18 | 19 | return (message) => { 20 | 21 | if (message.type === 'log') { 22 | 23 | shareData.Hub.logger('info', message.data); 24 | } 25 | else if (message.type === 'memory') { 26 | 27 | const workerInfo = shareData.workerMap.get(workerId); 28 | 29 | if (workerInfo) { 30 | 31 | let msgObj = { 32 | 'instanceId': workerInfo.instance.id, 33 | 'instanceName': instanceName, 34 | 'workerId': workerId, 35 | 'threadId': workerInfo.threadId, 36 | 'memoryUsage': { 37 | 'rss': message.data.rss, 38 | 'heapTotal': message.data.heapTotal, 39 | 'heapUsed': message.data.heapUsed 40 | } 41 | }; 42 | 43 | // Send memory usage to client 44 | shareData.Common.sendSocketMsg({ 45 | 46 | 'room': 'memory', 47 | 'type': 'log_memory', 48 | 'message': msgObj 49 | }); 50 | } 51 | else { 52 | 53 | shareData.Hub.logger('error', `Information for Worker ID ${workerId} not found.`); 54 | } 55 | } 56 | else if (message.type === 'deals_active') { 57 | 58 | //console.log(message.data); 59 | } 60 | else if (message.type === 'system_pause_all') { 61 | 62 | // Worker sent system pause for all instances 63 | shareData.Hub.logger('info', `Worker ID ${workerId} [${instanceName}] requested system pause for all instances`); 64 | 65 | // Relay message to all workers 66 | for (const { worker } of shareData.workerMap.values()) { 67 | 68 | worker.postMessage({ 69 | type: 'system_pause', 70 | data: message.data 71 | }); 72 | } 73 | } 74 | else if (message.type === 'shutdown_hub') { 75 | 76 | // Worker sent global Hub shutdown 77 | shareData.Hub.logger('info', `Worker ID ${workerId} [${instanceName}] requested Hub shutdown`); 78 | 79 | shutDownFunction(); 80 | } 81 | }; 82 | } 83 | 84 | 85 | function processWorkerExit(workerId) { 86 | 87 | return (code) => { 88 | 89 | shareData.Hub.logger('info', colors.red.bold(`Instance exited with code ${code}, Worker ID: ${workerId}`)); 90 | 91 | const workerInfo = shareData.workerMap.get(workerId); 92 | 93 | if (workerInfo) { 94 | 95 | const { instance } = workerInfo; 96 | 97 | const instanceName = instance.name; 98 | 99 | shareData.workerMap.delete(workerId); 100 | 101 | if (code !== 0) { 102 | 103 | shareData.Hub.logger('error', colors.red.bold(`Instance for ${instanceName} exited with code ${code}.`)); 104 | 105 | // Optionally restart the instance 106 | // startWorker(instance); 107 | } 108 | else { 109 | 110 | shareData.Hub.logger('info', colors.green.bold(`Instance for ${instanceName} completed successfully.`)); 111 | } 112 | } 113 | else { 114 | 115 | shareData.Hub.logger('error', colors.red.bold(`Worker ID ${workerId} does not exist in workerMap.`)); 116 | } 117 | }; 118 | } 119 | 120 | 121 | function startWorker(instanceData) { 122 | 123 | const workerId = shareData.Common.uuidv4(); 124 | const instanceName = instanceData.name; 125 | const currentDate = new Date().toISOString(); 126 | 127 | instanceData.dateStart = currentDate; 128 | 129 | const worker = new Worker(shareData.appData.hub_filename, { 130 | workerData: { 131 | ...instanceData, 132 | workerId 133 | } 134 | }); 135 | 136 | worker.on('message', processWorkerMessage(workerId, instanceName)); 137 | worker.on('error', (error) => Hub.logger('error', `Instance for ${instanceName} encountered an error:`, error)); 138 | worker.on('exit', processWorkerExit(workerId)); 139 | 140 | worker.once('online', () => { 141 | 142 | shareData.Hub.logger('info', `Instance: ${instanceName} (Worker ID: ${workerId}, Thread ID: ${worker.threadId}) started`); 143 | 144 | // Store worker and instanceData in workerMap 145 | shareData.workerMap.set(workerId, { 146 | worker, 147 | instance: instanceData, 148 | threadId: worker.threadId 149 | }); 150 | }); 151 | } 152 | 153 | 154 | async function startAllWorkers(configs) { 155 | 156 | for (const config of configs) { 157 | 158 | const serverIdInUse = [...shareData.workerMap.values()].some(worker => worker.instance.server_id === (config.overrides.server_id || null)); 159 | 160 | if (!serverIdInUse) { 161 | 162 | const enabled = config['enabled']; 163 | const startBoot = config['start_boot']; 164 | 165 | if (enabled && startBoot) { 166 | 167 | startWorker({ 168 | //instanceId: config.id, 169 | //instanceName: config.name, 170 | ...config 171 | }); 172 | 173 | await shareData.Common.delay(1000); 174 | } 175 | } 176 | else { 177 | 178 | shareData.Hub.logger('info', `Instance for ${config.name} already running.`); 179 | } 180 | } 181 | } 182 | 183 | 184 | async function start(configs) { 185 | 186 | startAllWorkers(configs); 187 | } 188 | 189 | 190 | module.exports = { 191 | 192 | start, 193 | startWorker, 194 | get shutDown() { 195 | return shutDownFunction; 196 | }, 197 | 198 | init: function(WorkerInit, shareDataInit, shutDown) { 199 | 200 | Worker = WorkerInit; 201 | shareData = shareDataInit; 202 | shutDownFunction = shutDown; 203 | } 204 | }; 205 | -------------------------------------------------------------------------------- /libs/app/Hub/Worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const colors = require('colors'); 6 | 7 | const pathRoot = path.resolve(__dirname, '..', '..', '..'); 8 | 9 | let parentPort; 10 | let shutdownTimeout; 11 | 12 | 13 | 14 | async function processWorkerTask(instanceData) { 15 | 16 | // Worker thread logic 17 | 18 | try { 19 | 20 | const instanceName = instanceData.name; 21 | const prefData = `[WORKER-LOG] [${instanceName}] `; 22 | 23 | // Override all console methods to send messages back to the main thread 24 | ['log', 'error', 'warn', 'info', 'debug'].forEach((method) => { 25 | 26 | console[method] = (...args) => parentPort.postMessage({ 27 | type: 'log', 28 | level: method, // 'log', 'error', 'warn', etc. 29 | data: prefData + args.join(' ') 30 | }); 31 | }); 32 | 33 | console.log(colors.bgBlack.brightYellow.bold(`Starting Instance: ${instanceName}`)); 34 | 35 | const SymBot = require(path.join(pathRoot, 'symbot.js')); 36 | 37 | SymBot.setInstanceConfig(Object.assign({}, 38 | instanceData, 39 | { shutdownTimeout } 40 | )); 41 | 42 | SymBot.setInstanceParentPort(parentPort); 43 | 44 | await SymBot.start(); 45 | 46 | console.log(colors.bgBlack.brightGreen.bold(`Finished Starting Instance: ${instanceName}`)); 47 | 48 | // Listen for command requests from the main thread 49 | parentPort.on('message', (message) => { 50 | 51 | processWorkerTaskMessage(SymBot, message); 52 | }); 53 | 54 | } 55 | catch (error) { 56 | 57 | // Log the error and inform the main thread 58 | console.log(colors.bgBlack.brightRed.bold(`Error performing task for ${instanceData.name}: ${error.message}`)); 59 | } 60 | } 61 | 62 | 63 | async function processWorkerTaskMessage(SymBot, message) { 64 | 65 | // Get worker instance memory usage 66 | if (message.type === 'memory') { 67 | 68 | const memoryUsage = process.memoryUsage(); 69 | 70 | parentPort.postMessage({ 71 | 72 | type: 'memory', 73 | data: memoryUsage 74 | }); 75 | } 76 | 77 | // Get worker instance active deals 78 | if (message.type === 'deals_active') { 79 | 80 | const deals = await SymBot.DCABot.getActiveDeals(); 81 | 82 | parentPort.postMessage({ 83 | 84 | type: 'deals_active_received', 85 | id: message.id, 86 | data: { 87 | 'name': message.name, 88 | 'deals': deals 89 | } 90 | }); 91 | } 92 | 93 | // System pause received for SymBot worker 94 | if (message.type === 'system_pause') { 95 | 96 | parentPort.postMessage({ 97 | 98 | type: 'system_pause_received' 99 | }); 100 | 101 | const data = message.data; 102 | 103 | const isPause = data.pause; 104 | const pauseMessage = data.message; 105 | 106 | await SymBot.System.pause(isPause, pauseMessage); 107 | } 108 | 109 | // Shutdown received for SymBot worker 110 | if (message.type === 'shutdown') { 111 | 112 | parentPort.postMessage({ 113 | 114 | type: 'shutdown_received' 115 | }); 116 | 117 | SymBot.shutDown(); 118 | } 119 | } 120 | 121 | 122 | async function start(instanceData) { 123 | 124 | processWorkerTask(instanceData); 125 | } 126 | 127 | 128 | module.exports = { 129 | 130 | start, 131 | 132 | init: function(parentPortInit, shutdownTimeoutInit) { 133 | 134 | parentPort = parentPortInit; 135 | shutdownTimeout = shutdownTimeoutInit; 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /libs/app/Queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async'); 4 | 5 | let shareData; 6 | 7 | 8 | async function create(concurrency) { 9 | 10 | const queue = async.queue((task, completed) => { 11 | 12 | const req = task.req; 13 | const res = task.res; 14 | const taskName = task.name; 15 | const initCallBack = task.init_callback; 16 | 17 | //console.log('Queued task: ' + taskName); 18 | 19 | if (taskName == 'start_deal') { 20 | 21 | initCallBack(req, res, { 'task': task, 'completed': completed }); 22 | } 23 | else { 24 | 25 | const remaining = queue.length(); 26 | 27 | return completed('Unknown task name', { task, remaining }); 28 | } 29 | 30 | }, concurrency); 31 | 32 | 33 | const add = async function(taskObj, initCallBack) { 34 | 35 | taskObj['init_callback'] = initCallBack; 36 | 37 | queue.push(taskObj, (error, { task, remaining }) => { 38 | 39 | if (error) { 40 | 41 | let msg = `Queue error: Task ${task.name}: ${error}`; 42 | 43 | shareData.Common.logger(msg); 44 | 45 | try { 46 | 47 | const res = task.res; 48 | 49 | let obj = { 50 | 51 | 'date': new Date(), 52 | 'error': msg 53 | }; 54 | 55 | res.status(404).send(obj); 56 | } 57 | catch(e) {} 58 | } 59 | else { 60 | 61 | //const data = JSON.stringify(task.data); 62 | //console.log(`Finished task ${task.name}. Data: ${data}. ${remaining} tasks remaining`); 63 | } 64 | }); 65 | } 66 | 67 | 68 | const callBack = async function(res, resObj, taskObj) { 69 | 70 | let task = taskObj['task']; 71 | let completed = taskObj['completed']; 72 | 73 | task.data = resObj; 74 | 75 | const remaining = queue.length(); 76 | 77 | completed(null, { task, remaining }); 78 | 79 | if (res) { 80 | 81 | try { 82 | 83 | res.send(resObj); 84 | } 85 | catch(e) {} 86 | } 87 | } 88 | 89 | 90 | queue.drain(() => { 91 | 92 | //console.log('Queue complete.'); 93 | }); 94 | 95 | 96 | return { 'queue': queue, 'add': add, 'callBack': callBack }; 97 | } 98 | 99 | 100 | 101 | module.exports = { 102 | 103 | create, 104 | 105 | init: function(obj) { 106 | 107 | shareData = obj; 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /libs/mongodb/DCABotSchema.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const BotSchema = new Schema({ 5 | active: Boolean, 6 | botId: { type: String, unique: true }, 7 | botName: String, 8 | config: Object, 9 | date: Date, 10 | }, { 11 | collection: 'bots', 12 | timestamps: true 13 | }); 14 | 15 | 16 | const DealSchema = new Schema({ 17 | active: Boolean, 18 | canceled: Boolean, 19 | paused: Boolean, 20 | pausedBuy: Boolean, 21 | pausedSell: Boolean, 22 | panicSell: Boolean, 23 | botId: String, 24 | botName: String, 25 | dealId: { type: String, unique: true }, 26 | exchange: String, 27 | pair: String, 28 | market: String, 29 | date: Date, 30 | status: Number, 31 | config: Object, 32 | sellData: Object, 33 | orders: Object, 34 | isStart: Number, 35 | dealCount: Number, 36 | dealMax: Number 37 | }, { 38 | collection: 'deals', 39 | timestamps: true 40 | }); 41 | 42 | 43 | //DealSchema.index({ 'sellData.date': 1 }); 44 | //DealSchema.index({ 'sellData.date': 1, 'status': 1 }); 45 | //DealSchema.index({ 'sellData.date': -1, 'status': 1 }); 46 | 47 | 48 | module.exports = { 49 | 50 | 'Bots': mongoose.model('Bots', BotSchema), 51 | 'Deals': mongoose.model('Deals', DealSchema) 52 | }; 53 | 54 | -------------------------------------------------------------------------------- /libs/mongodb/ServerSchema.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const ServerSchema = new Schema({ 5 | serverId: String, 6 | created: Date, 7 | data: Object, 8 | }, { 9 | collection: 'server', 10 | timestamps: true 11 | }); 12 | 13 | 14 | 15 | module.exports = { 16 | 17 | 'ServerSchema': mongoose.model('ServerSchema', ServerSchema) 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /libs/mongodb/Signals3CQSSchema.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const Signals3CQSSchema = new Schema({ 5 | bot_id: { type: String, required: true }, 6 | signal_id: { type: String, required: true }, 7 | signal_id_parent: String, 8 | signal_data: Object, 9 | created: Date, 10 | created_parent: Date 11 | }, { 12 | collection: 'signals_3cqs', 13 | timestamps: true 14 | }); 15 | 16 | 17 | Signals3CQSSchema.index({ bot_id: 1, signal_id: 1 }, { unique: true }); 18 | 19 | 20 | module.exports = { 21 | 22 | 'Signals3CQSSchema': mongoose.model('Signals3CQSSchema', Signals3CQSSchema) 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /libs/mongodb/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | 4 | let shareData; 5 | 6 | 7 | 8 | async function start(url) { 9 | 10 | mongoose.Promise = Promise; 11 | 12 | mongoose.connection.on('connected', () => { 13 | 14 | delete shareData.appData.database_error; 15 | 16 | shareData.Common.logger('Database Connected', true); 17 | }); 18 | 19 | mongoose.connection.on('reconnected', () => { 20 | 21 | delete shareData.appData.database_error; 22 | 23 | let msg = 'Database Reconnected'; 24 | 25 | log(msg); 26 | }); 27 | 28 | mongoose.connection.on('disconnected', () => { 29 | 30 | let msg = 'Database Disconnected'; 31 | 32 | shareData.appData.database_error = msg; 33 | 34 | log(msg); 35 | }); 36 | 37 | mongoose.connection.on('close', () => { 38 | 39 | let msg = 'Database Closed'; 40 | 41 | shareData.appData.database_error = msg; 42 | 43 | log(msg); 44 | }); 45 | 46 | mongoose.connection.on('error', error => { 47 | 48 | let msg = 'Database Error: ' + JSON.stringify(error); 49 | 50 | log(msg); 51 | }); 52 | 53 | mongoose.set('strictQuery', false); 54 | 55 | const run = async () => { 56 | 57 | await mongoose.connect( 58 | url, { } 59 | ); 60 | 61 | return true; 62 | }; 63 | 64 | let started = await run().catch(error => log('Database Run Error: ' + JSON.stringify(error))); 65 | 66 | return started; 67 | } 68 | 69 | 70 | async function log(msg) { 71 | 72 | shareData.Common.logger(msg, true); 73 | 74 | shareData.Common.sendNotification({ 'message': msg, 'type': 'database', 'telegram_id': shareData.appData.telegram_id }); 75 | } 76 | 77 | 78 | module.exports = { 79 | 80 | start, 81 | mongoose, 82 | 83 | init: function(obj) { 84 | 85 | shareData = obj; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /libs/signals/3CQS/signals.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "author": "3CQS.com", 4 | "info": "Valid 3CQS API key required to use", 5 | "description": "Trading signals from 3CQS", 6 | "provider_id": "3CQS", 7 | "provider_name": "3CQS" 8 | }, 9 | "start_conditions": [{ 10 | "id": 12, 11 | "description": "SymRank Top 10" 12 | }, 13 | { 14 | "id": 2, 15 | "description": "SymRank Top 30" 16 | }, 17 | { 18 | "id": 11, 19 | "description": "SymRank Top 50" 20 | }, 21 | { 22 | "id": 1, 23 | "description": "SymRank Top 100 Triple Tracker" 24 | }, 25 | { 26 | "id": 6, 27 | "description": "SymRank Top 100 Quadruple Tracker" 28 | }, 29 | { 30 | "id": 7, 31 | "description": "SymRank Top 250 Quadruple Tracker" 32 | }, 33 | { 34 | "id": 13, 35 | "description": "SymScore Super Bullish" 36 | }, 37 | { 38 | "id": 22, 39 | "description": "SymScore Super Bullish Range" 40 | }, 41 | { 42 | "id": 29, 43 | "description": "SymScore Super-Hyper Bullish Range" 44 | }, 45 | { 46 | "id": 14, 47 | "description": "SymScore Hyper Bullish" 48 | }, 49 | { 50 | "id": 23, 51 | "description": "SymScore Hyper Bullish Range" 52 | }, 53 | { 54 | "id": 27, 55 | "description": "SymScore Hyper-Ultra Bullish Range" 56 | }, 57 | { 58 | "id": 15, 59 | "description": "SymScore Ultra Bullish" 60 | }, 61 | { 62 | "id": 25, 63 | "description": "SymScore Ultra Bullish Range" 64 | }, 65 | { 66 | "id": 31, 67 | "description": "SymScore Ultra-X-Treme Bullish Range" 68 | }, 69 | { 70 | "id": 16, 71 | "description": "SymScore X-Treme Bullish" 72 | }, 73 | { 74 | "id": 54, 75 | "description": "SymScore Neutral" 76 | }, 77 | { 78 | "id": 17, 79 | "description": "SymScore Super Bearish" 80 | }, 81 | { 82 | "id": 21, 83 | "description": "SymScore Super Bearish Range" 84 | }, 85 | { 86 | "id": 30, 87 | "description": "SymScore Super-Hyper Bearish Range" 88 | }, 89 | { 90 | "id": 18, 91 | "description": "SymScore Hyper Bearish" 92 | }, 93 | { 94 | "id": 24, 95 | "description": "SymScore Hyper Bearish Range" 96 | }, 97 | { 98 | "id": 28, 99 | "description": "SymScore Hyper-Ultra Bearish Range" 100 | }, 101 | { 102 | "id": 19, 103 | "description": "SymScore Ultra Bearish" 104 | }, 105 | { 106 | "id": 26, 107 | "description": "SymScore Ultra Bearish Range" 108 | }, 109 | { 110 | "id": 32, 111 | "description": "SymScore Ultra-X-Treme Bearish Range" 112 | }, 113 | { 114 | "id": 20, 115 | "description": "SymScore X-Treme Bearish" 116 | }, 117 | { 118 | "id": 39, 119 | "description": "SymSense Super Greed" 120 | }, 121 | { 122 | "id": 48, 123 | "description": "SymSense Super Greed Range" 124 | }, 125 | { 126 | "id": 55, 127 | "description": "SymSense Super-Hyper Greed Range" 128 | }, 129 | { 130 | "id": 40, 131 | "description": "SymSense Hyper Greed" 132 | }, 133 | { 134 | "id": 49, 135 | "description": "SymSense Hyper Greed Range" 136 | }, 137 | { 138 | "id": 56, 139 | "description": "SymSense Hyper-Ultra Greed Range" 140 | }, 141 | { 142 | "id": 41, 143 | "description": "SymSense Ultra Greed" 144 | }, 145 | { 146 | "id": 50, 147 | "description": "SymSense Ultra Greed Range" 148 | }, 149 | { 150 | "id": 57, 151 | "description": "SymSense Ultra-X-Treme Greed Range" 152 | }, 153 | { 154 | "id": 42, 155 | "description": "SymSense X-Treme Greed" 156 | }, 157 | { 158 | "id": 43, 159 | "description": "SymSense Neutral" 160 | }, 161 | { 162 | "id": 44, 163 | "description": "SymSense Super Fear" 164 | }, 165 | { 166 | "id": 51, 167 | "description": "SymSense Super Fear Range" 168 | }, 169 | { 170 | "id": 58, 171 | "description": "SymSense Super-Hyper Fear Range" 172 | }, 173 | { 174 | "id": 45, 175 | "description": "SymSense Hyper Fear" 176 | }, 177 | { 178 | "id": 52, 179 | "description": "SymSense Hyper Fear Range" 180 | }, 181 | { 182 | "id": 59, 183 | "description": "SymSense Hyper-Ultra Fear Range" 184 | }, 185 | { 186 | "id": 46, 187 | "description": "SymSense Ultra Fear" 188 | }, 189 | { 190 | "id": 53, 191 | "description": "SymSense Ultra Fear Range" 192 | }, 193 | { 194 | "id": 60, 195 | "description": "SymSense Ultra-X-Treme Fear Range" 196 | }, 197 | { 198 | "id": 47, 199 | "description": "SymSense X-Treme Fear" 200 | }, 201 | { 202 | "id": 61, 203 | "description": "SymSync 100" 204 | }, 205 | { 206 | "id": 62, 207 | "description": "SymSync 90" 208 | }, 209 | { 210 | "id": 63, 211 | "description": "SymSync 80" 212 | }, 213 | { 214 | "id": 64, 215 | "description": "SymSync 70" 216 | }, 217 | { 218 | "id": 65, 219 | "description": "SymSync 60" 220 | }, 221 | { 222 | "id": 66, 223 | "description": "SymSync 50" 224 | }, 225 | { 226 | "id": 9, 227 | "description": "Super Volatility" 228 | }, 229 | { 230 | "id": 33, 231 | "description": "Super Volatility Range" 232 | }, 233 | { 234 | "id": 36, 235 | "description": "Super-Hyper Volatility Range" 236 | }, 237 | { 238 | "id": 10, 239 | "description": "Super Volatility Double Tracker" 240 | }, 241 | { 242 | "id": 3, 243 | "description": "Hyper Volatility" 244 | }, 245 | { 246 | "id": 34, 247 | "description": "Hyper Volatility Range" 248 | }, 249 | { 250 | "id": 37, 251 | "description": "Hyper-Ultra Volatility Range" 252 | }, 253 | { 254 | "id": 8, 255 | "description": "Hyper Volatility Double Tracker" 256 | }, 257 | { 258 | "id": 4, 259 | "description": "Ultra Volatility" 260 | }, 261 | { 262 | "id": 35, 263 | "description": "Ultra Volatility Range" 264 | }, 265 | { 266 | "id": 38, 267 | "description": "Ultra-X-Treme Volatility Range" 268 | }, 269 | { 270 | "id": 5, 271 | "description": "X-Treme Volatility" 272 | } 273 | ], 274 | "start_conditions_sub": [{ 275 | "id": "sym_rank", 276 | "description": "SymRank" 277 | }, 278 | { 279 | "id": "sym_score", 280 | "description": "SymScore" 281 | }, 282 | { 283 | "id": "sym_sense", 284 | "description": "SymSense" 285 | }, 286 | { 287 | "id": "sym_senser", 288 | "description": "SymSenser" 289 | }, 290 | { 291 | "id": "sym_sync", 292 | "description": "SymSync" 293 | }, 294 | { 295 | "id": "market_cap_rank", 296 | "description": "Market Cap Rank" 297 | }, 298 | { 299 | "id": "volatility_score", 300 | "description": "Volatility Score" 301 | }, 302 | { 303 | "id": "price_action_score", 304 | "description": "Price Action Score" 305 | }, 306 | { 307 | "id": "rsi14_15m", 308 | "description": "RSI-14 15m" 309 | } 310 | ] 311 | } 312 | -------------------------------------------------------------------------------- /libs/strategies/DCABot/telegram/dealComplete.json: -------------------------------------------------------------------------------- 1 | { 2 | "profit": "{BOT_NAME} ({PAIR}): Deal {DEAL_ID} Complete. Profit: {PROFIT} ({PROFIT_PERCENT}% from total volume 💰) #profit {DURATION}", 3 | "loss": "{BOT_NAME} ({PAIR}): Deal {DEAL_ID} Complete. Profit: {PROFIT} ({PROFIT_PERCENT}% from total volume 😱) #profit #loss {DURATION}" 4 | } 5 | -------------------------------------------------------------------------------- /libs/telegram/help.txt: -------------------------------------------------------------------------------- 1 | Below are the available commands you can use with {APP_NAME} 2 | 3 | /uptime Show length of time {APP_NAME} has been running 4 | 5 | /help Show this help message 6 | -------------------------------------------------------------------------------- /libs/telegram/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | let pathRoot = path.dirname(fs.realpathSync(__dirname)).split(path.sep).join(path.posix.sep); 7 | pathRoot = pathRoot.substring(0, pathRoot.lastIndexOf('/')); 8 | 9 | const { Telegraf } = require('telegraf'); 10 | 11 | 12 | let initSuccess = true; 13 | 14 | let bot; 15 | let shareData; 16 | 17 | 18 | 19 | async function initApp(tokenId) { 20 | 21 | bot = new Telegraf(tokenId, { handlerTimeout: 100 }); 22 | 23 | bot.command('start', (ctx) => { 24 | 25 | startCommand(ctx); 26 | }); 27 | 28 | 29 | bot.command('help', (ctx) => { 30 | 31 | helpCommand(ctx); 32 | }); 33 | 34 | 35 | bot.command('uptime', (ctx) => { 36 | 37 | let id = ctx.from.id; 38 | let text = ctx.message.text; 39 | 40 | let dateStart = shareData.appData.started; 41 | 42 | let upTime = shareData.Common.timeDiff(new Date(), new Date(dateStart)); 43 | 44 | sendMessage(id, shareData.appData.name + ' v' + shareData.appData.version + ' running for ' + upTime); 45 | }); 46 | 47 | 48 | bot.on('message', (ctx) => { 49 | 50 | let id = ctx.from.id; 51 | 52 | sendMessage(id, 'Unknown command. Use /help to show available commands'); 53 | }); 54 | 55 | 56 | bot.catch(e => { 57 | 58 | shareData.Common.logger('Telegram Error: ' + JSON.stringify(e)); 59 | }); 60 | 61 | 62 | bot.launch() 63 | .then(() => { 64 | initSuccess = true; 65 | }) 66 | .catch(err => { 67 | initSuccess = false; 68 | logError(err, ''); 69 | }); 70 | } 71 | 72 | 73 | async function startCommand(ctx) { 74 | 75 | let id = ctx.from.id; 76 | 77 | sendMessage(id, 'Welcome to ' + shareData.appData.name); 78 | } 79 | 80 | 81 | async function helpCommand(ctx) { 82 | 83 | let data; 84 | let id = ctx.from.id; 85 | 86 | let fileName = pathRoot + '/libs/telegram/help.txt'; 87 | 88 | try { 89 | 90 | data = fs.readFileSync(fileName, 'utf8'); 91 | } 92 | catch(e) { 93 | 94 | } 95 | 96 | data = data.replace(/\{APP_NAME\}/g, shareData.appData.name); 97 | 98 | sendMessage(id, data); 99 | } 100 | 101 | 102 | async function sendMessage(id, msg) { 103 | 104 | if (initSuccess) { 105 | 106 | if (id != shareData.appData.telegram_id) { 107 | 108 | msg = 'You are not authorized to access ' + shareData.appData.name; 109 | } 110 | 111 | if (bot && shareData.appData.telegram_enabled) { 112 | 113 | bot.telegram.sendMessage(id, msg).catch(err => logError(err, id)); 114 | } 115 | } 116 | } 117 | 118 | 119 | function logError(err, data) { 120 | 121 | let logData = 'Message: ' + JSON.stringify(err.message) + ' Stack: ' + JSON.stringify(err.stack) + ' Data: ' + JSON.stringify(data); 122 | 123 | shareData.Common.logger('Telegram Error: ' + logData); 124 | } 125 | 126 | 127 | function start(tokenId, enabled) { 128 | 129 | if (enabled && (tokenId != undefined && tokenId != null && tokenId != '')) { 130 | 131 | initApp(tokenId); 132 | } 133 | else { 134 | 135 | initSuccess = false; 136 | } 137 | } 138 | 139 | 140 | function stop() { 141 | 142 | try { 143 | 144 | bot.stop(); 145 | } 146 | catch(e) {} 147 | 148 | bot = null; 149 | initSuccess = false; 150 | } 151 | 152 | 153 | module.exports = { 154 | 155 | start, 156 | stop, 157 | sendMessage, 158 | 159 | init: function(obj) { 160 | 161 | shareData = obj; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /libs/webserver/Hub/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const pathRoot = path.dirname(fs.realpathSync(__dirname)).split(path.sep).join(path.posix.sep); 7 | 8 | const crypto = require('crypto'); 9 | const express = require('express'); 10 | const session = require('express-session'); 11 | const FileStore = require('session-file-store')(session); 12 | const bodyParser = require('body-parser'); 13 | const Routes = require(pathRoot + '/Hub/routes.js'); 14 | const { createProxyMiddleware } = require('http-proxy-middleware'); 15 | 16 | const app = express(); 17 | const router = express.Router(); 18 | 19 | const proxyMap = new Map(); 20 | 21 | let socket; 22 | let shareData; 23 | 24 | 25 | 26 | async function initApp() { 27 | 28 | const sessionExpireMins = 60 * 24; 29 | const sessionCookieName = 'SymBotHub'; 30 | 31 | const hashPassword = crypto.createHash('sha256').update(shareData.appData.password).digest('hex'); 32 | 33 | const sessionMiddleware = session({ 34 | 35 | 'secret': hashPassword, 36 | 'name': sessionCookieName, 37 | 'resave': false, 38 | 'saveUninitialized': false, 39 | 'store': new FileStore({ 40 | 'path': shareData.appData.path_root + '/sessions', 41 | 'logFn': function() {} 42 | }), 43 | 'cookie': { 44 | 'expires': (sessionExpireMins * 60) * 1000 45 | } 46 | }); 47 | 48 | // Middleware to handle incoming requests 49 | app.use('/instance/:appId', async (req, res, next) => { 50 | 51 | const { appId } = req.params; 52 | 53 | /* 54 | console.log(`Received request for appId: ${appId}`); 55 | console.log(`Original URL: ${req.originalUrl}`); 56 | console.log(`Method: ${req.method}`); 57 | console.log(`Request Body:`, req.body); 58 | */ 59 | 60 | const proxyMiddleware = await getProxyMiddleware(appId); 61 | 62 | if (proxyMiddleware.success) { 63 | 64 | const proxy = proxyMiddleware.proxy; 65 | 66 | return proxy(req, res, next); 67 | } 68 | else { 69 | 70 | const err = proxyMiddleware.error; 71 | 72 | const msg = 'Error during proxying: ' + err; 73 | 74 | shareData.Hub.logger('error', msg); 75 | 76 | if (!res.headersSent) { 77 | 78 | return res.status(500).send(msg); 79 | } 80 | } 81 | }); 82 | 83 | app.use(sessionMiddleware); 84 | 85 | app.disable('x-powered-by'); 86 | 87 | app.use((req, res, next) => { 88 | 89 | res.append('Server', 'SymBot Hub'); 90 | next(); 91 | }); 92 | 93 | app.use(express.json()); 94 | 95 | app.use(bodyParser.urlencoded({ 96 | extended: true 97 | })); 98 | 99 | app.use(bodyParser.json()); 100 | 101 | app.set('views', pathRoot + '/public/views'); 102 | app.set('view engine', 'ejs'); 103 | 104 | app.use('/js', express.static(pathRoot + '/public/js')); 105 | app.use('/css', express.static(pathRoot + '/public/css')); 106 | app.use('/data', express.static(pathRoot + '/public/data')); 107 | app.use('/images', express.static(pathRoot + '/public/images')); 108 | 109 | app.use('/', router); 110 | 111 | return { sessionMiddleware }; 112 | } 113 | 114 | 115 | // Create or get the proxy middleware for an appId 116 | const getProxyMiddleware = async (appId) => { 117 | 118 | let success = true; 119 | 120 | let isError; 121 | let proxyFound; 122 | 123 | if (!proxyMap.has(appId)) { 124 | 125 | const port = await getAppPort(appId); 126 | 127 | if (!port) { 128 | 129 | success = false; 130 | 131 | isError = `No matching port found for appId: ${appId}`; 132 | } 133 | 134 | if (success) { 135 | 136 | const targetUrl = `http://127.0.0.1:${port}`; 137 | 138 | //console.log(`Creating proxy middleware for ${appId} targeting ${targetUrl}`); 139 | 140 | const proxyMiddleware = createProxyMiddleware({ 141 | target: targetUrl, 142 | changeOrigin: true, 143 | followRedirects: false, 144 | autoRewrite: true, 145 | hostRewrite: true, 146 | cookieDomainRewrite: true, 147 | ws: true, 148 | pathRewrite: (path) => path.replace(`/instance/${appId}`, ''), 149 | timeout: 120000, 150 | proxyTimeout: 120000, 151 | on: { 152 | proxyReq: (proxyReq, req) => { 153 | 154 | //console.log('Proxy Request:', req.method, req.originalUrl); 155 | 156 | const realIp = req.headers['x-forwarded-for'] 157 | ? `${req.headers['x-forwarded-for']}, ${req.socket.remoteAddress}` 158 | : req.socket.remoteAddress; 159 | 160 | proxyReq.setHeader('X-Forwarded-For', realIp); 161 | 162 | if (req.headers.cookie) { 163 | 164 | proxyReq.setHeader('Cookie', req.headers.cookie); 165 | } 166 | }, 167 | proxyRes: (proxyRes, req, res) => { 168 | 169 | //console.log('Response received:', proxyRes.statusCode); 170 | 171 | if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400) { 172 | 173 | const location = proxyRes.headers['location']; 174 | const newLocation = `/instance/${appId}${location}`; 175 | 176 | //console.log(`Redirecting to: ${location}`); 177 | 178 | return res.redirect(proxyRes.statusCode, newLocation); 179 | } 180 | }, 181 | error: (err, req, res) => { 182 | 183 | let msg = 'Proxy Error: ' + JSON.stringify(err.message); 184 | 185 | shareData.Hub.logger('error', msg); 186 | 187 | if (res && res.status && !res.headersSent) { 188 | 189 | return res.status(500).send(msg); 190 | } 191 | else { 192 | 193 | //console.error('Proxy response object is not valid:', res); 194 | shareData.Hub.logger('error', 'Proxy response object is not valid'); 195 | } 196 | }, 197 | }, 198 | }); 199 | 200 | proxyMap.set(appId, proxyMiddleware); 201 | } 202 | } 203 | 204 | if (success) { 205 | 206 | proxyFound = proxyMap.get(appId); 207 | } 208 | 209 | return ( { 'success': success, 'error': isError, 'proxy': proxyFound } ); 210 | }; 211 | 212 | 213 | async function getAppPort(appId) { 214 | 215 | const ports = shareData.appData['web_server_ports']; 216 | 217 | for (let port of ports) { 218 | 219 | if (port == appId) { 220 | 221 | return port; 222 | } 223 | } 224 | 225 | return undefined; 226 | } 227 | 228 | 229 | async function initSocket(sessionMiddleware, server) { 230 | 231 | socket = require('socket.io')(server, { 232 | 233 | cors: { 234 | origin: '*', 235 | methods: ['PUT', 'GET', 'POST', 'DELETE', 'OPTIONS'], 236 | credentials: false 237 | }, 238 | path: '/' + shareData.appData['web_socket_path'], 239 | serveClient: false, 240 | pingInterval: 10000, 241 | pingTimeout: 5000, 242 | maxHttpBufferSize: 1e6, 243 | cookie: false 244 | }); 245 | 246 | const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); 247 | 248 | socket.use(wrap(sessionMiddleware)); 249 | 250 | socket.use((client, next) => { 251 | 252 | return next(); 253 | }); 254 | 255 | socket.on('connect', (client) => { 256 | 257 | let clientId = client.id; 258 | let loggedIn = client.request.session.loggedIn; 259 | let query = client.handshake.query; 260 | 261 | //console.log('Connected ID:', clientId, loggedIn); 262 | 263 | if (!loggedIn) { 264 | 265 | client.emit('error', 'Unauthorized'); 266 | client.disconnect(); 267 | } 268 | else { 269 | 270 | if (query.room == undefined || query.room == null || query.room == '') { 271 | 272 | client.join(roomAuth); 273 | } else { 274 | 275 | client.join(query.room); 276 | } 277 | 278 | client.on('joinRooms', (data) => { 279 | 280 | data.rooms.forEach(room => { 281 | 282 | client.join(room); 283 | //console.log(`Client joined room: ${room}`); 284 | }); 285 | }); 286 | 287 | client.on('leaveRoom', (room) => { 288 | 289 | client.leave(room); 290 | //console.log(`Client left room: ${room}`); 291 | }); 292 | 293 | client.on('notifications_history', function(data) { 294 | 295 | //shareData.Common.getNotificationHistory(client, data); 296 | }); 297 | } 298 | }); 299 | } 300 | 301 | 302 | async function getSocket() { 303 | 304 | return socket; 305 | } 306 | 307 | 308 | async function start(port) { 309 | 310 | let isError; 311 | 312 | const { sessionMiddleware } = await initApp(); 313 | 314 | let server = app.listen(port, () => { 315 | 316 | shareData.Hub.logger('info', `SymBot Hub running on port ${port}`); 317 | 318 | }).on('error', function(err) { 319 | 320 | isError = err; 321 | 322 | if (err.code === 'EADDRINUSE') { 323 | 324 | shareData.Hub.logger('error', `Port ${port} already in use`); 325 | 326 | process.exit(1); 327 | } 328 | else { 329 | 330 | shareData.Hub.logger('error', 'Web Server Error: ' + err); 331 | } 332 | }); 333 | 334 | /* 335 | server.on('upgrade', async (req, socket, head) => { 336 | 337 | const segments = req.url.split('/'); 338 | 339 | const appId = segments[2]; 340 | 341 | try { 342 | const proxyMiddleware = await getProxyMiddleware(appId); 343 | // Call the proxy middleware for WebSocket upgrade 344 | proxyMiddleware.upgrade(req, socket, head); 345 | } catch (err) { 346 | console.error('Error during WebSocket upgrade:', err); 347 | socket.destroy(); // Close the socket on error 348 | } 349 | }); 350 | */ 351 | 352 | if (isError == undefined || isError == null) { 353 | 354 | await initSocket(sessionMiddleware, server); 355 | 356 | Routes.start(router); 357 | } 358 | } 359 | 360 | 361 | 362 | module.exports = { 363 | 364 | app, 365 | start, 366 | getSocket, 367 | 368 | init: function(obj) { 369 | 370 | shareData = obj; 371 | Routes.init(shareData); 372 | } 373 | } -------------------------------------------------------------------------------- /libs/webserver/Hub/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | let shareData; 6 | 7 | 8 | function initRoutes(router) { 9 | 10 | router.get('/', (req, res) => { 11 | 12 | res.set('Cache-Control', 'no-store'); 13 | 14 | if (req.session.loggedIn) { 15 | 16 | shareData.Common.renderView('Hub/homeView', req, res, true); 17 | } 18 | else { 19 | 20 | res.redirect('/login'); 21 | } 22 | }); 23 | 24 | 25 | router.get('/login', (req, res) => { 26 | 27 | res.set('Cache-Control', 'no-store'); 28 | 29 | res.render( 'loginView', { 'isHub': true, 'appData': shareData.appData } ); 30 | }); 31 | 32 | 33 | router.post('/login', (req, res) => { 34 | 35 | res.set('Cache-Control', 'no-store'); 36 | 37 | shareData.Common.verifyLogin(req, res, true); 38 | }); 39 | 40 | 41 | router.get('/logout', (req, res) => { 42 | 43 | res.set('Cache-Control', 'no-store'); 44 | 45 | req.session.destroy((err) => {}); 46 | 47 | res.redirect('/login'); 48 | }); 49 | 50 | 51 | router.get('/manage', async (req, res) => { 52 | 53 | res.set('Cache-Control', 'no-store'); 54 | 55 | if (req.session.loggedIn) { 56 | 57 | let configs; 58 | 59 | const hubData = await shareData.Common.getConfig(shareData.appData.hub_config); 60 | 61 | if (hubData.success) { 62 | 63 | configs = hubData.data.instances; 64 | 65 | const processData = await shareData.Hub.processConfig(configs); 66 | 67 | if (processData.success) { 68 | 69 | configs = processData.configs; 70 | } 71 | } 72 | 73 | const exchanges = await shareData.Hub.getExchanges(); 74 | 75 | res.render('Hub/manageView', { 76 | 'isHub': true, 'configs': configs, 'appData': shareData.appData, 'exchanges': exchanges, 'numFormatter': shareData.Common.numFormatter 77 | }); 78 | } 79 | else { 80 | 81 | res.redirect('/login'); 82 | } 83 | }); 84 | 85 | 86 | router.post('/update_instances', async (req, res) => { 87 | 88 | res.set('Cache-Control', 'no-store'); 89 | 90 | if (req.session.loggedIn) { 91 | 92 | shareData.Hub.routeUpdateInstances(req, res); 93 | } 94 | else { 95 | 96 | res.redirect('/login'); 97 | } 98 | }); 99 | 100 | 101 | router.post('/add_instance', async (req, res) => { 102 | 103 | res.set('Cache-Control', 'no-store'); 104 | 105 | if (req.session.loggedIn) { 106 | 107 | shareData.Hub.routeAddInstance(req, res); 108 | } 109 | else { 110 | 111 | res.redirect('/login'); 112 | } 113 | }); 114 | 115 | 116 | router.post('/start_instance', async (req, res) => { 117 | 118 | res.set('Cache-Control', 'no-store'); 119 | 120 | if (req.session.loggedIn) { 121 | 122 | shareData.Hub.routeStartWorker(req, res); 123 | } 124 | else { 125 | 126 | res.redirect('/login'); 127 | } 128 | }); 129 | 130 | 131 | router.get('/news', (req, res) => { 132 | 133 | res.set('Cache-Control', 'no-store'); 134 | 135 | if (req.session.loggedIn) { 136 | 137 | shareData.Hub.routeShowNews(req, res); 138 | } 139 | else { 140 | 141 | res.redirect('/login'); 142 | } 143 | }); 144 | 145 | 146 | router.get('/config', (req, res) => { 147 | 148 | res.set('Cache-Control', 'no-store'); 149 | 150 | res.render( 'Hub/configView', { 'isHub': true, 'appData': shareData.appData } ); 151 | }); 152 | 153 | 154 | router.post('/config', (req, res) => { 155 | 156 | res.set('Cache-Control', 'no-store'); 157 | 158 | if (req.session.loggedIn) { 159 | 160 | shareData.Hub.routeUpdateConfig(req, res); 161 | } 162 | else { 163 | 164 | res.redirect('/login'); 165 | } 166 | }); 167 | 168 | 169 | router.get([ '/logs', '/backups' ], (req, res) => { 170 | 171 | res.set('Cache-Control', 'no-store'); 172 | 173 | const type = req.path.replace('/', ''); 174 | 175 | if (req.session.loggedIn) { 176 | 177 | shareData.Common.showFiles(type, req, res, true); 178 | } 179 | else { 180 | 181 | res.redirect('/login'); 182 | } 183 | }); 184 | 185 | 186 | router.get([ '/logs/download/:file', '/backups/download/:file' ], (req, res) => { 187 | 188 | res.set('Cache-Control', 'no-store'); 189 | 190 | if (req.session.loggedIn) { 191 | 192 | const fileName = req.params.file; 193 | const type = req.path.includes('/logs/') ? 'logs' : 'backups'; 194 | 195 | shareData.Common.downloadFile(fileName, type, req, res); 196 | } 197 | else { 198 | 199 | res.redirect('/login'); 200 | } 201 | }); 202 | 203 | 204 | router.all('*wildcard', (req, res) => { 205 | 206 | redirectNotFound(res); 207 | }); 208 | } 209 | 210 | 211 | function redirectNotFound(res) { 212 | 213 | let obj = { 214 | 215 | 'error': 'Not Found' 216 | }; 217 | 218 | res.status(404).send(obj); 219 | } 220 | 221 | 222 | function start(router) { 223 | 224 | initRoutes(router); 225 | } 226 | 227 | 228 | module.exports = { 229 | 230 | start, 231 | 232 | init: function(obj) { 233 | 234 | shareData = obj; 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /libs/webserver/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const pathRoot = path.dirname(fs.realpathSync(__dirname)).split(path.sep).join(path.posix.sep); 7 | 8 | const bodyParser = require('body-parser'); 9 | const compression = require('compression'); 10 | const cookieParser = require('cookie-parser'); 11 | const express = require('express'); 12 | const session = require('express-session'); 13 | const multer = require('multer'); 14 | const MongoDBStore = require('connect-mongodb-session')(session); 15 | const app = express(); 16 | const router = express.Router(); 17 | const Routes = require(pathRoot + '/webserver/routes.js'); 18 | 19 | const serverTimeoutMins = 3; 20 | 21 | let shareData; 22 | let socket; 23 | 24 | 25 | 26 | const shouldCompress = (req, res) => { 27 | 28 | if (req.headers['x-no-compression']) { 29 | 30 | return false; 31 | } 32 | 33 | return compression.filter(req, res); 34 | } 35 | 36 | 37 | 38 | function initApp() { 39 | 40 | const sessionExpireMins = 60 * 24; 41 | const sessionCookieName = 'SymBot' + shareData.appData.instance_name; 42 | 43 | let sessionSecret = shareData.appData.server_id; 44 | 45 | let store; 46 | 47 | if (!shareData.appData.config_mode) { 48 | 49 | store = new MongoDBStore({ 50 | 51 | 'uri': shareData.appData.mongo_db_url, 52 | 'collection': 'sessions' 53 | }, 54 | function(err) { 55 | 56 | if (err) { 57 | 58 | shareData.Common.logger(JSON.stringify(err)); 59 | } 60 | }); 61 | } 62 | else { 63 | 64 | sessionSecret = 'SymBot' + Math.floor(Math.random() * 1000000) + 1; 65 | 66 | const FileStore = require('session-file-store')(session); 67 | 68 | store = new FileStore({ 69 | 'path': path.join(pathRoot, '..', 'sessions'), 70 | 'logFn': function() {} 71 | }); 72 | } 73 | 74 | const sessionMiddleware = session({ 75 | 76 | 'secret': sessionSecret, 77 | 'name': sessionCookieName, 78 | 'resave': false, 79 | 'saveUninitialized': false, 80 | 'store': store, 81 | 'cookie': { 82 | 'expires': (sessionExpireMins * 60) * 1000 83 | } 84 | }); 85 | 86 | app.disable('x-powered-by'); 87 | 88 | app.use(sessionMiddleware); 89 | 90 | // Compress all HTTP responses 91 | app.use(compression({ 92 | 93 | filter: shouldCompress, 94 | level: 6, 95 | 96 | })); 97 | 98 | app.use(function(err, req, res, next) { 99 | 100 | shareData.Common.logger(JSON.stringify(err.stack)); 101 | }); 102 | 103 | app.set('views', pathRoot + '/webserver/public/views'); 104 | app.set('view engine', 'ejs'); 105 | 106 | app.use('/js', express.static(pathRoot + '/webserver/public/js')); 107 | app.use('/css', express.static(pathRoot + '/webserver/public/css')); 108 | app.use('/data', express.static(pathRoot + '/webserver/public/data')); 109 | app.use('/images', express.static(pathRoot + '/webserver/public/images')); 110 | 111 | app.use(express.json()); 112 | 113 | app.use(cookieParser()); 114 | 115 | app.use((req, res, next) => { 116 | 117 | const allowedRoutes = ['/login', '/config']; 118 | 119 | const timeOut = (60 * 1000) * serverTimeoutMins; 120 | 121 | req.setTimeout((timeOut - (1000 * 5))); 122 | res.append('Server', shareData.appData.name + ' v' + shareData.appData.version); 123 | 124 | if (shareData.appData.config_mode && allowedRoutes.length > 0 && !allowedRoutes.includes(req.path)) { 125 | 126 | //return res.status(403).send('Access Forbidden: This route is not allowed.'); 127 | res.redirect('/login'); 128 | 129 | return; 130 | } 131 | 132 | if (shareData.appData.database_error || shareData.appData.system_pause) { 133 | 134 | let obj = { 135 | 'date': new Date(), 136 | 'error': shareData.appData.database_error || shareData.appData.system_pause 137 | }; 138 | 139 | res.status(503).send(obj); 140 | } 141 | else { 142 | 143 | next(); 144 | } 145 | }); 146 | 147 | const upload = multer({ 148 | dest: 'uploads/', 149 | limits: { fileSize: 100000000 } 150 | }); 151 | 152 | app.use(bodyParser.json({ 153 | 154 | limit: "100mb", 155 | extended: true 156 | 157 | })); 158 | 159 | app.use(bodyParser.urlencoded({ 160 | 161 | limit: "100mb", 162 | extended: true, 163 | parameterLimit: 500000 164 | 165 | })); 166 | 167 | app.use('/', router); 168 | 169 | return { sessionMiddleware, upload }; 170 | } 171 | 172 | 173 | function initSocket(sessionMiddleware, server) { 174 | 175 | socket = require('socket.io')(server, { 176 | 177 | cors: { 178 | origin: '*', 179 | methods: ['PUT', 'GET', 'POST', 'DELETE', 'OPTIONS'], 180 | credentials: false 181 | }, 182 | path: '/' + shareData.appData['web_socket_path'], 183 | serveClient: false, 184 | pingInterval: 10000, 185 | pingTimeout: 5000, 186 | maxHttpBufferSize: 1e6, 187 | cookie: false 188 | }); 189 | 190 | 191 | const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); 192 | 193 | socket.use(wrap(sessionMiddleware)); 194 | 195 | socket.use((client, next) => { 196 | 197 | return next(); 198 | }); 199 | 200 | socket.on('connect', function (client) { 201 | 202 | let clientId = client.id; 203 | let loggedIn = client.request.session.loggedIn; 204 | let query = client.handshake.query; 205 | 206 | //console.log('Connected ID:', clientId, loggedIn); 207 | 208 | if (!loggedIn) { 209 | 210 | client.emit('error', 'Unauthorized'); 211 | client.disconnect(); 212 | } 213 | else { 214 | 215 | if (query.room == undefined || query.room == null || query.room == '') { 216 | 217 | client.join(roomAuth); 218 | } 219 | else { 220 | 221 | client.join(query.room); 222 | } 223 | 224 | client.on('joinRooms', (data) => { 225 | 226 | data.rooms.forEach(room => { 227 | 228 | client.join(room); 229 | //console.log(`Client joined room: ${room}`); 230 | }); 231 | }); 232 | 233 | client.on('leaveRoom', (room) => { 234 | 235 | client.leave(room); 236 | //console.log(`Client left room: ${room}`); 237 | }); 238 | 239 | client.on('notifications_history', function (data) { 240 | 241 | shareData.Common.getNotificationHistory(client, data); 242 | }); 243 | } 244 | }); 245 | } 246 | 247 | 248 | async function getSocket() { 249 | 250 | return socket; 251 | } 252 | 253 | 254 | function start(port) { 255 | 256 | let isError; 257 | 258 | const { sessionMiddleware, upload } = initApp(); 259 | 260 | let server = app.listen(port, () => { 261 | 262 | shareData.Common.logger(`${shareData.appData.name} v${shareData.appData.version} listening on port ${port}`, true); 263 | 264 | }).on('error', function(err) { 265 | 266 | isError = err; 267 | 268 | if (err.code === 'EADDRINUSE') { 269 | 270 | shareData.Common.logger(`Port ${port} already in use`, true); 271 | 272 | shareData.System.shutDown(); 273 | } 274 | else { 275 | 276 | shareData.Common.logger('Web Server Error: ' + err, true); 277 | } 278 | }); 279 | 280 | if (isError == undefined || isError == null) { 281 | 282 | const serverTimeout = (60 * 1000) * serverTimeoutMins; 283 | 284 | const keepAliveTimeout = serverTimeout - (1000 * 5); 285 | const headersTimeout = keepAliveTimeout + (1000 * 3); 286 | 287 | server.setTimeout(serverTimeout); 288 | 289 | server.keepAliveTimeout = keepAliveTimeout; 290 | server.headersTimeout = headersTimeout; 291 | 292 | initSocket(sessionMiddleware, server); 293 | 294 | Routes.start(router, upload); 295 | } 296 | } 297 | 298 | 299 | module.exports = { 300 | 301 | app, 302 | start, 303 | getSocket, 304 | 305 | init: function(obj) { 306 | 307 | shareData = obj; 308 | 309 | Routes.init(shareData); 310 | } 311 | } 312 | 313 | -------------------------------------------------------------------------------- /libs/webserver/public/css/style-news.css: -------------------------------------------------------------------------------- 1 | #articles-container { 2 | display: flex; 3 | flex-wrap: wrap; /* Allows items to wrap to the next line */ 4 | justify-content: center; /* Center the items */ 5 | border-radius: 5px; 6 | background-color: var(--table-background-color); 7 | } 8 | 9 | .article-card { 10 | flex: 1 1 300px; /* Each card takes up at least 300px */ 11 | margin: 10px; /* Margin around each card */ 12 | max-width: calc(33.33% - 20px); /* Allows up to 3 cards per row */ 13 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 14 | border-radius: 5px; 15 | overflow: hidden; 16 | background-color: var(--content-box-background-color); /* Background for cards */ 17 | position: relative; /* Positioning for hover effects */ 18 | } 19 | 20 | .card-img-top { 21 | height: 200px; /* Fixed height for consistent image size */ 22 | width: 100%; /* Full width of the card */ 23 | object-fit: cover; /* Ensures the image covers the area without stretching */ 24 | } 25 | 26 | .card-image-placeholder { 27 | height: 200px; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | background-color: #717171; /* Placeholder background */ 32 | color: #d3d4d6; 33 | } 34 | 35 | .card-body { 36 | padding: 15px; 37 | } 38 | 39 | .card-title { 40 | font-size: 1.5rem; /* Enlarging the title */ 41 | cursor: pointer; /* Pointer cursor for titles */ 42 | margin-bottom: 10px; /* Spacing below the title */ 43 | } 44 | 45 | .card-title a { 46 | text-decoration: none; /* Remove underline from links */ 47 | color: var(--content-box-color); /* Link color */ 48 | } 49 | 50 | .card-title a:hover { 51 | text-decoration: underline; /* Underline on hover */ 52 | } 53 | 54 | .card-text { 55 | font-size: 1.3rem; /* Enlarging the description text */ 56 | margin: 10px 0; /* Margin for spacing */ 57 | } 58 | 59 | .article-published-info { 60 | font-size: 1.4rem; /* Enlarging the published by text */ 61 | color: #6c757d; /* Muted text color */ 62 | } 63 | 64 | .article-link { 65 | cursor: pointer; /* Pointer cursor for images and titles */ 66 | } 67 | 68 | #search-input { 69 | width: 100%; 70 | max-width: 400px; 71 | margin: 20px auto; 72 | padding: 10px; 73 | font-size: 1.2rem; 74 | border: 1px solid #ccc; 75 | border-radius: 5px; 76 | display: block; 77 | outline: none; /* Removes the blue border on focus */ 78 | } 79 | 80 | #search-input:focus { 81 | outline: none; /* Ensures no outline on focus */ 82 | border-color: #999; /* Optional: Change border color on focus if needed */ 83 | } 84 | 85 | @media (max-width: 768px) { 86 | .article-card { 87 | max-width: calc(50% - 20px); /* 2 cards per row on tablets */ 88 | } 89 | } 90 | 91 | @media (max-width: 576px) { 92 | .article-card { 93 | max-width: 100%; /* 1 card per row on mobile */ 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/jquery-ui-timepicker-addon/jquery-ui-timepicker-addon.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery Timepicker Addon - v1.6.3 - 2016-04-20 2 | * http://trentrichardson.com/examples/timepicker 3 | * Copyright (c) 2016 Trent Richardson; Licensed MIT */ 4 | 5 | .ui-timepicker-div .ui-widget-header{margin-bottom:8px}.ui-timepicker-div dl{text-align:left}.ui-timepicker-div dl dt{float:left;clear:left;padding:0 0 0 5px}.ui-timepicker-div dl dd{margin:0 10px 10px 40%}.ui-timepicker-div td{font-size:90%}.ui-tpicker-grid-label{background:0 0;border:0;margin:0;padding:0}.ui-timepicker-div .ui_tpicker_unit_hide{display:none}.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input{background:0 0;color:inherit;border:0;outline:0;border-bottom:solid 1px #555;width:95%}.ui-timepicker-div .ui_tpicker_time .ui_tpicker_time_input:focus{border-bottom-color:#aaa}.ui-timepicker-rtl{direction:rtl}.ui-timepicker-rtl dl{text-align:right;padding:0 5px 0 0}.ui-timepicker-rtl dl dt{float:right;clear:right}.ui-timepicker-rtl dl dd{margin:0 40% 10px 10px}.ui-timepicker-div.ui-timepicker-oneLine{padding-right:2px}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time,.ui-timepicker-div.ui-timepicker-oneLine dt{display:none}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_time_label{display:block;padding-top:2px}.ui-timepicker-div.ui-timepicker-oneLine dl{text-align:right}.ui-timepicker-div.ui-timepicker-oneLine dl dd,.ui-timepicker-div.ui-timepicker-oneLine dl dd>div{display:inline-block;margin:0}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_minute:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_second:before{content:':';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_millisec:before,.ui-timepicker-div.ui-timepicker-oneLine dl dd.ui_tpicker_microsec:before{content:'.';display:inline-block}.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide,.ui-timepicker-div.ui-timepicker-oneLine .ui_tpicker_unit_hide:before{display:none} -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3cqs-coder/SymBot/72447369cafbf34b2499714f58728f4bab290a84/libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_555555_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3cqs-coder/SymBot/72447369cafbf34b2499714f58728f4bab290a84/libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_555555_256x240.png -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_777620_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3cqs-coder/SymBot/72447369cafbf34b2499714f58728f4bab290a84/libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_777620_256x240.png -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_777777_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3cqs-coder/SymBot/72447369cafbf34b2499714f58728f4bab290a84/libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_777777_256x240.png -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_cc0000_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3cqs-coder/SymBot/72447369cafbf34b2499714f58728f4bab290a84/libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_cc0000_256x240.png -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3cqs-coder/SymBot/72447369cafbf34b2499714f58728f4bab290a84/libs/webserver/public/css/vendor/jquery-ui/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/simple-switch/simple-switch.css: -------------------------------------------------------------------------------- 1 | .simple-switch-outter { 2 | width: 40px; 3 | height: 20px; 4 | background-color: #fff; 5 | border: 1px solid #dfdfdf; 6 | cursor: pointer; 7 | display: inline-block; 8 | position: relative; 9 | vertical-align: middle; 10 | border-radius: 20px; 11 | box-sizing: content-box; 12 | background-clip: content-box; 13 | } 14 | 15 | .simple-switch-outter .simple-switch { 16 | display: none; 17 | } 18 | 19 | .simple-switch-outter .simple-switch-circle { 20 | background: #fff; 21 | border-radius: 100%; 22 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); 23 | height: 20px; 24 | position: absolute; 25 | top: 0; 26 | width: 20px; 27 | } 28 | 29 | .simple-switch-outter.unchecked { 30 | box-shadow: #dfdfdf 0px 0px 0px 0px inset; 31 | border-color: #dfdfdf; 32 | transition: border 0.4s, box-shadow 0.4s; 33 | background-color: #ffffff; 34 | } 35 | 36 | .simple-switch-outter.unchecked .simple-switch-circle { 37 | left: 0px; 38 | transition: background-color 0.4s, left 0.2s; 39 | } 40 | 41 | .simple-switch-outter.checked { 42 | border-color: #5daacd; 43 | box-shadow: #5daacd 0px 0px 0px 16px inset; 44 | transition: border 0.4s, box-shadow 0.4s, background-color 1.2s; 45 | background-color: #5daacd; 46 | } 47 | 48 | .simple-switch-outter.checked .simple-switch-circle { 49 | left: 20px; 50 | transition: background-color 0.4s, left 0.2s; 51 | background-color: #ffffff; 52 | } 53 | -------------------------------------------------------------------------------- /libs/webserver/public/css/vendor/tablesorter/filter.formatter.min.css: -------------------------------------------------------------------------------- 1 | .tablesorter .tablesorter-filter-row td{text-align:center;font-size:.9em;font-weight:400}.tablesorter .ui-slider,.tablesorter input.range{width:90%;margin:2px auto 2px auto;font-size:.8em}.tablesorter .ui-slider{top:12px}.tablesorter .ui-slider .ui-slider-handle{width:.9em;height:.9em}.tablesorter .ui-datepicker{font-size:.8em}.tablesorter .ui-slider-horizontal{height:.5em}.tablesorter .value-popup:after{content:attr(data-value);position:absolute;bottom:14px;left:-7px;min-width:18px;height:12px;background-color:#444;background-image:-webkit-gradient(linear,left top,left bottom,from(#444),to(#999));background-image:-webkit-linear-gradient(top,#444,#999);background-image:-moz-linear-gradient(top,#444,#999);background-image:-o-linear-gradient(top,#444,#999);background-image:linear-gradient(to bottom,#444,#999);-webkit-border-radius:3px;border-radius:3px;-webkit-background-clip:padding-box;background-clip:padding-box;-webkit-box-shadow:0 0 4px 0 #777;box-shadow:0 0 4px 0 #777;border:#444 1px solid;color:#fff;font:1em/1.1em Arial,Sans-Serif;padding:1px;text-align:center}.tablesorter .value-popup:before{content:"";position:absolute;width:0;height:0;border-top:8px solid #777;border-left:8px solid transparent;border-right:8px solid transparent;top:-8px;left:50%;margin-left:-8px;margin-top:-1px}.tablesorter .dateFrom,.tablesorter .dateTo{width:80px;margin:2px 5px}.tablesorter .button{width:14px;height:14px;background:#fcfff4;background:-webkit-linear-gradient(top,#fcfff4 0,#dfe5d7 40%,#b3bead 100%);background:-moz-linear-gradient(top,#fcfff4 0,#dfe5d7 40%,#b3bead 100%);background:-o-linear-gradient(top,#fcfff4 0,#dfe5d7 40%,#b3bead 100%);background:-ms-linear-gradient(top,#fcfff4 0,#dfe5d7 40%,#b3bead 100%);background:linear-gradient(top,#fcfff4 0,#dfe5d7 40%,#b3bead 100%);margin:1px 5px 1px 1px;-webkit-border-radius:25px;-moz-border-radius:25px;border-radius:25px;-webkit-box-shadow:inset 0 1px 1px #fff,0 1px 3px rgba(0,0,0,.5);-moz-box-shadow:inset 0 1px 1px #fff,0 1px 3px rgba(0,0,0,.5);box-shadow:inset 0 1px 1px #fff,0 1px 3px rgba(0,0,0,.5);position:relative;top:3px;display:inline-block}.tablesorter .button label{cursor:pointer;position:absolute;width:10px;height:10px;-webkit-border-radius:25px;-moz-border-radius:25px;border-radius:25px;left:2px;top:2px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.5),0 1px 0 rgba(255,255,255,1);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.5),0 1px 0 rgba(255,255,255,1);box-shadow:inset 0 1px 1px rgba(0,0,0,.5),0 1px 0 rgba(255,255,255,1);background:#45484d;background:-webkit-linear-gradient(top,#222 0,#45484d 100%);background:-moz-linear-gradient(top,#222 0,#45484d 100%);background:-o-linear-gradient(top,#222 0,#45484d 100%);background:-ms-linear-gradient(top,#222 0,#45484d 100%);background:linear-gradient(top,#222 0,#45484d 100%)}.tablesorter .button label:after{opacity:0;content:'';position:absolute;width:8px;height:8px;background:#55f;background:-webkit-linear-gradient(top,#aaf 0,#55f 100%);background:-moz-linear-gradient(top,#aaf 0,#55f 100%);background:-o-linear-gradient(top,#aaf 0,#55f 100%);background:-ms-linear-gradient(top,#aaf 0,#55f 100%);background:linear-gradient(top,#aaf 0,#55f 100%);-webkit-border-radius:25px;-moz-border-radius:25px;border-radius:25px;top:1px;left:1px;-webkit-box-shadow:inset 0 1px 1px #fff,0 1px 3px rgba(0,0,0,.5);-moz-box-shadow:inset 0 1px 1px #fff,0 1px 3px rgba(0,0,0,.5);box-shadow:inset 0 1px 1px #fff,0 1px 3px rgba(0,0,0,.5)}.tablesorter .button label:hover::after{opacity:.3}.tablesorter .button input[type=checkbox]{visibility:hidden}.tablesorter .button input[type=checkbox]:checked+label:after{opacity:1}.tablesorter .colorpicker{width:30px;height:18px}.tablesorter .ui-spinner-input{width:100px;height:18px}.tablesorter .currentColor,.tablesorter .ui-spinner{position:relative}.tablesorter input.number{position:relative}.tablesorter .tablesorter-filter-row.hideme td *{height:1px;min-height:0;border:0;padding:0;margin:0;opacity:0} -------------------------------------------------------------------------------- /libs/webserver/public/data/tradingViewData.json: -------------------------------------------------------------------------------- 1 | { 2 | "intervals": [ 3 | "1-1m", 4 | "5-5m", 5 | "15-15m", 6 | "30-30m", 7 | "60-1h", 8 | "120-2h", 9 | "180-3h", 10 | "240-4h", 11 | "D-Day", 12 | "W-Week", 13 | "M-Month" 14 | ], 15 | "bar_styles": [ 16 | "0-Bars", 17 | "1-Candles", 18 | "2-Line", 19 | "3-Area", 20 | "4-Renko", 21 | "5-Kagi", 22 | "6-Point & Figure", 23 | "7-Line Break", 24 | "8-Heikin Ashi", 25 | "9-Hollow Candles" 26 | ], 27 | "studies": [ 28 | { 29 | "id": "ACCD@tv-basicstudies", 30 | "_name": "Accumulation/Distribution" 31 | }, 32 | { 33 | "id": "studyADR@tv-basicstudies", 34 | "_name": "ADR" 35 | }, 36 | { 37 | "id": "AROON@tv-basicstudies", 38 | "_name": "Aroon" 39 | }, 40 | { 41 | "id": "ATR@tv-basicstudies", 42 | "_name": "Average True Range" 43 | }, 44 | { 45 | "id": "AwesomeOscillator@tv-basicstudies", 46 | "_name": "Awesome Oscillator" 47 | }, 48 | { 49 | "id": "BB@tv-basicstudies", 50 | "_name": "Bollinger Bands" 51 | }, 52 | { 53 | "id": "BollingerBandsR@tv-basicstudies", 54 | "_name": "Bollinger Bands %B" 55 | }, 56 | { 57 | "id": "BollingerBandsWidth@tv-basicstudies", 58 | "_name": "Bollinger Bands Width" 59 | }, 60 | { 61 | "id": "CMF@tv-basicstudies", 62 | "_name": "Chaikin Money Flow" 63 | }, 64 | { 65 | "id": "ChaikinOscillator@tv-basicstudies", 66 | "_name": "Chaikin Oscillator" 67 | }, 68 | { 69 | "id": "chandeMO@tv-basicstudies", 70 | "_name": "Chande Momentum Oscillator" 71 | }, 72 | { 73 | "id": "ChoppinessIndex@tv-basicstudies", 74 | "_name": "Choppiness Index" 75 | }, 76 | { 77 | "id": "CCI@tv-basicstudies", 78 | "_name": "Commodity Channel Index" 79 | }, 80 | { 81 | "id": "CRSI@tv-basicstudies", 82 | "_name": "ConnorsRSI" 83 | }, 84 | { 85 | "id": "CorrelationCoefficient@tv-basicstudies", 86 | "_name": "Correlation Coefficient" 87 | }, 88 | { 89 | "id": "DetrendedPriceOscillator@tv-basicstudies", 90 | "_name": "Detrended Price Oscillator" 91 | }, 92 | { 93 | "id": "DM@tv-basicstudies", 94 | "_name": "Directional Movement" 95 | }, 96 | { 97 | "id": "DONCH@tv-basicstudies", 98 | "_name": "Donchian Channels" 99 | }, 100 | { 101 | "id": "DoubleEMA@tv-basicstudies", 102 | "_name": "Double EMA" 103 | }, 104 | { 105 | "id": "EaseOfMovement@tv-basicstudies", 106 | "_name": "Ease Of Movement" 107 | }, 108 | { 109 | "id": "EFI@tv-basicstudies", 110 | "_name": "Elder's Force Index" 111 | }, 112 | { 113 | "id": "ElliottWave@tv-basicstudies", 114 | "_name": "Elliott Wave" 115 | }, 116 | { 117 | "id": "ENV@tv-basicstudies", 118 | "_name": "Envelope" 119 | }, 120 | { 121 | "id": "FisherTransform@tv-basicstudies", 122 | "_name": "Fisher Transform" 123 | }, 124 | { 125 | "id": "HV@tv-basicstudies", 126 | "_name": "Historical Volatility" 127 | }, 128 | { 129 | "id": "hullMA@tv-basicstudies", 130 | "_name": "Hull Moving Average" 131 | }, 132 | { 133 | "id": "IchimokuCloud@tv-basicstudies", 134 | "_name": "Ichimoku Cloud" 135 | }, 136 | { 137 | "id": "KLTNR@tv-basicstudies", 138 | "_name": "Keltner Channels" 139 | }, 140 | { 141 | "id": "KST@tv-basicstudies", 142 | "_name": "Know Sure Thing" 143 | }, 144 | { 145 | "id": "LinearRegression@tv-basicstudies", 146 | "_name": "Linear Regression" 147 | }, 148 | { 149 | "id": "MACD@tv-basicstudies", 150 | "_name": "MACD" 151 | }, 152 | { 153 | "id": "MOM@tv-basicstudies", 154 | "_name": "Momentum" 155 | }, 156 | { 157 | "id": "MF@tv-basicstudies", 158 | "_name": "Money Flow" 159 | }, 160 | { 161 | "id": "MoonPhases@tv-basicstudies", 162 | "_name": "Moon Phases" 163 | }, 164 | { 165 | "id": "MASimple@tv-basicstudies", 166 | "_name": "Moving Average" 167 | }, 168 | { 169 | "id": "MAExp@tv-basicstudies", 170 | "_name": "Moving Average Exponentional" 171 | }, 172 | { 173 | "id": "MAWeighted@tv-basicstudies", 174 | "_name": "Moving Average Weighted" 175 | }, 176 | { 177 | "id": "OBV@tv-basicstudies", 178 | "_name": "On Balance Volume" 179 | }, 180 | { 181 | "id": "PSAR@tv-basicstudies", 182 | "_name": "Parabolic SAR" 183 | }, 184 | { 185 | "id": "PivotPointsHighLow@tv-basicstudies", 186 | "_name": "Pivot Points High Low" 187 | }, 188 | { 189 | "id": "PivotPointsStandard@tv-basicstudies", 190 | "_name": "Pivot Points Standard" 191 | }, 192 | { 193 | "id": "PriceOsc@tv-basicstudies", 194 | "_name": "Price Oscillator" 195 | }, 196 | { 197 | "id": "PriceVolumeTrend@tv-basicstudies", 198 | "_name": "Price Volume Trend" 199 | }, 200 | { 201 | "id": "ROC@tv-basicstudies", 202 | "_name": "Rate Of Change" 203 | }, 204 | { 205 | "id": "RSI@tv-basicstudies", 206 | "_name": "Relative Strength Index" 207 | }, 208 | { 209 | "id": "VigorIndex@tv-basicstudies", 210 | "_name": "Relative Vigor Index" 211 | }, 212 | { 213 | "id": "VolatilityIndex@tv-basicstudies", 214 | "_name": "Relative Volatility Index" 215 | }, 216 | { 217 | "id": "SMIErgodicIndicator@tv-basicstudies", 218 | "_name": "SMI Ergodic Indicator" 219 | }, 220 | { 221 | "id": "SMIErgodicOscillator@tv-basicstudies", 222 | "_name": "SMI Ergodic Oscillator" 223 | }, 224 | { 225 | "id": "Stochastic@tv-basicstudies", 226 | "_name": "Stochastic" 227 | }, 228 | { 229 | "id": "StochasticRSI@tv-basicstudies", 230 | "_name": "Stochastic RSI" 231 | }, 232 | { 233 | "id": "TripleEMA@tv-basicstudies", 234 | "_name": "Triple EMA" 235 | }, 236 | { 237 | "id": "Trix@tv-basicstudies", 238 | "_name": "TRIX" 239 | }, 240 | { 241 | "id": "UltimateOsc@tv-basicstudies", 242 | "_name": "Ultimate Oscillator" 243 | }, 244 | { 245 | "id": "VSTOP@tv-basicstudies", 246 | "_name": "Volatility Stop" 247 | }, 248 | { 249 | "id": "Volume@tv-basicstudies", 250 | "_name": "Volume" 251 | }, 252 | { 253 | "id": "VWAP@tv-basicstudies", 254 | "_name": "VWAP" 255 | }, 256 | { 257 | "id": "MAVolumeWeighted@tv-basicstudies", 258 | "_name": "VWMA" 259 | }, 260 | { 261 | "id": "WilliamR@tv-basicstudies", 262 | "_name": "Williams %R" 263 | }, 264 | { 265 | "id": "WilliamsAlligator@tv-basicstudies", 266 | "_name": "Williams Alligator" 267 | }, 268 | { 269 | "id": "WilliamsFractal@tv-basicstudies", 270 | "_name": "Williams Fractal" 271 | }, 272 | { 273 | "id": "ZigZag@tv-basicstudies", 274 | "_name": "Zig Zag" 275 | } 276 | ] 277 | } -------------------------------------------------------------------------------- /libs/webserver/public/images/SymBot-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3cqs-coder/SymBot/72447369cafbf34b2499714f58728f4bab290a84/libs/webserver/public/images/SymBot-Logo.png -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/chart.js/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2022 Chart.js Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/ejs/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/jquery-confirm/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Boniface Pereira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/jquery-ui-timepicker-addon/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Trent Richardson 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/jquery-ui/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery-ui 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | Copyright and related rights for sample code are waived via CC0. Sample 34 | code is defined as all source code contained within the demos directory. 35 | 36 | CC0: http://creativecommons.org/publicdomain/zero/1.0/ 37 | 38 | ==== 39 | 40 | All files located in the node_modules and external directories are 41 | externally maintained libraries used by this software which have their 42 | own licenses; we recommend you read them, as their terms may differ from 43 | the terms above. 44 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/marked/LICENSE.md: -------------------------------------------------------------------------------- 1 | # License information 2 | 3 | ## Contribution License Agreement 4 | 5 | If you contribute code to this project, you are implicitly allowing your code 6 | to be distributed under the MIT license. You are also implicitly verifying that 7 | all code is your original work. `` 8 | 9 | ## Marked 10 | 11 | Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) 12 | Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 30 | THE SOFTWARE. 31 | 32 | ## Markdown 33 | 34 | Copyright © 2004, John Gruber 35 | http://daringfireball.net/ 36 | All rights reserved. 37 | 38 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 39 | 40 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 41 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 42 | * Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 43 | 44 | This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. 45 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/select2/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/simple-switch/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nelson Kuang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/simple-switch/jquery.simpleswitch.min.js: -------------------------------------------------------------------------------- 1 | !function(e){e.fn.extend({switchReset:function(){return this.prop("checked",!1).parent().removeClass("checked").addClass("unchecked")},switchToggle:function(e){return e?this.prop("checked",e).parent().removeClass("unchecked").addClass("checked"):this.prop("checked",e).parent().removeClass("checked").addClass("unchecked")},simpleSwitch:function(){function c(e){var c=e;c.prop("checked")?(c.attr("data-switch",!0),c.parent().removeClass("unchecked").addClass("checked")):(c.attr("data-switch",!1),c.parent().removeClass("checked").addClass("unchecked"))}this.each(function(){var c=e(this);c.addClass("simple-switch");var t=c.prop("outerHTML"),s="";s+='',c.replaceWith(s)}),e(".simple-switch").each(function(){var t=e(this);c(t),t.click(function(){c(e(this))});var s={x:0,y:0},a={x:0,y:0},n=t.parent().find(".simple-switch-circle").get(0);n.addEventListener("touchstart",function(e){s={x:e.changedTouches[0].pageX,y:e.changedTouches[0].pageY}},!1),n.addEventListener("touchmove",function(e){e.preventDefault(),a={x:e.changedTouches[0].pageX,y:e.changedTouches[0].pageY}},!1),n.addEventListener("touchend",function(e){(a={x:e.changedTouches[0].pageX,y:e.changedTouches[0].pageY}).x!=s.x&&t.trigger("click"),s={x:0,y:0},a={x:0,y:0}},!1)})}})}(jQuery); -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/socket.io/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Guillermo Rauch 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/tablesorter/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining 2 | a copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be 10 | included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 13 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/tablesorter/parsers/parser-duration.min.js: -------------------------------------------------------------------------------- 1 | (function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery){ 2 | 3 | /*! Parser: duration & countdown - updated 2/7/2015 (v2.19.0) */ 4 | !function(e){"use strict";e.tablesorter.addParser({id:"duration",is:function(){return!1},format:function(e,r){var t,n,s=r.config,i="",o="",a=s.durationLength||4,u=new Array(a+1).join("0"),d=(s.durationLabels||"(?:years|year|y),(?:days|day|d),(?:hours|hour|h),(?:minutes|minute|min|m),(?:seconds|second|sec|s)").split(/\s*,\s*/),c=d.length;if(!s.durationRegex){for(t=0;t")}).$style=h("").prop("disabled",!0).appendTo("head"),a.$breakpoints=h("").prop("disabled",!0).appendTo("head"),a.isInitializing=!0,S.setUpColspan(t,o),S.setupSelector(t,o),o.columnSelector_mediaquery&&S.setupBreakpoints(t,o),a.isInitializing=!1,a.$container.length?S.updateCols(t,o):n&&console.warn("ColumnSelector >> container not found"),t.$table.off("refreshColumnSelector"+c).on("refreshColumnSelector"+c,function(e,t,o){S.refreshColumns(this.config,t,o)}),n&&console.log("ColumnSelector >> Widget initialized")):n&&console.error("ColumnSelector >> ERROR: Column Selector aborting, no input found in the layout! ***")},refreshColumns:function(e,t,o){var l,a,n,c,r=e.selector,s=h.isArray(o||t),i=e.widgetOptions;if(null!=t&&r.$container.length){if("selectors"===t&&(r.$container.empty(),S.setupSelector(e,i),S.setupBreakpoints(e,i),void 0===o&&null!==o&&(o=r.auto)),s)for(a=o||t,h.each(a,function(e,t){a[e]=parseInt(t,10)}),l=0;l tr > ",a=h(l+"th,"+l+"td"),n=[];for(t=0;t'),o=n.selector,l=n.widgetOptions,c.find(".tablesorter-column-selector").html(o.$container.html()).find("input").each(function(){var e=h(this).attr("data-column"),t="auto"===e?o.auto:o.states[e];h(this).toggleClass(l.columnSelector_cssChecked,t).prop("checked",t)}),o.$popup=c.on("change","input",function(){if(!o.isInitializing){if(!S.checkChange(n,this.checked))return this.checked=!this.checked,!1;a=h(this).toggleClass(l.columnSelector_cssChecked,this.checked).attr("data-column"),o.$container.find('input[data-column="'+a+'"]').prop("checked",this.checked).trigger("change")}}))}};f.window_resize=function(){f.timer_resize&&clearTimeout(f.timer_resize),f.timer_resize=setTimeout(function(){h(window).trigger("resizeEnd")},250)},f.addWidget({id:"columnSelector",priority:10,options:{columnSelector_container:null,columnSelector_columns:{},columnSelector_saveColumns:!0,columnSelector_layout:'',columnSelector_layoutCustomizer:null,columnSelector_name:"data-selector-name",columnSelector_mediaquery:!0,columnSelector_mediaqueryName:"Auto: ",columnSelector_mediaqueryState:!0,columnSelector_mediaqueryHidden:!1,columnSelector_maxVisible:null,columnSelector_minVisible:null,columnSelector_breakpoints:["20em","30em","40em","50em","60em","70em"],columnSelector_maxPriorities:6,columnSelector_priority:"data-priority",columnSelector_cssChecked:"checked",columnSelector_classHasSpan:"hasSpan",columnSelector_updated:"columnUpdate"},init:function(e,t,o,l){S.init(e,o,l)},remove:function(e,t,o,l){var a=t.selector;!l&&a&&(a&&a.$container.empty(),a.$popup&&a.$popup.empty(),a.$style.remove(),a.$breakpoints.remove(),h(t.namespace+"columnselector"+o.columnSelector_classHasSpan).removeClass(o.filter_filteredRow||"filtered"),t.$table.find("[data-col-span]").each(function(e,t){var o=h(t);o.attr("colspan",o.attr("data-col-span"))}),t.$table.off("updateAll"+c+" update"+c))}})}(jQuery);return jQuery;})); 4 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/tablesorter/widgets/widget-filter-formatter-jui.min.js: -------------------------------------------------------------------------------- 1 | (function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery){ 2 | 3 | /*! Widget: filter jQuery UI formatter functions - updated 7/17/2014 (v2.17.5) */ 4 | !function(f){"use strict";var u=f.tablesorter||{},m=".compare-select",v=u.filterFormatter=f.extend({},u.filterFormatter,{addCompare:function(e,a,t){if(t.compare&&f.isArray(t.compare)&&1'+t.cellText+"":"";f.each(t.compare,function(e,a){n+=""}),e.wrapInner('
').prepend(i+'').appendTo(r).bind("change"+s.namespace+"filter",function(){n({value:this.value,delayed:!1})}),c=[],n=function(e,a){var t,n=!0,d=e&&e.value&&u.formatFloat((e.value+"").replace(/[><=]/g,""))||r.find(".spinner").val()||o.value,i=(f.isArray(o.compare)?r.find(m).val()||o.compare[o.selected||0]:o.compare)||"",l=e&&"boolean"==typeof e.delayed?e.delayed:!s.$table[0].hasInitialized||(o.delayed||"");o.addToggle&&(n=r.find(".toggle").is(":checked")),t=o.disabled||!n?"disable":"enable",u.isEmptyObject(r.find(".spinner").data())||(r.find(".filter").val(n?(i||(o.exactMatch?"=":""))+d:"").trigger(a?"":"search",l).end().find(".spinner").spinner(t).val(d),c.length&&(c.find(".spinner").spinner(t).val(d).end().find(m).val(i),o.addToggle&&(c.find(".toggle")[0].checked=n)))};return o.oldcreate=o.create,o.oldspin=o.spin,o.create=function(e,a){n(),"function"==typeof o.oldcreate&&o.oldcreate(e,a)},o.spin=function(e,a){n(a),"function"==typeof o.oldspin&&o.oldspin(e,a)},o.addToggle&&f('
').appendTo(r).find(".toggle").bind("change",function(){n()}),r.closest("thead").find("th[data-column="+a+"]").addClass("filter-parsed"),f('').val(o.value).appendTo(r).spinner(o).bind("change keyup",function(){n()}),s.$table.bind("filterFomatterUpdate"+s.namespace+"filter",function(){var e=v.updateCompare(r,t,o)[0];r.find(".spinner").val(e),n({value:e},!0),u.filter.formatterUpdated(r,a)}),o.compare&&(v.addCompare(r,a,o),r.find(m).bind("change",function(){n()})),s.$table.bind("stickyHeadersInit"+s.namespace+"filter",function(){c=s.widgetOptions.$sticky.find(".tablesorter-filter-row").children().eq(a).empty(),o.addToggle&&f('
').appendTo(c).find(".toggle").bind("change",function(){r.find(".toggle")[0].checked=this.checked,n()}),f('').val(o.value).appendTo(c).spinner(o).bind("change keyup",function(){r.find(".spinner").val(this.value),n()}),o.compare&&(v.addCompare(c,a,o),c.find(m).bind("change",function(){r.find(m).val(f(this).val()),n()}))}),s.$table.bind("filterReset"+s.namespace+"filter",function(){f.isArray(o.compare)&&r.add(c).find(m).val(o.compare[o.selected||0]),o.addToggle&&(r.find(".toggle")[0].checked=!1),r.find(".spinner").spinner("value",o.value),setTimeout(function(){n()},0)}),n(),t},uiSlider:function(r,o,e){var s=f.extend({delayed:!0,valueToHeader:!1,exactMatch:!0,cellText:"",compare:"",allText:"all",value:0,min:0,max:100,step:1,range:"min"},e),c=r.closest("table")[0].config,a=f('').appendTo(r).bind("change"+c.namespace+"filter",function(){t({value:this.value})}),p=[],t=function(e,a){var t=void 0!==e&&u.formatFloat((e.value+"").replace(/[><=]/g,""))||s.value,n=s.compare?t:t===s.min?s.allText:t,d=(f.isArray(s.compare)?r.find(m).val()||s.compare[s.selected||0]:s.compare)||"",i=d+n,l=e&&"boolean"==typeof e.delayed?e.delayed:!c.$table[0].hasInitialized||(s.delayed||"");s.valueToHeader?r.closest("thead").find("th[data-column="+o+"]").find(".curvalue").html(" ("+i+")"):r.find(".ui-slider-handle").addClass("value-popup").attr("data-value",i),u.isEmptyObject(r.find(".slider").data())||(r.find(".filter").val(d?d+t:t===s.min?"":(s.exactMatch?"=":"")+t).trigger(a?"":"search",l).end().find(".slider").slider("value",t),p.length&&(p.find(m).val(d).end().find(".slider").slider("value",t),s.valueToHeader?p.closest("thead").find("th[data-column="+o+"]").find(".curvalue").html(" ("+i+")"):p.find(".ui-slider-handle").addClass("value-popup").attr("data-value",i)))};return r.closest("thead").find("th[data-column="+o+"]").addClass("filter-parsed"),s.valueToHeader&&r.closest("thead").find("th[data-column="+o+"]").find(".tablesorter-header-inner").append(''),s.oldcreate=s.create,s.oldslide=s.slide,s.create=function(e,a){t(),"function"==typeof s.oldcreate&&s.oldcreate(e,a)},s.slide=function(e,a){t(a),"function"==typeof s.oldslide&&s.oldslide(e,a)},f('
').appendTo(r).slider(s),c.$table.bind("filterFomatterUpdate"+c.namespace+"filter",function(){var e=v.updateCompare(r,a,s)[0];r.find(".slider").slider("value",e),t({value:e},!1),u.filter.formatterUpdated(r,o)}),s.compare&&(v.addCompare(r,o,s),r.find(m).bind("change",function(){t({value:r.find(".slider").slider("value")})})),c.$table.bind("filterReset"+c.namespace+"filter",function(){f.isArray(s.compare)&&r.add(p).find(m).val(s.compare[s.selected||0]),setTimeout(function(){t({value:s.value})},0)}),c.$table.bind("stickyHeadersInit"+c.namespace+"filter",function(){p=c.widgetOptions.$sticky.find(".tablesorter-filter-row").children().eq(o).empty(),f('
').val(s.value).appendTo(p).slider(s).bind("change keyup",function(){r.find(".slider").slider("value",this.value),t()}),s.compare&&(v.addCompare(p,o,s),p.find(m).bind("change",function(){r.find(m).val(f(this).val()),t()}))}),a},uiRange:function(l,r,e){var o=f.extend({delayed:!0,valueToHeader:!1,values:[0,100],min:0,max:100,range:!0},e),s=l.closest("table")[0].config,t=f('').appendTo(l).bind("change"+s.namespace+"filter",function(){a()}),c=[],a=function(){var e=t.val(),a=e.split(" - ");""===e&&(a=[o.min,o.max]),a&&a[1]&&n({values:a,delay:!1},!0)},n=function(e,a){var t=e&&e.values||o.values,n=t[0]+" - "+t[1],d=t[0]===o.min&&t[1]===o.max?"":n,i=e&&"boolean"==typeof e.delayed?e.delayed:!s.$table[0].hasInitialized||(o.delayed||"");o.valueToHeader?l.closest("thead").find("th[data-column="+r+"]").find(".currange").html(" ("+n+")"):l.find(".ui-slider-handle").addClass("value-popup").eq(0).attr("data-value",t[0]).end().eq(1).attr("data-value",t[1]),u.isEmptyObject(l.find(".range").data())||(l.find(".filter").val(d).trigger(a?"":"search",i).end().find(".range").slider("values",t),c.length&&(c.find(".range").slider("values",t),o.valueToHeader?c.closest("thead").find("th[data-column="+r+"]").find(".currange").html(" ("+n+")"):c.find(".ui-slider-handle").addClass("value-popup").eq(0).attr("data-value",t[0]).end().eq(1).attr("data-value",t[1])))};return l.closest("thead").find("th[data-column="+r+"]").addClass("filter-parsed"),o.valueToHeader&&l.closest("thead").find("th[data-column="+r+"]").find(".tablesorter-header-inner").append(''),o.oldcreate=o.create,o.oldslide=o.slide,o.create=function(e,a){n(),"function"==typeof o.oldcreate&&o.oldcreate(e,a)},o.slide=function(e,a){n(a),"function"==typeof o.oldslide&&o.oldslide(e,a)},f('
').appendTo(l).slider(o),s.$table.bind("filterFomatterUpdate"+s.namespace+"filter",function(){a(),u.filter.formatterUpdated(l,r)}),s.$table.bind("filterReset"+s.namespace+"filter",function(){l.find(".range").slider("values",o.values),setTimeout(function(){n()},0)}),s.$table.bind("stickyHeadersInit"+s.namespace+"filter",function(){c=s.widgetOptions.$sticky.find(".tablesorter-filter-row").children().eq(r).empty(),f('
').val(o.value).appendTo(c).slider(o).bind("change keyup",function(){l.find(".range").val(this.value),n()})}),t},uiDateCompare:function(l,t,e){function n(e){var a,t,n=r.datepicker("getDate")||"",d=(f.isArray(o.compare)?l.find(m).val()||o.compare[o.selected||0]:o.compare)||"",i=!s.$table[0].hasInitialized||(o.delayed||"");r.datepicker("setDate",(""===n?"":n)||null),""===n&&(e=!1),t=(a=r.datepicker("getDate"))&&(o.endOfDay&&/<=/.test(d)?a.setHours(23,59,59,999):a.getTime())||"",a&&o.endOfDay&&"="===d&&(d="",t+=" - "+a.setHours(23,59,59,999),e=!1),l.find(".dateCompare").val(d+t).trigger(e?"":"search",i).end(),c.length&&c.find(".dateCompare").val(d+t).end().find(m).val(d)}var r,a,o=f.extend({cellText:"",compare:"",endOfDay:!0,defaultDate:"",changeMonth:!0,changeYear:!0,numberOfMonths:1},e),s=l.closest("table")[0].config,d=l.closest("thead").find("th[data-column="+t+"]").addClass("filter-parsed"),i=f('').appendTo(l).bind("change"+s.namespace+"filter",function(){var e=this.value;e&&o.onClose(e)}),c=[];return a='',r=f(a).appendTo(l),o.oldonClose=o.onClose,o.onClose=function(e,a){n(),"function"==typeof o.oldonClose&&o.oldonClose(e,a)},r.datepicker(o),s.$table.bind("filterReset"+s.namespace+"filter",function(){f.isArray(o.compare)&&l.add(c).find(m).val(o.compare[o.selected||0]),l.add(c).find(".date").val(o.defaultDate).datepicker("setDate",o.defaultDate||null),setTimeout(function(){n()},0)}),s.$table.bind("filterFomatterUpdate"+s.namespace+"filter",function(){var e,a=i.val();/\s+-\s+/.test(a)?(l.find(m).val("="),e=a.split(/\s+-\s+/)[0],r.datepicker("setDate",e||null)):e=""!==(e=v.updateCompare(l,i,o)[1].toString()||"")?/\d{5}/g.test(e)?new Date(Number(e)):e||"":"",l.add(c).find(".date").datepicker("setDate",e||null),setTimeout(function(){n(!0),u.filter.formatterUpdated(l,t)},0)}),o.compare&&(v.addCompare(l,t,o),l.find(m).bind("change",function(){n()})),s.$table.bind("stickyHeadersInit"+s.namespace+"filter",function(){(c=s.widgetOptions.$sticky.find(".tablesorter-filter-row").children().eq(t).empty()).append(a).find(".date").datepicker(o),o.compare&&(v.addCompare(c,t,o),c.find(m).bind("change",function(){l.find(m).val(f(this).val()),n()}))}),i.val(o.defaultDate?o.defaultDate:"")},uiDatepicker:function(i,n,e){function l(e){return e instanceof Date&&isFinite(e)}var a,d,r=f.extend({endOfDay:!0,textFrom:"from",textTo:"to",from:"",to:"",changeMonth:!0,changeYear:!0,numberOfMonths:1},e),o=[],t=i.closest("table")[0].config,s=f('').appendTo(i).bind("change"+t.namespace+"filter",function(){var e=this.value;e.match(" - ")?(e=e.split(" - "),i.find(".dateTo").val(e[1]),d(e[0])):e.match(">=")?d(e.replace(">=","")):e.match("<=")&&d(e.replace("<=",""))}),c=i.closest("thead").find("th[data-column="+n+"]").addClass("filter-parsed");return a="',f(a).appendTo(i),r.oldonClose=r.onClose,d=r.onClose=function(e,a){var t,n=i.find(".dateFrom").datepicker("getDate"),d=i.find(".dateTo").datepicker("getDate");n=l(n)?n.getTime():"",d=l(d)&&(r.endOfDay?d.setHours(23,59,59,999):d.getTime())||"",t=n?d?n+" - "+d:">="+n:d?"<="+d:"",i.add(o).find(".dateRange").val(t).trigger("search"),n=n?new Date(n):"",d=d?new Date(d):"",/<=/.test(t)?i.add(o).find(".dateFrom").datepicker("option","maxDate",d||null).end().find(".dateTo").datepicker("option","minDate",null).datepicker("setDate",d||null):/>=/.test(t)?i.add(o).find(".dateFrom").datepicker("option","maxDate",null).datepicker("setDate",n||null).end().find(".dateTo").datepicker("option","minDate",n||null):i.add(o).find(".dateFrom").datepicker("option","maxDate",null).datepicker("setDate",n||null).end().find(".dateTo").datepicker("option","minDate",null).datepicker("setDate",d||null),"function"==typeof r.oldonClose&&r.oldonClose(e,a)},r.defaultDate=r.from||"",i.find(".dateFrom").datepicker(r),r.defaultDate=r.to||"+7d",i.find(".dateTo").datepicker(r),t.$table.bind("filterFomatterUpdate"+t.namespace+"filter",function(){var e=s.val()||"",a="",t="";/\s+-\s+/.test(e)?(a=(e=e.split(/\s+-\s+/)||[])[0]||"",t=e[1]||""):/>=/.test(e)?a=e.replace(/>=/,"")||"":/<=/.test(e)&&(t=e.replace(/<=/,"")||""),a=""!==a?/\d{5}/g.test(a)?new Date(Number(a)):a||"":"",t=""!==t?/\d{5}/g.test(t)?new Date(Number(t)):t||"":"",i.add(o).find(".dateFrom").datepicker("setDate",a||null),i.add(o).find(".dateTo").datepicker("setDate",t||null),setTimeout(function(){d(),u.filter.formatterUpdated(i,n)},0)}),t.$table.bind("stickyHeadersInit"+t.namespace+"filter",function(){(o=t.widgetOptions.$sticky.find(".tablesorter-filter-row").children().eq(n).empty()).append(a),r.defaultDate=r.from||"",o.find(".dateFrom").datepicker(r),r.defaultDate=r.to||"+7d",o.find(".dateTo").datepicker(r)}),i.closest("table").bind("filterReset"+t.namespace+"filter",function(){i.add(o).find(".dateFrom").val("").datepicker("setDate",r.from||null),i.add(o).find(".dateTo").val("").datepicker("setDate",r.to||null),setTimeout(function(){d()},0)}),s.val(r.from?r.to?r.from+" - "+r.to:">="+r.from:r.to?"<="+r.to:"")}})}(jQuery);return jQuery;})); 5 | -------------------------------------------------------------------------------- /libs/webserver/public/js/vendor/tablesorter/widgets/widget-filter-formatter-select2.min.js: -------------------------------------------------------------------------------- 1 | (function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery){ 2 | 3 | /*! Widget: filter, select2 formatter function - updated 12/1/2019 (v2.31.2) */ 4 | !function(g){"use strict";var h=g.tablesorter||{};h.filterFormatter=h.filterFormatter||{},h.filterFormatter.select2=function(i,c,e){function t(){a=[],l=h.filter.getOptionSource(s.$table[0],c,f)||[],g.each(l,function(e,t){a.push({id:""+t.parsed,text:t.text})}),n.data=a}var l,a,n=g.extend({cellText:"",match:!0,value:"",multiple:!0,width:"100%"},e),s=i.addClass("select2col"+c).closest("table")[0].config,d=s.widgetOptions,r=g('').appendTo(i).bind("change"+s.namespace+"filter",function(){var e=v(this.value);s.$table.find(".select2col"+c+" .select2").select2("val",e),$()}),o=s.$headerIndexed[c],f=o.hasClass(d.filter_onlyAvail),p=n.match?"":"^",u=n.match?"":"$",b=d.filter_ignoreCase?"i":"",v=function(e){return e.replace(/^\/\(\^?/,"").replace(/\$\|\^/g,"|").replace(/\$?\)\/i?$/g,"").replace(/\\/g,"").split("|")},$=function(){var e=!1,t=s.$table.find(".select2col"+c+" .select2").select2("val")||n.value||"";g.isArray(t)&&(e=!0,t=t.join("\0"));var l=t.replace(/[-[\]{}()*+?.,/\\^$|#]/g,"\\$&");e&&(t=t.split("\0"),l=l.split("\0")),h.isEmptyObject(i.find(".select2").data())||(r.val(g.isArray(l)&&l.length&&""!==l.join("")?"/("+p+(l||[]).join(u+"|"+p)+u+")/"+b:"").trigger("search"),i.find(".select2").select2("val",t),s.widgetOptions.$sticky&&s.widgetOptions.$sticky.find(".select2col"+c+" .select2").select2("val",t))};return o.toggleClass("filter-match",n.match),n.cellText&&i.prepend(""),n.ajax&&!g.isEmptyObject(n.ajax)||n.data||(t(),s.$table.bind("filterEnd",function(){t(),s.$table.find(".select2col"+c).add(s.widgetOptions.$sticky&&s.widgetOptions.$sticky.find(".select2col"+c)).find(".select2").select2(n)})),g('').val(n.value).appendTo(i).select2(n).bind("change",function(){$()}),s.$table.bind("filterFomatterUpdate",function(){var e=v(s.$table.data("lastSearch")[c]||"");(i=s.$table.find(".select2col"+c)).find(".select2").select2("val",e),$(),h.filter.formatterUpdated(i,c)}),s.$table.bind("stickyHeadersInit",function(){var e=s.widgetOptions.$sticky.find(".select2col"+c).empty();g('').val(n.value).appendTo(e).select2(n).bind("change",function(){s.$table.find(".select2col"+c).find(".select2").select2("val",s.widgetOptions.$sticky.find(".select2col"+c+" .select2").select2("val")),$()}),n.cellText&&e.prepend("")}),s.$table.bind("filterReset",function(){s.$table.find(".select2col"+c).find(".select2").select2("val",n.value||""),setTimeout(function(){$()},0)}),$(),r}}(jQuery);return jQuery;})); 5 | -------------------------------------------------------------------------------- /libs/webserver/public/views/Hub/configView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('../partialsHeaderView'); %> 2 | 3 | 75 | 76 | 77 |
78 | 79 |
80 | 81 |
82 | Configuration 83 |
84 | 85 |
86 | 87 |
88 | 93 | Change 94 |
95 | 96 | 100 | 101 | 105 | 106 |
107 | 110 |
111 | 112 |
113 |
114 | 115 |
116 | 117 | 118 | <%- include('../partialsFooterView'); %> 119 | -------------------------------------------------------------------------------- /libs/webserver/public/views/Hub/homeView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('../partialsHeaderView'); %> 2 | 3 |
4 | 5 | 6 |
7 | 8 |
9 | 10 | 13 | 14 | 17 | 18 | 21 | 22 | 25 | 26 | 29 | 30 | 33 | 34 | 37 | 38 |
39 | 40 |
41 | 42 |
43 | 44 | 45 | <%- include('../partialsFooterView'); %> 46 | -------------------------------------------------------------------------------- /libs/webserver/public/views/Hub/newsView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('../partialsHeaderView'); %> 2 | 3 | 4 | 5 | 52 | 53 |
54 |
55 |
56 | Latest News 57 |
58 | 59 | 60 | 61 |
62 | <% articles.forEach(article => { %> 63 |
64 |
65 | <% if (article.imageUrl) { %> 66 | <%= article.title %> 67 | <% } else { %> 68 |
69 | No Image Available 70 |
71 | <% } %> 72 |
73 |
74 |
75 | <%= article.title %> 76 |
77 |

<%= article.description %>

78 | 82 |
83 |
84 | <% }); %> 85 |
86 |
87 |
88 | 89 | <%- include('../partialsFooterView'); %> 90 | -------------------------------------------------------------------------------- /libs/webserver/public/views/backupsView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('./partialsHeaderView'); %> 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | <%- appData.name %> Backups 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | <% 18 | for (let i = 0; i < files.length; i++) { 19 | 20 | let fileName = files[i]['name'].split(/[\\\/]/).pop(); 21 | let size = files[i]['size_human']; 22 | %> 23 | 24 | 27 | 28 | <% } %> 29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 | <%- include('./partialsFooterView'); %> 37 | -------------------------------------------------------------------------------- /libs/webserver/public/views/dashboardView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('./partialsHeaderView'); %> 2 | 3 | 4 | 5 | 140 | 141 |
142 |
143 |
144 |
145 | 151 | Period: <%- period %> 152 |
153 |
154 |
155 |
156 | Balance: 157 | 159 |
160 |
161 |
162 |
163 | 164 |
165 |
166 | Active Deals 167 | <%- active_deals %> 168 |
169 |
170 | Total In Deals 171 | $<%- total_in_deals.toLocaleString() %> 172 |
173 |
174 | Available Funds 175 | 176 |
177 |
178 | Profit 179 | $<%- total_profit.toLocaleString() %> 180 |
181 |
182 | Active P/L 183 | $<%- total_pl.toLocaleString() %> 184 |
185 |
186 | 187 |
188 | 189 |
190 | 191 |
192 | 193 |
194 | 195 |
196 |
197 |
198 | 199 |
200 | 201 |
202 | 203 |
204 | 205 |
206 |
207 |
208 | 209 |
210 | 211 |
212 | 213 |
214 | 215 |
216 |
217 |
218 |
219 |
220 |
221 | 222 | 223 | <%- include('./partialsFooterView'); %> -------------------------------------------------------------------------------- /libs/webserver/public/views/homeView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('./partialsHeaderView'); %> 2 | 3 |
4 | 5 | 6 |
7 | 8 |
9 | 10 | 13 | 14 | 17 | 18 | 21 | 22 | 25 | 26 | 29 | 30 | 33 | 34 | 37 | 38 | 41 | 42 | 45 | 46 | 49 | 50 | 53 | 54 |
55 | 56 |
57 | 58 |
59 | 60 | 61 | <%- include('./partialsFooterView'); %> 62 | -------------------------------------------------------------------------------- /libs/webserver/public/views/loginView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('./partialsHeaderView'); %> 2 | 3 | 11 | 12 |
13 | 14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Password:
28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 | <%- include('./partialsFooterView'); %> 37 | -------------------------------------------------------------------------------- /libs/webserver/public/views/logsLiveView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('./partialsHeaderView'); %> 2 | 3 | 148 | 149 | 150 |
151 | 152 |
153 | 154 |
155 |
156 | 157 | Live 158 | 159 |
160 | <%- appData.name %> Realtime Logs 161 | 162 |
163 | <% if (isLiteLog) { %> 164 |
165 |
166 |

Lite Logging Enabled

167 |

All logs will be written to console.

168 |

To store logs, restart SymBot without params

169 |
170 |
171 | <% } 172 | else { %> 173 |
174 | <% } %> 175 |
176 | 177 |
178 | 179 | <%- include('./partialsFooterView'); %> 180 | -------------------------------------------------------------------------------- /libs/webserver/public/views/logsView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('./partialsHeaderView'); %> 2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | <%- appData.name %> Logs 10 | 11 | 12 |

13 | View Live Logs 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | <% 23 | for (let i = 0; i < files.length; i++) { 24 | 25 | let fileName = files[i]['name'].split(/[\\\/]/).pop(); 26 | let size = files[i]['size_human']; 27 | %> 28 | 29 | 32 | 33 | <% } %> 34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 | <%- include('./partialsFooterView'); %> 42 | -------------------------------------------------------------------------------- /libs/webserver/public/views/partialsFooterView.ejs: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | <%- appData.name %> v<%- appData.version %> - 3CQS.com 5 | 6 | <% if (appData.update_available) { %> 7 | 8 | Update Available 9 | 10 | <% } %> 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /libs/webserver/public/views/partialsSocketView.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/webserver/public/views/strategies/DCABot/DCABotDealsHistoryView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('../../partialsHeaderView'); %> 2 | 3 | 336 | 337 | 338 |
339 |
340 | 341 |
342 | 343 |
344 | 345 |
346 | 347 | DCA Bot Deals History 348 | 349 | 350 | 351 | 352 | 353 | 354 |

Total P/L:

355 |

Deals:

356 |
357 | 358 |
From:   To:   Bot:
359 | 360 |
361 | 362 |
363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 |
Date Bot Name Deal ID Pair Duration Profit Profit ◉ Profit % Safety Orders
374 |
375 |
376 |
377 | 378 |
379 |
380 | 381 | <%- include('../../partialsFooterView'); %> 382 | -------------------------------------------------------------------------------- /libs/webserver/public/views/strategies/DCABot/DCABotsView.ejs: -------------------------------------------------------------------------------- 1 | <%- include('../../partialsHeaderView'); %> 2 | 3 | 247 | 248 | 249 |
250 |
251 | 252 |
253 |
254 | 255 |
256 | 257 |
258 |
259 |
260 |
261 | 262 |
263 | 264 |
265 | 266 |
267 | DCA Bots 268 | 269 | 270 | Show Active 271 | 272 |
273 | 274 |
275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | <% 286 | 287 | const maxPairs = 10; 288 | 289 | for (let i = 0; i < bots.length; i++) { 290 | 291 | let pair; 292 | let startCondition = 'asap'; 293 | 294 | let bot = bots[i]; 295 | 296 | let active = bot['active']; 297 | let dealMax = bot['config']['dealMax']; 298 | let pairMax = bot['config']['pairMax']; 299 | let maxFunds = Math.round(bot['config']['maxFunds']); 300 | let botMaxFunds = Math.round(bot['config']['botMaxFunds']); 301 | let sandBox = bot['config']['sandBox']; 302 | 303 | if (bot['config']['startConditions'] != undefined && bot['config']['startConditions'] != null && bot['config']['startConditions'] != '') { 304 | 305 | startCondition = bot['config']['startConditions'][0]; 306 | } 307 | 308 | if (startCondition.indexOf('|') !== -1) { 309 | 310 | const data = startCondition.split('|'); 311 | 312 | const signalSource = data[1]; 313 | const signalId = data[2]; 314 | 315 | try { 316 | 317 | startCondition = appData['bots']['start_conditions'][startCondition]['description']; 318 | } 319 | catch(e) { 320 | 321 | startCondition = ' INVALID START CONDITION: ' + signalSource + ' ID: ' + signalId; 322 | } 323 | } 324 | else { 325 | 326 | startCondition = startCondition.toUpperCase(); 327 | } 328 | 329 | if (active) { 330 | 331 | active = 'checked'; 332 | } 333 | else { 334 | 335 | active = ''; 336 | } 337 | 338 | if (sandBox) { 339 | 340 | sandBox = 'Yes'; 341 | } 342 | else { 343 | 344 | sandBox = 'No'; 345 | } 346 | 347 | if (dealMax == 0) { 348 | 349 | dealMax = '∞'; 350 | } 351 | 352 | if (pairMax == 0) { 353 | 354 | pairMax = '∞'; 355 | } 356 | 357 | if (typeof bot['config']['pair'] !== 'string') { 358 | 359 | let pairs = bot['config']['pair'].slice(0, maxPairs); 360 | 361 | pair = pairs.join(', '); 362 | 363 | if (bot['config']['pair'].length > maxPairs) { 364 | 365 | pair += ' ...'; 366 | } 367 | } 368 | else { 369 | 370 | pair = bot['config']['pair']; 371 | } 372 | %> 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | <% } %> 395 | 396 | 397 |
Created Updated Bot Name Pairs BO SO Max SO Deviation Volume Scale Step Scale TP Max Deals Max Pairs Max Funds Start Sandbox Active
<%- bot['botName'] %><%- pair.toUpperCase() %><%- bot['config']['firstOrderAmount'] %><%- bot['config']['dcaOrderAmount'] %><%- bot['config']['dcaMaxOrder'] %><%- bot['config']['dcaOrderStepPercent'] %><%- bot['config']['dcaOrderSizeMultiplier'] %><%- bot['config']['dcaOrderStepPercentMultiplier'] %><%- bot['config']['dcaTakeProfitPercent'] %>%<%- dealMax %><%- pairMax %>$<%- botMaxFunds %><%- startCondition %><%- sandBox %>>
398 |
399 |
400 |
401 | 402 |
403 |
404 | 405 | <%- include('../../partialsFooterView'); %> 406 | -------------------------------------------------------------------------------- /libs/webserver/public/views/strategies/DCABot/ai/aiChatView.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%- appData.name %> Chat 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /libs/webserver/public/views/strategies/DCABot/ai/aiDealAnalyzeView.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | let ordersContent = ''; 3 | let lastOrderDate = ''; 4 | let sumTotal = 0; 5 | let qtySumTotal = 0; 6 | let averagePrice = 0; 7 | let allOrderDates = []; 8 | 9 | const duration = timeDiff(new Date(), new Date(dealTracker[dealId].date)); 10 | 11 | // Calculate totals, collect all order dates, and determine the last order date and average price 12 | dealTracker[dealId].orders.forEach(order => { 13 | if (order.filled) { 14 | const dateFilled = order.dateFilled ? dateConvertLocal(new Date(order.dateFilled)) : ''; 15 | if (dateFilled) { 16 | allOrderDates.push(dateFilled); 17 | if (!lastOrderDate || new Date(dateFilled) > new Date(lastOrderDate)) { 18 | lastOrderDate = dateFilled; 19 | averagePrice = order.average || 0; 20 | } 21 | } 22 | ordersContent += ` 23 | Order #${order.orderNo || ''}: Date: ${dateFilled}. Order Price: $${order.price !== undefined ? order.price : ''}. 24 | Order Amount: $${order.amount !== undefined ? order.amount : ''}. Order Quantity: ${order.qty !== undefined ? order.qty : ''}. 25 | `; 26 | sumTotal += order.amount || 0; 27 | qtySumTotal += order.qty || 0; 28 | } 29 | }); 30 | %> 31 | Below is the buy orders history for cryptocurrency pair: <%= dealTracker[dealId].config.pair %> under the DCA (Dollar Cost Average) deal ID <%= dealId %>. 32 | 33 | <%= ordersContent %> 34 | 35 | The current price for <%= dealTracker[dealId].config.pair %> as of <%= dateConvertLocal(new Date(dealTracker[dealId].info.updated)) %> is: $<%= dealTracker[dealId].info.price_last %>. Current profit at this price is <%= dealTracker[dealId].info.profit_percentage %>%. 36 | The total amount spent is $<%= sumTotal.toFixed(2) %>, and the total quantity bought is <%= qtySumTotal %>. 37 | The deal started on <%= dateConvertLocal(new Date(dealTracker[dealId].date)) %> and has been active for a total duration of <%= duration %>. 38 | The last buy order date is <%= lastOrderDate %>, and the average price is $<%= parseFloat(averagePrice.toFixed(10)) %>. 39 | 40 | Upon closing, the deal will sell the entire quantity at the final target sell price of $<%= dealTracker[dealId].info.price_target %>, achieving a profit margin of <%= dealTracker[dealId].config.dcaTakeProfitPercent %>%. 41 | 42 | The estimated final profit is calculated as follows: 43 | **(Target Sell Price × Total Quantity) - Total Amount Spent** 44 | 45 | Estimated Profit = ($<%= dealTracker[dealId].info.price_target %> × <%= qtySumTotal %>) - $<%= sumTotal %> 46 | = **$<%= (dealTracker[dealId].info.price_target * qtySumTotal - sumTotal).toFixed(2) %>** 47 | 48 | Use the above data, including all order dates and pricing details, to provide a realistic, factual, and accurate estimation of the deal's future closing date. Highlight key dates and the profit estimate clearly. 49 | 50 | Also provide a second estimated date if $<%= dealTracker[dealId].info.estimates.amount_net %> were added to the deal. This should close sooner than the first in profit since we're lowering the average and target prices. 51 | This results in buying <%= (Number(dealTracker[dealId].info.estimates.amount_net) / Number(dealTracker[dealId].info.price_last)) %> additional quantity for the deal at the current price of $<%= dealTracker[dealId].info.price_last %>, decreasing the average price to $<%= dealTracker[dealId].info.estimates.price_average_net %> and decreasing the target sell price to $<%= dealTracker[dealId].info.estimates.price_target_net %>. 52 | This will also increase the final take profit of the deal because of the additional funds added to the deal. 53 | 54 | Since today is <%= dateConvertLocal(new Date()) %>, the estimated closing dates and times must be after that. 55 | 56 | Separate both estimates clearly showing scenario one and two. Provide a final summary at the end in just a few sentences. 57 | -------------------------------------------------------------------------------- /libs/webserver/public/views/tradingView.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 | <% if (data['jquery']) { %> 11 | 12 | <% } %> 13 | 14 | <% if (data['script']) { %> 15 | 16 | <% } %> 17 | 18 | 166 | 167 |
168 | 169 | 170 |
171 | 172 | 173 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SymBot", 3 | "version": "2.0.5", 4 | "description": "SymBot", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node symbot.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "3CQS", 11 | "license": "CC BY-NC-SA 4.0", 12 | "dependencies": { 13 | "ansi-to-html": "^0.7.2", 14 | "archiver": "^7.0.1", 15 | "async": "^3.2.6", 16 | "body-parser": "^2.2.0", 17 | "bson": "^6.10.4", 18 | "ccxt": "^4.4.88", 19 | "check-dependencies": "^2.0.0", 20 | "colors": "^1.4.0", 21 | "compression": "^1.8.0", 22 | "connect-mongodb-session": "^5.0.0", 23 | "cookie-parser": "^1.4.7", 24 | "easy-table": "^1.2.0", 25 | "ejs": "^3.1.10", 26 | "express": "^5.1.0", 27 | "express-session": "^1.18.1", 28 | "http-proxy-middleware": "^3.0.5", 29 | "mongoose": "^8.15.1", 30 | "multer": "^2.0.1", 31 | "node-cron": "^4.1.0", 32 | "node-fetch-commonjs": "^3.3.2", 33 | "ollama": "^0.5.16", 34 | "percentagejs": "^1.0.2", 35 | "prompt-sync": "^4.2.0", 36 | "session-file-store": "^1.5.0", 37 | "socket.io": "^4.8.1", 38 | "socket.io-client": "^4.8.1", 39 | "telegraf": "^4.16.3", 40 | "unzipper": "^0.12.3", 41 | "uuid": "^11.1.0", 42 | "xml2js": "^0.6.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /symbot-hub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /* 5 | 6 | SymBot Hub 7 | Copyright © 2023 - 2025 3CQS.com All Rights Reserved 8 | Licensed under Creative Commons Attribution-NonCommerical-ShareAlike 4.0 International (CC BY-NC-SA 4.0) 9 | 10 | */ 11 | 12 | 13 | const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); 14 | const Common = require(__dirname + '/libs/app/Common.js'); 15 | const Hub = require(__dirname + '/libs/app/Hub/Hub.js'); 16 | const HubMain = require(__dirname + '/libs/app/Hub/Main.js'); 17 | const HubWorker = require(__dirname + '/libs/app/Hub/Worker.js'); 18 | const WebServer = require(__dirname + '/libs/webserver/Hub'); 19 | const packageJson = require(__dirname + '/package.json'); 20 | 21 | 22 | let gotSigInt = false; 23 | 24 | const shutdownTimeout = 2000; 25 | const hubConfigFile = 'hub.json'; 26 | const workerMap = new Map(); 27 | 28 | let shareData; 29 | 30 | 31 | 32 | function initSignalHandlers() { 33 | 34 | process.on('SIGINT', shutDown); 35 | process.on('SIGTERM', shutDown); 36 | 37 | process.on('message', function(msg) { 38 | 39 | if (msg == 'shutdown') { 40 | 41 | shutDown(); 42 | } 43 | }); 44 | 45 | process.on('uncaughtException', function(err) { 46 | 47 | let logData = 'Uncaught Exception: ' + JSON.stringify(err.message) + ' Stack: ' + JSON.stringify(err.stack); 48 | 49 | Hub.logger('error', logData); 50 | }); 51 | } 52 | 53 | 54 | async function startHub() { 55 | 56 | let port; 57 | let configs; 58 | 59 | let success = true; 60 | 61 | initSignalHandlers(); 62 | 63 | let hubData = await Common.getConfig(hubConfigFile); 64 | 65 | if (hubData.success) { 66 | 67 | port = hubData.data.port; 68 | configs = hubData.data.instances; 69 | 70 | const password = hubData['data']['password']; 71 | 72 | if (password == undefined || password == null || password == '') { 73 | 74 | // Set default password 75 | const dataPass = await Common.genPasswordHash({ 'data': 'admin' }); 76 | 77 | hubData['data']['password'] = dataPass['salt'] + ':' + dataPass['hash']; 78 | 79 | await Common.saveConfig(hubConfigFile, hubData.data); 80 | } 81 | 82 | // Create initial Hub instance 83 | if (configs.length < 1) { 84 | 85 | const instanceObj = { 86 | "name": "Instance-1", 87 | "app_config": "app.json", 88 | "bot_config": "bot.json", 89 | "server_config": "server.json", 90 | "server_id": "", 91 | "mongo_db_url": "", 92 | "web_server_port": null, 93 | "enabled": true, 94 | "start_boot": true, 95 | "overrides": { }, 96 | "updated": new Date().toISOString() 97 | } 98 | 99 | configs.push(instanceObj); 100 | } 101 | } 102 | else { 103 | 104 | success = false; 105 | 106 | Hub.logger('error', 'Hub Configuration Error: ' + hubData.data); 107 | } 108 | 109 | if (success) { 110 | 111 | shareData = { 112 | 'appData': { 113 | 114 | 'name': packageJson.description + ' Hub', 115 | 'version': packageJson.version, 116 | 'password': hubData['data']['password'], 117 | 'path_root': __dirname, 118 | 'hub_filename': __filename, 119 | 'web_server_ports': undefined, 120 | 'web_socket_path': 'wsHub_', 121 | 'hub_config': hubConfigFile, 122 | 'shutdown_timeout': shutdownTimeout, 123 | 'sig_int': false, 124 | 'started': new Date() 125 | }, 126 | 'Common': Common, 127 | 'WebServer': WebServer, 128 | 'Hub': Hub, 129 | 'HubMain': HubMain, 130 | 'workerMap': workerMap 131 | }; 132 | 133 | HubMain.init(Worker, shareData, shutDown); 134 | 135 | Common.init(shareData); 136 | WebServer.init(shareData); 137 | Hub.init(shareData); 138 | 139 | let processData = await Hub.processConfig(configs); 140 | 141 | if (!processData.success) { 142 | 143 | success = false; 144 | 145 | Hub.logger('error', JSON.stringify(processData.error)); 146 | } 147 | else { 148 | 149 | await Hub.setProxyPorts(processData['web_server_ports']); 150 | 151 | let foundMissing = false; 152 | 153 | for (let i = 0; i < configs.length; i++) { 154 | 155 | const config = configs[i]; 156 | 157 | let id = config['id']; 158 | 159 | if (id == undefined || id == null || id == '') { 160 | 161 | foundMissing = true; 162 | 163 | config['id'] = Common.uuidv4(); 164 | } 165 | } 166 | 167 | // Update data if found missing id's 168 | if (foundMissing) { 169 | 170 | processData = null; 171 | 172 | processData = await Hub.processConfig(configs); 173 | 174 | configs = processData.configs; 175 | hubData['data']['instances'] = configs; 176 | 177 | await Common.saveConfig(hubConfigFile, hubData.data); 178 | } 179 | 180 | configs = processData.configs; 181 | } 182 | } 183 | 184 | if (!success) { 185 | 186 | Hub.logger('error', 'Aborting due to configuration errors.'); 187 | 188 | process.exit(1); 189 | } 190 | 191 | await WebServer.start(port); 192 | 193 | HubMain.start(configs); 194 | 195 | setInterval(() => Hub.logMemoryUsage(), 5000); 196 | } 197 | 198 | 199 | async function startWorker() { 200 | 201 | HubWorker.init(parentPort, shutdownTimeout); 202 | HubWorker.start(workerData); 203 | } 204 | 205 | 206 | async function shutDown() { 207 | 208 | // Perform any post-shutdown processes here 209 | 210 | if (!gotSigInt) { 211 | 212 | gotSigInt = true; 213 | 214 | Hub.logger('info', 'Received kill signal. Shutting down gracefully.'); 215 | Hub.logger('info', 'Cleaning up instances...'); 216 | 217 | const terminationPromises = []; 218 | 219 | // Set timer to force shutdown if cleanup takes too long 220 | let timeOutShutdown = setTimeout(() => { 221 | 222 | Hub.logger('info', `Cleanup timed out. Forcing shutdown.`); 223 | 224 | process.exit(1); 225 | 226 | }, (shutdownTimeout + 20000)); 227 | 228 | for (const [workerId, { worker, instance }] of workerMap.entries()) { 229 | 230 | const dateStart = instance.dateStart; 231 | const upTime = Common.timeDiff(new Date(dateStart), new Date()); 232 | 233 | // Create a promise to track the worker shutdown process 234 | const shutdownPromise = new Promise((resolve, reject) => { 235 | 236 | // Wait for the worker to handle the shutdown 237 | worker.on('message', async (message) => { 238 | 239 | if (message.type === 'shutdown_received') { 240 | 241 | // Wait additional short delay to ensure worker shutdown gracefully 242 | await Common.delay(shutdownTimeout + 3000); 243 | 244 | // Once shutdown is complete, terminate the worker 245 | try { 246 | 247 | await worker.terminate(); 248 | 249 | Hub.logger('info', `Worker ${workerId} terminated after ${upTime}.`); 250 | 251 | resolve(); 252 | } 253 | catch (err) { 254 | 255 | Hub.logger('error', `Error terminating instance: ${err}`); 256 | 257 | reject(err); 258 | } 259 | } 260 | }); 261 | 262 | // Send a "shutdown" message to the worker 263 | worker.postMessage({ 264 | 265 | type: 'shutdown' 266 | }); 267 | }); 268 | 269 | terminationPromises.push(shutdownPromise); 270 | } 271 | 272 | // Wait for all workers to finish before starting the shutdown timeout 273 | try { 274 | 275 | await Promise.all(terminationPromises); 276 | 277 | clearTimeout(timeOutShutdown); 278 | 279 | Hub.logger('info', 'All workers have been terminated. Proceeding with shutdown.'); 280 | 281 | // Start shutdown timeout after all workers are processed 282 | setTimeout(() => { 283 | 284 | process.exit(1); 285 | 286 | }, (shutdownTimeout + 3000)); 287 | 288 | } 289 | catch (err) { 290 | 291 | Hub.logger('error', `Error during shutdown: ${err}`); 292 | } 293 | } 294 | } 295 | 296 | 297 | async function start() { 298 | 299 | if (isMainThread) { 300 | 301 | startHub(); 302 | } 303 | else { 304 | 305 | startWorker(); 306 | } 307 | } 308 | 309 | 310 | start(); 311 | --------------------------------------------------------------------------------