<commit_before>#!/usr/bin/env node
'use strict';
const childProcess = require('child_process');
const chalk = require('chalk');
const meow = require('meow');
const ora = require('ora');
const inquirer = require('inquirer');
const isBin = require('isbin');
const dateFormat = require('dateformat');
const trim = require('trim');
const termTitle = require('term-title');
const cliTruncate = require('cli-truncate');
const copy = require('copy-paste').copy;
const logUpdate = require('log-update');
const cliCursor = require('cli-cursor');
const notifier = require('node-notifier');
const editor = require('editor');
const figures = require('./figures');
const utils = require('./utils');
const somafm = require('.');

const cli = meow(`
  Usage
    $ somafm [<command> <args>] [<options>]

  Example
    $ somafm
    $ somafm list
    $ somafm list ambient
    $ somafm info groovesalad
    $ somafm play fluid

  Commands
    list [<keywords>]   list channels, optionally filter by keywords
    info <channel>      show channel information
    play <channel>      play channel
    record <channel>    start recording channel
    list-favourites     list your favourite songs
    edit-favourites     edit your favourite songs

  Options
    -n   Don't show desktop notifications
`, {
  alias: {
    h: 'help',
    v: 'version'
  }
});

const players = [
  {
    cmd: 'mplayer',
    args: ['-quiet'],
    titleRegex: /StreamTitle='(.*)';StreamUrl=/,
    keyPress: true
  },
  {
    cmd: 'mpv',
    args: ['--msg-level=all=info'],
    titleRegex: /.*icy-title: (.*)$/
  }
];
const streamripperBin = 'streamripper';

const spinner = ora({color: 'yellow'});

let notify = (data, favourite) => {
  if (favourite) {
    data.title = `${figures.heart} ${data.title}`;
  }

  notifier.notify(data);
};

function showChannelList(channels) {
  for (const channel of channels) {
    const str = `${chalk.bold(channel.title)} [${chalk.green(channel.id)}] (${chalk.blue(channel.genre)}) - ${(channel.description)}`;

    if (process.stdout.columns) {
      console.log(cliTruncate(str, process.stdout.columns - 1));
    } else {
      console.log(str);
    }
  }
}

function showChannel(channel) {
  console.log(
`  ${chalk.bold(channel.fullTitle)} [${chalk.green(channel.id)}]

  ${chalk.blue(channel.description)}

    ${chalk.yellow('Now playing')}   ${channel.lastPlaying}

             ${chalk.yellow('DJ')}   ${channel.dj}
          ${chalk.yellow('Genre')}   ${channel.genre}
      ${chalk.yellow('Listeners')}   ${channel.listeners}

     ${chalk.yellow('Stream URL')}   ${channel.stream.urls[0]}`
  );
}

function list(search) {
  spinner.start('Loading channels');

  somafm.getChannels({search, sortChannels: true})
    .then(channels => {
      spinner.stop();
      return Promise.resolve(channels);
    })
    .then(showChannelList)
    .catch(err => {
      spinner.fail(err.toString());
      process.exit(20);
    });
}

function info(channelId) {
  spinner.start('Loading channel information');

  somafm.getChannel(channelId)
    .then(channel => {
      spinner.stop();
      showChannel(channel);
    })
    .catch(err => {
      spinner.fail(err.toString());
      process.exit(10);
    });
}

function play(channelId) {
  spinner.start('Loading channel information');

  somafm.getChannel(channelId)
    .then(channel => {
      spinner.stop();
      return channel;
    })
    .then(playChannel)
    .catch(err => {
      spinner.fail(err.toString());
      process.exit(10);
    });
}

function getPlayer() {
  return new Promise((resolve, reject) => {
    for (const player of players) {
      if (isBin(player.cmd)) {
        resolve(player);
      }
    }

    reject(new Error('No player executable found. Please ensure MPlayer or mpv is installed on your system and runnable within your shell.'));
  });
}

function playChannel(channel) {
  return getPlayer()
    .then(player => {
      let currentTitle = '';
      let currentTitleOut;
      let currentTime;
      let currentFavourite;

      cliCursor.hide();
      console.log(`  Playing   ${chalk.bold(channel.fullTitle)}\n\n  ${chalk.blue(channel.description)}\n`);

      const args = player.args.concat(channel.stream.urls[0]);
      const playerProc = childProcess.spawn(player.cmd, args);

      const stdin = process.stdin;
      stdin.setRawMode(true);
      stdin.resume();
      stdin.setEncoding('utf-8');

      stdin.on('data', key => {
        if (player.keyPress && ['m', '9', '0', '/', '*'].indexOf(key) > -1) {
          playerProc.stdin.write(key);
        }

        if (key === 'c') {
          copy(currentTitle);
        }

        if (['f', '+'].indexOf(key) > -1) {
          utils.addToFavourites(currentTitle);
          currentFavourite = true;

          logTitle(currentTime, currentTitleOut, true, true);
          windowTitle(currentTitleOut, true);
        }

        if (['u', '-'].indexOf(key) > -1) {
          utils.removeFromFavourites(currentTitle);
          currentFavourite = false;

          logTitle(currentTime, currentTitleOut, false, true);
          windowTitle(currentTitleOut);
        }

        // `ctrl`+`c`, `esc`, `q`
        if (['\u0003', '\u001b', 'q'].indexOf(key) > -1) {
          if (currentTitleOut) {
            logTitle(currentTime, currentTitleOut, currentFavourite);
          }
          logUpdate.done();

          playerProc.kill();
        }
      });

      playerProc.stdout.on('data', data => {
        const line = trim(data.toString());

        const res = line.match(player.titleRegex);
        let title;

        if (res && (title = res[1])) {
          const time = dateFormat(new Date(), 'HH:MM:ss');
          const titleOut = title.match(new RegExp(`SomaFM|Big Url|${channel.title}`, 'i')) ? chalk.gray(title) : title;

          // Overwrite last line
          if (currentTime) {
            logTitle(currentTime, currentTitleOut, currentFavourite);
          }

          currentTitle = title;
          currentTime = chalk.yellow(time);
          currentTitleOut = titleOut;

          currentFavourite = utils.isFavourite(currentTitle);

          logUpdate.done();
          logTitle(currentTime, currentTitleOut, currentFavourite, true);
          windowTitle(currentTitle, currentFavourite);
          notify({
            title: currentTitle,
            message: channel.fullTitle,
            icon: channel.imageFile
          }, currentFavourite);
        }
      });

      playerProc.on('error', Promise.reject);

      playerProc.on('exit', () => {
        termTitle();
        process.exit(0);
      });

      return Promise.resolve();
    });
}

function interactive() {
  spinner.start('Loading channels');

  somafm.getChannels({sortChannels: true})
    .then(channels => {
      spinner.stop();
      return channels;
    })
    .then(showPrompt)
    .then(answers => {
      console.log();
      play(answers.channel);
    })
    .catch(err => {
      console.error(err.toString());
      process.exit(20);
    });
}

function showPrompt(channels) {
  inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'));

  const lines = channels.map(channel => ({
    name: `${channel.title} (${chalk.blue(channel.genre)})`,
    value: channel.id,
    short: channel.title
  }));

  return inquirer.prompt([
    {
      type: 'autocomplete',
      name: 'channel',
      message: 'Channel  ',
      source: (answers, input) => Promise.resolve().then(() => filerLines(input, lines))
    }
  ]);
}

function filerLines(input, lines) {
  if (input !== null) {
    return lines.filter(channel => channel.name.toLowerCase().includes(input.toLowerCase()));
  }

  return lines;
}

function logTitle(time, title, favourite, playing) {
  let state = 0;
  let prefix = '';

  if (favourite) {
    state++;
  }
  if (playing) {
    state += 2;
  }

  switch (state) {
    case 1:
      prefix = `${chalk.red(figures.heart)} `;
      break;
    case 2:
      prefix = `${chalk.green(figures.play)} `;
      break;
    case 3:
      prefix = `${chalk.green(figures.heart)} `;
      break;
    default:
  }

  logUpdate(`  ${time}  ${prefix}${title}`);
}

function windowTitle(title, favourite) {
  termTitle(`${favourite ? figures.heart : figures.play} ${title}`);
}

function record(channel) {
  return new Promise((resolve, reject) => {
    if (!isBin(streamripperBin)) {
      reject(new Error('Streamripper executable not found. Please ensure Streamripper is installed on your system and runnable with the "streamripper" command.'));
    }

    const date = dateFormat(new Date(), 'yyyymmdd_HHMMss');
    const args = [
      channel.stream.url,
      '-D', `${channel.fullTitle}/${date}/%1q %A - %T`
    ];
    const streamripperProc = childProcess.spawn(streamripperBin, args, {stdio: [process.stdin, 'pipe', 'pipe']});
    let currentStatus;
    let currentTitle;

    cliCursor.hide();
    console.log(
`  Recording ${chalk.bold(channel.fullTitle)}
  to directory ${chalk.yellow(`${channel.fullTitle}/${date}`)}\n`
    );

    streamripperProc.stdout.on('data', data => {
      const line = data.toString();

      const regex = /^\[(r|sk)ipping.*] (.*) \[(.{7})]$/m;
      const res = line.match(regex);

      if (res && res[1] && res[2]) {
        if ((currentStatus !== res[1] || currentTitle !== res[2]) && res[2].length > 1) {
          if (res[3] && trim(res[3]) === '0b') {
            return;
          }

          currentStatus = res[1];
          currentTitle = res[2];

          const time = dateFormat(new Date(), 'HH:MM:ss');
          const status = res[1] === 'r' ? 'Recording' : 'Skipping ';
          console.log(`  ${chalk.yellow(time)}  ${chalk.bold(status)}  ${currentTitle}`);
        }
      }
    });

    streamripperProc.on('error', reject);

    streamripperProc.on('exit', resolve);
  });
}

function init() {
  if (cli.flags.n) {
    // Disable desktop notifications
    notify = () => {};
  }

  console.log();

  if (cli.input.length === 0) {
    interactive();
    return;
  }

  if (['list', 'ls', 'l'].indexOf(cli.input[0]) > -1) {
    list(cli.input.slice(1));
    return;
  }

  if (['info', 'i'].indexOf(cli.input[0]) > -1 && cli.input[1]) {
    info(cli.input[1]);
    return;
  }

  if (['play', 'p'].indexOf(cli.input[0]) > -1 && cli.input[1]) {
    play(cli.input[1]);
    return;
  }

  if (['record', 'r'].indexOf(cli.input[0]) > -1 && cli.input[1]) {
    somafm.getChannel(cli.input[1])
      .then(channel => {
        record(channel);
      })
      .catch(err => {
        console.error(err.toString());
        process.exit(10);
      });
    return;
  }

  if (['list-favourites', 'list-favorites', 'lf'].indexOf(cli.input[0]) > -1) {
    utils.getFavourites().then(favourites => {
      for (const title of favourites) {
        console.log(`  ${chalk.red(figures.heart)} ${title}`);
      }
    });
    return;
  }

  if (['edit-favourites', 'edit-favorites', 'ef'].indexOf(cli.input[0]) > -1) {
    editor(utils.getFavouritesFile());
    return;
  }

  cli.showHelp();
  process.exit(1);
}

init();
<commit_msg>Fix channel list output width<commit_after>#!/usr/bin/env node
'use strict';
const childProcess = require('child_process');
const chalk = require('chalk');
const meow = require('meow');
const ora = require('ora');
const inquirer = require('inquirer');
const isBin = require('isbin');
const dateFormat = require('dateformat');
const trim = require('trim');
const termTitle = require('term-title');
const cliTruncate = require('cli-truncate');
const copy = require('copy-paste').copy;
const logUpdate = require('log-update');
const cliCursor = require('cli-cursor');
const notifier = require('node-notifier');
const editor = require('editor');
const figures = require('./figures');
const utils = require('./utils');
const somafm = require('.');

