<commit_before>import request from 'request'
import {EventEmitter} from 'events'

const _apiUrl = 'https://graph.facebook.com/v2.6/'
const _notificationTypes = ['REGULAR', 'SILENT_PUSH', 'NO_PUSH']  // supported types by fb
const _notificationDefault = 'REGULAR'

/** Class representing a Facebook Messenger App. */
class Messenger extends EventEmitter {

  constructor (token = null, options = {}) {
    super()
    if (!token) throw new Error('Facebook Page access token is missing.')  // case bad token
    if (_notificationTypes.indexOf(options.notificationType) === -1) {  // case bad notification type
      options.notificationType = _notificationDefault
    }
    this.token = token
    this.notificationType = options.notificationType
    this.apiUrl = options.apiUrl || _apiUrl
  }

  subscribeApp (cb) {
    const req = {
      url: `${this.apiUrl}me/subscribed_apps`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: true
    }
    sendRequest(req, cb)
  }

  sendApiMessage (recipient, message, notificationType, cb) {
    if (typeof notificationType === 'function') {  // case 3rd arg is a func, no notification type was entered but a cb
      cb = notificationType
      notificationType = this.notificationType
    }
    if (!notificationType) {  // case no notification type is entered
      notificationType = this.notificationType
    }
    const req = {
      url: `${this.apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        message: message,
        notification_type: notificationType
      }
    }
    sendRequest(req, cb)
  }

  sendSenderAction (recipient, senderAction, cb) {
    const req = {
      url: `${this.apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        sender_action: senderAction
      }
    }
    sendRequest(req, cb)
  }

