├── 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 |
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 |
58 |
59 | | Call | Method | Modify time |
@[files]@| @.name@ | @.method@ | @.mtime@ |
@[/files]@
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/applications/messenger/templates/index.template:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | @title@: @path@
5 |
6 |
7 |
25 |
54 |
55 |
56 |
57 | @title@
58 |
59 |
60 | | Name | Size | Modify time |
@[files]@| @.name@ | @.size@ | @.mtime@ |
@[/files]@
61 |
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 |
--------------------------------------------------------------------------------