const cli = meow(`
  Usage
    $ somafm [<command> <args>] [<options>]

  Example
    $ somafm
    $ somafm list
    $ somafm list ambient
    $ somafm info groovesalad
    $ somafm play fluid

  Commands
    list [<keywords>]   list channels, optionally filter by keywords
    info <channel>      show channel information
    play <channel>      play channel
    record <channel>    start recording channel
    list-favourites     list your favourite songs
    edit-favourites     edit your favourite songs

  Options
    -n   Don't show desktop notifications
`, {
  alias: {
    h: 'help',
    v: 'version'
  }
});

const players = [
  {
    cmd: 'mplayer',
    args: ['-quiet'],
    titleRegex: /StreamTitle='(.*)';StreamUrl=/,
    keyPress: true
  },
  {
    cmd: 'mpv',
    args: ['--msg-level=all=info'],
    titleRegex: /.*icy-title: (.*)$/
  }
];
const streamripperBin = 'streamripper';

const spinner = ora({color: 'yellow'});

let notify = (data, favourite) => {
  if (favourite) {
    data.title = `${figures.heart} ${data.title}`;
  }

  notifier.notify(data);
};

function getWidth(stream) {
  const columns = stream.columns;

  if (!columns) {
    return 80;
  }

  if (process.platform === 'win32') {
    return columns - 1;
  }

  return columns;
}

function showChannelList(channels) {
  for (const channel of channels) {
    const str = `${chalk.bold(channel.title)} [${chalk.green(channel.id)}] (${chalk.blue(channel.genre)}) - ${(channel.description)}`;

    console.log(cliTruncate(str, getWidth(process.stdout)));
  }
}

function showChannel(channel) {
  console.log(
`  ${chalk.bold(channel.fullTitle)} [${chalk.green(channel.id)}]

  ${chalk.blue(channel.description)}

    ${chalk.yellow('Now playing')}   ${channel.lastPlaying}

             ${chalk.yellow('DJ')}   ${channel.dj}
          ${chalk.yellow('Genre')}   ${channel.genre}
      ${chalk.yellow('Listeners')}   ${channel.listeners}

     ${chalk.yellow('Stream URL')}   ${channel.stream.urls[0]}`
  );
}

function list(search) {
  spinner.start('Loading channels');

  somafm.getChannels({search, sortChannels: true})
    .then(channels => {
      spinner.stop();
      return Promise.resolve(channels);
    })
    .then(showChannelList)
    .catch(err => {
      spinner.fail(err.toString());
      process.exit(20);
    });
}

function info(channelId) {
  spinner.start('Loading channel information');

  somafm.getChannel(channelId)
    .then(channel => {
      spinner.stop();
      showChannel(channel);
    })
    .catch(err => {
      spinner.fail(err.toString());
      process.exit(10);
    });
}

function play(channelId) {
  spinner.start('Loading channel information');

  somafm.getChannel(channelId)
    .then(channel => {
      spinner.stop();
      return channel;
    })
    .then(playChannel)
    .catch(err => {
      spinner.fail(err.toString());
      process.exit(10);
    });
}

function getPlayer() {
  return new Promise((resolve, reject) => {
    for (const player of players) {
      if (isBin(player.cmd)) {
        resolve(player);
      }
    }

    reject(new Error('No player executable found. Please ensure MPlayer or mpv is installed on your system and runnable within your shell.'));
  });
}

function playChannel(channel) {
  return getPlayer()
    .then(player => {
      let currentTitle = '';
      let currentTitleOut;
      let currentTime;
      let currentFavourite;

      cliCursor.hide();
      console.log(`  Playing   ${chalk.bold(channel.fullTitle)}\n\n  ${chalk.blue(channel.description)}\n`);

      const args = player.args.concat(channel.stream.urls[0]);
      const playerProc = childProcess.spawn(player.cmd, args);

      const stdin = process.stdin;
      stdin.setRawMode(true);
      stdin.resume();
      stdin.setEncoding('utf-8');

      stdin.on('data', key => {
        if (player.keyPress && ['m', '9', '0', '/', '*'].indexOf(key) > -1) {
          playerProc.stdin.write(key);
        }

        if (key === 'c') {
          copy(currentTitle);
        }

        if (['f', '+'].indexOf(key) > -1) {
          utils.addToFavourites(currentTitle);
          currentFavourite = true;

          logTitle(currentTime, currentTitleOut, true, true);
          windowTitle(currentTitleOut, true);
        }

        if (['u', '-'].indexOf(key) > -1) {
          utils.removeFromFavourites(currentTitle);
          currentFavourite = false;

          logTitle(currentTime, currentTitleOut, false, true);
          windowTitle(currentTitleOut);
        }

        // `ctrl`+`c`, `esc`, `q`
        if (['\u0003', '\u001b', 'q'].indexOf(key) > -1) {
          if (currentTitleOut) {
            logTitle(currentTime, currentTitleOut, currentFavourite);
          }
          logUpdate.done();

          playerProc.kill();
        }
      });

      playerProc.stdout.on('data', data => {
        const line = trim(data.toString());

        const res = line.match(player.titleRegex);
        let title;

        if (res && (title = res[1])) {
          const time = dateFormat(new Date(), 'HH:MM:ss');
          const titleOut = title.match(new RegExp(`SomaFM|Big Url|${channel.title}`, 'i')) ? chalk.gray(title) : title;

          // Overwrite last line
          if (currentTime) {
            logTitle(currentTime, currentTitleOut, currentFavourite);
          }

          currentTitle = title;
          currentTime = chalk.yellow(time);
          currentTitleOut = titleOut;

          currentFavourite = utils.isFavourite(currentTitle);

          logUpdate.done();
          logTitle(currentTime, currentTitleOut, currentFavourite, true);
          windowTitle(currentTitle, currentFavourite);
          notify({
            title: currentTitle,
            message: channel.fullTitle,
            icon: channel.imageFile
          }, currentFavourite);
        }
      });

      playerProc.on('error', Promise.reject);

      playerProc.on('exit', () => {
        termTitle();
        process.exit(0);
      });

      return Promise.resolve();
    });
}

function interactive() {
  spinner.start('Loading channels');

  somafm.getChannels({sortChannels: true})
    .then(channels => {
      spinner.stop();
      return channels;
    })
    .then(showPrompt)
    .then(answers => {
      console.log();
      play(answers.channel);
    })
    .catch(err => {
      console.error(err.toString());
      process.exit(20);
    });
}

function showPrompt(channels) {
  inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'));

  const lines = channels.map(channel => ({
    name: `${channel.title} (${chalk.blue(channel.genre)})`,
    value: channel.id,
    short: channel.title
  }));

  return inquirer.prompt([
    {
      type: 'autocomplete',
      name: 'channel',
      message: 'Channel  ',
      source: (answers, input) => Promise.resolve().then(() => filerLines(input, lines))
    }
  ]);
}

function filerLines(input, lines) {
  if (input !== null) {
    return lines.filter(channel => channel.name.toLowerCase().includes(input.toLowerCase()));
  }

  return lines;
}

function logTitle(time, title, favourite, playing) {
  let state = 0;
  let prefix = '';

  if (favourite) {
    state++;
  }
  if (playing) {
    state += 2;
  }

  switch (state) {
    case 1:
      prefix = `${chalk.red(figures.heart)} `;
      break;
    case 2:
      prefix = `${chalk.green(figures.play)} `;
      break;
    case 3:
      prefix = `${chalk.green(figures.heart)} `;
      break;
    default:
  }

  logUpdate(`  ${time}  ${prefix}${title}`);
}

function windowTitle(title, favourite) {
  termTitle(`${favourite ? figures.heart : figures.play} ${title}`);
}

function record(channel) {
  return new Promise((resolve, reject) => {
    if (!isBin(streamripperBin)) {
      reject(new Error('Streamripper executable not found. Please ensure Streamripper is installed on your system and runnable with the "streamripper" command.'));
    }

    const date = dateFormat(new Date(), 'yyyymmdd_HHMMss');
    const args = [
      channel.stream.url,
      '-D', `${channel.fullTitle}/${date}/%1q %A - %T`
    ];
    const streamripperProc = childProcess.spawn(streamripperBin, args, {stdio: [process.stdin, 'pipe', 'pipe']});
    let currentStatus;
    let currentTitle;

    cliCursor.hide();
    console.log(
`  Recording ${chalk.bold(channel.fullTitle)}
  to directory ${chalk.yellow(`${channel.fullTitle}/${date}`)}\n`
    );

    streamripperProc.stdout.on('data', data => {
      const line = data.toString();

      const regex = /^\[(r|sk)ipping.*] (.*) \[(.{7})]$/m;
      const res = line.match(regex);

      if (res && res[1] && res[2]) {
        if ((currentStatus !== res[1] || currentTitle !== res[2]) && res[2].length > 1) {
          if (res[3] && trim(res[3]) === '0b') {
            return;
          }

          currentStatus = res[1];
          currentTitle = res[2];

          const time = dateFormat(new Date(), 'HH:MM:ss');
          const status = res[1] === 'r' ? 'Recording' : 'Skipping ';
          console.log(`  ${chalk.yellow(time)}  ${chalk.bold(status)}  ${currentTitle}`);
        }
      }
    });

    streamripperProc.on('error', reject);

    streamripperProc.on('exit', resolve);
  });
}

function init() {
  if (cli.flags.n) {
    // Disable desktop notifications
    notify = () => {};
  }

  console.log();

  if (cli.input.length === 0) {
    interactive();
    return;
  }

  if (['list', 'ls', 'l'].indexOf(cli.input[0]) > -1) {
    list(cli.input.slice(1));
    return;
  }

  if (['info', 'i'].indexOf(cli.input[0]) > -1 && cli.input[1]) {
    info(cli.input[1]);
    return;
  }

  if (['play', 'p'].indexOf(cli.input[0]) > -1 && cli.input[1]) {
    play(cli.input[1]);
    return;
  }

  if (['record', 'r'].indexOf(cli.input[0]) > -1 && cli.input[1]) {
    somafm.getChannel(cli.input[1])
      .then(channel => {
        record(channel);
      })
      .catch(err => {
        console.error(err.toString());
        process.exit(10);
      });
    return;
  }

  if (['list-favourites', 'list-favorites', 'lf'].indexOf(cli.input[0]) > -1) {
    utils.getFavourites().then(favourites => {
      for (const title of favourites) {
        console.log(`  ${chalk.red(figures.heart)} ${title}`);
      }
    });
    return;
  }

  if (['edit-favourites', 'edit-favorites', 'ef'].indexOf(cli.input[0]) > -1) {
    editor(utils.getFavouritesFile());
    return;
  }

  cli.showHelp();
  process.exit(1);
}

init();
<|endoftext|>


<commit_before>// Copyright 2018-2020, University of Colorado Boulder

/**
 * ?fuzzBoard keyboard fuzzer
 * TODO: keep track of keystate so that we don't trigger a keydown of keyA before the previous keyA keyup event has been called.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import stepTimer from '../../../axon/js/stepTimer.js';
import Random from '../../../dot/js/Random.js';
import scenery from '../scenery.js';
import globalKeyStateTracker from './globalKeyStateTracker.js';
import KeyboardUtils from './KeyboardUtils.js';
import PDOMUtils from './pdom/PDOMUtils.js';

// uppercase matters
const keyboardTestingSchema = {
  INPUT: [ ...KeyboardUtils.ARROW_KEYS, KeyboardUtils.KEY_PAGE_UP, KeyboardUtils.KEY_PAGE_DOWN,
    KeyboardUtils.KEY_HOME, KeyboardUtils.KEY_END, KeyboardUtils.KEY_ENTER, KeyboardUtils.KEY_SPACE ],
  DIV: [ ...KeyboardUtils.ARROW_KEYS, ...KeyboardUtils.WASD_KEYS ],
  P: [ KeyboardUtils.KEY_ESCAPE ],
  BUTTON: [ KeyboardUtils.KEY_ENTER, KeyboardUtils.KEY_SPACE ]
};

const MAX_MS_KEY_HOLD_DOWN = 100;
const NEXT_ELEMENT_THRESHOLD = .1;

const DO_KNOWN_KEYS_THRESHOLD = .60; // for keydown/up, 60 percent of the events
const CLICK_EVENT_THRESHOLD = DO_KNOWN_KEYS_THRESHOLD + .10; // 10 percent of the events

const KEY_DOWN = 'keydown';
const KEY_UP = 'keyup';

const MIN_KEY_CODE = 8; // Backspace key
const MAX_KEY_CODE = 127; // Delete key

/**
 *
 * @param {Object} [options]
 * @constructor
 */
