<commit_before>// @flow weak
const EventEmitter = require('events');

const bodyParser = require('body-parser');
const Cacheman = require('cacheman');
const crypto = require('crypto');

const debug = require('debug')('messenger');
const logError = require('debug')('messenger:error');

const express = require('express');
const exphbs = require('express-handlebars');
const reqPromise = require('request-promise');
const urlJoin = require('url-join');

const config = require('./config');
const { ConversationLogger } = require('./conversationLogger');


const SESSION_TIMEOUT_MS = 3600 * 1000;  // 1 hour

const DEFAULT_GREETINGS_REGEX = /^(get started|good(morning|afternoon)|hello|hey|hi|hola|what's up)/i;
const DEFAULT_HELP_REGEX = /^help\b/i;

/*:: type Session = {_pageId: string|number, count: number, profile: ?Object} */

class Response {
  /*:: _messenger: Messenger */
  /*:: senderId: string|number */
  /*:: session: Session */
  constructor(messenger/*: Messenger */, options/*: Object */) {
    Object.assign(this, options);
    ['senderId', 'session'].forEach((required) => {
      // $FlowFixMe
      if (!this[required]) {
        throw new Error(`Incomplete Response, missing ${required}: ${JSON.stringify(options)}`);
      }
    });
    this._messenger = messenger;
    // $FlowFixMe
    this.reply = this.reply.bind(this);
  }

  reply(response) {
    return this._messenger.pageSend(this.session._pageId, this.senderId, response);
  }
}

class Messenger extends EventEmitter {
  /*:: app: Object */
  /*:: cache: Object */
  /*:: conversationLogger: Object */
  /*:: greetings: RegExp */
  /*:: help: RegExp */
  /*:: options: Object */
  /*:: pages: Object */
  constructor({
      hookPath = '/webhook',
      linkPath = '/link',
      emitGreetings = true,
      cache,
      pages = {}
    } = {}) {
    super();

    this.conversationLogger = new ConversationLogger(config);

    if (emitGreetings instanceof RegExp) {
      this.greetings = emitGreetings;
    } else {
      this.greetings = DEFAULT_GREETINGS_REGEX;
    }

    this.options = {
      emitGreetings: !!emitGreetings,
      hookPath,
      linkPath
    };

    if (cache) {
      this.cache = cache;
    } else {
      this.cache = new Cacheman('sessions', {ttl: SESSION_TIMEOUT_MS / 1000});
    }

    if (pages && Object.keys(pages).length) {
      this.pages = pages;
    } else if (config.has('messenger.pageAccessToken') && config.has('facebook.pageId')) {
      this.pages = {[config.get('facebook.pageId')]: config.get('messenger.pageAccessToken')};
    } else {
      this.pages = {};
      debug("MISSING options.pages; you won't be able to reply or get profile information");
    }

    this.app = express();
    this.app.engine('handlebars', exphbs({defaultLayout: 'main'}));
    this.app.set('view engine', 'handlebars');

    this.app.use(bodyParser.json({ verify: this.verifyRequestSignature.bind(this) }));
    this.app.use(bodyParser.urlencoded({ extended: true }));
    this.app.use(express.static('public'));

    this.help = DEFAULT_HELP_REGEX;

    // Facebook Messenger verification
    this.app.get(hookPath, (req, res) => {
      if (req.query['hub.mode'] === 'subscribe' &&
      req.query['hub.verify_token'] === config.get('messenger.validationToken')) {
        debug('Validating webhook');
        res.status(200).send(req.query['hub.challenge']);
      } else {
        debug('Failed validation. Make sure the validation tokens match.');
        res.sendStatus(403);
      }
    });

    this.app.post(hookPath, (req, res) => {
      const data = req.body;
      this.conversationLogger.logIncoming(data);
      // `data` reference:
      // https://developers.facebook.com/docs/messenger-platform/webhook-reference#format
      if (data.object === 'page') {
        data.entry.forEach((pageEntry) => {
          pageEntry.messaging.forEach((x) => this.routeEachMessage(x, pageEntry.id));
        });
        res.sendStatus(200);
      }
    });

    this.app.post(linkPath, (req, res) => {
      this.onLink(req.body);
      res.sendStatus(200);
    });

    // App routes

    // Stub routes for future functionality
    this.app.get('/login', (req, res) => res.render('login', {
      appId: config.get('facebook.appId'),
      serverUrl: config.get('serverUrl')
    }));
    this.app.get('/send-to-messenger', (req, res) => res.render('send-to-messenger', {
      appId: config.get('facebook.appId'),
      pageId: config.get('facebook.pageId')
    }));

    this.app.get('/', (req, res) => {
      res.send('👍');
    });

    this.app.get('/ping', (req, res) => {
      res.send('pong');
    });
  }

  start() {
    const port = config.get('port');
    this.emit('app.starting', {port});
    this.app.listen(port, (err) => {
      if (err) throw err;
      this.emit('app.started', {port});
      debug('Server running on port %s', port);
     // TODO console.log(`Set your webhook to: `)
    });
  }

  routeEachMessage(messagingEvent/*: Object */, pageId/*: string */)/*: Promise<Session> */ {
    const cacheKey = this.getCacheKey(messagingEvent.sender.id);
    return this.cache.get(cacheKey)
      // The cacheman-redis backend returns `null` instead of `undefined`
      .then((cacheResult) => cacheResult || undefined)
      .then((session/*: Session */ = {_key: cacheKey, _pageId: pageId, count: 0, profile: null}) => {
        // WISHLIST: logic to handle any thundering herd issues: https://en.wikipedia.org/wiki/Thundering_herd_problem
        if (session.profile) {
          return session;
        } else if (messagingEvent.sender.id === pageId) {
          // The page does not have a public profile and calling the Graph API here will always yield a 400.
          session.profile = {};
          return session;
        }

        return this.getPublicProfile(messagingEvent.sender.id, pageId)
          .then((profile) => {
            session.profile = profile;
            return session;
          });
      })
      .then((session) => {
        session.count++;
        if (session.source !== 'return' &&
            session.lastSeen &&<commit_msg>Log the webhook to ease first time setup (#58)<commit_after>// @flow weak
const EventEmitter = require('events');

const bodyParser = require('body-parser');
const Cacheman = require('cacheman');
const crypto = require('crypto');

const debug = require('debug')('messenger');
const logError = require('debug')('messenger:error');

const express = require('express');
const exphbs = require('express-handlebars');
const reqPromise = require('request-promise');
const urlJoin = require('url-join');

const config = require('./config');
const { ConversationLogger } = require('./conversationLogger');


const SESSION_TIMEOUT_MS = 3600 * 1000;  // 1 hour

const DEFAULT_GREETINGS_REGEX = /^(get started|good(morning|afternoon)|hello|hey|hi|hola|what's up)/i;
const DEFAULT_HELP_REGEX = /^help\b/i;

/*:: type Session = {_pageId: string|number, count: number, profile: ?Object} */

class Response {
  /*:: _messenger: Messenger */
  /*:: senderId: string|number */
  /*:: session: Session */
  constructor(messenger/*: Messenger */, options/*: Object */) {
    Object.assign(this, options);
    ['senderId', 'session'].forEach((required) => {
      // $FlowFixMe
      if (!this[required]) {
        throw new Error(`Incomplete Response, missing ${required}: ${JSON.stringify(options)}`);
      }
    });
    this._messenger = messenger;
    // $FlowFixMe
    this.reply = this.reply.bind(this);
  }

  reply(response) {
    return this._messenger.pageSend(this.session._pageId, this.senderId, response);
  }
}

class Messenger extends EventEmitter {
  /*:: app: Object */
  /*:: cache: Object */
  /*:: conversationLogger: Object */
  /*:: greetings: RegExp */
  /*:: help: RegExp */
  /*:: options: Object */
  /*:: pages: Object */
  constructor({
      hookPath = '/webhook',
      linkPath = '/link',
      emitGreetings = true,
      cache,
      pages = {}
    } = {}) {
    super();

    this.conversationLogger = new ConversationLogger(config);

    if (emitGreetings instanceof RegExp) {
      this.greetings = emitGreetings;
    } else {
      this.greetings = DEFAULT_GREETINGS_REGEX;
    }

    this.options = {
      emitGreetings: !!emitGreetings,
      hookPath,
      linkPath
    };

    if (cache) {
      this.cache = cache;
    } else {
      this.cache = new Cacheman('sessions', {ttl: SESSION_TIMEOUT_MS / 1000});
    }

    if (pages && Object.keys(pages).length) {
      this.pages = pages;
    } else if (config.has('messenger.pageAccessToken') && config.has('facebook.pageId')) {
      this.pages = {[config.get('facebook.pageId')]: config.get('messenger.pageAccessToken')};
    } else {
      this.pages = {};
      debug("MISSING options.pages; you won't be able to reply or get profile information");
    }

    this.app = express();
    this.app.engine('handlebars', exphbs({defaultLayout: 'main'}));
    this.app.set('view engine', 'handlebars');

    this.app.use(bodyParser.json({ verify: this.verifyRequestSignature.bind(this) }));
    this.app.use(bodyParser.urlencoded({ extended: true }));
    this.app.use(express.static('public'));

    this.help = DEFAULT_HELP_REGEX;

    // Facebook Messenger verification
    this.app.get(hookPath, (req, res) => {
      if (req.query['hub.mode'] === 'subscribe' &&
      req.query['hub.verify_token'] === config.get('messenger.validationToken')) {
        debug('Validating webhook');
        res.status(200).send(req.query['hub.challenge']);
      } else {
        debug('Failed validation. Make sure the validation tokens match.');
        res.sendStatus(403);
      }
    });

    this.app.post(hookPath, (req, res) => {
      const data = req.body;
      this.conversationLogger.logIncoming(data);
      // `data` reference:
      // https://developers.facebook.com/docs/messenger-platform/webhook-reference#format
      if (data.object === 'page') {
        data.entry.forEach((pageEntry) => {
          pageEntry.messaging.forEach((x) => this.routeEachMessage(x, pageEntry.id));
        });
        res.sendStatus(200);
      }
    });

    this.app.post(linkPath, (req, res) => {
      this.onLink(req.body);
      res.sendStatus(200);
    });

    // App routes

    // Stub routes for future functionality
    this.app.get('/login', (req, res) => res.render('login', {
      appId: config.get('facebook.appId'),
      serverUrl: config.get('serverUrl')
    }));
    this.app.get('/send-to-messenger', (req, res) => res.render('send-to-messenger', {
      appId: config.get('facebook.appId'),
      pageId: config.get('facebook.pageId')
    }));

    this.app.get('/', (req, res) => {
      res.send('👍');
    });

    this.app.get('/ping', (req, res) => {
      res.send('pong');
    });
  }

  start() {
    const port = config.get('port');
    this.emit('app.starting', {port});
    this.app.listen(port, (err) => {
      if (err) throw err;
      this.emit('app.started', {port});
      debug('Server running on: http://localhost:%s Set your webhook to: %s', port, urlJoin(config.get('serverUrl'), this.options.hookPath));
    });
  }

  routeEachMessage(messagingEvent/*: Object */, pageId/*: string */)/*: Promise<Session> */ {
    const cacheKey = this.getCacheKey(messagingEvent.sender.id);
    return this.cache.get(cacheKey)
      // The cacheman-redis backend returns `null` instead of `undefined`
      .then((cacheResult) => cacheResult || undefined)
      .then((session/*: Session */ = {_key: cacheKey, _pageId: pageId, count: 0, profile: null}) => {
        // WISHLIST: logic to handle any thundering herd issues: https://en.wikipedia.org/wiki/Thundering_herd_problem
        if (session.profile) {
          return session;
        } else if (messagingEvent.sender.id === pageId) {
          // The page does not have a public profile and calling the Graph API here will always yield a 400.
          session.profile = {};
          return session;
        }

        return this.getPublicProfile(messagingEvent.sender.id, pageId)
          .then((profile) => {
            session.profile = profile;
            return session;
          });
      })
      .then((session) => {
        session.count++;
        if (session.source !== 'return' &&
            session.lastSeen &&
{'fraction_non_alphanumeric': 0.16901523173413702, 'fraction_numerical': 0.004887187423637696, 'mean_word_length': 2.8990155604953953, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 35, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '163977', 'n_tokens_mistral': 3896, 'n_tokens_neox': 3756, 'n_words': 775}