<commit_before>// http://jsont.squarespace.com/
// http://developers.squarespace.com/json-formatters/
var jsonTemplate = require( "./jsontemplate" ),
    slug = require( "slug" ),
    moment = require( "moment" ),
    phpjs = require( "phpjs" ),
    jsontFormatters = [
        "html",
        "htmltag",
        "html-attr-value",
        "str",
        "raw",
        "AbsUrl",
        "plain-url"
    ],
    sqsFormatters = {
        "date": function ( val, args, ctx ) {
            return phpjs.strftime( args, val );
        },
        "timesince": function ( val, args, ctx ) {
            return moment( val ).fromNow();
        },
        "item-classes": function ( val, args, ctx ) {
            return "";
        },
        "social-button": function ( val, args, ctx ) {
            return "";
        },
        "comments": function ( val, args, ctx ) {
            return "";
        },
        "comment-link": function ( val, args, ctx ) {
            return "";
        },
        "comment-count": function ( val, args, ctx ) {
            return "";
        },
        "like-button": function ( val, args, ctx ) {
            return "";
        },
        "image-meta": function ( val, args, ctx ) {
            return "";
        },
        "product-price": function ( val, args, ctx ) {
            return "";
        },
        "product-status": function ( val, args, ctx ) {
            return "";
        },
        "json": function ( val, args, ctx ) {
            return JSON.stringify( val, null, 4 );
        },
        "json-pretty": function ( val, args, ctx ) {
            return JSON.stringify( val, null, 4 );
        },
        "slugify": function ( val, args, ctx ) {
            return slug( val.toLowerCase() );
        },
        "url-encode": function ( val, args, ctx ) {
            return encodeURI( encodeURIComponent( val ) );
        },
        //"html",
        "htmlattr": function ( val, args, ctx ) {
            return jsonTemplate.HtmlTagEscape( val );
        },
        "htmltag": function ( val, args, ctx ) {
            return jsonTemplate.HtmlTagEscape( val );
        },
        "activate-twitter-links": function ( val, args, ctx ) {
            return val.replace( /(^|\s)@(\w+)/g, "$1@$2" );
        },
        "safe": function ( val, args, ctx ) {
            return phpjs.strip_tags( val );
        }
    };

module.exports = function ( formatter ) {
    // Only run if formatter is NOT a JSONT default internal
    var splits = formatter.split( " " );

    if ( jsontFormatters.indexOf( formatter ) === -1 ) {
        // Wrapper function for passing along to correct formatter
        return function ( string, context ) {
            var ret = false,
                fn = sqsFormatters[ splits[ 0 ] ];

            if ( typeof fn === "function" ) {
                ret = fn( string, splits[ 1 ], context );
            }

            return ret;
        };
    }
};<commit_msg>Fix strftime implementation for date formatter<commit_after>// http://jsont.squarespace.com/
// http://developers.squarespace.com/json-formatters/
var jsonTemplate = require( "./jsontemplate" ),
    slug = require( "slug" ),
    moment = require( "moment" ),
    phpjs = require( "phpjs" ),
    jsontFormatters = [
        "html",
        "htmltag",
        "html-attr-value",
        "str",
        "raw",
        "AbsUrl",
        "plain-url"
    ],
    sqsFormatters = {
        "date": function ( val, args, ctx ) {
            // Divide by 1000 since phpjs accounts for this:
            // PHP API expects UNIX timestamp (auto-convert to int)
            return phpjs.strftime( args, (parseInt( val, 10 ) / 1000) );
        },
        "timesince": function ( val, args, ctx ) {
            return moment( val ).fromNow();
        },
        "item-classes": function ( val, args, ctx ) {
            return "";
        },
        "social-button": function ( val, args, ctx ) {
            return "";
        },
        "comments": function ( val, args, ctx ) {
            return "";
        },
        "comment-link": function ( val, args, ctx ) {
            return "";
        },
        "comment-count": function ( val, args, ctx ) {
            return "";
        },
        "like-button": function ( val, args, ctx ) {
            return "";
        },
        "image-meta": function ( val, args, ctx ) {
            return "";
        },
        "product-price": function ( val, args, ctx ) {
            return "";
        },
        "product-status": function ( val, args, ctx ) {
            return "";
        },
        "json": function ( val, args, ctx ) {
            return JSON.stringify( val, null, 4 );
        },
        "json-pretty": function ( val, args, ctx ) {
            return JSON.stringify( val, null, 4 );
        },
        "slugify": function ( val, args, ctx ) {
            return slug( val.toLowerCase() );
        },
        "url-encode": function ( val, args, ctx ) {
            return encodeURI( encodeURIComponent( val ) );
        },
        //"html",
        "htmlattr": function ( val, args, ctx ) {
            return jsonTemplate.HtmlTagEscape( val );
        },
        "htmltag": function ( val, args, ctx ) {
            return jsonTemplate.HtmlTagEscape( val );
        },
        "activate-twitter-links": function ( val, args, ctx ) {
            return val.replace( /(^|\s)@(\w+)/g, "$1@$2" );
        },
        "safe": function ( val, args, ctx ) {
            return phpjs.strip_tags( val );
        }
    };

module.exports = function ( formatter ) {
    // Only run if formatter is NOT a JSONT default internal
    var splits = formatter.split( " " );

    if ( jsontFormatters.indexOf( formatter ) === -1 ) {
        // Wrapper function for passing along to correct formatter
        return function ( string, context ) {
            var ret = false,
                fn = sqsFormatters[ splits[ 0 ] ];

            if ( typeof fn === "function" ) {
                ret = fn( string, [].join.call( splits.slice( 1 ), " " ), context );
            }

            return ret;
        };
    }
};<|endoftext|>


<commit_before>/* TODO: add "safe mode" that anticipates players joining or leaving and fixes the output accordingly
  unfortunately this may be a bit difficult to implement due to the fact that players are outputted via a stream and not saved anywhere else
  adding this without removing streams and sacrificing memory is very complicated
*/

// Dependencies
var JSONStream = require('JSONStream');
var Promise = require('bluebird');
var cheerio = require('whacko');

// Includes
var http = require('./util/http.js').func;
var getRoles = require('./util/getRoles.js').func;
var getRole = require('./util/getRole.js').func;
var promise = require('./util/promise.js');
var getVerificationInputs = require('./util/getVerificationInputs.js').func;

// Args
exports.args = ['group', 'rank', 'limit', 'online'];

// Define
// Because of the complexity of this function, it's easier to have shared variables initiated every time a call is made.
exports.func = function (args) {
  var stream = JSONStream.stringifyObject('{', ',\n', '}\n');
  var added = 0;
  var scanned = 0;
  var joined = 0;
  var left = 0;
  var interval = 1000;
  var delay = 2000;
  var debug = false; // Prints out information about what the function is doing, good for status updates or debugging
  var firstCount;
  var rank;
  var limit;
  var online;
  var group;
  var post;
  var pages;
  var total;
  var httpOpt;

  function addPlayer (name, id) {
    added++;
    stream.write([name, id]);
  }

  function echo (str) {
    if (debug) {
      console.log(str);
    }
  }

  function getPlayersOnPage (html) {
    var $ = cheerio.load(html);
    var currentCount = parseInt($('[name="ctl00$cphRoblox$rbxGroupRoleSetMembersPane$RolesetCountHidden"]').attr('value'), 10);
    if (!firstCount) {
      firstCount = currentCount;
    } else if (currentCount !== firstCount) {
      var change = Math.abs(firstCount - currentCount);
      if (currentCount > firstCount) {
        joined += change;
      } else {
        left += change;
      }
    }
    if (rank && rank !== -1) {
      if (total) {
        total = Math.min(total, currentCount);
      } else {
        total = currentCount;
      }
    }
    $('div[class=Avatar]').each(function (index, element) {
      if ((limit > -1) && added >= limit) {
        return;
      }
      scanned++;
      var link = $(element).find('a').first();
      var img = $(element).find('.OnlineStatus').find('img').first();
      if (!online || img.attr('src') === '../images/online.png') {
        var id = parseInt(link.attr('href').substring(29), 10);
        addPlayer(link.attr('title'), id);
      }
    });
    $ = null;
    html = null;
  }

  function runInterval (pass) {
    return promise(function (resolve, reject) {
      var toDo = [];

      var max = (pass + 1) * interval;
      echo('Current: ' + (pass * interval + 1 + (pass === 0 ? 1 : 0)));
      echo('To: ' + max + ' out of ' + pages + ' under pass ' + pass);
      var last = Math.min(pages, max);

      for (var i = pass * interval + 1 + (pass === 0 ? 1 : 0); i <= last; i++) {
        if (limit >= -1) {
          toDo.push(i);
        } else {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          toDo.push(http(httpOpt).then(getPlayersOnPage));
        }
      }

      function completed () {
        toDo = null;
        setTimeout(resolve, delay);
      }

      if (limit >= -1) {
        Promise.each(toDo, function (i) {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          return http(httpOpt).then(getPlayersOnPage);
        }).then(completed);
      } else {
        Promise.all(toDo).then(completed);
      }
    });
  }

  function getPlayersInRole (role) {
    firstCount = null;
    var url = 'http://www.roblox.com/Groups/group.aspx?gid=' + group;
    return http({url: url})
    .then(function (body) {
      var $ = cheerio.load(body);
      if (!rank || rank === -1 && !total) {
        total = parseInt($('#MemberCount').text().substring(9), 10);
      }
      post = getVerificationInputs({html: body});
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList = role;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$currentRoleSetID = role;
      post.__EVENTTARGET = 'ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList';
      post.__ASYNCPOST = 'true';
      delete post.__RequestVerificationToken;
      httpOpt = {
        url: url,
        options: {
          method: 'POST',
          form: post,
          headers: {
            'User-Agent': 'Mozilla'
          }
        }
      };
      return http(httpOpt)
    .then(function (body) {
      var $ = cheerio.load(body);
      pages = parseInt($('div[id=ctl00_cphRoblox_rbxGroupRoleSetMembersPane_dlUsers_Footer_ctl01_Div1]').find('div[class=paging_pagenums_container]').text(), 10) || 1;
      if (limit > -1) {
        pages = Math.min(pages, Math.ceil(limit / 12));
        total = (total != null) ? total : limit;
      }
      getPlayersOnPage(body);
      if (pages === 1) {
        return;
      }

      delete post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = 1;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$HiddenInputButton = '';

      var toDo = [];

      for (var pass = 0; pass < pages / 1000; pass++) {
        toDo.push(pass);
      }

      return Promise.each(toDo, runInterval);
    }); });
  }

  return {
    promise: promise(function (resolve, reject) {
      group = args.group;
      rank = args.rank;
      online = args.online || false;
      limit = args.limit || -2;
      getRoles({group: group}).then(function (roles) {
        var toDo = [];
        if (rank && rank !== -1) {
          toDo.push(getRole({roles: roles, rank: rank}).ID);
        } else {
          for (var i = 0; i < roles.length; i++) {
            toDo.push(roles[i].ID);
          }
        }

        Promise.each(toDo, getPlayersInRole).then(function () {
          stream.end();
          total = added;
          resolve({
            joined: joined,
            left: left,
            changed: (joined !== 0 || left !== 0),
            total: added
          });
        });
      });
    }),
    getStatus: function () {
      return Math.round((total ? (scanned / total) : 0) * 10000) / 100;
    },
    stream: stream
  };
};
<commit_msg>Removed stream, added settings<commit_after>// Dependencies
var Promise = require('bluebird');
var cheerio = require('whacko');