class KeyboardFuzzer {
  constructor( display, seed ) {

    // @private
    this.display = display;
    this.random = new Random( { seed: seed } );
    this.numberOfComponentsTested = 10;
    this.keyupListeners = [];

    // @private {HTMLElement}
    this.currentElement = null;
  }

  /**
   * @private
   * Randomly decide if we should focus the next element, or stay focused on the current element
   */
  chooseNextElement() {
    if ( this.currentElement === null ) {
      this.currentElement = document.activeElement;
    }
    else if ( this.random.nextDouble() < NEXT_ELEMENT_THRESHOLD ) {
      sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'choosing new element' );
      sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

      // before we change focus to the next item, immediately release all keys that were down on the active element
      this.clearListeners();
      const nextFocusable = PDOMUtils.getRandomFocusable( this.random );
      nextFocusable.focus();
      this.currentElement = nextFocusable;

      sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
    }
  }

  /**
   * @private
   */
  clearListeners() {
    this.keyupListeners.forEach( listener => {
      stepTimer.clearTimeout( listener );
      listener();
    } );
  }

  /**
   * @private
   * @param {HTMLElement} element
   */
  triggerClickEvent( element ) {
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'triggering click' );
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

    element.click();

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
  }

  /**
   * Trigger a keydown/keyup pair. The keyup is triggered with a timeout.
   * @private
   *
   * @param {HTMLElement} element
   * @param {number} keyCode
   */
  triggerKeyDownUpEvents( element, keyCode ) {

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'trigger keydown/up: ' + keyCode );
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

    // TODO: screen readers normally take our keydown events, but may not here, is the descrpency ok?
    this.triggerDOMEvent( KEY_DOWN, element, keyCode );

    const timeout = this.random.nextInt( MAX_MS_KEY_HOLD_DOWN );

    this.keyupListeners.push( stepTimer.setTimeout( () => {
      this.triggerDOMEvent( KEY_UP, element, keyCode );

    }, timeout === MAX_MS_KEY_HOLD_DOWN ? 2000 : timeout ) );

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
  }

  /**
   * Trigger a keydown/keyup pair with a random keyCode
   * @private
   * @param {HTMLElement} element
   */
  triggerRandomKeyDownUpEvents( element ) {

    const randomKeyCode = Math.floor( this.random.nextDouble() * ( MAX_KEY_CODE - MIN_KEY_CODE ) + MIN_KEY_CODE );

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'trigger random keydown/up: ' + randomKeyCode );
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

    this.triggerKeyDownUpEvents( element, randomKeyCode );

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
  }

  /**
   * A random event creater that sends keyboard events. Based on the idea of fuzzMouse, but to test/spam accessibility
   * related keyboard navigation and alternate input implementation.
   *
   * @public
   * TODO: NOTE: Right now this is a very experimental implementation. Tread wearily
   * TODO: @param keyboardPressesPerFocusedItem {number} - basically would be the same as fuzzRate, but handling
   * TODO:     the keydown events for a focused item
   */
  fuzzBoardEvents( fuzzRate ) {

    const a11yPointer = this.display._input.a11yPointer;
    if ( a11yPointer && !a11yPointer.blockTrustedEvents ) {
      a11yPointer.blockTrustedEvents = true;
    }

    for ( let i = 0; i < this.numberOfComponentsTested; i++ ) {

      // find a focus a random element
      this.chooseNextElement();

      for ( let i = 0; i < fuzzRate / this.numberOfComponentsTested; i++ ) {

        sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'main loop, i=' + i );
        sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

        // get active element, focus might have changed in the last press
        const elementWithFocus = document.activeElement;

        if ( keyboardTestingSchema[ elementWithFocus.tagName.toUpperCase() ] ) {

          const randomNumber = this.random.nextDouble();
          if ( randomNumber < DO_KNOWN_KEYS_THRESHOLD ) {
            const keyCodes = keyboardTestingSchema[ elementWithFocus.tagName ];
            const keyCode = this.random.sample( keyCodes );
            this.triggerKeyDownUpEvents( elementWithFocus, keyCode );
          }
          else if ( randomNumber < CLICK_EVENT_THRESHOLD ) {
            this.triggerClickEvent( elementWithFocus );
          }
          else {
            this.triggerRandomKeyDownUpEvents( elementWithFocus );
          }
        }
        else {
          this.triggerRandomKeyDownUpEvents( elementWithFocus );
        }
        // TODO: What about other types of events, not just keydown/keyup??!?!
        // TODO: what about application role elements

        sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
      }
    }
  }

  /**
   * Taken from example in http://output.jsbin.com/awenaq/3,
   * @param {string} event
   * @param {HTMLElement} element
   * @param {number} [keycode]
   * @private
   */
  triggerDOMEvent( event, element, keyCode ) {
    const eventObj = document.createEventObject ?
                     document.createEventObject() : document.createEvent( 'Events' );

    if ( eventObj.initEvent ) {
      eventObj.initEvent( event, true, true );
    }

    eventObj.key = '';
    eventObj.keyCode = keyCode;
    // eventObj.shiftKey = true; // TODO: we can add modifier keys in here with options?
    eventObj.which = keyCode;

    // add any modifier keys to the event
    if ( globalKeyStateTracker.shiftKeyDown ) {
      eventObj.shiftKey = true;
    }
    if ( globalKeyStateTracker.altKeyDown ) {
      eventObj.altKey = true;
    }
    if ( globalKeyStateTracker.ctrlKeyDown ) {
      eventObj.ctrlKey = true;
    }

    element.dispatchEvent ? element.dispatchEvent( eventObj ) : element.fireEvent( 'on' + event, eventObj );
  }
}

scenery.register( 'KeyboardFuzzer', KeyboardFuzzer );
export default KeyboardFuzzer;<commit_msg>remove ie11 support and use KeyboardEvent constructor instead of document.createEvent + initEvent (deprecated), https://github.com/phetsims/scenery/issues/1127<commit_after>// Copyright 2018-2020, University of Colorado Boulder

/**
 * ?fuzzBoard keyboard fuzzer
 * TODO: keep track of keystate so that we don't trigger a keydown of keyA before the previous keyA keyup event has been called.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import stepTimer from '../../../axon/js/stepTimer.js';
import Random from '../../../dot/js/Random.js';
import scenery from '../scenery.js';
import globalKeyStateTracker from './globalKeyStateTracker.js';
import KeyboardUtils from './KeyboardUtils.js';
import PDOMUtils from './pdom/PDOMUtils.js';

// uppercase matters
const keyboardTestingSchema = {
  INPUT: [ ...KeyboardUtils.ARROW_KEYS, KeyboardUtils.KEY_PAGE_UP, KeyboardUtils.KEY_PAGE_DOWN,
    KeyboardUtils.KEY_HOME, KeyboardUtils.KEY_END, KeyboardUtils.KEY_ENTER, KeyboardUtils.KEY_SPACE ],
  DIV: [ ...KeyboardUtils.ARROW_KEYS, ...KeyboardUtils.WASD_KEYS ],
  P: [ KeyboardUtils.KEY_ESCAPE ],
  BUTTON: [ KeyboardUtils.KEY_ENTER, KeyboardUtils.KEY_SPACE ]
};

const MAX_MS_KEY_HOLD_DOWN = 100;
const NEXT_ELEMENT_THRESHOLD = .1;

const DO_KNOWN_KEYS_THRESHOLD = .60; // for keydown/up, 60 percent of the events
const CLICK_EVENT_THRESHOLD = DO_KNOWN_KEYS_THRESHOLD + .10; // 10 percent of the events

const KEY_DOWN = 'keydown';
const KEY_UP = 'keyup';

const MIN_KEY_CODE = 8; // Backspace key
const MAX_KEY_CODE = 127; // Delete key

/**
 *
 * @param {Object} [options]
 * @constructor
 */
class KeyboardFuzzer {
  constructor( display, seed ) {

    // @private
    this.display = display;
    this.random = new Random( { seed: seed } );
    this.numberOfComponentsTested = 10;
    this.keyupListeners = [];

    // @private {HTMLElement}
    this.currentElement = null;
  }

  /**
   * @private
   * Randomly decide if we should focus the next element, or stay focused on the current element
   */
  chooseNextElement() {
    if ( this.currentElement === null ) {
      this.currentElement = document.activeElement;
    }
    else if ( this.random.nextDouble() < NEXT_ELEMENT_THRESHOLD ) {
      sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'choosing new element' );
      sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

      // before we change focus to the next item, immediately release all keys that were down on the active element
      this.clearListeners();
      const nextFocusable = PDOMUtils.getRandomFocusable( this.random );
      nextFocusable.focus();
      this.currentElement = nextFocusable;

      sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
    }
  }

  /**
   * @private
   */
  clearListeners() {
    this.keyupListeners.forEach( listener => {
      stepTimer.clearTimeout( listener );
      listener();
    } );
  }

  /**
   * @private
   * @param {HTMLElement} element
   */
  triggerClickEvent( element ) {
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'triggering click' );
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

    element.click();

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
  }

  /**
   * Trigger a keydown/keyup pair. The keyup is triggered with a timeout.
   * @private
   *
   * @param {HTMLElement} element
   * @param {number} keyCode
   */
  triggerKeyDownUpEvents( element, keyCode ) {

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'trigger keydown/up: ' + keyCode );
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

    // TODO: screen readers normally take our keydown events, but may not here, is the descrpency ok?
    this.triggerDOMEvent( KEY_DOWN, element, keyCode );

    const timeout = this.random.nextInt( MAX_MS_KEY_HOLD_DOWN );

    this.keyupListeners.push( stepTimer.setTimeout( () => {
      this.triggerDOMEvent( KEY_UP, element, keyCode );

    }, timeout === MAX_MS_KEY_HOLD_DOWN ? 2000 : timeout ) );

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
  }

  /**
   * Trigger a keydown/keyup pair with a random keyCode
   * @private
   * @param {HTMLElement} element
   */
  triggerRandomKeyDownUpEvents( element ) {

    const randomKeyCode = Math.floor( this.random.nextDouble() * ( MAX_KEY_CODE - MIN_KEY_CODE ) + MIN_KEY_CODE );

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'trigger random keydown/up: ' + randomKeyCode );
    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

    this.triggerKeyDownUpEvents( element, randomKeyCode );

    sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
  }

  /**
   * A random event creater that sends keyboard events. Based on the idea of fuzzMouse, but to test/spam accessibility
   * related keyboard navigation and alternate input implementation.
   *
   * @public
   * TODO: NOTE: Right now this is a very experimental implementation. Tread wearily
   * TODO: @param keyboardPressesPerFocusedItem {number} - basically would be the same as fuzzRate, but handling
   * TODO:     the keydown events for a focused item
   */
  fuzzBoardEvents( fuzzRate ) {

    const a11yPointer = this.display._input.a11yPointer;
    if ( a11yPointer && !a11yPointer.blockTrustedEvents ) {
      a11yPointer.blockTrustedEvents = true;
    }

    for ( let i = 0; i < this.numberOfComponentsTested; i++ ) {

      // find a focus a random element
      this.chooseNextElement();

      for ( let i = 0; i < fuzzRate / this.numberOfComponentsTested; i++ ) {

        sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.KeyboardFuzzer( 'main loop, i=' + i );
        sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.push();

        // get active element, focus might have changed in the last press
        const elementWithFocus = document.activeElement;

        if ( keyboardTestingSchema[ elementWithFocus.tagName.toUpperCase() ] ) {

          const randomNumber = this.random.nextDouble();
          if ( randomNumber < DO_KNOWN_KEYS_THRESHOLD ) {
            const keyCodes = keyboardTestingSchema[ elementWithFocus.tagName ];
            const keyCode = this.random.sample( keyCodes );
            this.triggerKeyDownUpEvents( elementWithFocus, keyCode );
          }
          else if ( randomNumber < CLICK_EVENT_THRESHOLD ) {
            this.triggerClickEvent( elementWithFocus );
          }
          else {
            this.triggerRandomKeyDownUpEvents( elementWithFocus );
          }
        }
        else {
          this.triggerRandomKeyDownUpEvents( elementWithFocus );
        }
        // TODO: What about other types of events, not just keydown/keyup??!?!
        // TODO: what about application role elements

        sceneryLog && sceneryLog.KeyboardFuzzer && sceneryLog.pop();
      }
    }
  }

  /**
   * Taken from example in http://output.jsbin.com/awenaq/3,
   * @param {string} event
   * @param {HTMLElement} element
   * @param {number} keycode
   * @private
   */
  triggerDOMEvent( event, element, keycode ) {
    const eventObj = new KeyboardEvent( event, {
      code: keycode,
      which: keycode,
      shiftKey: globalKeyStateTracker.shiftKeyDown,
      altKey: globalKeyStateTracker.altKeyDown,
      ctrlKey: globalKeyStateTracker.ctrlKeyDown
    } );

    element.dispatchEvent( eventObj );
  }
}

