<commit_before>#!/usr/bin/env node

var ROSLIB = require('roslib');
var fs = require('fs');

//http://mlg.eng.cam.ac.uk/yarin/blog_3d801aa532c1ce.html
//GLOBAL.convnetjs   = require('./vendor/uncertain/convnet.js');
//GLOBAL.deepqlearn  = require('./vendor/uncertain/deepqlearn.js');

// Global to bring into scope for ConvNetJS.
//GLOBAL.convnetjs   = require('./vendor/convnet.js'); // Not included in repo.
GLOBAL.convnetjs   = require('../bower_components/convnetjs/build/convnet.js'); // Compiled with `cd bower_components/convnetjs/compile && ant -lib yuicompressor-2.4.8.jar -f build.xml`
GLOBAL.deepqlearn  = require('../bower_components/convnetjs/build/deepqlearn.js');
GLOBAL.cnnutil     = require('../bower_components/convnetjs/build/util.js');

var config = require('./config.js');
var Ros    = require('./ros.js');
var Robot  = require('./robot.js');
var Rat    = require('./rat.js');
var Agent  = require('./agent.js');
var Utils  = require('./utils.js');

var ros = new Ros();
var rob = new Robot(ros, '/kulbu', 'diff_drive_controller/cmd_vel');
var rat = new Rat(ros);

/**
 * Bespoke function to initialise eye positions for specific robot.
 * @function
 * @return {array}
 */
var initEyes = function() {
  var res = [];
  var small_fov = (15*Math.PI/180);
  // Two large rear eyes from remainder of circle.
  var large_fov = ((2*Math.PI) - (6*small_fov))/2;

  var rad = ((0-3)*small_fov) - (large_fov/2);
  res.push({
    name:  config.eye_names[0],
    angle: rad,
    fov:   large_fov
  });
  for (var k=0; k<7; k++) {
    rad = ((k-3)*small_fov);
    res.push({
      name:  config.eye_names[k+1],
      angle: rad,
      fov:   small_fov
    });
  }
  rad = rad + (large_fov/2);
  res.push({
    name:  config.eye_names[8],
    angle: rad,
    fov:   large_fov
  });

  return res;
};

//console.log('Config:', config);

/**
 * Initialise Agent
 */
var agt = new Agent(
  ros,
  initEyes(),
  config.actions,
  config.agent_opts,
  config.brain_opts
);

/**
 * Overload `random_action` to randomly turn on the spot.
 * Simulated robot with wheels out front not as slippery as original demo.
 * @function
 * @return {integer}
 */
var repeat_cnt = 0;
var repeat_act = 0;
agt.brain.random_action_legacy = agt.brain.random_action;
agt.brain.random_action = function() {
    // Going badly for a long time, or at start of training.
    // Robot physics make getting unstuck harder than original, give it a hand.
    if (agt.brain.average_reward_window.get_average() < 0.6 || agt.brain.epsilon > 0.3) {
        var seed = convnetjs.randi(0, 150);
        if (seed < 1) {
            repeat_act = convnetjs.randi(3, 5);
            repeat_cnt = 30;
            console.log('Repeat: ' + repeat_act);
        }
        if (repeat_cnt > 0) {
            repeat_cnt--;
            return repeat_act;
        }
    }
    return agt.brain.random_action_legacy();
};

/**
 * Inform agent of range sensors from ROS
 * @callback getRange
 * @param {object} message
 */
var getRange = function(message) {
  var frameId = message.header.frame_id;
  var topicName = frameId.replace('_link', '');
  //console.log('getRange', message.range, config.eye_names.indexOf(topicName));
  agt.eyes[config.eye_names.indexOf(topicName)].sensed_proximity = message.range;
  agt.eyes[config.eye_names.indexOf(topicName)].sensed_type = 0;
  agt.eyes[config.eye_names.indexOf(topicName)].updated = true;
};

/**
 * Inform agent of sub goals from RatSLAM
 * @callback getGoal
 * @param {object} message
 */
var getGoal = function(message) {
  // Find matching eye.
  var num_eyes = agt.eyes.length;
  for (var i=0; i<num_eyes; i++) {
    var e = agt.eyes[i];
    var min = e.angle - (e.fov/2);
    var max = e.angle + (e.fov/2);

    if (
      message.dis > 0 &&
      // Within FoV of an eye;
      message.rad > min &&
      message.rad < max
      /* &&
      // Closer than wall;
        (
          e.sensed_type !== 0 ||
          (
            e.sensed_type === 0 &&
            e.sensed_proximity < e.max_range &&
            message.dis < e.sensed_proximity
          )
        )
      */
      ) {
      // TODO: Publish goal sight direction for debugging.
      //console.log('ang', i, e.angle, message.dis);
      e.sensed_goal = Math.min(e.goal_range, message.dis); // limit to within `goal_range`.

    // Force sensed type reset, allow `sensed_proximity` to be set by Sonar.
    // for signal moving away.
    // for sitting on the goal `dis == 0`, move around a little.
    } else {
      e.sensed_goal = e.goal_range;
    }
  }

  // Record for rewarding later.
  agt.addGoal(message.id, message.dis, message.rad);
};

/**
 * Create goals each time RatSLAM map updates
 * @callback getMap
 * @param {object} message
 */
var getMap = function(message) {
  var past_exp = message.node[message.node.length - config.goal_distance];
  if (past_exp && past_exp.pose && past_exp.pose.position && agt.brain.epsilon < 0.5 && message.node.length < 50000) {
    rat.createGoal(past_exp.pose.position.x, past_exp.pose.position.y);
  }
};

// Subscribe to each range sensors ROS topic.
for (var i=0; i<config.eye_names.length; i++) {
  // FIXME: Skip for goal only sensors.
  rob.subRange(config.eye_names[i], getRange);
}

// Subscribe to RatSLAM `SubGoal`.
rat.subGoal(getGoal);

// Subscribe to RatSLAM `Map`.
rat.subMap(getMap);

var utils = new Utils(ros, '/dqn', agt);

/**
 * Main loop.
 */
agt.cnt = 0; // Counter for limiting logs to repeating events.
var actionix = 0;

var timer_cnt = 0;
var timer_time = 0;
var tick = function() {
  if (!utils.dqn_paused) {
    var time_start = new Date().getTime();

    // Foward
    agt.forward();

    // Mark sensors as not updated.
    for (var i=0; i<agt.eyes[i].length; i++) {
      agt.eyes[i].updated = false;
    }

    // Execute the move.
    rob.doMove(agt.linX, agt.angZ);

    // Keep track of how many repeats of same command.
    if (actionix === agt.actionix) {
      agt.cnt++;
    } else {
      agt.cnt = 0;
    }
  }

  // Give the state a chance to change.
  var timer = setInterval(function() {
    // Wait for an update from all sonar sensors.
    var updated = 0;
    for (var j=0; j<config.eye_names.length; j++) {
      if (config.eye_names[j].indexOf('range') !== -1 && agt.eyes[j].updated) {
        updated++;
      }
    }

    // TODO: Wait for update to sub_goal.
    //console.log('updated', updated);

    if (updated >= 7) {
      clearInterval(timer);

      // agents like to look at goals, especially up close
      // overruling the reward from running through them
      if (agt.eyes[config.eye_names.indexOf('range_0')].sensed_goal < agt.eyes[config.eye_names.indexOf('range_0')].goal_range) {
        // Inversely proportional to the square of the distance.
        agt.digestion_signal += 0.05 * (1/Math.pow(agt.eyes[config.eye_names.indexOf('range_0')].sensed_goal, 2)) * proximity_reward;
        console.log('digest goal', agt.eyes[config.eye_names.indexOf('range_0')].sensed_goal, agt.digestion_signal);
      }

      // Backward
      agt.backward();
      actionix = agt.actionix;

      if (timer_time > 5000) {
        console.log('fps:' + (timer_cnt/(timer_time/1000)).toFixed(1));
        timer_time = 0;
        timer_cnt = 0;
      }
      var time_end = new Date().getTime();
      timer_time += time_end - time_start;
      timer_cnt++;

      tick();
    }
  }, (1000/config.main_loop)); // Hz, Faster than sonars but checking for their updates.
};
tick();
<commit_msg>Proximity reward not in scope<commit_after>#!/usr/bin/env node

var ROSLIB = require('roslib');
var fs = require('fs');

//http://mlg.eng.cam.ac.uk/yarin/blog_3d801aa532c1ce.html
//GLOBAL.convnetjs   = require('./vendor/uncertain/convnet.js');
//GLOBAL.deepqlearn  = require('./vendor/uncertain/deepqlearn.js');

// Global to bring into scope for ConvNetJS.
//GLOBAL.convnetjs   = require('./vendor/convnet.js'); // Not included in repo.
GLOBAL.convnetjs   = require('../bower_components/convnetjs/build/convnet.js'); // Compiled with `cd bower_components/convnetjs/compile && ant -lib yuicompressor-2.4.8.jar -f build.xml`
GLOBAL.deepqlearn  = require('../bower_components/convnetjs/build/deepqlearn.js');
GLOBAL.cnnutil     = require('../bower_components/convnetjs/build/util.js');

var config = require('./config.js');
var Ros    = require('./ros.js');
var Robot  = require('./robot.js');
var Rat    = require('./rat.js');
var Agent  = require('./agent.js');
var Utils  = require('./utils.js');

var ros = new Ros();
var rob = new Robot(ros, '/kulbu', 'diff_drive_controller/cmd_vel');
var rat = new Rat(ros);

/**
 * Bespoke function to initialise eye positions for specific robot.
 * @function
 * @return {array}
 */
var initEyes = function() {
  var res = [];
  var small_fov = (15*Math.PI/180);
  // Two large rear eyes from remainder of circle.
  var large_fov = ((2*Math.PI) - (6*small_fov))/2;

  var rad = ((0-3)*small_fov) - (large_fov/2);
  res.push({
    name:  config.eye_names[0],
    angle: rad,
    fov:   large_fov
  });
  for (var k=0; k<7; k++) {
    rad = ((k-3)*small_fov);
    res.push({
      name:  config.eye_names[k+1],
      angle: rad,
      fov:   small_fov
    });
  }
  rad = rad + (large_fov/2);
  res.push({
    name:  config.eye_names[8],
    angle: rad,
    fov:   large_fov
  });

  return res;
};

//console.log('Config:', config);

/**
 * Initialise Agent
 */
var agt = new Agent(
  ros,
  initEyes(),
  config.actions,
  config.agent_opts,
  config.brain_opts
);

/**
 * Overload `random_action` to randomly turn on the spot.
 * Simulated robot with wheels out front not as slippery as original demo.
 * @function
 * @return {integer}
 */
var repeat_cnt = 0;
var repeat_act = 0;
agt.brain.random_action_legacy = agt.brain.random_action;
agt.brain.random_action = function() {
    // Going badly for a long time, or at start of training.
    // Robot physics make getting unstuck harder than original, give it a hand.
    if (agt.brain.average_reward_window.get_average() < 0.6 || agt.brain.epsilon > 0.3) {
        var seed = convnetjs.randi(0, 150);
        if (seed < 1) {
            repeat_act = convnetjs.randi(3, 5);
            repeat_cnt = 30;
            console.log('Repeat: ' + repeat_act);
        }
        if (repeat_cnt > 0) {
            repeat_cnt--;
            return repeat_act;
        }
    }
    return agt.brain.random_action_legacy();
};

/**
 * Inform agent of range sensors from ROS
 * @callback getRange
 * @param {object} message
 */