// Includes
var http = require('./util/http.js').func;
var getRoles = require('./util/getRoles.js').func;
var getRole = require('./util/getRole.js').func;
var promise = require('./util/promise.js');
var getVerificationInputs = require('./util/getVerificationInputs.js').func;
var settings = require('../settings').getPlayers;

// Args
exports.args = ['group', 'rank', 'limit', 'online'];

// Define
var interval = settings.interval; // Players for every set of requests
var delay = settings.delay; // Time (in milliseconds) to wait after every interval
var debug = true; // Prints out information about what the function is doing, good for status updates or debugging

// Because of the complexity of this function, it's easier to have shared variables initiated every time a call is made.
exports.func = function (args) {
  var players = {};
  var added = 0;
  var scanned = 0;
  var rank;
  var limit;
  var online;
  var group;
  var post;
  var pages;
  var total;
  var httpOpt;

  function addPlayer (name, id) {
    if (!players[name]) {
      added++;
      players[name] = id;
    }
  }

  function echo (str) {
    if (debug) {
      console.log(str);
    }
  }

  function getPlayersOnPage (html) {
    var $ = cheerio.load(html);
    var currentCount = parseInt($('[name="ctl00$cphRoblox$rbxGroupRoleSetMembersPane$RolesetCountHidden"]').attr('value'), 10);
    if (rank && rank !== -1) {
      if (total) {
        total = Math.min(total, currentCount);
      } else {
        total = currentCount;
      }
    }
    $('div[class=Avatar]').each(function (index, element) {
      if ((limit > -1) && added >= limit) {
        return;
      }
      scanned++;
      var link = $(element).find('a').first();
      var img = $(element).find('.OnlineStatus').find('img').first();
      if (!online || img.attr('src') === '../images/online.png') {
        var id = parseInt(link.attr('href').substring(29), 10);
        addPlayer(link.attr('title'), id);
      }
    });
    $ = null;
    html = null;
  }

  function runInterval (pass) {
    return promise(function (resolve, reject) {
      var toDo = [];

      var max = (pass + 1) * interval;
      echo('Current: ' + (pass * interval + 1 + (pass === 0 ? 1 : 0)));
      echo('To: ' + max + ' out of ' + pages + ' under pass ' + pass);
      var last = Math.min(pages, max);

      for (var i = pass * interval + 1 + (pass === 0 ? 1 : 0); i <= last; i++) {
        if (limit >= -1) {
          toDo.push(i);
        } else {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          toDo.push(http(httpOpt).then(getPlayersOnPage));
        }
      }

      function completed () {
        toDo = null;
        setTimeout(resolve, delay);
      }

      if (limit >= -1) {
        Promise.each(toDo, function (i) {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          return http(httpOpt).then(getPlayersOnPage);
        }).then(completed);
      } else {
        Promise.all(toDo).then(completed);
      }
    });
  }

  function getPlayersInRole (role) {
    var url = 'http://www.roblox.com/Groups/group.aspx?gid=' + group;
    return http({url: url})
    .then(function (body) {
      var $ = cheerio.load(body);
      if (!rank || rank === -1 && !total) {
        total = parseInt($('#MemberCount').text().substring(9), 10);
      }
      post = getVerificationInputs({html: body});
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList = role;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$currentRoleSetID = role;
      post.__EVENTTARGET = 'ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList';
      post.__ASYNCPOST = 'true';
      delete post.__RequestVerificationToken;
      httpOpt = {
        url: url,
        options: {
          method: 'POST',
          form: post,
          headers: {
            'User-Agent': 'Mozilla'
          }
        }
      };
      return http(httpOpt)
    .then(function (body) {
      var $ = cheerio.load(body);
      pages = parseInt($('div[id=ctl00_cphRoblox_rbxGroupRoleSetMembersPane_dlUsers_Footer_ctl01_Div1]').find('div[class=paging_pagenums_container]').text(), 10) || 1;
      if (limit > -1) {
        pages = Math.min(pages, Math.ceil(limit / 12));
        total = (total != null) ? total : limit;
      }
      getPlayersOnPage(body);
      if (pages === 1) {
        return;
      }

      delete post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = 1;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$HiddenInputButton = '';

      var toDo = [];

      for (var pass = 0; pass < pages / 1000; pass++) {
        toDo.push(pass);
      }

      return Promise.each(toDo, runInterval);
    }); });
  }

  return {
    promise: promise(function (resolve, reject) {
      group = args.group;
      rank = args.rank;
      online = args.online || false;
      limit = args.limit || -2;
      getRoles({group: group}).then(function (roles) {
        var toDo = [];
        if (rank && rank !== -1) {
          toDo.push(getRole({roles: roles, rank: rank}).ID);
        } else {
          for (var i = 0; i < roles.length; i++) {
            toDo.push(roles[i].ID);
          }
        }

        Promise.each(toDo, getPlayersInRole).then(function () {
          total = added;
          resolve({
            total: added,
            players: players
          });
        });
      });
    }),
    getStatus: function () {
      return Math.round((total ? (scanned / total) : 0) * 10000) / 100;
    }
  };
};
<|endoftext|>


<commit_before>// Dependencies
var Promise = require('bluebird');
var cheerio = require('whacko');

// Includes
var http = require('./util/http.js').func;
var getRoles = require('./util/getRoles.js').func;
var getRole = require('./util/getRole.js').func;
var promise = require('./util/promise.js');
var getVerificationInputs = require('./util/getVerificationInputs.js').func;
var settings = require('../settings').getPlayers;

// Args
exports.args = ['group', 'rank', 'limit', 'online'];

// Define
var interval = settings.interval; // Players for every set of requests
var delay = settings.delay; // Time (in milliseconds) to wait after every interval
var debug = true; // Prints out information about what the function is doing, good for status updates or debugging

