<commit_before>/*
Copyright 2007 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
 * Javascript code specific to the lobby page
 */
$(document).ready(function() {
    // Start refreshing the page as soon as we're loaded
    lobby.refresh();

    // Initialize the chat text input to send contents when enter pressed
    $("#chatInput").keypress(function(e) {
        if (e.keyCode == 13) {
          lobby.sendChat();
        }
      });

  });

$(window).unload(function() {
    // When we leave the page, notify the server
    lobby.leaving=true;
    lobby.leave();
  });

var lobby = {
  GAME_STATUS_ACTIVE:2,

  msgId: 0,
  // The games the user doesn't want to be prompted about
  ignoreMap: {},
  // The games that are dex battles.
  dexMap: {},

  forceRefresh: function() {
    // If there's not already a refresh in process, refresh immediately.
    if (lobby.refreshTimer) {
      // There's a timer, which means there's no refresh in process, so
      // fire off the refresh - this will stop the existing timer so we don't
      // get a double update.
      lobby.refresh();
    }
  },

  // Calls the server to refresh the player list, chat window, and open games
  refresh: function() {
    // Stop any existing timer
    if (lobby.refreshTimer) {
      window.clearTimeout(lobby.refreshTimer);
      delete lobby.refreshTimer;
    }
    // Turn on to display busy cursor during ajax calls for debugging
    // $(".busy").show();
    var options = {
      url: "/lobby_ajax/" + blitz.user_email + "/" + lobby.msgId,
      // Add random param to URL to avoid browser caching of HTTP GETs
      data: {z: new Date().getTime()},
      dataType: "json",
      error: blitz.retryOnFail,
      success: lobby.handleAjaxResponse
    };
    $.ajax(options);
  },

  handleAjaxResponse: function(data) {
    // We got our response - make sure the busy cursor is off
    $(".busy").hide();
    lobby.msgId = data.chat_list.msg_id;

    // Convert the player name array to a list we can display
    lobby.updatePlayerList(data.player_list);

    // Update the chat
    chat.updateChat(data.chat_list.data);

    lobby.updateGameList(data.game_list);

    // Refresh the data every 5 secs
    lobby.refreshTimer = window.setTimeout(lobby.refresh, 5*1000);
  },

  updateGameList: function(gameList) {
    // Generate content of game table based on what was sent to us
    content = "";
    jQuery.each(gameList, function(index, game) {
        content += "
" + game.creator + "	" +
          (game.opponent ? game.opponent : " ") + "	" +
          (game.time_limit ?  (game.time_limit + " min") : "untimed") +
           "	";
        if (game.type == 'dex') {
          lobby.dexMap[game.key] = true;
        }
        // OK, figure out what actions we have on this game. Choices are:
        // 1) Delete - if is_participant && opponent = null
        // 2) Join - if !is_participant && opponent = null
        // 3) Watch - if !is_participant && opponent = null
        //
        // We also check to see if the user is an invitee or if he's a
        // participant in an active blitz game, in which case we prompt him to
        // enter the game immediately.
        action = [];
        if (game.is_participant || game.is_invitee) {
          // Haven't prompted about this game before
          if (game.is_invitee) {
            // Display an invite dialog (after setting the flag so we don't
            // prompt about this game again)
            if (!lobby.ignoreMap[game.key] && !blitz.dialogActive()) {
              lobby.ignoreMap[game.key] = true;
              lobby.displayJoinDialog(game.creator, game.key);
            }
            action.push({id: "joinGame", label: "Join"});
          } else {
            // If the user is a participant in an active blitz game, warn
            // them and let them enter.
            if (!lobby.ignoreMap[game.key] && !blitz.dialogActive() &&
                game.status == lobby.GAME_STATUS_ACTIVE && game.time_limit) {
              // Don't prompt about this game again.
              lobby.ignoreMap[game.key] = true;
              lobby.displayEnterDialog(game.is_creator ?
                                       game.opponent : game.creator, game.key);
            }
            if (game.status == lobby.GAME_STATUS_ACTIVE) {
              action.push({id: "goGame", label: "Go"});
            }
          }
          if (game.can_delete) {
            action.push({id: "deleteGame", label: "Delete"});
          }
        } else if (game.status == lobby.GAME_STATUS_ACTIVE) {
          // Not a participant, and the game is full - action = watch
          action.push({id: "goGame", label: "Watch"});
        } else {
          // Not a participant, and there's no opponent yet - action = Join
          action.push({id: "joinGame", label: "Join"});
        }
        if (action.length == 0) {
          content += " ";
        } else {
          jQuery.each(action, function(index, item) {
              if (index > 0) {
                content += " ";
              }
              content += '' + item.label + "";
            });
        }
        content += "
";
      });
    // Remove the old games, replace with new content
    $(".gameTable tr:not(.gameTableHeader)").remove();
    if (content.length) {
      $(".gameTable").append(content);
    }

    // Setup click handlers for the newly added buttons.
    $(".gameTable a").click(function(args) {
        var key = this.getAttribute('key');
        var func = lobby[this.id];
        func(key);
      });

    // Add stripes to the table
    $(".gameTable tr:even").addClass("even");
    $(".gameTable tr:odd").addClass("odd");
  },

  displayJoinDialog: function(creator, key) {
    $('#inviter').text(creator);
    $('#joinGame').attr({key: key});
    blitz.initAndDisplayDialog("#joinDialog");
  },

  displayEnterDialog: function(opponent, key) {
    $('#blitzOpponent').text(opponent);
    $('#goGame').attr({key: key});
    blitz.initAndDisplayDialog("#enterDialog");
    $('#goGame').attr({ref: key});
  },

  goDex: function(gameKey) {
    blitz.clickHandlers.goDex(gameKey);
  },

  goGame: function(gameKey) {
    blitz.clickHandlers.goGame(gameKey);
  },

  joinGame: function(gameKey) {
    var options = {
      url: "/game_ajax/" + gameKey + "/join",
      // We don't actually need to send the key up (since it's already in
      // the URL) but some proxies don't like having a zero-length body for
      // POSTs so we need to put something in there.
      data: {gameKey: gameKey},
      type: "POST",
      success: function() {lobby.goGame(gameKey); },
      error: function() {
        alert("Could not join game");
        lobby.forceRefresh();
      }
    }
    if (lobby.dexMap[gameKey]) {
      options.url = "/dex_ajax/" + gameKey + "/join";
      options.success = function() {lobby.goDex(gameKey); };
    }
    $.ajax(options);
  },

  // Given the key to a game, delete it
  deleteGame: function(gameKey) {
    $(".busy").show();
    var options = {
      url: "/game_ajax/" + gameKey,
      type: "DELETE",
      success: lobby.forceRefresh
    };
    if (lobby.dexMap[gameKey]) {
      options.url = "/dex_ajax/" + gameKey;
    }
    $.ajax(options);
  },

  // Displays a list of players in the window, and updates the chat window
  // when people enter/leave
  updatePlayerList: function(playerList) {
    var playerText = playerList.join("
");
    $(".playerList").html(playerText);

    $("#lobbyStatus").html("Blitz Lobby
" +
        (playerList.length == 1 ?
         "1 player" : "" + playerList.length + " players") +
         " in lobby");

    // Make a map of the players in the list
    var playerMap = {};
    jQuery.each(playerList, function(index, player) {
        playerMap[player] = true;
      });

    var chatStr = "";
    if (lobby.playerMap) {
      // Figure out the differences between the two maps and display who
      // joined/left the lobby
      for (oldPlayer in lobby.playerMap) {
        if (!playerMap[oldPlayer]) {
          chatStr += oldPlayer + " has left the lobby
";
        }
      }
      for (newPlayer in playerMap) {
        if (!lobby.playerMap[newPlayer]) {
          chatStr += newPlayer + " has entered the lobby
";
        }
      }
      chat.appendStringToChat(chatStr);
    }
    lobby.playerMap = playerMap;

  },

  leave: function() {
    // Tell the server we're leaving (this might fail since we are unloading,
    // but it's the best we can do)
    var options = {
      dataType: "json",
      url: "/lobby_ajax/" + blitz.user_email,
      type: "DELETE"
    };
    $.ajax(options);
  },

  sendChat: function() {
    var data = $("#chatInput").val();
    data = jQuery.trim(data);
    if (data.length > 0) {
      $("#chatInput").val("");
      var options = {
        url: "/lobby_ajax/" + blitz.user_email + "/chat",
        data: {chat: data},
        type: "POST",
        error: blitz.retryOnFail,
        success: lobby.forceRefresh
      };
      $.ajax(options);
    }
  },

  // Submits a game offer to the server
  sendGameOffer: function() {
    var offer = $("#offerForm").serialize();
    lobby.uploadGameInvite(offer);
  },

  uploadGameInvite: function(offer) {
    $(".busy").show();
    var options = {
      url: "/game_ajax?" + offer,
      // Need to send something up in the PUT body, as some proxies reject
      // PUT and POST requests with empty bodies
      data: offer,
      type: "PUT",
      error: blitz.retryOnFail,
      success: lobby.forceRefresh
    };
    re = RegExp('game_type=dex', 'i');
    if (offer.search(re) != -1) {
      options.url = "/dex_ajax?" + offer;
    }
    $.ajax(options);
    $.modal.close();
  },

  sendInvite: function() {
    if (lobby.validateInvite("#inviteForm")) {
      var invitation = $("#inviteForm").serialize();
      lobby.uploadGameInvite(invitation);
    }
  },

  validateInvite: function(form) {
    // Make sure that if there's an email input, the contents match the regexp.
    var success = true;
    $(form).find("#email").each(function(index) {
        // See if it matches our regexp - this isn't a perfect email regexp,
        // but it basically allows xxx@xxx.xxx, xxx@xxx.xxx.xxx, etc.
        var regexp = /^\w+@\w+(\.\w+)+$/;
        var err;
        if (!this.value.match(regexp)) {
          err = "Please enter a valid email address";
        } else if (this.value.toLowerCase() == blitz.user_email.toLowerCase()) {
          err = "You cannot invite yourself to a game";
        }
        if (err) {
          success = false;
          $(form).
            find(".error").
            text(err).
            show();
        }
      });

    return success;
  }

};
<commit_msg>Put the switch inside goGame to capture all attempts.<commit_after>/*
Copyright 2007 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
 * Javascript code specific to the lobby page
 */
$(document).ready(function() {
    // Start refreshing the page as soon as we're loaded
    lobby.refresh();

    // Initialize the chat text input to send contents when enter pressed
    $("#chatInput").keypress(function(e) {
        if (e.keyCode == 13) {
          lobby.sendChat();
        }
      });

  });

$(window).unload(function() {
    // When we leave the page, notify the server
    lobby.leaving=true;
    lobby.leave();
  });

var lobby = {
  GAME_STATUS_ACTIVE:2,

  msgId: 0,
  // The games the user doesn't want to be prompted about
  ignoreMap: {},
  // The games that are dex battles.
  dexMap: {},

  forceRefresh: function() {
    // If there's not already a refresh in process, refresh immediately.
    if (lobby.refreshTimer) {
      // There's a timer, which means there's no refresh in process, so
      // fire off the refresh - this will stop the existing timer so we don't
      // get a double update.
      lobby.refresh();
    }
  },

  // Calls the server to refresh the player list, chat window, and open games
  refresh: function() {
    // Stop any existing timer
    if (lobby.refreshTimer) {
      window.clearTimeout(lobby.refreshTimer);
      delete lobby.refreshTimer;
    }
    // Turn on to display busy cursor during ajax calls for debugging
    // $(".busy").show();
    var options = {
      url: "/lobby_ajax/" + blitz.user_email + "/" + lobby.msgId,
      // Add random param to URL to avoid browser caching of HTTP GETs
      data: {z: new Date().getTime()},
      dataType: "json",
      error: blitz.retryOnFail,
      success: lobby.handleAjaxResponse
    };
    $.ajax(options);
  },

  handleAjaxResponse: function(data) {
    // We got our response - make sure the busy cursor is off
    $(".busy").hide();
    lobby.msgId = data.chat_list.msg_id;

    // Convert the player name array to a list we can display
    lobby.updatePlayerList(data.player_list);

    // Update the chat
    chat.updateChat(data.chat_list.data);

    lobby.updateGameList(data.game_list);

    // Refresh the data every 5 secs
    lobby.refreshTimer = window.setTimeout(lobby.refresh, 5*1000);
  },

  updateGameList: function(gameList) {
    // Generate content of game table based on what was sent to us
    content = "";
    jQuery.each(gameList, function(index, game) {
        content += "
" + game.creator + "	" +
          (game.opponent ? game.opponent : " ") + "	" +
          (game.time_limit ?  (game.time_limit + " min") : "untimed") +
           "	";
        if (game.type == 'dex') {
          lobby.dexMap[game.key] = true;
        }
        // OK, figure out what actions we have on this game. Choices are:
        // 1) Delete - if is_participant && opponent = null
        // 2) Join - if !is_participant && opponent = null
        // 3) Watch - if !is_participant && opponent = null
        //
        // We also check to see if the user is an invitee or if he's a
        // participant in an active blitz game, in which case we prompt him to
        // enter the game immediately.
        action = [];
        if (game.is_participant || game.is_invitee) {
          // Haven't prompted about this game before
          if (game.is_invitee) {
            // Display an invite dialog (after setting the flag so we don't
            // prompt about this game again)
            if (!lobby.ignoreMap[game.key] && !blitz.dialogActive()) {
              lobby.ignoreMap[game.key] = true;
              lobby.displayJoinDialog(game.creator, game.key);
            }
            action.push({id: "joinGame", label: "Join"});
          } else {
            // If the user is a participant in an active blitz game, warn
            // them and let them enter.
            if (!lobby.ignoreMap[game.key] && !blitz.dialogActive() &&
                game.status == lobby.GAME_STATUS_ACTIVE && game.time_limit) {
              // Don't prompt about this game again.
              lobby.ignoreMap[game.key] = true;
              lobby.displayEnterDialog(game.is_creator ?
                                       game.opponent : game.creator, game.key);
            }
            if (game.status == lobby.GAME_STATUS_ACTIVE) {
              action.push({id: "goGame", label: "Go"});
            }
          }
          if (game.can_delete) {
            action.push({id: "deleteGame", label: "Delete"});
          }
        } else if (game.status == lobby.GAME_STATUS_ACTIVE) {
          // Not a participant, and the game is full - action = watch
          action.push({id: "goGame", label: "Watch"});
        } else {
          // Not a participant, and there's no opponent yet - action = Join
          action.push({id: "joinGame", label: "Join"});
        }
        if (action.length == 0) {
          content += " ";
        } else {
          jQuery.each(action, function(index, item) {
              if (index > 0) {
                content += " ";
              }
              content += '' + item.label + "";
            });
        }
        content += "
";
      });
    // Remove the old games, replace with new content
    $(".gameTable tr:not(.gameTableHeader)").remove();
    if (content.length) {
      $(".gameTable").append(content);
    }

    // Setup click handlers for the newly added buttons.
    $(".gameTable a").click(function(args) {
        var key = this.getAttribute('key');
        var func = lobby[this.id];
        func(key);
      });

    // Add stripes to the table
    $(".gameTable tr:even").addClass("even");
    $(".gameTable tr:odd").addClass("odd");
  },

  displayJoinDialog: function(creator, key) {
    $('#inviter').text(creator);
    $('#joinGame').attr({key: key});
    blitz.initAndDisplayDialog("#joinDialog");
  },

  displayEnterDialog: function(opponent, key) {
    $('#blitzOpponent').text(opponent);
    $('#goGame').attr({key: key});
    blitz.initAndDisplayDialog("#enterDialog");
    $('#goGame').attr({ref: key});
  },

  goGame: function(gameKey) {
    if (lobby.dexMap[gameKey]) {
      blitz.clickHandlers.goDex(gameKey);
    } else {
      blitz.clickHandlers.goGame(gameKey);
    }
  },

  joinGame: function(gameKey) {
    var options = {
      url: "/game_ajax/" + gameKey + "/join",
      // We don't actually need to send the key up (since it's already in
      // the URL) but some proxies don't like having a zero-length body for
      // POSTs so we need to put something in there.
      data: {gameKey: gameKey},
      type: "POST",
      success: function() {lobby.goGame(gameKey); },
      error: function() {
        alert("Could not join game");
        lobby.forceRefresh();
      }
    }
    if (lobby.dexMap[gameKey]) {
      options.url = "/dex_ajax/" + gameKey + "/join";
    }
    $.ajax(options);
  },

  // Given the key to a game, delete it
  deleteGame: function(gameKey) {
    $(".busy").show();
    var options = {
      url: "/game_ajax/" + gameKey,
      type: "DELETE",
      success: lobby.forceRefresh
    };
    if (lobby.dexMap[gameKey]) {
      options.url = "/dex_ajax/" + gameKey;
    }
    $.ajax(options);
  },

  // Displays a list of players in the window, and updates the chat window
  // when people enter/leave
  updatePlayerList: function(playerList) {
    var playerText = playerList.join("
");
    $(".playerList").html(playerText);

    $("#lobbyStatus").html("Blitz Lobby
" +
        (playerList.length == 1 ?
         "1 player" : "" + playerList.length + " players") +
         " in lobby");

    // Make a map of the players in the list
    var playerMap = {};
    jQuery.each(playerList, function(index, player) {
        playerMap[player] = true;
      });

    var chatStr = "";
    if (lobby.playerMap) {
      // Figure out the differences between the two maps and display who
      // joined/left the lobby
      for (oldPlayer in lobby.playerMap) {
        if (!playerMap[oldPlayer]) {
          chatStr += oldPlayer + " has left the lobby
";
        }
      }
      for (newPlayer in playerMap) {
        if (!lobby.playerMap[newPlayer]) {
          chatStr += newPlayer + " has entered the lobby
";
        }
      }
      chat.appendStringToChat(chatStr);
    }
    lobby.playerMap = playerMap;

  },

  leave: function() {
    // Tell the server we're leaving (this might fail since we are unloading,
    // but it's the best we can do)
    var options = {
      dataType: "json",
      url: "/lobby_ajax/" + blitz.user_email,
      type: "DELETE"
    };
    $.ajax(options);
  },

  sendChat: function() {
    var data = $("#chatInput").val();
    data = jQuery.trim(data);
    if (data.length > 0) {
      $("#chatInput").val("");
      var options = {
        url: "/lobby_ajax/" + blitz.user_email + "/chat",
        data: {chat: data},
        type: "POST",
        error: blitz.retryOnFail,
        success: lobby.forceRefresh
      };
      $.ajax(options);
    }
  },

  // Submits a game offer to the server
  sendGameOffer: function() {
    var offer = $("#offerForm").serialize();
    lobby.uploadGameInvite(offer);
  },

  uploadGameInvite: function(offer) {
    $(".busy").show();
    var options = {
      url: "/game_ajax?" + offer,
      // Need to send something up in the PUT body, as some proxies reject
      // PUT and POST requests with empty bodies
      data: offer,
      type: "PUT",
      error: blitz.retryOnFail,
      success: lobby.forceRefresh
    };
    re = RegExp('game_type=dex', 'i');
    if (offer.search(re) != -1) {
      options.url = "/dex_ajax?" + offer;
    }
    $.ajax(options);
    $.modal.close();
  },

  sendInvite: function() {
    if (lobby.validateInvite("#inviteForm")) {
      var invitation = $("#inviteForm").serialize();
      lobby.uploadGameInvite(invitation);
    }
  },

  validateInvite: function(form) {
    // Make sure that if there's an email input, the contents match the regexp.
    var success = true;
    $(form).find("#email").each(function(index) {
        // See if it matches our regexp - this isn't a perfect email regexp,
        // but it basically allows xxx@xxx.xxx, xxx@xxx.xxx.xxx, etc.
        var regexp = /^\w+@\w+(\.\w+)+$/;
        var err;
        if (!this.value.match(regexp)) {
          err = "Please enter a valid email address";
        } else if (this.value.toLowerCase() == blitz.user_email.toLowerCase()) {
          err = "You cannot invite yourself to a game";
        }
        if (err) {
          success = false;
          $(form).
            find(".error").
            text(err).
            show();
        }
      });

    return success;
  }

};
<|endoftext|>


<commit_before>/**
 * Copyright reelyActive 2017
 * We believe in an open Internet of Things
 */

var dgram = require('dgram');
var util = require('util');
var events = require('events');
var time = require('../../common/util/time');
var reelPacket = require('../../decoders/protocols/reelpacket');

// Constants
var PROTOCOL = 'bluecats-udp';
var RSSI_OFFSET = 100;
var ANNOUNCE_INTERVAL_MILLISECONDS = 15000;

/**
 * BlueCatsUdpListener Class
 * Listens for BlueCats data on a UDP port and emits this data
 * @param {string} path Path to UDP host and port, ex: 192.168.1.1:50000.
 * @constructor
 * @extends {events.EventEmitter}
 */
function BlueCatsUdpListener(path) {
  var self = this;
  var pathElements = path.split(':');
  var host = pathElements[0];
  var port = pathElements[1];
  var server = dgram.createSocket('udp4');

  self.receivers = [];
  self.lastAnnounceTimes = {};

  events.EventEmitter.call(this);

  server.on('listening', function () {
    var address = server.address();
    console.log('UDP Listening (BlueCats) on ' + address.address + ':'
                + address.port);
  });
  server.on('message', function (data, remote) {
    var origin = remote.address + ':' + remote.port;
    var timestamp = time.getCurrent();
    var reelData = handleBlueCatsData(self, data, origin);
    if(reelData) {
      self.emit('data', reelData, origin, timestamp);
    }
  });
  server.on('error', function (err) {
    server.close();
    self.emit('error', err);
  });

  server.bind(port, host);
}
util.inherits(BlueCatsUdpListener, events.EventEmitter);

/**
 * Handle the BlueCats data and return a reel packet, if applicable.
 * @param {Buffer} data UDP payload data.
 * @param {String} origin The origin of the UDP packet.
 */
function handleBlueCatsData(instance, data, origin) {
  var json;

  try {
    json = JSON.parse(data.toString());
  }
  catch(err) {
    // Perhaps handle errors in future
  }

  // Abort if any of the requisite properties are missing
  if(!json.hasOwnProperty('edgeMAC') || !json.hasOwnProperty('beaconMAC') ||
     !json.hasOwnProperty('rssiSmooth') || !json.hasOwnProperty('adData')) {
    return null;
  }

  handleBlueCatsEdge(instance, json.edgeMAC.toLowerCase(), origin);

  var blePacketLengthBytes = (json.adData.length / 2) + 6;
  var reelHeader = ('0' + (blePacketLengthBytes + 2).toString(16)).substr(-2);
  var blePacketLength = ('0' + blePacketLengthBytes.toString(16)).substr(-2);
  var bleAddress = json.beaconMAC.substr(10,2) +
                   json.beaconMAC.substr(8,2) +
                   json.beaconMAC.substr(6,2) +
                   json.beaconMAC.substr(4,2) +
                   json.beaconMAC.substr(2,2) +
                   json.beaconMAC.substr(0,2);

  var reelData = reelPacket.PREFIX;                    // Prefix
  reelData += reelHeader;                              // Header
  reelData += '01';                                    // Count
  reelData += 4;                                       // txAdd (Always random)
  reelData += 0;                                       // Type (FIX!)
  reelData += blePacketLength;                         // Length
  reelData += bleAddress.toLowerCase();                // Address
  reelData += json.adData.toLowerCase();               // PDU
  reelData += '00';                                    // Reel pos (FIX!)
  reelData += json.rssiSmooth + RSSI_OFFSET;           // RSSI

  return reelData;
}

/**
 * Handle the BlueCats edge relay, sending an announce if it is new.
 * @param {String} receiverId The Edge Relay MAC.
 * @param {String} origin The origin of the UDP packet.
 */
function handleBlueCatsEdge(instance, receiverId, origin) {

  // New receiverId, add to list
  if(instance.receivers.indexOf(receiverId) < 0) {
    instance.receivers.push(receiverId);
  }

  var lastAnnounceTime = instance.lastAnnounceTimes[receiverId];
  var currentTime = new Date();

  // Due for a periodic announce message
  if(!lastAnnounceTime ||
     ((currentTime - ANNOUNCE_INTERVAL_MILLISECONDS) > lastAnnounceTime)) {
    var reelOffset = ('0' + (instance.receivers.length - 1).toString(16))
                     .substr(-2);
    var simulatedPacket = new Buffer('aaaa70' + reelOffset +
                                     receiverId.substr(4,8) +
                                     '00000000000000000000000000000000','hex');
    instance.emit('data', simulatedPacket, origin, currentTime);
    instance.lastAnnounceTimes[receiverId] = currentTime;
  }
}

module.exports = BlueCatsUdpListener;
module.exports.PROTOCOL = PROTOCOL;
<commit_msg>Improved RSSI conversion<commit_after>/**
 * Copyright reelyActive 2017
 * We believe in an open Internet of Things
 */

var dgram = require('dgram');
var util = require('util');
var events = require('events');
var time = require('../../common/util/time');
var reelPacket = require('../../decoders/protocols/reelpacket');

// Constants
var PROTOCOL = 'bluecats-udp';
var RSSI_OFFSET = 100;
var ANNOUNCE_INTERVAL_MILLISECONDS = 15000;

/**
 * BlueCatsUdpListener Class
 * Listens for BlueCats data on a UDP port and emits this data
 * @param {string} path Path to UDP host and port, ex: 192.168.1.1:50000.
 * @constructor
 * @extends {events.EventEmitter}
 */
function BlueCatsUdpListener(path) {
  var self = this;
  var pathElements = path.split(':');
  var host = pathElements[0];
  var port = pathElements[1];
  var server = dgram.createSocket('udp4');

  self.receivers = [];
  self.lastAnnounceTimes = {};

  events.EventEmitter.call(this);

  server.on('listening', function () {
    var address = server.address();
    console.log('UDP Listening (BlueCats) on ' + address.address + ':'
                + address.port);
  });
  server.on('message', function (data, remote) {
    var origin = remote.address + ':' + remote.port;
    var timestamp = time.getCurrent();
    var reelData = handleBlueCatsData(self, data, origin);
    if(reelData) {
      self.emit('data', reelData, origin, timestamp);
    }
  });
  server.on('error', function (err) {
    server.close();
    self.emit('error', err);
  });

  server.bind(port, host);
}
util.inherits(BlueCatsUdpListener, events.EventEmitter);

/**
 * Handle the BlueCats data and return a reel packet, if applicable.
 * @param {Buffer} data UDP payload data.
 * @param {String} origin The origin of the UDP packet.
 */
function handleBlueCatsData(instance, data, origin) {
  var json;

  try {
    json = JSON.parse(data.toString());
  }
  catch(err) {
    // Perhaps handle errors in future
  }

  // Abort if any of the requisite properties are missing
  if(!json.hasOwnProperty('edgeMAC') || !json.hasOwnProperty('beaconMAC') ||
     !json.hasOwnProperty('rssiSmooth') || !json.hasOwnProperty('adData')) {
    return null;
  }

  handleBlueCatsEdge(instance, json.edgeMAC.toLowerCase(), origin);

  var blePacketLengthBytes = (json.adData.length / 2) + 6;
  var reelHeader = ('0' + (blePacketLengthBytes + 2).toString(16)).substr(-2);
  var blePacketLength = ('0' + blePacketLengthBytes.toString(16)).substr(-2);
  var bleAddress = json.beaconMAC.substr(10,2) +
                   json.beaconMAC.substr(8,2) +
                   json.beaconMAC.substr(6,2) +
                   json.beaconMAC.substr(4,2) +
                   json.beaconMAC.substr(2,2) +
                   json.beaconMAC.substr(0,2);

  var reelData = reelPacket.PREFIX;                    // Prefix
  reelData += reelHeader;                              // Header
  reelData += '01';                                    // Count
  reelData += 4;                                       // txAdd (Always random)
  reelData += 0;                                       // Type (FIX!)
  reelData += blePacketLength;                         // Length
  reelData += bleAddress.toLowerCase();                // Address
  reelData += json.adData.toLowerCase();               // PDU
  reelData += '00';                                    // Reel pos (FIX!)
  reelData += ('00' + (json.rssiSmooth + RSSI_OFFSET).toString(16)).substr(-2);
                                                       // RSSI
  return reelData;
}

/**
 * Handle the BlueCats edge relay, sending an announce if it is new.
 * @param {String} receiverId The Edge Relay MAC.
 * @param {String} origin The origin of the UDP packet.
 */
function handleBlueCatsEdge(instance, receiverId, origin) {

  // New receiverId, add to list
  if(instance.receivers.indexOf(receiverId) < 0) {
    instance.receivers.push(receiverId);
  }

  var lastAnnounceTime = instance.lastAnnounceTimes[receiverId];
  var currentTime = new Date();

  // Due for a periodic announce message
  if(!lastAnnounceTime ||
     ((currentTime - ANNOUNCE_INTERVAL_MILLISECONDS) > lastAnnounceTime)) {
    var reelOffset = ('0' + (instance.receivers.length - 1).toString(16))
                     .substr(-2);
    var simulatedPacket = new Buffer('aaaa70' + reelOffset +
                                     receiverId.substr(4,8) +
                                     '00000000000000000000000000000000','hex');
    instance.emit('data', simulatedPacket, origin, currentTime);
    instance.lastAnnounceTimes[receiverId] = currentTime;
  }
}

module.exports = BlueCatsUdpListener;
module.exports.PROTOCOL = PROTOCOL;
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict';

const async = require('async');
const _ = require('lodash');

const request = require('request');
const io = require('socket.io-client');
const wildcardPatch = require('socketio-wildcard')(io.Manager);

const deepEqual = require('deep-equal');
const debug = require('debug')('socketio');
const engineUtil = require('./engine_util');
const EngineHttp = require('./engine_http');
const template = engineUtil.template;
module.exports = SocketIoEngine;

function SocketIoEngine(script) {
  this.config = script.config;

  this.socketioOpts = this.config.socketio || {};
  this.httpDelegate = new EngineHttp(script);
}

SocketIoEngine.prototype.createScenario = function(scenarioSpec, ee) {
  var self = this;
  let tasks = _.map(scenarioSpec.flow, function(rs) {
    if (rs.think) {
      return engineUtil.createThink(rs, _.get(self.config, 'defaults.think', {}));
    }
    return self.step(rs, ee);
  });

  return self.compile(tasks, scenarioSpec.flow, ee);
};

function markEndTime(ee, context, startedAt) {
  let endedAt = process.hrtime(startedAt);
  let delta = (endedAt[0] * 1e9) + endedAt[1];
  ee.emit('response', delta, 0, context._uid);
}

function isResponseRequired(spec) {
  return (spec.emit && spec.emit.response && spec.emit.response.channel);
}

function isAcknowledgeRequired(spec) {
    return (spec.emit && spec.emit.acknowledge);
}

function processResponse(ee, data, response, context, callback) {
  // Do we have supplied data to validate?
  if (response.data && !deepEqual(data, response.data)) {
    debug(data);
    let err = 'data is not valid';
    ee.emit('error', err);
    return callback(err, context);
  }

  // If no capture or match specified, then we consider it a success at this point...
  if (!response.capture && !response.match) {
    return callback(null, context);
  }

  // Construct the (HTTP) response...
  let fauxResponse = {body: JSON.stringify(data)};

  // Handle the capture or match clauses...
  engineUtil.captureOrMatch(response, fauxResponse, context, function(err, result) {
    // Were we unable to invoke captureOrMatch?
    if (err) {
      debug(data);
      ee.emit('error', err);
      return callback(err, context);
    }

    // Do we have any failed matches?
    let haveFailedMatches = _.some(result.matches, function(v, k) {
      return !v.success;
    });

    // How to handle failed matches?
    if (haveFailedMatches) {
      // TODO: Should log the details of the match somewhere
      ee.emit('error', 'Failed match');
      return callback(new Error('Failed match'), context);
    } else {
      // Emit match events...
      _.each(result.matches, function(v, k) {
        ee.emit('match', v.success, {
          expected: v.expected,
          got: v.got,
          expression: v.expression
        });
      });

      // Populate the context with captured values
      _.each(result.captures, function(v, k) {
        context.vars[k] = v;
      });

      // Replace the base object context
      // Question: Should this be JSON object or String?
      context.vars.$ = fauxResponse.body;

      // Increment the success count...
      context._successCount++;

      return callback(null, context);
    }
  });
}

SocketIoEngine.prototype.step = function (requestSpec, ee) {
  let self = this;

  if (requestSpec.loop) {
    let steps = _.map(requestSpec.loop, function(rs) {
      if (!rs.emit) {
        return self.httpDelegate.step(rs, ee);
      }
      return self.step(rs, ee);
    });

    return engineUtil.createLoopWithCount(
      requestSpec.count || -1,
      steps,
      {
        loopValue: requestSpec.loopValue,
        overValues: requestSpec.over
      }
    );
  }

  let f = function(context, callback) {
    // Only process emit requests; delegate the rest to the HTTP engine (or think utility)
    if (requestSpec.think) {
      return engineUtil.createThink(requestSpec, _.get(self.config, 'defaults.think', {}));
    }
    if (!requestSpec.emit) {
      let delegateFunc = self.httpDelegate.step(requestSpec, ee);
      return delegateFunc(context, callback);
    }
    ee.emit('request');
    let startedAt = process.hrtime();
    let socketio = context.sockets[requestSpec.emit.namespace] || null;

    if (!(requestSpec.emit && requestSpec.emit.channel && socketio)) {
      return ee.emit('error', 'invalid arguments');
    }

    let outgoing = {
      channel: template(requestSpec.emit.channel, context),
      data: template(requestSpec.emit.data, context)
    };

    let endCallback = function (err, context) {
      if (err) {
        debug(err);
      }

      if (isAcknowledgeRequired(requestSpec)) {
        // Acknowledge required so add callback to emit
        socketio.emit(outgoing.channel, outgoing.data, function () {
          let response = {
            data: template(requestSpec.emit.acknowledge.data, context),
            capture: template(requestSpec.emit.acknowledge.capture, context),
            match: template(requestSpec.emit.acknowledge.match, context)
          };
          // Make sure data, capture or match has a default json spec for parsing socketio responses
          _.each(response, function (r) {
            if (_.isPlainObject(r) && !('json' in r)) {
              r.json = '$.0'; // Default to the first callback argument
            }
          });
          // Acknowledge data can take up multiple arguments of the emit callback
          processResponse(ee, arguments, response, context, function (err) {
            if (!err) {
              markEndTime(ee, context, startedAt);
            }
            return callback(err, context);
          });
        });
      } else {
        // No acknowledge data is expected, so emit without a listener
        socketio.emit(outgoing.channel, outgoing.data);
        markEndTime(ee, context, startedAt);
        return callback(null, context);
      }
    };

    if (isResponseRequired(requestSpec)) {
      let response = {
        channel: template(requestSpec.emit.response.channel, context),
        data: template(requestSpec.emit.response.data, context),
        capture: template(requestSpec.emit.response.capture, context),
        match: template(requestSpec.emit.response.match, context)
      };
      // Listen for the socket.io response on the specified channel
      let done = false;
      socketio.on(response.channel, function receive(data) {
        done = true;
        processResponse(ee, data, response, context, function(err) {
          if (!err) {
            markEndTime(ee, context, startedAt);
          }
          // Stop listening on the response channel
          socketio.off(response.channel);
          return endCallback(err, context);
        });
      });
      // Send the data on the specified socket.io channel
      socketio.emit(outgoing.channel, outgoing.data);
      // If we don't get a response within the timeout, fire an error
      let waitTime = self.config.timeout || 10;
      waitTime *= 1000;
      setTimeout(function responseTimeout() {
        if (!done) {
          let err = 'response timeout';
          ee.emit('error', err);
          return callback(err, context);
        }
      }, waitTime);
    } else {
      endCallback(null, context);
    }
  };

  function preStep(context, callback){
    // Set default namespace in emit action
    requestSpec.emit.namespace = template(requestSpec.emit.namespace, context) || "/";

    self.loadContextSocket(requestSpec.emit.namespace, context, function(err, socket){
      if(err) {
        debug(err);
        ee.emit('error', err.message);
        return callback(err, context);
      }

      return f(context, callback);
    });
  }

  if(requestSpec.emit) {
    return preStep;
  } else {
    return f;
  }
};

SocketIoEngine.prototype.loadContextSocket = function(namespace, context, cb) {
  context.sockets = context.sockets || {};

  if(!context.sockets[namespace]) {
    let target = this.config.target + namespace;
    let tls = this.config.tls || {};

    const socketioOpts = template(this.socketioOpts, context);
    let options = _.extend(
      {},
      socketioOpts, // templated
      tls
    );

    let socket = io(target, options);
    context.sockets[namespace] = socket;
    wildcardPatch(socket);

    socket.on('*', function () {
      context.__receivedMessageCount++;
    });

    socket.once('connect', function() {
      cb(null, socket);
    });
    socket.once('connect_error', function(err) {
      cb(err, null);
    });
  } else {
    return cb(null, context.sockets[namespace]);
  }
};

SocketIoEngine.prototype.closeContextSockets = function (context) {
  // if(context.socketio) {
  //   context.socketio.disconnect();
  // }
  if(context.sockets && Object.keys(context.sockets).length > 0) {
    var namespaces = Object.keys(context.sockets);
    namespaces.forEach(function(namespace){
      context.sockets[namespace].disconnect();
    });
  }
};

SocketIoEngine.prototype.compile = function (tasks, scenarioSpec, ee) {
  let config = this.config;
  let self = this;

  function zero(callback, context) {
    context.__receivedMessageCount = 0;
    ee.emit('started');
    self.loadContextSocket('/', context, function done(err) {
      if (err) {
        ee.emit('error', err);
        return callback(err, context);
      }

      return callback(null, context);
    });
  }

  return function scenario(initialContext, callback) {
    initialContext._successCount = 0;
    initialContext._jar = request.jar();
    initialContext._pendingRequests = _.size(
        _.reject(scenarioSpec, function(rs) {
          return (typeof rs.think === 'number');
        }));

    let steps = _.flatten([
      function z(cb) {
        return zero(cb, initialContext);
      },
      tasks
    ]);

    async.waterfall(
        steps,
        function scenarioWaterfallCb(err, context) {
          if (err) {
            debug(err);
          }
          if (context) {
            self.closeContextSockets(context);
          }
          return callback(err, context);
        });
  };
};
<commit_msg>fix: event emit once more when 'response' is comming of socketio engine.<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict';

const async = require('async');
const _ = require('lodash');

const request = require('request');
const io = require('socket.io-client');
const wildcardPatch = require('socketio-wildcard')(io.Manager);

const deepEqual = require('deep-equal');
const debug = require('debug')('socketio');
const engineUtil = require('./engine_util');
const EngineHttp = require('./engine_http');
const template = engineUtil.template;
module.exports = SocketIoEngine;

function SocketIoEngine(script) {
  this.config = script.config;

  this.socketioOpts = this.config.socketio || {};
  this.httpDelegate = new EngineHttp(script);
}

SocketIoEngine.prototype.createScenario = function(scenarioSpec, ee) {
  var self = this;
  let tasks = _.map(scenarioSpec.flow, function(rs) {
    if (rs.think) {
      return engineUtil.createThink(rs, _.get(self.config, 'defaults.think', {}));
    }
    return self.step(rs, ee);
  });

  return self.compile(tasks, scenarioSpec.flow, ee);
};

function markEndTime(ee, context, startedAt) {
  let endedAt = process.hrtime(startedAt);
  let delta = (endedAt[0] * 1e9) + endedAt[1];
  ee.emit('response', delta, 0, context._uid);
}

function isResponseRequired(spec) {
  return (spec.emit && spec.emit.response && spec.emit.response.channel);
}

function isAcknowledgeRequired(spec) {
    return (spec.emit && spec.emit.acknowledge);
}

function processResponse(ee, data, response, context, callback) {
  // Do we have supplied data to validate?
  if (response.data && !deepEqual(data, response.data)) {
    debug(data);
    let err = 'data is not valid';
    ee.emit('error', err);
    return callback(err, context);
  }

  // If no capture or match specified, then we consider it a success at this point...
  if (!response.capture && !response.match) {
    return callback(null, context);
  }

  // Construct the (HTTP) response...
  let fauxResponse = {body: JSON.stringify(data)};

  // Handle the capture or match clauses...
  engineUtil.captureOrMatch(response, fauxResponse, context, function(err, result) {
    // Were we unable to invoke captureOrMatch?
    if (err) {
      debug(data);
      ee.emit('error', err);
      return callback(err, context);
    }

    // Do we have any failed matches?
    let haveFailedMatches = _.some(result.matches, function(v, k) {
      return !v.success;
    });

    // How to handle failed matches?
    if (haveFailedMatches) {
      // TODO: Should log the details of the match somewhere
      ee.emit('error', 'Failed match');
      return callback(new Error('Failed match'), context);
    } else {
      // Emit match events...
      _.each(result.matches, function(v, k) {
        ee.emit('match', v.success, {
          expected: v.expected,
          got: v.got,
          expression: v.expression
        });
      });

      // Populate the context with captured values
      _.each(result.captures, function(v, k) {
        context.vars[k] = v;
      });

      // Replace the base object context
      // Question: Should this be JSON object or String?
      context.vars.$ = fauxResponse.body;

      // Increment the success count...
      context._successCount++;

      return callback(null, context);
    }
  });
}

SocketIoEngine.prototype.step = function (requestSpec, ee) {
  let self = this;

  if (requestSpec.loop) {
    let steps = _.map(requestSpec.loop, function(rs) {
      if (!rs.emit) {
        return self.httpDelegate.step(rs, ee);
      }
      return self.step(rs, ee);
    });

    return engineUtil.createLoopWithCount(
      requestSpec.count || -1,
      steps,
      {
        loopValue: requestSpec.loopValue,
        overValues: requestSpec.over
      }
    );
  }

  let f = function(context, callback) {
    // Only process emit requests; delegate the rest to the HTTP engine (or think utility)
    if (requestSpec.think) {
      return engineUtil.createThink(requestSpec, _.get(self.config, 'defaults.think', {}));
    }
    if (!requestSpec.emit) {
      let delegateFunc = self.httpDelegate.step(requestSpec, ee);
      return delegateFunc(context, callback);
    }
    ee.emit('request');
    let startedAt = process.hrtime();
    let socketio = context.sockets[requestSpec.emit.namespace] || null;

    if (!(requestSpec.emit && requestSpec.emit.channel && socketio)) {
      return ee.emit('error', 'invalid arguments');
    }

    let outgoing = {
      channel: template(requestSpec.emit.channel, context),
      data: template(requestSpec.emit.data, context)
    };

    let endCallback = function (err, context, needEmit) {
      if (err) {
        debug(err);
      }

      if (isAcknowledgeRequired(requestSpec)) {
        let ackCallback = function () {
          let response = {
            data: template(requestSpec.emit.acknowledge.data, context),
            capture: template(requestSpec.emit.acknowledge.capture, context),
            match: template(requestSpec.emit.acknowledge.match, context)
          };
          // Make sure data, capture or match has a default json spec for parsing socketio responses
          _.each(response, function (r) {
            if (_.isPlainObject(r) && !('json' in r)) {
              r.json = '$.0'; // Default to the first callback argument
            }
          });
          // Acknowledge data can take up multiple arguments of the emit callback
          processResponse(ee, arguments, response, context, function (err) {
            if (!err) {
              markEndTime(ee, context, startedAt);
            }
            return callback(err, context);
          });
        }

        // Acknowledge required so add callback to emit
        if (needEmit) {
          socketio.emit(outgoing.channel, outgoing.data, ackCallback);
        } else {
          ackCallback();
        }
      } else {
        // No acknowledge data is expected, so emit without a listener
        if (needEmit) {
          socketio.emit(outgoing.channel, outgoing.data);
        }
        markEndTime(ee, context, startedAt);
        return callback(null, context);
      }
    };

    if (isResponseRequired(requestSpec)) {
      let response = {
        channel: template(requestSpec.emit.response.channel, context),
        data: template(requestSpec.emit.response.data, context),
        capture: template(requestSpec.emit.response.capture, context),
        match: template(requestSpec.emit.response.match, context)
      };
      // Listen for the socket.io response on the specified channel
      let done = false;
      socketio.on(response.channel, function receive(data) {
        done = true;
        processResponse(ee, data, response, context, function(err) {
          if (!err) {
            markEndTime(ee, context, startedAt);
          }
          // Stop listening on the response channel
          socketio.off(response.channel);
          return endCallback(err, context, false);
        });
      });
      // Send the data on the specified socket.io channel
      socketio.emit(outgoing.channel, outgoing.data);
      // If we don't get a response within the timeout, fire an error
      let waitTime = self.config.timeout || 10;
      waitTime *= 1000;
      setTimeout(function responseTimeout() {
        if (!done) {
          let err = 'response timeout';
          ee.emit('error', err);
          return callback(err, context);
        }
      }, waitTime);
    } else {
      endCallback(null, context, true);
    }
  };

  function preStep(context, callback){
    // Set default namespace in emit action
    requestSpec.emit.namespace = template(requestSpec.emit.namespace, context) || "/";

    self.loadContextSocket(requestSpec.emit.namespace, context, function(err, socket){
      if(err) {
        debug(err);
        ee.emit('error', err.message);
        return callback(err, context);
      }

      return f(context, callback);
    });
  }

  if(requestSpec.emit) {
    return preStep;
  } else {
    return f;
  }
};

SocketIoEngine.prototype.loadContextSocket = function(namespace, context, cb) {
  context.sockets = context.sockets || {};

  if(!context.sockets[namespace]) {
    let target = this.config.target + namespace;
    let tls = this.config.tls || {};

    const socketioOpts = template(this.socketioOpts, context);
    let options = _.extend(
      {},
      socketioOpts, // templated
      tls
    );

    let socket = io(target, options);
    context.sockets[namespace] = socket;
    wildcardPatch(socket);

    socket.on('*', function () {
      context.__receivedMessageCount++;
    });

    socket.once('connect', function() {
      cb(null, socket);
    });
    socket.once('connect_error', function(err) {
      cb(err, null);
    });
  } else {
    return cb(null, context.sockets[namespace]);
  }
};

SocketIoEngine.prototype.closeContextSockets = function (context) {
  // if(context.socketio) {
  //   context.socketio.disconnect();
  // }
  if(context.sockets && Object.keys(context.sockets).length > 0) {
    var namespaces = Object.keys(context.sockets);
    namespaces.forEach(function(namespace){
      context.sockets[namespace].disconnect();
    });
  }
};

SocketIoEngine.prototype.compile = function (tasks, scenarioSpec, ee) {
  let config = this.config;
  let self = this;

  function zero(callback, context) {
    context.__receivedMessageCount = 0;
    ee.emit('started');
    self.loadContextSocket('/', context, function done(err) {
      if (err) {
        ee.emit('error', err);
        return callback(err, context);
      }

      return callback(null, context);
    });
  }

  return function scenario(initialContext, callback) {
    initialContext._successCount = 0;
    initialContext._jar = request.jar();
    initialContext._pendingRequests = _.size(
        _.reject(scenarioSpec, function(rs) {
          return (typeof rs.think === 'number');
        }));

    let steps = _.flatten([
      function z(cb) {
        return zero(cb, initialContext);
      },
      tasks
    ]);

    async.waterfall(
        steps,
        function scenarioWaterfallCb(err, context) {
          if (err) {
            debug(err);
          }
          if (context) {
            self.closeContextSockets(context);
          }
          return callback(err, context);
        });
  };
};
<|endoftext|>


<commit_before>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

Zotero.QuickCopy = new function() {

	var _initialized = false;
	var _formattedNames = {};

	/*
	 * Return Quick Copy setting object from string, stringified object, or object
	 *
	 * Example string format: "bibliography/html=http://www.zotero.org/styles/apa"
	 *
	 * Quick Copy setting object has the following properties:
	 * - "mode": "bibliography" (for styles) or "export" (for export translators)
	 * - "contentType: "" (plain text output) or "html" (HTML output; for styles
	 *   only)
	 * - "id": style ID or export translator ID
	 * - "locale": locale code (for styles only)
	 */
	this.unserializeSetting = function (setting) {
		var settingObject = {};

		if (typeof setting === 'string') {
			try {
				// First test if string input is a stringified object
				settingObject = JSON.parse(setting);
			} catch (e) {
				// Try parsing as formatted string
				var parsedSetting = setting.match(/(bibliography|export)(?:\/([^=]+))?=(.+)$/);
				if (parsedSetting) {
					settingObject.mode = parsedSetting[1];
					settingObject.contentType = parsedSetting[2] || '';
					settingObject.id = parsedSetting[3];
					settingObject.locale = '';
				}
			}
		} else {
			// Return input if not a string; it might already be an object
			return setting;
		}

		return settingObject;
	};

	this.getFormattedNameFromSetting = function (setting) {
		if (!_initialized) {
			_init();
		}

		var format = this.unserializeSetting(setting);

		var name = _formattedNames[format.mode + "=" + format.id];
		return name ? name : '';
	};

	this.getSettingFromFormattedName = function (name) {
		if (!_initialized) {
			_init();
		}

		for (var setting in _formattedNames) {
			if (_formattedNames[setting] == name) {
				return setting;
			}
		}

		return '';
	};

	this.getFormatFromURL = function(url) {
		var quickCopyPref = Zotero.Prefs.get("export.quickCopy.setting");
		quickCopyPref = JSON.stringify(this.unserializeSetting(quickCopyPref));

		if (!url) {
			return quickCopyPref;
		}

		var ioService = Components.classes["@mozilla.org/network/io-service;1"]
			.getService(Components.interfaces.nsIIOService);
		try {
			var nsIURI = ioService.newURI(url, null, null);
			// Accessing some properties may throw for URIs that do not support those
			// parts. E.g. hostPort throws NS_ERROR_FAILURE for about:blank
			var urlHostPort = nsIURI.hostPort;
			var urlPath = nsIURI.path;
		}
		catch (e) {
			return quickCopyPref;
		}

		var matches = [];

		var sql = "SELECT key AS domainPath, value AS format FROM settings "
			+ "WHERE setting='quickCopySite' AND (key LIKE ? OR key LIKE ?)";
		var urlDomain = urlHostPort.match(/[^\.]+\.[^\.]+$/);
		var rows = Zotero.DB.query(sql, ['%' + urlDomain[0] + '%', '/%']);
		for (let i = 0; i < rows.length; i++) {
			let row = rows[i];
			let domain = row.domainPath.split('/',1)[0];
			let path = row.domainPath.substr(domain.length) || '/';
			let re = new RegExp('(^|[./])' + Zotero.Utilities.quotemeta(domain) + '$', 'i');
			if (re.test(urlHostPort) && urlPath.indexOf(path) === 0) {
				matches.push({
					format: JSON.stringify(this.unserializeSetting(row.format)),
					domainLength: domain.length,
					pathLength: path.length
				});
			}
		}

		// Give priority to longer domains, then longer paths
		var sort = function(a, b) {
			if (a.domainLength > b.domainLength) {
				return -1;
			}
			else if (a.domainLength  b.pathLength) {
				return -1;
			}
			else if (a.pathLength < b.pathLength) {
				return 1;
			}

			return -1;
		};

		if (matches.length) {
			matches.sort(sort);
			return matches[0].format;
		} else {
			return quickCopyPref;
		}
	};

	/*
	 * Get text and (when applicable) HTML content from items
	 *
	 * |items| is an array of Zotero.Item objects
	 *
	 * |format| may be a Quick Copy format string
	 * (e.g. "bibliography=http://www.zotero.org/styles/apa")
	 * or an Quick Copy format object
	 *
	 * |callback| is only necessary if using an export format and should be
	 * a function suitable for Zotero.Translate.setHandler, taking parameters
	 * |obj| and |worked|. The generated content should be placed in obj.string
	 * and |worked| should be true if the operation is successful.
	 *
	 * If bibliography format, the process is synchronous and an object
	 * contain properties 'text' and 'html' is returned.
	 */
	this.getContentFromItems = function (items, format, callback, modified) {
		if (items.length > Zotero.Prefs.get('export.quickCopy.dragLimit')) {
			Zotero.debug("Skipping quick copy for " + items.length + " items");
			return false;
		}

		format = this.unserializeSetting(format);

		if (format.mode == 'export') {
			var translation = new Zotero.Translate.Export;
			translation.noWait = true;	// needed not to break drags
			translation.setItems(items);
			translation.setTranslator(format.id);
			translation.setHandler("done", callback);
			translation.translate();
			return true;
		}
		else if (format.mode == 'bibliography') {
			// Move notes to separate array
			var allNotes = true;
			var notes = [];
			for (var i=0; i<items.length; i++) {
				if (items[i].isNote()) {
					notes.push(items.splice(i, 1)[0]);
					i--;
				}
				else {
					allNotes = false;
				}
			}

			// If all notes, export full content
			if (allNotes) {
				var content = [],
					parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
						.createInstance(Components.interfaces.nsIDOMParser),
					doc = parser.parseFromString('
', 'text/html'),
					textDoc = parser.parseFromString('
', 'text/html'),
					container = doc.documentElement,
					textContainer = textDoc.documentElement;
				for (var i=0; i<notes.length; i++) {
					var div = doc.createElement("div");
					div.className = "zotero-note";
					div.insertAdjacentHTML('afterbegin', notes[i].getNote());
					container.appendChild(div);
					textContainer.appendChild(textDoc.importNode(div, true));
				}

				// Raw HTML output
				var html = container.outerHTML;

				// Add placeholders for newlines between notes
				if (notes.length > 1) {
					var divs = Zotero.Utilities.xpath(container, "div"),
						textDivs = Zotero.Utilities.xpath(textContainer, "div");
					for (var i=1, len=divs.length; i<len; i++) {
						var p = doc.createElement("p");
						p.appendChild(doc.createTextNode("--------------------------------------------------"));
						container.insertBefore(p, divs[i]);
						textContainer.insertBefore(textDoc.importNode(p, true), textDivs[i]);
					}
				}

				const BLOCKQUOTE_PREFS = {
					'export.quickCopy.quoteBlockquotes.richText':doc,
					'export.quickCopy.quoteBlockquotes.plainText':textDoc
				};
				for(var pref in BLOCKQUOTE_PREFS) {
					if (Zotero.Prefs.get(pref)) {
						var currentDoc = BLOCKQUOTE_PREFS[pref];
						// Add quotes around blockquote paragraphs
						var addOpenQuote = Zotero.Utilities.xpath(currentDoc, "//blockquote/p[1]"),
							addCloseQuote = Zotero.Utilities.xpath(currentDoc, "//blockquote/p[last()]");
						for(var i=0; i<addOpenQuote.length; i++) {
							addOpenQuote[i].insertBefore(currentDoc.createTextNode("\u201c"),
								addOpenQuote[i].firstChild);
						}
						for(var i=0; i<addCloseQuote.length; i++) {
							addCloseQuote[i].appendChild(currentDoc.createTextNode("\u201d"));
						}
					}
				}

				//
				// Text-only adjustments
				//

				// Replace span styles with characters
				var spans = textDoc.getElementsByTagName("span");
				for(var i=0; i<spans.length; i++) {
					var span = spans[i];
					if(span.style.textDecoration == "underline") {
						span.insertBefore(textDoc.createTextNode("_"), span.firstChild);
						span.appendChild(textDoc.createTextNode("_"));
					}
				}

				//
				// And add spaces for indents
				//
				// Placeholder for 4 spaces in final output
				const ZTAB = "%%ZOTEROTAB%%";
				var ps = textDoc.getElementsByTagName("p");
				for(var i=0; i<ps.length; i++) {
					var p = ps[i],
						paddingLeft = p.style.paddingLeft;
					if(paddingLeft && paddingLeft.substr(paddingLeft.length-2) === "px") {
						var paddingPx = parseInt(paddingLeft, 10),
							ztabs = "";
						for (let j = 30; j <= paddingPx; j += 30) ztabs += ZTAB;
						p.insertBefore(textDoc.createTextNode(ztabs), p.firstChild);
					}
				}

				// Use plaintext serializer to output formatted text
				var docEncoder = Components.classes["@mozilla.org/layout/documentEncoder;1?type=text/html"]
					.createInstance(Components.interfaces.nsIDocumentEncoder);
				docEncoder.init(textDoc, "text/plain", docEncoder.OutputFormatted);
				var text = docEncoder.encodeToString().trim().replace(ZTAB, "    ", "g");

				//
				// Adjustments for the HTML copied to the clipboard
				//

				// Everything seems to like margin-left better than padding-left
				var ps = Zotero.Utilities.xpath(doc, "p");
				for(var i=0; i<ps.length; i++) {
					var p = ps[i];
					if(p.style.paddingLeft) {
						p.style.marginLeft = p.style.paddingLeft;
						p.style.paddingLeft = "";
					}
				}

				// Word and TextEdit don't indent blockquotes on their own and need this
				//
				// OO gets it right, so this results in an extra indent
				if (Zotero.Prefs.get('export.quickCopy.compatibility.indentBlockquotes')) {
					var ps = Zotero.Utilities.xpath(doc, "//blockquote/p");
					for(var i=0; i<ps.length; i++) ps[i].style.marginLeft = "30px";
				}

				// Add Word Normal style to paragraphs and add double-spacing
				//
				// OO inserts the conditional style code as a document comment
				if (Zotero.Prefs.get('export.quickCopy.compatibility.word')) {
					var ps = doc.getElementsByTagName("p");
					for (var i=0; i<ps.length; i++) ps[i].className = "msoNormal";
					var copyHTML = "\n"
									+ container.outerHTML;
				}
				else {
					var copyHTML = container.outerHTML;
				}

				var content = {
					text: format.contentType == "html" ? html : text,
					html: copyHTML
				};

				return content;
			}

			// determine locale preference
			var locale = format.locale ? format.locale : Zotero.Prefs.get('export.quickCopy.locale');

			// Copy citations if shift key pressed
			if (modified) {
				var csl = Zotero.Styles.get(format.id).getCiteProc(locale);
				csl.updateItems([item.id for each(item in items)]);
				var citation = {citationItems:[{id:item.id} for each(item in items)], properties:{}};
				var html = csl.previewCitationCluster(citation, [], [], "html");
				var text = csl.previewCitationCluster(citation, [], [], "text");
			} else {
				var style = Zotero.Styles.get(format.id);
				var cslEngine = style.getCiteProc(locale);
 				var html = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html");
				var text = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "text");
			}

			return {text:(format.contentType == "html" ? html : text), html:html};
		}

		throw ("Invalid mode '" + format.mode + "' in Zotero.QuickCopy.getContentFromItems()");
	};

	function _init() {
		var translation = new Zotero.Translate.Export;
		var translators = translation.getTranslators();

		// add styles to list
		var styles = Zotero.Styles.getVisible();
		for each(var style in styles) {
			_formattedNames['bibliography=' + style.styleID] = style.title;
		}

		for (var i=0; i<translators.length; i++) {
			// Skip RDF formats
			switch (translators[i].translatorID) {
				case '6e372642-ed9d-4934-b5d1-c11ac758ebb7':
				case '14763d24-8ba0-45df-8f52-b8d1108e7ac9':
					continue;
			}
			_formattedNames['export=' + translators[i].translatorID] = translators[i].label;
		}
	}
}
<commit_msg>Fix Quick Copy in tab mode<commit_after>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

Zotero.QuickCopy = new function() {

	var _initialized = false;
	var _formattedNames = {};

	/*
	 * Return Quick Copy setting object from string, stringified object, or object
	 *
	 * Example string format: "bibliography/html=http://www.zotero.org/styles/apa"
	 *
	 * Quick Copy setting object has the following properties:
	 * - "mode": "bibliography" (for styles) or "export" (for export translators)
	 * - "contentType: "" (plain text output) or "html" (HTML output; for styles
	 *   only)
	 * - "id": style ID or export translator ID
	 * - "locale": locale code (for styles only)
	 */
	this.unserializeSetting = function (setting) {
		var settingObject = {};

		if (typeof setting === 'string') {
			try {
				// First test if string input is a stringified object
				settingObject = JSON.parse(setting);
			} catch (e) {
				// Try parsing as formatted string
				var parsedSetting = setting.match(/(bibliography|export)(?:\/([^=]+))?=(.+)$/);
				if (parsedSetting) {
					settingObject.mode = parsedSetting[1];
					settingObject.contentType = parsedSetting[2] || '';
					settingObject.id = parsedSetting[3];
					settingObject.locale = '';
				}
			}
		} else {
			// Return input if not a string; it might already be an object
			return setting;
		}

		return settingObject;
	};

	this.getFormattedNameFromSetting = function (setting) {
		if (!_initialized) {
			_init();
		}

		var format = this.unserializeSetting(setting);

		var name = _formattedNames[format.mode + "=" + format.id];
		return name ? name : '';
	};

	this.getSettingFromFormattedName = function (name) {
		if (!_initialized) {
			_init();
		}

		for (var setting in _formattedNames) {
			if (_formattedNames[setting] == name) {
				return setting;
			}
		}

		return '';
	};

	this.getFormatFromURL = function(url) {
		var quickCopyPref = Zotero.Prefs.get("export.quickCopy.setting");
		quickCopyPref = JSON.stringify(this.unserializeSetting(quickCopyPref));

		if (!url) {
			return quickCopyPref;
		}

		var ioService = Components.classes["@mozilla.org/network/io-service;1"]
			.getService(Components.interfaces.nsIIOService);
		try {
			var nsIURI = ioService.newURI(url, null, null);
			// Accessing some properties may throw for URIs that do not support those
			// parts. E.g. hostPort throws NS_ERROR_FAILURE for about:blank
			var urlHostPort = nsIURI.hostPort;
			var urlPath = nsIURI.path;
		}
		catch (e) {}

		// Skip about:, chrome:, etc.
		if (!urlHostPort) {
			return quickCopyPref;
		}

		var matches = [];

		var sql = "SELECT key AS domainPath, value AS format FROM settings "
			+ "WHERE setting='quickCopySite' AND (key LIKE ? OR key LIKE ?)";
		var urlDomain = urlHostPort.match(/[^\.]+\.[^\.]+$/);
		var rows = Zotero.DB.query(sql, ['%' + urlDomain[0] + '%', '/%']);
		for (let i = 0; i < rows.length; i++) {
			let row = rows[i];
			let domain = row.domainPath.split('/',1)[0];
			let path = row.domainPath.substr(domain.length) || '/';
			let re = new RegExp('(^|[./])' + Zotero.Utilities.quotemeta(domain) + '$', 'i');
			if (re.test(urlHostPort) && urlPath.indexOf(path) === 0) {
				matches.push({
					format: JSON.stringify(this.unserializeSetting(row.format)),
					domainLength: domain.length,
					pathLength: path.length
				});
			}
		}

		// Give priority to longer domains, then longer paths
		var sort = function(a, b) {
			if (a.domainLength > b.domainLength) {
				return -1;
			}
			else if (a.domainLength  b.pathLength) {
				return -1;
			}
			else if (a.pathLength < b.pathLength) {
				return 1;
			}

			return -1;
		};

		if (matches.length) {
			matches.sort(sort);
			return matches[0].format;
		} else {
			return quickCopyPref;
		}
	};

	/*
	 * Get text and (when applicable) HTML content from items
	 *
	 * |items| is an array of Zotero.Item objects
	 *
	 * |format| may be a Quick Copy format string
	 * (e.g. "bibliography=http://www.zotero.org/styles/apa")
	 * or an Quick Copy format object
	 *
	 * |callback| is only necessary if using an export format and should be
	 * a function suitable for Zotero.Translate.setHandler, taking parameters
	 * |obj| and |worked|. The generated content should be placed in obj.string
	 * and |worked| should be true if the operation is successful.
	 *
	 * If bibliography format, the process is synchronous and an object
	 * contain properties 'text' and 'html' is returned.
	 */
	this.getContentFromItems = function (items, format, callback, modified) {
		if (items.length > Zotero.Prefs.get('export.quickCopy.dragLimit')) {
			Zotero.debug("Skipping quick copy for " + items.length + " items");
			return false;
		}

		format = this.unserializeSetting(format);

		if (format.mode == 'export') {
			var translation = new Zotero.Translate.Export;
			translation.noWait = true;	// needed not to break drags
			translation.setItems(items);
			translation.setTranslator(format.id);
			translation.setHandler("done", callback);
			translation.translate();
			return true;
		}
		else if (format.mode == 'bibliography') {
			// Move notes to separate array
			var allNotes = true;
			var notes = [];
			for (var i=0; i<items.length; i++) {
				if (items[i].isNote()) {
					notes.push(items.splice(i, 1)[0]);
					i--;
				}
				else {
					allNotes = false;
				}
			}

			// If all notes, export full content
			if (allNotes) {
				var content = [],
					parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
						.createInstance(Components.interfaces.nsIDOMParser),
					doc = parser.parseFromString('
', 'text/html'),
					textDoc = parser.parseFromString('
', 'text/html'),
					container = doc.documentElement,
					textContainer = textDoc.documentElement;
				for (var i=0; i<notes.length; i++) {
					var div = doc.createElement("div");
					div.className = "zotero-note";
					div.insertAdjacentHTML('afterbegin', notes[i].getNote());
					container.appendChild(div);
					textContainer.appendChild(textDoc.importNode(div, true));
				}

				// Raw HTML output
				var html = container.outerHTML;

				// Add placeholders for newlines between notes
				if (notes.length > 1) {
					var divs = Zotero.Utilities.xpath(container, "div"),
						textDivs = Zotero.Utilities.xpath(textContainer, "div");
					for (var i=1, len=divs.length; i<len; i++) {
						var p = doc.createElement("p");
						p.appendChild(doc.createTextNode("--------------------------------------------------"));
						container.insertBefore(p, divs[i]);
						textContainer.insertBefore(textDoc.importNode(p, true), textDivs[i]);
					}
				}

				const BLOCKQUOTE_PREFS = {
					'export.quickCopy.quoteBlockquotes.richText':doc,
					'export.quickCopy.quoteBlockquotes.plainText':textDoc
				};
				for(var pref in BLOCKQUOTE_PREFS) {
					if (Zotero.Prefs.get(pref)) {
						var currentDoc = BLOCKQUOTE_PREFS[pref];
						// Add quotes around blockquote paragraphs
						var addOpenQuote = Zotero.Utilities.xpath(currentDoc, "//blockquote/p[1]"),
							addCloseQuote = Zotero.Utilities.xpath(currentDoc, "//blockquote/p[last()]");
						for(var i=0; i<addOpenQuote.length; i++) {
							addOpenQuote[i].insertBefore(currentDoc.createTextNode("\u201c"),
								addOpenQuote[i].firstChild);
						}
						for(var i=0; i<addCloseQuote.length; i++) {
							addCloseQuote[i].appendChild(currentDoc.createTextNode("\u201d"));
						}
					}
				}

				//
				// Text-only adjustments
				//

				// Replace span styles with characters
				var spans = textDoc.getElementsByTagName("span");
				for(var i=0; i<spans.length; i++) {
					var span = spans[i];
					if(span.style.textDecoration == "underline") {
						span.insertBefore(textDoc.createTextNode("_"), span.firstChild);
						span.appendChild(textDoc.createTextNode("_"));
					}
				}

				//
				// And add spaces for indents
				//
				// Placeholder for 4 spaces in final output
				const ZTAB = "%%ZOTEROTAB%%";
				var ps = textDoc.getElementsByTagName("p");
				for(var i=0; i<ps.length; i++) {
					var p = ps[i],
						paddingLeft = p.style.paddingLeft;
					if(paddingLeft && paddingLeft.substr(paddingLeft.length-2) === "px") {
						var paddingPx = parseInt(paddingLeft, 10),
							ztabs = "";
						for (let j = 30; j <= paddingPx; j += 30) ztabs += ZTAB;
						p.insertBefore(textDoc.createTextNode(ztabs), p.firstChild);
					}
				}

				// Use plaintext serializer to output formatted text
				var docEncoder = Components.classes["@mozilla.org/layout/documentEncoder;1?type=text/html"]
					.createInstance(Components.interfaces.nsIDocumentEncoder);
				docEncoder.init(textDoc, "text/plain", docEncoder.OutputFormatted);
				var text = docEncoder.encodeToString().trim().replace(ZTAB, "    ", "g");

				//
				// Adjustments for the HTML copied to the clipboard
				//

				// Everything seems to like margin-left better than padding-left
				var ps = Zotero.Utilities.xpath(doc, "p");
				for(var i=0; i<ps.length; i++) {
					var p = ps[i];
					if(p.style.paddingLeft) {
						p.style.marginLeft = p.style.paddingLeft;
						p.style.paddingLeft = "";
					}
				}

				// Word and TextEdit don't indent blockquotes on their own and need this
				//
				// OO gets it right, so this results in an extra indent
				if (Zotero.Prefs.get('export.quickCopy.compatibility.indentBlockquotes')) {
					var ps = Zotero.Utilities.xpath(doc, "//blockquote/p");
					for(var i=0; i<ps.length; i++) ps[i].style.marginLeft = "30px";
				}

				// Add Word Normal style to paragraphs and add double-spacing
				//
				// OO inserts the conditional style code as a document comment
				if (Zotero.Prefs.get('export.quickCopy.compatibility.word')) {
					var ps = doc.getElementsByTagName("p");
					for (var i=0; i<ps.length; i++) ps[i].className = "msoNormal";
					var copyHTML = "\n"
									+ container.outerHTML;
				}
				else {
					var copyHTML = container.outerHTML;
				}

				var content = {
					text: format.contentType == "html" ? html : text,
					html: copyHTML
				};

				return content;
			}

			// determine locale preference
			var locale = format.locale ? format.locale : Zotero.Prefs.get('export.quickCopy.locale');

			// Copy citations if shift key pressed
			if (modified) {
				var csl = Zotero.Styles.get(format.id).getCiteProc(locale);
				csl.updateItems([item.id for each(item in items)]);
				var citation = {citationItems:[{id:item.id} for each(item in items)], properties:{}};
				var html = csl.previewCitationCluster(citation, [], [], "html");
				var text = csl.previewCitationCluster(citation, [], [], "text");
			} else {
				var style = Zotero.Styles.get(format.id);
				var cslEngine = style.getCiteProc(locale);
 				var html = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html");
				var text = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "text");
			}

			return {text:(format.contentType == "html" ? html : text), html:html};
		}

		throw ("Invalid mode '" + format.mode + "' in Zotero.QuickCopy.getContentFromItems()");
	};

	function _init() {
		var translation = new Zotero.Translate.Export;
		var translators = translation.getTranslators();

		// add styles to list
		var styles = Zotero.Styles.getVisible();
		for each(var style in styles) {
			_formattedNames['bibliography=' + style.styleID] = style.title;
		}

		for (var i=0; i<translators.length; i++) {
			// Skip RDF formats
			switch (translators[i].translatorID) {
				case '6e372642-ed9d-4934-b5d1-c11ac758ebb7':
				case '14763d24-8ba0-45df-8f52-b8d1108e7ac9':
					continue;
			}
			_formattedNames['export=' + translators[i].translatorID] = translators[i].label;
		}
	}
}
<|endoftext|>


<commit_before>//
// Instagram
//
hello.init({
	instagram : {
		name : 'Instagram',
		auth_options: {
			display: ''
		},
		uri : {
			auth : 'https://instagram.com/oauth/authorize/',
			base : 'https://api.instagram.com/v1/',
			'me' : 'users/self',
			'me/feed' : 'users/self/feed',
			'me/photos' : 'users/self/media/recent?min_id=0',
			'me/friends' : 'users/self/follows'
		},
		scope : {
			basic : 'basic'
		},
		wrap : {
			me : function(o){

				if("data" in o ){
					o.id = o.data.id;
					o.thumbnail = o.data.profile_picture;
					o.name = o.data.full_name || o.data.username;
				}
				return o;
			},
			"me/photos" : function(o){
				if("data" in o){
					for(var i=0;i<o.data.length;i++){
						if(o.data[i].type !== 'image'){
							delete o.data[i];
							i--;
						}
						o.data[i].thumbnail = o.data[i].images.thumbnail.url;
						o.data[i].picture = o.data[i].images.standard_resolution.url;
						o.data[i].name = o.data[i].caption ? o.data[i].caption.text : null;
					}
				}
				return o;
			}
		},
		// Use JSONP
		xhr : false
	}
});
<commit_msg>Set the default max number of photos in instagram<commit_after>//
// Instagram
//
hello.init({
	instagram : {
		name : 'Instagram',
		auth_options: {
			display: ''
		},
		uri : {
			auth : 'https://instagram.com/oauth/authorize/',
			base : 'https://api.instagram.com/v1/',
			'me' : 'users/self',
			'me/feed' : 'users/self/feed',
			'me/photos' : 'users/self/media/recent?min_id=0&count=100',
			'me/friends' : 'users/self/follows'
		},
		scope : {
			basic : 'basic'
		},
		wrap : {
			me : function(o){

				if("data" in o ){
					o.id = o.data.id;
					o.thumbnail = o.data.profile_picture;
					o.name = o.data.full_name || o.data.username;
				}
				return o;
			},
			"me/photos" : function(o){
				if("data" in o){
					for(var i=0;i<o.data.length;i++){
						if(o.data[i].type !== 'image'){
							delete o.data[i];
							i--;
						}
						o.data[i].thumbnail = o.data[i].images.thumbnail.url;
						o.data[i].picture = o.data[i].images.standard_resolution.url;
						o.data[i].name = o.data[i].caption ? o.data[i].caption.text : null;
					}
				}
				return o;
			}
		},
		// Use JSONP
		xhr : false
	}
});
<|endoftext|>


<commit_before>var _ = require("underscore");
var Backbone = require("backbone");
var Models = require("./models");
var Views = require("./views");
var ChannelManageView = require("edit_channel/new_channel/views");

//var saveDispatcher = _.clone(Backbone.Events);

ChannelEditRouter  = Backbone.Router.extend({
	nodeCollection: new Models.ContentNodeCollection(),
    initialize: function(options) {
        _.bindAll(this, "navigate_channel_home", "preview_page", "edit_page", "clipboard_page");
		this.nodeCollection = new Models.ContentNodeCollection();
		this.nodeCollection.fetch();
		window.licenses = new Models.LicenseCollection(window.license_list);
		window.current_channel = new Models.ChannelModel(window.channel);
		window.current_user = new Models.UserModel(window.user);
		//this.listenTo(saveDispatcher, "save", this.save);
		this.channelCollection = new Models.ChannelCollection(window.channels);

		this.fileformats = new Models.FileFormatCollection(window.fformats);
		this.fileformats.fetch({async:false, cache:true});
		this.formatpresets = new Models.FormatPresetCollection(window.presets);
		this.formatpresets.fetch({async:false, cache:true});
		this.contentkinds = new Models.ContentKindCollection(window.kinds);
		this.contentkinds.fetch({async:false, cache:true});
    },

    routes: {
		"": "navigate_channel_home",
		":channel/edit": "edit_page",
		":channel/preview": "preview_page",
		":channel/clipboard": "clipboard_page"
    },

	navigate_channel_home: function() {
		var channel_manager_view = new ChannelManageView.ChannelList ({
			el: $("#channel-container"),
			channels: this.channelCollection,
			licenses:window.licenses
		});
    },

	edit_page : function(){
		this.open_channel(true, false, window.current_channel.get_root("main_tree"));
	},
	preview_page : function(){
		this.open_channel(false, false, window.current_channel.get_root("main_tree"));
	},
	clipboard_page:function(){
		this.open_channel(true, true, window.current_user.get_clipboard());
	},

	open_channel: function(edit_mode_on, is_clipboard, root){
		window.fileformats = this.fileformats ;
		window.channels = this.channelCollection;
		window.formatpresets = this.formatpresets;
		window.contentkinds = this.contentkinds;
		window.contenttags = new Models.TagCollection(window.channel_tags);
		window.access_channels = new Models.ChannelCollection(window.accessible_channels);

		var EditViews = require("edit_channel/tree_edit/views");
		var edit_page_view = new EditViews.TreeEditView({
			el: $("#main-content-area"),
			collection: this.nodeCollection,
			edit: edit_mode_on,
			model : root,
			is_clipboard : is_clipboard,
		});

	}
});

module.exports = ChannelEditRouter;<commit_msg>Don't fetch full contentnode set at load.<commit_after>var _ = require("underscore");
var Backbone = require("backbone");
var Models = require("./models");
var Views = require("./views");
var ChannelManageView = require("edit_channel/new_channel/views");

//var saveDispatcher = _.clone(Backbone.Events);

ChannelEditRouter  = Backbone.Router.extend({
	nodeCollection: new Models.ContentNodeCollection(),
    initialize: function(options) {
        _.bindAll(this, "navigate_channel_home", "preview_page", "edit_page", "clipboard_page");
		this.nodeCollection = new Models.ContentNodeCollection();
		window.licenses = new Models.LicenseCollection(window.license_list);
		window.current_channel = new Models.ChannelModel(window.channel);
		window.current_user = new Models.UserModel(window.user);
		//this.listenTo(saveDispatcher, "save", this.save);
		this.channelCollection = new Models.ChannelCollection(window.channels);

		this.fileformats = new Models.FileFormatCollection(window.fformats);
		this.fileformats.fetch({async:false, cache:true});
		this.formatpresets = new Models.FormatPresetCollection(window.presets);
		this.formatpresets.fetch({async:false, cache:true});
		this.contentkinds = new Models.ContentKindCollection(window.kinds);
		this.contentkinds.fetch({async:false, cache:true});
    },

    routes: {
		"": "navigate_channel_home",
		":channel/edit": "edit_page",
		":channel/preview": "preview_page",
		":channel/clipboard": "clipboard_page"
    },

	navigate_channel_home: function() {
		var channel_manager_view = new ChannelManageView.ChannelList ({
			el: $("#channel-container"),
			channels: this.channelCollection,
			licenses:window.licenses
		});
    },

	edit_page : function(){
		this.open_channel(true, false, window.current_channel.get_root("main_tree"));
	},
	preview_page : function(){
		this.open_channel(false, false, window.current_channel.get_root("main_tree"));
	},
	clipboard_page:function(){
		this.open_channel(true, true, window.current_user.get_clipboard());
	},

	open_channel: function(edit_mode_on, is_clipboard, root){
		window.fileformats = this.fileformats ;
		window.channels = this.channelCollection;
		window.formatpresets = this.formatpresets;
		window.contentkinds = this.contentkinds;
		window.contenttags = new Models.TagCollection(window.channel_tags);
		window.access_channels = new Models.ChannelCollection(window.accessible_channels);

		var EditViews = require("edit_channel/tree_edit/views");
		var edit_page_view = new EditViews.TreeEditView({
			el: $("#main-content-area"),
			collection: this.nodeCollection,
			edit: edit_mode_on,
			model : root,
			is_clipboard : is_clipboard,
		});

	}
});

module.exports = ChannelEditRouter;<|endoftext|>


<commit_before>// Set the margins
var margin = {top: 60, right: 100, bottom: 20, left: 80},
  width = 850 - margin.left - margin.right,
  height = 380 - margin.top - margin.bottom;

// Parse the month variable
var formatYear = d3.timeFormat("%Y");
var parseYear = d3.timeParse("%Y");

// Set the ranges
var xyear = d3.scaleTime().domain([parseYear("2018"), parseYear("2056")]).range([0, width]);
var ygas = d3.scaleLinear().range([height, 0]);

// Define the line
var valueLine = d3.line()
    .x(function(d) { return xyear(d.year); })
    .y(function(d) { return ygas(+d.Waarde); })

// Create the svg canvas in the "graph" div
var svg = d3.select("#graph")
        .append("svg")
        .style("width", width + margin.left + margin.right + "px")
        .style("height", height + margin.top + margin.bottom + "px")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform","translate(" + margin.left + "," + margin.top + ")")
        .attr("class", "svg");

// Append title
svg.append("text")
      .attr("x", (10))
      .attr("y", 3)
      .attr("text-anchor", "left")
      .style("font-size", "16px")
      .text("Vermogensfonds in voor de Nederlandse aardgasbaten vanaf 2018");

// Import the CSV data
d3.csv("/Programmeerproject/data/Fund2018.csv", function(error, data) {
  if (error) throw error;

   // Format the data
  data.forEach(function(d) {
      d.year = parseYear(d.year);
      d.Waarde = +d.Waarde;
      d.Fruit = d.Fruit;
      d.Code = d.Code;
  });

  var nest = d3.nest()
	  .key(function(d){
	    return d.Fruit;
	  })
	  .key(function(d){
	  	return d.Code;
	  })
	  .entries(data)

  // Scale the range of the data
  xyear.domain(d3.extent(data, function(d) { return d.year; }));
  ygas.domain([0, d3.max(data, function(d) { return d.Waarde +10; })]);

  // Set up the x axis
  var xyearaxis = svg.append("g")
       .attr("transform", "translate(0," + height + ")")
       .attr("class", "x axis")
       .call(d3.axisBottom(xyear)
          .ticks(13)
          .tickSizeInner(25)
          .tickSize(4, 4)
          .tickFormat(d3.timeFormat("%Y"))
          .tickSizeInner(0)
          .tickPadding(10));

  // Add the Y Axis
   var ygasaxis = svg.append("g")
       .attr("class", "y axis")
       .call(d3.axisLeft(ygas)
          .ticks(10)
          .tickSizeInner(0)
          .tickPadding(6)
          .tickSize(0, 0));

  // Add a label to the y axis
  svg.append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 3)
        .attr("x", -50)
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text("In miljarden euro's")
        .attr("class", "y axis label");

  // Create a dropdown
    var fruitMenu = d3.select("#oilDropdown")

    fruitMenu
		.append("select")
		.selectAll("option")
        .data(nest)
        .enter()
        .append("option")
        .attr("value", function(d){
            return d.key;
        })
        .text(function(d){
            return d.key;
        })

 	// Function to create the initial graph
 	var initialGraph = function(fruit){

 		// Filter the data to include only fruit of interest
 		var selectGroup = nest.filter(function(d){
                return d.key == fruit;
              })

    console.log(selectGroup);
    console.log(xyear(parseYear("2020")))

	    var selectGroups = svg.selectAll(".fruitGroups")
		    .data(selectGroup, function(d){
		      return d ? d.key : this.key;
		    })
		    .enter()
		    .append("g")
		    .attr("class", "fruitGroups")

colors = ["#d0743c", "#ff8c00"]

		var initialPath = selectGroups.selectAll(".line")
			.data(function(d) { return d.values; })
			.enter()
			.append("path")

		initialPath
			.attr("d", function(d){
				return valueLine(d.values)
			})
			.attr("class", "line")
      .style("stroke", function(d) {
        return colors[d.key - 1]
      })

 	}

 	// Create initial graph
 	initialGraph("Olieprijs 51 dollar per vat")

 	// Update the data
 	var updateGraph = function(fruit){

 		// Filter the data to include only fruit of interest
 		var selectGroup = nest.filter(function(d){
                return d.key == fruit;
              })

    console.log(selectGroup);
    console.log(xyear(parseYear("2020")))

 		// Select all of the grouped elements and update the data
	    var selectGroups = svg.selectAll(".fruitGroups")
		    .data(selectGroup)

		    // Select all the lines and transition to new positions
            selectGroups.selectAll("path.line")
               .data(function(d){
                  return (d.values);
                })
                .transition()
                  .duration(1000)
                  .attr("d", function(d){
                    return valueLine(d.values)
                  })
 	}

 	// Run update function when dropdown selection changes
 	fruitMenu.on('change', function(){

 		// Find which fruit was selected from the dropdown
 		var selectedFruit = d3.select(this)
            .select("select")
            .property("value")

        // Run update function with the selected fruit
        updateGraph(selectedFruit)
    });

})<commit_msg>Last update paths GitHub<commit_after>// Set the margins
var margin = {top: 60, right: 100, bottom: 20, left: 80},
  width = 850 - margin.left - margin.right,
  height = 380 - margin.top - margin.bottom;

// Parse the month variable
var formatYear = d3.timeFormat("%Y");
var parseYear = d3.timeParse("%Y");

// Set the ranges
var xyear = d3.scaleTime().domain([parseYear("2018"), parseYear("2056")]).range([0, width]);
var ygas = d3.scaleLinear().range([height, 0]);

// Define the line
var valueLine = d3.line()
    .x(function(d) { return xyear(d.year); })
    .y(function(d) { return ygas(+d.Waarde); })

// Create the svg canvas in the "graph" div
var svg = d3.select("#graph")
        .append("svg")
        .style("width", width + margin.left + margin.right + "px")
        .style("height", height + margin.top + margin.bottom + "px")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform","translate(" + margin.left + "," + margin.top + ")")
        .attr("class", "svg");

// Append title
svg.append("text")
      .attr("x", (10))
      .attr("y", 0)
      .attr("text-anchor", "left")
      .style("font-size", "16px")
      .text("Vermogensfonds in voor de Nederlandse aardgasbaten vanaf 2018");

svg.append("rect")
      .attr("x", width - 19)
      .attr("width", 19)
      .attr("height", 19)
      .attr("fill", "#d0743c");

svg.append("rect")
    .attr("x", width - 19)
    .attr("y", 20)
    .attr("width", 19)
    .attr("height", 19)
    .attr("fill", "#ff8c00");

svg.append("text")
        .attr("x", width - 84)
        .attr("y", 9.5)
        .attr("dy", "0.32em")
        .text("Marktwaarde")
        .style("font-family", "Arial")
        .style("font-size", "10px");

svg.append("text")
        .attr("x", width - 84)
        .attr("y", 29.5)
        .attr("dy", "0.32em")
        .text("Gasbaten")
        .style("font-family", "Arial")
        .style("font-size", "10px");

// Import the CSV data
d3.csv("/Programmeerproject/data/Fund2018.csv", function(error, data) {
  if (error) throw error;

   // Format the data
  data.forEach(function(d) {
      d.year = parseYear(d.year);
      d.Waarde = +d.Waarde;
      d.Fruit = d.Fruit;
      d.Code = d.Code;
  });

  var nest = d3.nest()
	  .key(function(d){
	    return d.Fruit;
	  })
	  .key(function(d){
	  	return d.Code;
	  })
	  .entries(data)

  // Scale the range of the data
  xyear.domain(d3.extent(data, function(d) { return d.year; }));
  ygas.domain([0, d3.max(data, function(d) { return d.Waarde +10; })]);

  // Set up the x axis
  var xyearaxis = svg.append("g")
       .attr("transform", "translate(0," + height + ")")
       .attr("class", "x axis")
       .call(d3.axisBottom(xyear)
          .ticks(13)
          .tickSizeInner(25)
          .tickSize(4, 4)
          .tickFormat(d3.timeFormat("%Y"))
          .tickSizeInner(0)
          .tickPadding(10));

  // Add the Y Axis
   var ygasaxis = svg.append("g")
       .attr("class", "y axis")
       .call(d3.axisLeft(ygas)
          .ticks(10)
          .tickSizeInner(0)
          .tickPadding(6)
          .tickSize(0, 0));

  // Add a label to the y axis
  svg.append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 3)
        .attr("x", -50)
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text("In miljarden euro's")
        .attr("class", "y axis label");

  // Create a dropdown
    var fruitMenu = d3.select("#oilDropdown")

    fruitMenu
		.append("select")
		.selectAll("option")
        .data(nest)
        .enter()
        .append("option")
        .attr("value", function(d){
            return d.key;
        })
        .text(function(d){
            return d.key;
        })

 	// Function to create the initial graph
 	var initialGraph = function(fruit){

 		// Filter the data to include only fruit of interest
 		var selectGroup = nest.filter(function(d){
                return d.key == fruit;
              })

	    var selectGroups = svg.selectAll(".fruitGroups")
		    .data(selectGroup, function(d){
		      return d ? d.key : this.key;
		    })
		    .enter()
		    .append("g")
		    .attr("class", "fruitGroups")

colors = ["#d0743c", "#ff8c00"]

		var initialPath = selectGroups.selectAll(".line")
			.data(function(d) { return d.values; })
			.enter()
			.append("path")

		initialPath
			.attr("d", function(d){
				return valueLine(d.values)
			})
			.attr("class", "line")
      .style("stroke", function(d) {
        return colors[d.key - 1]
      })

 	}

 	// Create initial graph
 	initialGraph("Olieprijs 51 dollar per vat")

 	// Update the data
 	var updateGraph = function(fruit){

 		// Filter the data to include only fruit of interest
 		var selectGroup = nest.filter(function(d){
                return d.key == fruit;
              })

 		// Select all of the grouped elements and update the data
	    var selectGroups = svg.selectAll(".fruitGroups")
		    .data(selectGroup)

		    // Select all the lines and transition to new positions
            selectGroups.selectAll("path.line")
               .data(function(d){
                  return (d.values);
                })
                .transition()
                  .duration(1000)
                  .attr("d", function(d){
                    return valueLine(d.values)
                  })
 	}

 	// Run update function when dropdown selection changes
 	fruitMenu.on('change', function(){

 		// Find which fruit was selected from the dropdown
 		var selectedFruit = d3.select(this)
            .select("select")
            .property("value")

        // Run update function with the selected fruit
        updateGraph(selectedFruit)
    });

})<|endoftext|>