code/local.js

"use strict";

/**
 * LocalService
 * @namespace LocalService
 * @description Manages local connected hooks, allowing device/server code execution
 * @example See ["hooks/examples/helloworld.js"]{@link hooks/examples.helloworld} for a basic local hook definition
 * @example See ["hooks/examples/beep.js"]{@link hooks/examples.beep} for a regex local hook definition
 * @example See ["hooks/examples/systat.js"]{@link hooks/examples.systat} for a system local hook definition
 * @example See ["hooks/examples/blink.js"]{@link hooks/examples.blink} for a GPIO local hook definition
 * @example See ["hooks/examples/mawkish.js"]{@link hooks/examples.mawkish} for a local hook with confirmation and custom keyboard
 */

const hooks = require('./hooks');
const config = require('./config');
const logger = require('./logger');
const path = require('path');
const _ = require('underscore');
const s = require("underscore.string");
const asyncP = require('async-promises');
_.mixin(s.exports());

/**
 * @property {TelegramService} api Link to TelegramService
 * @private
 * @memberof LocalService
 */

let api = null;
let hooks_dir = hooks.get_hooks_dir();

/**
 * @property {Boolean} initialized If initialized
 * @private
 * @memberof LocalService
 */

let initialized = false;

/**
 * @function manage_response
 * @description Check if call is authorized
 * @static
 * @param {Object} message Received message
 * @param {Object} hook_def Hook reference
 * @param {Error|String} error Contains Exception or error message
 * @param {String} output Contains output string to be sent to user
 * @param {Boolean} plain Disable markdown/html parse mode
 * @memberof LocalService
 * @private
 * @returns {Promise}
 */

let manage_response = function (message, hook_def, error, output, plain) {
  let error_msg = hook_def.error || "@error@";
  let response_msg = (_.isString(hook_def.response) ? hook_def.response : null) || "@response@";

  return new Promise(function (resolve, reject) {
    if (hook_def.response === false) {
      return resolve();
    }

    if (_.isFunction(hook_def.response)) {
      return hook_def.response(message, error, output).then(function () {
        output = output || "";
        if (!output) {
          return resolve();
        }
        api.respond(message, output, plain).then(resolve).catch(reject);
      }).catch(function (error) {
        api.respond(message, (error.message || error), plain).then(resolve).catch(reject);
      });
    } else {
      if (error) {
        if (hook_def.error !== false) {
          api.respond(message, error_msg.replace(/@error@/mi, (error.message || error)), plain).then(resolve).catch(reject);
        } else {
          reject(error.message || error);
        }
      } else {
        output = output || "";
        api.respond(message, response_msg.replace(/@response@/mi, output), plain).then(resolve).catch(reject);
      }
    }
  });
};

/**
 * @class
 * @classdesc Manages local connected hooks, allowing device/server code execution
 */