// Because of the complexity of this function, it's easier to have shared variables initiated every time a call is made.
exports.func = function (args) {
  var players = {};
  var added = 0;
  var scanned = 0;
  var rank;
  var limit;
  var online;
  var group;
  var post;
  var pages;
  var total;
  var httpOpt;

  function addPlayer (name, id) {
    if (!players[name]) {
      added++;
      players[name] = id;
    }
  }

  function echo (str) {
    if (debug) {
      console.log(str);
    }
  }

  function getPlayersOnPage (html) {
    var $ = cheerio.load(html);
    var currentCount = parseInt($('[name="ctl00$cphRoblox$rbxGroupRoleSetMembersPane$RolesetCountHidden"]').attr('value'), 10);
    if (rank && rank !== -1) {
      if (total) {
        total = Math.min(total, currentCount);
      } else {
        total = currentCount;
      }
    }
    $('div[class=Avatar]').each(function (index, element) {
      if ((limit > -1) && added >= limit) {
        return;
      }
      scanned++;
      var link = $(element).find('a').first();
      var img = $(element).find('.OnlineStatus').find('img').first();
      if (!online || img.attr('src') === '../images/online.png') {
        var id = parseInt(link.attr('href').substring(29), 10);
        addPlayer(link.attr('title'), id);
      }
    });
    $ = null;
    html = null;
  }

  function runInterval (pass) {
    return promise(function (resolve, reject) {
      var toDo = [];

      var max = (pass + 1) * interval;
      echo('Current: ' + (pass * interval + 1 + (pass === 0 ? 1 : 0)));
      echo('To: ' + max + ' out of ' + pages + ' under pass ' + pass);
      var last = Math.min(pages, max);

      for (var i = pass * interval + 1 + (pass === 0 ? 1 : 0); i <= last; i++) {
        if (limit >= -1) {
          toDo.push(i);
        } else {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          toDo.push(http(httpOpt).then(getPlayersOnPage));
        }
      }

      function completed () {
        toDo = null;
        setTimeout(resolve, delay);
      }

      if (limit >= -1) {
        Promise.each(toDo, function (i) {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          return http(httpOpt).then(getPlayersOnPage);
        }).then(completed);
      } else {
        Promise.all(toDo).then(completed);
      }
    });
  }

  function getPlayersInRole (role) {
    var url = 'http://www.roblox.com/Groups/group.aspx?gid=' + group;
    return http({url: url})
    .then(function (body) {
      var $ = cheerio.load(body);
      if (!rank || rank === -1 && !total) {
        total = parseInt($('#MemberCount').text().substring(9), 10);
      }
      post = getVerificationInputs({html: body});
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList = role;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$currentRoleSetID = role;
      post.__EVENTTARGET = 'ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList';
      post.__ASYNCPOST = 'true';
      delete post.__RequestVerificationToken;
      httpOpt = {
        url: url,
        options: {
          method: 'POST',
          form: post,
          headers: {
            'User-Agent': 'Mozilla'
          }
        }
      };
      return http(httpOpt)
    .then(function (body) {
      var $ = cheerio.load(body);
      pages = parseInt($('div[id=ctl00_cphRoblox_rbxGroupRoleSetMembersPane_dlUsers_Footer_ctl01_Div1]').find('div[class=paging_pagenums_container]').text(), 10) || 1;
      if (limit > -1) {
        pages = Math.min(pages, Math.ceil(limit / 12));
        total = (total != null) ? total : limit;
      }
      getPlayersOnPage(body);
      if (pages === 1) {
        return;
      }

      delete post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = 1;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$HiddenInputButton = '';

      var toDo = [];

      for (var pass = 0; pass < pages / 1000; pass++) {
        toDo.push(pass);
      }

      return Promise.each(toDo, runInterval);
    }); });
  }

  return {
    promise: promise(function (resolve, reject) {
      group = args.group;
      rank = args.rank;
      online = args.online || false;
      limit = args.limit || -2;
      getRoles({group: group}).then(function (roles) {
        var toDo = [];
        if (rank && rank !== -1) {
          toDo.push(getRole({roles: roles, rank: rank}).ID);
        } else {
          for (var i = 0; i < roles.length; i++) {
            toDo.push(roles[i].ID);
          }
        }

        Promise.each(toDo, getPlayersInRole).then(function () {
          total = added;
          resolve({
            total: added,
            players: players
          });
        });
      });
    }),
    getStatus: function () {
      return Math.round((total ? (scanned / total) : 0) * 10000) / 100;
    }
  };
};
<commit_msg>cheerio -> parser and fix<commit_after>// Dependencies
var Promise = require('bluebird');
var parser = require('whacko');

// Includes
var http = require('./util/http.js').func;
var getRoles = require('./util/getRoles.js').func;
var getRole = require('./util/getRole.js').func;
var promise = require('./util/promise.js');
var getVerificationInputs = require('./util/getVerificationInputs.js').func;
var settings = require('../settings.json').getPlayers;

// Args
exports.args = ['group', 'rank', 'limit', 'online'];

// Define
var interval = settings.interval; // Players for every set of requests
var delay = settings.delay; // Time (in milliseconds) to wait after every interval
var debug = true; // Prints out information about what the function is doing, good for status updates or debugging

// Because of the complexity of this function, it's easier to have shared variables initiated every time a call is made.
exports.func = function (args) {
  var players = {};
  var added = 0;
  var scanned = 0;
  var rank;
  var limit;
  var online;
  var group;
  var post;
  var pages;
  var total;
  var httpOpt;

  function addPlayer (name, id) {
    if (!players[name]) {
      added++;
      players[name] = id;
    }
  }

  function echo (str) {
    if (debug) {
      console.log(str);
    }
  }

  function getPlayersOnPage (html) {
    var $ = parser.load(html);
    var currentCount = parseInt($('[name="ctl00$cphRoblox$rbxGroupRoleSetMembersPane$RolesetCountHidden"]').attr('value'), 10);
    if (rank && rank !== -1) {
      if (total) {
        total = Math.min(total, currentCount);
      } else {
        total = currentCount;
      }
    }
    $('div[class=Avatar]').each(function (index, element) {
      if ((limit > -1) && added >= limit) {
        return;
      }
      scanned++;
      var link = $(element).find('a').first();
      var img = $(element).find('.OnlineStatus').find('img').first();
      if (!online || img.attr('src') === '../images/online.png') {
        var id = parseInt(link.attr('href').substring(29), 10);
        addPlayer(link.attr('title'), id);
      }
    });
    $ = null;
    html = null;
  }

  function runInterval (pass) {
    return promise(function (resolve, reject) {
      var toDo = [];

      var max = (pass + 1) * interval;
      echo('Current: ' + (pass * interval + 1 + (pass === 0 ? 1 : 0)));
      echo('To: ' + max + ' out of ' + pages + ' under pass ' + pass);
      var last = Math.min(pages, max);

      for (var i = pass * interval + 1 + (pass === 0 ? 1 : 0); i <= last; i++) {
        if (limit >= -1) {
          toDo.push(i);
        } else {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          toDo.push(http(httpOpt).then(getPlayersOnPage));
        }
      }

      function completed () {
        toDo = null;
        setTimeout(resolve, delay);
      }

      if (limit >= -1) {
        Promise.each(toDo, function (i) {
          httpOpt.options.form.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = i;
          return http(httpOpt).then(getPlayersOnPage);
        }).then(completed);
      } else {
        Promise.all(toDo).then(completed);
      }
    });
  }

  function getPlayersInRole (role) {
    var url = 'http://www.roblox.com/Groups/group.aspx?gid=' + group;
    return http({url: url})
    .then(function (body) {
      var $ = parser.load(body);
      if (!rank || rank === -1 && !total) {
        total = parseInt($('#MemberCount').text().substring(9), 10);
      }
      post = getVerificationInputs({html: body});
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList = role;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$currentRoleSetID = role;
      post.__EVENTTARGET = 'ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList';
      post.__ASYNCPOST = 'true';
      delete post.__RequestVerificationToken;
      httpOpt = {
        url: url,
        options: {
          method: 'POST',
          form: post,
          headers: {
            'User-Agent': 'Mozilla'
          }
        }
      };
      return http(httpOpt)
    .then(function (body) {
      var $ = parser.load(body);
      pages = parseInt($('div[id=ctl00_cphRoblox_rbxGroupRoleSetMembersPane_dlUsers_Footer_ctl01_Div1]').find('div[class=paging_pagenums_container]').text(), 10) || 1;
      if (limit > -1) {
        pages = Math.min(pages, Math.ceil(limit / 12));
        total = (total != null) ? total : limit;
      }
      getPlayersOnPage(body);
      if (pages === 1) {
        return;
      }

      delete post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlRolesetList;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$PageTextBox = 1;
      post.ctl00$cphRoblox$rbxGroupRoleSetMembersPane$dlUsers_Footer$ctl01$HiddenInputButton = '';

      var toDo = [];

      for (var pass = 0; pass < pages / 1000; pass++) {
        toDo.push(pass);
      }

      return Promise.each(toDo, runInterval);
    }); });
  }

  return {
    promise: promise(function (resolve, reject) {
      group = args.group;
      rank = args.rank;
      online = args.online || false;
      limit = args.limit || -2;
      getRoles({group: group}).then(function (roles) {
        var toDo = [];
        if (rank && rank !== -1) {
          toDo.push(getRole({roles: roles, rank: rank}).ID);
        } else {
          for (var i = 0; i < roles.length; i++) {
            toDo.push(roles[i].ID);
          }
        }

        Promise.each(toDo, getPlayersInRole).then(function () {
          total = added;
          resolve({
            total: added,
            players: players
          });
        });
      });
    }),
    getStatus: function () {
      return Math.round((total ? (scanned / total) : 0) * 10000) / 100;
    }
  };
};
<|endoftext|>


<commit_before>var assert = require('assert');
var gifEncoder = require('gif-encoder');
function noop() {}

function Gifsockets(options) {
  // Assert we have a height and width
  var width = options.width;
  var height = options.height;
  assert(width, 'Width was not provided for gifsockets');
  assert(height, 'Height was not provided for gifsockets');

  // Save options
  this.width = width;
  this.height = height;

  // Create connections for later
  this.firstConnections = [];
  this.secondConnections = [];
}
Gifsockets.prototype = {
  createGif: function () {
    return new GifEncoder(this.width, this.height);
  },
  addListener: function (stream, cb) {
    // Write out the header info
    // DEV: It would be nice to write out image info here too (e.g. width x height)
    var gif = this.createGif();
    var that = this;
    gif.on('data', function getHeader (buff) {
      // Clean up our GIF and write out the info
      gif.removeAllListeners();
      stream.write(buff);

      // Add the response to our list of open connections
      that.firstConnections.push({
        stream: stream
      });

      // TODO: Callback with a handle for writing directly to this connection
      (cb || noop)(null);
    });
    gif.writeHeader();
  },
  writeRgbFrame: function (pixels, cb) {
    // DEV: These are inline to avoid `.bind` calls
    // TODO: Which is more performant, scope resolution or context binding?
    var firstConnections = this.firstConnections;
    var secondConnections = this.secondConnections;
    function writeToFirstConnections(buff) {
      firstConnections.forEach(function writeToFirstConnection (conn) {
        conn.res.write(buff);
      });
    }
    function writeToSecondConnections(buff) {
      secondConnections.forEach(function writeToSecondConnection (conn) {
        conn.res.write(buff);
      });
    }

    // Create a gif for ourselves
    var gif = this.createGif();

    // Process the image (addFrame#1)
    gif.setImagePixels(pixels);
    gif.analyzePixels();

    // Write out the image info for the first connections (addFrame#2)
    gif.on('data', writeToFirstConnections);
    gif.writeImageInfo();
    gif.flushData();

    // Write out the image info for the second connections (addFrame#2)
    gif.removeListener('data', writeToFirstConnections);
    gif.on('data', writeToSecondConnections);
    gif.writeImageInfo();
    gif.flushData();

    // Write out the image itself for all connections (addFrame#3)
    gif.on('data', writeToFirstConnections);
    gif.outputImage();
    gif.flushData();

    // Clean up event connections
    gif.removeAllListeners();

    // Move all firstConnections to secondConnections
    secondConnections.push.apply(secondConnections, firstConnections);
    firstConnections.splice(0, firstConnections.length);

    // Callback
    (cb || noop)(null);
  },
  closeAll: function (cb) {
    // Remove and save the open connections
    var connections = this.firstConnections.concat(this.secondConnections);
    this.firstConnections.splice(0, this.firstConnections.length);
    this.secondConnections.splice(0, this.secondConnections.length);

    // Write footer
    var gif = this.createGif();
    gif.on('data', function getFooter (buff) {
      // Clean up our GIF and write out footer
      gif.removeAllListeners();
      connections.forEach(function writeFooter (conn) {
        conn.stream.end(buff);
      });

      // Callback
      (cb || noop)(null);
    });
    gif.finish();
  }
};