scenery.register( 'KeyboardFuzzer', KeyboardFuzzer );
export default KeyboardFuzzer;<|endoftext|>


<commit_before>var Collection = require('./collection');

var util = require('../util');

function property(obj, name, value) {
  if (value !== null && value !== undefined) {
    util.property.apply(this, arguments);
  }
}

function memoizedProperty(obj, name) {
  if (!obj.constructor.prototype[name]) {
    util.memoizedProperty.apply(this, arguments);
  }
}

function Shape(shape, options, memberName) {
  options = options || {};

  property(this, 'shape', shape.shape);
  property(this, 'api', options.api, false);
  property(this, 'type', shape.type);
  property(this, 'location', shape.location || 'body');
  property(this, 'name', this.name || shape.xmlName || shape.locationName ||
                         memberName);
  property(this, 'isStreaming', shape.streaming || false);
  property(this, 'isComposite', shape.isComposite || false);
  property(this, 'isShape', true, false);

  if (options.documentation) {
    property(this, 'documentation', shape.documentation);
    property(this, 'documentationUrl', shape.documentationUrl);
  }

  if (shape.xmlAttribute) {
    property(this, 'isXmlAttribute', shape.xmlAttribute || false);
  }

  // type conversion and parsing
  property(this, 'defaultValue', null);
  this.toWireFormat = function(value) {
    if (value === null || value === undefined) return '';
    return value;
  };
  this.toType = function(value) { return value; };
}

/**
 * @api private
 */
Shape.normalizedTypes = {
  character: 'string',
  double: 'float',
  long: 'integer',
  short: 'integer',
  biginteger: 'integer',
  bigdecimal: 'float',
  blob: 'binary'
};

/**
 * @api private
 */
Shape.types = {
  'structure': StructureShape,
  'list': ListShape,
  'map': MapShape,
  'boolean': BooleanShape,
  'timestamp': TimestampShape,
  'float': FloatShape,
  'integer': IntegerShape,
  'string': StringShape,
  'base64': Base64Shape,
  'binary': BinaryShape
};

Shape.resolve = function resolve(shape, options) {
  if (shape.shape) {
    var refShape = options.api.shapes[shape.shape];
    if (!refShape) {
      throw new Error('Cannot find shape reference: ' + shape.shape);
    }

    return refShape;
  } else {
    return null;
  }
};

Shape.create = function create(shape, options, memberName) {
  if (shape.isShape) return shape;

  var refShape = Shape.resolve(shape, options);
  if (refShape) {
    var filteredKeys = Object.keys(shape);
    if (!options.documentation) {
      filteredKeys = filteredKeys.filter(function(name) {
        return !name.match(/documentation/);
      });
    }
    if (filteredKeys === ['shape']) { // no inline customizations
      return refShape;
    }

    // create an inline shape with extra members
    var InlineShape = function() {
      refShape.constructor.call(this, shape, options, memberName);
    };
    InlineShape.prototype = refShape;
    return new InlineShape();
  } else {
    // set type if not set
    if (!shape.type) {
      if (shape.members) shape.type = 'structure';
      else if (shape.member) shape.type = 'list';
      else if (shape.key) shape.type = 'map';
      else shape.type = 'string';
    }

    // normalize types
    var origType = shape.type;
    if (Shape.normalizedTypes[shape.type]) {
      shape.type = Shape.normalizedTypes[shape.type];
    }

    if (Shape.types[shape.type]) {
      return new Shape.types[shape.type](shape, options, memberName);
    } else {
      throw new Error('Unrecognized shape type: ' + origType);
    }
  }
};

function CompositeShape(shape) {
  Shape.apply(this, arguments);
  property(this, 'isComposite', true);

  if (shape.flattened) {
    property(this, 'flattened', shape.flattened || false);
  }
}

function StructureShape(shape, options) {
  var requiredMap = null, firstInit = !this.isShape;

  CompositeShape.apply(this, arguments);

  if (firstInit) {
    property(this, 'defaultValue', function() { return {}; });
    property(this, 'members', {});
    property(this, 'memberNames', []);
    property(this, 'required', []);
    property(this, 'isRequired', function() { return false; });
  }

  if (shape.members) {
    property(this, 'members', new Collection(shape.members, options, function(name, member) {
      return Shape.create(member, options, name);
    }));
    memoizedProperty(this, 'memberNames', function() {
      return shape.xmlOrder || Object.keys(shape.members);
    });
  }

  if (shape.required) {
    property(this, 'required', shape.required);
    property(this, 'isRequired', function(name) {
      if (!requiredMap) {
        requiredMap = {};
        for (var i = 0; i < shape.required.length; i++) {
          requiredMap[shape.required[i]] = true;
        }
      }

      return requiredMap[name];
    }, false, true);
  }

  property(this, 'resultWrapper', shape.resultWrapper || null);

  if (shape.payload) {
    property(this, 'payload', shape.payload);
  }

  if (typeof shape.xmlNamespace === 'string') {
    property(this, 'xmlNamespaceUri', shape.xmlNamespace);
  } else if (typeof shape.xmlNamespace === 'object') {
    property(this, 'xmlNamespacePrefix', shape.xmlNamespace.prefix);
    property(this, 'xmlNamespaceUri', shape.xmlNamespace.uri);
  }
}

function ListShape(shape, options) {
  var self = this, firstInit = !this.isShape;
  CompositeShape.apply(this, arguments);

  if (firstInit) {
    property(this, 'defaultValue', function() { return []; });
  }

  if (shape.member) {
    memoizedProperty(this, 'member', function() {
      return Shape.create(shape.member, options);
    });
  }

  if (this.flattened) {
    var oldName = this.name;
    memoizedProperty(this, 'name', function() {
      return self.member.name || oldName;
    });
  }
}

function MapShape(shape, options) {
  var firstInit = !this.isShape;
  CompositeShape.apply(this, arguments);

  if (firstInit) {
    property(this, 'defaultValue', function() { return {}; });
    property(this, 'key', Shape.create({type: 'string'}, options));
    property(this, 'value', Shape.create({type: 'string'}, options));
  }

  if (shape.key) {
    memoizedProperty(this, 'key', function() {
      return Shape.create(shape.key, options);
    });
  }
  if (shape.value) {
    memoizedProperty(this, 'value', function() {
      return Shape.create(shape.value, options);
    });
  }
}

function TimestampShape(shape) {
  var self = this;
  Shape.apply(this, arguments);

  if (this.location === 'header') {
    property(this, 'timestampFormat', 'rfc822');
  } else if (shape.timestampFormat) {
    property(this, 'timestampFormat', shape.timestampFormat);
  } else if (this.api) {
    if (this.api.timestampFormat) {
      property(this, 'timestampFormat', this.api.timestampFormat);
    } else {
      switch (this.api.protocol) {
        case 'json':
        case 'rest-json':
          property(this, 'timestampFormat', 'unixTimestamp');
          break;
        case 'rest-xml':
        case 'query':
          property(this, 'timestampFormat', 'iso8601');
          break;
      }
    }
  }

  this.toType = function(value) {
    if (value === null || value === undefined) return null;
    if (typeof value.toUTCString === 'function') return value;
    return typeof value === 'string' || typeof value === 'number' ?
           util.date.parseTimestamp(value) : null;
  };

  this.toWireFormat = function(value) {
    return util.date.format(value, self.timestampFormat);
  };
}

function StringShape() {
  Shape.apply(this, arguments);

  switch (this.api.protocol) {
    case 'rest-xml':
    case 'query':
      this.toType = function(value) { return value || ''; };
  }
}

function FloatShape() {
  Shape.apply(this, arguments);

  this.toType = function(value) {
    if (value === null || value === undefined) return null;
    return parseFloat(value);
  };
  this.toWireFormat = this.toType;
}

function IntegerShape() {
  Shape.apply(this, arguments);

  this.toType = function(value) {
    if (value === null || value === undefined) return null;
    return parseInt(value, 10);
  };
  this.toWireFormat = this.toType;
}

function BinaryShape() {
  Shape.apply(this, arguments);
  this.toType = util.base64.decode;
  this.toWireFormat = util.base64.encode;
}

function Base64Shape() {
  BinaryShape.apply(this, arguments);
}

function BooleanShape() {
  Shape.apply(this, arguments);

  this.toType = function(value) {
    if (typeof value === 'boolean') return value;
    if (value === null || value === undefined) return null;
    return value === 'true';
  };
}

/**
 * @api private
 */
Shape.shapes = {
  StructureShape: StructureShape,
  ListShape: ListShape,
  MapShape: MapShape,
  StringShape: StringShape,
  BooleanShape: BooleanShape,
  Base64Shape: Base64Shape
};

module.exports = Shape;
<commit_msg>Fix shape tests when no API is defined<commit_after>var Collection = require('./collection');

var util = require('../util');

function property(obj, name, value) {
  if (value !== null && value !== undefined) {
    util.property.apply(this, arguments);
  }
}

function memoizedProperty(obj, name) {
  if (!obj.constructor.prototype[name]) {
    util.memoizedProperty.apply(this, arguments);
  }
}

function Shape(shape, options, memberName) {
  options = options || {};

  property(this, 'shape', shape.shape);
  property(this, 'api', options.api, false);
  property(this, 'type', shape.type);
  property(this, 'location', shape.location || 'body');
  property(this, 'name', this.name || shape.xmlName || shape.locationName ||
                         memberName);
  property(this, 'isStreaming', shape.streaming || false);
  property(this, 'isComposite', shape.isComposite || false);
  property(this, 'isShape', true, false);

  if (options.documentation) {
    property(this, 'documentation', shape.documentation);
    property(this, 'documentationUrl', shape.documentationUrl);
  }

  if (shape.xmlAttribute) {
    property(this, 'isXmlAttribute', shape.xmlAttribute || false);
  }

  // type conversion and parsing
  property(this, 'defaultValue', null);
  this.toWireFormat = function(value) {
    if (value === null || value === undefined) return '';
    return value;
  };
  this.toType = function(value) { return value; };
}

/**
 * @api private
 */
Shape.normalizedTypes = {
  character: 'string',
  double: 'float',
  long: 'integer',
  short: 'integer',
  biginteger: 'integer',
  bigdecimal: 'float',
  blob: 'binary'
};

/**
 * @api private
 */
Shape.types = {
  'structure': StructureShape,
  'list': ListShape,
  'map': MapShape,
  'boolean': BooleanShape,
  'timestamp': TimestampShape,
  'float': FloatShape,
  'integer': IntegerShape,
  'string': StringShape,
  'base64': Base64Shape,
  'binary': BinaryShape
};

Shape.resolve = function resolve(shape, options) {
  if (shape.shape) {
    var refShape = options.api.shapes[shape.shape];
    if (!refShape) {
      throw new Error('Cannot find shape reference: ' + shape.shape);
    }

    return refShape;
  } else {
    return null;
  }
};