  setGreetingText (text, cb) {
    if (typeof text === 'string') {
      text = {text: text}
    }
    const method = 'POST'
    const params = {
      setting_type: 'greeting',
      greeting: text
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  setGetStartedButton (payload, cb) {
    if (typeof payload === 'string') {  // Case a string is entered
      payload = [{
        payload: payload
      }]
    }
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'new_thread',
      call_to_actions: payload // max 1
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  setPersistentMenu (menuItems, cb) {
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'existing_thread',
      call_to_actions: menuItems
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  deleteThreadSetting (threadType, cb) {
    const method = 'DELETE'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: threadType  // Get started button('new_thread') or Persistent menu('existing_thread')
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  sendThreadSettingsRequest (method, params, cb) {
    const req = {
      url: `${this.apiUrl}me/thread_settings`,
      qs: {
        access_token: this.token
      },
      method: method,
      json: params
    }
    sendRequest(req, cb)
  }

  sendAccountLinkingMessage (recipient, title, imageUrl, authUrl, cb) {
    const message = {
      attachment: {
        type: 'tempalte',
        payload: {
          template_type: 'generic',
          elements: [{
            title: title,
            image_url: imageUrl,
            buttons: [{
              type: 'account_link',
              url: authUrl
            }]
          }]
        }
      }
    }
    this.sendApiMessage(recipient, message, cb)
  }

  getUserPsid (token, cb) {
    const req = {
      url: `${this.apiUrl}me`,
      qs: {
        access_token: this.token,
        fields: 'recipient',
        account_linking_token: token
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }

  unlinkAccount (psid, cb) {
    const req = {
      url: `${this.apiUrl}me/unlink_accounts`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        psid: psid
      }
    }
    sendRequest(req, cb)
  }

  getUserProfile (userId, cb) {
    const req = {
      url: `${this.apiUrl}${userId}`,
      qs: {
        access_token: this.token,
        fields: 'first_name,last_name,profile_pic,locale,timezone,gender'
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }

  _handleCallback (res, data) {
    if (data.object !== 'page') { // check that it's a page subscription
      data.entry.forEach(entry => { // iterate over {array} entry
        console.log(`Callback received from page ${entry.id} at ${entry.time}`)
        entry.messaging.forEach(event => {
          switch (event) {  // switch by callback type
            case (event.message && event.message.is_echo):
              this._handleEvent('echoMessage', event)
              break
            case (event.message && event.message.quick_reply):
              this._handleEvent('quickReply', event)
              break
            case (event.message && event.message.text):
              this._handleEvent('message', event)
              break
            case (event.message && event.message.attachments):
              event.message.attachments.forEach(attachment => {
                this._handleEvent(attachment.type, event) // image, audio, video, file or location
              })
              break
            case (event.optin):
              this._handleEvent('authentication', event)
              break
            case (event.delivery):
              this._handleEvent('delivery', event)
              break
            case (event.postback):
              this._handleEvent('postback', event)
              break
            case (event.read):
              this._handleEvent('read', event)
              break
            case (event.account_linking && event.account_linking.status === 'linked'):
              this._handleEvent('accountLinked', event)
              break
            case (event.account_linking && event.account_linking.status === 'unlinked'):
              this._handleEvent('accountLinked', event)
              break
            default:
              console.error('Webhook received an unknown messaging event: ', JSON.stringify(event))
          }
        })
      })
      // Send back a 200, within 20 seconds, to let FB know that the bot has successfully received the callback.
      // Otherwise, the request will time out.
      res.sendStatus(200)
    } else {
      console.error(`Wrong callback object param: must be 'page' instead of ${data.object}`)
    }
  }

  _handleEvent (type, event) {
    this.emit(type, event, this.sendApiMessage.bind(this, event.sender.id))
  }
}

const sendRequest = (req, cb) => {
  request(req, (error, response, body) => {
    if (!cb) return
    if (error) return cb(error)
    if (response.body.error) return cb(response.body.error)
    cb(null, response.body)
  })
}

export default Messenger
<commit_msg>refactor(app.js): delete sendAccountLinkingMessage method<commit_after>import request from 'request'
import {EventEmitter} from 'events'

const _apiUrl = 'https://graph.facebook.com/v2.6/'
const _notificationTypes = ['REGULAR', 'SILENT_PUSH', 'NO_PUSH']  // supported types by fb
const _notificationDefault = 'REGULAR'

/** Class representing a Facebook Messenger App. */
class Messenger extends EventEmitter {

  constructor (token = null, options = {}) {
    super()
    if (!token) throw new Error('Facebook Page access token is missing.')  // case bad token
    if (_notificationTypes.indexOf(options.notificationType) === -1) {  // case bad notification type
      options.notificationType = _notificationDefault
    }
    this.token = token
    this.notificationType = options.notificationType
    this.apiUrl = options.apiUrl || _apiUrl
  }

  subscribeApp (cb) {
    const req = {
      url: `${this.apiUrl}me/subscribed_apps`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: true
    }
    sendRequest(req, cb)
  }

  sendApiMessage (recipient, message, notificationType, cb) {
    if (typeof notificationType === 'function') {  // case 3rd arg is a func, no notification type was entered but a cb
      cb = notificationType
      notificationType = this.notificationType
    }
    if (!notificationType) {  // case no notification type is entered
      notificationType = this.notificationType
    }
    const req = {
      url: `${this.apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        message: message,
        notification_type: notificationType
      }
    }
    sendRequest(req, cb)
  }

  sendSenderAction (recipient, senderAction, cb) {
    const req = {
      url: `${this.apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        sender_action: senderAction
      }
    }
    sendRequest(req, cb)
  }

  setGreetingText (text, cb) {
    if (typeof text === 'string') {
      text = {text: text}
    }
    const method = 'POST'
    const params = {
      setting_type: 'greeting',
      greeting: text
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  setGetStartedButton (payload, cb) {
    if (typeof payload === 'string') {  // Case a string is entered
      payload = [{
        payload: payload
      }]
    }
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'new_thread',
      call_to_actions: payload // max 1
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  setPersistentMenu (menuItems, cb) {
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'existing_thread',
      call_to_actions: menuItems
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  deleteThreadSetting (threadType, cb) {
    const method = 'DELETE'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: threadType  // Get started button('new_thread') or Persistent menu('existing_thread')
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  sendThreadSettingsRequest (method, params, cb) {
    const req = {
      url: `${this.apiUrl}me/thread_settings`,
      qs: {
        access_token: this.token
      },
      method: method,
      json: params
    }
    sendRequest(req, cb)
  }

  getUserPsid (token, cb) {
    const req = {
      url: `${this.apiUrl}me`,
      qs: {
        access_token: this.token,
        fields: 'recipient',
        account_linking_token: token
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }

  unlinkAccount (psid, cb) {
    const req = {
      url: `${this.apiUrl}me/unlink_accounts`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        psid: psid
      }
    }
    sendRequest(req, cb)
  }

  getUserProfile (userId, cb) {
    const req = {
      url: `${this.apiUrl}${userId}`,
      qs: {
        access_token: this.token,
        fields: 'first_name,last_name,profile_pic,locale,timezone,gender'
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }

  _handleCallback (res, data) {
    if (data.object !== 'page') { // check that it's a page subscription
      data.entry.forEach(entry => { // iterate over {array} entry
        console.log(`Callback received from page ${entry.id} at ${entry.time}`)
        entry.messaging.forEach(event => {
          switch (event) {  // switch by callback type
            case (event.message && event.message.is_echo):
              this._handleEvent('echoMessage', event)
              break
            case (event.message && event.message.quick_reply):
              this._handleEvent('quickReply', event)
              break
            case (event.message && event.message.text):
              this._handleEvent('message', event)
              break
            case (event.message && event.message.attachments):
              event.message.attachments.forEach(attachment => {
                this._handleEvent(attachment.type, event) // image, audio, video, file or location
              })
              break
            case (event.optin):
              this._handleEvent('authentication', event)
              break
            case (event.delivery):
              this._handleEvent('delivery', event)
              break
            case (event.postback):
              this._handleEvent('postback', event)
              break
            case (event.read):
              this._handleEvent('read', event)
              break
            case (event.account_linking && event.account_linking.status === 'linked'):
              this._handleEvent('accountLinked', event)
              break
            case (event.account_linking && event.account_linking.status === 'unlinked'):
              this._handleEvent('accountLinked', event)
              break
            default:
              console.error('Webhook received an unknown messaging event: ', JSON.stringify(event))
          }
        })
      })
      // Send back a 200, within 20 seconds, to let FB know that the bot has successfully received the callback.
      // Otherwise, the request will time out.
      res.sendStatus(200)
    } else {
      console.error(`Wrong callback object param: must be 'page' instead of ${data.object}`)
    }
  }

  _handleEvent (type, event) {
    this.emit(type, event, this.sendApiMessage.bind(this, event.sender.id))
  }
}

const sendRequest = (req, cb) => {
  request(req, (error, response, body) => {
    if (!cb) return
    if (error) return cb(error)
    if (response.body.error) return cb(response.body.error)
    cb(null, response.body)
  })
}

export default Messenger
<|endoftext|>


<commit_before>const express = require('express');
const app = express();
const lists = require('./routes/lists-route');
const items = require('./routes/items-route');

// logging:
const morgan = require('morgan');
app.use(morgan('dev'));

app.use('/lists', lists);

const path = require('path');
const publicPath = path.join(__dirname, '../public');
app.use(express.static(publicPath));

app.use('/lists', lists);
app.use('/items', items);

module.exports = app;<commit_msg>removed extra refernce to list route<commit_after>const express = require('express');
const app = express();
const lists = require('./routes/lists-route');
const items = require('./routes/items-route');

// logging:
const morgan = require('morgan');
app.use(morgan('dev'));

const path = require('path');
const publicPath = path.join(__dirname, '../public');
app.use(express.static(publicPath));

app.use('/lists', lists);
app.use('/items', items);

module.exports = app;<|endoftext|>


<commit_before>var express    = require('express'),
    flatten    = require('flatten').flatten,
    hogan      = require('hogan.js'),
    gobo       = require('gobo'),
    path       = require('path'),
    fs         = require('fs'),
    app        = express(),
    errors     = require('./errors'),
    options    = require('./config'),
    store      = require('./store')(options.store),
    models     = require('./models'),
    handlers   = require('./handlers'),
    middleware = require('./middleware'),
    binHandler = new handlers.BinHandler(),
    sessionHandler = new handlers.SessionHandler(),
    flattened;

app.store  = store;
app.models = models.createModels(store);
app.templates = {};

app.PRODUCTION  = 'production';
app.DEVELOPMENT = 'development';

// Apply the keys from the config file. All nested properties are
// space delimited to match the express style.
//
// For example, app.set('url prefix'); //=> '/'
flattened = flatten(options, ' ');
Object.getOwnPropertyNames(flattened).forEach(function (key) {
  app.set(key, flattened[key]);
});

app.set('root', path.resolve(path.join(__dirname, '..')));
app.set('version', require('../package').version);
app.set('url full', (options.url.ssl ? 'https://' : 'http://') + options.url.host + options.url.prefix);

// Register all the middleware.
app.use(express.logger());
app.use(express.static(path.join(app.set('root'), 'public')));
app.use(express.cookieParser(app.set('session secret')));
app.use(express.cookieSession({key: 'jsbin'}));
app.use(express.bodyParser());
app.use(express.csrf());
app.use(middleware.noslashes());
app.use(middleware.ajax());
app.use(middleware.cors());
app.use(middleware.jsonp());
app.use(middleware.helpers(app));

// Create a Hogan/Mustache handler for templates.
app.engine('html', function (path, options, fn) {
  fs.readFile(path, 'utf8', function (err, template) {
    if (err) {
      return fn(err);
    }

    try {
      var compiled = app.templates[path];
      if (!compiled) {
        compiled = app.templates[path] = hogan.compile(template);
      }

      fn(null, compiled.render(options));
    } catch (error) {
      fn(error);
    }
  });
});

// Configure the template engine.
app.set('view engine', 'html');
app.set('views', path.join(app.set('root'), 'views'));

// Define some generic template variables.
app.locals({
  root: app.set('url full'),
  version: app.set('version')
});

// Load the bin from the store when encountered in the url. Also handles the
// "latest" url action.
app.param('bin', binHandler.loadBin);

// Set up the routes.
app.get('/', binHandler.getDefault);

// Login/Create account.
app.post('/sethome', sessionHandler.loadUser, sessionHandler.loginUser, sessionHandler.createUser);
app.post('/logout', sessionHandler.logoutUser);

// List
app.get('/list', binHandler.getUserBins);

// Latest
app.get('/:bin/latest((.|\/):format)?', binHandler.redirectToLatest);
app.get('/:bin/latest/edit', binHandler.redirectToLatest);

// Edit
app.get('/:bin/:rev?/edit', binHandler.getBin);

// Save
app.post('/save', binHandler.createBin);
app.post('/:bin/:rev?/save', binHandler.createRevision);

// Source
app.get('/:bin/:rev?/source', binHandler.getBinSource);
app.get('/:bin/:rev?.:format(js|json|css|html)', binHandler.getBinSourceFile);
app.get('/:bin/:rev?/:format(js)', function (req, res) {
  // Redirect legacy /js suffix to the new .js extension.
  res.redirect(301, req.path.replace(/\/js$/, '.js'));
});

// Preview
app.get('/:bin/:quiet(quiet)?', binHandler.getBinPreview);
app.get('/:bin/:rev?/:quiet(quiet)?', binHandler.getBinPreview);

// Serve the concatenated JavaScript.
app.get('/js/debug/jsbin.js', function (req, res) {
  var js = path.join(app.set('root'), 'public', 'js');
  gobo(path.join(js, 'jsbin.js'), 'vendor', function (content) {
    res.contentType('js');
    res.send(content);
  });
});

// Error handler.
app.use(function (err, req, res, next) {
  if (err instanceof errors.NotFound) {
    return binHandler.notFound(req, res);
  }
  next(err);
});

// Final connect in error handler. Ideally should never hit this.
app.use(express.errorHandler({showStack: true, dumpExceptions: true}));

// Export the application to allow it to be included.
module.exports = app;
if (app.set('url prefix') !== '/') {
  // If we have a prefix then mount the app within another
  // express app to save us hacking around with the routes.
  module.exports = express().use(app.set('url prefix'), app);
}

// Run a local development server if this file is called directly.
if (require.main === module) {
  store.connect(function (err) {
    if (err) {
      throw err;
    }

    var port = 80;
    options.url.host.replace(/\:(\d+)$/, function (m, p) {
      if (p.length) {
        port = p;
      } else if (process.env.PORT) {
        port = process.env.PORT;
      }
    });

    process.stdout.write('Running jsbin on port ' + port + '\n');
    module.exports.listen(port);
  });
}
<commit_msg>Add download endpoint<commit_after>var express    = require('express'),
    flatten    = require('flatten').flatten,
    hogan      = require('hogan.js'),
    gobo       = require('gobo'),
    path       = require('path'),
    fs         = require('fs'),
    app        = express(),
    errors     = require('./errors'),
    options    = require('./config'),
    store      = require('./store')(options.store),
    models     = require('./models'),
    handlers   = require('./handlers'),
    middleware = require('./middleware'),
    binHandler = new handlers.BinHandler(),
    sessionHandler = new handlers.SessionHandler(),
    flattened;

app.store  = store;
app.models = models.createModels(store);
app.templates = {};

app.PRODUCTION  = 'production';
app.DEVELOPMENT = 'development';

// Apply the keys from the config file. All nested properties are
// space delimited to match the express style.
//
// For example, app.set('url prefix'); //=> '/'
flattened = flatten(options, ' ');
Object.getOwnPropertyNames(flattened).forEach(function (key) {
  app.set(key, flattened[key]);
});

app.set('root', path.resolve(path.join(__dirname, '..')));
app.set('version', require('../package').version);
app.set('url full', (options.url.ssl ? 'https://' : 'http://') + options.url.host + options.url.prefix);

// Register all the middleware.
app.use(express.logger());
app.use(express.static(path.join(app.set('root'), 'public')));
app.use(express.cookieParser(app.set('session secret')));
app.use(express.cookieSession({key: 'jsbin'}));
app.use(express.bodyParser());
app.use(express.csrf());
app.use(middleware.noslashes());
app.use(middleware.ajax());
app.use(middleware.cors());
app.use(middleware.jsonp());
app.use(middleware.helpers(app));

// Create a Hogan/Mustache handler for templates.
app.engine('html', function (path, options, fn) {
  fs.readFile(path, 'utf8', function (err, template) {
    if (err) {
      return fn(err);
    }

    try {
      var compiled = app.templates[path];
      if (!compiled) {
        compiled = app.templates[path] = hogan.compile(template);
      }

      fn(null, compiled.render(options));
    } catch (error) {
      fn(error);
    }
  });
});

// Configure the template engine.
app.set('view engine', 'html');
app.set('views', path.join(app.set('root'), 'views'));

// Define some generic template variables.
app.locals({
  root: app.set('url full'),
  version: app.set('version')
});

// Load the bin from the store when encountered in the url. Also handles the
// "latest" url action.
app.param('bin', binHandler.loadBin);

// Set up the routes.
app.get('/', binHandler.getDefault);

// Login/Create account.
app.post('/sethome', sessionHandler.loadUser, sessionHandler.loginUser, sessionHandler.createUser);
app.post('/logout', sessionHandler.logoutUser);

// List
app.get('/list', binHandler.getUserBins);

// Latest
app.get('/:bin/latest((.|\/):format)?', binHandler.redirectToLatest);
app.get('/:bin/latest/edit', binHandler.redirectToLatest);

// Edit
app.get('/:bin/:rev?/edit', binHandler.getBin);

// Save
app.post('/save', binHandler.createBin);
app.post('/:bin/:rev?/save', binHandler.createRevision);

// Download
app.post('/download', binHandler.downloadBin);

// Source
app.get('/:bin/:rev?/source', binHandler.getBinSource);
app.get('/:bin/:rev?.:format(js|json|css|html)', binHandler.getBinSourceFile);
app.get('/:bin/:rev?/:format(js)', function (req, res) {
  // Redirect legacy /js suffix to the new .js extension.
  res.redirect(301, req.path.replace(/\/js$/, '.js'));
});

// Preview
app.get('/:bin/:quiet(quiet)?', binHandler.getBinPreview);
app.get('/:bin/:rev?/:quiet(quiet)?', binHandler.getBinPreview);

// Serve the concatenated JavaScript.
app.get('/js/debug/jsbin.js', function (req, res) {
  var js = path.join(app.set('root'), 'public', 'js');
  gobo(path.join(js, 'jsbin.js'), 'vendor', function (content) {
    res.contentType('js');
    res.send(content);
  });
});

// Error handler.
app.use(function (err, req, res, next) {
  if (err instanceof errors.NotFound) {
    return binHandler.notFound(req, res);
  }
  next(err);
});

// Final connect in error handler. Ideally should never hit this.
app.use(express.errorHandler({showStack: true, dumpExceptions: true}));

// Export the application to allow it to be included.
module.exports = app;
if (app.set('url prefix') !== '/') {
  // If we have a prefix then mount the app within another
  // express app to save us hacking around with the routes.
  module.exports = express().use(app.set('url prefix'), app);
}

// Run a local development server if this file is called directly.
if (require.main === module) {
  store.connect(function (err) {
    if (err) {
      throw err;
    }

    var port = 80;
    options.url.host.replace(/\:(\d+)$/, function (m, p) {
      if (p.length) {
        port = p;
      } else if (process.env.PORT) {
        port = process.env.PORT;
      }
    });

    process.stdout.write('Running jsbin on port ' + port + '\n');
    module.exports.listen(port);
  });
}
<|endoftext|>


<commit_before>/**
 * lib/app.js: main module for `bonta` CLI utility
 *
 * Copyright 2015, Sudaraka Wijesinghe <sudaraka@sudaraka.org>
 *
 * This program comes with ABSOLUTELY NO WARRANTY;
 * This is free software, and you are welcome to redistribute it and/or modify
 * it under the terms of the BSD 2-clause License. See the LICENSE file for more
 * details.
 */

(() => {
  'use strict';

  const
    cluster = require('cluster'),
    path = require('path'),
    growl = require('growl'),

    INTERVAL = 5;  // In seconds

  module.exports = () => {

    if(cluster.isMaster) {
      cluster.fork();

      cluster.on('exit', () => {
        setTimeout(() => {
          cluster.fork();
        }, INTERVAL * 1000);
      });
    }
    else {
      growl(`It's time to Have a Drink!!!`, {
        'title': `Another ${INTERVAL} seconds have passed.`,
        'image': path.join(__dirname, '../assets/images/bottle.png'),
        'sticky': false
      });

      cluster.worker.kill();
    }

  };

})();
<commit_msg>app.js: string literals FTW<commit_after>/**
 * lib/app.js: main module for `bonta` CLI utility
 *
 * Copyright 2015, Sudaraka Wijesinghe <sudaraka@sudaraka.org>
 *
 * This program comes with ABSOLUTELY NO WARRANTY;
 * This is free software, and you are welcome to redistribute it and/or modify
 * it under the terms of the BSD 2-clause License. See the LICENSE file for more
 * details.
 */

(() => {
  'use strict';

  const
    cluster = require(`cluster`),
    path = require(`path`),
    growl = require(`growl`),

    INTERVAL = 5;  // In seconds

  module.exports = () => {

    if(cluster.isMaster) {
      cluster.fork();

      cluster.on(`exit`, () => {
        setTimeout(() => {
          cluster.fork();
        }, INTERVAL * 1000);
      });
    }
    else {
      growl(`It's time to Have a Drink!!!`, {
        'title': `Another ${INTERVAL} seconds have passed.`,
        'image': path.join(__dirname, `../assets/images/bottle.png`),
        'sticky': false
      });

      cluster.worker.kill();
    }

  };

})();
<|endoftext|>


<commit_before>/*global
 $,
 jQuery,
 error,
 success,
 endless,
 ajax,
 templateHelper,
 Routing,
 appLocale,
 exports,
 module,
 require,
 define
 */
/*jslint nomen: true */                 /* Tolerate dangling _ in identifiers */
/*jslint vars: true */           /* Tolerate many var statements per function */
/*jslint white: true */                       /* tolerate messy whithe spaces */
/*jslint browser: true */
/*jslint devel: true */                         /* Assume console, alert, ... */
/*jslint windows: true */               /* Assume window object (for browsers)*/

/*******************************************************************************
 * Class dependencies:                                                         *
 *      jquery > 1.8.3                                                         *
 *      isotope                                                                *
 *      jsrender                                                               *
 *      jsviews                                                                *
 * external dependencies:                                                      *
 *      templateHelper                                                         *
 *      base genericAction                                                     *
 *      ExposeTranslation                                                      *
 *      FOS Routing                                                            *
 ******************************************************************************/

$(document).ready(function () {
    "use strict";
    var pluginName = "fwHomeGallery";
    var defaults = {
        videoCategory: null,
        videoFeed: Routing.generate(appLocale + '_home_ajaxfilter'),
        imteStyle: {
            'width': '16%',
            'height': '160px',
            'margin-top': '5px',
            'margin-bottom': '5px',
            'border': '1px solid #333',
            'border-radius': '4px',
            'overflow': 'hidden'
        },
        itemSelector: '.video',
        feedSource: '',
        feedfilter: {}
    };
    function Plugin(element, options) {
        this.element = element;
        this.options = $.extend({}, defaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        this.init();
    }
    Plugin.prototype = {
        init: function () {
            var that = this;
            var self = $(that.element);
            self.bind("destroyed", $.proxy(that.teardown, that));
            self.addClass(that._name);

            return true;
        },
        makePackery: function(videoCategory) {
            var that = this;
            var i = 0;
            var cnt = 0;
            var container = document.querySelector('section.highlights');
            var packery = new Packery(container, {
                itemSelector: '.video',
                gutter: ".gutter-sizer",
                columnWidth: ".grid-sizer"
            });

            var feed = Routing.generate(appLocale + '_home_ajaxfilter');

            $.ajax({url: that.options.videoFeed, data: {'vc': videoCategory}}).then(function(response){
                for(i in response.highlighted) {
                    if (response.highlighted.hasOwnProperty(i)) {
                        var video = response.highlighted[i];
                        $.when(templateHelper.htmlTemplate('video-home_element', video))
                        .then(function(response){
                            var $thumb = $(response).clone();
                            $thumb.addClass('video');
                            if(cnt === 1) {
                                $thumb.addClass('double');
                            }
                            cnt += 1;

                            $('section.highlights').append($thumb);
                            packery.appended($thumb);
                            packery.layout();
                        });
                    }
                }
            });
        },
        destroy: function() {
            var that = this;
            $(that.element).unbind("destroyed", that.teardown);
            that.teardown();
            return true;
        },
        teardown: function() {
            var that = this;
            $.removeData($(that.element)[0], that._name);
            $(that.element).removeClass(that._name);
            that.unbind();
            that.element = null;
            return that.element;
        },
        bind: function() { },
        unbind: function() { }
    };
    $.fn[pluginName] = function (options) {
        return this.each(function () {
            if (!$.data(this, pluginName)) {
                $.data(this, pluginName, new Plugin(this, options));
            }
        });
    };
});

//Attach plugin to all matching element
$(document).ready(function () {
    "use strict";
    //$('section.highlights').fwHomeGallery({});
});

$(document).ready(function () {

    var $container = $('section.highlights');

    function appendFollowed(videoCategory) {
        var i = 0;
        var cnt = 0;
        var $container = $('section.followed > .videos-container');
        $container.empty();
        var feed = Routing.generate(appLocale + '_home_ajaxfilter');
        $.ajax({url: feed, data: {'vc': videoCategory}}).then(function(response){
            for(i in response.followed) {
                if (response.followed.hasOwnProperty(i)) {
                    var video = response.followed[i];
                    $thumb = $('

');
                    $.when(templateHelper.htmlTemplate('video-home_element', video))
                        .then(function(response){
                            $thumb = $(response).clone();
                            $container.append($thumb);
                        });
                }
            }
        });
    }

    function appendPopular(videoCategory) {
        var i = 0;
        var cnt = 0;
        var $container = $('section.popular > .videos-container');
        $container.empty();
        var feed = Routing.generate(appLocale + '_home_ajaxfilter');
        $.ajax({url: feed, data: {'vc': videoCategory}}).then(function(response){
            for(i in response.popular) {
                if (response.popular.hasOwnProperty(i)) {
                    var video = response.popular[i];
                    //console.log(video);
                    $thumb = $('

');
                    $.when(templateHelper.htmlTemplate('video-home_element', video))
                        .then(function(response){
                            $thumb = $(response).clone();
                            $container.append($thumb);;
                    });
                }
            }
        });
    }

    function makePackery(videoCategory) {
        var i = 0;
        var cnt = 0;
        var container = document.querySelector('section.highlights');
        var packery = new Packery( container, {
            itemSelector: '.video',
//            gutter: '25px',
            gutter: ".gutter-sizer",
            columnWidth: ".grid-sizer"
//            columnWidth: container.querySelector('.grid-sizer')
        });

        window.packery = packery;
        // ajax.genericAction('home_ajaxfilter', {paginate:{'page':1, 'block':'popular','vc': 6}}, function(r){console.log(r);});
        // $.ajax({url: Routing.generate('es_home_ajaxfilter'), data: {'vc': 1}}).then(function(r){console.log(r)})
        var feed = Routing.generate(appLocale + '_home_ajaxfilter');
        $.ajax({url: feed, data: {'vc': videoCategory}}).then(function(response){
            for(i in response.highlighted) {
                if (response.highlighted.hasOwnProperty(i)) {
                    var video = response.highlighted[i];

                    var thumb = document.createElement('article');
                    thumb.classList.add('video');

                    var image = document.createElement('img');

                    $thumb = $('

');

                    $.when(templateHelper.htmlTemplate('video-home_element', video))
                    .then(function(response){
                        $thumb = $(response).clone();

                        $thumb.addClass('video');
                        if(cnt === 1) {
                            $thumb.addClass('double');
                        }
                        cnt += 1;

                        $('section.highlights').append($thumb);
                        packery.appended($thumb);
                        packery.layout();
                    });
                }
            }
        });
    }

    var videoCategory = $('.filter-home').find('.active').attr('data-category-id');
    makePackery(videoCategory);
    appendFollowed(videoCategory);
    appendPopular(videoCategory);
});<commit_msg>con creador de tags<commit_after>/*global
 $,
 jQuery,
 error,
 success,
 endless,
 ajax,
 templateHelper,
 Routing,
 appLocale,
 exports,
 module,
 require,
 define
 */
/*jslint nomen: true */                 /* Tolerate dangling _ in identifiers */
/*jslint vars: true */           /* Tolerate many var statements per function */
/*jslint white: true */                       /* tolerate messy whithe spaces */
/*jslint browser: true */
/*jslint devel: true */                         /* Assume console, alert, ... */
/*jslint windows: true */               /* Assume window object (for browsers)*/

/*******************************************************************************
 * Class dependencies:                                                         *
 *      jquery > 1.8.3                                                         *
 *      isotope                                                                *
 *      jsrender                                                               *
 *      jsviews                                                                *
 * external dependencies:                                                      *
 *      templateHelper                                                         *
 *      base genericAction                                                     *
 *      ExposeTranslation                                                      *
 *      FOS Routing                                                            *
 ******************************************************************************/

$(document).ready(function () {
    "use strict";
    var pluginName = "fwHomeGallery";
    var defaults = {
        videoCategory: null,
        videoFeed: Routing.generate(appLocale + '_home_ajaxfilter'),
        imteStyle: {
            'width': '16%',
            'height': '160px',
            'margin-top': '5px',
            'margin-bottom': '5px',
            'border': '1px solid #333',
            'border-radius': '4px',
            'overflow': 'hidden'
        },
        itemSelector: '.video',
        feedSource: '',
        feedfilter: {}
    };
    function Plugin(element, options) {
        this.element = element;
        this.options = $.extend({}, defaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        this.init();
    }
    Plugin.prototype = {
        init: function () {
            var that = this;
            var self = $(that.element);
            self.bind("destroyed", $.proxy(that.teardown, that));
            self.addClass(that._name);

            return true;
        },
        makePackery: function(videoCategory) {
            var that = this;
            var i = 0;
            var cnt = 0;
            var container = document.querySelector('section.highlights');
            var packery = new Packery(container, {
                itemSelector: '.video',
                gutter: ".gutter-sizer",
                columnWidth: ".grid-sizer"
            });

            var feed = Routing.generate(appLocale + '_home_ajaxfilter');

            $.ajax({url: that.options.videoFeed, data: {'vc': videoCategory}}).then(function(response){
                for(i in response.highlighted) {
                    if (response.highlighted.hasOwnProperty(i)) {
                        var video = response.highlighted[i];
                        $.when(templateHelper.htmlTemplate('video-home_element', video))
                        .then(function(response){
                            var $thumb = $(response).clone();
                            $thumb.addClass('video');
                            if(cnt === 1) {
                                $thumb.addClass('double');
                            }
                            cnt += 1;

                            $('section.highlights').append($thumb);
                            packery.appended($thumb);
                            packery.layout();
                        });
                    }
                }
            });
        },
        destroy: function() {
            var that = this;
            $(that.element).unbind("destroyed", that.teardown);
            that.teardown();
            return true;
        },
        teardown: function() {
            var that = this;
            $.removeData($(that.element)[0], that._name);
            $(that.element).removeClass(that._name);
            that.unbind();
            that.element = null;
            return that.element;
        },
        bind: function() { },
        unbind: function() { }
    };
    $.fn[pluginName] = function (options) {
        return this.each(function () {
            if (!$.data(this, pluginName)) {
                $.data(this, pluginName, new Plugin(this, options));
            }
        });
    };
});

//Attach plugin to all matching element
$(document).ready(function () {
    "use strict";
    //$('section.highlights').fwHomeGallery({});
});

$(document).ready(function () {

    var $container = $('section.highlights');

    function appendFollowed(videoCategory) {
        var i = 0;
        var cnt = 0;
        var $container = $('section.followed > .videos-container');
        $container.empty();
        var feed = Routing.generate(appLocale + '_home_ajaxfilter');
        $.ajax({url: feed, data: {'vc': videoCategory}}).then(function(response){
            for(i in response.followed) {
                if (response.followed.hasOwnProperty(i)) {
                    var video = response.followed[i];
                    $thumb = $('

');
                    $.when(templateHelper.htmlTemplate('video-home_element', video))
                        .then(function(response){
                            $thumb = $(response).clone();
                            $container.append($thumb);
                        });
                }
            }
        });
    }

    function appendPopular(videoCategory) {
        var i = 0;
        var cnt = 0;
        var $container = $('section.popular > .videos-container');
        $container.empty();
        var feed = Routing.generate(appLocale + '_home_ajaxfilter');
        $.ajax({url: feed, data: {'vc': videoCategory}}).then(function(response){
            for(i in response.popular) {
                if (response.popular.hasOwnProperty(i)) {
                    var video = response.popular[i];
                    //console.log(video);
                    $thumb = $('

');
                    $.when(templateHelper.htmlTemplate('video-home_element', video))
                        .then(function(response){
                            $thumb = $(response).clone();
                            $container.append($thumb);;
                    });
                }
            }
        });
    }

    function makePackery(videoCategory) {
        var i = 0;
        var cnt = 0;
        var container = document.querySelector('section.highlights');
        var packery = new Packery( container, {
            itemSelector: '.video',
//            gutter: '25px',
            gutter: ".gutter-sizer",
            columnWidth: ".grid-sizer"
//            columnWidth: container.querySelector('.grid-sizer')
        });

        window.packery = packery;
        // ajax.genericAction('home_ajaxfilter', {paginate:{'page':1, 'block':'popular','vc': 6}}, function(r){console.log(r);});
        // $.ajax({url: Routing.generate('es_home_ajaxfilter'), data: {'vc': 1}}).then(function(r){console.log(r)})
        var feed = Routing.generate(appLocale + '_home_ajaxfilter');
        $.ajax({url: feed, data: {'vc': videoCategory}}).then(function(response){
            for(i in response.highlighted) {
                if (response.highlighted.hasOwnProperty(i)) {
                    var video = response.highlighted[i];

                    var thumb = document.createElement('article');
                    thumb.classList.add('video');

                    var image = document.createElement('img');

                    $thumb = $('

');

                    $.when(templateHelper.htmlTemplate('video-home_element', video))
                    .then(function(response){
                        $thumb = $(response).clone();

                        $thumb.addClass('video');
                        if(cnt === 1) {
                            $thumb.addClass('double');
                        }
                        cnt += 1;

                        $('section.highlights').append($thumb);
                        packery.appended($thumb);
                        packery.layout();
                    });
                }
            }
        });
    }

    function makeTags(channel, filter, page) {
        $.ajax({
            url: Routing.generate(appLocale + '_tag_ajaxgetusedinvideos'),
            data: {
                channel: channel,
                filter: filter,
                page: page
            }
        }).then(function(response){
            var i = 0;
            var tags = response.tags;
            var container = $('section.'+filter+'-tags > ul'); //$('section.followed-tags > ul');
            $(container).empty();
            for(i in tags){
                if (tags.hasOwnProperty(i)) {
                    $(container).append("
"+tags[i].title+"
");
                    console.log("adding tag number: " + i)
                    if(i > 5) {
                        break;
                    }
                }
            }
        })
    }

    var videoCategory = $('.filter-home').find('.active').attr('data-category-id');
    makePackery(videoCategory);
    appendFollowed(videoCategory);
    appendPopular(videoCategory);
    makeTags(videoCategory, 'popular', 1);
    makeTags(videoCategory, 'followed', 1);
});<|endoftext|>


<commit_before>'use strict';

var SlackClient = require('slack-client'),
  introject = require('introject');

var constants = require('./constants'),
  Response = require('./response').Response;

var Bot = function Bot(options) {
  this.client = new SlackClient(options.token);

  this.commands = {};
};

Bot.prototype.on = function on(type, cb) {
  // build a context. We will use this to inject dependencies.
  // bootstrap context with non event specific resources. can be injected
  // into any event.
  var context = {
    client: this.client,
    channels: this.client.channels,
    dms: this.client.dms,
    groups: this.client.groups,
    users: this.client.users,
    bots: this.client.bots
  };
  var response;

  this.client.on(type, function() {
    // build an array out of the arguments to this callback
    var args = Array.apply(null, arguments);

    // match up the arguments to constants.eventArgs, and then add to context
    constants.eventArgs[type].forEach(function(argName) {
      var arg = args.shift();
      context[argName] = arg;
    });

    // do some sort of seminonunintelligent context inference here
    if (context.message) {
      context.channel = context.client.getChannelGroupOrDMByID(context.message.channel);
      context.user = context.client.getUserByID(context.message.user);

      // TODO: don't construct response inside this if-block.
      response = new Response(context.channel);
    }

    // now, add our own deps
    context.response = response;

    // add the context itself as a dependency!
    context.context = context;

    // finally, inject stuff from our context to the callback, and run it
    introject.injectDeps(cb, context)();

    // if the callback put data into the response, send it.
    // TODO: somehow try to fix this for async callbacks.
    context.response.data && context.response.end();
  });
};

Bot.prototype.onMessage = function onMessage(subtype, cb) {
  if (typeof subtype === 'function') {
    cb = subtype;
    subtype = null;
  };

  this.on('message', function(context, message) {
    if (message.subtype === subtype || !subtype) {
      introject.injectDeps(cb, context)();
    }
  });
};

Bot.prototype.command = function command(name, cb) {
  // construct a hashset of the command names.
  var names = Array.isArray(name) ? name : [name];
  var commands = this.commands;
  names.forEach(function(name) {
    commands[name] = cb;
  });

  this.onMessage(function(context, message) {
    if (message.text && message.text.startsWith('!')) {
      message.text = message.text.slice(1);
    }
    else {
      return;
    }

    var words = message.text.split(' ');
    message.argv = context.argv = words;
    commands[words[0]] && introject.injectDeps(commands[words[0]], context)();
  });
};

Bot.prototype.start = function start() {
  this.client.login();
};

Bot.prototype.importBundle = function importBundle(bundle) {
  (typeof bundle === 'string') && (bundle = require(string));
  bundle(this);
};

module.exports = Bot;
<commit_msg>Add bot into base context<commit_after>'use strict';

var SlackClient = require('slack-client'),
  introject = require('introject');

var constants = require('./constants'),
  Response = require('./response').Response;

var Bot = function Bot(options) {
  this.client = new SlackClient(options.token);

  this.commands = {};
};

Bot.prototype.on = function on(type, cb) {
  // build a context. We will use this to inject dependencies.
  // bootstrap context with non event specific resources. can be injected
  // into any event.
  var context = {
    bot: this,
    client: this.client,
    channels: this.client.channels,
    dms: this.client.dms,
    groups: this.client.groups,
    users: this.client.users,
    bots: this.client.bots
  };
  var response;

  this.client.on(type, function() {
    // build an array out of the arguments to this callback
    var args = Array.apply(null, arguments);

    // match up the arguments to constants.eventArgs, and then add to context
    constants.eventArgs[type].forEach(function(argName) {
      var arg = args.shift();
      context[argName] = arg;
    });

    // do some sort of seminonunintelligent context inference here
    if (context.message) {
      context.channel = context.client.getChannelGroupOrDMByID(context.message.channel);
      context.user = context.client.getUserByID(context.message.user);

      // TODO: don't construct response inside this if-block.
      response = new Response(context.channel);
    }

    // now, add our own deps
    context.response = response;

    // add the context itself as a dependency!
    context.context = context;

    // finally, inject stuff from our context to the callback, and run it
    introject.injectDeps(cb, context)();

    // if the callback put data into the response, send it.
    // TODO: somehow try to fix this for async callbacks.
    context.response.data && context.response.end();
  });
};

Bot.prototype.onMessage = function onMessage(subtype, cb) {
  if (typeof subtype === 'function') {
    cb = subtype;
    subtype = null;
  };

  this.on('message', function(context, message) {
    if (message.subtype === subtype || !subtype) {
      introject.injectDeps(cb, context)();
    }
  });
};

Bot.prototype.command = function command(name, cb) {
  // construct a hashset of the command names.
  var names = Array.isArray(name) ? name : [name];
  var commands = this.commands;
  names.forEach(function(name) {
    commands[name] = cb;
  });

  this.onMessage(function(context, message) {
    if (message.text && message.text.startsWith('!')) {
      message.text = message.text.slice(1);
    }
    else {
      return;
    }

    var words = message.text.split(' ');
    message.argv = context.argv = words;
    commands[words[0]] && introject.injectDeps(commands[words[0]], context)();
  });
};

Bot.prototype.start = function start() {
  this.client.login();
};

Bot.prototype.importBundle = function importBundle(bundle) {
  (typeof bundle === 'string') && (bundle = require(string));
  bundle(this);
};

module.exports = Bot;
<|endoftext|>


<commit_before>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import discord from 'discord.js';
import { ConfigurationError } from './errors';
import { validateChannelMapping } from './validators';
import { formatFromDiscordToIRC, formatFromIRCToDiscord } from './formatting';

const REQUIRED_FIELDS = ['server', 'nickname', 'channelMapping', 'discordToken'];
const NICK_COLORS = ['light_blue', 'dark_blue', 'light_red', 'dark_red', 'light_green',
  'dark_green', 'magenta', 'light_magenta', 'orange', 'yellow', 'cyan', 'light_cyan'];
const patternMatch = /{\$(.+?)}/g;

/**
 * An IRC bot, works as a middleman for all communication
 * @param {object} options - server, nickname, channelMapping, outgoingToken, incomingURL
 */
class Bot {
  constructor(options) {
    REQUIRED_FIELDS.forEach((field) => {
      if (!options[field]) {
        throw new ConfigurationError(`Missing configuration field ${field}`);
      }
    });

    validateChannelMapping(options.channelMapping);

    this.discord = new discord.Client({ autoReconnect: true });

    this.server = options.server;
    this.nickname = options.nickname;
    this.ircOptions = options.ircOptions;
    this.discordToken = options.discordToken;
    this.commandCharacters = options.commandCharacters || [];
    this.ircNickColor = options.ircNickColor !== false; // default to true
    this.channels = _.values(options.channelMapping);
    this.ircStatusNotices = options.ircStatusNotices;
    this.announceSelfJoin = options.announceSelfJoin;

    this.format = options.format || {};
    // "{$keyName}" => "variableValue"
    // nickname: discord nickname
    // displayUsername: nickname with wrapped colors
    // text: the (IRC-formatted) message content
    // discordChannel: Discord channel (e.g. #general)
    // ircChannel: IRC channel (e.g. #irc)
    // attachmentURL: the URL of the attachment (only applicable in formatURLAttachment)
    this.formatCommandPrelude = this.format.commandPrelude || 'Command sent from Discord by {$nickname}:';
    this.formatIRCText = this.format.ircText || '<{$displayUsername}> {$text}';
    this.formatURLAttachment = this.format.urlAttachment || '<{$displayUsername}> {$attachmentURL}';

    // "{$keyName}" => "variableValue"
    // author: IRC nickname
    // text: the (Discord-formatted) message content
    // withMentions: text with appropriate mentions reformatted
    // discordChannel: Discord channel (e.g. #general)
    // ircChannel: IRC channel (e.g. #irc)
    this.formatDiscord = this.format.discord || '**<{$author}>** {$withMentions}';

    // Keep track of { channel => [list, of, usernames] } for ircStatusNotices
    this.channelUsers = {};

    this.channelMapping = {};

    // Remove channel passwords from the mapping and lowercase IRC channel names
    _.forOwn(options.channelMapping, (ircChan, discordChan) => {
      this.channelMapping[discordChan] = ircChan.split(' ')[0].toLowerCase();
    });

    this.invertedMapping = _.invert(this.channelMapping);
    this.autoSendCommands = options.autoSendCommands || [];
  }

  connect() {
    logger.debug('Connecting to IRC and Discord');
    this.discord.login(this.discordToken);

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      floodProtection: true,
      floodProtectionDelay: 500,
      retryCount: 10,
      ...this.ircOptions
    };

    this.ircClient = new irc.Client(this.server, this.nickname, ircOptions);
    this.attachListeners();
  }

  attachListeners() {
    this.discord.on('ready', () => {
      logger.info('Connected to Discord');
    });

    this.ircClient.on('registered', (message) => {
      logger.info('Connected to IRC');
      logger.debug('Registered event: ', message);
      this.autoSendCommands.forEach((element) => {
        this.ircClient.send(...element);
      });
    });

    this.ircClient.on('error', (error) => {
      logger.error('Received error event from IRC', error);
    });

    this.discord.on('error', (error) => {
      logger.error('Received error event from Discord', error);
    });

    this.discord.on('warn', (warning) => {
      logger.warn('Received warn event from Discord', warning);
    });

    this.discord.on('message', (message) => {
      // Ignore bot messages and people leaving/joining
      this.sendToIRC(message);
    });

    this.ircClient.on('message', this.sendToDiscord.bind(this));

    this.ircClient.on('notice', (author, to, text) => {
      this.sendToDiscord(author, to, `*${text}*`);
    });

    this.ircClient.on('nick',(oldnick, newnick, channels) => {
        channels.forEach(channelName => {
           const channel = channelName.toLowerCase();
           this.sendExactToDiscord(channel,`*${oldnick}* are now known as ${newnick}`);
        });
    });

    this.ircClient.on('join', (channelName, nick) => {
      logger.debug('Received join:', channelName, nick);
      if (!this.ircStatusNotices) return;
      if (nick === this.nickname && !this.announceSelfJoin) return;
      const channel = channelName.toLowerCase();
      // self-join is announced before names (which includes own nick)
      // so don't add nick to channelUsers
      if (nick !== this.nickname) this.channelUsers[channel].add(nick);
      this.sendExactToDiscord(channel, `*${nick}* has joined the channel`);
    });

    this.ircClient.on('part', (channelName, nick, reason) => {
      logger.debug('Received part:', channelName, nick, reason);
      if (!this.ircStatusNotices) return;
      const channel = channelName.toLowerCase();
      // remove list of users when no longer in channel (as it will become out of date)
      if (nick === this.nickname) {
        logger.debug('Deleting channelUsers as bot parted:', channel);
        delete this.channelUsers[channel];
        return;
      }
      if (this.channelUsers[channel]) {
        this.channelUsers[channel].delete(nick);
      } else {
        logger.warn(`No channelUsers found for ${channel} when ${nick} parted.`);
      }
      this.sendExactToDiscord(channel, `*${nick}* has left the channel (${reason})`);
    });

    this.ircClient.on('quit', (nick, reason, channels) => {
      logger.debug('Received quit:', nick, channels);
      if (!this.ircStatusNotices || nick === this.nickname) return;
      channels.forEach((channelName) => {
        const channel = channelName.toLowerCase();
        if (!this.channelUsers[channel]) {
          logger.warn(`No channelUsers found for ${channel} when ${nick} quit, ignoring.`);
          return;
        }
        if (!this.channelUsers[channel].delete(nick)) return;
        this.sendExactToDiscord(channel, `*${nick}* has quit (${reason})`);
      });
    });

    this.ircClient.on('names', (channelName, nicks) => {
      logger.debug('Received names:', channelName, nicks);
      if (!this.ircStatusNotices) return;
      const channel = channelName.toLowerCase();
      this.channelUsers[channel] = new Set(Object.keys(nicks));
    });

    this.ircClient.on('action', (author, to, text) => {
      this.sendToDiscord(author, to, `_${text}_`);
    });

    this.ircClient.on('invite', (channel, from) => {
      logger.debug('Received invite:', channel, from);
      if (!this.invertedMapping[channel]) {
        logger.debug('Channel not found in config, not joining:', channel);
      } else {
        this.ircClient.join(channel);
        logger.debug('Joining channel:', channel);
      }
    });

    if (logger.level === 'debug') {
      this.discord.on('debug', (message) => {
        logger.debug('Received debug event from Discord', message);
      });
    }
  }

  static getDiscordNicknameOnServer(user, guild) {
    const userDetails = guild.members.get(user.id);
    if (userDetails) {
      return userDetails.nickname || user.username;
    }
    return user.username;
  }

  parseText(message) {
    const text = message.mentions.users.reduce((content, mention) => {
      const displayName = Bot.getDiscordNicknameOnServer(mention, message.guild);
      return content.replace(`<@${mention.id}>`, `@${displayName}`)
             .replace(`<@!${mention.id}>`, `@${displayName}`)
             .replace(`<@&${mention.id}>`, `@${displayName}`);
    }, message.content);

    return text
      .replace(/\n|\r\n|\r/g, ' ')
      .replace(/<#(\d+)>/g, (match, channelId) => {
        const channel = this.discord.channels.get(channelId);
        if (channel) return `#${channel.name}`;
        return '#deleted-channel';
      })
      .replace(/<@&(\d+)>/g, (match, roleId) => {
        const role = message.guild.roles.get(roleId);
        if (role) return `@${role.name}`;
        return '@deleted-role';
      })
      .replace(/<(:\w+:)\d+>/g, (match, emoteName) => emoteName);
  }

  isCommandMessage(message) {
    return this.commandCharacters.indexOf(message[0]) !== -1;
  }

  static substitutePattern(message, patternMapping) {
    return message.replace(patternMatch, (match, varName) => patternMapping[varName] || match);
  }

  sendToIRC(message) {
    const author = message.author;
    // Ignore messages sent by the bot itself:
    if (author.id === this.discord.user.id) return;

    const channelName = `#${message.channel.name}`;
    const ircChannel = this.channelMapping[message.channel.id] ||
                                           this.channelMapping[channelName];

    logger.debug('Channel Mapping', channelName, this.channelMapping[channelName]);
    if (ircChannel) {
      const fromGuild = message.guild;
      const nickname = Bot.getDiscordNicknameOnServer(author, fromGuild);
      let text = this.parseText(message);
      let displayUsername = nickname;
      if (this.ircNickColor) {
        const colorIndex = (nickname.charCodeAt(0) + nickname.length) % NICK_COLORS.length;
        displayUsername = irc.colors.wrap(NICK_COLORS[colorIndex], nickname);
      }

      const patternMap = {
        nickname,
        displayUsername,
        text,
        discordChannel: channelName,
        ircChannel
      };

      if (this.isCommandMessage(text)) {
        const prelude = Bot.substitutePattern(this.formatCommandPrelude, patternMap);
        this.ircClient.say(ircChannel, prelude);
        this.ircClient.say(ircChannel, text);
      } else {
        if (text !== '') {
          // Convert formatting
          text = formatFromDiscordToIRC(text);
          patternMap.text = text;

          text = Bot.substitutePattern(this.formatIRCText, patternMap);
          logger.debug('Sending message to IRC', ircChannel, text);
          this.ircClient.say(ircChannel, text);
        }

        if (message.attachments && message.attachments.size) {
          message.attachments.forEach((a) => {
            patternMap.attachmentURL = a.url;
            const urlMessage = Bot.substitutePattern(this.formatURLAttachment, patternMap);

            logger.debug('Sending attachment URL to IRC', ircChannel, urlMessage);
            this.ircClient.say(ircChannel, urlMessage);
          });
        }
      }
    }
  }

  findDiscordChannel(ircChannel) {
    const discordChannelName = this.invertedMapping[ircChannel.toLowerCase()];
    if (discordChannelName) {
      // #channel -> channel before retrieving and select only text channels:
      const discordChannel = discordChannelName.startsWith('#') ? this.discord.channels
        .filter(c => c.type === 'text')
        .find('name', discordChannelName.slice(1)) : this.discord.channels.get(discordChannelName);

      if (!discordChannel) {
        logger.info('Tried to send a message to a channel the bot isn\'t in: ',
          discordChannelName);
        return null;
      }
      return discordChannel;
    }
    return null;
  }

  sendToDiscord(author, channel, text) {
    const discordChannel = this.findDiscordChannel(channel);
    if (!discordChannel) return;

    // Convert text formatting (bold, italics, underscore)
    const withFormat = formatFromIRCToDiscord(text);

    const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => {
      const search = match.substring(1);
      const guild = discordChannel.guild;
      const nickUser = guild.members.find('nickname', search);
      if (nickUser) {
        return nickUser;
      }

      const user = this.discord.users.find('username', search);
      if (user) {
        return user;
      }

      const role = guild.roles.find('name', search);
      if (role && role.mentionable) {
        return role;
      }

      return match;
    });

    const patternMap = {
      author,
      text: withFormat,
      withMentions,
      discordChannel: `#${discordChannel.name}`,
      ircChannel: channel
    };

    // Add bold formatting:
    // Use custom formatting from config / default formatting with bold author
    const withAuthor = Bot.substitutePattern(this.formatDiscord, patternMap);
    logger.debug('Sending message to Discord', withAuthor, channel, '->', `#${discordChannel.name}`);
    discordChannel.sendMessage(withAuthor);
  }

  /* Sends a message to Discord exactly as it appears */
  sendExactToDiscord(channel, text) {
    const discordChannel = this.findDiscordChannel(channel);
    if (!discordChannel) return;

    logger.debug('Sending special message to Discord', text, channel, '->', `#${discordChannel.name}`);
    discordChannel.sendMessage(text);
  }
}

export default Bot;
<commit_msg>fixed linter stuff<commit_after>import _ from 'lodash';
import irc from 'irc';
import logger from 'winston';
import discord from 'discord.js';
import { ConfigurationError } from './errors';
import { validateChannelMapping } from './validators';
import { formatFromDiscordToIRC, formatFromIRCToDiscord } from './formatting';

const REQUIRED_FIELDS = ['server', 'nickname', 'channelMapping', 'discordToken'];
const NICK_COLORS = ['light_blue', 'dark_blue', 'light_red', 'dark_red', 'light_green',
  'dark_green', 'magenta', 'light_magenta', 'orange', 'yellow', 'cyan', 'light_cyan'];
const patternMatch = /{\$(.+?)}/g;

/**
 * An IRC bot, works as a middleman for all communication
 * @param {object} options - server, nickname, channelMapping, outgoingToken, incomingURL
 */
class Bot {
  constructor(options) {
    REQUIRED_FIELDS.forEach((field) => {
      if (!options[field]) {
        throw new ConfigurationError(`Missing configuration field ${field}`);
      }
    });

    validateChannelMapping(options.channelMapping);

    this.discord = new discord.Client({ autoReconnect: true });

    this.server = options.server;
    this.nickname = options.nickname;
    this.ircOptions = options.ircOptions;
    this.discordToken = options.discordToken;
    this.commandCharacters = options.commandCharacters || [];
    this.ircNickColor = options.ircNickColor !== false; // default to true
    this.channels = _.values(options.channelMapping);
    this.ircStatusNotices = options.ircStatusNotices;
    this.announceSelfJoin = options.announceSelfJoin;

    this.format = options.format || {};
    // "{$keyName}" => "variableValue"
    // nickname: discord nickname
    // displayUsername: nickname with wrapped colors
    // text: the (IRC-formatted) message content
    // discordChannel: Discord channel (e.g. #general)
    // ircChannel: IRC channel (e.g. #irc)
    // attachmentURL: the URL of the attachment (only applicable in formatURLAttachment)
    this.formatCommandPrelude = this.format.commandPrelude || 'Command sent from Discord by {$nickname}:';
    this.formatIRCText = this.format.ircText || '<{$displayUsername}> {$text}';
    this.formatURLAttachment = this.format.urlAttachment || '<{$displayUsername}> {$attachmentURL}';

    // "{$keyName}" => "variableValue"
    // author: IRC nickname
    // text: the (Discord-formatted) message content
    // withMentions: text with appropriate mentions reformatted
    // discordChannel: Discord channel (e.g. #general)
    // ircChannel: IRC channel (e.g. #irc)
    this.formatDiscord = this.format.discord || '**<{$author}>** {$withMentions}';

    // Keep track of { channel => [list, of, usernames] } for ircStatusNotices
    this.channelUsers = {};

    this.channelMapping = {};

    // Remove channel passwords from the mapping and lowercase IRC channel names
    _.forOwn(options.channelMapping, (ircChan, discordChan) => {
      this.channelMapping[discordChan] = ircChan.split(' ')[0].toLowerCase();
    });

    this.invertedMapping = _.invert(this.channelMapping);
    this.autoSendCommands = options.autoSendCommands || [];
  }

  connect() {
    logger.debug('Connecting to IRC and Discord');
    this.discord.login(this.discordToken);

    const ircOptions = {
      userName: this.nickname,
      realName: this.nickname,
      channels: this.channels,
      floodProtection: true,
      floodProtectionDelay: 500,
      retryCount: 10,
      ...this.ircOptions
    };

    this.ircClient = new irc.Client(this.server, this.nickname, ircOptions);
    this.attachListeners();
  }

  attachListeners() {
    this.discord.on('ready', () => {
      logger.info('Connected to Discord');
    });

    this.ircClient.on('registered', (message) => {
      logger.info('Connected to IRC');
      logger.debug('Registered event: ', message);
      this.autoSendCommands.forEach((element) => {
        this.ircClient.send(...element);
      });
    });

    this.ircClient.on('error', (error) => {
      logger.error('Received error event from IRC', error);
    });

    this.discord.on('error', (error) => {
      logger.error('Received error event from Discord', error);
    });

    this.discord.on('warn', (warning) => {
      logger.warn('Received warn event from Discord', warning);
    });

    this.discord.on('message', (message) => {
      // Ignore bot messages and people leaving/joining
      this.sendToIRC(message);
    });

    this.ircClient.on('message', this.sendToDiscord.bind(this));

    this.ircClient.on('notice', (author, to, text) => {
      this.sendToDiscord(author, to, `*${text}*`);
    });

    this.ircClient.on('nick', (oldnick, newnick, channels) => {
      channels.forEach((channelName) => {
        const channel = channelName.toLowerCase();
        this.sendExactToDiscord(channel, `*${oldnick}* are now known as ${newnick}`);
      });
    });

    this.ircClient.on('join', (channelName, nick) => {
      logger.debug('Received join:', channelName, nick);
      if (!this.ircStatusNotices) return;
      if (nick === this.nickname && !this.announceSelfJoin) return;
      const channel = channelName.toLowerCase();
      // self-join is announced before names (which includes own nick)
      // so don't add nick to channelUsers
      if (nick !== this.nickname) this.channelUsers[channel].add(nick);
      this.sendExactToDiscord(channel, `*${nick}* has joined the channel`);
    });

    this.ircClient.on('part', (channelName, nick, reason) => {
      logger.debug('Received part:', channelName, nick, reason);
      if (!this.ircStatusNotices) return;
      const channel = channelName.toLowerCase();
      // remove list of users when no longer in channel (as it will become out of date)
      if (nick === this.nickname) {
        logger.debug('Deleting channelUsers as bot parted:', channel);
        delete this.channelUsers[channel];
        return;
      }
      if (this.channelUsers[channel]) {
        this.channelUsers[channel].delete(nick);
      } else {
        logger.warn(`No channelUsers found for ${channel} when ${nick} parted.`);
      }
      this.sendExactToDiscord(channel, `*${nick}* has left the channel (${reason})`);
    });

    this.ircClient.on('quit', (nick, reason, channels) => {
      logger.debug('Received quit:', nick, channels);
      if (!this.ircStatusNotices || nick === this.nickname) return;
      channels.forEach((channelName) => {
        const channel = channelName.toLowerCase();
        if (!this.channelUsers[channel]) {
          logger.warn(`No channelUsers found for ${channel} when ${nick} quit, ignoring.`);
          return;
        }
        if (!this.channelUsers[channel].delete(nick)) return;
        this.sendExactToDiscord(channel, `*${nick}* has quit (${reason})`);
      });
    });

    this.ircClient.on('names', (channelName, nicks) => {
      logger.debug('Received names:', channelName, nicks);
      if (!this.ircStatusNotices) return;
      const channel = channelName.toLowerCase();
      this.channelUsers[channel] = new Set(Object.keys(nicks));
    });

    this.ircClient.on('action', (author, to, text) => {
      this.sendToDiscord(author, to, `_${text}_`);
    });

    this.ircClient.on('invite', (channel, from) => {
      logger.debug('Received invite:', channel, from);
      if (!this.invertedMapping[channel]) {
        logger.debug('Channel not found in config, not joining:', channel);
      } else {
        this.ircClient.join(channel);
        logger.debug('Joining channel:', channel);
      }
    });

    if (logger.level === 'debug') {
      this.discord.on('debug', (message) => {
        logger.debug('Received debug event from Discord', message);
      });
    }
  }

  static getDiscordNicknameOnServer(user, guild) {
    const userDetails = guild.members.get(user.id);
    if (userDetails) {
      return userDetails.nickname || user.username;
    }
    return user.username;
  }

  parseText(message) {
    const text = message.mentions.users.reduce((content, mention) => {
      const displayName = Bot.getDiscordNicknameOnServer(mention, message.guild);
      return content.replace(`<@${mention.id}>`, `@${displayName}`)
             .replace(`<@!${mention.id}>`, `@${displayName}`)
             .replace(`<@&${mention.id}>`, `@${displayName}`);
    }, message.content);

    return text
      .replace(/\n|\r\n|\r/g, ' ')
      .replace(/<#(\d+)>/g, (match, channelId) => {
        const channel = this.discord.channels.get(channelId);
        if (channel) return `#${channel.name}`;
        return '#deleted-channel';
      })
      .replace(/<@&(\d+)>/g, (match, roleId) => {
        const role = message.guild.roles.get(roleId);
        if (role) return `@${role.name}`;
        return '@deleted-role';
      })
      .replace(/<(:\w+:)\d+>/g, (match, emoteName) => emoteName);
  }

  isCommandMessage(message) {
    return this.commandCharacters.indexOf(message[0]) !== -1;
  }

  static substitutePattern(message, patternMapping) {
    return message.replace(patternMatch, (match, varName) => patternMapping[varName] || match);
  }

  sendToIRC(message) {
    const author = message.author;
    // Ignore messages sent by the bot itself:
    if (author.id === this.discord.user.id) return;

    const channelName = `#${message.channel.name}`;
    const ircChannel = this.channelMapping[message.channel.id] ||
                                           this.channelMapping[channelName];

    logger.debug('Channel Mapping', channelName, this.channelMapping[channelName]);
    if (ircChannel) {
      const fromGuild = message.guild;
      const nickname = Bot.getDiscordNicknameOnServer(author, fromGuild);
      let text = this.parseText(message);
      let displayUsername = nickname;
      if (this.ircNickColor) {
        const colorIndex = (nickname.charCodeAt(0) + nickname.length) % NICK_COLORS.length;
        displayUsername = irc.colors.wrap(NICK_COLORS[colorIndex], nickname);
      }

      const patternMap = {
        nickname,
        displayUsername,
        text,
        discordChannel: channelName,
        ircChannel
      };

      if (this.isCommandMessage(text)) {
        const prelude = Bot.substitutePattern(this.formatCommandPrelude, patternMap);
        this.ircClient.say(ircChannel, prelude);
        this.ircClient.say(ircChannel, text);
      } else {
        if (text !== '') {
          // Convert formatting
          text = formatFromDiscordToIRC(text);
          patternMap.text = text;

          text = Bot.substitutePattern(this.formatIRCText, patternMap);
          logger.debug('Sending message to IRC', ircChannel, text);
          this.ircClient.say(ircChannel, text);
        }

        if (message.attachments && message.attachments.size) {
          message.attachments.forEach((a) => {
            patternMap.attachmentURL = a.url;
            const urlMessage = Bot.substitutePattern(this.formatURLAttachment, patternMap);

            logger.debug('Sending attachment URL to IRC', ircChannel, urlMessage);
            this.ircClient.say(ircChannel, urlMessage);
          });
        }
      }
    }
  }

  findDiscordChannel(ircChannel) {
    const discordChannelName = this.invertedMapping[ircChannel.toLowerCase()];
    if (discordChannelName) {
      // #channel -> channel before retrieving and select only text channels:
      const discordChannel = discordChannelName.startsWith('#') ? this.discord.channels
        .filter(c => c.type === 'text')
        .find('name', discordChannelName.slice(1)) : this.discord.channels.get(discordChannelName);

      if (!discordChannel) {
        logger.info('Tried to send a message to a channel the bot isn\'t in: ',
          discordChannelName);
        return null;
      }
      return discordChannel;
    }
    return null;
  }

  sendToDiscord(author, channel, text) {
    const discordChannel = this.findDiscordChannel(channel);
    if (!discordChannel) return;

    // Convert text formatting (bold, italics, underscore)
    const withFormat = formatFromIRCToDiscord(text);

    const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => {
      const search = match.substring(1);
      const guild = discordChannel.guild;
      const nickUser = guild.members.find('nickname', search);
      if (nickUser) {
        return nickUser;
      }

      const user = this.discord.users.find('username', search);
      if (user) {
        return user;
      }

      const role = guild.roles.find('name', search);
      if (role && role.mentionable) {
        return role;
      }

      return match;
    });

    const patternMap = {
      author,
      text: withFormat,
      withMentions,
      discordChannel: `#${discordChannel.name}`,
      ircChannel: channel
    };

    // Add bold formatting:
    // Use custom formatting from config / default formatting with bold author
    const withAuthor = Bot.substitutePattern(this.formatDiscord, patternMap);
    logger.debug('Sending message to Discord', withAuthor, channel, '->', `#${discordChannel.name}`);
    discordChannel.sendMessage(withAuthor);
  }

  /* Sends a message to Discord exactly as it appears */
  sendExactToDiscord(channel, text) {
    const discordChannel = this.findDiscordChannel(channel);
    if (!discordChannel) return;

    logger.debug('Sending special message to Discord', text, channel, '->', `#${discordChannel.name}`);
    discordChannel.sendMessage(text);
  }
}

export default Bot;
<|endoftext|>


<commit_before>const
path=require('path'),
args= require('pico-args'),
picoObj=require('pico-common').export('pico/obj'),
SigSlot= require('./SigSlot'),
mergeOpt = {mergeArr: true},
defaults= {
    path: ['', 'app config path'],
    p: '@path',
    config: [void 0, 'app config json object'],
    c: '@config',
	dir: ['', 'relative working directory to current dir'],
	d: '@dir',
    name: ['', 'app name'],
    n: '@name',
    id: ['', 'app id'],
    i: '@id',
    env: ['', 'app environment'],
    e: '@env',
    master: [false, 'run as master in cluster mode'],
    m: '@master',
    help: [false, 'show this help'],
    h: '@help'
},
parseFname=function(fpath){
	const
	fname=path.basename(fpath),
	[base,env,ext]=fname.split('.')
	if (!base) return []
	const [appName,count]=base.split('-')

	// TODO: count is ignored for now, always 1 fork
	return [appName,parseInt(count)||require('os').cpus().length,env,ext]
}

module.exports = {
    parse(rootPath, force){
		const options = args.parse(Object.assign(defaults, force))

		if (!options || options.help){
			args.usage(defaults)
			process.exit(0)
		}

		let configPath,config
		if (options.path){
			configPath=path.resolve(process.cwd(),options.path)
			config= require(configPath)
		}else{
			config= options.config
		}
		if (!config) return console.error('Error: missing config',options.path)

		if (options.path){
			var [appName,count,env,ext]=parseFname(options.path)
			if (config.deps){
				const
				r = config.deps,
				p = path.dirname(options.path)
				for(let i=0,d; d=r[i]; i++){
					config = picoObj.extend(require(path.resolve(p,d)), config, mergeOpt)
				}
			}
		}

		const app = config.app
		if (app.path) console.warn('Warn: app.path[%s] is replaced by %s', app.path, rootPath)

		Object.assign(app, {
			path: rootPath, // picos path
			workingPath: path.resolve(process.cwd(), options.dir), // instance path
			configPath: path.dirname(configPath), // config path
			sigslot: new SigSlot(app.signalTTL),

			id: options.id || app.id,
			master: options.master || app.master,
			name: options.name || appName || app.name,
			env: options.env || env || app.env
		})

		return config
    },
	parsePath:parseFname
}
<commit_msg>refactored<commit_after>const path = require('path')
const args = require('pico-args')
const pObj = require('pico-common').export('pico/obj')
const SigSlot = require('./SigSlot')
const mergeOpt = {mergeArr: true}
const defaults = {
    path: ['', 'app config path'],
    p: '@path',
    config: [void 0, 'app config json object'],
    c: '@config',
	dir: ['', 'relative working directory to current dir'],
	d: '@dir',
    name: ['', 'app name'],
    n: '@name',
    id: ['', 'app id'],
    i: '@id',
    env: ['', 'app environment'],
    e: '@env',
    master: [false, 'run as master in cluster mode'],
    m: '@master',
    help: [false, 'show this help'],
    h: '@help'
}

function parseFname(fpath){
	const fname = path.basename(fpath)
	const [base,env,ext] = fname.split('.')
	if (!base) return []
	const [appName,count] = base.split('-')

	// TODO: count is ignored for now, always 1 fork
	return [appName,parseInt(count)||require('os').cpus().length,env,ext]
}

module.exports = {
    parse(rootPath, force){
		const options = args.parse(Object.assign(defaults, force))

		if (!options || options.help){
			args.usage(defaults)
			process.exit(0)
		}

		let configPath,config
		if (options.path){
			configPath=path.resolve(process.cwd(),options.path)
			config= require(configPath)
		}else{
			config= options.config
		}
		if (!config) return console.error('Error: missing config',options.path)

		if (options.path){
			var [appName,count,env,ext]=parseFname(options.path)
			if (config.deps){
				const
				r = config.deps,
				p = path.dirname(options.path)
				for(let i=0,d; d=r[i]; i++){
					config = pObj.extend(require(path.resolve(p,d)), config, mergeOpt)
				}
			}
		}

		const app = config.app
		if (app.path) console.warn('Warn: app.path[%s] is replaced by %s', app.path, rootPath)

		Object.assign(app, {
			path: rootPath, // picos path
			workingPath: path.resolve(process.cwd(), options.dir), // instance path
			configPath: path.dirname(configPath), // config path
			sigslot: new SigSlot(app.signalTTL),

			id: options.id || app.id,
			master: options.master || app.master,
			name: options.name || appName || app.name,
			env: options.env || env || app.env
		})

		return config
    },
	parsePath:parseFname
}
<|endoftext|>


<commit_before>/**
 * CGI执行器
 *
 * @author Zongmin Lei<leizongmin@gmail.com>
 */

var os = require('os');
var spawn = require('child_process').spawn;
var path = require('path');
var fs = require('fs');
var url = require('url');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var debug = require('./debug')('cgi');

// 默认环境
var DEFAULT_ENV = {
  SERVER_SOFTWARE:      'Node.js/' + process.version,
  SERVER_PROTOCOL:      'HTTP/1.1',
  GATEWAY_INTERFACE:    'CGI/1.1',
  SERVER_NAME:          os.hostname() || 'unknown',
  SERVER_PORT:          80,
  REDIRECT_STATUS_ENV:  0,
  REDIRECT_STATUS:      200
};

/**
 * 合并对象
 *
 * @param {Object} a
 * @param {Object} b
 * @return {Object}
 */
function merge (a, b) {
  var c = {};
  for (var i in a) c[i] = a[i];
  for (var i in b) c[i] = b[i];
  return c;
}

/**
 * 根据 ServerRequest 对象生成环境变量
 *
 * @param {ServerRequest} req
 * @param {Object} reqEnv
 * @return {Object}
 */
function makeRequestEnv (req, reqEnv) {
  var reqdata = url.parse(req.url);
  reqEnv = reqEnv || {};

  // 基本信息
  reqEnv.SCRIPT_NAME = reqdata.pathname;
  reqEnv.PATH_INFO = path.normalize(reqEnv.DOCUMENT_ROOT + reqdata.pathname);
  reqEnv.PATH_TRANSLATED = path.normalize(reqEnv.DOCUMENT_ROOT + reqdata.pathname);
  reqEnv.REQUEST_METHOD = req.method;
  reqEnv.SERVER_NAME = reqdata.hostname || reqEnv.SERVER_NAME;
  reqEnv.SERVER_PORT = reqdata.port || reqEnv.SERVER_PORT;
  reqEnv.QUERY_STRING = reqdata.query;

  // 请求头
  for (var i in req.headers) {
    reqEnv['HTTP_' + i.toUpperCase().split('-').join('_')] = req.headers[i];
  }
  if ('content-length' in req.headers) {
    reqEnv.CONTENT_LENGTH = req.headers['content-length'];
  }
  if ('content-type' in req.headers) {
    reqEnv.CONTENT_TYPE = req.headers['content-type'];
  }
  if ('authorization' in req.headers) {
    reqEnv.AUTH_TYPE = req.headers.authorization.split(' ')[0];
  }

  return reqEnv;
}

/**
 * 查找CRLF+CRLF的位置
 *
 * @param {Buffer} buf
 * @return {Number}
 */
function find2CRLF (buf) {
  var end = buf.length - 3;
  for (i = 0; i < end; i++) {
    if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) {
      return i;
    }
  }
  return -1;
}

/**
 * 生成处理请求实例
 *
 * @param {String} bin 可执行文件路径
 * @param {Object} env 环境变量
 * @param {Array} args 启动参数
 */
function CGI (bin, env, args) {
  this.bin = bin;
  this.env = merge(DEFAULT_ENV, env);
  this.args = args || [];
  debug('new CGI instance: bin=%s, env=%s, args=%s', bin, JSON.stringify(env), args.join(' '));
}

/**
 * 处理请求
 *
 * @param {ServerRequest} req
 * @param {ServerResponse} res
 * @param {Function} next
 * @return {ChildProcess}
 */
CGI.prototype.handle = function (req, res, next) {
  var me = this;
  next = next || function (err) {
    debug('handle: no next()');
    if (err) {
      res.statusCode = 500;
      res.end((err.stack || err).toString());
    } else {
      res.end();
    }
  };

  debug('spawn');
  var cgi = spawn(me.bin, me.args, {
    env: makeRequestEnv(req, me.env)
  });

  req.pipe(cgi.stdin);

  var p = new ResponseParser(cgi);
  p.on('error', function (err) {
    debug('response: error=%s', err.stack || err);
    next(err);
  });
  p.on('status', function (s) {
    debug('response: status=%s', s);
    res.statusCode = s;
  });
  p.on('header', function (h, v) {
    debug('response: header=%s:%s', h, v);
    res.setHeader(h, v);
  });
  p.on('body', function () {
    if (!res.getHeader('content-length')) {
      res.setHeader('Transfer-Encoding', 'chunked');
    }
  });
  p.on('stdout', function (b) {
    res.write(b);
  });
  p.on('stderr', function (b) {
    res.write(b);
  });
  p.on('end', function () {
    debug('response: end');
    res.end();
  });

  cgi._responseParser = p;
  return cgi;
}

/**
 * 解析CGI程序的响应
 *
 * @param {ChildProcess} cgi
 */
function ResponseParser (cgi) {
  var me = this;
  this.cgi = cgi;
  this._hasError = false;
  this._hasEnd = false;
  this._headersSent = false;
  this._headersBuffer = [];

  // stdout输出
  cgi.stdout.on('data', function (b) {
    me._push(b);
  });

  // stderr输出
  cgi.stderr.on('data', function (b) {
    me.emit('stderr', b);
  });

  // 进程退出
  cgi.on('exit', function() {
    me._end();
  });

  // 进程出错
  cgi.on('error', function (err) {
    if (err.code === 'OK') return me._end();
    me._error(err);
  });
}
util.inherits(ResponseParser, EventEmitter);

/**
 * 检查是否已结束
 *
 * @return {Boolean}
 */
ResponseParser.prototype.isEnd = function () {
  return this._hasEnd || this._hasError;
};

/**
 * 程序结束
 */
ResponseParser.prototype._end = function () {
  var isEnd = this.isEnd();
  this._hasEnd = true;
  if (!isEnd) this.emit('end');
};

/**
 * 程序出错
 */
ResponseParser.prototype._error = function (err) {
  var isEnd = this.isEnd();
  this._hasError = true;
  if (!isEnd) this.emit('error', err);
};

/**
 * 收到数据
 *
 * @param {Buffer} data
 */
ResponseParser.prototype._push = function (data) {
  var me = this;

  if (me._headersSent) return me.emit('stdout', data);

  me._headersBuffer.push(data);
  var b = Buffer.concat(me._headersBuffer);
  var bi = find2CRLF(b);
  if (bi === -1) {
    me._headersBuffer = [b];
  } else {

    var lines = b.slice(0, bi).toString().split('\r\n');
    var status = lines[0].trim().match(/\d+/g)[0];
    me.emit('status', Number(status));

    for (var l = 1; l < lines.length; l++) {
      var i = lines[l].indexOf(':');
      if (i === -1) i = lines[l].length;
      var h = lines[l].substr(0, i).trim();
      var v = lines[l].substr(i + 1).trim();
      me.emit('header', h, v);
    }

    me._headersSent = true;
    me._headersBuffer = null;
    me.emit('body');
    me.emit('stdout', b.slice(bi + 4));
  }
};

module.exports = CGI;
<commit_msg>cgi显示调试信息：启动进程时的环境变量<commit_after>/**
 * CGI执行器
 *
 * @author Zongmin Lei<leizongmin@gmail.com>
 */

var os = require('os');
var spawn = require('child_process').spawn;
var path = require('path');
var fs = require('fs');
var url = require('url');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var debug = require('./debug')('cgi');

// 默认环境
var DEFAULT_ENV = {
  SERVER_SOFTWARE:      'Node.js/' + process.version,
  SERVER_PROTOCOL:      'HTTP/1.1',
  GATEWAY_INTERFACE:    'CGI/1.1',
  SERVER_NAME:          os.hostname() || 'unknown',
  SERVER_PORT:          80,
  REDIRECT_STATUS_ENV:  0,
  REDIRECT_STATUS:      200
};

/**
 * 合并对象
 *
 * @param {Object} a
 * @param {Object} b
 * @return {Object}
 */
function merge (a, b) {
  var c = {};
  for (var i in a) c[i] = a[i];
  for (var i in b) c[i] = b[i];
  return c;
}

/**
 * 根据 ServerRequest 对象生成环境变量
 *
 * @param {ServerRequest} req
 * @param {Object} reqEnv
 * @return {Object}
 */
function makeRequestEnv (req, reqEnv) {
  var reqdata = url.parse(req.url);
  reqEnv = reqEnv || {};

  // 基本信息
  reqEnv.SCRIPT_NAME = reqdata.pathname;
  reqEnv.PATH_INFO = path.normalize(reqEnv.DOCUMENT_ROOT + reqdata.pathname);
  reqEnv.PATH_TRANSLATED = path.normalize(reqEnv.DOCUMENT_ROOT + reqdata.pathname);
  reqEnv.REQUEST_METHOD = req.method;
  reqEnv.SERVER_NAME = reqdata.hostname || reqEnv.SERVER_NAME;
  reqEnv.SERVER_PORT = reqdata.port || reqEnv.SERVER_PORT;
  reqEnv.QUERY_STRING = reqdata.query;

  // 请求头
  for (var i in req.headers) {
    reqEnv['HTTP_' + i.toUpperCase().split('-').join('_')] = req.headers[i];
  }
  if ('content-length' in req.headers) {
    reqEnv.CONTENT_LENGTH = req.headers['content-length'];
  }
  if ('content-type' in req.headers) {
    reqEnv.CONTENT_TYPE = req.headers['content-type'];
  }
  if ('authorization' in req.headers) {
    reqEnv.AUTH_TYPE = req.headers.authorization.split(' ')[0];
  }

  return reqEnv;
}

/**
 * 查找CRLF+CRLF的位置
 *
 * @param {Buffer} buf
 * @return {Number}
 */
function find2CRLF (buf) {
  var end = buf.length - 3;
  for (i = 0; i < end; i++) {
    if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) {
      return i;
    }
  }
  return -1;
}

/**
 * 生成处理请求实例
 *
 * @param {String} bin 可执行文件路径
 * @param {Object} env 环境变量
 * @param {Array} args 启动参数
 */
function CGI (bin, env, args) {
  this.bin = bin;
  this.env = merge(DEFAULT_ENV, env);
  this.args = args || [];
  debug('new CGI instance: bin=%s, env=%s, args=%s', bin, JSON.stringify(env), args.join(' '));
}

/**
 * 处理请求
 *
 * @param {ServerRequest} req
 * @param {ServerResponse} res
 * @param {Function} next
 * @return {ChildProcess}
 */
CGI.prototype.handle = function (req, res, next) {
  var me = this;
  next = next || function (err) {
    debug('handle: no next()');
    if (err) {
      res.statusCode = 500;
      res.end((err.stack || err).toString());
    } else {
      res.end();
    }
  };

  debug('spawn');
  var env = makeRequestEnv(req, me.env);
  for (var i in env) debug('env: %s=%s', i, env[i]);
  var cgi = spawn(me.bin, me.args, {
    env: env
  });

  req.pipe(cgi.stdin);

  var p = new ResponseParser(cgi);
  p.on('error', function (err) {
    debug('response: error=%s', err.stack || err);
    next(err);
  });
  p.on('status', function (s) {
    debug('response: status=%s', s);
    res.statusCode = s;
  });
  p.on('header', function (h, v) {
    debug('response: header=%s:%s', h, v);
    res.setHeader(h, v);
  });
  p.on('body', function () {
    if (!res.getHeader('content-length')) {
      res.setHeader('Transfer-Encoding', 'chunked');
    }
  });
  p.on('stdout', function (b) {
    res.write(b);
  });
  p.on('stderr', function (b) {
    res.write(b);
  });
  p.on('end', function () {
    debug('response: end');
    res.end();
  });

  cgi._responseParser = p;
  return cgi;
}

/**
 * 解析CGI程序的响应
 *
 * @param {ChildProcess} cgi
 */
function ResponseParser (cgi) {
  var me = this;
  this.cgi = cgi;
  this._hasError = false;
  this._hasEnd = false;
  this._headersSent = false;
  this._headersBuffer = [];

  // stdout输出
  cgi.stdout.on('data', function (b) {
    me._push(b);
  });

  // stderr输出
  cgi.stderr.on('data', function (b) {
    me.emit('stderr', b);
  });

  // 进程退出
  cgi.on('exit', function() {
    me._end();
  });

  // 进程出错
  cgi.on('error', function (err) {
    if (err.code === 'OK') return me._end();
    me._error(err);
  });
}
util.inherits(ResponseParser, EventEmitter);

/**
 * 检查是否已结束
 *
 * @return {Boolean}
 */
ResponseParser.prototype.isEnd = function () {
  return this._hasEnd || this._hasError;
};

/**
 * 程序结束
 */
ResponseParser.prototype._end = function () {
  var isEnd = this.isEnd();
  this._hasEnd = true;
  if (!isEnd) this.emit('end');
};

/**
 * 程序出错
 */
ResponseParser.prototype._error = function (err) {
  var isEnd = this.isEnd();
  this._hasError = true;
  if (!isEnd) this.emit('error', err);
};

/**
 * 收到数据
 *
 * @param {Buffer} data
 */
ResponseParser.prototype._push = function (data) {
  var me = this;

  if (me._headersSent) return me.emit('stdout', data);

  me._headersBuffer.push(data);
  var b = Buffer.concat(me._headersBuffer);
  var bi = find2CRLF(b);
  if (bi === -1) {
    me._headersBuffer = [b];
  } else {

    var lines = b.slice(0, bi).toString().split('\r\n');
    var status = lines[0].trim().match(/\d+/g)[0];
    me.emit('status', Number(status));

    for (var l = 1; l < lines.length; l++) {
      var i = lines[l].indexOf(':');
      if (i === -1) i = lines[l].length;
      var h = lines[l].substr(0, i).trim();
      var v = lines[l].substr(i + 1).trim();
      me.emit('header', h, v);
    }

    me._headersSent = true;
    me._headersBuffer = null;
    me.emit('body');
    me.emit('stdout', b.slice(bi + 4));
  }
};

module.exports = CGI;
<|endoftext|>