module.exports = Gifsockets;<commit_msg>Adding notes for self<commit_after>var assert = require('assert');
var gifEncoder = require('gif-encoder');
function noop() {}

function Gifsockets(options) {
  // Assert we have a height and width
  var width = options.width;
  var height = options.height;
  assert(width, 'Width was not provided for gifsockets');
  assert(height, 'Height was not provided for gifsockets');

  // Save options
  this.width = width;
  this.height = height;

  // Create connections for later
  this.firstConnections = [];
  this.secondConnections = [];
}
Gifsockets.prototype = {
  createGif: function () {
    return new GifEncoder(this.width, this.height);
  },
  addListener: function (stream, cb) {
    // Write out the header info
    // DEV: It would be nice to write out image info here too (e.g. width x height)
    // DEV: Unfortunately, we write out global color pallete before width x height
    // DEV: meaning we would need the first frame before we can do that =(
    var gif = this.createGif();
    var that = this;
    gif.on('data', function getHeader (buff) {
      // Clean up our GIF and write out the info
      gif.removeAllListeners();
      stream.write(buff);

      // Add the response to our list of open connections
      that.firstConnections.push({
        stream: stream
      });

      // TODO: Callback with a handle for writing directly to this connection
      (cb || noop)(null);
    });
    gif.writeHeader();
  },
  writeRgbFrame: function (pixels, cb) {
    // DEV: These are inline to avoid `.bind` calls
    // TODO: Which is more performant, scope resolution or context binding?
    var firstConnections = this.firstConnections;
    var secondConnections = this.secondConnections;
    function writeToFirstConnections(buff) {
      firstConnections.forEach(function writeToFirstConnection (conn) {
        conn.res.write(buff);
      });
    }
    function writeToSecondConnections(buff) {
      secondConnections.forEach(function writeToSecondConnection (conn) {
        conn.res.write(buff);
      });
    }

    // Create a gif for ourselves
    var gif = this.createGif();

    // Process the image (addFrame#1)
    gif.setImagePixels(pixels);
    gif.analyzePixels();

    // Write out the image info for the first connections (addFrame#2)
    gif.on('data', writeToFirstConnections);
    gif.writeImageInfo();
    gif.flushData();

    // Write out the image info for the second connections (addFrame#2)
    gif.removeListener('data', writeToFirstConnections);
    gif.on('data', writeToSecondConnections);
    gif.writeImageInfo();
    gif.flushData();

    // Write out the image itself for all connections (addFrame#3)
    gif.on('data', writeToFirstConnections);
    gif.outputImage();
    gif.flushData();

    // Clean up event connections
    gif.removeAllListeners();

    // Move all firstConnections to secondConnections
    secondConnections.push.apply(secondConnections, firstConnections);
    firstConnections.splice(0, firstConnections.length);

    // Callback
    (cb || noop)(null);
  },
  closeAll: function (cb) {
    // Remove and save the open connections
    var connections = this.firstConnections.concat(this.secondConnections);
    this.firstConnections.splice(0, this.firstConnections.length);
    this.secondConnections.splice(0, this.secondConnections.length);

    // Write footer
    var gif = this.createGif();
    gif.on('data', function getFooter (buff) {
      // Clean up our GIF and write out footer
      gif.removeAllListeners();
      connections.forEach(function writeFooter (conn) {
        conn.stream.end(buff);
      });

      // Callback
      (cb || noop)(null);
    });
    gif.finish();
  }
};

module.exports = Gifsockets;<|endoftext|>


<commit_before>/**
* Author: Sodhan Manandhar
* Description: Rotate heads on mouse hover
* Limitation: Currently only supports 5 images
* Dependency: jQuery
*/
(function ( $ ) {

	$.fn.rotateHeads = function( options ){

			 var settings = $.extend({
					 total_banner_width: 2155,
					 total_width: 431,
					 number_of_images: 5
			 }, options );

		const single_width = settings.total_width / settings.number_of_images;

		var rotate_container = this;

	 		rotate_container.mousemove(function( e ){

							var parentOffset = $(this).parent().offset();
							var relativeXPosition = (e.pageX - parentOffset.left); //offset -> method allows you to retrieve the current position of an element 'relative' to the document
							var count_of_images = settings.number_of_images;

							for( var i = 0; i< count_of_images; i++ ){

								if( relativeXPosition > single_width * i && relativeXPosition <= (single_width * (i+1)) ){

									$(this).css("background-position", "-" + settings.total_width * i +"px" + " 0");
								}
							}

			});

			return this;
	};

}( jQuery ));
<commit_msg>Added ability to have any number of seperate blocks inside of a sprite<commit_after>/**
* Author: Sodhan Manandhar
* Description: Rotate heads on mouse hover
* Dependency: jQuery
* Additions: Number of seperate images can be any number
*/
(function ( $ ) {

	$.fn.rotateHeads = function( options ){

			 var settings = $.extend({
					 total_banner_width: 2155,
					 total_width: 431,
					 number_of_images: 5
			 }, options );

		const single_width = settings.total_width / settings.number_of_images;

		var rotate_container = this;

	 		rotate_container.mousemove(function( e ){

							var parentOffset = $(this).parent().offset();
							var relativeXPosition = (e.pageX - parentOffset.left); //offset -> method allows you to retrieve the current position of an element 'relative' to the document
							var count_of_images = settings.number_of_images;

							for( var i = 0; i< count_of_images; i++ ){

								if( relativeXPosition > single_width * i && relativeXPosition <= (single_width * (i+1)) ){

									$(this).css("background-position", "-" + settings.total_width * i +"px" + " 0");
								}
							}

			});

			return this;
	};

}( jQuery ));
<|endoftext|>


<commit_before>import { put, call, takeLatest, select, all } from 'redux-saga/effects';

import { MAPPINGS } from '../actions/constants';
import { fetchMappings, addMapping, fetchTermsAggregations } from '../apis';
import {
	fetchMappingsSuccess,
	fetchMappingsFailure,
	fetchTermsAggregationSuccess,
	setError,
	addMappingSuccess,
	addMappingFailure,
	clearError,
} from '../actions';
import { getAppname, getUrl } from '../reducers/app';
import { isEmptyObject } from '../utils';
import {
	extractColumns,
	META_FIELDS,
	getSortableTypes,
} from '../utils/mappings';

const INGNORE_META_TYPES = ['~logs', '.percolator', '~percolator', '_default_'];

function* handleFetchMappings() {
	try {
		yield put(clearError());
		const appname = yield select(getAppname);
		const url = yield select(getUrl);
		const data = yield call(fetchMappings, appname, url);
		if (!isEmptyObject(data)) {
			const indexes = Object.keys(data);
			let properties = {};
			const types = [];
			const indexTypeMap = {};
			const typePropertyMapping = {};

			indexes.forEach(index => {
				Object.keys(data[index].mappings).forEach(type => {
					if (
						data[index].mappings[type].properties &&
						INGNORE_META_TYPES.indexOf(type) === -1
					) {
						indexTypeMap[index] = [
							...(indexTypeMap[index] || []),
							type,
						];
						types.push(type);

						properties = {
							...properties,
							...data[index].mappings[type].properties,
						};

						typePropertyMapping[index] = {};
						typePropertyMapping[index][type] =
							data[index].mappings[type].properties;
					}
				});
			});

			const mappings = {
				[appname]: {
					properties,
				},
			};

			const allColumns = [
				...META_FIELDS,
				...extractColumns(mappings[appname]),
			];

			let visibleColumns = allColumns.filter(col => col !== '_type');
			if (indexes.length <= 1) {
				visibleColumns = visibleColumns.filter(col => col !== '_index');
			}

			const filteredTypes = types.filter(
				type => !INGNORE_META_TYPES.includes(type),
			);

			const searchableColumns = Object.keys(properties).filter(
				property =>
					properties[property].type === 'string' ||
					properties[property].type === 'text',
			);

			// get fields for which aggregations can be found
			const sortableTypes = getSortableTypes();
			const fieldsForAggregations = Object.keys(properties).filter(
				property =>
					sortableTypes.indexOf(properties[property].type) > -1,
			);

			// fetch terms aggregation
			const aggregationResponse = yield call(
				fetchTermsAggregations,
				appname,
				url,
				fieldsForAggregations,
			);

			yield put(
				fetchTermsAggregationSuccess(aggregationResponse.aggregations),
			);

			yield put(
				fetchMappingsSuccess(
					mappings,
					indexes,
					filteredTypes,
					indexTypeMap,
					allColumns,
					visibleColumns,
					searchableColumns,
					typePropertyMapping,
				),
			);
		} else {
			throw new Error('Unable to fetch content');
		}
	} catch (error) {
		yield put(fetchMappingsFailure());
		yield put(setError(error));
	}
}