Shape.create = function create(shape, options, memberName) {
  if (shape.isShape) return shape;

  var refShape = Shape.resolve(shape, options);
  if (refShape) {
    var filteredKeys = Object.keys(shape);
    if (!options.documentation) {
      filteredKeys = filteredKeys.filter(function(name) {
        return !name.match(/documentation/);
      });
    }
    if (filteredKeys === ['shape']) { // no inline customizations
      return refShape;
    }

    // create an inline shape with extra members
    var InlineShape = function() {
      refShape.constructor.call(this, shape, options, memberName);
    };
    InlineShape.prototype = refShape;
    return new InlineShape();
  } else {
    // set type if not set
    if (!shape.type) {
      if (shape.members) shape.type = 'structure';
      else if (shape.member) shape.type = 'list';
      else if (shape.key) shape.type = 'map';
      else shape.type = 'string';
    }

    // normalize types
    var origType = shape.type;
    if (Shape.normalizedTypes[shape.type]) {
      shape.type = Shape.normalizedTypes[shape.type];
    }

    if (Shape.types[shape.type]) {
      return new Shape.types[shape.type](shape, options, memberName);
    } else {
      throw new Error('Unrecognized shape type: ' + origType);
    }
  }
};

function CompositeShape(shape) {
  Shape.apply(this, arguments);
  property(this, 'isComposite', true);

  if (shape.flattened) {
    property(this, 'flattened', shape.flattened || false);
  }
}

function StructureShape(shape, options) {
  var requiredMap = null, firstInit = !this.isShape;

  CompositeShape.apply(this, arguments);

  if (firstInit) {
    property(this, 'defaultValue', function() { return {}; });
    property(this, 'members', {});
    property(this, 'memberNames', []);
    property(this, 'required', []);
    property(this, 'isRequired', function() { return false; });
  }

  if (shape.members) {
    property(this, 'members', new Collection(shape.members, options, function(name, member) {
      return Shape.create(member, options, name);
    }));
    memoizedProperty(this, 'memberNames', function() {
      return shape.xmlOrder || Object.keys(shape.members);
    });
  }

  if (shape.required) {
    property(this, 'required', shape.required);
    property(this, 'isRequired', function(name) {
      if (!requiredMap) {
        requiredMap = {};
        for (var i = 0; i < shape.required.length; i++) {
          requiredMap[shape.required[i]] = true;
        }
      }

      return requiredMap[name];
    }, false, true);
  }

  property(this, 'resultWrapper', shape.resultWrapper || null);

  if (shape.payload) {
    property(this, 'payload', shape.payload);
  }

  if (typeof shape.xmlNamespace === 'string') {
    property(this, 'xmlNamespaceUri', shape.xmlNamespace);
  } else if (typeof shape.xmlNamespace === 'object') {
    property(this, 'xmlNamespacePrefix', shape.xmlNamespace.prefix);
    property(this, 'xmlNamespaceUri', shape.xmlNamespace.uri);
  }
}

function ListShape(shape, options) {
  var self = this, firstInit = !this.isShape;
  CompositeShape.apply(this, arguments);

  if (firstInit) {
    property(this, 'defaultValue', function() { return []; });
  }

  if (shape.member) {
    memoizedProperty(this, 'member', function() {
      return Shape.create(shape.member, options);
    });
  }

  if (this.flattened) {
    var oldName = this.name;
    memoizedProperty(this, 'name', function() {
      return self.member.name || oldName;
    });
  }
}

function MapShape(shape, options) {
  var firstInit = !this.isShape;
  CompositeShape.apply(this, arguments);

  if (firstInit) {
    property(this, 'defaultValue', function() { return {}; });
    property(this, 'key', Shape.create({type: 'string'}, options));
    property(this, 'value', Shape.create({type: 'string'}, options));
  }

  if (shape.key) {
    memoizedProperty(this, 'key', function() {
      return Shape.create(shape.key, options);
    });
  }
  if (shape.value) {
    memoizedProperty(this, 'value', function() {
      return Shape.create(shape.value, options);
    });
  }
}

function TimestampShape(shape) {
  var self = this;
  Shape.apply(this, arguments);

  if (this.location === 'header') {
    property(this, 'timestampFormat', 'rfc822');
  } else if (shape.timestampFormat) {
    property(this, 'timestampFormat', shape.timestampFormat);
  } else if (this.api) {
    if (this.api.timestampFormat) {
      property(this, 'timestampFormat', this.api.timestampFormat);
    } else {
      switch (this.api.protocol) {
        case 'json':
        case 'rest-json':
          property(this, 'timestampFormat', 'unixTimestamp');
          break;
        case 'rest-xml':
        case 'query':
          property(this, 'timestampFormat', 'iso8601');
          break;
      }
    }
  }

  this.toType = function(value) {
    if (value === null || value === undefined) return null;
    if (typeof value.toUTCString === 'function') return value;
    return typeof value === 'string' || typeof value === 'number' ?
           util.date.parseTimestamp(value) : null;
  };

  this.toWireFormat = function(value) {
    return util.date.format(value, self.timestampFormat);
  };
}

function StringShape() {
  Shape.apply(this, arguments);

  if (this.api) {
    switch (this.api.protocol) {
      case 'rest-xml':
      case 'query':
        this.toType = function(value) { return value || ''; };
    }
  }
}

function FloatShape() {
  Shape.apply(this, arguments);

  this.toType = function(value) {
    if (value === null || value === undefined) return null;
    return parseFloat(value);
  };
  this.toWireFormat = this.toType;
}

function IntegerShape() {
  Shape.apply(this, arguments);

  this.toType = function(value) {
    if (value === null || value === undefined) return null;
    return parseInt(value, 10);
  };
  this.toWireFormat = this.toType;
}

function BinaryShape() {
  Shape.apply(this, arguments);
  this.toType = util.base64.decode;
  this.toWireFormat = util.base64.encode;
}

function Base64Shape() {
  BinaryShape.apply(this, arguments);
}

function BooleanShape() {
  Shape.apply(this, arguments);

  this.toType = function(value) {
    if (typeof value === 'boolean') return value;
    if (value === null || value === undefined) return null;
    return value === 'true';
  };
}

/**
 * @api private
 */
Shape.shapes = {
  StructureShape: StructureShape,
  ListShape: ListShape,
  MapShape: MapShape,
  StringShape: StringShape,
  BooleanShape: BooleanShape,
  Base64Shape: Base64Shape
};

module.exports = Shape;
<|endoftext|>


<commit_before>#!/usr/bin/env node
var args = require('minimist')(process.argv.splice(2), {
  alias: {p: 'port', q: 'quiet', v: 'version'}
})

process.title = 'dat'

// set debug before requiring other modules
if (args.debug) {
  var debug = args.debug
  if (typeof args.debug === 'boolean') debug = '*' // default
  process.env.DEBUG = debug
}

if (args.version) {
  var pkg = require(__dirname + '/package.json')
  console.log(pkg.version)
  process.exit(0)
}

var fs = require('fs')
var singleLineLog = require('single-line-log')
var prettyBytes = require('pretty-bytes')
var chalk = require('chalk')
var xtend = require('xtend')
var dat = require('./index.js')
var usage = require('./usage')

function noop () {}

var cmd = args._[0]
var logger = getLogger()

var LOG_INTERVAL = (args.logspeed ? +args.logspeed : 200)
if (isNaN(LOG_INTERVAL)) LOG_INTERVAL = 200

checkLocation()

function checkLocation () {
  var loc = process.cwd()
  if (!args.path) return runCommand(loc)
  loc = args.path
  fs.exists(loc, function (exists) {
    if (!exists) {
      logger.error('Does not exist:', loc)
      return usage('root.txt')
    }
    runCommand(loc)
  })
}

function runCommand (loc) {
  if (!cmd) return usage('root.txt')

  logger.stdout(chalk.inverse('   Welcome to Dat!   '))
  logger.log('\n') // newline & break

  var db = dat({home: args.home})

  if (cmd === 'link') link(loc, db)
  else if (cmd === 'list') list(loc, db)
  else if (cmd) download(loc, db)
}

function link (loc, db) {
  var dirs = args._.slice(1)
  if (dirs.length === 0) dirs = loc

  logger.stdout(chalk.dim('Making Dat Link...'))
  logger.log('') // newline

  var statsScan = db.fileStats(dirs, function (err, stats) {
    if (err) throw err
    printScanProgress(stats, true)
    clearInterval(scanInterval)
    logger.log('') // newline
    var statsAdd = db.addFiles(dirs, function (err, link) {
      printAddProgress(statsAdd, statsScan)
      clearInterval(addInterval)
      if (err) throw err
      db.joinTcpSwarm({link: link, port: args.port}, function (_err, swarm) {
        // ignore _err
        logger.stdout('') // clear progress lines
        logger.log(
          chalk.green.bold('Your Dat Link: ') +
          chalk.underline.blue('dat://' + swarm.link)
        )
        startProgressLogging({swarm: swarm})
      })
    })

    var addInterval = setInterval(function () {
      printAddProgress(statsAdd, statsScan)
    }, LOG_INTERVAL)
  })

  var scanInterval = setInterval(function () {
    printScanProgress(statsScan)
  }, LOG_INTERVAL)
}

function list (loc, db) {
  var hash = args._[1]
  db.joinTcpSwarm({link: hash, port: args.port}, function (err, swarm) {
    if (err) throw err
    var archive = db.drive.get(swarm.link, loc)
    archive.ready(function (err) {
      if (err) throw err
      archive.createEntryStream().on('data', function (o) {
        logger.log(o)
      })
    })
  })
}

function download (loc, db) {
  // download/share
  var hash = args._[0]
  if (!hash) return usage('root.txt')
  hash = hash.trim().replace('dat://', '').replace('dat:', '')
  if (hash.length !== 64) {
    logger.error('Error: Invalid dat link\n')
    return usage('root.txt')
  }
  var downloadStats = db.download(hash, loc, function (err, swarm) {
    if (err) throw err
    swarm.downloading = false
    swarm.downloadComplete = true
  })
  startProgressLogging(downloadStats)
}

function startProgressLogging (stats) {
  setInterval(function () {
    printSwarmStatus(stats)
  }, LOG_INTERVAL)
  printSwarmStatus(stats)

  process.on('SIGINT', function () {
    process.exit(1)
    setTimeout(function () {
      // sigterm if it is still running
      process.kill(process.pid)
    }, 2000)
  })
}

function printScanProgress (stats, last) {
  var dirCount = stats.directories + 1 // parent folder
  var msg = chalk.bold.blue('Calculating Size: ')
  if (last) msg = chalk.bold.green('Creating New Dat: ')
  msg += chalk.bgBlue.white(
    '[ ' + stats.files + ' files | ' + dirCount + ' folders | ' +
    (stats.size ? ' ' + prettyBytes(stats.size) + ' total' : '') + ' ]'
  )
  logger.stdout(msg)
}

function printAddProgress (statsAdd, statsScan) {
  var msg = ''
  var indent = '  ' // indent for single file info
  var i = 0

  while (statsAdd.files.length > 1) {
    var complete = printFileProgress(statsAdd.files[i])
    if (complete) statsAdd.files.shift()
    else i++
  }

  function printFileProgress (fileStats) {
    var complete = (fileStats.stats.bytesTotal === fileStats.stats.bytesRead)
    if (complete) {
      if (msg === '') {
        // hack to avoid flashing no progress
        msg += chalk.bold.gray(indent + '[' + 100 + '%] ')
        msg += chalk.blue(fileStats.name)
      }
      logger.stdout(chalk.green.dim(indent + '[Done] ') + chalk.dim(fileStats.name))
      logger.log('')
      return true
    }

    var filePercent = 0
    if (fileStats.stats.bytesTotal > 0) {
      filePercent = Math.floor(
        100 * (fileStats.stats.bytesRead / fileStats.stats.bytesTotal)
      )
    }
    // = to overwrite fake 100% msg
    msg = chalk.bold.gray(indent + '[' + ('   ' + filePercent).slice(-3) + '%] ')
    msg += chalk.blue(fileStats.name)
  }

  var totalPer = Math.floor(100 * (statsAdd.totalStats.bytesRead / statsScan.size))
  msg += '\n'
  msg += chalk.bold.red('[' + ('  ' + totalPer).slice(-3) + '% ] ')
  msg += chalk.magenta(
    'Adding Files to Dat: ' +
    statsAdd.totalStats.filesRead + ' of ' + statsScan.files +
    chalk.dim(
      ' (' + prettyBytes(statsAdd.totalStats.bytesRead) +
      ' of ' + prettyBytes(statsScan.size) + ')'
    )
  )
  logger.stdout(msg)
}