var getRange = function(message) {
  var frameId = message.header.frame_id;
  var topicName = frameId.replace('_link', '');
  //console.log('getRange', message.range, config.eye_names.indexOf(topicName));
  agt.eyes[config.eye_names.indexOf(topicName)].sensed_proximity = message.range;
  agt.eyes[config.eye_names.indexOf(topicName)].sensed_type = 0;
  agt.eyes[config.eye_names.indexOf(topicName)].updated = true;
};

/**
 * Inform agent of sub goals from RatSLAM
 * @callback getGoal
 * @param {object} message
 */
var getGoal = function(message) {
  // Find matching eye.
  var num_eyes = agt.eyes.length;
  for (var i=0; i<num_eyes; i++) {
    var e = agt.eyes[i];
    var min = e.angle - (e.fov/2);
    var max = e.angle + (e.fov/2);

    if (
      message.dis > 0 &&
      // Within FoV of an eye;
      message.rad > min &&
      message.rad < max
      /* &&
      // Closer than wall;
        (
          e.sensed_type !== 0 ||
          (
            e.sensed_type === 0 &&
            e.sensed_proximity < e.max_range &&
            message.dis < e.sensed_proximity
          )
        )
      */
      ) {
      // TODO: Publish goal sight direction for debugging.
      //console.log('ang', i, e.angle, message.dis);
      e.sensed_goal = Math.min(e.goal_range, message.dis); // limit to within `goal_range`.

    // Force sensed type reset, allow `sensed_proximity` to be set by Sonar.
    // for signal moving away.
    // for sitting on the goal `dis == 0`, move around a little.
    } else {
      e.sensed_goal = e.goal_range;
    }
  }

  // Record for rewarding later.
  agt.addGoal(message.id, message.dis, message.rad);
};

/**
 * Create goals each time RatSLAM map updates
 * @callback getMap
 * @param {object} message
 */
var getMap = function(message) {
  var past_exp = message.node[message.node.length - config.goal_distance];
  if (past_exp && past_exp.pose && past_exp.pose.position && agt.brain.epsilon < 0.5 && message.node.length < 50000) {
    rat.createGoal(past_exp.pose.position.x, past_exp.pose.position.y);
  }
};

// Subscribe to each range sensors ROS topic.
for (var i=0; i<config.eye_names.length; i++) {
  // FIXME: Skip for goal only sensors.
  rob.subRange(config.eye_names[i], getRange);
}

// Subscribe to RatSLAM `SubGoal`.
rat.subGoal(getGoal);

// Subscribe to RatSLAM `Map`.
rat.subMap(getMap);

var utils = new Utils(ros, '/dqn', agt);

/**
 * Main loop.
 */
agt.cnt = 0; // Counter for limiting logs to repeating events.
var actionix = 0;

var timer_cnt = 0;
var timer_time = 0;
var tick = function() {
  if (!utils.dqn_paused) {
    var time_start = new Date().getTime();

    // Foward
    agt.forward();

    // Mark sensors as not updated.
    for (var i=0; i<agt.eyes[i].length; i++) {
      agt.eyes[i].updated = false;
    }

    // Execute the move.
    rob.doMove(agt.linX, agt.angZ);

    // Keep track of how many repeats of same command.
    if (actionix === agt.actionix) {
      agt.cnt++;
    } else {
      agt.cnt = 0;
    }
  }

  // Give the state a chance to change.
  var timer = setInterval(function() {
    // Wait for an update from all sonar sensors.
    var updated = 0;
    for (var j=0; j<config.eye_names.length; j++) {
      if (config.eye_names[j].indexOf('range') !== -1 && agt.eyes[j].updated) {
        updated++;
      }
    }

    // TODO: Wait for update to sub_goal.
    //console.log('updated', updated);

    if (updated >= 7) {
      clearInterval(timer);

      // agents like to look at goals, especially up close
      // overruling the reward from running through them
      if (agt.eyes[config.eye_names.indexOf('range_0')].sensed_goal < agt.eyes[config.eye_names.indexOf('range_0')].goal_range) {
        // Inversely proportional to the square of the distance.
        // TODO: Need to restrict against walls? As agent can see goals through them. `* proximity_reward;``
        agt.digestion_signal += 0.05 * (1/Math.pow(agt.eyes[config.eye_names.indexOf('range_0')].sensed_goal, 2));
        console.log('digest goal', agt.eyes[config.eye_names.indexOf('range_0')].sensed_goal, agt.digestion_signal);
      }

      // Backward
      agt.backward();
      actionix = agt.actionix;

      if (timer_time > 5000) {
        console.log('fps:' + (timer_cnt/(timer_time/1000)).toFixed(1));
        timer_time = 0;
        timer_cnt = 0;
      }
      var time_end = new Date().getTime();
      timer_time += time_end - time_start;
      timer_cnt++;

      tick();
    }
  }, (1000/config.main_loop)); // Hz, Faster than sonars but checking for their updates.
};
tick();
<|endoftext|>


<commit_before>const Eris = require('eris');
const moment = require('moment');

const config = require('./config');
const bot = require('./bot');
const Queue = require('./queue');
const utils = require('./utils');
const threadUtils = require('./threadUtils');
const blocked = require('./data/blocked');
const threads = require('./data/threads');

const snippets = require('./plugins/snippets');
const webserver = require('./plugins/webserver');
const greeting = require('./plugins/greeting');
const attachments = require("./data/attachments");

const messageQueue = new Queue();

const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);

// Once the bot has connected, set the status/"playing" message
bot.on('ready', () => {
  bot.editStatus(null, {name: config.status});
});

/**
 * When a moderator posts in a modmail thread...
 * 1) If alwaysReply is enabled, reply to the user
 * 2) If alwaysReply is disabled, save that message as a chat message in the thread
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg)) return;
  if (msg.author.bot) return;
  if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return;

  const thread = await threads.findByChannelId(msg.channel.id);
  if (! thread) return;

  if (config.alwaysReply) {
    // AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
    if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
    await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false);
    msg.delete();
  } else {
    // Otherwise just save the messages as "chat" in the logs
    thread.saveChatMessage(msg);
  }
});

/**
 * When we get a private message...
 * 1) Find the open modmail thread for this user, or create a new one
 * 2) Post the message as a user reply in the thread
 */
bot.on('messageCreate', async msg => {
  if (! (msg.channel instanceof Eris.PrivateChannel)) return;
  if (msg.author.bot) return;
  if (msg.type !== 0) return; // Ignore pins etc.

  if (await blocked.isBlocked(msg.author.id)) return;

  // Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
  messageQueue.add(async () => {
    const thread = await threads.findOrCreateThreadForUser(msg.author);
    await thread.receiveUserReply(msg);
  });
});

/**
 * When a message is edited...
 * 1) If that message was in DMs, and we have a thread open with that user, post the edit as a system message in the thread
 * 2) If that message was moderator chatter in the thread, update the corresponding chat message in the DB
 */
bot.on('messageUpdate', async (msg, oldMessage) => {
  if (msg.author.bot) return;
  if (await blocked.isBlocked(msg.author.id)) return;

  let oldContent = oldMessage.content;
  const newContent = msg.content;

  // Old message content doesn't persist between bot restarts
  if (oldContent == null) oldContent = '*Unavailable due to bot restart*';

  // Ignore bogus edit events with no changes
  if (newContent.trim() === oldContent.trim()) return;

  // 1) Edit in DMs
  if (msg.channel instanceof Eris.PrivateChannel) {
    const thread = await threads.findOpenThreadByUserId(msg.author.id);
    const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);

    thread.postSystemMessage(editMessage);
  }

  // 2) Edit in the thread
  else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) {
    const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
    if (! thread) return;

    thread.updateChatMessage(msg);
  }
});

/**
 * When a staff message is deleted in a modmail thread, delete it from the database as well
 */
bot.on('messageDelete', async msg => {
  if (msg.author.bot) return;
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg.member)) return;

  const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
  if (! thread) return;

  thread.deleteChatMessage(msg.id);
});

/**
 * When the bot is mentioned on the main server, ping staff in the log channel about it
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnMainServer(msg)) return;
  if (! msg.mentions.some(user => user.id === bot.user.id)) return;

  // If the person who mentioned the modmail bot is also on the modmail server, ignore them
  if (utils.getInboxGuild().members.get(msg.author.id)) return;

  // If the person who mentioned the bot is blocked, ignore them
  if (await blocked.isBlocked(msg.author.id)) return;

  bot.createMessage(utils.getLogChannel(bot).id, {
    content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`,
    disableEveryone: false,
  });
});

// Mods can reply to modmail threads using !r or !reply
// These messages get relayed back to the DM thread between the bot and the user
addInboxServerCommand('reply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, false);
  msg.delete();
});

bot.registerCommandAlias('r', 'reply');

// Anonymous replies only show the role, not the username
addInboxServerCommand('anonreply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, true);
  msg.delete();
});

bot.registerCommandAlias('ar', 'anonreply');

// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
addInboxServerCommand('close', async (msg, args, thread) => {
  if (! thread) return;
  thread.close();
});

addInboxServerCommand('block', (msg, args, thread) => {
  async function block(userId) {
    await blocked.block(userId);
    msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    block(userId);
  } else if (thread) {
    // Calling !block without args in a modmail thread blocks the user of that thread
    block(thread.user_id);
  }
});

addInboxServerCommand('unblock', (msg, args, thread) => {
  async function unblock(userId) {
    await blocked.unblock(userId);
    msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    unblock(userId);
  } else if (thread) {
    // Calling !unblock without args in a modmail thread unblocks the user of that thread
    unblock(thread.user_id);
  }
});

addInboxServerCommand('logs', (msg, args, thread) => {
  async function getLogs(userId) {
    const userThreads = await threads.getClosedThreadsByUserId(userId);
    const threadLines = await Promise.all(userThreads.map(async thread => {
      const logUrl = await thread.getLogUrl();
      const formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]');
      return `\`${formattedDate}\`: <${logUrl}>`;
    }));

    const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`;

    // Send the list of logs in chunks of 15 lines per message
    const lines = message.split('\n');
    const chunks = utils.chunk(lines, 15);

    let root = Promise.resolve();
    chunks.forEach(lines => {
      root = root.then(() => msg.channel.createMessage(lines.join('\n')));
    });
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    getLogs(userId);
  } else if (thread) {
    // Calling !logs without args in a modmail thread returns the logs of the user of that thread
    getLogs(thread.user_id);
  }
});

module.exports = {
  async start() {
    // Load plugins
    console.log('Loading plugins...');
    await snippets(bot);
    await greeting(bot);
    await webserver(bot);

    console.log('Connecting to Discord...');
    await bot.connect();

    console.log('Done! Now listening to DMs.');
  }
};
<commit_msg>Show logs in reverse order again<commit_after>const Eris = require('eris');
const moment = require('moment');

const config = require('./config');
const bot = require('./bot');
const Queue = require('./queue');
const utils = require('./utils');
const threadUtils = require('./threadUtils');
const blocked = require('./data/blocked');
const threads = require('./data/threads');

const snippets = require('./plugins/snippets');
const webserver = require('./plugins/webserver');
const greeting = require('./plugins/greeting');
const attachments = require("./data/attachments");

const messageQueue = new Queue();

const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);

// Once the bot has connected, set the status/"playing" message
bot.on('ready', () => {
  bot.editStatus(null, {name: config.status});
});

/**
 * When a moderator posts in a modmail thread...
 * 1) If alwaysReply is enabled, reply to the user
 * 2) If alwaysReply is disabled, save that message as a chat message in the thread
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg)) return;
  if (msg.author.bot) return;
  if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return;

  const thread = await threads.findByChannelId(msg.channel.id);
  if (! thread) return;

  if (config.alwaysReply) {
    // AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
    if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
    await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false);
    msg.delete();
  } else {
    // Otherwise just save the messages as "chat" in the logs
    thread.saveChatMessage(msg);
  }
});

/**
 * When we get a private message...
 * 1) Find the open modmail thread for this user, or create a new one
 * 2) Post the message as a user reply in the thread
 */
