├── tests └── run.js ├── applications └── messenger │ ├── client │ ├── src │ │ ├── actions │ │ │ └── .gitkeep │ │ ├── components │ │ │ └── .gitkeep │ │ ├── constants │ │ │ └── .gitkeep │ │ ├── containers │ │ │ ├── App.css │ │ │ └── App.js │ │ ├── reducers │ │ │ ├── index.js │ │ │ └── rooms.js │ │ ├── index.css │ │ ├── views │ │ │ └── RoomList.js │ │ ├── store │ │ │ └── configureStore.js │ │ ├── index.js │ │ └── impress.js │ ├── config │ │ ├── flow │ │ │ ├── css.js.flow │ │ │ └── file.js.flow │ │ ├── babel.dev.js │ │ ├── babel.prod.js │ │ ├── webpack.config.dev.js │ │ ├── webpack.config.prod.js │ │ └── eslint.js │ ├── metarhia.png │ ├── index.html │ ├── scripts │ │ ├── build.js │ │ ├── reloadChrome.applescript │ │ ├── openChrome.applescript │ │ ├── placeFiles.js │ │ └── start.js │ └── README.md │ ├── tasks │ └── README.md │ ├── log │ └── README.md │ ├── tmp │ └── README.md │ ├── model │ └── README.md │ ├── db │ └── README.md │ ├── doc │ ├── README.md │ └── db-structure.md │ ├── init │ └── README.md │ ├── setup │ └── README.md │ ├── files │ └── README.md │ ├── www │ ├── get.js │ ├── access.js │ ├── api │ │ ├── access.js │ │ └── application │ │ │ └── jstp.ws │ │ │ └── get.js │ └── request.js │ ├── config │ ├── routes.js │ ├── hosts.js │ ├── application.js │ ├── mail.js │ ├── files.js │ ├── databases.js │ ├── log.js │ ├── sessions.js │ ├── filestorage.js │ ├── passport.js │ └── sandbox.js │ ├── templates │ ├── README.md │ ├── error.template │ ├── introspection.template │ └── index.template │ ├── api │ └── auth │ │ ├── signOut.js │ │ ├── signIn.js │ │ └── signUp.js │ ├── static │ └── favicon.ico │ ├── package.json │ └── lib │ └── api.auth.js ├── server.js ├── server.sh ├── config ├── ssl │ ├── README.md │ ├── example.key │ └── example.cer ├── scale.test.js ├── servers.test.js ├── log.js ├── sandbox │ ├── sandbox.test.js │ └── sandbox.js ├── scale.js └── servers.js ├── .gitattributes ├── README.md ├── .eslintrc.json ├── .gitignore ├── package.json └── cli └── app.js /tests/run.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/messenger/client/src/actions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/messenger/client/src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/messenger/client/src/constants/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/messenger/client/src/containers/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/messenger/tasks/README.md: -------------------------------------------------------------------------------- 1 | Scheduled tasks -------------------------------------------------------------------------------- /applications/messenger/log/README.md: -------------------------------------------------------------------------------- 1 | Folder for fog files -------------------------------------------------------------------------------- /applications/messenger/tmp/README.md: -------------------------------------------------------------------------------- 1 | Folder for temporary files -------------------------------------------------------------------------------- /applications/messenger/client/config/flow/css.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | -------------------------------------------------------------------------------- /applications/messenger/model/README.md: -------------------------------------------------------------------------------- 1 | Applied domain-specific models -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('impress'); 2 | 3 | impress.server.start(); 4 | -------------------------------------------------------------------------------- /applications/messenger/db/README.md: -------------------------------------------------------------------------------- 1 | Database scripts, schemas and backups -------------------------------------------------------------------------------- /applications/messenger/doc/README.md: -------------------------------------------------------------------------------- 1 | Place application documentation here -------------------------------------------------------------------------------- /applications/messenger/init/README.md: -------------------------------------------------------------------------------- 1 | Application startup initialization -------------------------------------------------------------------------------- /applications/messenger/setup/README.md: -------------------------------------------------------------------------------- 1 | Application setup (first-run) logic -------------------------------------------------------------------------------- /applications/messenger/files/README.md: -------------------------------------------------------------------------------- 1 | Application will store uploaded files here -------------------------------------------------------------------------------- /applications/messenger/www/get.js: -------------------------------------------------------------------------------- 1 | (client, callback) => { 2 | callback(); 3 | } 4 | -------------------------------------------------------------------------------- /applications/messenger/client/config/flow/file.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare export default string; 3 | -------------------------------------------------------------------------------- /applications/messenger/config/routes.js: -------------------------------------------------------------------------------- 1 | [ 2 | // URL rewriting and request forwarding rules 3 | 4 | ] 5 | -------------------------------------------------------------------------------- /applications/messenger/templates/README.md: -------------------------------------------------------------------------------- 1 | Templates for HTTP status, directory index and API introspaection -------------------------------------------------------------------------------- /applications/messenger/api/auth/signOut.js: -------------------------------------------------------------------------------- 1 | (callback) => { 2 | api.auth.signOut(connection, callback); 3 | } 4 | -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | node --stack-trace-limit=1000 --allow-natives-syntax --max_old_space_size=2048 server.js 4 | -------------------------------------------------------------------------------- /config/ssl/README.md: -------------------------------------------------------------------------------- 1 | Place here: 2 | * Certificates (.cer files) from your to root authority 3 | * Private keys (.key files) -------------------------------------------------------------------------------- /applications/messenger/client/metarhia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarhia/messenger/HEAD/applications/messenger/client/metarhia.png -------------------------------------------------------------------------------- /applications/messenger/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarhia/messenger/HEAD/applications/messenger/static/favicon.ico -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js eol=lf 2 | *.css eol=lf 3 | *.template eol=lf 4 | *.txt eol=lf 5 | *.md eol=lf 6 | *.json eol=lf 7 | *.jstp eol=lf 8 | *.sh eol=lf 9 | -------------------------------------------------------------------------------- /applications/messenger/www/access.js: -------------------------------------------------------------------------------- 1 | { 2 | guests: true, 3 | logged: true, 4 | http: true, 5 | https: true, 6 | intro: false, 7 | groups: [] 8 | } -------------------------------------------------------------------------------- /applications/messenger/www/api/access.js: -------------------------------------------------------------------------------- 1 | { 2 | guests: true, 3 | logged: true, 4 | http: true, 5 | https: true, 6 | intro: true, 7 | groups: [] 8 | } -------------------------------------------------------------------------------- /applications/messenger/client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import rooms from './rooms'; 4 | 5 | export default { 6 | rooms 7 | }; 8 | -------------------------------------------------------------------------------- /applications/messenger/client/src/reducers/rooms.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | }; 3 | 4 | export default function rooms(state = initialState, action) { 5 | return state; 6 | } 7 | -------------------------------------------------------------------------------- /applications/messenger/client/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | /* html, body { */ 3 | /* min-height: 100%; */ 4 | /* } */ 5 | 6 | /* body { */ 7 | /* margin: 0; */ 8 | /* padding: 0; */ 9 | /* font-family: sans-serif; */ 10 | /* } */ 11 | -------------------------------------------------------------------------------- /applications/messenger/www/api/application/jstp.ws/get.js: -------------------------------------------------------------------------------- 1 | (client, callback) => { 2 | var connection = client.websocket.accept(); 3 | if (connection) { 4 | api.jstp.serveOverWebsocket(connection); 5 | } 6 | callback(); 7 | } 8 | -------------------------------------------------------------------------------- /applications/messenger/api/auth/signIn.js: -------------------------------------------------------------------------------- 1 | (login, password, callback) => { 2 | api.auth.signIn(connection, login, password, (err, user) => { 3 | if (err) { 4 | return callback(err); 5 | } 6 | callback(null); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /applications/messenger/config/hosts.js: -------------------------------------------------------------------------------- 1 | [ 2 | // Hosts (and IPs) array to be handled by application 3 | // Wildcard '*' is allowed for masking random or empty substring 4 | 5 | '127.0.0.1', 6 | 'localhost', 7 | 'messenger.metarhia.com' 8 | 9 | ] 10 | -------------------------------------------------------------------------------- /applications/messenger/www/request.js: -------------------------------------------------------------------------------- 1 | (client, callback) => { 2 | if (client.schema === 'http' && 3 | !['localhost', '127.0.0.1'].includes(client.host)) { 4 | client.redirect('https://' + client.host + client.req.url); 5 | } 6 | 7 | callback(); 8 | } 9 | -------------------------------------------------------------------------------- /applications/messenger/config/application.js: -------------------------------------------------------------------------------- 1 | { 2 | // Application configuration 3 | 4 | slowTime: '1s', 5 | // preload: true, // preload application to memory at startup (default: false) 6 | // allowOrigin: '*' // set HTTP header Access-Control-Allow-Origin (default: not set) 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metarhia Messenger 2 | 3 | ## Installation 4 | 5 | ```sh 6 | $ npm install 7 | 8 | # If you want a developer build with code-watching, 9 | # fast background rebuild and automatic page reload: 10 | 11 | $ npm run dev 12 | 13 | # If you want a production build: 14 | 15 | $ npm run build 16 | ``` 17 | -------------------------------------------------------------------------------- /applications/messenger/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Metarhia Messenger 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /applications/messenger/config/mail.js: -------------------------------------------------------------------------------- 1 | { 2 | // Mail configuration 3 | 4 | enabled: false, // enable or disable smtp transport 5 | robot: 'Robot name ', 6 | options: { 7 | service: 'Gmail', 8 | auth: { 9 | user: 'username@gmail.com', 10 | pass: 'password' 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /config/scale.test.js: -------------------------------------------------------------------------------- 1 | { 2 | cloud: 'PrivateCloud', 3 | server: 'S1', 4 | instance: 'standalone', 5 | 6 | key: 'edec4a498a604f2da754d173cd58b361', // Cloud access key 7 | cookie: 'node', 8 | 9 | firewall: { 10 | enabled: false 11 | }, 12 | 13 | health: '5s', 14 | nagle: false, 15 | gc: 0, 16 | watch: '1s' 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module", 10 | "ecmaFeatures": { 11 | "jsx": true, 12 | "experimentalObjectSpread": true 13 | } 14 | }, 15 | "plugins": [ 16 | "react" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /applications/messenger/config/files.js: -------------------------------------------------------------------------------- 1 | { 2 | // Static files configuration 3 | 4 | index: true, // display HTTP directory index for /static 5 | cacheSize: '50mb', // memory cache size 6 | cacheMaxFileSize: '10mb', // max file size to cache 7 | gzip: true, // default true 8 | preprocess: [ 9 | // 'js', // minify js 10 | // 'sass' // compile sass to css 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /applications/messenger/api/auth/signUp.js: -------------------------------------------------------------------------------- 1 | (login, password, name, email, callback) => { 2 | api.auth.signUp(login, password, name, email, (err, user) => { 3 | if (err) { 4 | return callback(new api.jstp.RemoteError(20, 'Registration failed')); 5 | } 6 | api.auth.signIn(connection, login, password, (err) => { 7 | if (err) { 8 | return callback(err); 9 | } 10 | callback(null, user.id); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM 2 | node_modules 3 | npm-debug.log 4 | 5 | # Impress 6 | /log/ 7 | *.log 8 | *.done 9 | 10 | # Development environments files 11 | /.idea/ 12 | .*.swp 13 | 14 | # OS-specific 15 | .DS_Store 16 | 17 | # Build files 18 | /applications/messenger/www/html.template 19 | /applications/messenger/client/build/ 20 | /applications/messenger/static/* 21 | !/applications/messenger/static/favicon.ico 22 | webpack-assets.json 23 | 24 | # Deployment script 25 | /deploy.sh 26 | -------------------------------------------------------------------------------- /applications/messenger/config/databases.js: -------------------------------------------------------------------------------- 1 | { 2 | // Databases including persistent session storage and application specific 3 | 4 | messenger: { 5 | alias: 'messengerDb', // optional alias to access database from global context 6 | url: 'mongodb://127.0.0.1:27017/messenger', // connection string 7 | // collections: ['sessions', 'users', 'groups', 'testCollection'], // optional 8 | slowTime: '2s', // timeout to mark requests as "slow" 9 | security: true 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /applications/messenger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Example application", 6 | "dependencies": { 7 | "ncp": "2.0.x" 8 | }, 9 | "jshintConfig": { 10 | "predef": ["api", "impress", "global", "window", "navigator", "document", "frames", "escape", "localStorage", "WebSocket", "XMLHttpRequest", "EventSource", "application"], 11 | "node": true, 12 | "indent": 2, 13 | "maxcomplexity": 25 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /applications/messenger/client/config/babel.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cacheDirectory: true, 3 | presets: [ 4 | 'babel-preset-es2015', 5 | 'babel-preset-es2016', 6 | 'babel-preset-react' 7 | ].map(require.resolve), 8 | plugins: [ 9 | 'babel-plugin-syntax-trailing-function-commas', 10 | 'babel-plugin-transform-class-properties', 11 | 'babel-plugin-transform-object-rest-spread', 12 | 'babel-plugin-transform-function-bind', 13 | 'babel-plugin-antd' 14 | ].map(require.resolve) 15 | }; 16 | -------------------------------------------------------------------------------- /config/servers.test.js: -------------------------------------------------------------------------------- 1 | { 2 | 3 | master: { 4 | protocol: 'jstp', 5 | address: '127.0.0.1', 6 | ports: [2500], 7 | slowTime: '1s' 8 | }, 9 | 10 | test: { 11 | protocol: 'http', 12 | address: '127.0.0.1', 13 | ports: [8080], 14 | nagle: true, 15 | slowTime: '1s', 16 | timeout: '10s' 17 | }, 18 | 19 | rpc: { 20 | protocol: 'jstp', 21 | address: '*', 22 | ports: [3000, [1]], // Example: [81, [-1]] 23 | slowTime: '1s' 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /applications/messenger/client/src/views/RoomList.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Tree } from 'antd'; 3 | 4 | export default class RoomList extends Component { 5 | render() { 6 | let nodes = this.props.rooms.map((room, index) => ( 7 | 8 | )); 9 | return ( 10 | 11 | {nodes} 12 | 13 | ); 14 | } 15 | } 16 | 17 | RoomList.propTypes = { 18 | rooms: PropTypes.array.isRequired 19 | }; 20 | -------------------------------------------------------------------------------- /applications/messenger/client/config/babel.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | 'babel-preset-es2015', 4 | 'babel-preset-es2016', 5 | 'babel-preset-react' 6 | ].map(require.resolve), 7 | plugins: [ 8 | 'babel-plugin-syntax-trailing-function-commas', 9 | 'babel-plugin-transform-class-properties', 10 | 'babel-plugin-transform-object-rest-spread', 11 | 'babel-plugin-transform-function-bind', 12 | 'babel-plugin-transform-react-constant-elements', 13 | 'babel-plugin-antd' 14 | ].map(require.resolve) 15 | }; 16 | -------------------------------------------------------------------------------- /config/log.js: -------------------------------------------------------------------------------- 1 | { 2 | // Server logs configuration 3 | 4 | keepDays: 100, // Delete files after N days 5 | writeInterval: '3s', // Flush log to disk interval (milliseconds) 6 | writeBuffer: 64*1024, // Buffer size 64kb 7 | applicationLog: false, // Write log to application folder 8 | serverLog: true, // Write log to server global folder 9 | files: ['access', 'api', 'error', 'debug', 'slow', 'server', 'node', 'cloud', 'warning'], // write to files 10 | stdout: ['error', 'debug', 'warning'] // output log files to stdout 11 | } 12 | -------------------------------------------------------------------------------- /applications/messenger/config/log.js: -------------------------------------------------------------------------------- 1 | { 2 | // Server logs configuration 3 | 4 | keepDays: 100, // Delete files after N days 5 | writeInterval: '3s', // Flush log to disk interval (milliseconds) 6 | writeBuffer: 64*1024, // Buffer size 64kb 7 | applicationLog: false, // Write log to application folder 8 | serverLog: true, // Write log to server global folder 9 | files: ['access', 'api', 'error', 'debug', 'slow', 'server', 'node', 'cloud', 'warning'], // write to files 10 | stdout: ['error', 'debug', 'warning'] // output log files to stdout 11 | } 12 | -------------------------------------------------------------------------------- /config/sandbox/sandbox.test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | api: [ 3 | // Node internal modules 4 | 'console', 'os', 'fs', 'sd', 'tls','net', 'dns', 'url', 'util', 'path', 'zlib', 'http', 'https', 'dgram', 5 | 'stream', 'buffer', 'domain', 'crypto', 'events', 'punycode', 'readline', 'querystring', 6 | 7 | // Preinstalled modules 8 | 'db', 9 | 'con', 10 | 'jstp', 11 | 'common', 12 | 'impress', 13 | 'registry', 14 | 'definition', 15 | 16 | // Preinstalled modules 17 | 'csv', 18 | 'async', 19 | 'iconv', 20 | 'colors', 21 | 'zipStream', 22 | 23 | // Additional modules 24 | 'bcrypt' 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /applications/messenger/client/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import createLogger from 'redux-logger'; 4 | import { routerReducer } from 'react-router-redux'; 5 | import rootReducer from '../reducers'; 6 | 7 | export default function configureStore(initialState) { 8 | let middleware = [thunk]; 9 | 10 | // eslint-disable-next-line 11 | if (process.env.NODE_ENV === 'development') { 12 | middleware.push(createLogger()); 13 | } 14 | 15 | console.log(...rootReducer); 16 | 17 | return createStore( 18 | combineReducers({ 19 | ...rootReducer, 20 | routing: routerReducer 21 | }), 22 | initialState, 23 | applyMiddleware(...middleware) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /applications/messenger/client/scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'production'; 4 | 5 | const childProcess = require('child_process'); 6 | const path = require('path'); 7 | const webpack = require('webpack'); 8 | const config = require('../config/webpack.config.prod'); 9 | const placeFiles = require('./placeFiles'); 10 | 11 | const buildDir = path.join(__dirname, '../build'); 12 | const staticDir = path.join(__dirname, '../../static'); 13 | 14 | childProcess.execSync('rm -rf ' + buildDir); 15 | 16 | webpack(config).run((err, stats) => { 17 | if (err) { 18 | console.error('Failed to create a production build. Reason:'); 19 | console.error(err.message || err); 20 | process.exit(1); 21 | } 22 | 23 | placeFiles(() => { 24 | console.log('The bundle is optimized and ready to be deployed to production.'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /applications/messenger/config/sessions.js: -------------------------------------------------------------------------------- 1 | { 2 | // Sessions configuration 3 | 4 | anonymous: true, // Allow anonymous sessions (client should request /api/auth/anonymous to generate SID) 5 | cookie: 'SID', // Session cookie name 6 | characters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', // Possible characters for SID 7 | secret: 'secret', // session secret is a string known just at server side to sign session cookie 8 | length: 64, // SID length in bytes 9 | persist: true, // Store sessions in persistent database 10 | 11 | perIpLimit: '20', 12 | perUserLimit: '5', 13 | //confirmTime: '1m', 14 | //expireTime: '2m', 15 | 16 | database: 'messenger', // Database connection name to store sessions 17 | // domain: 'name.com' // optional domain for cookie '.domain.com' for all subdomains 18 | } 19 | -------------------------------------------------------------------------------- /applications/messenger/config/filestorage.js: -------------------------------------------------------------------------------- 1 | application.extCompressed = [ 2 | 'gif','jpg','jpe','jpeg','png','svgz', 3 | 'docx','xlsx','pptx','dotx','odm','odt','ott','odp','otp','djvu','djv', 4 | 'zip','rar','z7','gz','jar','arj', 5 | 'iso','nrg','img','apk', 6 | 'mp2','mp3','mp4','avi','flv','fla','swf','3gp','mkv','mpeg','mpg','mpe','mov','asf','wmv','vob' 7 | ]; 8 | 9 | application.extNotCompressed = [ 10 | 'txt','pdf','doc','dot','xls','ppt','rtf','hlp','inf','eml','uu','uue', 11 | 'css','htm','html','xhtml','tpl','vsd','chm','ps', 12 | 'bmp','ico','eps','svg','psd','ai','tif','tiff','wmf','emf','ani','cur','wav','wave','mid', 13 | 'bak','sql','csv','xml','rss','atom','url','torrent', 14 | 'bas','js','php','pl','pm','py','c','cpp','cs','d','e','h','inc','java','m','pas','dpr','prg','asp','aspx','ini','asm','res','bat','cmd', 15 | 'exe','dll','com','obj','ocx','sys','msi' 16 | ]; 17 | -------------------------------------------------------------------------------- /config/ssl/example.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDgfvteRgV1c0Kj6TwB6xO0IlmDWiS+LKezPBHbbZPA1QeMG7ol 3 | yKc+mJhQMv3EJqOj2mdycwpZke+I8uod4An0DTfJPSOVYiFagT7mYvHSBKt5cdnw 4 | 74lQrb7YeCMzdNL0LCE8yVSsZZcCpBWsVOtgpH9ReHTotEnUZ1fkIUSycwIDAQAB 5 | AoGAC8l/Bz8j8DvCBvsrBGrEWRPooUXtYfLUR0vjICNLu7czsz6Ncohrh5TZgnR6 6 | 8H6a3vKLte2mYHeSHR2r/y+RNh799QxJqBdPjTiC3rtYYPheXMOvfonkayggaTJe 7 | LJo7HtGoEQZkBtilW0XHifAVmkXant+BulZhEI03FZ0A1iECQQD5TpTneoNJT/aq 8 | QXJJ17ybVzyRAteuQCtMnoKxD1uTF6tokk5YlAAMT/dEc/QTJZol7tAO9XxUuqz+ 9 | 3cTxFrhDAkEA5oXhqU37CUuougr5xf4DM/YwrHINBnDez307mzrnZo0Bbw1H+vp2 10 | SVdq9qvu5HBERAWgbRk2ppQRvN7LL/9SEQJAJH//xwAl1obxiy23yN3gDDIyeNyc 11 | rTNR447VfYzNEHUHsHCzLb7FXwgaIJiFZIQ91E3pgjcos9L83vejDNURtwJBAKJc 12 | o58KdIiMiG9YsamZCgp8GLYssi3aK6R6cRQZPchGAj+EXDFSXSO09Xp9iInO5NYA 13 | HIka3Bohagjb5sKMkvECQQC1aIay93umIiOku8RGlMM2NR65qKh2nF/MDdoKmkfm 14 | Vb7MBWneCo4DTjMghu/WvcBdeU1KQKLxoQN6NMMxWQE8 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /applications/messenger/client/scripts/reloadChrome.applescript: -------------------------------------------------------------------------------- 1 | on run argv 2 | set theURL to item 1 of argv 3 | 4 | tell application "Chrome" 5 | 6 | if (count every window) = 0 then 7 | make new window 8 | end if 9 | 10 | -- Find a tab currently running the debugger 11 | set found to false 12 | set theTabIndex to -1 13 | repeat with theWindow in every window 14 | set theTabIndex to 0 15 | repeat with theTab in every tab of theWindow 16 | set theTabIndex to theTabIndex + 1 17 | if theTab's URL starts with theURL then 18 | set found to true 19 | exit repeat 20 | end if 21 | end repeat 22 | 23 | if found then 24 | exit repeat 25 | end if 26 | end repeat 27 | 28 | if found then 29 | tell theTab to reload 30 | set index of theWindow to 1 31 | set theWindow's active tab index to theTabIndex 32 | end if 33 | end tell 34 | end run 35 | -------------------------------------------------------------------------------- /applications/messenger/client/scripts/openChrome.applescript: -------------------------------------------------------------------------------- 1 | on run argv 2 | set theURL to item 1 of argv 3 | 4 | tell application "Chrome" 5 | 6 | if (count every window) = 0 then 7 | make new window 8 | end if 9 | 10 | -- Find a tab currently running the debugger 11 | set found to false 12 | set theTabIndex to -1 13 | repeat with theWindow in every window 14 | set theTabIndex to 0 15 | repeat with theTab in every tab of theWindow 16 | set theTabIndex to theTabIndex + 1 17 | if theTab's URL is theURL then 18 | set found to true 19 | exit repeat 20 | end if 21 | end repeat 22 | 23 | if found then 24 | exit repeat 25 | end if 26 | end repeat 27 | 28 | if found then 29 | tell theTab to reload 30 | set index of theWindow to 1 31 | set theWindow's active tab index to theTabIndex 32 | else 33 | tell window 1 34 | activate 35 | make new tab with properties {URL:theURL} 36 | end tell 37 | end if 38 | end tell 39 | end run 40 | -------------------------------------------------------------------------------- /applications/messenger/client/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | 5 | import './App.css'; 6 | 7 | import { Card, Icon } from 'antd'; 8 | 9 | import RoomList from '../views/RoomList'; 10 | 11 | class App extends Component { 12 | render() { 13 | const imageStyle = { 14 | width: '24px', 15 | float: 'left' 16 | }; 17 | let heading = 18 | 21 | Metarhia Messenger 22 | ; 23 | 24 | let { rooms } = this.props; 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | function mapStateToProps(state) { 37 | return { 38 | rooms: state.rooms 39 | }; 40 | } 41 | 42 | function mapDispatchToProps(dispatch) { 43 | return { 44 | }; 45 | } 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(App); 48 | -------------------------------------------------------------------------------- /config/ssl/example.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC5DCCAk2gAwIBAgIJALBUH+OV35WUMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD 3 | VQQGEwJVQTENMAsGA1UECAwES2lldjENMAsGA1UEBwwES2lldjEUMBIGA1UECgwL 4 | TWV0YVN5c3RlbXMxCzAJBgNVBAsMAklUMQ4wDAYDVQQDDAVUaW11cjEqMCgGCSqG 5 | SIb3DQEJARYbdGltdXIuc2hlbXNlZGlub3ZAZ21haWwuY29tMB4XDTEzMTIyOTE0 6 | MzMyNVoXDTE2MDkyNTE0MzMyNVowgYoxCzAJBgNVBAYTAlVBMQ0wCwYDVQQIDARL 7 | aWV2MQ0wCwYDVQQHDARLaWV2MRQwEgYDVQQKDAtNZXRhU3lzdGVtczELMAkGA1UE 8 | CwwCSVQxDjAMBgNVBAMMBVRpbXVyMSowKAYJKoZIhvcNAQkBFht0aW11ci5zaGVt 9 | c2VkaW5vdkBnbWFpbC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOB+ 10 | +15GBXVzQqPpPAHrE7QiWYNaJL4sp7M8Edttk8DVB4wbuiXIpz6YmFAy/cQmo6Pa 11 | Z3JzClmR74jy6h3gCfQNN8k9I5ViIVqBPuZi8dIEq3lx2fDviVCtvth4IzN00vQs 12 | ITzJVKxllwKkFaxU62Ckf1F4dOi0SdRnV+QhRLJzAgMBAAGjUDBOMB0GA1UdDgQW 13 | BBSkrwuihmgMMRtVKE1oXs0mDTDhazAfBgNVHSMEGDAWgBSkrwuihmgMMRtVKE1o 14 | Xs0mDTDhazAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAEdTziS/3zya 15 | cEMP0BWE0mKWH6IUpVKYEq3XscLKzQn0xfW3uhxyiC6APnJZjk316a225JNdm/3Z 16 | tIAvmLefjC+bN+h9nXgoZLSFE31LVMfoyjs4tGuM8H4TYsqSkmCqm/zFfVNP9AtS 17 | LDlNunKPtNSb8rcu/8QqKKbNQniSkyf/ 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /applications/messenger/client/src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import './impress'; 3 | 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { Provider } from 'react-redux'; 7 | import { Router, Route, browserHistory } from 'react-router'; 8 | import { syncHistoryWithStore } from 'react-router-redux'; 9 | 10 | import App from './containers/App'; 11 | import configureStore from './store/configureStore'; 12 | 13 | import './index.css'; 14 | import 'antd/dist/antd.css'; 15 | 16 | const store = configureStore({ 17 | rooms: [ 18 | { 19 | title: 'Test 1', 20 | messages: [ 21 | { 22 | sender: 'user1', 23 | text: 'lorem ipsum' 24 | } 25 | ] 26 | }, 27 | { 28 | title: 'Test 2', 29 | messages: [ 30 | { 31 | sender: 'user1', 32 | text: 'lorem ipsum' 33 | } 34 | ] 35 | } 36 | ] 37 | }); 38 | 39 | const history = syncHistoryWithStore(browserHistory, store); 40 | 41 | ReactDOM.render( 42 | 43 | 44 | 45 | 46 | , 47 | document.getElementById('root') 48 | ); 49 | -------------------------------------------------------------------------------- /applications/messenger/config/passport.js: -------------------------------------------------------------------------------- 1 | if (api.passport) { 2 | 3 | // used to serialize the user for the session 4 | api.passport.serializeUser(function(user, done) { 5 | done(null, user); 6 | }); 7 | 8 | // used to deserialize the user 9 | api.passport.deserializeUser(function(user, done) { 10 | done(null, user); 11 | }); 12 | 13 | module.exports = { 14 | strategies: { 15 | google: { 16 | param: { 17 | clientID: '-place-id-here-', 18 | clientSecret: '-place-secret-here-', 19 | callbackURL: '/api/auth/google/callback' 20 | }, 21 | strategy: api.passportGoogle.OAuth2Strategy, 22 | authenticate: function(req, token, refreshToken, profile, done) { 23 | try { 24 | if (!req.user) { 25 | // validate request and find user ... 26 | // if (err) done(err) 27 | // else 28 | done(null, profile); 29 | } else { 30 | done(null, req.user); 31 | } 32 | } catch (e) { console.trace(); } 33 | }, 34 | successRedirect: '/api/auth/userInfo.json', 35 | failureRedirect: '/' 36 | } 37 | } 38 | }; 39 | 40 | } -------------------------------------------------------------------------------- /config/scale.js: -------------------------------------------------------------------------------- 1 | { 2 | // Cloud and health configuration 3 | 4 | // check: 'http://127.0.0.1/', // if we can get this page it means that another copy is running 5 | 6 | cloud: 'PrivateCloud', // cloud name 7 | server: 'S1', // Server name to identify it in loadbalancing infrastructure 8 | instance: 'controller', // cloud instance type: standalone, controller, server 9 | //instance: 'server', 10 | //instance: 'standalone', 11 | 12 | key: 'edec4a498a604f2da754d173cd58b361', // Cloud access key 13 | cookie: 'node', // Cookie name for loadbalancing (cookie value will be 'S1'+'N1') 14 | 15 | firewall: { // Web Application Firewall config 16 | enabled: false, 17 | limits: { // limit concurent connection count 18 | ip: 20, // per client ip 19 | sid: 10, // per user session 20 | host: 100, // per host name 21 | url: 50, // per url 22 | app: 200, // per application 23 | srv: 500 // per server port 24 | } 25 | }, 26 | 27 | health: '5m', // health monitoring interval '5s' 28 | nagle: false, // Nagle algorithm 29 | gc: 0, // garbage collector interval '1h' - 1 hour, '10m' - 10 minutes 30 | watch: '2s' // combine wached file system events if interval less then specified 31 | 32 | } 33 | -------------------------------------------------------------------------------- /applications/messenger/doc/db-structure.md: -------------------------------------------------------------------------------- 1 | Database Structure 2 | ========================= 3 | 4 | ## Collection 'users' 5 | 6 | This collection is intended to store the user accounts. Despite the name and the fact 7 | that it is stored in the security database by default, its schema is different from 8 | that the IAS uses, hence conflicting with it if you want to use Messenger and Impress 9 | accounts simultaneously. If it is the case, the collection should be renamed or stored 10 | in another database, and, preferrably, some solution to synchronize the two authentication 11 | systems should be build on top of it. 12 | 13 | There were real reasons not to use the IAS built-in security mechanisms, and `lib/api.auth.js` 14 | provides a generic JSTP authentication solution that will eventually become a part 15 | of JSTP itself after thorough testing in this application for some time before transition 16 | to SRP zero-password-knowledge authentication scheme. 17 | 18 | The structure of documents in this collection is as follows: 19 | 20 | ```javascript 21 | { 22 | _id: ObjectID('...'), 23 | login: 'string', 24 | hash: 'bcrypt-hashed password', 25 | email: 'user@example.com', 26 | active: true 27 | } 28 | ``` 29 | 30 | The `login` field should be unique and indexed: 31 | 32 | ```javascript 33 | db.users.createIndex({ login: 1}, { unique: true }); 34 | ``` 35 | -------------------------------------------------------------------------------- /applications/messenger/client/README.md: -------------------------------------------------------------------------------- 1 | Configuration files and scripts in this directory are based on Facebook's 2 | [create-react-app](https://github.com/facebookincubator/create-react-app). 3 | 4 | ### Available Scripts 5 | 6 | In the project directory, you can run: 7 | 8 | #### `npm run dev` 9 | 10 | Runs Webpack in the development mode. 11 | 12 | Development assets are rsync'ed into the `static` directory. 13 | 14 | The page will reload automatically in Chrome on macOS if you make edits. 15 | You will also see any lint errors in the console. 16 | 17 | #### `npm run build` 18 | 19 | Builds the app for production to the `build` folder. 20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | Production assets are rsync'ed into the `static` directory and ready for deployment. 23 | 24 | The build is minified and the filenames include the hashes. 25 | 26 | ### Adding Flow 27 | 28 | In order for Flow to work, change your `.flowconfig` to look like this: 29 | 30 | ``` 31 | [libs] 32 | ./node_modules/fbjs/flow/lib 33 | 34 | [options] 35 | esproposal.class_static_fields=enable 36 | esproposal.class_instance_fields=enable 37 | 38 | module.name_mapper='^\(.*\)\.css$' -> '/config/flow/css' 39 | module.name_mapper='^\(.*\)\.\(jpg\|png\|gif\|eot\|svg\|ttf\|woff\|woff2\|mp4\|webm\)$' -> '/config/flow/file' 40 | 41 | suppress_type=$FlowIssue 42 | suppress_type=$FlowFixMe 43 | ``` 44 | 45 | Re-run flow, and you sholdn’t get any extra issues. 46 | -------------------------------------------------------------------------------- /config/sandbox/sandbox.js: -------------------------------------------------------------------------------- 1 | // Modules available in application sandbox 2 | 3 | module.exports = { 4 | 5 | // Following identifiers will be visible in sandbox global 6 | // There is no need to uncomment this if you you do not want to override list 7 | // You can hide 8 | // 9 | // global: [ 10 | // 'require', 'console', 'Buffer', 'SlowBuffer', 'process', 11 | // 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate' 12 | // ], 13 | 14 | // Following identifiers will be visible in sandbox as api. 15 | // 16 | api: [ 17 | // Node internal modules 18 | 'console', 'os', 'fs', 'sd', 'tls', 'net', 'dns', 'url', 'util', 'path', 'zlib', 'http', 'https', 'dgram', 19 | 'stream', 'buffer', 'domain', 'crypto', 'events', 'punycode', 'readline', 'querystring', 20 | 21 | // Impress API modules 22 | 'db', 23 | 'con', 24 | 'jstp', 25 | 'common', 26 | 'impress', 27 | 'registry', 28 | 'definition', 29 | 30 | // Preinstalled modules 31 | 'csv', 32 | 'async', 33 | 'iconv', 34 | 'colors', 35 | 'zipStream', // npm module zip-stream 36 | 37 | // Additional modules 38 | //'mkdirp', 39 | //'geoip', 40 | //'nodemailer', 41 | //'request', 42 | 'bcrypt', 43 | 44 | // Passport providers 45 | //'passport', // npm install passport 46 | //'passportGoogle', // npm install passport-google-oauth 47 | //'passportTwitter', // npm install passport-twitter 48 | //'passportFacebook' // npm install passport-facebook 49 | ] 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /config/servers.js: -------------------------------------------------------------------------------- 1 | { 2 | 3 | // Server ports bind configuration 4 | // Each server is named server on specified address and port 5 | 6 | master: { 7 | protocol: 'jstp', 8 | address: '127.0.0.1', 9 | ports: [250], 10 | slowTime: '1s' 11 | }, 12 | 13 | www: { 14 | protocol: 'http', // http, https, jstp, jstps 15 | address: '*', 16 | ports: [80], 17 | // list [81,82,83] 18 | // range from..to [81,,91] 19 | // range from..count [81, [8]] 20 | // range from..cpu-n [81, [-2]] 21 | slowTime: '1s', 22 | timeout: '30s', 23 | keepAlive: '5s', 24 | //applications: ['example'] // undefined for all 25 | }, 26 | 27 | rpc: { 28 | protocol: 'jstp', 29 | address: '*', 30 | ports: [3000, [1]], // Example: [81, [-1]] 31 | slowTime: '1s' 32 | }, 33 | 34 | //secureRpc: { 35 | // protocol: 'jstps', 36 | // address: '*', 37 | // ports: [4000, [1]], 38 | // slowTime: '1s', 39 | // key: 'example.key', 40 | // cert: 'example.cer' 41 | //}, 42 | 43 | //local: { 44 | // protocol: 'http', 45 | // address: '127.0.0.1', 46 | // ports: [80], 47 | // nagle: true, // Nagle algorithm, default true, set to false for latency optimization 48 | // slowTime: '1s', 49 | // timeout: '120s' // default 30s 50 | //}, 51 | 52 | //ssl: { 53 | // protocol: 'https', 54 | // address: '127.0.0.1', 55 | // ports: [443], 56 | // key: 'example.key', 57 | // cert: 'example.cer' 58 | //}, 59 | 60 | //static: { 61 | // protocol: 'http', 62 | // address: '127.0.0.1', 63 | // ports: [8080], 64 | // slowTime: 1000 65 | //} 66 | 67 | } 68 | -------------------------------------------------------------------------------- /applications/messenger/client/scripts/placeFiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const chalk = require('chalk'); 6 | const execSync = require('child_process').execSync; 7 | 8 | const buildPath = path.resolve(__dirname, '../build'); 9 | const staticPath = path.resolve(__dirname, '../../static'); 10 | const webpackAssets = path.join(buildPath, 'webpack-assets.json'); 11 | 12 | function fixFavicon(callback) { 13 | fs.readFile(webpackAssets, (err, data) => { 14 | if (err) throw err; 15 | let assets = JSON.parse(data).assets; 16 | let favicon = assets.find(asset => { 17 | return asset.name.endsWith('favicon.ico'); 18 | }); 19 | if (favicon) { 20 | copyFavicon(favicon.name, finalize); 21 | } else { 22 | console.log(chalk.yellow('Warning: no favicon.ico')); 23 | finalize(); 24 | } 25 | }); 26 | 27 | function finalize() { 28 | removeAssetsData(callback); 29 | } 30 | } 31 | 32 | function copyFavicon(filename, callback) { 33 | let source = path.join(buildPath, filename); 34 | let destination = path.join(buildPath, 'favicon.ico'); 35 | let sourceStream = fs.createReadStream(source); 36 | let destinationStream = fs.createWriteStream(destination); 37 | destinationStream.on('close', callback); 38 | sourceStream.pipe(destinationStream); 39 | } 40 | 41 | function removeAssetsData(callback) { 42 | fs.unlink(webpackAssets, callback); 43 | } 44 | 45 | module.exports = (callback) => { 46 | fixFavicon(() => { 47 | let staticIndex = path.join(staticPath, 'index.html'); 48 | let wwwIndex = path.resolve(staticPath, '../www/html.template'); 49 | execSync('rsync -a --delete ' + buildPath + '/ ' + staticPath); 50 | execSync('mv ' + staticIndex + ' ' + wwwIndex); 51 | callback(); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /applications/messenger/config/sandbox.js: -------------------------------------------------------------------------------- 1 | { 2 | // Modules available in application sandbox 3 | 4 | // Following identifiers will be visible in sandbox global 5 | // There is no need to uncomment this if you you do not want to override list 6 | // You can hide 7 | // 8 | // global: [ 9 | // 'require', 'console', 'Buffer', 'SlowBuffer', 10 | // 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate' 11 | // ], 12 | 13 | // Following identifiers will be visible in sandbox as api. 14 | // 15 | api: [ 16 | // Node internal modules 17 | 'console', 'os', 'fs', 'sd', 'tls','net', 'dns', 'url', 'util', 'path', 'zlib', 'http', 'https', 'dgram', 18 | 'stream', 'buffer', 'domain', 'crypto', 'events', 'punycode', 'readline', 'querystring', 'process', 'timers', 19 | 20 | // Impress API modules 21 | 'db', 'con', 'jstp', 'common', 'impress', 'metasync', 'registry', 'definition', 22 | 23 | // Preinstalled modules 24 | 'csv', 25 | 'async', 26 | 'iconv', 27 | 'colors', 28 | 'zipStream', // npm module zip-stream 29 | 30 | // Additional modules 31 | //'mkdirp', 32 | //'geoip', 33 | //'nodemailer', 34 | //'request', 35 | 'bcrypt', 36 | 37 | // Passport providers 38 | //'passport', // npm install passport 39 | //'passportGoogle', // npm install passport-google-oauth 40 | //'passportTwitter', // npm install passport-twitter 41 | //'passportFacebook' // npm install passport-facebook 42 | ], 43 | 44 | // Import from other applications 45 | //import: { 46 | // appName: { // application name 47 | // 'api.nameExport': 'api.nameImport' // name mapping hash 48 | // } 49 | //}, 50 | 51 | // Allow to export to other applications 52 | //export: [ 53 | // 'api.nameExport' 54 | //] 55 | 56 | } 57 | -------------------------------------------------------------------------------- /applications/messenger/templates/error.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @title@ 5 | 6 | 7 | 16 | 17 | 18 | 19 |
@title@
20 |
@message@ refresh home
21 | 22 | -------------------------------------------------------------------------------- /applications/messenger/lib/api.auth.js: -------------------------------------------------------------------------------- 1 | api.auth = {}; 2 | 3 | api.auth.config = { 4 | saltRounds: 10, 5 | database: application.databases.security 6 | }; 7 | 8 | api.auth.hash = (password, callback) => { 9 | api.bcrypt.genSalt(api.auth.config.saltRounds, (err, salt) => { 10 | if (err) { 11 | return callback(err); 12 | } 13 | 14 | api.bcrypt.hash(password, salt, (err, hash) => { 15 | if (err) { 16 | return callback(err); 17 | } 18 | 19 | callback(null, hash); 20 | }); 21 | }); 22 | }; 23 | 24 | api.auth.verify = (password, hashed, callback) => { 25 | api.bcrypt.compare(password, hashed, callback); 26 | }; 27 | 28 | api.auth.signUp = (login, password, name, email, callback) => { 29 | api.auth.hash(password, (err, hash) => { 30 | if (err) { 31 | return callback(err); 32 | } 33 | 34 | let user = { 35 | login, 36 | hash, 37 | name, 38 | email, 39 | active: true 40 | }; 41 | 42 | let db = api.auth.config.database; 43 | db.users.insert(user, (err, result) => { 44 | if (err) { 45 | return callback(err); 46 | } 47 | 48 | let oid = result.ops[0]._id; 49 | user.id = oid.toString(); 50 | 51 | application.emit('newUser', user); 52 | callback(null, user); 53 | }); 54 | }); 55 | }; 56 | 57 | api.auth.signIn = (connection, login, password, callback) => { 58 | if (connection.isAuthenticated) { 59 | return callback(new Error('user is already authenticated')); 60 | } 61 | 62 | let db = api.auth.config.database; 63 | db.users.findOne({ login }, (err, user) => { 64 | if (err || !user) { 65 | return callback(new Error('user not found')); 66 | } 67 | 68 | if (!user.active) { 69 | return callback(new Error('user is not active')); 70 | } 71 | 72 | api.auth.verify(password, user.hash, (err, success) => { 73 | if (err || !success) { 74 | return callback('incorrect password'); 75 | } 76 | 77 | connection.isAuthenticated = true; 78 | connection.user = user; 79 | callback(null, user); 80 | }); 81 | }); 82 | }; 83 | 84 | api.auth.signOut = (connection, callback) => { 85 | connection.isAuthenticated = false; 86 | delete connection.user; 87 | callback(); 88 | }; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metarhia-messenger", 3 | "version": "0.0.1", 4 | "description": "Unopinionated messaging app built using the experimental stack of technologies", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "node tests/run.js", 8 | "build": "node applications/messenger/client/scripts/build.js", 9 | "dev": "node applications/messenger/client/scripts/start.js", 10 | "cli": "node cli/app.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/metarhia/Messenger.git" 15 | }, 16 | "author": "Alexey Orlenko ", 17 | "license": "UNLICENSED", 18 | "bugs": { 19 | "url": "https://github.com/metarhia/Messenger/issues" 20 | }, 21 | "homepage": "https://github.com/metarhia/Messenger#readme", 22 | "private": true, 23 | "dependencies": { 24 | "bcrypt": "^0.8.7", 25 | "impress": "^0.3.15", 26 | "mongodb": "^2.2.4" 27 | }, 28 | "devDependencies": { 29 | "antd": "^1.7.0", 30 | "assets-webpack-plugin": "github:aqrln/assets-webpack-plugin", 31 | "autoprefixer": "6.3.7", 32 | "babel-core": "6.10.4", 33 | "babel-eslint": "6.1.2", 34 | "babel-loader": "6.2.4", 35 | "babel-plugin-antd": "^0.4.1", 36 | "babel-plugin-syntax-trailing-function-commas": "6.8.0", 37 | "babel-plugin-transform-class-properties": "6.10.2", 38 | "babel-plugin-transform-function-bind": "^6.8.0", 39 | "babel-plugin-transform-object-rest-spread": "6.8.0", 40 | "babel-plugin-transform-react-constant-elements": "6.9.1", 41 | "babel-polyfill": "^6.9.1", 42 | "babel-preset-es2015": "6.9.0", 43 | "babel-preset-es2016": "6.11.3", 44 | "babel-preset-react": "6.11.1", 45 | "chalk": "1.1.3", 46 | "cross-spawn": "4.0.0", 47 | "css-loader": "0.23.1", 48 | "eslint": "3.1.1", 49 | "eslint-loader": "1.4.1", 50 | "eslint-plugin-import": "1.10.3", 51 | "eslint-plugin-react": "5.2.2", 52 | "extract-text-webpack-plugin": "1.0.1", 53 | "favicons-webpack-plugin": "0.0.6", 54 | "file-loader": "0.9.0", 55 | "fs-extra": "^0.30.0", 56 | "html-webpack-plugin": "2.22.0", 57 | "json-loader": "0.5.4", 58 | "opn": "4.0.2", 59 | "postcss-loader": "0.9.1", 60 | "react": "^15.2.1", 61 | "react-dom": "^15.2.1", 62 | "react-redux": "^4.4.5", 63 | "react-router": "^2.6.1", 64 | "react-router-redux": "^4.0.5", 65 | "redux": "^3.5.2", 66 | "redux-logger": "^2.6.1", 67 | "redux-thunk": "^2.1.0", 68 | "style-loader": "0.13.1", 69 | "url-loader": "0.5.7", 70 | "webpack": "1.13.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /applications/messenger/client/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const autoprefixer = require('autoprefixer'); 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); 8 | const AssetsWebpackPlugin = require('assets-webpack-plugin'); 9 | 10 | const srcPath = path.resolve(__dirname, '../src'); 11 | const nodeModulesPath = path.join(__dirname, '../../../../node_modules'); 12 | const indexHtmlPath = path.resolve(__dirname, '../index.html'); 13 | const logoPath = path.resolve(__dirname, '../metarhia.png'); 14 | const buildPath = path.join(__dirname, '../build'); 15 | 16 | module.exports = { 17 | devtool: 'source-map', 18 | entry: [ 19 | 'babel-polyfill', 20 | path.join(srcPath, 'index') 21 | ], 22 | watch: true, 23 | output: { 24 | path: buildPath, 25 | pathinfo: true, 26 | filename: 'bundle.js', 27 | publicPath: '/' 28 | }, 29 | resolve: { 30 | extensions: ['', '.js'], 31 | }, 32 | resolveLoader: { 33 | root: nodeModulesPath, 34 | moduleTemplates: ['*-loader'] 35 | }, 36 | module: { 37 | preLoaders: [ 38 | { 39 | test: /\.js$/, 40 | loader: 'eslint', 41 | include: srcPath, 42 | } 43 | ], 44 | loaders: [ 45 | { 46 | test: /\.js$/, 47 | include: srcPath, 48 | loader: 'babel', 49 | query: require('./babel.dev') 50 | }, 51 | { 52 | test: /\.css$/, 53 | loader: 'style!css!postcss' 54 | }, 55 | { 56 | test: /\.json$/, 57 | loader: 'json' 58 | }, 59 | { 60 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/, 61 | loader: 'url?limit=10000', 62 | }, 63 | { 64 | test: /\.(mp4|webm)$/, 65 | loader: 'url?limit=10000' 66 | } 67 | ] 68 | }, 69 | eslint: { 70 | configFile: path.join(__dirname, 'eslint.js'), 71 | useEslintrc: false 72 | }, 73 | postcss: function() { 74 | return [autoprefixer]; 75 | }, 76 | plugins: [ 77 | new AssetsWebpackPlugin({ 78 | path: buildPath, 79 | assetsRegex: /favicon\.ico$/ 80 | }), 81 | new FaviconsWebpackPlugin({ 82 | logo: logoPath, 83 | persistentCache: true, 84 | inject: true, 85 | title: 'Metarhia Messenger', 86 | icons: { 87 | android: true, 88 | appleIcon: true, 89 | appleStartup: true, 90 | coast: false, 91 | favicons: true, 92 | firefox: false, 93 | opengraph: false, 94 | twitter: false, 95 | yandex: false, 96 | windows: false 97 | } 98 | }), 99 | new HtmlWebpackPlugin({ 100 | inject: true, 101 | template: indexHtmlPath 102 | }), 103 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }), 104 | ] 105 | }; 106 | -------------------------------------------------------------------------------- /applications/messenger/templates/introspection.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @title@: @path@ 5 | 6 | 7 | 24 | 53 | 54 | 55 | 56 |
@title@
57 |
@[dirs]@/@.name@@[/dirs]@/
58 | 59 | @[files]@@[/files]@ 60 |
CallMethodModify time
@.name@@.method@@.mtime@
61 | 62 | 63 | -------------------------------------------------------------------------------- /applications/messenger/templates/index.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @title@: @path@ 5 | 6 | 7 | 25 | 54 | 55 | 56 | 57 |
@title@
58 |
@[dirs]@/@.name@@[/dirs]@/
59 | 60 | @[files]@@[/files]@ 61 |
NameSizeModify time
@.name@@.size@@.mtime@
62 | 63 | 64 | -------------------------------------------------------------------------------- /cli/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Create an Impress-like environment 4 | 5 | global.api = {}; 6 | 7 | api.jstp = {}; 8 | api.url = require('url'); 9 | api.events = require('events'); 10 | api.readline = require('readline'); 11 | api.metasync = require('metasync'); 12 | 13 | require('impress'); 14 | 15 | // Readline interface 16 | const rl = api.readline.createInterface({ 17 | input: process.stdin, 18 | output: process.stdout, 19 | completer(line) { 20 | const completions = [ 21 | 'rooms', 'users', 'create', 22 | 'join', 'leave', 'post', 'quit' 23 | ]; 24 | const hints = completions.filter(c => c.startsWith(line)); 25 | return [hints.length ? hints : completions, line]; 26 | } 27 | }); 28 | 29 | // CLI commands handlers 30 | const commands = { 31 | signUp(login, password, name, email, callback) { 32 | api.auth.signUp(login, password, name, email, (err, id) => { 33 | if (err) { 34 | console.log(err); 35 | } else { 36 | console.log('User ID:', id); 37 | } 38 | callback(); 39 | }); 40 | }, 41 | 42 | signIn(login, password, callback) { 43 | api.auth.signIn(login, password, (err) => { 44 | if (err) { 45 | console.log(err); 46 | } 47 | callback(); 48 | }); 49 | }, 50 | 51 | signOut(callback) { 52 | api.auth.signOut((err) => { 53 | if (err) { 54 | console.log(err); 55 | } 56 | callback(); 57 | }); 58 | }, 59 | 60 | quit() { 61 | rl.close(); 62 | process.exit(0); 63 | } 64 | }; 65 | 66 | // Fail with an error message 67 | // message - detailed custom error message 68 | // error - exception instance 69 | // 70 | function fatal(message, error) { 71 | console.error(message); 72 | console.error(error); 73 | process.exit(1); 74 | } 75 | 76 | // Load introspection of a remote interface 77 | // connection - JSTP connection 78 | // interfaceName - name of the interface 79 | // callback - callback function 80 | // 81 | function loadIntrospection(connection, interfaceName, callback) { 82 | connection.inspect(interfaceName, (err, proxy) => { 83 | if (err) { 84 | fatal('Could not read introspection of remote API', err); 85 | } 86 | api[interfaceName] = proxy; 87 | callback(err); 88 | }); 89 | } 90 | 91 | // Setup JSTP connection with the server 92 | // callback - callback function 93 | // 94 | function setupConnection(callback) { 95 | const address = process.argv[2] || 'jstp://127.0.0.1:3000'; 96 | const parsedAddress = api.url.parse(address); 97 | const host = parsedAddress.hostname; 98 | const port = parsedAddress.port; 99 | const secure = parsedAddress.protocol === 'jstps:'; 100 | 101 | console.log('Waiting for connection...'); 102 | const connection = api.jstp.connect('messenger', host, port, secure); 103 | 104 | connection.application = new api.events.EventEmitter(); 105 | connection.application.api = {}; 106 | connection.application.connections = {}; 107 | connection.application.sandbox = global; 108 | 109 | connection.on('connect', () => { 110 | connection.handshake('messenger', 'user', 'pass', (err, sessionHash) => { 111 | api.metasync.each(['auth', 'messaging'], 112 | loadIntrospection.bind(null, connection), 113 | (err) => { 114 | if (err) throw err; 115 | callback(); 116 | }); 117 | }); 118 | }); 119 | } 120 | 121 | // Custom readline prompt function that doesn't block us from using 122 | // rl.question inside handlers 123 | // 124 | function prompt() { 125 | setImmediate(() => { 126 | rl.question('> ', (line) => { 127 | let input = line.split(' ').filter(fragment => fragment !== ''); 128 | if (input.length === 0 || input[0] === '') { 129 | return prompt(); 130 | } 131 | 132 | let command = commands[input[0]]; 133 | if (command) { 134 | let args = [...input.slice(1), prompt]; 135 | command.apply(commands, args); 136 | } else { 137 | console.log('Unknown command, press to show completions'); 138 | } 139 | }); 140 | }); 141 | } 142 | 143 | setupConnection(prompt); 144 | -------------------------------------------------------------------------------- /applications/messenger/client/config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const autoprefixer = require('autoprefixer'); 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); 9 | const AssetsWebpackPlugin = require('assets-webpack-plugin'); 10 | 11 | const srcPath = path.resolve(__dirname, '../src'); 12 | const nodeModulesPath = path.join(__dirname, '../../../../node_modules'); 13 | const indexHtmlPath = path.resolve(__dirname, '../index.html'); 14 | const logoPath = path.resolve(__dirname, '../metarhia.png'); 15 | const buildPath = path.join(__dirname, '../build'); 16 | 17 | module.exports = { 18 | bail: true, 19 | devtool: 'source-map', 20 | entry: [ 21 | 'babel-polyfill', 22 | path.join(srcPath, 'index') 23 | ], 24 | output: { 25 | path: buildPath, 26 | filename: '[name].[chunkhash].js', 27 | chunkFilename: '[name].[chunkhash].chunk.js', 28 | publicPath: '/' 29 | }, 30 | resolve: { 31 | extensions: ['', '.js'], 32 | }, 33 | resolveLoader: { 34 | root: nodeModulesPath, 35 | moduleTemplates: ['*-loader'] 36 | }, 37 | module: { 38 | preLoaders: [ 39 | { 40 | test: /\.js$/, 41 | loader: 'eslint', 42 | include: srcPath 43 | } 44 | ], 45 | loaders: [ 46 | { 47 | test: /\.js$/, 48 | include: srcPath, 49 | loader: 'babel', 50 | query: require('./babel.prod') 51 | }, 52 | { 53 | test: /\.css$/, 54 | // Disable autoprefixer in css-loader itself: 55 | // https://github.com/webpack/css-loader/issues/281 56 | // We already have it thanks to postcss. 57 | loader: ExtractTextPlugin.extract('style', 'css?-autoprefixer!postcss') 58 | }, 59 | { 60 | test: /\.json$/, 61 | loader: 'json' 62 | }, 63 | { 64 | test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)$/, 65 | loader: 'url?limit=10000', 66 | }, 67 | { 68 | test: /\.(mp4|webm)$/, 69 | loader: 'url?limit=10000' 70 | } 71 | ] 72 | }, 73 | eslint: { 74 | // TODO: consider separate config for production, 75 | // e.g. to enable no-console and no-debugger only in prod. 76 | configFile: path.join(__dirname, 'eslint.js'), 77 | useEslintrc: false 78 | }, 79 | postcss: function() { 80 | return [autoprefixer]; 81 | }, 82 | plugins: [ 83 | new AssetsWebpackPlugin({ 84 | path: buildPath, 85 | assetsRegex: /favicon\.ico$/ 86 | }), 87 | new FaviconsWebpackPlugin({ 88 | logo: logoPath, 89 | persistentCache: true, 90 | inject: true, 91 | title: 'Metarhia Messenger', 92 | icons: { 93 | android: true, 94 | appleIcon: true, 95 | appleStartup: true, 96 | coast: true, 97 | favicons: true, 98 | firefox: true, 99 | opengraph: true, 100 | twitter: true, 101 | yandex: true, 102 | windows: true 103 | } 104 | }), 105 | new HtmlWebpackPlugin({ 106 | inject: true, 107 | template: indexHtmlPath, 108 | minify: { 109 | removeComments: true, 110 | collapseWhitespace: true, 111 | removeRedundantAttributes: true, 112 | useShortDoctype: true, 113 | removeEmptyAttributes: true, 114 | removeStyleLinkTypeAttributes: true, 115 | keepClosingSlash: true, 116 | minifyJS: true, 117 | minifyCSS: true, 118 | minifyURLs: true 119 | } 120 | }), 121 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), 122 | new webpack.optimize.OccurrenceOrderPlugin(), 123 | new webpack.optimize.DedupePlugin(), 124 | new webpack.optimize.UglifyJsPlugin({ 125 | compressor: { 126 | screw_ie8: true, 127 | warnings: false 128 | }, 129 | mangle: { 130 | screw_ie8: true 131 | }, 132 | output: { 133 | comments: false, 134 | screw_ie8: true 135 | } 136 | }), 137 | new ExtractTextPlugin('[name].[contenthash].css') 138 | ] 139 | }; 140 | -------------------------------------------------------------------------------- /applications/messenger/client/scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | const path = require('path'); 6 | const chalk = require('chalk'); 7 | const webpack = require('webpack'); 8 | const config = require('../config/webpack.config.dev'); 9 | const execSync = require('child_process').execSync; 10 | const opn = require('opn'); 11 | const placeFiles = require('./placeFiles'); 12 | 13 | const buildDir = path.join(__dirname, '../build'); 14 | const staticDir = path.join(__dirname, '../../static'); 15 | 16 | const developmentUrl = 'http://localhost:8080/'; 17 | 18 | execSync('rm -rf ' + buildDir); 19 | 20 | const friendlySyntaxErrorLabel = 'Syntax error:'; 21 | 22 | function isLikelyASyntaxError(message) { 23 | return message.indexOf(friendlySyntaxErrorLabel) !== -1; 24 | } 25 | 26 | // This is a little hacky. 27 | // It would be easier if webpack provided a rich error object. 28 | 29 | function formatMessage(message) { 30 | return message 31 | // Make some common errors shorter: 32 | .replace( 33 | // Babel syntax error 34 | 'Module build failed: SyntaxError:', 35 | friendlySyntaxErrorLabel 36 | ) 37 | .replace( 38 | // Webpack file not found error 39 | /Module not found: Error: Cannot resolve 'file' or 'directory'/, 40 | 'Module not found:' 41 | ) 42 | // Internal stacks are generally useless so we strip them 43 | .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y 44 | // Webpack loader names obscure CSS filenames 45 | .replace('./~/css-loader!./~/postcss-loader!', ''); 46 | } 47 | 48 | function clearConsole() { 49 | process.stdout.write('\x1B[2J\x1B[0f'); 50 | } 51 | 52 | const compiler = webpack(config); 53 | compiler.plugin('invalid', () => { 54 | clearConsole(); 55 | console.log('Compiling...'); 56 | }); 57 | compiler.plugin('done', (stats) => { 58 | clearConsole(); 59 | let hasErrors = stats.hasErrors(); 60 | let hasWarnings = stats.hasWarnings(); 61 | if (!hasErrors && !hasWarnings) { 62 | console.log(chalk.green('Compiled successfully!')); 63 | console.log(); 64 | return; 65 | } 66 | 67 | let json = stats.toJson(); 68 | let formattedErrors = json.errors.map(message => 69 | 'Error in ' + formatMessage(message) 70 | ); 71 | let formattedWarnings = json.warnings.map(message => 72 | 'Warning in ' + formatMessage(message) 73 | ); 74 | 75 | if (hasErrors) { 76 | console.log(chalk.red('Failed to compile.')); 77 | console.log(); 78 | if (formattedErrors.some(isLikelyASyntaxError)) { 79 | // If there are any syntax errors, show just them. 80 | // This prevents a confusing ESLint parsing error 81 | // preceding a much more useful Babel syntax error. 82 | formattedErrors = formattedErrors.filter(isLikelyASyntaxError); 83 | } 84 | formattedErrors.forEach(message => { 85 | console.log(message); 86 | console.log(); 87 | }); 88 | // If errors exist, ignore warnings. 89 | return; 90 | } 91 | 92 | if (hasWarnings) { 93 | console.log(chalk.yellow('Compiled with warnings.')); 94 | console.log(); 95 | formattedWarnings.forEach(message => { 96 | console.log(message); 97 | console.log(); 98 | }); 99 | 100 | console.log('You may use special comments to disable some warnings.'); 101 | console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.'); 102 | console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.'); 103 | } 104 | }); 105 | 106 | function openBrowser(reloadAfterChange) { 107 | let scriptName = reloadAfterChange ? 'reloadChrome' 108 | : 'openChrome'; 109 | if (process.platform === 'darwin') { 110 | try { 111 | // Try our best to reuse existing tab 112 | // on OS X Google Chrome with AppleScript 113 | execSync('ps cax | grep "Google Chrome"'); 114 | execSync( 115 | 'osascript ' + 116 | path.resolve(__dirname, './' + scriptName + '.applescript') + 117 | ' ' + developmentUrl 118 | ); 119 | return; 120 | } catch (err) { 121 | // Ignore errors. 122 | } 123 | } 124 | 125 | if (!reloadAfterChange) { 126 | // Fallback to opn 127 | // (It will always open new tab) 128 | opn(developmentUrl); 129 | } 130 | } 131 | 132 | let firstCompile = true; 133 | 134 | compiler.watch({ 135 | aggregateTimeout: 300 136 | }, (err, stats) => { 137 | if (err || stats.hasErrors()) { 138 | return; 139 | } 140 | placeFiles(() => { 141 | setTimeout(() => { 142 | openBrowser(!firstCompile); 143 | firstCompile = false; 144 | }, 2000); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /applications/messenger/client/config/eslint.js: -------------------------------------------------------------------------------- 1 | // We use eslint-loader so even warnings are very visibile. 2 | // This is why we only use "WARNING" level for potential errors, 3 | // and we don't use "ERROR" level at all. 4 | 5 | // In the future, we might create a separate list of rules for production. 6 | // It would probably be more strict. 7 | 8 | const WARNING = 1; 9 | 10 | module.exports = { 11 | root: true, 12 | 13 | parser: 'babel-eslint', 14 | 15 | // import plugin is termporarily disabled, scroll below to see why 16 | plugins: ['react'/*, 'import'*/], 17 | 18 | env: { 19 | es6: true, 20 | commonjs: true, 21 | browser: true 22 | }, 23 | 24 | parserOptions: { 25 | ecmaVersion: 6, 26 | sourceType: 'module', 27 | ecmaFeatures: { 28 | jsx: true, 29 | generators: true, 30 | experimentalObjectRestSpread: true 31 | } 32 | }, 33 | 34 | settings: { 35 | 'import/ignore': [ 36 | 'node_modules', 37 | '\\.(json|css|jpg|png|gif|eot|svg|ttf|woff|woff2|mp4|webm)$', 38 | ], 39 | 'import/extensions': ['.js'], 40 | 'import/resolver': { 41 | node: { 42 | extensions: ['.js', '.json'] 43 | } 44 | } 45 | }, 46 | 47 | rules: { 48 | // http://eslint.org/docs/rules/ 49 | 'array-callback-return': WARNING, 50 | 'default-case': [WARNING, { commentPattern: '^no default$' }], 51 | 'dot-location': [WARNING, 'property'], 52 | eqeqeq: [WARNING, 'allow-null'], 53 | 'guard-for-in': WARNING, 54 | 'new-cap': [WARNING, { newIsCap: true }], 55 | 'new-parens': WARNING, 56 | 'no-array-constructor': WARNING, 57 | 'no-caller': WARNING, 58 | 'no-cond-assign': [WARNING, 'always'], 59 | 'no-const-assign': WARNING, 60 | 'no-control-regex': WARNING, 61 | 'no-delete-var': WARNING, 62 | 'no-dupe-args': WARNING, 63 | 'no-dupe-class-members': WARNING, 64 | 'no-dupe-keys': WARNING, 65 | 'no-duplicate-case': WARNING, 66 | 'no-empty-character-class': WARNING, 67 | 'no-empty-pattern': WARNING, 68 | 'no-eval': WARNING, 69 | 'no-ex-assign': WARNING, 70 | 'no-extend-native': WARNING, 71 | 'no-extra-bind': WARNING, 72 | 'no-extra-label': WARNING, 73 | 'no-fallthrough': WARNING, 74 | 'no-func-assign': WARNING, 75 | 'no-implied-eval': WARNING, 76 | 'no-invalid-regexp': WARNING, 77 | 'no-iterator': WARNING, 78 | 'no-label-var': WARNING, 79 | 'no-labels': [WARNING, { allowLoop: false, allowSwitch: false }], 80 | 'no-lone-blocks': WARNING, 81 | 'no-loop-func': WARNING, 82 | 'no-mixed-operators': [WARNING, { 83 | groups: [ 84 | ['+', '-', '*', '/', '%', '**'], 85 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 86 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 87 | ['&&', '||'], 88 | ['in', 'instanceof'] 89 | ], 90 | allowSamePrecedence: false 91 | }], 92 | 'no-multi-str': WARNING, 93 | 'no-native-reassign': WARNING, 94 | 'no-negated-in-lhs': WARNING, 95 | 'no-new-func': WARNING, 96 | 'no-new-object': WARNING, 97 | 'no-new-symbol': WARNING, 98 | 'no-new-wrappers': WARNING, 99 | 'no-obj-calls': WARNING, 100 | 'no-octal': WARNING, 101 | 'no-octal-escape': WARNING, 102 | 'no-redeclare': WARNING, 103 | 'no-regex-spaces': WARNING, 104 | 'no-restricted-syntax': [ 105 | WARNING, 106 | 'LabeledStatement', 107 | 'WithStatement', 108 | ], 109 | 'no-return-assign': WARNING, 110 | 'no-script-url': WARNING, 111 | 'no-self-assign': WARNING, 112 | 'no-self-compare': WARNING, 113 | 'no-sequences': WARNING, 114 | 'no-shadow-restricted-names': WARNING, 115 | 'no-sparse-arrays': WARNING, 116 | 'no-this-before-super': WARNING, 117 | 'no-throw-literal': WARNING, 118 | 'no-undef': WARNING, 119 | 'no-unexpected-multiline': WARNING, 120 | 'no-unreachable': WARNING, 121 | 'no-unused-expressions': WARNING, 122 | 'no-unused-labels': WARNING, 123 | 'no-unused-vars': [WARNING, { vars: 'local', args: 'none' }], 124 | 'no-use-before-define': [WARNING, 'nofunc'], 125 | 'no-useless-computed-key': WARNING, 126 | 'no-useless-concat': WARNING, 127 | 'no-useless-constructor': WARNING, 128 | 'no-useless-escape': WARNING, 129 | 'no-useless-rename': [WARNING, { 130 | ignoreDestructuring: false, 131 | ignoreImport: false, 132 | ignoreExport: false, 133 | }], 134 | 'no-with': WARNING, 135 | 'no-whitespace-before-property': WARNING, 136 | 'operator-assignment': [WARNING, 'always'], 137 | radix: WARNING, 138 | 'require-yield': WARNING, 139 | 'rest-spread-spacing': [WARNING, 'never'], 140 | strict: [WARNING, 'never'], 141 | 'unicode-bom': [WARNING, 'never'], 142 | 'use-isnan': WARNING, 143 | 'valid-typeof': WARNING, 144 | 145 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/ 146 | 147 | // TODO: import rules are temporarily disabled because they don't play well 148 | // with how eslint-loader only checks the file you change. So if module A 149 | // imports module B, and B is missing a default export, the linter will 150 | // record this as an issue in module A. Now if you fix module B, the linter 151 | // will not be aware that it needs to re-lint A as well, so the error 152 | // will stay until the next restart, which is really confusing. 153 | 154 | // This is probably fixable with a patch to eslint-loader. 155 | // When file A is saved, we want to invalidate all files that import it 156 | // *and* that currently have lint errors. This should fix the problem. 157 | 158 | // 'import/default': WARNING, 159 | // 'import/export': WARNING, 160 | // 'import/named': WARNING, 161 | // 'import/namespace': WARNING, 162 | // 'import/no-amd': WARNING, 163 | // 'import/no-duplicates': WARNING, 164 | // 'import/no-extraneous-dependencies': WARNING, 165 | // 'import/no-named-as-default': WARNING, 166 | // 'import/no-named-as-default-member': WARNING, 167 | // 'import/no-unresolved': [WARNING, { commonjs: true }], 168 | 169 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules 170 | 'react/jsx-equals-spacing': [WARNING, 'never'], 171 | 'react/jsx-handler-names': [WARNING, { 172 | eventHandlerPrefix: 'handle', 173 | eventHandlerPropPrefix: 'on', 174 | }], 175 | 'react/jsx-no-duplicate-props': [WARNING, { ignoreCase: true }], 176 | 'react/jsx-no-undef': WARNING, 177 | 'react/jsx-pascal-case': [WARNING, { 178 | allowAllCaps: true, 179 | ignore: [], 180 | }], 181 | 'react/jsx-uses-react': WARNING, 182 | 'react/jsx-uses-vars': WARNING, 183 | 'react/no-deprecated': WARNING, 184 | 'react/no-direct-mutation-state': WARNING, 185 | 'react/no-is-mounted': WARNING, 186 | 'react/react-in-jsx-scope': WARNING, 187 | 'react/require-render-return': WARNING 188 | } 189 | }; 190 | -------------------------------------------------------------------------------- /applications/messenger/client/src/impress.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 'use strict'; 4 | 5 | window.global = window; 6 | global.api = {}; 7 | global.application = {}; 8 | 9 | api.impress = {}; 10 | api.common = {}; 11 | 12 | api.common.falseness = function() { return false; }; 13 | api.common.trueness = function() { return true; }; 14 | api.common.emptyness = function() { }; 15 | 16 | // Extend obj with properties of ext 17 | // 18 | api.common.extend = function(obj, ext) { 19 | if (obj === undefined) obj = null; 20 | for (var property in ext) obj[property] = ext[property]; 21 | return obj; 22 | }; 23 | 24 | // Make URL absolute 25 | // 26 | api.common.absoluteUrl = function(url) { 27 | if (url.charAt(0) === '/') { 28 | var site = window.location, 29 | absoluteUrl = 'ws'; 30 | if (site.protocol === 'https:') absoluteUrl += 's'; 31 | absoluteUrl += '://' + site.host + url; 32 | return absoluteUrl; 33 | } else return url; 34 | }; 35 | 36 | // Return random number less then one argument random(100) or between two argumants random(50,150) 37 | // 38 | api.common.random = function(min, max) { 39 | if (arguments.length === 1) { 40 | max = min; 41 | min = 0; 42 | } 43 | return min + Math.floor(Math.random() * (max - min + 1)); 44 | }; 45 | 46 | // Simple EventEmitter implementation 47 | // 48 | api.events = {}; 49 | 50 | // EventEmitter class 51 | // 52 | api.events.EventEmitter = function() { 53 | api.events.mixinEmitter(this); 54 | }; 55 | 56 | // EventEmitter mixin 57 | // 58 | api.events.mixinEmitter = function(ee) { 59 | 60 | ee.listeners = {}; 61 | 62 | // Add named event handler 63 | // 64 | ee.on = function(name, callback) { 65 | var namedEvent = ee.listeners[name]; 66 | if (!namedEvent) ee.listeners[name] = [callback]; 67 | else namedEvent.push(callback); 68 | }; 69 | 70 | // Emit named event 71 | // 72 | ee.emit = function(name, data) { 73 | var namedEvent = ee.listeners[name]; 74 | if (namedEvent) namedEvent.forEach(function(callback) { 75 | callback(data); 76 | }); 77 | }; 78 | 79 | return ee; 80 | 81 | }; 82 | 83 | // DOM utilities 84 | // 85 | api.dom = {}; 86 | api.dom.html = document.documentElement || document.getElementsByTagName('html')[0]; 87 | api.dom.head = document.head || document.getElementsByTagName('head')[0]; 88 | api.dom.body = null; 89 | api.dom.form = null; 90 | 91 | // Platform detection 92 | // 93 | api.dom.platform = { 94 | iPhone: navigator.userAgent.match(/iPhone/i), 95 | iPod: navigator.userAgent.match(/iPod/i), 96 | iPad: navigator.userAgent.match(/iPad/i), 97 | Android: navigator.userAgent.match(/Android/i), 98 | IE: navigator.appName.indexOf('Microsoft') !== -1, 99 | IEMobile: navigator.userAgent.match(/IEMobile/i), 100 | Chrome: !!window.chrome, // navigator.userAgent.match(/Chrome/i), 101 | Safari: navigator.userAgent.match(/Safari/i) && !window.chrome, 102 | FireFox: navigator.userAgent.indexOf('Firefox') > -1, 103 | BlackBerry: navigator.userAgent.match(/BlackBerry/i), 104 | WebOS: navigator.userAgent.match(/webOS/i), 105 | Opera: window.opera, // navigator.userAgent.indexOf('Presto') > -1 106 | OperaMini: navigator.userAgent.match(/Opera Mini/i), 107 | OperaMobi: navigator.userAgent.match(/Opera Mobi/i) 108 | }; 109 | 110 | var platform = api.dom.platform; 111 | 112 | platform.iOS = platform.iPhone || platform.iPod || platform.iPad; 113 | platform.Mobile = platform.iOS || platform.Android || platform.OperaMini || platform.OperaMobi || platform.BlackBerry || platform.WebOS; 114 | platform.WebKit = platform.Chrome || platform.Safari; 115 | 116 | if (platform.IE) platform.IEVersion = parseFloat(navigator.appVersion.split('MSIE')[1]); 117 | 118 | // Patch page links to prevent page reload 119 | // 120 | api.dom.fixLinks = function(persist) { 121 | 122 | function makeLink(link) { 123 | link.addEventListener('click', function(/*event*/) { 124 | //event.preventDefault(); 125 | if (persist && this.host === window.location.host) localStorage.setItem('location', this.pathname + this.search); 126 | window.location = this.href; 127 | }, false); 128 | } 129 | 130 | if (platform.iOS) { 131 | if (persist === null) persist = true; 132 | persist = persist && localStorage; 133 | if (persist) { 134 | var currentLocation = window.location.pathname + window.location.search, 135 | storedLocation = localStorage.getItem('location'); 136 | if (storedLocation && storedLocation !== currentLocation) window.location = storedLocation; 137 | } 138 | var links = document.getElementsByTagName('a'); 139 | for (var i = 0; i < links.length; i++) makeLink(links[i]); 140 | } 141 | 142 | }; 143 | 144 | // Save cookies in localstorage 145 | // 146 | api.dom.fixCookie = function(sessionCookieName) { 147 | if (localStorage && platform.iOS) { 148 | var cookieSession = document.cookie.match(new RegExp(sessionCookieName + '=[^;]+')), 149 | localSession = localStorage.getItem(sessionCookieName); 150 | if (cookieSession) { 151 | cookieSession = cookieSession[0].replace(sessionCookieName + '=', ''); 152 | if (localSession !== cookieSession) localStorage.setItem(sessionCookieName, cookieSession); 153 | } else if (localSession && localSession !== cookieSession) { 154 | document.cookie = sessionCookieName + '=' + localSession + '; path=/'; 155 | window.location.reload(true); 156 | } 157 | } 158 | }; 159 | 160 | // Get element by tag id 161 | // 162 | api.dom.id = function(id) { 163 | return document.getElementById(id); 164 | }; 165 | 166 | if (document.getElementsByClassName) { 167 | api.dom.getElementsByClass = function(classList, context) { 168 | return (context || document).getElementsByClassName(classList); 169 | }; 170 | } else { 171 | api.dom.getElementsByClass = function(classList, context) { 172 | context = context || document; 173 | var list = context.getElementsByTagName('*'), 174 | classArray = classList.split(/\s+/), 175 | result = [], i, j; 176 | for (i = 0; i < list.length; i++) { 177 | for(j = 0; j < classArray.length; j++) { 178 | if(list[i].className.search('\\b' + classArray[j] + '\\b') !== -1) { 179 | result.push(list[i]); 180 | break; 181 | } 182 | } 183 | } 184 | return result; 185 | }; 186 | } 187 | 188 | // Add element class 189 | // 190 | api.dom.addClass = function(element, className) { 191 | element = api.dom.element(element); 192 | if (!element) return false; 193 | if (element.classList) { 194 | return element.classList.add(className); 195 | } 196 | var regex = new RegExp('(^|\\s)' + className + '(\\s|$)', 'g'); 197 | if (regex.test(element.className)) { 198 | element.className = (element.className + ' ' + className).replace(/\s+/g, ' ').replace(/(^ | $)/g, ''); 199 | return element.className; 200 | } 201 | }; 202 | 203 | // Remove element class 204 | // 205 | api.dom.removeClass = function(element, className) { 206 | element = api.dom.element(element); 207 | if (!element) return false; 208 | if (element.classList) { 209 | return element.classList.remove(className); 210 | } 211 | var regex = new RegExp('(^|\\s)' + className + '(\\s|$)', 'g'); 212 | element.className = element.className.replace(regex, '$1').replace(/\s+/g, ' ').replace(/(^ | $)/g, ''); 213 | }; 214 | 215 | // Check element class 216 | // 217 | api.dom.hasClass = function(element, className) { 218 | element = api.dom.element(element); 219 | if (!element) return false; 220 | if (element.classList) { 221 | return element.classList.contains(className); 222 | } 223 | return element.className.match(new RegExp('(^|\b)' + className + '($|\b)')); 224 | }; 225 | 226 | // Toggle element class 227 | // 228 | api.dom.toggleClass = function(element, className) { 229 | element = api.dom.element(element); 230 | if (!element) return false; 231 | if (element.classList) { 232 | return element.classList.toggle(className); 233 | } 234 | element = api.dom.element(element); 235 | if (api.dom.hasClass(element, className)) api.dom.removeClass(element, className); 236 | else api.dom.addClass(element, className); 237 | }; 238 | 239 | // Insert element after 240 | // 241 | api.dom.insertAfter = function(parent, node, referenceNode) { 242 | parent.insertBefore(node, referenceNode.nextSibling); 243 | }; 244 | 245 | // Add element event 246 | // 247 | api.dom.addEvent = function(element, event, fn) { 248 | element = api.dom.element(element); 249 | if (!element) return false; 250 | if (element.addEventListener) { 251 | return element.addEventListener(event, fn, false); 252 | } else if (element.attachEvent) { 253 | var callback = function() { 254 | fn.call(element); 255 | }; 256 | return element.attachEvent('on' + event, callback); 257 | } else return false; 258 | }; 259 | 260 | // Remove element event 261 | // 262 | api.dom.removeEvent = function(element, event, fn) { 263 | if (arguments.length === 2) { 264 | fn = element; 265 | element = window; 266 | } 267 | element = api.dom.element(element); 268 | if (!element) return false; 269 | if (element.removeEventListener) { 270 | return element.removeEventListener(event, fn, false); 271 | } else if (element.detachEvent) { 272 | return element.detachEvent('on' + event, fn); 273 | } else return false; 274 | }; 275 | 276 | // Events: 'load', 'unload', 'click', etc. 277 | // 278 | api.dom.on = function(event, element, fn) { 279 | if (arguments.length === 2) { 280 | fn = element; 281 | element = window; 282 | } 283 | api.dom.addEvent(element, event, fn); 284 | }; 285 | 286 | // Use element or selector 287 | // 288 | api.dom.element = function(element) { 289 | if (typeof(element) !== 'string') { 290 | return element; 291 | } 292 | var result; 293 | try { 294 | //catching DOMException if element is not a valid selector 295 | result = document.querySelector(element); 296 | } catch (e) { 297 | result = null; 298 | } 299 | return result; 300 | }; 301 | 302 | // Get page body reference 303 | // 304 | api.dom.on('load', function() { 305 | api.dom.body = document.body || document.getElementsByTagName('body')[0]; 306 | }); 307 | 308 | // fn(event) should terurn not empty string for confirmation dialog 309 | // 310 | api.dom.onBeforeUnload = function(fn) { 311 | api.dom.addEvent(api.dom, 'beforeunload', function(event) { 312 | var message = fn(event); 313 | if (typeof(event) === 'undefined') event = window.event; 314 | if (event) event.returnValue = message; 315 | return message; 316 | }); 317 | }; 318 | 319 | // Fire event 320 | // 321 | api.dom.fireEvent = function(element, eventName) { 322 | if (element.fireEvent) element.fireEvent('on' + eventName); 323 | else { 324 | var event = document.createEvent('Events'); 325 | event.initEvent(eventName, true, false); 326 | element.dispatchEvent(event); 327 | } 328 | }; 329 | 330 | // Enable element 331 | // 332 | api.dom.enable = function(element, flag) { 333 | if (flag) api.dom.removeClass(element, 'disabled'); 334 | else api.dom.addClass(element, 'disabled'); 335 | }; 336 | 337 | // Visible element 338 | // 339 | api.dom.visible = function(element, flag) { 340 | if (flag) api.dom.show(element); 341 | else api.dom.hide(element); 342 | }; 343 | 344 | // Toggle element 345 | // 346 | api.dom.toggle = function(element) { 347 | if (api.dom.hasClass(element, 'hidden')) api.dom.show(element); 348 | else api.dom.hide(element); 349 | }; 350 | 351 | // Hide element 352 | // 353 | api.dom.hide = function(element) { 354 | if (!api.dom.hasClass(element, 'hidden')) { 355 | api.dom.addClass(element, 'hidden'); 356 | element.setAttribute('_display', element.style.display); 357 | element.style.display = 'none'; 358 | } 359 | }; 360 | 361 | // Show element 362 | // 363 | api.dom.show = function(element) { 364 | if (api.dom.hasClass(element, 'hidden')) { 365 | api.dom.removeClass(element, 'hidden'); 366 | element.style.display = element.getAttribute('_display') || ''; 367 | } 368 | }; 369 | 370 | // Load element content using AJAX 371 | // 372 | api.dom.load = function(url, element, callback) { 373 | element.innerHTML = '
'; 374 | api.ajax.get(url, {}, function(err, res) { 375 | element.innerHTML = res; 376 | if (callback) callback(err, res, element); 377 | }); 378 | }; 379 | 380 | // Center element 381 | // 382 | api.dom.alignCenter = function(element, context, styles) { 383 | var wrapper; 384 | var popupMargin = (element.style.margin.match(/\d+/) || [0])[0] || 0; 385 | 386 | if (api.dom.hasClass(element.parentNode, 'centering-wrapper')) { 387 | wrapper = element.parentNode; 388 | } else { 389 | wrapper = api.dom.wrapElement(element, 'centering-wrapper'); 390 | if (styles) api.dom.setStyles(wrapper, styles); 391 | if (context && context.appendChild) { 392 | context.appendChild(wrapper); 393 | } 394 | api.dom.setStyles(wrapper, { 395 | 'position': 'absolute', 396 | 'z-index': '10', 397 | 'text-align': 'center', //horizontal centering 398 | 'overflow': 'hidden' 399 | }); 400 | api.dom.setStyles(element, { 401 | 'display': 'inline-block', //text-like behaviour for centering by line-height and vertical-align 402 | 'box-sizing': 'border-box', //include padding to height/width 403 | 'text-align': 'initial', //rewrite wrapper's value 404 | 'line-height': 'normal', //rewrite wrapper's value 405 | 'vertical-align': 'middle' //vertical centering 406 | }); 407 | } 408 | api.dom.setStyles(wrapper, { 409 | 'height': window.innerHeight + 'px', 410 | 'width': window.innerWidth + 'px', 411 | 'line-height': window.innerHeight + 'px' //vertical centering 412 | }); 413 | api.dom.setStyles(element, { 414 | 'max-width': (wrapper.offsetWidth - popupMargin * 2) + 'px', 415 | 'max-height': (wrapper.offsetHeight - popupMargin * 2) + 'px' 416 | }); 417 | 418 | return wrapper; 419 | }; 420 | 421 | // Popup 422 | // 423 | api.dom.wrapElement = function(element, classname) { 424 | var wrapper = document.createElement('div'); 425 | if (classname) api.dom.addClass(wrapper, classname); 426 | wrapper.appendChild(element); 427 | return wrapper; 428 | }; 429 | 430 | api.dom.generateResizeHandler = function(wrapper, popup, popupMargin) { 431 | return function() { 432 | api.dom.setStyles(wrapper, { 433 | 'height': window.innerHeight + 'px', 434 | 'width': window.innerWidth + 'px', 435 | 'line-height': window.innerHeight + 'px' 436 | }); 437 | api.dom.setStyles(popup, { 438 | 'max-width': (wrapper.offsetWidth - popupMargin * 2) + 'px', 439 | 'max-height': (wrapper.offsetHeight - popupMargin * 2) + 'px' 440 | }); 441 | }; 442 | }; 443 | 444 | api.dom.generateClosePopup = function(wrapper, content, resizeHandler, prevPlaceRefs) { 445 | var closePopup = function(event) { 446 | if (event.target !== wrapper && event.target !== closePopup.closeElement) return true; 447 | api.dom.setStyles(wrapper, { 448 | 'opacity': '0' 449 | }); 450 | setTimeout(function() { 451 | if (prevPlaceRefs.previousParent) { 452 | prevPlaceRefs.previousParent.insertBefore( 453 | content.childNodes.item(0), 454 | prevPlaceRefs.previousSibling 455 | ); 456 | } 457 | api.dom.body.removeChild(wrapper); 458 | api.dom.body.style.overflow = api.dom.body.bodyPrevOverflow; 459 | }, 500); //wait 0.5s for animation end 460 | api.dom.removeEvent(wrapper, 'click', closePopup); 461 | api.dom.removeEvent('resize', resizeHandler); 462 | event.stopImmediatePropagation(); 463 | return false; 464 | }; 465 | return closePopup; 466 | }; 467 | 468 | function injectInnerContent(content, contentHolder) { 469 | var contentNode = api.dom.element(content), 470 | prevPlaceRefs; 471 | if (contentNode) { 472 | prevPlaceRefs = {}; 473 | prevPlaceRefs.previousParent = contentNode.parentNode; 474 | prevPlaceRefs.previousSibling = contentNode.nextElementSibling; 475 | contentHolder.appendChild(contentNode); 476 | } else if (typeof(content) === 'string') { 477 | contentHolder.innerHTML = content; 478 | } 479 | return prevPlaceRefs; 480 | } 481 | 482 | api.dom.popup = function(content) { 483 | var popupMargin = 10; 484 | var popupPadding = { 485 | x: api.dom.detectScrollbarWidth() || 20, 486 | y: 20, 487 | }; 488 | 489 | var popup = document.createElement('div'), 490 | contentHolder = document.createElement('div'); 491 | 492 | popup.appendChild(contentHolder); 493 | 494 | api.dom.setStyles(popup, { 495 | 'background': 'white', 496 | 'box-shadow': '0 0 15px #333', 497 | 'min-width': '300px', 498 | 'min-height': '100px', 499 | 'overflow': 'auto', 500 | 'margin': popupMargin + 'px', 501 | 'padding': popupPadding.y + 'px ' + popupPadding.x + 'px' 502 | }); 503 | var wrapper = api.dom.alignCenter(popup, api.dom.body, { 504 | 'transition': 'opacity 0.5s', 505 | 'background': 'rgba(0, 0, 0, 0.5)', 506 | 'opacity': '0' 507 | }); 508 | api.dom.setStyles(wrapper, { 509 | 'opacity': '1' 510 | }); 511 | api.dom.setStyles(contentHolder, { 512 | 'display': 'inline-block' 513 | }); 514 | api.dom.body.bodyPrevOverflow = api.dom.body.style.overflow; 515 | api.dom.setStyles(api.dom.body, { 516 | 'overflow': 'hidden' 517 | }); 518 | var prevPlaceRefs = injectInnerContent(content, contentHolder); 519 | var resizeHandler = api.dom.alignCenter.bind(null, popup); 520 | var closePopup = api.dom.generateClosePopup(wrapper, contentHolder, resizeHandler, prevPlaceRefs); 521 | api.dom.on('resize', resizeHandler); 522 | api.dom.on('click', wrapper, closePopup); 523 | return closePopup; 524 | }; 525 | 526 | api.dom.detectScrollbarWidth = function() { 527 | var scrollDiv = document.createElement("div"); 528 | api.dom.setStyles(scrollDiv, { 529 | 'width': '100px', 530 | 'height': '100px', 531 | 'overflow': 'scroll', 532 | 'position': 'absolute', 533 | 'top': '-9999px' 534 | }); 535 | api.dom.body.appendChild(scrollDiv); 536 | 537 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; 538 | document.body.removeChild(scrollDiv); 539 | 540 | return scrollbarWidth; 541 | }; 542 | 543 | function dashedToUpperCase(key) { 544 | return key.replace(/-(\w)/g, function(match, p1) { 545 | return p1.toUpperCase(); 546 | }); 547 | } 548 | 549 | //transform CSS string to Object 550 | // 551 | var cssStringToObject = function(styles) { 552 | if (typeof(styles) === 'string') { 553 | var stylesStr = styles; 554 | styles = {}; 555 | stylesStr.split(/\s*;\s*/).filter(Boolean).forEach(function(val) { 556 | //split by first ':' 557 | var delimPos = val.search(/\s*:\s*/); 558 | var delimLength = val.match(/\s*:\s*/)[0].length; 559 | var key = val.substr(0, delimPos); 560 | val = val.substr(delimPos + delimLength); 561 | styles[key] = val; //storing to object 562 | }); 563 | } 564 | return styles; 565 | }; 566 | 567 | function extractPrefixedStyles(styleName) { 568 | styleName = styleName || styleName; 569 | var keys = [styleName]; 570 | //adding vendor prefixes if needed 571 | for (var pref in api.dom.styleProps) { 572 | if (api.dom.styleProps[pref].indexOf(styleName) >= 0) { 573 | keys.push('-' + pref + '-' + styleName); 574 | } 575 | } 576 | return keys; 577 | } 578 | 579 | // Set given styles to element 580 | // 581 | api.dom.setStyles = function(element, styles) { 582 | styles = cssStringToObject(styles); 583 | if (typeof(styles) !== 'object') return false; 584 | 585 | for (var styleName in styles) { 586 | if (!styles[styleName]) break; 587 | var styleNames = extractPrefixedStyles(styleName); 588 | for (var dashedName in styleNames) { 589 | var key = dashedToUpperCase(styleNames[dashedName]); 590 | element.style[key] = styles[styleName]; 591 | } 592 | } 593 | return true; 594 | }; 595 | 596 | /* jshint ignore:start */ 597 | api.dom.styleProps = { //taken from Emmet lib - https://github.com/emmetio/emmet/blob/master/lib/resolver/css.js#L155 598 | 'webkit': 'animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-clip, background-composite, background-origin, background-size, border-fit, border-horizontal-spacing, border-image, border-vertical-spacing, box-align, box-direction, box-flex, box-flex-group, box-lines, box-ordinal-group, box-orient, box-pack, box-reflect, box-shadow, color-correction, column-break-after, column-break-before, column-break-inside, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-span, column-width, dashboard-region, font-smoothing, highlight, hyphenate-character, hyphenate-limit-after, hyphenate-limit-before, hyphens, line-box-contain, line-break, line-clamp, locale, margin-before-collapse, margin-after-collapse, marquee-direction, marquee-increment, marquee-repetition, marquee-style, mask-attachment, mask-box-image, mask-box-image-outset, mask-box-image-repeat, mask-box-image-slice, mask-box-image-source, mask-box-image-width, mask-clip, mask-composite, mask-image, mask-origin, mask-position, mask-repeat, mask-size, nbsp-mode, perspective, perspective-origin, rtl-ordering, text-combine, text-decorations-in-effect, text-emphasis-color, text-emphasis-position, text-emphasis-style, text-fill-color, text-orientation, text-security, text-stroke-color, text-stroke-width, transform, transition, transform-origin, transform-style, transition-delay, transition-duration, transition-property, transition-timing-function, user-drag, user-modify, user-select, writing-mode, svg-shadow, box-sizing, border-radius', 599 | 'moz': 'animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-inline-policy, binding, border-bottom-colors, border-image, border-left-colors, border-right-colors, border-top-colors, box-align, box-direction, box-flex, box-ordinal-group, box-orient, box-pack, box-shadow, box-sizing, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-width, float-edge, font-feature-settings, font-language-override, force-broken-image-icon, hyphens, image-region, orient, outline-radius-bottomleft, outline-radius-bottomright, outline-radius-topleft, outline-radius-topright, perspective, perspective-origin, stack-sizing, tab-size, text-blink, text-decoration-color, text-decoration-line, text-decoration-style, text-size-adjust, transform, transform-origin, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-focus, user-input, user-modify, user-select, window-shadow, background-clip, border-radius', 600 | 'ms': 'accelerator, backface-visibility, background-position-x, background-position-y, behavior, block-progression, box-align, box-direction, box-flex, box-line-progression, box-lines, box-ordinal-group, box-orient, box-pack, content-zoom-boundary, content-zoom-boundary-max, content-zoom-boundary-min, content-zoom-chaining, content-zoom-snap, content-zoom-snap-points, content-zoom-snap-type, content-zooming, filter, flow-from, flow-into, font-feature-settings, grid-column, grid-column-align, grid-column-span, grid-columns, grid-layer, grid-row, grid-row-align, grid-row-span, grid-rows, high-contrast-adjust, hyphenate-limit-chars, hyphenate-limit-lines, hyphenate-limit-zone, hyphens, ime-mode, interpolation-mode, layout-flow, layout-grid, layout-grid-char, layout-grid-line, layout-grid-mode, layout-grid-type, line-break, overflow-style, perspective, perspective-origin, perspective-origin-x, perspective-origin-y, scroll-boundary, scroll-boundary-bottom, scroll-boundary-left, scroll-boundary-right, scroll-boundary-top, scroll-chaining, scroll-rails, scroll-snap-points-x, scroll-snap-points-y, scroll-snap-type, scroll-snap-x, scroll-snap-y, scrollbar-arrow-color, scrollbar-base-color, scrollbar-darkshadow-color, scrollbar-face-color, scrollbar-highlight-color, scrollbar-shadow-color, scrollbar-track-color, text-align-last, text-autospace, text-justify, text-kashida-space, text-overflow, text-size-adjust, text-underline-position, touch-action, transform, transform-origin, transform-origin-x, transform-origin-y, transform-origin-z, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-select, word-break, wrap-flow, wrap-margin, wrap-through, writing-mode', 601 | 'o': 'dashboard-region, animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, border-image, link, link-source, object-fit, object-position, tab-size, table-baseline, transform, transform-origin, transition, transition-delay, transition-duration, transition-property, transition-timing-function, accesskey, input-format, input-required, marquee-dir, marquee-loop, marquee-speed, marquee-style' 602 | }; 603 | /* jshint ignore:end */ 604 | 605 | for (var i in api.dom.styleProps) { 606 | api.dom.styleProps[i] = api.dom.styleProps[i].split(/\s*,\s*/); 607 | } 608 | 609 | // Confirmation dialog 610 | // Buttons: ['Yes', 'No', 'Ok', 'Cancel'] 611 | // 612 | api.dom.confirmation = function(title, message, eventYes, buttons) { 613 | // TODO: implement api.dom.confirmation 614 | }; 615 | 616 | // Input dialog 617 | // 618 | api.dom.input = function(title, prompt, defaultValue, eventOk) { 619 | }; 620 | 621 | // Call disableSelection on page load with element to disable or without parameters to disable selection in whole page 622 | // 623 | api.dom.disableSelection = function(target) { 624 | target = target || api.dom.html; 625 | if (typeof(target.onselectstart) !== 'undefined') target.onselectstart = api.common.falseness; // For IE 626 | else if (typeof(target.style.MozUserSelect) !== 'undefined') { //For Firefox 627 | target.style.MozUserSelect='none'; 628 | // if (target === body || target === api.dom.html) 629 | // for (var i = 0; i < body.children.length; i++) 630 | // body.children[i].style.MozUserSelect='none'; 631 | } else target.onmousedown = api.common.falseness; // All other browsers (Opera) 632 | target.style.cursor = 'default'; 633 | }; 634 | 635 | // Disable browser context menu 636 | // 637 | api.dom.disableContextMenu = function(target) { 638 | target = target || api.dom.html; 639 | api.dom.addEvent(document, 'contextmenu', function(event) { 640 | event = event || window.event; 641 | if (document.addEventListener) event.preventDefault(); 642 | else event.returnValue = false; 643 | }); 644 | }; 645 | 646 | // Disable browser content copy function 647 | // 648 | api.dom.disableCopy = function(target) { 649 | target = target || api.dom.html; 650 | var fn = function(event) { 651 | event = event || window.event; 652 | if (api.dom.clipboardData) api.dom.clipboardData.setData('Text', ''); 653 | event.returnValue = false; 654 | if (event.preventDefault) event.preventDefault(); 655 | return false; 656 | }; 657 | api.dom.addEvent(target, 'copy', fn); 658 | 659 | /*api.dom.addEvent(target, 'keydown', function(event) { 660 | event = event || window.event; 661 | event.returnValue = false; 662 | var key = event.keyCode; 663 | var ctrlDown = event.ctrlKey || event.metaKey; // Mac support 664 | var result = true; 665 | 666 | console.log('key=' + key + ' ctrlDown=' + ctrlDown); 667 | // Check for Alt+Gr (http://en.wikipedia.org/wiki/AltGr_key) 668 | if (ctrlDown && event.altKey) result = true; 669 | else if (ctrlDown && key === 67) result = false // ctrl+c 670 | else if (ctrlDown && key === 86) result = false // ctrl+v 671 | else if (ctrlDown && key === 88) result = false; // ctrl+x 672 | 673 | event.returnValue = result; 674 | return result; 675 | });*/ 676 | }; 677 | 678 | // Escape HTML 679 | // 680 | api.dom.htmlEscape = function(content) { 681 | return content.replace(/[&<>"'\/]/g,function(char) { 682 | return ({ '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' }[char]); 683 | }); 684 | }; 685 | 686 | // Simple string template 687 | // 688 | api.dom.template = function(tpl, data, escapeHtml) { 689 | return tpl.replace(/@([\-\.0-9a-zA-Z]+)@/g, function(s, key) { 690 | return escapeHtml ? api.dom.htmlEscape(data[key]) : data[key]; 691 | }); 692 | }; 693 | 694 | // Simple HTML template 695 | // 696 | api.dom.templateHtml = function(tpl, data) { 697 | return api.dom.template(tpl, data, true); 698 | }; 699 | 700 | // Cookie utils 701 | // 702 | api.cookie = {}; 703 | 704 | // Get cookie value by name 705 | // 706 | api.cookie.get = function(name) { 707 | var matches = document.cookie.match(new RegExp( 708 | '(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)' 709 | )); 710 | return matches ? decodeURIComponent(matches[1]) : false; 711 | }; 712 | 713 | // Set cookie value 714 | // 715 | api.cookie.set = function(name, value) { 716 | var cookie = name + '=' + escape(value) + '; path=/'; 717 | document.cookie = cookie; 718 | }; 719 | 720 | // Delete cookie value 721 | // 722 | api.cookie.delete = function(name) { 723 | api.cookie.set(name, null, { expires: -1 }); 724 | }; 725 | 726 | // Tabs API 727 | // 728 | api.tabs = new api.events.EventEmitter(); 729 | api.tabs.tabId = 0; 730 | api.tabs.tabKey = ''; 731 | api.tabs.masterTab = false; 732 | api.tabs.masterTabId = 0; 733 | api.tabs.masterTabKey = ''; 734 | api.tabs.heartbeatInterval = 2000; 735 | api.tabs.heartbeatEvent = null; 736 | api.tabs.initialized = false; 737 | api.tabs.initializationCallbacks = []; 738 | api.tabs.supportsLocalStorage = false; 739 | 740 | // localStorage structure: 741 | // api.tabs.master = tabId e.g. 1 742 | // api.tabs.tab1 = Date.now() e.g. 1424185702490 743 | // api.tabs.tab2 = Date.now() e.g. 1424185704772 744 | // api.tabs.newtab = tabId (signal to master) 745 | // api.tabs.event = signal in format { name:s, data:d, time: Date.now() } 746 | // 747 | api.tabs.initializationWait = function(callback) { 748 | if (!api.tabs.initialized) api.tabs.initializationCallbacks.push(callback); 749 | else callback(); 750 | }; 751 | 752 | // Initialize tabs 753 | // 754 | api.tabs.initialize = function() { 755 | try { 756 | api.tabs.supportsLocalStorage = 'localStorage' in window && window.localStorage !== null; 757 | } catch(e) { 758 | } 759 | if (api.tabs.supportsLocalStorage) api.tabs.initializeConnection(); 760 | }; 761 | 762 | // Initialize tabs done 763 | // 764 | api.tabs.initializeDone = function() { 765 | api.tabs.heartbeatEvent = setInterval(api.tabs.listenHandler, api.tabs.heartbeatInterval); 766 | api.tabs.initialized = true; 767 | api.tabs.initializationCallbacks.forEach(function(callback) { 768 | callback(); 769 | }); 770 | api.tabs.initializationCallbacks = []; 771 | }; 772 | 773 | // Get free browser tab 774 | // 775 | api.tabs.getFreeTab = function() { 776 | for (var id = 1;;id++) { 777 | if (typeof(localStorage['impress.tab' + id]) === 'undefined') return id; 778 | } 779 | }; 780 | 781 | // Initialize tabs connection 782 | // 783 | api.tabs.initializeConnection = function() { 784 | if (!api.tabs.initialized) { 785 | api.tabs.tabId = api.tabs.getFreeTab(); 786 | api.tabs.tabKey = 'impress.tab' + api.tabs.tabId; 787 | api.tabs.heartbeat(); 788 | api.tabs.heartbeatEvent = setInterval(api.tabs.heartbeat, api.tabs.heartbeatInterval); 789 | localStorage['impress.newtab'] = api.tabs.tabId; 790 | global.addEventListener('storage', api.tabs.onStorageChange, false); 791 | } 792 | var master = localStorage['impress.master']; 793 | if (master) api.tabs.setMaster(master); 794 | else api.tabs.createMaster(); 795 | api.tabs.initializeDone(); 796 | }; 797 | 798 | // Master tab heartbeat 799 | // 800 | api.tabs.heartbeat = function() { 801 | localStorage[api.tabs.tabKey] = Date.now(); 802 | if (api.tabs.masterTab) api.tabs.checkTabs(); 803 | else api.tabs.checkMaster(); 804 | }; 805 | 806 | // Check master tab 807 | // 808 | api.tabs.checkMaster = function() { 809 | var masterNow = parseInt(localStorage[api.tabs.masterTabKey], 10); 810 | if (Date.now() - masterNow > api.tabs.heartbeatInterval * 2) { 811 | var tabId, tabNow, key, 812 | keys = Object.keys(localStorage), 813 | maxId = 0, 814 | now = Date.now(); 815 | for (var i = 0; i < keys.length; i++) { 816 | key = keys[i]; 817 | if (key.indexOf('impress.tab') === 0) { 818 | tabId = parseInt(key.match(/\d+/)[0], 10); 819 | tabNow = parseInt(localStorage[key], 10); 820 | if (now - tabNow < api.tabs.heartbeatInterval * 2 && tabId > maxId) maxId = tabId; 821 | } 822 | } 823 | if (maxId === api.tabs.tabId) api.tabs.createMaster(); 824 | } 825 | }; 826 | 827 | // Check browser babs 828 | // 829 | api.tabs.checkTabs = function() { 830 | var tabNow, key, keys = Object.keys(localStorage); 831 | for (var i = 0; i < keys.length; i++) { 832 | key = keys[i]; 833 | if (key !== api.tabs.tabKey && key.indexOf('impress.tab') === 0) { 834 | tabNow = parseInt(localStorage[key], 10); 835 | if (Date.now() - tabNow > api.tabs.heartbeatInterval * 2) { 836 | localStorage.removeItem(key); 837 | } 838 | } 839 | } 840 | }; 841 | 842 | // Set master tab 843 | // 844 | api.tabs.setMaster = function(id) { 845 | api.tabs.masterTab = false; 846 | api.tabs.masterTabId = id; 847 | api.tabs.masterTabKey = 'impress.tab' + id; 848 | }; 849 | 850 | // Create master tab 851 | // 852 | api.tabs.createMaster = function() { 853 | api.tabs.masterTab = true; 854 | api.tabs.masterTabId = api.tabs.tabId; 855 | api.tabs.masterTabKey = api.tabs.tabKey; 856 | localStorage['impress.master'] = api.tabs.tabId; 857 | api.tabs.initializeDone(); 858 | }; 859 | 860 | // Impress cross-tab communication using localstorage 861 | // 862 | api.tabs.onStorageChange = function(e) { 863 | if (e.key === 'impress.event') { 864 | var event = JSON.parse(e.newValue); 865 | api.tabs.emit(event.name, event.data); 866 | } else if (api.tabs.masterTab) { 867 | if (e.key === 'impress.newtab') api.tabs.heartbeat(); 868 | else if (e.key === 'impress.master') console.log('WARNING: master collision'); 869 | } else { 870 | if (e.key === 'impress.master') api.tabs.setMaster(e.newValue); 871 | } 872 | }; 873 | 874 | // Emit cross-tab event 875 | // 876 | api.tabs.emitTabs = function(name, data) { 877 | localStorage['impress.event'] = JSON.stringify({ name: name, data: data, time: Date.now() }); 878 | }; 879 | 880 | // Initialize tabs modile 881 | // 882 | api.tabs.initialize(); 883 | 884 | // Prepare AJAX namespace stub 885 | // 886 | api.ajax = function(methods) { // params: { method: { get/post:url }, ... } 887 | 888 | function createMethod(apiStub, apiMethod) { 889 | if (apiMethod === 'introspect') { 890 | apiStub[apiMethod] = function(params, callback) { 891 | apiStub.request(apiMethod, params, function(err, data) { 892 | apiStub.init(data); 893 | callback(err, data); 894 | }); 895 | }; 896 | } else { 897 | apiStub[apiMethod] = function(params, callback) { 898 | apiStub.request(apiMethod, params, callback); 899 | }; 900 | } 901 | } 902 | 903 | var apiStub = {}; 904 | 905 | apiStub.request = function(apiMethod, params, callback) { 906 | var err = null, requestParams = this.methods[apiMethod]; 907 | if (requestParams) { 908 | var httpMethod, url; 909 | if (requestParams.get) { httpMethod = 'GET'; url = requestParams.get; } 910 | if (requestParams.post) { httpMethod = 'POST'; url = requestParams.post; } 911 | if (httpMethod) { 912 | api.ajax.request(httpMethod, url, params, true, callback); 913 | return; 914 | } else err = new Error('DataSource error: HTTP method is not specified'); 915 | } else err = new Error('DataSource error: AJAX method is not specified'); 916 | callback(err, null); 917 | }; 918 | 919 | apiStub.init = function(methods) { 920 | apiStub.methods = methods; 921 | for (var apiMethod in apiStub.methods) createMethod(apiStub, apiMethod); 922 | }; 923 | 924 | apiStub.init(methods); 925 | return apiStub; 926 | 927 | }; 928 | 929 | // Data source abstract interface 930 | // 931 | // just abstract, see implementation below 932 | // should be implemented methods: 933 | // read(query, callback) return one record as object, callback(err, obj) 934 | // insert(obj, callback) insert one record, callback(err) on done 935 | // update(obj, callback) update one record, callback(err) on done 936 | // delete(query, callback) delete multiple records, callback(err) on done 937 | // may be implemented methods: 938 | // introspect(params, callback) populates dataSource.methods with introspection metadata returning from server 939 | // metadata(params, callback) populates dataSource.metadata with metadata from server 940 | // find(query, callback) return multiple records as Array, callback(err, Array) 941 | 942 | // AJAX data source interface 943 | // 944 | api.ajax.ajaxDataSource = function(methods) { 945 | var ds = api.ajax.ajax(methods); 946 | ds.read = function(query, callback) { 947 | ds.request('read', query, function(err, data) { 948 | // autocreate Record 949 | // callback(err, api.ajax.record({ data: data })); 950 | // 951 | callback(err, data); 952 | }); 953 | }; 954 | return ds; 955 | }; 956 | 957 | // Send HTTP request 958 | // method - HTTP verb (string) 959 | // url - request URL (string) 960 | // params - request parameters (hash, optional) 961 | // parseResponse - boolean flag to parse JSON (boolean, optional) 962 | // callback - function to call on response received 963 | // 964 | api.ajax.request = function(method, url, params, parseResponse, callback) { 965 | var key, data = [], value = '', 966 | req = new XMLHttpRequest(); 967 | req.open(method, url, true); 968 | for (key in params) { 969 | if (!params.hasOwnProperty(key)) continue; 970 | value = params[key]; 971 | if (typeof(value) !== 'string') value = JSON.stringify(value); 972 | data.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); 973 | } 974 | data = data.join('&'); 975 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 976 | req.onreadystatechange = function() { 977 | if (req.readyState === 4) { 978 | var err = null, res = req.responseText; 979 | if (req.status === 0 || req.status === 200) { 980 | if (parseResponse) { 981 | try { 982 | res = JSON.parse(res); 983 | } catch(e) { 984 | err = new Error('JSON parse code: ' + e); 985 | } 986 | } 987 | } else err = new Error('HTTP error code: ' + req.status); 988 | if (callback) callback(err, res); 989 | } 990 | }; 991 | try { 992 | req.send(data); 993 | } catch(e) { } 994 | }; 995 | 996 | // Send HTTP GET request 997 | // 998 | api.ajax.get = function(url, params, callback) { 999 | if (arguments.length === 2) { 1000 | callback = params; 1001 | params = {}; 1002 | } 1003 | api.ajax.request('GET', url, params, true, callback); 1004 | }; 1005 | 1006 | // Send HTTP POST request 1007 | // 1008 | api.ajax.post = function(url, params, callback) { 1009 | if (arguments.length === 2) { 1010 | callback = params; 1011 | params = {}; 1012 | } 1013 | api.ajax.request('POST', url, params, true, callback); 1014 | }; 1015 | 1016 | // JavaScript Transfer Protocol 1017 | // 1018 | api.jstp = {}; 1019 | 1020 | // Packet delimiter used to separate packets 1021 | // 1022 | api.jstp.PACKET_DELIMITER = ',{\f},'; 1023 | api.jstp.DELIMITER_LENGTH = api.jstp.PACKET_DELIMITER.length; 1024 | api.jstp.CHUNKS_FIRST = '['; 1025 | api.jstp.CHUNKS_LAST = ']'; 1026 | 1027 | // Chunks is an Array of String 1028 | // 1029 | api.jstp.Chunks = function() { 1030 | this.items = [api.jstp.CHUNKS_FIRST]; 1031 | }; 1032 | 1033 | // Chunks.add method adds new chunk to array and return 1034 | // packets (if any) as a string or null 1035 | // chunk - instance of String 1036 | // 1037 | api.jstp.Chunks.prototype.add = function(chunk) { 1038 | if (chunk.endsWith(api.jstp.PACKET_DELIMITER)) { 1039 | var chunks = this.items; 1040 | this.items = [api.jstp.CHUNKS_FIRST]; 1041 | chunks.push(chunk.slice(0, -api.jstp.DELIMITER_LENGTH)); 1042 | chunks.push(api.jstp.CHUNKS_LAST); 1043 | return api.jstp.parse(chunks.join('')); 1044 | } else { 1045 | this.items.push(chunk); 1046 | return null; 1047 | } 1048 | }; 1049 | 1050 | // Filter delimiters from from packets 1051 | // packets - array of object with expected delimiters 1052 | // returns - array of object without delemiters 1053 | // 1054 | api.jstp.removeDelimiters = function(packets) { 1055 | return packets.filter(function(packet, i) { 1056 | return (i % 2 === 0); 1057 | }); 1058 | }; 1059 | 1060 | // Packet constructor 1061 | // 1062 | api.jstp.Packet = function(kind, id, iface, verb, args) { 1063 | this[kind] = [id]; 1064 | if (iface) this[kind].push(iface); 1065 | this[verb] = args; 1066 | }; 1067 | 1068 | // Sandbox class used for parsing 1069 | // context - optional hash of properties to add to sandbox context 1070 | // 1071 | api.jstp.Sandbox = function(context) { 1072 | this.iframe = document.createElement('iframe'); 1073 | this.iframe.style.display = 'none'; 1074 | this.iframe.sandbox = 'allow-same-origin allow-scripts'; 1075 | 1076 | if (context) { 1077 | this.addProperties(context); 1078 | } 1079 | 1080 | document.body.appendChild(this.iframe); 1081 | }; 1082 | 1083 | // Add properties of an object to the sandbox context 1084 | // obj - a hash of properties 1085 | // 1086 | api.jstp.Sandbox.prototype.addProperties = function(obj) { 1087 | for (var key in obj) { 1088 | if (!obj.hasOwnProperty(key)) { 1089 | continue; 1090 | } 1091 | this.iframe.contentWindow[key] = obj[key]; 1092 | } 1093 | }; 1094 | 1095 | // Remove the sandbox from DOM 1096 | // 1097 | api.jstp.Sandbox.prototype.destroy = function() { 1098 | document.body.removeChild(this.iframe); 1099 | }; 1100 | 1101 | // Evaluate JavaScript code in the sandbox 1102 | // 1103 | api.jstp.Sandbox.prototype.eval = function(code) { 1104 | return this.iframe.contentWindow.eval(code); 1105 | }; 1106 | 1107 | // Deserialize string to object, just data: objects and arrays 1108 | // no expressions and functions allowed in object definition 1109 | // str - object serialized to string 1110 | // return - deserialized JavaScript object 1111 | // Example: api.jstp.parse("{ field: 'value', node: { a: [5,6,7] } }") 1112 | // 1113 | api.jstp.parse = function(str) { 1114 | var sandbox = new api.jstp.Sandbox(); 1115 | var result = sandbox.eval('(' + str + ')'); 1116 | sandbox.destroy(); 1117 | return result; 1118 | }; 1119 | 1120 | // Serializer factory 1121 | // additionalTypes - parsers for custom data types 1122 | // 1123 | api.jstp.createSerializer = function(additionalTypes) { 1124 | function serialize(obj, i, arr) { 1125 | var type; 1126 | if (obj instanceof Array) type = 'array'; 1127 | else if (obj instanceof Date) type = 'date'; 1128 | else if (obj === null) type = 'undefined'; 1129 | else type = typeof(obj); 1130 | var fn = serialize.types[type]; 1131 | return fn(obj, arr); 1132 | }; 1133 | 1134 | serialize.types = api.common.extend({ 1135 | number: function(n) { return n + ''; }, 1136 | string: function(s) { return '\'' + s.replace(/'/g, '\\\'') + '\''; }, 1137 | boolean: function(b) { return b ? 'true' : 'false'; }, 1138 | undefined: function(u, arr) { return !!arr ? '' : 'undefined'; }, 1139 | array: function(a) { 1140 | return '[' + a.map(serialize).join(',') + ']'; 1141 | }, 1142 | object: function(obj) { 1143 | var a = [], s, key; 1144 | for (key in obj) { 1145 | s = serialize(obj[key]); 1146 | if (s !== 'undefined') { 1147 | a.push(key + ':' + s); 1148 | } 1149 | } 1150 | return '{' + a.join(',') + '}'; 1151 | } 1152 | }, additionalTypes); 1153 | 1154 | return serialize; 1155 | }; 1156 | 1157 | // Serialize object to string, just data: objects and arrays 1158 | // no expressions and functions will be serialized 1159 | // obj - JavaScript object to be serialized 1160 | // return - object serialized to string 1161 | // Example: api.jstp.stringify({ field: 'value', node: { a: [5,6,7] } }) 1162 | // 1163 | api.jstp.stringify = api.jstp.createSerializer({ 1164 | function: function() { return 'undefined'; }, 1165 | date: function(d) { 1166 | return '\'' + d.toISOString().split('T')[0] + '\''; 1167 | } 1168 | }); 1169 | 1170 | // Serialize object to string. Allowed: objects, arrays, functions 1171 | // obj - JavaScript object to be serialized 1172 | // return - object serialized to string 1173 | // Example: api.jstp.dump({ field: 'value', func: () => {} }) 1174 | // 1175 | api.jstp.dump = api.jstp.createSerializer({ 1176 | function: function(fn) { 1177 | return fn.toString(); 1178 | }, 1179 | date: function(d) { 1180 | var date = d.toISOString().split('T')[0]; 1181 | return 'new Date(\'' + date + '\')'; 1182 | } 1183 | }); 1184 | 1185 | // Deserialize string to object with functions allowed in object definition 1186 | // str - object serialized to string 1187 | // return - deserialized JavaScript object 1188 | // 1189 | api.jstp.interprete = function(str) { 1190 | var sandbox = new api.jstp.Sandbox(); 1191 | var exported = sandbox.eval('(' + str + ')'); 1192 | sandbox.addProperties(exported); 1193 | return exported; 1194 | }; 1195 | 1196 | // Serialize object to string, data and functions 1197 | // functions will be serialized with source code 1198 | // obj - JavaScript object to be serialized 1199 | // return - object serialized to string 1200 | // Example: api.jstp.serialize([['a','b'],[5,7],'c',5]) 1201 | // 1202 | api.jstp.serialize = function(a, i, arr) { 1203 | // Try to implement better then api.jstp.stringify 1204 | if (a instanceof Array) { 1205 | return '[' + a.map(api.jstp.serialize).join(',') + ']'; 1206 | } else { 1207 | return a; // a may be number, boolean, string, etc. 1208 | // like in api.jstp.stringify 1209 | // also if a is { ... } we use '' 1210 | } 1211 | }; 1212 | 1213 | // Deserialize array of scalar or array of array 1214 | // no objects allowed, just arrays and values 1215 | // str - array serialized to string 1216 | // return - deserialized JavaScript array 1217 | // 1218 | api.jstp.deserialize = function(str) { 1219 | // Try to implement better then api.jstp.parse 1220 | }; 1221 | 1222 | // Connect to a JSTP endpoint and create persistent connection 1223 | // 1224 | api.jstp.connect = function(url) { 1225 | var socket = new WebSocket(api.common.absoluteUrl(url)); 1226 | var connection = new api.jstp.Connection(socket); 1227 | 1228 | socket.onopen = function() { 1229 | connection.emit('connect', connection); 1230 | }; 1231 | 1232 | return connection; 1233 | }; 1234 | 1235 | // JSTP Connection Class 1236 | // socket - instance of WebSocket connection 1237 | // 1238 | var Connection = function(socket) { 1239 | var connection = this; 1240 | api.events.EventEmitter.call(this); 1241 | 1242 | socket.connection = connection; 1243 | connection.socket = socket; 1244 | connection.cid = 0; 1245 | connection.packetId = 0; 1246 | connection.startTime = Date.now(); 1247 | connection.kind = 'client'; 1248 | connection.deltaId = 1; 1249 | connection.chunks = new api.jstp.Chunks(); 1250 | connection.callbacks = {}; 1251 | connection.interfaces = {}; 1252 | 1253 | socket.onmessage = function(message) { 1254 | var data = message.data; 1255 | var packets = connection.chunks.add(data); 1256 | if (packets) { 1257 | packets = api.jstp.removeDelimiters(packets); 1258 | connection.process(packets); 1259 | } 1260 | }; 1261 | 1262 | socket.onclose = function() { 1263 | connection.emit('close', connection); 1264 | }; 1265 | 1266 | socket.onerror = function(err) { 1267 | connection.emit('error', connection); 1268 | }; 1269 | 1270 | }; 1271 | 1272 | Object.setPrototypeOf(Connection.prototype, 1273 | api.events.EventEmitter.prototype); 1274 | api.jstp.Connection = Connection; 1275 | 1276 | // Process received packets 1277 | // packets - array of packet 1278 | // 1279 | Connection.prototype.process = function(packets) { 1280 | var cb, keys, kind, kindHandler, packet, packetId, 1281 | connection = this; 1282 | 1283 | function sendCallback() { 1284 | var error = arguments[0], 1285 | result = Array.prototype.slice.call(arguments, 1); 1286 | if (error && error instanceof RemoteError) { 1287 | error = error.jstpArray; 1288 | } else if (error && !Array.isArray(error)) { 1289 | error = [0, error.toString()]; 1290 | } 1291 | application.connection = null; 1292 | connection.callback(packetId, error, result); 1293 | } 1294 | 1295 | var kinds = { 1296 | 1297 | handshake: function () { 1298 | packetId = packet.handshake[0]; 1299 | if (packet.ok) { 1300 | cb = connection.callbacks[packetId]; 1301 | connection.emit('handshake', packet.ok, connection); 1302 | if (cb) { 1303 | delete connection.callbacks[packetId]; 1304 | cb(null, packet.ok); 1305 | } 1306 | } else if (packet.error) { 1307 | cb = connection.callbacks[packetId]; 1308 | if (cb) { 1309 | delete connection.callbacks[packetId]; 1310 | cb(new RemoteError(packet.error[0], packet.error[1])); 1311 | } 1312 | } 1313 | }, 1314 | 1315 | call: function () { 1316 | packetId = packet.call[0]; 1317 | var ifName = packet.call[1], 1318 | apiInterface = application.api[ifName], 1319 | methodName = keys[1], 1320 | args = packet[methodName]; 1321 | if (!apiInterface) { 1322 | connection.callback(packetId, RemoteError.INTERFACE_NOT_FOUND.jstpArray); 1323 | return; 1324 | } 1325 | var method = apiInterface[methodName]; 1326 | if (!method) { 1327 | connection.callback(packetId, RemoteError.METHOD_NOT_FOUND.jstpArray); 1328 | return; 1329 | } 1330 | application.connection = connection; 1331 | args.push(sendCallback); 1332 | method.apply(application, args); 1333 | }, 1334 | 1335 | callback: function () { 1336 | packetId = packet.callback[0]; 1337 | cb = connection.callbacks[packetId]; 1338 | if (cb) { 1339 | delete connection.callbacks[packetId]; 1340 | if (packet.ok) { 1341 | cb.apply(connection, [null].concat(packet.ok)); 1342 | } else if (packet.error) { 1343 | cb(new RemoteError(packet.error[0], packet.error[1])); 1344 | } 1345 | } 1346 | }, 1347 | 1348 | event: function () { 1349 | packetId = packet.event[0]; 1350 | var interfaceName = packet.event[1], 1351 | eventName = keys[1], 1352 | eventArgs = packet[eventName]; 1353 | connection.emit('event', interfaceName, eventName, eventArgs); 1354 | var interfaceProxy = connection.interfaces[interfaceName]; 1355 | if (interfaceProxy) { 1356 | interfaceProxy.emit(eventName, eventArgs, true); 1357 | } 1358 | }, 1359 | 1360 | inspect: function () { 1361 | packetId = packet.inspect[0]; 1362 | var ifName = packet.inspect[1], 1363 | iface = application.api[ifName]; 1364 | if (iface) { 1365 | connection.callback(packetId, null, Object.keys(iface)); 1366 | } else { 1367 | connection.callback(packetId, RemoteError.INTERFACE_NOT_FOUND.jstpArray); 1368 | } 1369 | } 1370 | 1371 | }; 1372 | 1373 | while (packets.length) { 1374 | packet = packets.shift(); 1375 | connection.emit('packet', packet, connection); 1376 | keys = Object.keys(packet); 1377 | kind = keys[0]; 1378 | kindHandler = kinds[kind]; 1379 | if (kindHandler) kindHandler(); 1380 | } 1381 | }; 1382 | 1383 | // Create packet for connection 1384 | // kind - packet classification: call, callback, event, state, stream, handshake, health 1385 | // iface - interface name, optional string 1386 | // verb - method name, string 1387 | // args - arguments 1388 | // 1389 | Connection.prototype.packet = function(kind, iface, verb, args) { 1390 | var packet = new api.jstp.Packet(kind, this.packetId, iface, verb, args); 1391 | this.packetId += this.deltaId; 1392 | return packet; 1393 | }; 1394 | 1395 | // Send data 1396 | // data - hash or object 1397 | // 1398 | Connection.prototype.send = function(data) { 1399 | var packet = api.jstp.stringify(data) + api.jstp.PACKET_DELIMITER; 1400 | this.socket.send(packet); 1401 | }; 1402 | 1403 | // Send data and close socket 1404 | // data - hash or object 1405 | // 1406 | Connection.prototype.end = function(data) { 1407 | var packet = api.jstp.stringify(data) + api.jstp.PACKET_DELIMITER; 1408 | this.socket.send(packet); 1409 | this.socket.close(); 1410 | }; 1411 | 1412 | // Send call packet 1413 | // interfaceName - interface containing required method 1414 | // methodName - method name to be called 1415 | // parameters - method call parameters 1416 | // callback - function 1417 | // 1418 | Connection.prototype.call = function(interfaceName, methodName, parameters, callback) { 1419 | var packet = this.packet('call', interfaceName, methodName, parameters), 1420 | packetId = packet.call[0]; 1421 | this.callbacks[packetId] = callback; 1422 | this.send(packet); 1423 | }; 1424 | 1425 | // Send callback packet 1426 | // packetId - id of original `call` packet 1427 | // result - return this tesult to callback function 1428 | // 1429 | Connection.prototype.callback = function(packetId, error, result) { 1430 | var packet; 1431 | if (error) { 1432 | packet = this.packet('callback', null, 'error', error); 1433 | } else { 1434 | packet = this.packet('callback', null, 'ok', result); 1435 | } 1436 | packet.callback[0] = packetId; 1437 | this.send(packet); 1438 | }; 1439 | 1440 | // Send event packet 1441 | // interfaceName - name of interface sending event to 1442 | // eventName - name of event 1443 | // parameters - hash or object, event parameters 1444 | // 1445 | Connection.prototype.event = function(interfaceName, eventName, parameters) { 1446 | var packet = this.packet('event', interfaceName, eventName, parameters); 1447 | this.send(packet); 1448 | }; 1449 | 1450 | // Send state packet 1451 | // path - path in data structure to be changed 1452 | // verb - operation with data inc, dec, let, delete, push, pop, shift, unshift 1453 | // value - delta or new value 1454 | // 1455 | Connection.prototype.state = function(path, verb, value) { 1456 | var packet = this.packet('state', path, verb, value); 1457 | this.send(packet); 1458 | }; 1459 | 1460 | // Send handshake packet 1461 | // appName - application name 1462 | // login - user login 1463 | // password - password hash 1464 | // callback - function callback 1465 | // 1466 | Connection.prototype.handshake = function(appName, login, password, callback) { 1467 | var packet = this.packet('handshake', appName, login, password), 1468 | packetId = packet.handshake[0]; 1469 | if (callback) this.callbacks[packetId] = callback; 1470 | this.send(packet); 1471 | }; 1472 | 1473 | // Send introspection request packet 1474 | // interfaceName - name of the interface to inspect 1475 | // callback - callback function proxy object is passed to 1476 | // 1477 | Connection.prototype.inspect = function(interfaceName, callback) { 1478 | var packet = this.packet('inspect', interfaceName, null, null), 1479 | packetId = packet.inspect[0], 1480 | connection = this; 1481 | 1482 | this.callbacks[packetId] = function(err) { 1483 | if (err) return callback(err); 1484 | var methods = Array.prototype.slice.call(arguments, 1); 1485 | 1486 | var proxy = new api.events.EventEmitter(), 1487 | clientEmit = proxy.emit; 1488 | proxy.emit = function(eventName, eventArgs, dontRetranslate) { 1489 | if (!dontRetranslate) { 1490 | connection.event(interfaceName, eventName, eventArgs); 1491 | } 1492 | clientEmit.call(proxy, eventName, eventArgs); 1493 | }; 1494 | 1495 | for (var i = 0; i < methods.length; i++) { 1496 | connection.wrapRemoteMethod(proxy, interfaceName, methods[i]); 1497 | } 1498 | connection.interfaces[interfaceName] = proxy; 1499 | callback(null, proxy); 1500 | }; 1501 | 1502 | this.send(packet); 1503 | }; 1504 | 1505 | // Wrap a remote method using the current connection 1506 | // and save into a proxy object 1507 | // proxy - the proxy object 1508 | // ifName - name of the interface 1509 | // methodName - name of the method 1510 | // 1511 | Connection.prototype.wrapRemoteMethod = function(proxy, ifName, methodName) { 1512 | var connection = this; 1513 | proxy[methodName] = function() { 1514 | var callback = arguments[arguments.length - 1]; 1515 | var args = Array.prototype.slice.call(arguments, 0, -1); 1516 | connection.call(ifName, methodName, args, callback); 1517 | }; 1518 | }; 1519 | 1520 | // JSTP remote error class 1521 | // TODO: implement RPC stacktrace 1522 | // code - error code 1523 | // message - optional error message 1524 | // 1525 | function RemoteError(code, message) { 1526 | message = message || RemoteError.defaultMessages[code]; 1527 | Error.call(this, message); 1528 | 1529 | this.code = code; 1530 | this.message = message; 1531 | 1532 | if (message) { 1533 | this.jstpArray = [code, message]; 1534 | } else { 1535 | this.message = code; 1536 | this.jstpArray = [code]; 1537 | } 1538 | 1539 | this.name = 'RemoteError'; 1540 | } 1541 | 1542 | Object.setPrototypeOf(RemoteError.prototype, Error.prototype); 1543 | api.jstp.RemoteError = RemoteError; 1544 | 1545 | // Default messages for predefined error codes 1546 | // (see JSTP specs at https://github.com/metarhia/JSTP) 1547 | // 1548 | RemoteError.defaultMessages = { 1549 | 10: 'Application not found', 1550 | 11: 'Authentication failed', 1551 | 12: 'Interface not found', 1552 | 13: 'Incompatible interface', 1553 | 14: 'Method not found' 1554 | }; 1555 | 1556 | RemoteError.APP_NOT_FOUND = new RemoteError(10); 1557 | RemoteError.AUTH_FAILED = new RemoteError(11); 1558 | RemoteError.INTERFACE_NOT_FOUND = new RemoteError(12); 1559 | RemoteError.INTERFACE_INCOMPATIBLE = new RemoteError(13); 1560 | RemoteError.METHOD_NOT_FOUND = new RemoteError(14); 1561 | 1562 | // Create websocket instance 1563 | // 1564 | api.ws = function(url) { 1565 | 1566 | var ws = new api.events.EventEmitter(), 1567 | socket = new WebSocket(api.common.absoluteUrl(url)); 1568 | 1569 | ws.socket = socket; 1570 | 1571 | socket.onopen = function() { 1572 | ws.emit('open'); 1573 | }; 1574 | 1575 | socket.onclose = function() { 1576 | ws.emit('close'); 1577 | }; 1578 | 1579 | socket.onmessage = function(event) { 1580 | ws.emit('message', event); 1581 | }; 1582 | 1583 | ws.close = function() { 1584 | socket.close(); 1585 | ws.socket = null; 1586 | }; 1587 | 1588 | ws.send = function(data) { 1589 | socket.send(data); 1590 | }; 1591 | 1592 | return ws; 1593 | 1594 | }; 1595 | 1596 | // Create Server-Sent Events instance 1597 | // 1598 | api.sse = function(url) { 1599 | var sse = new EventSource(url); 1600 | sse.on = sse.addEventListener; 1601 | return sse; 1602 | }; 1603 | 1604 | // Client-side load balancer 1605 | // 1606 | application.balancer = {}; 1607 | application.balancer.servers = {}; 1608 | application.balancer.sequence = []; 1609 | application.balancer.sequenceIndex = 0; 1610 | application.balancer.currentServer = null; 1611 | application.balancer.currentNode = null; 1612 | application.balancer.currentRetry = 0; 1613 | application.balancer.currentRetryMax = 10; 1614 | application.balancer.globalRetry = 0; 1615 | application.balancer.retryInterval = 3000; 1616 | 1617 | // Main Impress binding to server-side 1618 | // TODO: use JSTP here after get ip:port from balancer 1619 | // 1620 | /* 1621 | application.connect = function(callback) { 1622 | api.ajax.get('/api/application/balancer.json', {}, function(err, res) { 1623 | if (!err) { 1624 | application.balancer.servers = res.servers; 1625 | application.balancer.generateSequence(); 1626 | application.reconnect(); 1627 | if (callback) application.on('connect', callback); 1628 | } 1629 | }); 1630 | }; 1631 | 1632 | application.reconnect = function() { 1633 | var node = application.balancer.getNextNode(), 1634 | schema = node.server.secure ? 'wss' : 'ws', 1635 | path = '/examples/impress.rpc', 1636 | url = schema + '://' + node.host + path; 1637 | application.rpc = api.rpc(url); 1638 | application.rpc.on('open', function() { 1639 | application.connect.url = url; 1640 | console.log('opened ' + url); 1641 | }); 1642 | application.rpc.on('close', function() { 1643 | console.log('closed ' + url); 1644 | setTimeout(function() { 1645 | application.reconnect(); 1646 | }, application.balancer.retryInterval); 1647 | }); 1648 | }; 1649 | */ 1650 | 1651 | application.balancer.generateSequence = function() { 1652 | var i, server, serverName, 1653 | servers = application.balancer.servers; 1654 | if (servers) { 1655 | for (serverName in servers) { 1656 | server = servers[serverName]; 1657 | for (i = 0; i < server.ports.length; i++) { 1658 | application.balancer.sequence.push({ 1659 | server: server, 1660 | host: server.host + ':' + server.ports[i] 1661 | }); 1662 | } 1663 | } 1664 | } 1665 | }; 1666 | 1667 | application.balancer.getNextNode = function() { 1668 | var balancer = application.balancer; 1669 | balancer.globalRetry++; 1670 | if (balancer.currentRetry < balancer.currentRetryMax) { 1671 | // Next retry 1672 | balancer.currentRetry++; 1673 | } else { 1674 | // New node 1675 | balancer.currentRetry = 0; 1676 | if (balancer.sequenceIndex < balancer.sequence.length) { 1677 | // Next node 1678 | balancer.sequenceIndex++; 1679 | } else { 1680 | // First node 1681 | balancer.sequenceIndex = 0; 1682 | } 1683 | } 1684 | var node = balancer.sequence[balancer.sequenceIndex]; 1685 | balancer.currentNode = node; 1686 | balancer.currentServer = node.server; 1687 | return node; 1688 | }; 1689 | --------------------------------------------------------------------------------