const LocalService = {
  /**
   * @function connect_hook
   * @description Connect local hook
   * @static
   * @param {Object} hook_def Hook reference
   * @memberof LocalService
   * @public
   * @returns {Promise}
   */
  connect_hook: function (hook_def) {
    return new Promise(function (resolve, reject) {

      hook_def.action_type = _.isString(hook_def.action) ? "string" : "function";
      if (config.get("gpio") !== false) {

        if (_.isArray(hook_def.signal)) {
          hook_def.action = function (message, service, matches) {
            return new Promise(function (resolve, reject) {
              try {
                const Gpio = require('onoff').Gpio;
                let gpios = _.uniq(_.pluck(hook_def.signal, "gpio"));
                let gpios_map = {};
                _.each(gpios, function (gpio) {
                  gpios_map[gpio] = new Gpio(gpio, 'out');
                });

                asyncP.each(hook_def.signal, function (gpio_el_list) {
                  return new Promise(function (resolve, reject) {
                    if (!_.isArray(gpio_el_list)) {
                      gpio_el_list = [gpio_el_list];
                    }
                    let promises_els = _.map(gpio_el_list, function (gpio_el) {
                      return new Promise(function (resolve, reject) {
                        gpios_map[gpio_el.gpio].write(((_.isNull(gpio_el.value) || _.isUndefined(gpio_el.value)) ? 1 : (!!gpio_el.value) * 1), function (err) {
                          if (err) {
                            return reject(err);
                          }
                          if (gpio_el.time) {
                            setTimeout(resolve, gpio_el.time);
                          } else {
                            resolve();
                          }
                        });
                      });
                    });
                    Promise.all(promises_els).then(resolve).catch(reject);
                  });
                }).then(function () {
                  _.each(gpios, function (gpio) {
                    gpios_map[gpio] = new Gpio(gpio, 'out');
                  });
                  resolve("");
                }).catch(reject);
              } catch (e) {
                return reject(e);
              }
            });
          };
        } else if (_.isFunction(hook_def.signal)) {
          hook_def.action = hook_def.signal;
        }
      }

      if (_.isString(hook_def.shell)) {
        let path_to_script = path.resolve(hooks_dir, path.dirname(hook_def.path), ".", hook_def.shell);
        hook_def.action = `"${path_to_script}"`;
      }

      if (_.isString(hook_def.action)) {
        hook_def._action = hook_def.action;

        let action_fn = _.bind(function (message, service, matches) {
          return new Promise(function (resolve, reject) {
            const exec = require('child_process').exec;
            matches = matches || [];
            let result_command = hook_def._action;

            for (let i = 0; i < matches.length; i++) {
              let placeholder = new RegExp(`@${i}@`, 'mgi');
              result_command = result_command.replace(placeholder, matches[i]);
            }

            let options = {};

            if (config.get('shell')) {
              options.shell = config.get('shell');
            }

            let child = exec(result_command, options, function (error, stdout, stderr) {

              let error_msg = hook_def.error || "Error: @error@";
              let has_error = false;

              if (error) return reject(error, null);

              let stderr_str = stderr.toString('utf8');
              let stdout_str = stdout.toString('utf8');

              if (stderr_str) return reject(new Error(stderr_str));

              if (hook_def.check) {
                if (_.isString(hook_def.check)) {
                  has_error = hook_def.check.toLowerCase() != stdout_str.toLowerCase();
                } else if (_.isFunction(hook_def.check)) {
                  has_error = !hook_def.check(message, stdout_str);
                } else if (_.isRegExp(hook_def.check)) {
                  has_error = !hook_def.check.test(stdout_str);
                }
              }

              if (has_error) {
                reject(new Error(stdout_str));
              } else if (hook_def.response !== false) {
                resolve(stdout_str);
              }

            });
          });
        }, hook_def);

        hook_def.action = action_fn;
      }
      if (!_.isFunction(hook_def.action) && _.isFunction(hook_def.parse_response)) {
        hook_def.action = function () {
          return Promise.resolve();
        };
      }

      if (_.isFunction(hook_def.action)) {
        let _action = _.bind(hook_def.action, hook_def);

        hook_def.action = _.bind(function (message, service, matches) {
          return new Promise(function (resolve, reject) {
            if (hook_def.confirmation || hook_def.buttons) {

              let confirm_message = _.isString(hook_def.confirmation) ? hook_def.confirmation : "Are you sure?";

              let buttons = hook_def.buttons || (_.isBoolean(hook_def.confirmation) ? [
                ["Yes", "No"]
              ] : true);

              let parse_response = _.isFunction(hook_def.parse_response) ? function (response_message) {
                hook_def.parse_response(message, response_message, api).then(resolve).catch(reject);
              } : function (response_message) {
                let response_text = response_message.text.toString().toLowerCase();
                if (response_text == "yes") {
                  return api.respond(response_message, (hook_def.continue || "Ok, executing action...")).then(function () {
                    return _action(message, service, matches).then(function (output, plain) {
                      manage_response(message, hook_def, null, output, (hook_def.plain || plain)).then(resolve).catch(reject);
                    }).catch(function (error) {
                      manage_response(message, hook_def, error, null, hook_def.plain).then(resolve).catch(reject);
                    });
                  }).catch(reject);
                } else {
                  manage_response(message, hook_def, (hook_def.abort || "Oh, nevermind..."), null, hook_def.plain).then(resolve).catch(reject);
                }
              };
              return api.send(confirm_message, buttons, (hook_def.accepted_responses || true), hook_def.one_time_keyboard, hook_def.plain).then(parse_response).catch(reject);
            } else {
              return _action(message, service, matches).then(function (output, plain) {
                manage_response(message, hook_def, null, output, (hook_def.plain || plain)).then(resolve).catch(reject);
              }).catch(function (error) {
                manage_response(message, hook_def, error, null, hook_def.plain).then(resolve).catch(reject);
              });
            }
          });
        }, hook_def);
      }

      return api.register_message_hook(hook_def).then(function(){
        logger.notify(`Registered "${hook_def.full_name}" local hook with ${hook_def.command || hook_def.match} ${hook_def.command ? "command" : "match"}`);
        resolve();
      }).catch(reject);
    });
  },
  /**
   * @function init
   * @description Initialize local hooks manager
   * @static
   * @param {TelegramService} tapi Link to Telegram service
   * @memberof LocalService
   * @public
   * @returns {Promise}
   */
  init: function (tapi) {
    api = tapi;

    return new Promise(function (resolve, reject) {

      hooks.load().then(function () {

        if (config.get("local:active") == false) {
          return resolve(api);
        }

        var lo_hooks = hooks.get_hooks("has_local_hook", "full_name");
        var promises = [];

        for (let monitor_name in lo_hooks) {
          let hook = lo_hooks[monitor_name];
          promises.push(LocalService.connect_hook(hook));
        }

        if (promises.length) {
          Promise.all(promises).then(function () {
            initialized = true;
            resolve(api);
          }).catch(reject);
        } else {
          process.nextTick(function () {
            initialized = true;
            resolve(api);
          });
        }
      }).catch(reject);
    });
  }
}

module.exports = LocalService;