bot.on('messageCreate', async msg => {
  if (! (msg.channel instanceof Eris.PrivateChannel)) return;
  if (msg.author.bot) return;
  if (msg.type !== 0) return; // Ignore pins etc.

  if (await blocked.isBlocked(msg.author.id)) return;

  // Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
  messageQueue.add(async () => {
    const thread = await threads.findOrCreateThreadForUser(msg.author);
    await thread.receiveUserReply(msg);
  });
});

/**
 * When a message is edited...
 * 1) If that message was in DMs, and we have a thread open with that user, post the edit as a system message in the thread
 * 2) If that message was moderator chatter in the thread, update the corresponding chat message in the DB
 */
bot.on('messageUpdate', async (msg, oldMessage) => {
  if (msg.author.bot) return;
  if (await blocked.isBlocked(msg.author.id)) return;

  let oldContent = oldMessage.content;
  const newContent = msg.content;

  // Old message content doesn't persist between bot restarts
  if (oldContent == null) oldContent = '*Unavailable due to bot restart*';

  // Ignore bogus edit events with no changes
  if (newContent.trim() === oldContent.trim()) return;

  // 1) Edit in DMs
  if (msg.channel instanceof Eris.PrivateChannel) {
    const thread = await threads.findOpenThreadByUserId(msg.author.id);
    const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);

    thread.postSystemMessage(editMessage);
  }

  // 2) Edit in the thread
  else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) {
    const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
    if (! thread) return;

    thread.updateChatMessage(msg);
  }
});

/**
 * When a staff message is deleted in a modmail thread, delete it from the database as well
 */
bot.on('messageDelete', async msg => {
  if (msg.author.bot) return;
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg.member)) return;

  const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
  if (! thread) return;

  thread.deleteChatMessage(msg.id);
});