function* handleAddMapping({ indexName, typeName, field, mapping }) {
	try {
		yield put(clearError());
		const url = yield select(getUrl);
		yield call(addMapping, indexName, typeName, url, field, mapping);
		yield put(addMappingSuccess());
		yield call(handleFetchMappings); // sagas FTW
	} catch (error) {
		yield put(addMappingFailure());
		yield put(setError(error));
	}
}

function* watchAddMapping() {
	yield takeLatest(MAPPINGS.ADD_MAPPING_REQUEST, handleAddMapping);
}

function* watchFetchMappings() {
	yield takeLatest(MAPPINGS.MAPPINGS_FETCH_REQUEST, handleFetchMappings);
}

export default function* mappingsWatcher() {
	yield all([watchAddMapping(), watchFetchMappings()]);
}
<commit_msg>Remove manual terms aggregation call<commit_after>import { put, call, takeLatest, select, all } from 'redux-saga/effects';

import { MAPPINGS } from '../actions/constants';
import { fetchMappings, addMapping } from '../apis';
import {
	fetchMappingsSuccess,
	fetchMappingsFailure,
	setError,
	addMappingSuccess,
	addMappingFailure,
	clearError,
} from '../actions';
import { getAppname, getUrl } from '../reducers/app';
import { isEmptyObject } from '../utils';
import { extractColumns, META_FIELDS } from '../utils/mappings';

const INGNORE_META_TYPES = ['~logs', '.percolator', '~percolator', '_default_'];

function* handleFetchMappings() {
	try {
		yield put(clearError());
		const appname = yield select(getAppname);
		const url = yield select(getUrl);
		const data = yield call(fetchMappings, appname, url);
		if (!isEmptyObject(data)) {
			const indexes = Object.keys(data);
			let properties = {};
			const types = [];
			const indexTypeMap = {};
			const typePropertyMapping = {};

			indexes.forEach(index => {
				Object.keys(data[index].mappings).forEach(type => {
					if (
						data[index].mappings[type].properties &&
						INGNORE_META_TYPES.indexOf(type) === -1
					) {
						indexTypeMap[index] = [
							...(indexTypeMap[index] || []),
							type,
						];
						types.push(type);

						properties = {
							...properties,
							...data[index].mappings[type].properties,
						};

						typePropertyMapping[index] = {};
						typePropertyMapping[index][type] =
							data[index].mappings[type].properties;
					}
				});
			});

			const mappings = {
				[appname]: {
					properties,
				},
			};

			const allColumns = [
				...META_FIELDS,
				...extractColumns(mappings[appname]),
			];

			let visibleColumns = allColumns.filter(col => col !== '_type');
			if (indexes.length <= 1) {
				visibleColumns = visibleColumns.filter(col => col !== '_index');
			}

			const filteredTypes = types.filter(
				type => !INGNORE_META_TYPES.includes(type),
			);

			const searchableColumns = Object.keys(properties).filter(
				property =>
					properties[property].type === 'string' ||
					properties[property].type === 'text',
			);

			// // get fields for which aggregations can be found
			// const sortableTypes = getSortableTypes();
			// const fieldsForAggregations = Object.keys(properties).filter(
			// 	property =>
			// 		sortableTypes.indexOf(properties[property].type) > -1,
			// );

			// // fetch terms aggregation
			// const aggregationResponse = yield call(
			// 	fetchTermsAggregations,
			// 	appname,
			// 	url,
			// 	fieldsForAggregations,
			// );

			// yield put(
			// 	fetchTermsAggregationSuccess(aggregationResponse.aggregations),
			// );

			yield put(
				fetchMappingsSuccess(
					mappings,
					indexes,
					filteredTypes,
					indexTypeMap,
					allColumns,
					visibleColumns,
					searchableColumns,
					typePropertyMapping,
				),
			);
		} else {
			throw new Error('Unable to fetch content');
		}
	} catch (error) {
		yield put(fetchMappingsFailure());
		yield put(setError(error));
	}
}

function* handleAddMapping({ indexName, typeName, field, mapping }) {
	try {
		yield put(clearError());
		const url = yield select(getUrl);
		yield call(addMapping, indexName, typeName, url, field, mapping);
		yield put(addMappingSuccess());
		yield call(handleFetchMappings); // sagas FTW
	} catch (error) {
		yield put(addMappingFailure());
		yield put(setError(error));
	}
}

function* watchAddMapping() {
	yield takeLatest(MAPPINGS.ADD_MAPPING_REQUEST, handleAddMapping);
}

function* watchFetchMappings() {
	yield takeLatest(MAPPINGS.MAPPINGS_FETCH_REQUEST, handleFetchMappings);
}

export default function* mappingsWatcher() {
	yield all([watchAddMapping(), watchFetchMappings()]);
}
<|endoftext|>


<commit_before>import React from 'react'
import Reflux from 'reflux'
import Route from 'react-route'

import Card from 'material-ui/lib/card/card'
import CardActions from 'material-ui/lib/card/card-actions'
import CardHeader from 'material-ui/lib/card/card-header'
import CardMedia from 'material-ui/lib/card/card-media'
import CardTitle from 'material-ui/lib/card/card-title'
import CardText from 'material-ui/lib/card/card-text'
import List from 'material-ui/lib/lists/list'
import ListItem from 'material-ui/lib/lists/list-item'
import colors from 'material-ui/lib/styles/colors'
import DescriptionIcon from 'material-ui/lib/svg-icons/action/description'
import RaisedButton from 'material-ui/lib/raised-button'
import FlatButton from 'material-ui/lib/flat-button'
import Avatar from 'material-ui/lib/avatar'
import ActionDescription from 'material-ui/lib/svg-icons/action/description'
import CircularProgress from 'material-ui/lib/circular-progress'

import { actions as searchActions } from './search_store'

const TYPE_MEETING = "https://oparl.org/schema/1.0/Meeting"
const TYPE_PAPER = "https://oparl.org/schema/1.0/Paper"
const TYPE_FILE = "https://oparl.org/schema/1.0/File"

export default React.createClass({
  mixins: [
    Reflux.listenTo(searchActions.search, "onSearch"),
    Reflux.listenTo(searchActions.search.completed, "onSearchCompleted"),
    Reflux.listenTo(searchActions.search.failed, "onSearchFailed")
  ],

  getInitialState: function() {
    return {
      results: []
    }
  },

  componentDidMount: function() {
    // Start an empty search on startup
    searchActions.search("")
  },

  onSearch: function() {
    this.setState({
      loading: true
    })
  },

  onSearchFailed: function() {
    this.setState({
      loading: false
    })
  },

  onSearchCompleted: function(results) {
    this.setState({
      loading: false,
      results: results
    })
  },

  render: function() {
    return (

        {this.state.loading ?
          <CircularProgress size={2}/> :
          this.state.results.map((result, i) =>
            <SearchResult key={i} {...result}/>
          )}

    )
  },
})

class SearchResult extends React.Component {
  render() {
    switch(this.props.type) {
    case TYPE_MEETING:
      return <Meeting {...this.props}/>
      break
    case TYPE_PAPER:
      return <Paper {...this.props}/>
      break
    case TYPE_FILE:
      return <File {...this.props}/>
      break
    default:
      return

    }
  }
}

class Meeting extends React.Component {
  render() {
    return (
      <Card style={{ marginBottom: "1em" }}>
        <CardHeader
            title={this.props.name}
            subtitle={`${this.props.shortName} ${this.props.start}`}
            style={{ backgroundColor: colors.lime500 }}
            />
        <CardText>
          {findFilesInObject(this.props).map(id =>
            <FileItem key={id} id={id}/>
          )}
          <List>
            {this.props.agendaItem ?
              this.props.agendaItem.map((item, i) =>
                <ListItem
                    key={i}
                    disabled={!item.consultation}
                    innerDivStyle={{ paddingRight: "0" }}
                    leftIcon={(item.number && item.number.length <= 2) ?
                      <Avatar size={24}>{item.number}</Avatar> :
                      {item.number}
                    }>

                    {item.name}

                  {(findFilesInObject(item).length > 0) ? (
                    <List>
                      {findFilesInObject(item).map(id =>
                        <FileItem key={id} id={id}/>
                      )}
                    </List>
                  ) : ""}
                </ListItem>
              ) : ""}
          </List>
        </CardText>
      </Card>
    )
  }
}

class Paper extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      meetings: []
    }
  }

  render() {
    let paper = this.props

    return (
      <Card style={{ marginBottom: "1em" }}>
        <CardHeader
            avatar={<Avatar title={paper.shortName} size={32}
                        backgroundColor={paperShortNameToColor(paper.shortName)}
                   >
                {paper.shortName[0]}
              </Avatar>}
            title={paper.name}
            titleStyle={{
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              // Vorlagen names tend to get really long, though
              // they're included in the @title in full
              maxWidth: '56em'
            }}
            subtitle={`${paper.shortName} ${iso8601ToDate(paper.publishedDate)}`}
            style={{ backgroundColor: colors.lime700 }}
            />
        <CardText>
          {findFilesInObject(paper).map(id =>
            <FileItem key={id} id={id}/>
          )}
          <List>
            {(paper.consultation || [])
              .filter(consultation => !!consultation.meeting)
              .map((consultation, i) =>
                <MeetingItem key={i} id={consultation.meeting} filesOf={paper.id}/>
            )}
          </List>
        </CardText>
        <CardActions style={{ textAlign: 'right' }}>
          <RaisedButton label="Vorlage lesen" primary={true}/>
        </CardActions>
      </Card>
    )
  }
}