function printSwarmStatus (stats) {
  var swarm = stats.swarm
  if (!swarm) return logger.stdout('Finding data sources...\n')
  var totalCount = swarm.blocks
  var downloadCount = stats.files + stats.directories
  var activePeers = xtend({}, swarm.activeInboundPeers, swarm.activeOutboundPeers)
  var activeCount = Object.keys(activePeers).length

  var msg = ''
  var count = '0'
  if (swarm.peerCount > 0) count = activeCount + '/' + swarm.peerCount
  if ((swarm.downloading || swarm.downloadComplete) && stats.downloaded > 0) {
    msg += 'Downloaded ' + downloadCount + '/' + totalCount + ' files' +
      ' (' + prettyBytes(stats.downloadRate()) + '/s, ' + prettyBytes(stats.downloaded) + ' total)\n'
  }
  if (swarm.downloadComplete) msg += 'Download complete, sharing data. Connected to ' + count + ' sources\n'
  else if (swarm.downloading) msg += 'Connected to ' + count + ' sources\n'
  else msg += chalk.green('Sharing data on port ' + chalk.bold(swarm.port) + ', connected to ' + chalk.bold(count) + ' sources\n')
  logger.stdout(msg)
}

function getLogger () {
  if (args.quiet) {
    return {
      stderr: noop,
      stdout: noop,
      log: noop,
      error: noop
    }
  }

  if (args.debug) {
    return {
      stderr: console.error.bind(console),
      stdout: console.log.bind(console),
      log: console.error.bind(console),
      error: console.log.bind(console)
    }
  }

  return {
    stderr: singleLineLog.stderr,
    stdout: singleLineLog.stdout,
    log: console.log.bind(console),
    error: console.error.bind(console)
  }
}
<commit_msg>make sure we get the last file<commit_after>#!/usr/bin/env node
var args = require('minimist')(process.argv.splice(2), {
  alias: {p: 'port', q: 'quiet', v: 'version'}
})

process.title = 'dat'

// set debug before requiring other modules
if (args.debug) {
  var debug = args.debug
  if (typeof args.debug === 'boolean') debug = '*' // default
  process.env.DEBUG = debug
}

if (args.version) {
  var pkg = require(__dirname + '/package.json')
  console.log(pkg.version)
  process.exit(0)
}

var fs = require('fs')
var singleLineLog = require('single-line-log')
var prettyBytes = require('pretty-bytes')
var chalk = require('chalk')
var xtend = require('xtend')
var dat = require('./index.js')
var usage = require('./usage')

function noop () {}

var cmd = args._[0]
var logger = getLogger()

var LOG_INTERVAL = (args.logspeed ? +args.logspeed : 200)
if (isNaN(LOG_INTERVAL)) LOG_INTERVAL = 200

checkLocation()

function checkLocation () {
  var loc = process.cwd()
  if (!args.path) return runCommand(loc)
  loc = args.path
  fs.exists(loc, function (exists) {
    if (!exists) {
      logger.error('Does not exist:', loc)
      return usage('root.txt')
    }
    runCommand(loc)
  })
}

function runCommand (loc) {
  if (!cmd) return usage('root.txt')

  logger.stdout(chalk.inverse('   Welcome to Dat!   '))
  logger.log('\n') // newline & break

  var db = dat({home: args.home})

  if (cmd === 'link') link(loc, db)
  else if (cmd === 'list') list(loc, db)
  else if (cmd) download(loc, db)
}

function link (loc, db) {
  var dirs = args._.slice(1)
  if (dirs.length === 0) dirs = loc

  logger.stdout(chalk.dim('Making Dat Link...'))
  logger.log('') // newline

  var statsScan = db.fileStats(dirs, function (err, stats) {
    if (err) throw err
    printScanProgress(stats, true)
    clearInterval(scanInterval)
    logger.log('') // newline
    var statsAdd = db.addFiles(dirs, function (err, link) {
      printAddProgress(statsAdd, statsScan)
      clearInterval(addInterval)
      if (err) throw err
      db.joinTcpSwarm({link: link, port: args.port}, function (_err, swarm) {
        // ignore _err
        logger.stdout('') // clear progress lines
        logger.log(
          chalk.green.bold('Your Dat Link: ') +
          chalk.underline.blue('dat://' + swarm.link)
        )
        startProgressLogging({swarm: swarm})
      })
    })

    var addInterval = setInterval(function () {
      printAddProgress(statsAdd, statsScan)
    }, LOG_INTERVAL)
  })

  var scanInterval = setInterval(function () {
    printScanProgress(statsScan)
  }, LOG_INTERVAL)
}

function list (loc, db) {
  var hash = args._[1]
  db.joinTcpSwarm({link: hash, port: args.port}, function (err, swarm) {
    if (err) throw err
    var archive = db.drive.get(swarm.link, loc)
    archive.ready(function (err) {
      if (err) throw err
      archive.createEntryStream().on('data', function (o) {
        logger.log(o)
      })
    })
  })
}

function download (loc, db) {
  // download/share
  var hash = args._[0]
  if (!hash) return usage('root.txt')
  hash = hash.trim().replace('dat://', '').replace('dat:', '')
  if (hash.length !== 64) {
    logger.error('Error: Invalid dat link\n')
    return usage('root.txt')
  }
  var downloadStats = db.download(hash, loc, function (err, swarm) {
    if (err) throw err
    swarm.downloading = false
    swarm.downloadComplete = true
  })
  startProgressLogging(downloadStats)
}

function startProgressLogging (stats) {
  setInterval(function () {
    printSwarmStatus(stats)
  }, LOG_INTERVAL)
  printSwarmStatus(stats)

  process.on('SIGINT', function () {
    process.exit(1)
    setTimeout(function () {
      // sigterm if it is still running
      process.kill(process.pid)
    }, 2000)
  })
}

function printScanProgress (stats, last) {
  var dirCount = stats.directories + 1 // parent folder
  var msg = chalk.bold.blue('Calculating Size: ')
  if (last) msg = chalk.bold.green('Creating New Dat: ')
  msg += chalk.bgBlue.white(
    '[ ' + stats.files + ' files | ' + dirCount + ' folders | ' +
    (stats.size ? ' ' + prettyBytes(stats.size) + ' total' : '') + ' ]'
  )
  logger.stdout(msg)
}

function printAddProgress (statsAdd, statsScan) {
  var msg = ''
  var indent = '  ' // indent for single file info
  var i = 0

  while (statsAdd.files.length > 1) {
    var complete = printFileProgress(statsAdd.files[i])
    if (complete) statsAdd.files.shift()
    else i++
  }

  if (statsAdd.files.length) {
    printFileProgress(statsAdd.files.shift())
  }

  function printFileProgress (fileStats) {
    var complete = (fileStats.stats.bytesTotal === fileStats.stats.bytesRead)
    if (complete) {
      if (msg === '') {
        // hack to avoid flashing no progress
        msg += chalk.bold.gray(indent + '[' + 100 + '%] ')
        msg += chalk.blue(fileStats.name)
      }
      logger.stdout(chalk.green.dim(indent + '[Done] ') + chalk.dim(fileStats.name))
      logger.log('')
      return true
    }

    var filePercent = 0
    if (fileStats.stats.bytesTotal > 0) {
      filePercent = Math.floor(
        100 * (fileStats.stats.bytesRead / fileStats.stats.bytesTotal)
      )
    }
    // = to overwrite fake 100% msg
    msg = chalk.bold.gray(indent + '[' + ('   ' + filePercent).slice(-3) + '%] ')
    msg += chalk.blue(fileStats.name)
  }

  var totalPer = Math.floor(100 * (statsAdd.totalStats.bytesRead / statsScan.size))
  msg += '\n'
  msg += chalk.bold.red('[' + ('  ' + totalPer).slice(-3) + '% ] ')
  msg += chalk.magenta(
    'Adding Files to Dat: ' +
    statsAdd.totalStats.filesRead + ' of ' + statsScan.files +
    chalk.dim(
      ' (' + prettyBytes(statsAdd.totalStats.bytesRead) +
      ' of ' + prettyBytes(statsScan.size) + ')'
    )
  )
  logger.stdout(msg)
}

function printSwarmStatus (stats) {
  var swarm = stats.swarm
  if (!swarm) return logger.stdout('Finding data sources...\n')
  var totalCount = swarm.blocks
  var downloadCount = stats.files + stats.directories
  var activePeers = xtend({}, swarm.activeInboundPeers, swarm.activeOutboundPeers)
  var activeCount = Object.keys(activePeers).length

  var msg = ''
  var count = '0'
  if (swarm.peerCount > 0) count = activeCount + '/' + swarm.peerCount
  if ((swarm.downloading || swarm.downloadComplete) && stats.downloaded > 0) {
    msg += 'Downloaded ' + downloadCount + '/' + totalCount + ' files' +
      ' (' + prettyBytes(stats.downloadRate()) + '/s, ' + prettyBytes(stats.downloaded) + ' total)\n'
  }
  if (swarm.downloadComplete) msg += 'Download complete, sharing data. Connected to ' + count + ' sources\n'
  else if (swarm.downloading) msg += 'Connected to ' + count + ' sources\n'
  else msg += chalk.green('Sharing data on port ' + chalk.bold(swarm.port) + ', connected to ' + chalk.bold(count) + ' sources\n')
  logger.stdout(msg)
}

function getLogger () {
  if (args.quiet) {
    return {
      stderr: noop,
      stdout: noop,
      log: noop,
      error: noop
    }
  }

  if (args.debug) {
    return {
      stderr: console.error.bind(console),
      stdout: console.log.bind(console),
      log: console.error.bind(console),
      error: console.log.bind(console)
    }
  }

  return {
    stderr: singleLineLog.stderr,
    stdout: singleLineLog.stdout,
    log: console.log.bind(console),
    error: console.error.bind(console)
  }
}
<|endoftext|>


<commit_before>var fs = require('fs');
var config = require('../config');
var rss = require('../utils/rss');
var client = require('./discord');