/**
 * When the bot is mentioned on the main server, ping staff in the log channel about it
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnMainServer(msg)) return;
  if (! msg.mentions.some(user => user.id === bot.user.id)) return;

  // If the person who mentioned the modmail bot is also on the modmail server, ignore them
  if (utils.getInboxGuild().members.get(msg.author.id)) return;

  // If the person who mentioned the bot is blocked, ignore them
  if (await blocked.isBlocked(msg.author.id)) return;

  bot.createMessage(utils.getLogChannel(bot).id, {
    content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`,
    disableEveryone: false,
  });
});

// Mods can reply to modmail threads using !r or !reply
// These messages get relayed back to the DM thread between the bot and the user
addInboxServerCommand('reply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, false);
  msg.delete();
});

bot.registerCommandAlias('r', 'reply');

// Anonymous replies only show the role, not the username
addInboxServerCommand('anonreply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, true);
  msg.delete();
});

bot.registerCommandAlias('ar', 'anonreply');

// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
addInboxServerCommand('close', async (msg, args, thread) => {
  if (! thread) return;
  thread.close();
});

addInboxServerCommand('block', (msg, args, thread) => {
  async function block(userId) {
    await blocked.block(userId);
    msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    block(userId);
  } else if (thread) {
    // Calling !block without args in a modmail thread blocks the user of that thread
    block(thread.user_id);
  }
});

addInboxServerCommand('unblock', (msg, args, thread) => {
  async function unblock(userId) {
    await blocked.unblock(userId);
    msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    unblock(userId);
  } else if (thread) {
    // Calling !unblock without args in a modmail thread unblocks the user of that thread
    unblock(thread.user_id);
  }
});

addInboxServerCommand('logs', (msg, args, thread) => {
  async function getLogs(userId) {
    const userThreads = await threads.getClosedThreadsByUserId(userId);
    userThreads.reverse();

    const threadLines = await Promise.all(userThreads.map(async thread => {
      const logUrl = await thread.getLogUrl();
      const formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]');
      return `\`${formattedDate}\`: <${logUrl}>`;
    }));

    const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`;

    // Send the list of logs in chunks of 15 lines per message
    const lines = message.split('\n');
    const chunks = utils.chunk(lines, 15);

    let root = Promise.resolve();
    chunks.forEach(lines => {
      root = root.then(() => msg.channel.createMessage(lines.join('\n')));
    });
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    getLogs(userId);
  } else if (thread) {
    // Calling !logs without args in a modmail thread returns the logs of the user of that thread
    getLogs(thread.user_id);
  }
});

module.exports = {
  async start() {
    // Load plugins
    console.log('Loading plugins...');
    await snippets(bot);
    await greeting(bot);
    await webserver(bot);

    console.log('Connecting to Discord...');
    await bot.connect();

    console.log('Done! Now listening to DMs.');
  }
};
<|endoftext|>


<commit_before>require.config({
  baseUrl : '../',
  paths: {
    // Editor core
    'editor': './src/editor',

    // Editor modules
    'scribe-wrapper': './src/modules/lib/scribe-wrapper/scribe-wrapper',
    'source-converter': './src/modules/lib/source-converter/source-converter',

    // Editor plugins
    'editor-plugin-sticky-toolbar': './src/modules/plugins/editor-sticky-toolbar/editor-sticky-toolbar',

    // Scribe
    'he': './bower_components/he/he',
    'scribe': './bower_components/scribe/scribe',
    'scribe-plugin-blockquote-command': './bower_components/scribe-plugin-blockquote-command/scribe-plugin-blockquote-command',
    'scribe-plugin-formatter-plain-text-convert-new-lines-to-html': './bower_components/scribe-plugin-formatter-plain-text-convert-new-lines-to-html/scribe-plugin-formatter-plain-text-convert-new-lines-to-html',
    'scribe-plugin-heading-command': './bower_components/scribe-plugin-heading-command/scribe-plugin-heading-command',
    'scribe-plugin-keyboard-shortcuts': './bower_components/scribe-plugin-keyboard-shortcuts/scribe-plugin-keyboard-shortcuts',
    'scribe-plugin-link-prompt-command': './bower_components/scribe-plugin-link-prompt-command/scribe-plugin-link-prompt-command',
    'scribe-plugin-insertimage-command': './bower_components/scribe-plugin-insertimage-command/scribe-plugin-insertimage-command',
    'scribe-plugin-inserttable-command': './bower_components/scribe-plugin-inserttable-command/scribe-plugin-inserttable-command',
    'scribe-plugin-inserthtml-command': './bower_components/scribe-plugin-inserthtml-command/scribe-plugin-inserthtml-command',
    'scribe-plugin-sanitizer': './bower_components/scribe-plugin-sanitizer/scribe-plugin-sanitizer',
    'scribe-plugin-smart-lists': './bower_components/scribe-plugin-smart-lists/scribe-plugin-smart-lists',
    'scribe-plugin-toolbar': './bower_components/scribe-plugin-toolbar/scribe-plugin-toolbar',

    // Converters
    'marked': './bower_components/marked/lib/marked',
    'to-markdown' : './bower_components/to-markdown/src/to-markdown'
  },
  shim: {
    'to-markdown' : {
      deps : ['he']
    },
    'he': {
        'exports': 'he'
    }
  }
});
<commit_msg>Remove he shim, instead make he a dependency of to-markdown<commit_after>require.config({
  baseUrl : '../',
  paths: {
    // Editor core
    'editor': './src/editor',

    // Editor modules
    'scribe-wrapper': './src/modules/lib/scribe-wrapper/scribe-wrapper',
    'source-converter': './src/modules/lib/source-converter/source-converter',

    // Editor plugins
    'editor-plugin-sticky-toolbar': './src/modules/plugins/editor-sticky-toolbar/editor-sticky-toolbar',

    // Scribe
    'he': './bower_components/he/he',
    'scribe': './bower_components/scribe/scribe',
    'scribe-plugin-blockquote-command': './bower_components/scribe-plugin-blockquote-command/scribe-plugin-blockquote-command',
    'scribe-plugin-formatter-plain-text-convert-new-lines-to-html': './bower_components/scribe-plugin-formatter-plain-text-convert-new-lines-to-html/scribe-plugin-formatter-plain-text-convert-new-lines-to-html',
    'scribe-plugin-heading-command': './bower_components/scribe-plugin-heading-command/scribe-plugin-heading-command',
    'scribe-plugin-keyboard-shortcuts': './bower_components/scribe-plugin-keyboard-shortcuts/scribe-plugin-keyboard-shortcuts',
    'scribe-plugin-link-prompt-command': './bower_components/scribe-plugin-link-prompt-command/scribe-plugin-link-prompt-command',
    'scribe-plugin-insertimage-command': './bower_components/scribe-plugin-insertimage-command/scribe-plugin-insertimage-command',
    'scribe-plugin-inserttable-command': './bower_components/scribe-plugin-inserttable-command/scribe-plugin-inserttable-command',
    'scribe-plugin-inserthtml-command': './bower_components/scribe-plugin-inserthtml-command/scribe-plugin-inserthtml-command',
    'scribe-plugin-sanitizer': './bower_components/scribe-plugin-sanitizer/scribe-plugin-sanitizer',
    'scribe-plugin-smart-lists': './bower_components/scribe-plugin-smart-lists/scribe-plugin-smart-lists',
    'scribe-plugin-toolbar': './bower_components/scribe-plugin-toolbar/scribe-plugin-toolbar',

    // Converters
    'marked': './bower_components/marked/lib/marked',
    'to-markdown' : './bower_components/to-markdown/src/to-markdown'
  },
  shim: {
    'to-markdown' : {
      deps : ['he']
    }
  }
});
<|endoftext|>


<commit_before>import './main.scss';

import { Publish } from './xs';
import Clipboard from 'clipboard';

import ColorList from './components/colors';
import Swatch from './components/swatch';
import Download from './components/download';
import Toast from './components/toast';
import Favicon from './components/favicon';

const $body = document.body;
const $head = document.head;

$head.appendChild(Favicon());
$body.appendChild(ColorList());
$body.appendChild(Swatch());
$body.appendChild(Download());
$body.appendChild(Toast());

const clipboard = new Clipboard('hex-', {
  text: (trigger) => trigger.textContent,
});

clipboard.on('success', e =>
  Publish('hex/copied', [e.text])
);
<commit_msg>Ensures https for malette.io host<commit_after>import './main.scss';

import { Publish } from './xs';
import Clipboard from 'clipboard';

import ColorList from './components/colors';
import Swatch from './components/swatch';
import Download from './components/download';
import Toast from './components/toast';
import Favicon from './components/favicon';

const host = 'malette.io';
if ((host == window.location.host) && (window.location.protocol != 'https:'))
  window.location.protocol = 'https';

const $body = document.body;
const $head = document.head;

$head.appendChild(Favicon());
$body.appendChild(ColorList());
$body.appendChild(Swatch());
$body.appendChild(Download());
$body.appendChild(Toast());

const clipboard = new Clipboard('hex-', {
  text: (trigger) => trigger.textContent,
});

clipboard.on('success', e =>
  Publish('hex/copied', [e.text])
);
<|endoftext|>


<commit_before>(function (global, factory) {
  /* global module, define */

  "use strict";

  var i18n = factory();

  if(typeof(module) === "object" && typeof(module.exports) === "object") {
    module.exports = i18n;
  } else if(typeof(define) === "function") {
    define([], i18n);
  } else {
    global.i18n = i18n;
  }
}(this, function () {
  "use strict";

  var languageDialect = (function(lang) {
        window.location.search.slice(1).split("&").some(function (searchTerm) {
          if (searchTerm.indexOf("lang=") === 0) {
            lang = searchTerm.split("=").slice(1).join("=");
            return true;
          }
        });

        return lang.toLowerCase();
      })(window.navigator.userLanguage || window.navigator.language),
      language = languageDialect.split("-")[0],
      Translations = (function() {
        var scope = "_scope";

        function Translations(language, o) {
          this.language = language;
          this[scope] = o;
        }

        Translations.prototype.find = function(path) {
          return path.split(".").reduce(function (obj, key) {
            return obj ? obj[key] : undefined;
          }, this[scope]);
        };

        Translations.prototype.translate = function(ele) {
          if(getLang(ele) === this.language) {
            if(!ele.hasAttribute("data-i18n")) {
              [].slice.call(ele.querySelectorAll("[lang]:not([lang='" + this.language + "'])")).forEach(this.translate, this);
            }
          } else if(ele.hasAttribute("data-i18n")) {
            applyTranslationToElement(ele, this);
          } else {
            [].slice.call(ele.querySelectorAll("[data-i18n]")).map(function(match) {
              if(getLang(match, ele) !== this.language) {
                applyTranslationToElement(match, this);
              }
            }, this);
          }

          return ele;
        };

        return Translations;
      }()),
      Formats = (function() {
        var scope = "_scope";

        function Formats(language, o) {
          this.language = language;
          this[scope] = o;
        }

        Formats.prototype.localiseNumber = function(number) {
          var separator = [this.number.thousands, this.number.thousandths];

          return parseFloat(number).toString().split(".").map(function(part, i) {
            var chrs = part.split(""),
              length = chrs.length,
              ret = [],
              i2;

            if(!i) {
              chrs.reverse();
            }

            for(i2 = 0; i2 < length; i2 += 3) {
              ret.push(chrs[i2], chrs[i2 + 1], chrs[i2 + 2]);
              if(i2 + 3 < length) {
                ret.push(separator[i]);
              }
            }

            if(!i) {
              ret.reverse();
            }

            return ret.join("");
          }).join(this.number.decimal);
        };

        return Formats;
      }()),
      debug = (function() {
        var debug = {
          enabled: false
        };

        ["info", "warn", "error"].forEach(function(name) {
          debug[name] = function() {
            if(this.enabled) {
              console[name].apply(console, arguments);
            }
          };
        });

        return debug;
      }()),
      i18n;

  function applyTranslationToElement(ele, obj) {
    if(ele.hasAttribute("data-i18n")) {
      applyTranslation(ele, ele.getAttribute("data-i18n"), obj);
    }
  }

  function applyTranslation(ele, path, obj) {
    var translated = obj.find(path);

    if (typeof(translated) === "object" && !Array.isArray(translated)) {
      debug.warn("Could not translate %o: path '%s' is of type object", ele, path);
    } else if(typeof(translated) !== "undefined") {
      clean(ele);
      ele.appendChild(toDom(translated));
      ele.lang = language;
    }
  }

  function getLang(ele, threshold) {
    do {
      if(ele.lang) {
        return ele.lang.toLowerCase();
      }
    } while((ele = ele.parentElement) && ele !== threshold);
  }

  function clean(ele) {
    var child;
    while((child = ele.firstChild)) {
      ele.removeChild(child);
    }
  }

  function toDom(content) {
    if(Array.isArray(content)) {
      return content.reduce(function(frag, text) {
        var ele = document.createElement("p");
        ele.textContent = text;
        frag.appendChild(ele);

        return frag;
      }, document.createDocumentFragment());
    }
    return document.createTextNode(content);
  }

  i18n = {
    base: (document.documentElement.getAttribute("data-i18n-base") || "locales/"),
    set: (document.documentElement.getAttribute("data-i18n-set") || "translation"),
    loadTranslations: function(lang, set, base) {
      var url;
      base = base || this.base || "";
      lang = lang || language;
      set = set || this.set;
      url = base + lang + "/" + set + ".json";

      debug.info("loading translations for %s from: %s", lang, url);

      return fetch(url, {
        headers: {
          "Accept": "application/json"
        }
      }).then(function (res) {
        if (res.status >= 200 && res.status < 300) {
          debug.info("successfully loaded translations for %s", lang);

          return res.json().then(function(obj) {
            return new Translations(lang || language, obj);
          });
        }

        // TODO add error object instead of false
        return Promise.reject(false);
      }).catch(function (err) {
        debug.error("Error loading translations for %s: %o", lang, err);
        return Promise.reject(err);
      });
    },

    translate: function(ele) {
      if(!this.translations) {
        this.translations = this.loadTranslations();
      }

      return this.translations.then(function(translations) {
        return translations.translate(ele);
      });
    },
    translateAll: function() {
      this.translatedAll = true;
      return this.translate(document.documentElement);
    },
    get: function(path) {
      if(!this.translations) {
        this.translations = this.loadTranslations();
      }

      return this.translations.then(function(obj) {
        return obj.find(path);
      });
    },

    formatBase: (document.documentElement.getAttribute("data-i18n-format-base") || "formats/"),
    loadFormats: function(lang, base) {
      var url;
      base = base || this.formatBase || "";
      lang = lang || language;
      url = base + lang + ".json";

      return fetch(url, {
        headers: {
          "Accept": "application/json"
        }
      }).then(function (res) {
        if (res.status >= 200 && res.status < 300) {
          debug.info("successfully loaded translations for %s", lang);

          return res.json().then(function(obj) {
            return new Formats(lang || language, obj);
          });
        }

        // TODO add error object instead of false
        return Promise.reject(false);
      }).catch(function (err) {
        debug.error("Error loading translations for %s: %o", lang, err);
        return Promise.reject(err);
      });
    },

    set language(lang) {
      lang = String(lang);

      if(lang !== language) {
        language = lang;

        if(this.translations) {
          this.translations = this.loadTranslations();
        }
        if(this.translatedAll) {
          this.translateAll(lang);
        }
      }
    },
    get language() {
      return language;
    },

    get debug() {
      return debug.enabled;
    },
    set debug(val) {
      debug.enabled = Boolean(val);
    }
  };

  if(!document.documentElement.hasAttribute("data-i18n-disable-auto")) {
    i18n.translateAll();
  }

  return i18n;
}));
<commit_msg>Add Formats.prototype.localise (#34, #35)<commit_after>(function (global, factory) {
  /* global module, define */

  "use strict";

  var i18n = factory();

  if(typeof(module) === "object" && typeof(module.exports) === "object") {
    module.exports = i18n;
  } else if(typeof(define) === "function") {
    define([], i18n);
  } else {
    global.i18n = i18n;
  }
}(this, function () {
  "use strict";

  var languageDialect = (function(lang) {
        window.location.search.slice(1).split("&").some(function (searchTerm) {
          if (searchTerm.indexOf("lang=") === 0) {
            lang = searchTerm.split("=").slice(1).join("=");
            return true;
          }
        });

        return lang.toLowerCase();
      })(window.navigator.userLanguage || window.navigator.language),
      language = languageDialect.split("-")[0],
      Translations = (function() {
        var scope = "_scope";

        function Translations(language, o) {
          this.language = language;
          this[scope] = o;
        }

        Translations.prototype.find = function(path) {
          return path.split(".").reduce(function (obj, key) {
            return obj ? obj[key] : undefined;
          }, this[scope]);
        };

        Translations.prototype.translate = function(ele) {
          if(getLang(ele) === this.language) {
            if(!ele.hasAttribute("data-i18n")) {
              [].slice.call(ele.querySelectorAll("[lang]:not([lang='" + this.language + "'])")).forEach(this.translate, this);
            }
          } else if(ele.hasAttribute("data-i18n")) {
            applyTranslationToElement(ele, this);
          } else {
            [].slice.call(ele.querySelectorAll("[data-i18n]")).map(function(match) {
              if(getLang(match, ele) !== this.language) {
                applyTranslationToElement(match, this);
              }
            }, this);
          }

          return ele;
        };

        return Translations;
      }()),
      Formats = (function() {
        var scope = "_scope";

        function Formats(language, o) {
          this.language = language;
          this[scope] = o;
        }

        Formats.prototype.localise = function(ele, type) {
          var attrValue = ["DATA", "META"].indexOf(ele.tagName) === -1,
            val = attrValue ? ele.getAttribute("value") : ele.nodeValue;

          if(!type && ele.nodeType === Node.ELEMENT_NODE) {
            type = ele.getAttribute("data-localise-as");
          }

          if(!type) {
            throw new Error("Formats.prototype.localise: missing type");
          }

          switch(type) {
            case "number":
              val = this.localiseNumber(val);
              break;

            default:
              throw new Error("Formats.prototype.localise: missing type");
          }

          ele.nodeValue = val;
        };

        Formats.prototype.localiseNumber = function(number) {
          var separator = [this.number.thousands, this.number.thousandths];

          return parseFloat(number).toString().split(".").map(function(part, i) {
            var chrs = part.split(""),
              length = chrs.length,
              ret = [],
              i2;

            if(!i) {
              chrs.reverse();
            }

            for(i2 = 0; i2 < length; i2 += 3) {
              ret.push(chrs[i2], chrs[i2 + 1], chrs[i2 + 2]);
              if(i2 + 3 < length) {
                ret.push(separator[i]);
              }
            }

            if(!i) {
              ret.reverse();
            }

            return ret.join("");
          }).join(this.number.decimal);
        };

        return Formats;
      }()),
      debug = (function() {
        var debug = {
          enabled: false
        };

        ["info", "warn", "error"].forEach(function(name) {
          debug[name] = function() {
            if(this.enabled) {
              console[name].apply(console, arguments);
            }
          };
        });

        return debug;
      }()),
      i18n;

  function applyTranslationToElement(ele, obj) {
    if(ele.hasAttribute("data-i18n")) {
      applyTranslation(ele, ele.getAttribute("data-i18n"), obj);
    }
  }

  function applyTranslation(ele, path, obj) {
    var translated = obj.find(path);

    if (typeof(translated) === "object" && !Array.isArray(translated)) {
      debug.warn("Could not translate %o: path '%s' is of type object", ele, path);
    } else if(typeof(translated) !== "undefined") {
      clean(ele);
      ele.appendChild(toDom(translated));
      ele.lang = language;
    }
  }

  function getLang(ele, threshold) {
    do {
      if(ele.lang) {
        return ele.lang.toLowerCase();
      }
    } while((ele = ele.parentElement) && ele !== threshold);
  }

  function clean(ele) {
    var child;
    while((child = ele.firstChild)) {
      ele.removeChild(child);
    }
  }

  function toDom(content) {
    if(Array.isArray(content)) {
      return content.reduce(function(frag, text) {
        var ele = document.createElement("p");
        ele.textContent = text;
        frag.appendChild(ele);

        return frag;
      }, document.createDocumentFragment());
    }
    return document.createTextNode(content);
  }

  i18n = {
    base: (document.documentElement.getAttribute("data-i18n-base") || "locales/"),
    set: (document.documentElement.getAttribute("data-i18n-set") || "translation"),
    loadTranslations: function(lang, set, base) {
      var url;
      base = base || this.base || "";
      lang = lang || language;
      set = set || this.set;
      url = base + lang + "/" + set + ".json";

      debug.info("loading translations for %s from: %s", lang, url);

      return fetch(url, {
        headers: {
          "Accept": "application/json"
        }
      }).then(function (res) {
        if (res.status >= 200 && res.status < 300) {
          debug.info("successfully loaded translations for %s", lang);

          return res.json().then(function(obj) {
            return new Translations(lang || language, obj);
          });
        }

        // TODO add error object instead of false
        return Promise.reject(false);
      }).catch(function (err) {
        debug.error("Error loading translations for %s: %o", lang, err);
        return Promise.reject(err);
      });
    },

    translate: function(ele) {
      if(!this.translations) {
        this.translations = this.loadTranslations();
      }

      return this.translations.then(function(translations) {
        return translations.translate(ele);
      });
    },
    translateAll: function() {
      this.translatedAll = true;
      return this.translate(document.documentElement);
    },
    get: function(path) {
      if(!this.translations) {
        this.translations = this.loadTranslations();
      }

      return this.translations.then(function(obj) {
        return obj.find(path);
      });
    },

    formatBase: (document.documentElement.getAttribute("data-i18n-format-base") || "formats/"),
    loadFormats: function(lang, base) {
      var url;
      base = base || this.formatBase || "";
      lang = lang || language;
      url = base + lang + ".json";

      return fetch(url, {
        headers: {
          "Accept": "application/json"
        }
      }).then(function (res) {
        if (res.status >= 200 && res.status < 300) {
          debug.info("successfully loaded translations for %s", lang);

          return res.json().then(function(obj) {
            return new Formats(lang || language, obj);
          });
        }

        // TODO add error object instead of false
        return Promise.reject(false);
      }).catch(function (err) {
        debug.error("Error loading translations for %s: %o", lang, err);
        return Promise.reject(err);
      });
    },

    set language(lang) {
      lang = String(lang);

      if(lang !== language) {
        language = lang;

        if(this.translations) {
          this.translations = this.loadTranslations();
        }
        if(this.translatedAll) {
          this.translateAll(lang);
        }
      }
    },
    get language() {
      return language;
    },

    get debug() {
      return debug.enabled;
    },
    set debug(val) {
      debug.enabled = Boolean(val);
    }
  };

  if(!document.documentElement.hasAttribute("data-i18n-disable-auto")) {
    i18n.translateAll();
  }

  return i18n;
}));
<|endoftext|>