class File extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
    }
  }

  componentDidMount() {
    fetch(`/api/oparl/file/${encodeURIComponent(this.props.id)}/context`)
      .then(res => res.json())
      .then(results => {
        for(let result of results) {
          if (result.type == TYPE_MEETING) {
            this.setState({
              meeting: result
            })
          } else if (result.type == TYPE_PAPER) {
            this.setState({
              paper: result
            })
          }
        }
      })
  }

  render() {
    return (
      <Card style={{ marginBottom: "1em" }}>
        <CardHeader
            title={<Avatar backgroundColor="white" size={36}><ActionDescription/></Avatar> {this.props.name}}
            style={{ backgroundColor: colors.lime300 }}
            />
        <CardText>
          <List>
            {this.state.meeting ? <MeetingItem {...this.state.meeting}/> : ""}
            {this.state.paper ? <PaperItem {...this.state.paper}/> : ""}
          </List>
        </CardText>
        <CardActions style={{ textAlign: 'right' }}>
          <RaisedButton label="Text Annotieren" primary={true}
              style={{ verticalAlign: 'top' }}
              onClick={ev => Route.go(`/file/${this.props.id}`)}
              />
          <RaisedButton label="Original-PDF" secondary={true}
              linkButton={true} href={this.props.downloadUrl}
              />
        </CardActions>
      </Card>
    )
  }
}

class MeetingItem extends React.Component {
  componentDidMount() {
    if (!this.props.name) {
      fetch(`/api/oparl/meeting/${encodeURIComponent(this.props.id)}`)
        .then(res => res.json())
        .then(result => {
          this.setState(result)
        })
    }
  }

  render() {
    let meeting = this.state || this.props

    return !this.props.filesOf ?
      <ListItem disabled={true}
          primaryText={meeting.name}
          secondaryText={iso8601ToDate(meeting.start)}
          >
      </ListItem> :
      <ListItem disabled={true} innerDivStyle={{ paddingRight: "0" }}>
        <List subheader={meeting.name}>
          {findFilesInObject(meeting).map(id =>
            <FileItem key={id} id={id}/>
          )}
          {meeting.agendaItem ?
            <ListItem innerDivStyle={{ paddingRight: "0" }}>
              {meeting.agendaItem
                .filter(item =>
                  item.consultation &&
                  item.consultation.parentID === this.props.filesOf
                )
                .map((item, i) =>
                  <List key={i} subheader={item.name}>
                    {findFilesInObject(item).map(id =>
                      <FileItem key={id} id={id}/>
                    )}
                  </List>
                )}
            </ListItem> : ""
          }
        </List>
      </ListItem>
  }
}

class PaperItem extends React.Component {
  componentDidMount() {
    if (!this.props.name) {
      fetch(`/api/oparl/paper/${encodeURIComponent(this.props.id)}`)
        .then(res => res.json())
        .then(result => {
          this.setState(result)
        })
    }
  }

  render() {
    let paper = this.state || this.props

    return <ListItem
        leftIcon={
          <Avatar title={paper.shortName} size={32} color="white"
              backgroundColor={paperShortNameToColor(paper.shortName)}
              >
            {paper.shortName[0]}
          </Avatar>
        }
        primaryText={paper.name}
        secondaryText={iso8601ToDate(paper.publishedDate)}
        />
  }
}

class FileItem extends React.Component {
  componentDidMount() {
    if (!this.props.name) {
      fetch(`/api/oparl/file/${encodeURIComponent(this.props.id)}`)
        .then(res => res.json())
        .then(result => {
          this.setState(result)
        })
    }
  }

  render() {
    let file = this.state || this.props

    return (
      <ListItem
          innerDivStyle={{ paddingRight: "0" }}
          primaryText={file.name}
          leftIcon={<ActionDescription/>}
          onClick={ev => Route.go(`/file/${this.props.id}`)}
          />
    )
  }

  handleClickAnnotate(ev) {
    ev.preventDefault()

    Route.go(`/file/${this.props.id}`)
  }
}

const FILES_KEYS = [
  'invitation',
  'masterFile',
  'mainFile',
  'derivativeFile',
  'verbatimProtocol',
  'resultsProtocol',
  'resolutionFile',
  'auxiliaryFile'
]

function findFilesInObject(obj) {
  let results = []
  for(let k of FILES_KEYS) {
    let v = obj[k]
    if (typeof v == 'string') {
      results.push(v)
    } else if (v) {
      results.push(...v)
    }
  }
  return results
}

function iso8601ToDate(iso8601) {
  let m
  if (iso8601 && (m = iso8601.match(/(\d{4})-(\d\d)-(\d\d)/))) {
    return `${m[3]}.${m[2]}.${m[1]}`
  } else {
    return "?"
  }
}

function paperShortNameToColor(id) {
  if (/^V/.test(id)) {
    return colors.deepPurple500
  } else if (/^A/.test(id)) {
    return colors.lightBlue500
  } else if (id) {
    return colors.lightGreen500
  } else {
    return colors.lightGreen200
  }
}
<commit_msg>search_results: allow multiple meetings/papers for File<commit_after>import React from 'react'
import Reflux from 'reflux'
import Route from 'react-route'

import Card from 'material-ui/lib/card/card'
import CardActions from 'material-ui/lib/card/card-actions'
import CardHeader from 'material-ui/lib/card/card-header'
import CardMedia from 'material-ui/lib/card/card-media'
import CardTitle from 'material-ui/lib/card/card-title'
import CardText from 'material-ui/lib/card/card-text'
import List from 'material-ui/lib/lists/list'
import ListItem from 'material-ui/lib/lists/list-item'
import colors from 'material-ui/lib/styles/colors'
import DescriptionIcon from 'material-ui/lib/svg-icons/action/description'
import RaisedButton from 'material-ui/lib/raised-button'
import FlatButton from 'material-ui/lib/flat-button'
import Avatar from 'material-ui/lib/avatar'
import ActionDescription from 'material-ui/lib/svg-icons/action/description'
import CircularProgress from 'material-ui/lib/circular-progress'

import { actions as searchActions } from './search_store'

const TYPE_MEETING = "https://oparl.org/schema/1.0/Meeting"
const TYPE_PAPER = "https://oparl.org/schema/1.0/Paper"
const TYPE_FILE = "https://oparl.org/schema/1.0/File"

export default React.createClass({
  mixins: [
    Reflux.listenTo(searchActions.search, "onSearch"),
    Reflux.listenTo(searchActions.search.completed, "onSearchCompleted"),
    Reflux.listenTo(searchActions.search.failed, "onSearchFailed")
  ],

  getInitialState: function() {
    return {
      results: []
    }
  },

  componentDidMount: function() {
    // Start an empty search on startup
    searchActions.search("")
  },

  onSearch: function() {
    this.setState({
      loading: true
    })
  },

  onSearchFailed: function() {
    this.setState({
      loading: false
    })
  },

  onSearchCompleted: function(results) {
    this.setState({
      loading: false,
      results: results
    })
  },

  render: function() {
    return (

        {this.state.loading ?
          <CircularProgress size={2}/> :
          this.state.results.map((result, i) =>
            <SearchResult key={i} {...result}/>
          )}

    )
  },
})

class SearchResult extends React.Component {
  render() {
    switch(this.props.type) {
    case TYPE_MEETING:
      return <Meeting {...this.props}/>
      break
    case TYPE_PAPER:
      return <Paper {...this.props}/>
      break
    case TYPE_FILE:
      return <File {...this.props}/>
      break
    default:
      return

    }
  }
}

class Meeting extends React.Component {
  render() {
    return (
      <Card style={{ marginBottom: "1em" }}>
        <CardHeader
            title={this.props.name}
            subtitle={`${this.props.shortName} ${this.props.start}`}
            style={{ backgroundColor: colors.lime500 }}
            />
        <CardText>
          {findFilesInObject(this.props).map(id =>
            <FileItem key={id} id={id}/>
          )}
          <List>
            {this.props.agendaItem ?
              this.props.agendaItem.map((item, i) =>
                <ListItem
                    key={i}
                    disabled={!item.consultation}
                    innerDivStyle={{ paddingRight: "0" }}
                    leftIcon={(item.number && item.number.length <= 2) ?
                      <Avatar size={24}>{item.number}</Avatar> :
                      {item.number}
                    }>

                    {item.name}

                  {(findFilesInObject(item).length > 0) ? (
                    <List>
                      {findFilesInObject(item).map(id =>
                        <FileItem key={id} id={id}/>
                      )}
                    </List>
                  ) : ""}
                </ListItem>
              ) : ""}
          </List>
        </CardText>
      </Card>
    )
  }
}

class Paper extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      meetings: []
    }
  }

  render() {
    let paper = this.props

    return (
      <Card style={{ marginBottom: "1em" }}>
        <CardHeader
            avatar={<Avatar title={paper.shortName} size={32}
                        backgroundColor={paperShortNameToColor(paper.shortName)}
                   >
                {paper.shortName[0]}
              </Avatar>}
            title={paper.name}
            titleStyle={{
              whiteSpace: 'nowrap',
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              // Vorlagen names tend to get really long, though
              // they're included in the @title in full
              maxWidth: '56em'
            }}
            subtitle={`${paper.shortName} ${iso8601ToDate(paper.publishedDate)}`}
            style={{ backgroundColor: colors.lime700 }}
            />
        <CardText>
          {findFilesInObject(paper).map(id =>
            <FileItem key={id} id={id}/>
          )}
          <List>
            {(paper.consultation || [])
              .filter(consultation => !!consultation.meeting)
              .map((consultation, i) =>
                <MeetingItem key={i} id={consultation.meeting} filesOf={paper.id}/>
            )}
          </List>
        </CardText>
        <CardActions style={{ textAlign: 'right' }}>
          <RaisedButton label="Vorlage lesen" primary={true}/>
        </CardActions>
      </Card>
    )
  }
}