// FEED class
module.exports = function(params) {

  var that = this;

  // -------------------- INIT -------------------- //
  this.init = function(params) {

    // try to get channel by id
    that.setChannel(params.channel);

    this.title =        params.title;
    this.description =  params.description;
    this.url =          params.url;
    this.tags =         params.tags;

    this.worker = null;
    this.startWorker();
  };

  // -------------------- TO STRING -------------------- //
  this.toString = function() {
    var str = this.title + ' - ' + this.url;
    if(this.tags.length > 0) {
      str += ' [' + this.tags.toString() + ']';
    }
    str += ' > #' + this.channel.name;
    return str;
  };

  // -------------------- TO JSON -------------------- //
  this.toJson = function() {
    return {
      channel:      this.channel.id,
      title:        this.title,
      description:  this.description,
      url:          this.url,
      tags:         this.tags,
    };
  };

  // -------------------- SORT BY DATE -------------------- //
  this.sortByDate = function(a, b) {
    var dateA = new Date(a.date);
    var dateB = new Date(b.date);
    return dateB - dateA;
  };

  // -------------------- SET CHANNEL -------------------- //
  this.setChannel = function(value) {
    var channel = client.Channels.getBy('id', value);
    if(channel && (!that.channel || that.channel.guild.id === channel.guild.id)) {
      that.channel = channel;
      if(that.worker) {
        that.restartWorker();
      }
      return channel;
    }
    else {
      console.log('SET CHANNEL - channel "' + value + '" not found');
    }
  };

  // -------------------- ADD TAGS -------------------- //
  this.addTags = function(tags) {
    if(tags) {
      for(var i=0; i<tags.length; i++) {
        this.tags.push(tags[i]);
      }
      this.restartWorker();
    }
  };

  // -------------------- REMOVE TAGS -------------------- //
  this.removeTags = function(tags) {
    if(tags) {
      for(var i=0; i<tags.length; i++) {
        var tag = tags[i];
        var tagIndex = this.tags.indexOf(tag);
        if(tagIndex !== -1) {
          this.tags.splice(tagIndex, 1);
        }
      }
      this.restartWorker();
    }
  };

  // -------------------- START WORKER -------------------- //
  this.startWorker = function() {
    this.getDate();
    this.worker = setInterval(this.getArticles, config.timer * 1000);
  };

  // -------------------- STOP WORKER -------------------- //
  this.stopWorker = function() {
    clearInterval(this.worker);
  };

  // -------------------- RESTART WORKER -------------------- //
  this.restartWorker = function() {
    this.stopWorker();
    this.startWorker();
  };

  // -------------------- CHECK TAGS -------------------- //
  this.checkTags = function(article) {
    if(that.tags.length > 0) {
      var matches = 0;
      // check if article matches all tags
      for(var i=0; i<that.tags.length; i++) {
        var tag = that.tags[i].toLowerCase();
        var title = article.title.toLowerCase();
        var descr = article.description.toLowerCase();
        var summary = article.summary.toLowerCase();
        if(
          title.indexOf(tag) !== -1 ||
          descr.indexOf(tag) !== -1 ||
          summary.indexOf(tag) !== -1
        ) {
          matches++;
        }
      }
      return (matches === that.tags.length);
    }
    else {
      return true;
    }
  };

  // -------------------- GET ARTICLES -------------------- //
  this.getArticles = function() {
    rss.get(that.url)
    .then(function(data) {

      // sort articles
      data.sort(that.sortByDate);

      // parse articles
      for(var i=data.length-1; i>=0; i--) {

        var article = data[i];
        var date = new Date(article.date);
        var link = article.origlink || article.link;

        // if new article
        if(that.minDate && date.getTime()>that.minDate) {
          // if tags match
          if(that.checkTags(article)) {
            var msg = 'I found an article in "' + that.title + '"';
            if(that.tags.length > 0) {
              msg += ' matching the tags [' + that.tags.toString() + ']'
            }
            msg += ':\n' + link;
            that.channel.sendMessage(msg);
          }
        }

        // get date from latest article
        if(i===0) {
          that.minDate = date.getTime();
        }
      }
    })
    .catch(function(e) {
      console.log('GET ARTICLES - ' + e);
    });
  };

  // -------------------- GET DATE -------------------- //
  this.getDate = function() {
    rss.get(that.url)
    .then(function(data) {

      // sort articles
      data.sort(that.sortByDate);

      if(data[0]) {
        var date = new Date(data[0].date);
        that.minDate = date.getTime();
      }
      else {
        that.minDate = Date.now();
      }
    })
    .catch(function(e) {
      console.log('GET DATE - ' + e);
    });
  };

  this.init(params);
};<commit_msg>check if already posted<commit_after>var fs = require('fs');
var config = require('../config');
var rss = require('../utils/rss');
var client = require('./discord');

// FEED class
module.exports = function(params) {

  var that = this;

  // -------------------- INIT -------------------- //
  this.init = function(params) {

    // try to get channel by id
    that.setChannel(params.channel);

    this.title =        params.title;
    this.description =  params.description;
    this.url =          params.url;
    this.tags =         params.tags;

    this.posted = [];

    this.worker = null;
    this.startWorker();
  };

  // -------------------- TO STRING -------------------- //
  this.toString = function() {
    var str = this.title + ' - ' + this.url;
    if(this.tags.length > 0) {
      str += ' [' + this.tags.toString() + ']';
    }
    str += ' > #' + this.channel.name;
    return str;
  };

  // -------------------- TO JSON -------------------- //
  this.toJson = function() {
    return {
      channel:      this.channel.id,
      title:        this.title,
      description:  this.description,
      url:          this.url,
      tags:         this.tags,
    };
  };

  // -------------------- SORT BY DATE -------------------- //
  this.sortByDate = function(a, b) {
    var dateA = new Date(a.date);
    var dateB = new Date(b.date);
    return dateB - dateA;
  };

  // -------------------- SET CHANNEL -------------------- //
  this.setChannel = function(value) {
    var channel = client.Channels.getBy('id', value);
    if(channel && (!that.channel || that.channel.guild.id === channel.guild.id)) {
      that.channel = channel;
      if(that.worker) {
        that.restartWorker();
      }
      return channel;
    }
    else {
      console.log('SET CHANNEL - channel "' + value + '" not found');
    }
  };

  // -------------------- ADD TAGS -------------------- //
  this.addTags = function(tags) {
    if(tags) {
      for(var i=0; i<tags.length; i++) {
        this.tags.push(tags[i]);
      }
      this.restartWorker();
    }
  };

  // -------------------- REMOVE TAGS -------------------- //
  this.removeTags = function(tags) {
    if(tags) {
      for(var i=0; i<tags.length; i++) {
        var tag = tags[i];
        var tagIndex = this.tags.indexOf(tag);
        if(tagIndex !== -1) {
          this.tags.splice(tagIndex, 1);
        }
      }
      this.restartWorker();
    }
  };

  // -------------------- START WORKER -------------------- //
  this.startWorker = function() {
    this.getDate();
    this.worker = setInterval(this.getArticles, config.timer * 1000);
  };

  // -------------------- STOP WORKER -------------------- //
  this.stopWorker = function() {
    clearInterval(this.worker);
  };

  // -------------------- RESTART WORKER -------------------- //
  this.restartWorker = function() {
    this.stopWorker();
    this.startWorker();
  };

  // -------------------- CHECK TAGS -------------------- //
  this.checkTags = function(article) {
    if(that.tags.length > 0) {
      var matches = 0;
      // check if article matches all tags
      for(var i=0; i<that.tags.length; i++) {
        var tag = that.tags[i].toLowerCase();
        var title = article.title.toLowerCase();
        var descr = article.description.toLowerCase();
        var summary = article.summary.toLowerCase();
        if(
          title.indexOf(tag) !== -1 ||
          descr.indexOf(tag) !== -1 ||
          summary.indexOf(tag) !== -1
        ) {
          matches++;
        }
      }
      return (matches === that.tags.length);
    }
    else {
      return true;
    }
  };

  // -------------------- GET ARTICLES -------------------- //
  this.getArticles = function() {
    rss.get(that.url)
    .then(function(data) {

      // sort articles
      data.sort(that.sortByDate);

      // parse articles
      for(var i=data.length-1; i>=0; i--) {

        var article = data[i];
        var date = new Date(article.date);
        var link = article.origlink || article.link;

        // check article
        if(that.minDate && date.getTime()>that.minDate
          && that.checkTags(article)
          && link && that.posted.indexOf(link)===-1
          ) {

          // add to posted list
          that.posted.push(link);
          if(that.posted.length > 100) {
            that.posted.shift();
          }

          // post msg
          var msg = 'I found an article in "' + that.title + '"';
          if(that.tags.length > 0) {
            msg += ' matching the tags [' + that.tags.toString() + ']'
          }
          msg += ':\n' + link;
          that.channel.sendMessage(msg);
        }

        // get date from latest article
        if(i===0) {
          that.minDate = date.getTime();
        }
      }
    })
    .catch(function(e) {
      console.log('GET ARTICLES - ' + e);
    });
  };

  // -------------------- GET DATE -------------------- //
  this.getDate = function() {
    rss.get(that.url)
    .then(function(data) {

      // sort articles
      data.sort(that.sortByDate);

      if(data[0]) {
        var date = new Date(data[0].date);
        that.minDate = date.getTime();
      }
      else {
        that.minDate = Date.now();
      }
    })
    .catch(function(e) {
      console.log('GET DATE - ' + e);
    });
  };

  this.init(params);
};<|endoftext|>


<commit_before>#!/usr/bin/env node
'use strict';
var _ = require('lodash');
var eachAsync = require('each-async');
var multiline = require('multiline');
var nopt = require('nopt');
var updateNotifier = require('update-notifier');
var stdin = require('get-stdin');
var subarg = require('subarg');
var sudoBlock = require('sudo-block');
var logSymbols = require('log-symbols');
var pkg = require('./package.json');
var Pageres = require('./');

var options = nopt({
	help: Boolean,
	version: Boolean,
	crop: Boolean,
	delay: Number,
	cookie: Array
}, {
	h: '--help',
	v: '--version',
	c: '--crop',
	d: '--delay'
});

var args = subarg(options.argv.remain)._;

function showHelp() {
	console.log(multiline.stripIndent(function () {/*
		Get screenshots of websites in different resolutions.

		Specify urls and screen resolutions as arguments. Order doesn't matter. Group arguments with [ ]
		Screenshots are saved in the current directory.

		Usage
		  pageres <url> <resolution>
		  pageres [ <url> <resolution> ] [ <url> <resolution> ]
		  pageres [ <url> <resolution> ... ] < <file>
		  cat <file> | pageres [ <url> <resolution> ... ]

		Example
		  pageres todomvc.com yeoman.io 1366x768 1600x900
		  pageres [ yeoman.io 1366x768 1600x900 ] [ todomvc.com 1024x768 480x320 ]
		  pageres --delay 3 1366x768 < urls.txt
		  pageres unicorn.html 1366x768
		  cat screen-resolutions.txt | pageres todomvc.com yeoman.io

		Options
		  -d, --delay <seconds>  Delay capturing the screenshot
		  -c, --crop             Crop to the set height
		  --cookie <cookie>      Browser cookie, can be set multiple times

		<url> can also be a local file path.

		You can also pipe in a newline separated list of urls and screen resolutions which will get merged with the arguments. If no screen resolutions are specified it will fall back to the ten most popular ones according to w3counter.
	*/}));
}

function generate(args, opts) {
	var pageres = new Pageres(opts)
		.dest(process.cwd());

	args.forEach(function (arg) {
		pageres.src(arg.url, arg.sizes);
	});

	pageres.run(function (err) {
		if (err) {
			if (err instanceof Error) {
				throw err;
			} else {
				console.error(err);
				process.exit(1);
			}
		}

		pageres._logSuccessMessage();
	});
}

function get(args, options, cb) {
	var ret = [];

	eachAsync(args, function (arg, i, next) {
		if (arg.url.length === 0) {
			console.error(logSymbols.warning, 'Specify a url\n');
			showHelp();
			return;
		}

		if (arg.sizes.length === 0 && arg.keywords.length === 0) {
			console.log('No sizes specified. Falling back to the ten most popular screen resolutions according to w3counter.');
		}

		if (arg.keywords.length > 0) {
			arg.sizes = arg.sizes.concat(arg.keywords);
		}

		arg.url.forEach(function (el) {
			ret.push({ url: el, sizes: arg.sizes });
		});

		next();
	}, function (err) {
		if (err) {
			cb(err);
		}

		cb(null, ret);
	});
}

function parse(args) {
	var ret = [];

	args.forEach(function (arg) {
		arg = arg._;

		var url = _.uniq(arg.filter(/./.test, /\.|localhost/));
		var sizes = _.uniq(arg.filter(/./.test, /^\d{3,4}x\d{3,4}$/i));
		var keywords = _.difference(arg, url.concat(sizes));

		ret.push({ url: url, sizes: sizes, keywords: keywords });
	});

	return ret;
}

function init(args, options) {
	if (options.help || args.length === 0) {
		return showHelp();
	}

	if (options.version) {
		return console.log(require('./package').version);
	}

	if (args.some(function (arr) { return arr._ === undefined; })) {
		args = [{ _: args }];
	}

	get(parse(args), options, function (err, items) {
		if (err) {
			throw err;
		}

		// plural makes more sense for a programmatic option
		options.cookies = options.cookie;
		delete options.cookie;

		generate(items, options);
	});
}

sudoBlock();

updateNotifier({
	packageName: pkg.name,
	packageVersion: pkg.version
}).notify();

if (process.stdin.isTTY) {
	init(args, options);
} else {
	stdin(function (data) {
		[].push.apply(args, data.trim().split('\n'));
		init(args, options);
	});
}
<commit_msg>tweak cli help output<commit_after>#!/usr/bin/env node
'use strict';
var _ = require('lodash');
var eachAsync = require('each-async');
var multiline = require('multiline');
var nopt = require('nopt');
var updateNotifier = require('update-notifier');
var stdin = require('get-stdin');
var subarg = require('subarg');
var sudoBlock = require('sudo-block');
var logSymbols = require('log-symbols');
var pkg = require('./package.json');
var Pageres = require('./');