<commit_before>var packageAttribute = require('livefyre-package-attribute');
var backgroundColorCss = require('text!./styles/card-background-color.css');
var textColorCss = require('text!./styles/text-color.css');
var anchorColorCss = require('text!./styles/anchor-color.css');
var displayNameColorCss = require('text!./styles/display-name-color.css');
var usernameColorCss = require('text!./styles/username-color.css');

var HEAD_EL = document.getElementsByTagName('head')[0];

var ThemeStyler = function (opts) {
    opts = opts || {};
    this._styleEl = document.createElement('style');
    HEAD_EL.appendChild(this._styleEl);
    this._stylePrefix = opts.prefix || ['[',packageAttribute.attribute,'~="',packageAttribute.value,'"] '].join('');
};

ThemeStyler.prototype.applyTheme = function (theme) {
    var cssText = getThemeCss(theme);
    var prefixedCss = prefixCss(this._stylePrefix, cssText);
    this._styleEl.innerHTML = prefixedCss;
};

function prefixCss(prefix, cssText) {
    var match, results = [],
        cssPattern = new RegExp("([^\\s][\\s\\S]*?)(\\{[\\s\\S]*?\\})", "g"),
        selectors, prefixedSelectors;

    while (match = cssPattern.exec(cssText)) {
        //There might be a concatenation of selectors, explode them
        selectors = match[1].split(",");
        prefixedSelectors = [];

        for (var i = 0, l = selectors.length; i < l; i += 1) {
           prefixedSelectors.push(prefix + selectors[i]);
        }
        results.push(prefixedSelectors.join(","), match[2]);
    }

    return results.join("");
};

function getThemeCss(theme) {
    var cssVarRegex = /var\(--[\w-]+\)/g;
    var cssStyles = [];
    for (var themeVar in theme) {
        if (theme.hasOwnProperty(themeVar)) {
            var val = theme[themeVar];
            var cssText = getStyleTemplate(themeVar);
            cssText = cssText.replace(cssVarRegex, val);
            cssStyles.push(cssText);
        }
    }
    return cssStyles.join('');
};

ThemeStyler.TEMPLATE_MAP = {
    'card-background-color': backgroundColorCss,
    'anchor-color': anchorColorCss,
    'text-color': textColorCss,
    'display-name-color': displayNameColorCss,
    'username-color': usernameColorCss
};

function getStyleTemplate(themeVar) {
    return ThemeStyler.TEMPLATE_MAP[themeVar];
};

module.exports = ThemeStyler;
<commit_msg>ThemeStyler can have a configured packageAttribute<commit_after>var backgroundColorCss = require('text!livefyre-theme-styler/styles/card-background-color.css');
var textColorCss = require('text!livefyre-theme-styler/styles/text-color.css');
var anchorColorCss = require('text!livefyre-theme-styler/styles/anchor-color.css');
var displayNameColorCss = require('text!livefyre-theme-styler/styles/display-name-color.css');
var usernameColorCss = require('text!livefyre-theme-styler/styles/username-color.css');

var HEAD_EL = document.getElementsByTagName('head')[0];

var ThemeStyler = function (opts) {
    opts = opts || {};
    this._styleEl = document.createElement('style');
    HEAD_EL.appendChild(this._styleEl);
    var packageAttribute = opts.packageAttribute || {};
    this._stylePrefix = opts.prefix || ['[',packageAttribute.attribute,'~="',packageAttribute.value,'"] '].join('');
};

ThemeStyler.prototype.applyTheme = function (theme) {
    var cssText = getThemeCss(theme);
    var prefixedCss = prefixCss(this._stylePrefix, cssText);
    this._styleEl.innerHTML = prefixedCss;
};

function prefixCss(prefix, cssText) {
    var match, results = [],
        cssPattern = new RegExp("([^\\s][\\s\\S]*?)(\\{[\\s\\S]*?\\})", "g"),
        selectors, prefixedSelectors;

    while (match = cssPattern.exec(cssText)) {
        //There might be a concatenation of selectors, explode them
        selectors = match[1].split(",");
        prefixedSelectors = [];

        for (var i = 0, l = selectors.length; i < l; i += 1) {
           prefixedSelectors.push(prefix + selectors[i]);
        }
        results.push(prefixedSelectors.join(","), match[2]);
    }

    return results.join("");
};

function getThemeCss(theme) {
    var cssVarRegex = /var\(--[\w-]+\)/g;
    var cssStyles = [];
    for (var themeVar in theme) {
        if (theme.hasOwnProperty(themeVar)) {
            var val = theme[themeVar];
            var cssText = getStyleTemplate(themeVar);
            cssText = cssText.replace(cssVarRegex, val);
            cssStyles.push(cssText);
        }
    }
    return cssStyles.join('');
};

ThemeStyler.TEMPLATE_MAP = {
    'card-background-color': backgroundColorCss,
    'anchor-color': anchorColorCss,
    'text-color': textColorCss,
    'display-name-color': displayNameColorCss,
    'username-color': usernameColorCss
};

function getStyleTemplate(themeVar) {
    return ThemeStyler.TEMPLATE_MAP[themeVar];
};

module.exports = ThemeStyler;
<|endoftext|>


<commit_before>Crafty.math = {
    /**
     * Returns the absolute value.
     * @param x Some value.
     * @return param Absolute value.
     */
    abs: function(x) {
        return x < 0 ? -x : x;
    },

    /**
     * Returns the amount of how much a checkValue is more like minValue (=0)
     * or more like maxValue (=1)
     * @param checkValue Value that should checked with minimum and maximum.
     * @param minValue Minimum value to check.
     * @param maxValue Maximum value to check.
     * @return param Amount of checkValue compared to minValue and maxValue.
     */
    amountOf: function(checkValue, minValue, maxValue) {
        if (minValue < maxValue)
            return (checkValue - minValue) / (maxValue - minValue);
        else
            return (checkValue - maxValue) / (minValue - maxValue);
    },

    /**
     * Restricts a value to be within a specified range.
     * @param value A value.
     * @param max Maximum that value can be.
     * @param min Minimum that value can be.
     * @return param The value between minimum and maximum.
     */
    clamp: function(value, min, max) {
        if (value > max)
            return max;
        else if (value < min)
            return min;
        else
            return value;
    },

    /**
     * Converts angle from degree to radian.
     * @param angleInDeg The angle in degree.
     * @return param The angle in radian.
     */
    degToRad: function(angleInDeg) {
        return angleInDeg * Math.PI / 180;
    },

    /**
     * Distance between two points.
     * @param x1 First x coordinate.
     * @param y1 First y coordinate.
     * @param x2 Second x coordinate.
     * @param y2 Second y coordinate.
     * @return param The distance between the two points.
     */
    distance: function(x1, y1, x2, y2) {
        var squaredDistance = Crafty.math.squaredDistance(x1, y1, x2, y2);
        return Math.sqrt(parseFloat(squaredDistance));
    },

    /**
     * Linear interpolation. Passing amount with a value of 0 will cause value1 to be returned,
     * a value of 1 will cause value2 to be returned.
     * @param value1 One value.
     * @param value2 Another value.
     * @param amount Amount of value2 to value1.
     * @return param Linear interpolated value.
     */
    lerp: function(value1, value2, amount) {
        return value1 + (value2 - value1) * amount;
    },

    /**
     * Returnes "randomly" -1.
     * @param percent If you pass 1 a -1 will be returned. If you pass 0 a 1 will be returned.
     * @return param 1 or -1.
     */
    negate: function(percent) {
        if (Math.random() < percent)
            return -1;
        else
            return 1;
    },

    /**
     * Converts angle from radian to degree.
     * @param angleInRad The angle in radian.
     * @return param The angle in degree.
     */
    radToDeg: function(angleInRad) {
        return angleInRad * 180 / Math.PI;
    },

    /**
     * Returns a random element of a specific array.
     * @param array A specific array.
     * @return param A random element of a specific array.
     */
    randomElementOfArray: function(array) {
        return array[array.length * Math.random()];
    },

    /**
     * Returns a random int in within a specific range.
     * @param start Smallest int value that can be returned.
     * @param end Biggest int value that can be returned.
     * @return param A random int.
     */
    randomInt: function(start, end) {
        return start + Math.floor((1 + end - start) * Math.random());
    },

    /**
     * Returns a random number in within a specific range.
     * @param start Smallest number value that can be returned.
     * @param end Biggest number value that can be returned.
     * @return param A random number.
     */
    randomNumber: function(start, end) {
        return start + (end - start) * Math.random();
    },

    /**
     * Squared distance between two points.
     * @param x1 First x coordinate.
     * @param y1 First y coordinate.
     * @param x2 Second x coordinate.
     * @param y2 Second y coordinate.
     * @return param The squared distance between the two points.
     */
    squaredDistance: function(x1, y1, x2, y2) {
        return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
    },

    /**
     * Check if a value is within a specific range.
     * @param value The specific value.
     * @param min Minimum value.
     * @param max Maximum value.
     * @return param Returns true if value is within a specific range.
     */
    withinRange: function(value, min, max) {
        return (value >= min && value <= max);
    }

};<commit_msg>Added Vector2D class to Crafty.math.<commit_after>/**
 * Crafty.math - Static functions.
 */

