silentrob/superscript

View on GitHub
src/bot/db/models/gambit.js

Summary

Maintainability
C
1 day
Test Coverage
/**
  A Gambit is a Trigger + Reply or Reply Set
  - We define a Reply as a subDocument in Mongo.
**/

import mongoose from 'mongoose';
import mongoTenant from 'mongo-tenant';
import debuglog from 'debug-levels';
import async from 'async';
import parser from 'ss-parser';

import modelNames from '../modelNames';
import Utils from '../../utils';

const debug = debuglog('SS:Gambit');

/**
  A trigger is the matching rule behind a piece of input. It lives in a topic or several topics.
  A trigger also contains one or more replies.
**/

const createGambitModel = function createGambitModel(db, factSystem) {
  const gambitSchema = new mongoose.Schema({
    id: { type: String, index: true, default: Utils.genId() },

    // This is the input string that generates a rule,
    // In the event we want to export this, we will use this value.
    // Make this filed conditionally required if trigger is supplied
    input: { type: String },

    // The Trigger is a partly baked regex.
    trigger: { type: String },

    // If the trigger is a Question Match
    isQuestion: { type: Boolean, default: false },

    // If this gambit is nested inside a conditional block
    conditions: [{ type: String, default: '' }],

    // The filter function for the the expression
    filter: { type: String, default: '' },

    // An array of replies.
    replies: [{ type: String, ref: modelNames.reply }],

    // How we choose gambits can be `random` or `ordered`
    reply_order: { type: String, default: 'random' },

    // How we handle the reply exhaustion can be `keep` or `exhaust`
    reply_exhaustion: { type: String },

    // Save a reference to the parent Reply, so we can walk back up the tree
    parent: { type: String, ref: modelNames.reply },

    // This will redirect anything that matches elsewhere.
    // If you want to have a conditional rediect use reply redirects
    // TODO, change the type to a ID and reference another gambit directly
    // this will save us a lookup down the road (and improve performace.)
    redirect: { type: String, default: '' },
  });

  gambitSchema.pre('save', function (next) {
    // FIXME: This only works when the replies are populated which is not always the case.
    // this.replies = _.uniq(this.replies, (item, key, id) => {
    //   return item.id;
    // });

    // If we created the trigger in an external editor, normalize the trigger before saving it.
    if (this.input && !this.trigger) {
      const facts = factSystem.getFactSystem(this.getTenantId());
      return parser.normalizeTrigger(this.input, facts, (err, cleanTrigger) => {
        this.trigger = cleanTrigger;
        next();
      });
    }
    next();
  });

  gambitSchema.methods.addReply = function (replyData, callback) {
    if (!replyData) {
      return callback('No data');
    }

    const Reply = db.model(modelNames.reply).byTenant(this.getTenantId());
    const reply = new Reply(replyData);
    reply.save((err) => {
      if (err) {
        return callback(err);
      }
      this.replies.addToSet(reply._id);
      this.save((err) => {
        callback(err, reply);
      });
    });
  };

  gambitSchema.methods.clearReplies = function (callback) {
    const self = this;

    const clearReply = function (replyId, cb) {
      self.replies.pull({ _id: replyId });
      db.model(modelNames.reply).byTenant(this.getTenantId()).remove({ _id: replyId }, (err) => {
        if (err) {
          console.log(err);
        }

        debug.verbose('removed reply %s', replyId);

        cb(null, replyId);
      });
    };

    async.map(self.replies, clearReply, (err, clearedReplies) => {
      self.save((err2) => {
        callback(err2, clearedReplies);
      });
    });
  };

  gambitSchema.plugin(mongoTenant);

  return db.model('ss_gambit', gambitSchema);
};

export default createGambitModel;