class File extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      meetings: [],
      papers: []
    }
  }

  componentDidMount() {
    fetch(`/api/oparl/file/${encodeURIComponent(this.props.id)}/context`)
      .then(res => res.json())
      .then(results => {
        let meetings = []
        let papers = []
        for(let result of results) {
          if (result.type == TYPE_MEETING) {
            meetings.push(result)
          } else if (result.type == TYPE_PAPER) {
            papers.push(result)
          }
        }
        this.setState({
          meetings: meetings,
          papers: papers
        })
      })
  }

  render() {
    return (
      <Card style={{ marginBottom: "1em" }}>
        <CardHeader
            title={<Avatar backgroundColor="white" size={36}><ActionDescription/></Avatar> {this.props.name}}
            style={{ backgroundColor: colors.lime300 }}
            />
        <CardText>
          <List>
            {this.state.meetings.map(meeting =>
              <MeetingItem {...meeting}/>
            )}
            {this.state.papers.map(paper =>
              <PaperItem {...paper}/>
            )}
          </List>
        </CardText>
        <CardActions style={{ textAlign: 'right' }}>
          <RaisedButton label="Text Annotieren" primary={true}
              style={{ verticalAlign: 'top' }}
              onClick={ev => Route.go(`/file/${this.props.id}`)}
              />
          <RaisedButton label="Original-PDF" secondary={true}
              linkButton={true} href={this.props.downloadUrl}
              />
        </CardActions>
      </Card>
    )
  }
}

class MeetingItem extends React.Component {
  componentDidMount() {
    if (!this.props.name) {
      fetch(`/api/oparl/meeting/${encodeURIComponent(this.props.id)}`)
        .then(res => res.json())
        .then(result => {
          this.setState(result)
        })
    }
  }

  render() {
    let meeting = this.state || this.props

    return !this.props.filesOf ?
      <ListItem disabled={true}
          primaryText={meeting.name}
          secondaryText={iso8601ToDate(meeting.start)}
          >
      </ListItem> :
      <ListItem disabled={true} innerDivStyle={{ paddingRight: "0" }}>
        <List subheader={meeting.name}>
          {findFilesInObject(meeting).map(id =>
            <FileItem key={id} id={id}/>
          )}
          {meeting.agendaItem ?
            <ListItem innerDivStyle={{ paddingRight: "0" }}>
              {meeting.agendaItem
                .filter(item =>
                  item.consultation &&
                  item.consultation.parentID === this.props.filesOf
                )
                .map((item, i) =>
                  <List key={i} subheader={item.name}>
                    {findFilesInObject(item).map(id =>
                      <FileItem key={id} id={id}/>
                    )}
                  </List>
                )}
            </ListItem> : ""
          }
        </List>
      </ListItem>
  }
}

class PaperItem extends React.Component {
  componentDidMount() {
    if (!this.props.name) {
      fetch(`/api/oparl/paper/${encodeURIComponent(this.props.id)}`)
        .then(res => res.json())
        .then(result => {
          this.setState(result)
        })
    }
  }

  render() {
    let paper = this.state || this.props

    return <ListItem
        leftIcon={
          <Avatar title={paper.shortName} size={32} color="white"
              backgroundColor={paperShortNameToColor(paper.shortName)}
              >
            {paper.shortName[0]}
          </Avatar>
        }
        primaryText={paper.name}
        secondaryText={iso8601ToDate(paper.publishedDate)}
        />
  }
}

class FileItem extends React.Component {
  componentDidMount() {
    if (!this.props.name) {
      fetch(`/api/oparl/file/${encodeURIComponent(this.props.id)}`)
        .then(res => res.json())
        .then(result => {
          this.setState(result)
        })
    }
  }

  render() {
    let file = this.state || this.props

    return (
      <ListItem
          innerDivStyle={{ paddingRight: "0" }}
          primaryText={file.name}
          leftIcon={<ActionDescription/>}
          onClick={ev => Route.go(`/file/${this.props.id}`)}
          />
    )
  }

  handleClickAnnotate(ev) {
    ev.preventDefault()

    Route.go(`/file/${this.props.id}`)
  }
}

const FILES_KEYS = [
  'invitation',
  'masterFile',
  'mainFile',
  'derivativeFile',
  'verbatimProtocol',
  'resultsProtocol',
  'resolutionFile',
  'auxiliaryFile'
]

function findFilesInObject(obj) {
  let results = []
  for(let k of FILES_KEYS) {
    let v = obj[k]
    if (typeof v == 'string') {
      results.push(v)
    } else if (v) {
      results.push(...v)
    }
  }
  return results
}

function iso8601ToDate(iso8601) {
  let m
  if (iso8601 && (m = iso8601.match(/(\d{4})-(\d\d)-(\d\d)/))) {
    return `${m[3]}.${m[2]}.${m[1]}`
  } else {
    return "?"
  }
}

function paperShortNameToColor(id) {
  if (/^V/.test(id)) {
    return colors.deepPurple500
  } else if (/^A/.test(id)) {
    return colors.lightBlue500
  } else if (id) {
    return colors.lightGreen500
  } else {
    return colors.lightGreen200
  }
}
<|endoftext|>


<commit_before>export default function(onenter, onupdate, onexit) {
  var enter = this.enter(), update = this, exit = this.exit();
  enter = typeof onenter === "function" ? onenter(enter) : enter.append(onenter + "");
  if (onupdate != null) update = onupdate(update);
  if (onexit == null) exit.remove(); else onexit(exit);
  return enter && update ? enter.merge(update).order() : update;
}
<commit_msg>Alternative solution for #257<commit_after>export default function(onenter, onupdate, onexit) {
  var enter = this.enter(), update = this, exit = this.exit();
  enter = typeof onenter === "function" ? onenter(enter) : enter.append(onenter + "");
  if (onupdate != null) update = onupdate(update);
  if (onexit == null) exit.remove(); else onexit(exit);
  return enter && update ? enter.selection().merge(update.selection()).order() : update;
}
<|endoftext|>


<commit_before>import { toUpper } from 'ramda';
import { getP, getLocale } from './selectors';

const fakeState = {
    polyglot: {
        locale: 'fr',
        phrases: { hello: 'bonjour' },
    },
};

describe('selectors', () => {
    describe('getLocale', () => {
        it('doesn\'t crash when state is an empty object', () => {
            expect(getLocale({})).toBe(undefined);
        });

        it('doesn\'t crash when state is an empty object', () => {
            expect(getLocale(fakeState)).toBe('fr');
        });
    });

    describe('getP', () => {
        const p = getP(fakeState);

        it('doesn\'t crash when state is an empty object', () => {
            expect(getP({})).toBe(undefined);
        });

        it('translates "hello" to "bonjour"', () => {
            expect(p.t('hello')).toBe('bonjour');
        });

        it('translates "hello" to "Bonjour" (capitalize)', () => {
            expect(p.tc('hello')).toBe('Bonjour');
        });

        it('translates "hello" to "BONJOUR" (upper-case)', () => {
            expect(p.tu('hello')).toBe('BONJOUR');
        });

        it('translates "hello" to "BONJOUR" (morphed with upper-case function)', () => {
            expect(p.tm(toUpper)('hello')).toBe('BONJOUR');
        });
    });
});
<commit_msg>selectors.spec : check than getP() selector return a valid polyglot object.<commit_after>import { toUpper, curry, evolve, is, pipe, values, all, equals } from 'ramda';
import { getP, getLocale } from './selectors';

const on = curry((x, f) => f(x));

const fakeState = {
    polyglot: {
        locale: 'fr',
        phrases: { hello: 'bonjour' },
    },
};

describe('selectors', () => {
    describe('getLocale', () => {
        it('doesn\'t crash when state is an empty object', () => {
            expect(getLocale({})).toBe(undefined);
        });

        it('doesn\'t crash when state is an empty object', () => {
            expect(getLocale(fakeState)).toBe('fr');
        });
    });

    describe('getP', () => {
        const p = getP(fakeState);

        it('gives a valid redux-polyglot object', () => {
            expect(
                on(p)(pipe(
                    evolve({
                        phrases: is(Object),
                        currentLocale: is(String),
                        allowMissing: is(Boolean),
                        warn: is(Function),
                        t: is(Function),
                        tc: is(Function),
                        tu: is(Function),
                        tm: is(Function),
                    }),
                    values,
                    all(equals(true)),
                ))
            ).toBe(true);
        });

        it('doesn\'t crash when state is an empty object', () => {
            expect(getP({})).toBe(undefined);
        });

        it('translates "hello" to "bonjour"', () => {
            expect(p.t('hello')).toBe('bonjour');
        });

        it('translates "hello" to "Bonjour" (capitalize)', () => {
            expect(p.tc('hello')).toBe('Bonjour');
        });

        it('translates "hello" to "BONJOUR" (upper-case)', () => {
            expect(p.tu('hello')).toBe('BONJOUR');
        });

        it('translates "hello" to "BONJOUR" (morphed with upper-case function)', () => {
            expect(p.tm(toUpper)('hello')).toBe('BONJOUR');
        });
    });
});
<|endoftext|>


<commit_before>'use strict'

let app = require('electron').app
let path = require('path')
let fstream = require('fstream')
let Promise = require('bluebird')

let partial = require('underscore').partial
let needle = require('needle')

let install = require('../tasks/install/core')
let os = require('./os')
let log = require('./log')('ibrew')