Crafty.math = {
    /**
     * Returns the absolute value.
     * @param x Some value.
     * @return param Absolute value.
     */
    abs: function(x) {
        return x < 0 ? -x : x;
    },

    /**
     * Returns the amount of how much a checkValue is more like minValue (=0)
     * or more like maxValue (=1)
     * @param checkValue Value that should checked with minimum and maximum.
     * @param minValue Minimum value to check.
     * @param maxValue Maximum value to check.
     * @return param Amount of checkValue compared to minValue and maxValue.
     */
    amountOf: function(checkValue, minValue, maxValue) {
        if (minValue < maxValue)
            return (checkValue - minValue) / (maxValue - minValue);
        else
            return (checkValue - maxValue) / (minValue - maxValue);
    },

    /**
     * Restricts a value to be within a specified range.
     * @param value A value.
     * @param max Maximum that value can be.
     * @param min Minimum that value can be.
     * @return param The value between minimum and maximum.
     */
    clamp: function(value, min, max) {
        if (value > max)
            return max;
        else if (value < min)
            return min;
        else
            return value;
    },

    /**
     * Converts angle from degree to radian.
     * @param angleInDeg The angle in degree.
     * @return param The angle in radian.
     */
    degToRad: function(angleInDeg) {
        return angleInDeg * Math.PI / 180;
    },

    /**
     * Distance between two points.
     * @param x1 First x coordinate.
     * @param y1 First y coordinate.
     * @param x2 Second x coordinate.
     * @param y2 Second y coordinate.
     * @return param The distance between the two points.
     */
    distance: function(x1, y1, x2, y2) {
        var squaredDistance = Crafty.math.squaredDistance(x1, y1, x2, y2);
        return Math.sqrt(parseFloat(squaredDistance));
    },

    /**
     * Linear interpolation. Passing amount with a value of 0 will cause value1 to be returned,
     * a value of 1 will cause value2 to be returned.
     * @param value1 One value.
     * @param value2 Another value.
     * @param amount Amount of value2 to value1.
     * @return param Linear interpolated value.
     */
    lerp: function(value1, value2, amount) {
        return value1 + (value2 - value1) * amount;
    },

    /**
     * Returnes "randomly" -1.
     * @param percent If you pass 1 a -1 will be returned. If you pass 0 a 1 will be returned.
     * @return param 1 or -1.
     */
    negate: function(percent) {
        if (Math.random() < percent)
            return -1;
        else
            return 1;
    },

    /**
     * Converts angle from radian to degree.
     * @param angleInRad The angle in radian.
     * @return param The angle in degree.
     */
    radToDeg: function(angleInRad) {
        return angleInRad * 180 / Math.PI;
    },

    /**
     * Returns a random element of a specific array.
     * @param array A specific array.
     * @return param A random element of a specific array.
     */
    randomElementOfArray: function(array) {
        return array[array.length * Math.random()];
    },

    /**
     * Returns a random int in within a specific range.
     * @param start Smallest int value that can be returned.
     * @param end Biggest int value that can be returned.
     * @return param A random int.
     */
    randomInt: function(start, end) {
        return start + Math.floor((1 + end - start) * Math.random());
    },

    /**
     * Returns a random number in within a specific range.
     * @param start Smallest number value that can be returned.
     * @param end Biggest number value that can be returned.
     * @return param A random number.
     */
    randomNumber: function(start, end) {
        return start + (end - start) * Math.random();
    },

    /**
     * Squared distance between two points.
     * @param x1 First x coordinate.
     * @param y1 First y coordinate.
     * @param x2 Second x coordinate.
     * @param y2 Second y coordinate.
     * @return param The squared distance between the two points.
     */
    squaredDistance: function(x1, y1, x2, y2) {
        return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
    },

    /**
     * Check if a value is within a specific range.
     * @param value The specific value.
     * @param min Minimum value.
     * @param max Maximum value.
     * @return param Returns true if value is within a specific range.
     */
    withinRange: function(value, min, max) {
        return (value >= min && value <= max);
    }
};

/**
 * Crafty.math - Classes.
 */

/**
 * Vector2D
 *
 * A vector class for basic vector calculations.
 * @param x The x value of the vector (default: 0).
 * @param y The y value of the vector (default: 0).
 */
Crafty.math.Vector2D = (function() {
    // place vor private attributes/methodes

    // return constructor
    return function Vector2D(x, y) {
        if (arguments.length === 1)
            y = 0;
        else if (arguments.length === 0)
            x = y = 0;

        this.x = x;
        this.y = y;
    };
})();

/**
 * Public functions.
 */

/**
 * Normalize vector. (Set length to 1.)
 * @return This vector.
 */
Crafty.math.Vector2D.prototype.normalize = function() {
    var l = this.getLength();
    if (l === 0)
        this.x = 1;
    else {
        this.x = this.x / l;
        this.y = this.y / l;
    }
    return this;
};

/**
 * Get vector length.
 * @return Length of Vector.
 */
Crafty.math.Vector2D.prototype.getLength = function() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
};

/**
 * Set vector length.
 * @param length New length of Vector.
 */
Crafty.math.Vector2D.prototype.setLength = function(length) {
    this.normalize().multiply(length);
};

/**
 * Calculate the dot product of this vector and another.
 * @param vector Another vector.
 * @return The dot product.
 */
Crafty.math.Vector2D.prototype.dotProduct = function(vector) {
    return this.x * vector.x + this.y * vector.y;
};

/**
 * Calculate normal of line between this vector and another and
 * returns righthand normal vector (-dy, dx).
 * (Lefthand would be (dy, -dx). dy first because normal is rotated in 90° to vector.)
 * @param vector Another vector. If no vector is passed, a zero-vector will be used.
 * @return Normal vector.
 */
Crafty.math.Vector2D.prototype.getNormal = function(vector) {
    if (arguments.length === 0) vector = new Crafty.math.Vector2D();
    return new Crafty.math.Vector2D(-(this.y - vector.y), this.x - vector.x);	// (-dy, dx) == right or (dy, -dx) == left
};

/**
 * Get angle between this vector and another in rad.
 * Math.acos( a * b / ( |a| * |b| ) );
 * @param vector Another vector.
 * @return The angle between this vector and another in rad.
 */
Crafty.math.Vector2D.prototype.getAngle = function(vector) {
    return Math.acos(this.dotProduct(vector) / (this.getLength() * vector.getLength()));
    // or: Math.atan2(y, x) ?
};

/**
 * Checks if x and y are 0.
 * @return A boolean which specifies if this vector is a zero-vector.
 */
Crafty.math.Vector2D.prototype.isZero = function() {
    return this.x === 0 && this.y === 0;
};

/**
 * Negates this vector (-x, -y).
 * @return Returns this vector.
 */
Crafty.math.Vector2D.prototype.negate = function() {
    this.x *= -1;
    this.y *= -1;
    return this;
};

/**
 * Calculates distance between this vector and another.
 * @param vector Another vector.
 * @return Returns distance.
 */
Crafty.math.Vector2D.prototype.distance = function(vector) {
    return Math.sqrt(this.squaredDistance(vector));
};

/**
 * Multiply this vector with a scalar.
 * @param scalar A scalar which will be multiplied to this vector.
 * @return This vector.
 */
Crafty.math.Vector2D.prototype.multiply = function(scalar) {
    this.x *= scalar;
    this.y *= scalar;
    return this;
};

/**
 * Calculates squared distance between this vector and another.
 * @param vector Another vector.
 * @return Returns squared distance.
 */
Crafty.math.Vector2D.prototype.squaredDistance = function(vector) {
    return (this.x - vector.x) * (this.x - vector.x) + (this.y - vector.y) * (this.y - vector.y);
};

/**
 * Subtracts another vector from this vector.
 * @param vector Another vector.
 * @return This vector.
 */
Crafty.math.Vector2D.prototype.subtract = function(vector) {
    this.x -= vector.x;
    this.y -= vector.y;
    return this;
};

/**
 * Adds another vector to this vector.
 * @param vector Another vector.
 * @return This vector.
 */
Crafty.math.Vector2D.prototype.add = function(vector) {
    this.x += vector.x;
    this.y += vector.y;
    return this;
};

/**
 * Creates a copy of this vector.
 * @return Copy of this vector.
 */
Crafty.math.Vector2D.prototype.getCopy = function() {
    return new Crafty.math.Vector2D(this.x, this.y);
};

/**
 * Creates a new vector wich is the negative vector of this one.
 * @return New vector, which is the negative of this one.
 */
Crafty.math.Vector2D.prototype.getNegative = function() {
    return new Crafty.math.Vector2D(-this.x, -this.y);
};

/**
 * Get new vector from this vector to another.
 * @param vector Another vector.
 * @return New vector which points form this vector to antoher..
 */
Crafty.math.Vector2D.prototype.to = function(vector) {
    return new Crafty.math.Vector2D(vector.x - this.x, vector.y - this.y);
};

/**
 * Set x and y values of this vector
 * @param vector Holds the new x and y values.
 */
Crafty.math.Vector2D.prototype.setValues = function(vector) {
    this.x = vector.x;
    this.y = vector.y;
};

/**
 * Static public functions.
 */

/**
 * Create a triple product.
 * @param a Vecvtor1.
 * @param b Vecvtor2.
 * @param c Vecvtor3.
 * @return Returns the triple product of three vectors as a new vector.
 */
Crafty.math.Vector2D.tripleProduct = function(a, b, c) {
    var v = new Crafty.math.Vector2D();
    var ac = a.dotProduct(c);
    var bc = b.dotProduct(c);
    v.x = b.x * ac - a.x * bc;
    v.y = b.y * ac - a.y * bc;
    return v;
};<|endoftext|>


<commit_before>import os from 'os'
import path from 'path'
import electron, { app, BrowserWindow, shell } from 'electron'
import certInstaller from 'cert-installer'

const appName = app.getName()

function sendAction (action) {
  const win = BrowserWindow.getAllWindows()[0]

  win.webContents.send(action)
}

const helpSubmenu = [
  {
    label: `${appName} Website`,
    click () {
      shell.openExternal('https://github.com/niklasi/halland-proxy')
    }
  },
  {
    label: 'Report an Issue...',
    click () {
      const body = `
<!-- Please succinctly describe your issue and steps to reproduce it. -->

-

${app.getName()} ${app.getVersion()}
Electron ${process.versions.electron}
${process.platform} ${process.arch} ${os.release()}`

      shell.openExternal(`https://github.com/niklasi/halland-proxy/issues/new?body=${encodeURIComponent(body)}`)
    }
  }
]

if (process.platform !== 'darwin') {
  helpSubmenu.push({
    type: 'separator'
  }, {
    role: 'about',
    click () {
      electron.dialog.showMessageBox({
        title: `About ${appName}`,
        message: `${appName} ${app.getVersion()}`,
        detail: 'Created by Niklas Ingholt',
        icon: path.join(__dirname, 'static/Icon.png'),
        buttons: []
      })
    }
  })
}

const toolsSubmenu = [
  {
    label: 'Update plugins',
    accelerator: 'CmdOrCtrl+U',
    click () {
      sendAction('update-plugins')
    }
  },
  {
    type: 'separator'
  },
  {
    label: 'Install root cert',
    accelerator: 'CmdOrCtrl+i',
    click () {
      const cert = path.resolve(app.getPath('userData'), 'halland-proxy-ca.pem').replace(/ /g, '\\ ')
      certInstaller(cert, {trust: true}, (err) => {
        if (err) console.log('Error installing ca-cert.', err)
      })
    }
  }
]