var options = nopt({
	help: Boolean,
	version: Boolean,
	crop: Boolean,
	delay: Number,
	cookie: Array
}, {
	h: '--help',
	v: '--version',
	c: '--crop',
	d: '--delay'
});

var args = subarg(options.argv.remain)._;

function showHelp() {
  console.log(multiline(function () {/*

  Get screenshots of websites in different resolutions.

  Specify urls and screen resolutions as arguments. Order doesn't matter. Group arguments with [ ]
  Screenshots are saved in the current directory.

  Usage
    pageres <url> <resolution>
    pageres [ <url> <resolution> ] [ <url> <resolution> ]
    pageres [ <url> <resolution> ... ] < <file>
    cat <file> | pageres [ <url> <resolution> ... ]

  Example
    pageres todomvc.com yeoman.io 1366x768 1600x900
    pageres [ yeoman.io 1366x768 1600x900 ] [ todomvc.com 1024x768 480x320 ]
    pageres --delay 3 1366x768 < urls.txt
    pageres unicorn.html 1366x768
    cat screen-resolutions.txt | pageres todomvc.com yeoman.io

  Options
    -d, --delay <seconds>    Delay capturing the screenshot
    -c, --crop               Crop to the set height
    --cookie <cookie>        Browser cookie, can be set multiple times

  <url> can also be a local file path.

  You can also pipe in a newline separated list of urls and screen resolutions which will get merged with the arguments.
  */}));
}

function generate(args, opts) {
	var pageres = new Pageres(opts)
		.dest(process.cwd());

	args.forEach(function (arg) {
		pageres.src(arg.url, arg.sizes);
	});

	pageres.run(function (err) {
		if (err) {
			if (err instanceof Error) {
				throw err;
			} else {
				console.error(err);
				process.exit(1);
			}
		}

		pageres._logSuccessMessage();
	});
}

function get(args, options, cb) {
	var ret = [];

	eachAsync(args, function (arg, i, next) {
		if (arg.url.length === 0) {
			console.error(logSymbols.warning, 'Specify a url\n');
			showHelp();
			return;
		}

		if (arg.sizes.length === 0 && arg.keywords.length === 0) {
			console.log('No sizes specified. Falling back to the ten most popular screen resolutions according to w3counter.');
		}

		if (arg.keywords.length > 0) {
			arg.sizes = arg.sizes.concat(arg.keywords);
		}

		arg.url.forEach(function (el) {
			ret.push({ url: el, sizes: arg.sizes });
		});

		next();
	}, function (err) {
		if (err) {
			cb(err);
		}

		cb(null, ret);
	});
}

function parse(args) {
	var ret = [];

	args.forEach(function (arg) {
		arg = arg._;

		var url = _.uniq(arg.filter(/./.test, /\.|localhost/));
		var sizes = _.uniq(arg.filter(/./.test, /^\d{3,4}x\d{3,4}$/i));
		var keywords = _.difference(arg, url.concat(sizes));

		ret.push({ url: url, sizes: sizes, keywords: keywords });
	});

	return ret;
}

function init(args, options) {
	if (options.help || args.length === 0) {
		return showHelp();
	}

	if (options.version) {
		return console.log(require('./package').version);
	}

	if (args.some(function (arr) { return arr._ === undefined; })) {
		args = [{ _: args }];
	}

	get(parse(args), options, function (err, items) {
		if (err) {
			throw err;
		}

		// plural makes more sense for a programmatic option
		options.cookies = options.cookie;
		delete options.cookie;

		generate(items, options);
	});
}

sudoBlock();

updateNotifier({
	packageName: pkg.name,
	packageVersion: pkg.version
}).notify();

if (process.stdin.isTTY) {
	init(args, options);
} else {
	stdin(function (data) {
		[].push.apply(args, data.trim().split('\n'));
		init(args, options);
	});
}
<|endoftext|>


<commit_before>// Copyright 2015 Endless Mobile, Inc.

const Cairo = imports.gi.cairo;
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const Lang = imports.lang;

const Card = imports.app.interfaces.card;
const MarginButton = imports.app.widgets.marginButton;
const Module = imports.app.interfaces.module;
const Utils = imports.app.utils;

/**
 * Class: SearchResultCard
 *
 * A thumbnail card for the new reader app
 */
const SearchResultCard = new Lang.Class({
    Name: 'SearchResultCard',
    GTypeName: 'EknSearchResultCard',
    Extends: MarginButton.MarginButton,
    Implements: [ Module.Module, Card.Card ],

    Properties: {
        'factory': GObject.ParamSpec.override('factory', Module.Module),
        'factory-name': GObject.ParamSpec.override('factory-name', Module.Module),
        'model': GObject.ParamSpec.override('model', Card.Card),
        'title-capitalization': GObject.ParamSpec.override('title-capitalization',
            Card.Card),
        'highlight-string': GObject.ParamSpec.override('highlight-string', Card.Card),
    },

    Template: 'resource:///com/endlessm/knowledge/data/widgets/searchResultCard.ui',
    InternalChildren: [ 'thumbnail-frame', 'content-frame', 'title-label', 'synopsis-label'],

    _init: function (props={}) {
        this.parent(props);

        this.set_title_label_from_model(this._title_label);
        this.set_thumbnail_frame_from_model(this._thumbnail_frame);
        this.set_label_or_hide(this._synopsis_label, this.model.synopsis);
        this.set_label_with_highlight(this._synopsis_label, this.model.synopsis);
        this.set_size_request(Card.MinSize.E, Card.MinSize.A);

        Utils.set_hand_cursor_on_widget(this);
    },

    _TEXT_SIZE_RATIO: 0.64,

    vfunc_size_allocate: function (alloc) {
        let text_width = alloc.width * this._TEXT_SIZE_RATIO;
        let image_width = alloc.height;
        let total_width = text_width + image_width;
        let margin = alloc.width - total_width;

        let card_alloc = new Cairo.RectangleInt({
            x: alloc.x + (margin / 2),
            y: alloc.y,
            width: total_width,
            height: alloc.height,
        });

        this.parent(card_alloc);

        let image_alloc = new Cairo.RectangleInt({
            x: alloc.x + (margin / 2),
            y: alloc.y,
            width: image_width,
            height: alloc.height,
        });
        this._thumbnail_frame.size_allocate(image_alloc);

        let text_alloc = new Cairo.RectangleInt({
            x: alloc.x + (margin / 2) + image_width,
            y: alloc.y,
            width: text_width,
            height: alloc.height,
        });
        this._content_frame.size_allocate(text_alloc);

        this.update_card_sizing_classes(total_width, alloc.width);
    },

    vfunc_draw: function (cr) {
        this.parent(cr);
        Utils.render_border_with_arrow(this, cr);
        cr.$dispose();  // workaround bug for not freeing cairo context
        return Gdk.EVENT_PROPAGATE;
    },
});
<commit_msg>Make image dimension ratios match the designs<commit_after>// Copyright 2015 Endless Mobile, Inc.

const Cairo = imports.gi.cairo;
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const Lang = imports.lang;

const Card = imports.app.interfaces.card;
const MarginButton = imports.app.widgets.marginButton;
const Module = imports.app.interfaces.module;
const Utils = imports.app.utils;

/**
 * Class: SearchResultCard
 *
 * A thumbnail card for the new reader app
 */
const SearchResultCard = new Lang.Class({
    Name: 'SearchResultCard',
    GTypeName: 'EknSearchResultCard',
    Extends: MarginButton.MarginButton,
    Implements: [ Module.Module, Card.Card ],

    Properties: {
        'factory': GObject.ParamSpec.override('factory', Module.Module),
        'factory-name': GObject.ParamSpec.override('factory-name', Module.Module),
        'model': GObject.ParamSpec.override('model', Card.Card),
        'title-capitalization': GObject.ParamSpec.override('title-capitalization',
            Card.Card),
        'highlight-string': GObject.ParamSpec.override('highlight-string', Card.Card),
    },

    Template: 'resource:///com/endlessm/knowledge/data/widgets/searchResultCard.ui',
    InternalChildren: [ 'thumbnail-frame', 'content-frame', 'title-label', 'synopsis-label'],

    _init: function (props={}) {
        this.parent(props);

        this.set_title_label_from_model(this._title_label);
        this.set_thumbnail_frame_from_model(this._thumbnail_frame);
        this.set_label_or_hide(this._synopsis_label, this.model.synopsis);
        this.set_label_with_highlight(this._synopsis_label, this.model.synopsis);
        this.set_size_request(Card.MinSize.E, Card.MinSize.A);

        Utils.set_hand_cursor_on_widget(this);
    },

    _TEXT_SIZE_RATIO: 0.64,
    _IMAGE_WIDTH_RATIO: 1.5,

    vfunc_size_allocate: function (alloc) {
        let text_width = alloc.width * this._TEXT_SIZE_RATIO;
        let image_width = alloc.height * this._IMAGE_WIDTH_RATIO;
        let total_width = text_width + image_width;
        let margin = alloc.width - total_width;

        let card_alloc = new Cairo.RectangleInt({
            x: alloc.x + (margin / 2),
            y: alloc.y,
            width: total_width,
            height: alloc.height,
        });

        this.parent(card_alloc);

        let image_alloc = new Cairo.RectangleInt({
            x: alloc.x + (margin / 2),
            y: alloc.y,
            width: image_width,
            height: alloc.height,
        });
        this._thumbnail_frame.size_allocate(image_alloc);

        let text_alloc = new Cairo.RectangleInt({
            x: alloc.x + (margin / 2) + image_width,
            y: alloc.y,
            width: text_width,
            height: alloc.height,
        });
        this._content_frame.size_allocate(text_alloc);

        this.update_card_sizing_classes(total_width, alloc.width);
    },

    vfunc_draw: function (cr) {
        this.parent(cr);
        Utils.render_border_with_arrow(this, cr);
        cr.$dispose();  // workaround bug for not freeing cairo context
        return Gdk.EVENT_PROPAGATE;
    },
});
<|endoftext|>


<commit_before>var app = require('app');
var BrowserWindow = require('browser-window');

var mainWindow = null;

function quit(err) {
  if(err) console.log(err);
  mainWindow = null;
  app.quit();
}

app.on('window-all-closed', function() {
  if (process.platform != 'darwin')
    app.quit();
});

app.on('ready', function() {

  mainWindow = new BrowserWindow({width: 1032, height: 600});

  var server = require('./server');
  server(false, function(data) {

    if(data.err) return quit(data.err);
    mainWindow.loadUrl(data.url);
    mainWindow.on('closed', function() {
      mainWindow = null;
    });
    // mainWindow.openDevTools({detach:true});

  });

});
<commit_msg>update electron app width<commit_after>var app = require('app');
var BrowserWindow = require('browser-window');

var mainWindow = null;

function quit(err) {
  if(err) console.log(err);
  mainWindow = null;
  app.quit();
}

app.on('window-all-closed', function() {
  if (process.platform != 'darwin')
    app.quit();
});

app.on('ready', function() {

  mainWindow = new BrowserWindow({width: 1040, height: 600});

  var server = require('./server');
  server(false, function(data) {

    if(data.err) return quit(data.err);
    mainWindow.loadUrl(data.url);
    mainWindow.on('closed', function() {
      mainWindow = null;
    });
    // mainWindow.openDevTools({detach:true});

  });

});
<|endoftext|>


<commit_before>import { setPage, PROFILE } from '../page/actions'
import { addMessage } from '../messages/actions'

export function loadProfile(id) {
  const url = id ? `/api/profile/${id}` : '/api/my/profile'
  return dispatch => fetch(url, { credentials: 'include' })
  .then((response) => {
    switch (response.status) {
      case 200:
        response.json().then(profile =>
          dispatch(setPage(PROFILE, profile))
        )
        break
      case 404:
        dispatch(addMessage(`User ${id}'s profile was not found.`))
        break
      case 500:
      default: // All other error messages
        dispatch(addMessage(`Error encountered while loading profile for User ${id}.`))
    }
  })
}
<commit_msg>Remove redux file<commit_after><|endoftext|>