let self = {
  formulas: {
    '7za': {
      format: 'executable',
      on_missing: () => {
        if (self.os() === 'linux') {
          // TODO: add link to doc page too
          let msg = '7-zip missing: 7za must be in $PATH\n(Try installing p7zip-full)'
          throw new Error(msg)
        }
      },
      version_check: {
        args: [],
        parser: /([0-9a-z.v]*)\s+Copyright/
      }
    },
    'butler': {
      format: '7z'
    },
    'elevate': {
      format: '7z',
      os_whitelist: ['windows']
    }
  },

  bin_path: () => {
    return path.join(app.getPath('userData'), 'bin')
  },

  root_url: () => {
    return 'https://misc.amos.me'
  },

  ext: () => {
    if (os.platform() === 'win32') {
      return '.exe'
    } else {
      return ''
    }
  },

  /** platform in go format */
  os: () => {
    let result = os.platform()
    if (result === 'win32') {
      return 'windows'
    }
    return result
  },

  /** arch in go format */
  arch: () => {
    let result = os.arch()
    if (result === 'x64') {
      return 'amd64'
    } else if (result === 'ia32') {
      return '386'
    } else {
      return 'unknown'
    }
  },

  normalize_version: (version) => {
    return version.replace(/^v/, '')
  },

  version_equal: (a, b) => {
    let aa = self.normalize_version(a)
    let bb = self.normalize_version(b)
    return aa === bb
  },

  archive_name: (name) => {
    let formula = self.formulas[name]

    if (formula.format === '7z') {
      return `${name}.7z`
    } else if (formula.format === 'executable') {
      return `${name}${self.ext()}`
    } else {
      throw new Error(`Unknown formula format: ${formula.format}`)
    }
  },

  download_to_file: (opts, url, file) => {
    let req = needle.get(url)
    let sink = fstream.Writer({
      path: file,
      mode: 0o777
    })
    req.pipe(sink)

    return new Promise((resolve, reject) => {
      sink.on('close', resolve)
      req.on('error', reject)
    })
  },

  get_latest_version: (channel) => {
    return new Promise((resolve, reject) => {
      let url = `${channel}/LATEST`
      needle.get(url, (err, res) => {
        if (err || res.statusCode !== 200) {
          return reject(err || `status code: ${res.statusCode}`)
        }
        let version = res.body.toString('utf8').replace(/\s/g, '')
        resolve(version)
      })
    })
  },

  fetch: async (opts, name) => {
    let noop = () => null
    let onstatus = opts.onstatus || noop

    let formula = self.formulas[name]
    if (!formula) throw new Error(`Unknown formula: ${name}`)

    let os_whitelist = formula.os_whitelist
    if (os_whitelist && os_whitelist.indexOf(self.os()) === -1) {
      log(opts, `${name}: skipping, it's irrelevant on ${self.os()}`)
      return
    }

    let channel = `${self.root_url()}/${name}/${self.os()}-${self.arch()}`

    let download_version = async (version) => {
      let archive_name = self.archive_name(name)
      let archive_path = path.join(self.bin_path(), archive_name)
      let archive_url = `${channel}/${version}/${archive_name}`
      onstatus(`Downloading ${name} ${version}`, 'download')
      log(opts, `${name}: downloading '${version}' from ${archive_url}`)

      await self.download_to_file(opts, archive_url, archive_path)

      if (formula.format === 'executable') {
        log(opts, `${name}: installed!`)
      } else {
        log(opts, `${name}: extracting ${formula.format} archive`)
        onstatus(`Installing ${name}`, 'install')
        await install.install({ archive_path, dest_path: self.bin_path() })
        log(opts, `${name}: installed!`)
      }
    }

    onstatus(`Making sure we have the latest of everything...`, 'stopwatch')
    let get_latest_version = partial(self.get_latest_version, channel)

    let check = Object.assign({
      args: ['-V'],
      parser: /([a-zA-Z0-9\.]+)/
    }, formula.version_check || {})
    let info

    try {
      info = await os.check_presence(name, check.args, check.parser)
    } catch (err) {
      if (formula.on_missing) formula.on_missing()
      log(opts, `${name}: missing, downloading latest`)
      return await download_version(await get_latest_version())
    }

    let local_version = info.parsed
    log(opts, `${name}: have local version '${local_version}'`)

    let latest_version
    try {
      latest_version = await get_latest_version()
    } catch (err) {
      log(opts, `${name}: cannot get latest version, skipping: ${err.stack || err}`)
      return
    }

    if (self.version_equal(local_version, latest_version) ||
        local_version === 'head') {
      log(opts, `${name}: up-to-date`)
      return
    }

    log(opts, `${name}: upgrading '${local_version}' => '${latest_version}'`)
    await download_version(latest_version)
  }
}

module.exports = self
<commit_msg>use new dl server<commit_after>'use strict'

let app = require('electron').app
let path = require('path')
let fstream = require('fstream')
let Promise = require('bluebird')

let partial = require('underscore').partial
let needle = require('needle')

let install = require('../tasks/install/core')
let os = require('./os')
let log = require('./log')('ibrew')

let self = {
  formulas: {
    '7za': {
      format: 'executable',
      on_missing: () => {
        if (self.os() === 'linux') {
          // TODO: add link to doc page too
          let msg = '7-zip missing: 7za must be in $PATH\n(Try installing p7zip-full)'
          throw new Error(msg)
        }
      },
      version_check: {
        args: [],
        parser: /([0-9a-z.v]*)\s+Copyright/
      }
    },
    'butler': {
      format: '7z'
    },
    'elevate': {
      format: '7z',
      os_whitelist: ['windows']
    }
  },

  bin_path: () => {
    return path.join(app.getPath('userData'), 'bin')
  },

  root_url: () => {
    return 'https://dl.itch.ovh'
  },

  ext: () => {
    if (os.platform() === 'win32') {
      return '.exe'
    } else {
      return ''
    }
  },

  /** platform in go format */
  os: () => {
    let result = os.platform()
    if (result === 'win32') {
      return 'windows'
    }
    return result
  },

  /** arch in go format */
  arch: () => {
    let result = os.arch()
    if (result === 'x64') {
      return 'amd64'
    } else if (result === 'ia32') {
      return '386'
    } else {
      return 'unknown'
    }
  },

  normalize_version: (version) => {
    return version.replace(/^v/, '')
  },

  version_equal: (a, b) => {
    let aa = self.normalize_version(a)
    let bb = self.normalize_version(b)
    return aa === bb
  },

  archive_name: (name) => {
    let formula = self.formulas[name]

    if (formula.format === '7z') {
      return `${name}.7z`
    } else if (formula.format === 'executable') {
      return `${name}${self.ext()}`
    } else {
      throw new Error(`Unknown formula format: ${formula.format}`)
    }
  },

  download_to_file: (opts, url, file) => {
    let req = needle.get(url)
    let sink = fstream.Writer({
      path: file,
      mode: 0o777
    })
    req.pipe(sink)

    return new Promise((resolve, reject) => {
      sink.on('close', resolve)
      req.on('error', reject)
    })
  },

  get_latest_version: (channel) => {
    return new Promise((resolve, reject) => {
      let url = `${channel}/LATEST`
      needle.get(url, (err, res) => {
        if (err || res.statusCode !== 200) {
          return reject(err || `status code: ${res.statusCode}`)
        }
        let version = res.body.toString('utf8').replace(/\s/g, '')
        resolve(version)
      })
    })
  },

  fetch: async (opts, name) => {
    let noop = () => null
    let onstatus = opts.onstatus || noop

    let formula = self.formulas[name]
    if (!formula) throw new Error(`Unknown formula: ${name}`)

    let os_whitelist = formula.os_whitelist
    if (os_whitelist && os_whitelist.indexOf(self.os()) === -1) {
      log(opts, `${name}: skipping, it's irrelevant on ${self.os()}`)
      return
    }

    let channel = `${self.root_url()}/${name}/${self.os()}-${self.arch()}`

    let download_version = async (version) => {
      let archive_name = self.archive_name(name)
      let archive_path = path.join(self.bin_path(), archive_name)
      let archive_url = `${channel}/${version}/${archive_name}`
      onstatus(`Downloading ${name} ${version}`, 'download')
      log(opts, `${name}: downloading '${version}' from ${archive_url}`)

      await self.download_to_file(opts, archive_url, archive_path)

      if (formula.format === 'executable') {
        log(opts, `${name}: installed!`)
      } else {
        log(opts, `${name}: extracting ${formula.format} archive`)
        onstatus(`Installing ${name}`, 'install')
        await install.install({ archive_path, dest_path: self.bin_path() })
        log(opts, `${name}: installed!`)
      }
    }

    onstatus(`Making sure we have the latest of everything...`, 'stopwatch')
    let get_latest_version = partial(self.get_latest_version, channel)

    let check = Object.assign({
      args: ['-V'],
      parser: /([a-zA-Z0-9\.]+)/
    }, formula.version_check || {})
    let info

    try {
      info = await os.check_presence(name, check.args, check.parser)
    } catch (err) {
      if (formula.on_missing) formula.on_missing()
      log(opts, `${name}: missing, downloading latest`)
      return await download_version(await get_latest_version())
    }

    let local_version = info.parsed
    log(opts, `${name}: have local version '${local_version}'`)

    let latest_version
    try {
      latest_version = await get_latest_version()
    } catch (err) {
      log(opts, `${name}: cannot get latest version, skipping: ${err.stack || err}`)
      return
    }

    if (self.version_equal(local_version, latest_version) ||
        local_version === 'head') {
      log(opts, `${name}: up-to-date`)
      return
    }

    log(opts, `${name}: upgrading '${local_version}' => '${latest_version}'`)
    await download_version(latest_version)
  }
}

module.exports = self
<|endoftext|>