const darwinTpl = [
  {
    label: appName,
    submenu: [
      {
        role: 'about'
      },
      {
        type: 'separator'
      },
      {
        role: 'services',
        submenu: []
      },
      {
        type: 'separator'
      },
      {
        role: 'hide'
      },
      {
        role: 'hideothers'
      },
      {
        role: 'unhide'
      },
      {
        type: 'separator'
      },
      {
        role: 'quit'
      }
    ]
  },
  {
    label: 'Edit',
    submenu: [
      {
        role: 'undo'
      },
      {
        role: 'redo'
      },
      {
        type: 'separator'
      },
      {
        role: 'cut'
      },
      {
        role: 'copy'
      },
      {
        role: 'paste'
      },
      {
        role: 'pasteandmatchstyle'
      },
      {
        role: 'delete'
      },
      {
        role: 'selectall'
      }
    ]
  },
  {
    label: 'Tools',
    submenu: toolsSubmenu
  },
  {
    role: 'window',
    submenu: [
      {
        role: 'minimize'
      },
      {
        role: 'close'
      },
      {
        type: 'separator'
      },
      {
        role: 'front'
      },
      {
        role: 'togglefullscreen'
      }
    ]
  },
  {
    role: 'help',
    submenu: helpSubmenu
  }
]

const otherTpl = [
  {
    label: 'File',
    submenu: [
      {
        role: 'quit'
      }
    ]
  },
  {
    label: 'Edit',
    submenu: [
      {
        role: 'undo'
      },
      {
        role: 'redo'
      },
      {
        type: 'separator'
      },
      {
        role: 'cut'
      },
      {
        role: 'copy'
      },
      {
        role: 'paste'
      },
      {
        role: 'pasteandmatchstyle'
      },
      {
        role: 'delete'
      },
      {
        type: 'separator'
      },
      {
        role: 'selectall'
      }
    ]
  },
  {
    label: 'Tools',
    submenu: toolsSubmenu
  },
  {
    role: 'help',
    submenu: helpSubmenu
  }
]

const tpl = process.platform === 'darwin' ? darwinTpl : otherTpl

module.exports = electron.Menu.buildFromTemplate(tpl)
<commit_msg>Add more options to menu.<commit_after>import os from 'os'
import path from 'path'
import electron, { app, BrowserWindow, shell } from 'electron'
import certInstaller from 'cert-installer'

const appName = app.getName()

function getWindow () {
  return BrowserWindow.getAllWindows()[0]
}

function sendAction (action) {
  getWindow().webContents.send(action)
}

const helpSubmenu = [
  {
    label: `${appName} Website`,
    click () {
      shell.openExternal('https://github.com/niklasi/halland-proxy')
    }
  },
  {
    label: 'Report an Issue...',
    click () {
      const body = `
<!-- Please succinctly describe your issue and steps to reproduce it. -->

-

${app.getName()} ${app.getVersion()}
Electron ${process.versions.electron}
${process.platform} ${process.arch} ${os.release()}`

      shell.openExternal(`https://github.com/niklasi/halland-proxy/issues/new?body=${encodeURIComponent(body)}`)
    }
  }
]

if (process.platform !== 'darwin') {
  helpSubmenu.push({
    type: 'separator'
  }, {
    role: 'about',
    click () {
      electron.dialog.showMessageBox({
        title: `About ${appName}`,
        message: `${appName} ${app.getVersion()}`,
        detail: 'Created by Niklas Ingholt',
        icon: path.join(__dirname, 'static/Icon.png'),
        buttons: []
      })
    }
  })
}

const viewSubmenu = [
  {
    label: 'Reload',
    accelerator: 'CmdOrCtrl+R',
    click () {
      getWindow().reload()
    }
  },
  {
    label: 'Toggle Developer Tools',
    accelerator: 'Alt+CmdOrCtrl+I',
    click () {
      getWindow().toggleDevTools()
    }
  }
]
const toolsSubmenu = [
  {
    label: 'Update plugins',
    accelerator: 'CmdOrCtrl+U',
    click () {
      sendAction('update-plugins')
    }
  },
  {
    type: 'separator'
  },
  {
    label: 'Install root cert',
    accelerator: 'CmdOrCtrl+i',
    click () {
      const cert = path.resolve(app.getPath('userData'), 'halland-proxy-ca.pem').replace(/ /g, '\\ ')
      certInstaller(cert, {trust: true}, (err) => {
        if (err) console.log('Error installing ca-cert.', err)
      })
    }
  }
]

const darwinTpl = [
  {
    label: appName,
    submenu: [
      {
        role: 'about'
      },
      {
        type: 'separator'
      },
      {
        role: 'services',
        submenu: []
      },
      {
        type: 'separator'
      },
      {
        role: 'hide'
      },
      {
        role: 'hideothers'
      },
      {
        role: 'unhide'
      },
      {
        type: 'separator'
      },
      {
        role: 'quit'
      }
    ]
  },
  {
    label: 'Edit',
    submenu: [
      {
        role: 'undo'
      },
      {
        role: 'redo'
      },
      {
        type: 'separator'
      },
      {
        role: 'cut'
      },
      {
        role: 'copy'
      },
      {
        role: 'paste'
      },
      {
        role: 'pasteandmatchstyle'
      },
      {
        role: 'delete'
      },
      {
        role: 'selectall'
      }
    ]
  },
  {
    label: 'View',
    submenu: viewSubmenu
  },
  {
    label: 'Tools',
    submenu: toolsSubmenu
  },
  {
    role: 'window',
    submenu: [
      {
        role: 'minimize'
      },
      {
        role: 'close'
      },
      {
        type: 'separator'
      },
      {
        role: 'front'
      },
      {
        role: 'togglefullscreen'
      }
    ]
  },
  {
    role: 'help',
    submenu: helpSubmenu
  }
]

const otherTpl = [
  {
    label: 'File',
    submenu: [
      {
        role: 'quit'
      }
    ]
  },
  {
    label: 'Edit',
    submenu: [
      {
        role: 'undo'
      },
      {
        role: 'redo'
      },
      {
        type: 'separator'
      },
      {
        role: 'cut'
      },
      {
        role: 'copy'
      },
      {
        role: 'paste'
      },
      {
        role: 'pasteandmatchstyle'
      },
      {
        role: 'delete'
      },
      {
        type: 'separator'
      },
      {
        role: 'selectall'
      }
    ]
  },
  {
    label: 'View',
    submenu: viewSubmenu
  },
  {
    label: 'Tools',
    submenu: toolsSubmenu
  },
  {
    role: 'help',
    submenu: helpSubmenu
  }
]

const tpl = process.platform === 'darwin' ? darwinTpl : otherTpl

module.exports = electron.Menu.buildFromTemplate(tpl)
<|endoftext|>


<commit_before>/* global jQuery */

// disable all events
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.trigger = function (options, parent) {
		this.init = function (el, options) {
			// do not forget parent
			parent.init.call(this, el, options);
			this._data.trigger.disabled = false;
		};
		this.trigger = function (ev, data) {
			if(!this._data.trigger.disabled) {
				parent.trigger.call(this, ev, data);
			}
		};
		this.disable_events = function () { this._data.trigger.disabled = true; };
		this.enable_events = function () { this._data.trigger.disabled = false; };
	};
})(jQuery);

// no hover
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.nohover = function () {
		this.hover_node = $.noop;
	};
})(jQuery);

// quick search
(function ($, undefined) {
	"use strict";
	$.jstree.defaults.quicksearch = {
		search_timeout : 200,
		clear_timeout : 1000,
		case_sensitive : false
	};
	$.jstree.plugins.quicksearch = function (options, parent) {
		// own function
		this.bind = function () {
			parent.bind.call(this);
			var word = '',
				to1 = null,
				to2 = null;
			this.element.on('keypress.jstree', $.proxy(function (e) {
				if(to1) { clearTimeout(to1); }
				if(to2) { clearTimeout(to2); }
				word += String.fromCharCode(e.which);
				if(!this.settings.quicksearch.case_sensitive) {
					word = word.toLowerCase();
				}
				to1 = setTimeout(function () {
					word = '';
				}, this.settings.quicksearch.clear_timeout);
				to2 = setTimeout($.proxy(function () {
					this.element.find('.jstree-anchor').each($.proxy(function (i, v) {
						var t = $(v).text();
						if(!this.settings.quicksearch.case_sensitive) {
							t = t.toLowerCase();
						}
						if(t.indexOf(word) === 0) {
							this.hover_node(v);
							return false;
						}
					}, this));
				}, this), this.settings.quicksearch.search_timeout);
			}, this));
		};
	};
})(jQuery);

// conditional select
(function ($, undefined) {
	"use strict";
	$.jstree.defaults.conditionalselect = function () { return true; };
	$.jstree.plugins.conditionalselect = function (options, parent) {
		// own function
		this.activate_node = function (obj, e) {
			if(this.settings.conditionalselect.call(this, this.get_node(obj))) {
				parent.activate_node.call(this, obj, e);
			}
		};
	};
})(jQuery);

// real checkboxes
(function ($, undefined) {
	"use strict";

	var inp = document.createElement("INPUT");
	inp.type = "checkbox";
	inp.className = "jstree-realcheckbox";

	$.jstree.defaults.realcheckboxes = {};

	$.jstree.plugins.realcheckboxes = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this._data.realcheckboxes.uto = false;
			this.element
				.on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree ready.jstree loaded.jstree', $.proxy(function () {
						// only if undetermined is in setting
						if(this._data.realcheckboxes.uto) { clearTimeout(this._data.realcheckboxes.uto); }
						this._data.realcheckboxes.uto = setTimeout($.proxy(this._realcheckboxes, this), 50);
					}, this));
		};
		this.redraw_node = function(obj, deep, callback, force_draw) {
			obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
			if(obj) {
				var i, j, tmp = null, chk = inp.cloneNode(true);
				for(i = 0, j = obj.childNodes.length; i < j; i++) {
					if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
						tmp = obj.childNodes[i];
						break;
					}
				}
				if(tmp) {
					for(i = 0, j = tmp.childNodes.length; i < j; i++) {
						if(tmp.childNodes[i] && tmp.childNodes[i].className && tmp.childNodes[i].className.indexOf("jstree-checkbox") !== -1) {
							tmp = tmp.childNodes[i];
							break;
						}
					}
				}
				if(tmp && tmp.tagName === "I") {
					tmp.style.backgroundColor = "transparent";
					tmp.style.backgroundImage = "none";
					tmp.appendChild(chk);
				}
			}
			return obj;
		};
		this._realcheckboxes = function () {
			var ts = this.settings.checkbox.tie_selection;
			$('.jstree-realcheckbox').each(function () {
				this.checked = (!ts && this.parentNode.className.indexOf("jstree-checked") !== -1) || (ts && this.parentNode.parentNode.className.indexOf('jstree-clicked') !== -1);
				this.indeterminate = this.parentNode.className.indexOf("jstree-undetermined") !== -1;
				this.disabled = this.parentNode.parentNode.className.indexOf("disabled") !== -1;
			});
		};
	};
})(jQuery);

// no state
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.nostate = function () {
		this.set_state = function (state, callback) {
			if(callback) { callback.call(this); }
			this.trigger('set_state');
		};
	};
})(jQuery);

// no selected in state
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.noselectedstate = function (options, parent) {
		this.get_state = function () {
			var state = parent.get_state.call(this);
			delete state.core.selected;
			return state;
		};
	};
})(jQuery);

// allow search results expanding
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.show_matches_children = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this.element
				.on('search.jstree before_open.jstree', function (e, data) {
					if(data.instance.settings.search && data.instance.settings.search.show_only_matches) {
						data.instance._data.search.dom.find('.jstree-node')
							.show().filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last')
							.end().end().end().find(".jstree-children").each(function () { $(this).children(".jstree-node:visible").eq(-1).addClass("jstree-last"); });
					}
				});
		};
	};
})(jQuery);

// additional icon on node (outside of anchor)
(function ($, undefined) {
	"use strict";
	var img = document.createElement('IMG');
	//img.src = "http://www.dpcd.vic.gov.au/__data/assets/image/0004/30667/help.gif";
	img.className = "jstree-questionmark";

	$.jstree.defaults.questionmark = $.noop;
	$.jstree.plugins.questionmark = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this.element
				.on("click.jstree", ".jstree-questionmark", $.proxy(function (e) {
						e.stopImmediatePropagation();
						this.settings.questionmark.call(this, this.get_node(e.target));
					}, this));
		};
		this.teardown = function () {
			if(this.settings.questionmark) {
				this.element.find(".jstree-questionmark").remove();
			}
			parent.teardown.call(this);
		};
		this.redraw_node = function(obj, deep, callback, force_draw) {
			obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
			if(obj) {
				var tmp = img.cloneNode(true);
				obj.insertBefore(tmp, obj.childNodes[2]);
			}
			return obj;
		};
	};
})(jQuery);

// auto numbering
(function ($, undefined) {
	"use strict";
	var span = document.createElement('SPAN');
	span.className = "jstree-numbering";

	$.jstree.defaults.numbering = {};
	$.jstree.plugins.numbering = function (options, parent) {
		this.teardown = function () {
			if(this.settings.questionmark) {
				this.element.find(".jstree-numbering").remove();
			}
			parent.teardown.call(this);
		};
		this.get_number = function (obj) {
			obj = this.get_node(obj);
			var ind = $.inArray(obj.id, this.get_node(obj.parent).children) + 1;
			return obj.parent === '#' ? ind : this.get_number(obj.parent) + '.' + ind;
		};
		this.redraw_node = function(obj, deep, callback, force_draw) {
			var i, j, tmp = null, elm = null, org = this.get_number(obj);
			obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
			if(obj) {
				for(i = 0, j = obj.childNodes.length; i < j; i++) {
					if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
						tmp = obj.childNodes[i];
						break;
					}
				}
				if(tmp) {
					elm = span.cloneNode(true);
					elm.innerHTML = org + '. ';
					tmp.insertBefore(elm, tmp.childNodes[tmp.childNodes.length - 1]);
				}
			}
			return obj;
		};
	};
})(jQuery);

// selecting a node opens it
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.selectopens = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this.element.on('select_node.jstree', function (e, data) { data.instance.open_node(data.node); });
		};
	};
})(jQuery);

// paste override
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.nohover = function () {
		this.paste = function (obj, pos) {
			obj = this.get_node(obj);
			if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; }
			if(this[ccp_mode](ccp_node, obj, pos)) {
				this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode });
			}
			ccp_node = false;
			ccp_mode = false;
			ccp_inst = false;
		};
	};
})(jQuery);<commit_msg>added proper focus handling to sample quicksearch plugin<commit_after>/* global jQuery */

// disable all events
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.trigger = function (options, parent) {
		this.init = function (el, options) {
			// do not forget parent
			parent.init.call(this, el, options);
			this._data.trigger.disabled = false;
		};
		this.trigger = function (ev, data) {
			if(!this._data.trigger.disabled) {
				parent.trigger.call(this, ev, data);
			}
		};
		this.disable_events = function () { this._data.trigger.disabled = true; };
		this.enable_events = function () { this._data.trigger.disabled = false; };
	};
})(jQuery);

// no hover
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.nohover = function () {
		this.hover_node = $.noop;
	};
})(jQuery);

// quick search
(function ($, undefined) {
	"use strict";
	$.jstree.defaults.quicksearch = {
		search_timeout : 200,
		clear_timeout : 1000,
		case_sensitive : false
	};
	$.jstree.plugins.quicksearch = function (options, parent) {
		// own function
		this.bind = function () {
			parent.bind.call(this);
			var word = '',
				to1 = null,
				to2 = null;
			this.element.on('keypress.jstree', $.proxy(function (e) {
				if(to1) { clearTimeout(to1); }
				if(to2) { clearTimeout(to2); }
				word += String.fromCharCode(e.which);
				if(!this.settings.quicksearch.case_sensitive) {
					word = word.toLowerCase();
				}
				to1 = setTimeout(function () {
					word = '';
				}, this.settings.quicksearch.clear_timeout);
				to2 = setTimeout($.proxy(function () {
					this.element.find('.jstree-anchor').each($.proxy(function (i, v) {
						var t = $(v).text();
						if(!this.settings.quicksearch.case_sensitive) {
							t = t.toLowerCase();
						}
						if(t.indexOf(word) === 0) {
							this.hover_node(v);
							$(v).focus();
							return false;
						}
					}, this));
				}, this), this.settings.quicksearch.search_timeout);
			}, this));
		};
	};
})(jQuery);

// conditional select
(function ($, undefined) {
	"use strict";
	$.jstree.defaults.conditionalselect = function () { return true; };
	$.jstree.plugins.conditionalselect = function (options, parent) {
		// own function
		this.activate_node = function (obj, e) {
			if(this.settings.conditionalselect.call(this, this.get_node(obj))) {
				parent.activate_node.call(this, obj, e);
			}
		};
	};
})(jQuery);

// real checkboxes
(function ($, undefined) {
	"use strict";

	var inp = document.createElement("INPUT");
	inp.type = "checkbox";
	inp.className = "jstree-realcheckbox";

	$.jstree.defaults.realcheckboxes = {};

	$.jstree.plugins.realcheckboxes = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this._data.realcheckboxes.uto = false;
			this.element
				.on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree ready.jstree loaded.jstree', $.proxy(function () {
						// only if undetermined is in setting
						if(this._data.realcheckboxes.uto) { clearTimeout(this._data.realcheckboxes.uto); }
						this._data.realcheckboxes.uto = setTimeout($.proxy(this._realcheckboxes, this), 50);
					}, this));
		};
		this.redraw_node = function(obj, deep, callback, force_draw) {
			obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
			if(obj) {
				var i, j, tmp = null, chk = inp.cloneNode(true);
				for(i = 0, j = obj.childNodes.length; i < j; i++) {
					if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
						tmp = obj.childNodes[i];
						break;
					}
				}
				if(tmp) {
					for(i = 0, j = tmp.childNodes.length; i < j; i++) {
						if(tmp.childNodes[i] && tmp.childNodes[i].className && tmp.childNodes[i].className.indexOf("jstree-checkbox") !== -1) {
							tmp = tmp.childNodes[i];
							break;
						}
					}
				}
				if(tmp && tmp.tagName === "I") {
					tmp.style.backgroundColor = "transparent";
					tmp.style.backgroundImage = "none";
					tmp.appendChild(chk);
				}
			}
			return obj;
		};
		this._realcheckboxes = function () {
			var ts = this.settings.checkbox.tie_selection;
			$('.jstree-realcheckbox').each(function () {
				this.checked = (!ts && this.parentNode.className.indexOf("jstree-checked") !== -1) || (ts && this.parentNode.parentNode.className.indexOf('jstree-clicked') !== -1);
				this.indeterminate = this.parentNode.className.indexOf("jstree-undetermined") !== -1;
				this.disabled = this.parentNode.parentNode.className.indexOf("disabled") !== -1;
			});
		};
	};
})(jQuery);

// no state
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.nostate = function () {
		this.set_state = function (state, callback) {
			if(callback) { callback.call(this); }
			this.trigger('set_state');
		};
	};
})(jQuery);

// no selected in state
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.noselectedstate = function (options, parent) {
		this.get_state = function () {
			var state = parent.get_state.call(this);
			delete state.core.selected;
			return state;
		};
	};
})(jQuery);

// allow search results expanding
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.show_matches_children = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this.element
				.on('search.jstree before_open.jstree', function (e, data) {
					if(data.instance.settings.search && data.instance.settings.search.show_only_matches) {
						data.instance._data.search.dom.find('.jstree-node')
							.show().filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last')
							.end().end().end().find(".jstree-children").each(function () { $(this).children(".jstree-node:visible").eq(-1).addClass("jstree-last"); });
					}
				});
		};
	};
})(jQuery);

// additional icon on node (outside of anchor)
(function ($, undefined) {
	"use strict";
	var img = document.createElement('IMG');
	//img.src = "http://www.dpcd.vic.gov.au/__data/assets/image/0004/30667/help.gif";
	img.className = "jstree-questionmark";

	$.jstree.defaults.questionmark = $.noop;
	$.jstree.plugins.questionmark = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this.element
				.on("click.jstree", ".jstree-questionmark", $.proxy(function (e) {
						e.stopImmediatePropagation();
						this.settings.questionmark.call(this, this.get_node(e.target));
					}, this));
		};
		this.teardown = function () {
			if(this.settings.questionmark) {
				this.element.find(".jstree-questionmark").remove();
			}
			parent.teardown.call(this);
		};
		this.redraw_node = function(obj, deep, callback, force_draw) {
			obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
			if(obj) {
				var tmp = img.cloneNode(true);
				obj.insertBefore(tmp, obj.childNodes[2]);
			}
			return obj;
		};
	};
})(jQuery);

// auto numbering
(function ($, undefined) {
	"use strict";
	var span = document.createElement('SPAN');
	span.className = "jstree-numbering";

	$.jstree.defaults.numbering = {};
	$.jstree.plugins.numbering = function (options, parent) {
		this.teardown = function () {
			if(this.settings.questionmark) {
				this.element.find(".jstree-numbering").remove();
			}
			parent.teardown.call(this);
		};
		this.get_number = function (obj) {
			obj = this.get_node(obj);
			var ind = $.inArray(obj.id, this.get_node(obj.parent).children) + 1;
			return obj.parent === '#' ? ind : this.get_number(obj.parent) + '.' + ind;
		};
		this.redraw_node = function(obj, deep, callback, force_draw) {
			var i, j, tmp = null, elm = null, org = this.get_number(obj);
			obj = parent.redraw_node.call(this, obj, deep, callback, force_draw);
			if(obj) {
				for(i = 0, j = obj.childNodes.length; i < j; i++) {
					if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
						tmp = obj.childNodes[i];
						break;
					}
				}
				if(tmp) {
					elm = span.cloneNode(true);
					elm.innerHTML = org + '. ';
					tmp.insertBefore(elm, tmp.childNodes[tmp.childNodes.length - 1]);
				}
			}
			return obj;
		};
	};
})(jQuery);

// selecting a node opens it
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.selectopens = function (options, parent) {
		this.bind = function () {
			parent.bind.call(this);
			this.element.on('select_node.jstree', function (e, data) { data.instance.open_node(data.node); });
		};
	};
})(jQuery);

// paste override
(function ($, undefined) {
	"use strict";
	$.jstree.plugins.nohover = function () {
		this.paste = function (obj, pos) {
			obj = this.get_node(obj);
			if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; }
			if(this[ccp_mode](ccp_node, obj, pos)) {
				this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode });
			}
			ccp_node = false;
			ccp_mode = false;
			ccp_inst = false;
		};
	};
})(jQuery);<|endoftext|>