<commit_before>/**
 * Filters
 * Gold Server - http://gold.psim.us/
 *
 * Manually sets filters for chatting and names.
 * In this, we also handle proxy connections with a blacklist feature.
 * Credits: jd, panpawn
 *
 * @license MIT license
 */
'use strict';

const fs = require('fs');

let adWhitelist = Config.adWhitelist ? Config.adWhitelist : [];
let adRegex = new RegExp("(play.pokemonshowdown.com\\/~~)(?!(" + adWhitelist.join('|') + "))", "g");

let bannedMessages = Config.bannedMessages ? Config.bannedMessages : [];

let watchPhrases = Config.watchPhrases ? Config.watchPhrases : [];
let watchUsers = Config.watchUsers ? Config.watchUsers : [];

let proxyWhitelist = Config.proxyWhitelist || false;

/*********************
 * Chatfilter Magic *
 * ******************/

exports.chatfilter = function (message, user, room, connection, targetUser) {
	user.lastActiveTime = Date.now();
	if (!room && !Users(targetUser)) targetUser = {name: 'unknown user'};

	// watch phrases and watch users
	let watchRoom = Rooms('watchroom') ? Rooms('watchroom') : false;
	if (watchRoom) {
		let watchWords = watchPhrases.filter(phrase => { return ~toId(message).indexOf(phrase); }).length;
		let watchUserslist = watchUsers.filter(name => { return ~user.userid.indexOf(name); }).length;
		if (!user.can('hotpatch') && watchWords >= 1 || watchUserslist >= 1) {
			watchRoom.add('|c|' + user.getIdentity() + '| __(' + (room ? "To " + room.id : "Private message to " + targetUser.name) + ")__ " + message).update();
		}
	}

	// global banned messages
	for (let x in bannedMessages) {
		if (message.toLowerCase().indexOf(bannedMessages[x]) > -1 && bannedMessages[x] !== '' && message.substr(0, 1) !== '/') {
			if (user.locked) return false;
			Punishments.lock(user, Date.now() + 7 * 24 * 60 * 60 * 1000, null, "Said a banned word: " + bannedMessages[x]);
			user.popup('You have been automatically locked for sending a message containing a banned word.');
			Monitor.log('[PornMonitor] ' + user.name + ' __(' + (room ? 'In ' + room.id : 'Private message to ' + targetUser.name) + ')__ was automatically locked (and shadow banned) for trying to say "' + message + '"');
			fs.appendFile('logs/modlog/modlog_staff.txt', '[' + (new Date().toJSON()) + '] (staff) ' + user.name + ' was locked from talking by the Server (' +
			bannedMessages[x] + ') (' + connection.ip + ')\n');
			Gold.pmUpperStaff(user.name + ' has been automatically locked for sending a message containing a banned word' +
			(room ? ". **Room:**" + room.id : " in a private message to " + targetUser.name + ".") + ' **Message:** ' + message, '~Server');
			Users.ShadowBan.addUser(user);
		}
	}

	// advertising
	let pre_matches = (message.match(/psim|psim.us|psim us|psm.us|psm us/g) || []).length;
	let final_check = (pre_matches >= 1 ? adWhitelist.filter(server => { return ~message.indexOf(server); }).length : 0);
	if (!user.can('hotpatch') && (pre_matches >= 1 && final_check === 0 || pre_matches >= 2 && final_check >= 1 || message.match(adRegex))) {
		if (user.locked) return false;
		if (!Users.ShadowBan.checkBanned(user)) {
			Users.ShadowBan.addUser(user);
			fs.appendFile('logs/modlog/modlog_staff.txt', '[' + (new Date().toJSON()) + '] (staff) ' + user.name + ' was shadow banned by the Server. (Advertising) (' + connection.ip + ')\n');
			Gold.pmUpperStaff(user.name + " has been sbanned for attempting to advertise" + (room ? ". **Room:**" + room.id : " in a private message to " + targetUser.name + ".") + " **Message:** " + message, "~Server");
			Monitor.log("[AdvMonitor] " + user.name + " has been sbanned for attempting to advertise" + (room ? ". **Room:** " + room.id : " in a private message to " + targetUser.name + ".") + " **Message:** " + message);
		}
	}
	return message;
};
Config.chatfilter = exports.chatfilter;

/*********************
 * Namefilter Magic *
 * ******************/
try {
	Config.bannedNames = fs.readFileSync('config/bannednames.txt', 'utf8').toLowerCase().split('\n');
} catch (e) {
	Config.bannedNames = [];
}

function loadBannedNames() {
	try {
		Config.bannedNames = fs.readFileSync('config/bannednames.txt', 'utf8').toLowerCase().split('\n');
	} catch (e) {
		Config.bannedNames = [];
	}
}
loadBannedNames();

Config.namefilter = function (name, user) {
	let badNames = Config.bannedNames, badHosts = Object.keys(Gold.lockedHosts), nameId = toId(name);
	for (let x in badNames) {
		if (nameId.indexOf(badNames[x]) > -1 && badNames[x] !== '') {
			Monitor.log('[NameFilter] should probably FR: ' + name);
		}
	}

	// Hostfilter stuff
	let conNum = Object.keys(user.connections).length - 1;
	let ip = user.connections[conNum].ip;
	let trusted = trustedHack(nameId);

	Dnsbl.reverse(ip).then(host => {
		if (!host) return;
		if (badHosts.length < 0) return; // there are no blacklisted hosts (yet)

		// handling "trusted" users...
		if (trusted) return;
		if (Gold.userData[toId(name)] && Gold.userData[toId(name)].proxywhitelist) return;
		if (proxyWhitelist && proxyWhitelist.includes(nameId)) return;

		badHosts.forEach(badHost => {
			if (host.includes(badHost)) {
				user.locked = '#hostfilter';
				user.updateIdentity();
				user.popup("|modal|You have been automatically locked due to being on a proxy known for spam and abuse.\n\nLog off PS! and try reconnecting without a proxy to be unlocked.");
				// Monitor.log("[ProxyMonitor] " + name + " (" + ip + ") has been automatically locked. (" + host + ")");
				return;
			}
		});
	});

	if (user.connections[conNum].headers && user.connections[conNum].headers['user-agent']) {
		user.useragent = user.connections[conNum].headers['user-agent'];
	}

	Gold.evadeMonitor(user, name);

	return name;
};

// deal with global ranked user's manually...
function trustedHack(name) {
	let nameId = toId(name);
	let userSymbol = (Users.usergroups[nameId] ? Users.usergroups[nameId].substr(0, 1) : ' ');
	let rankIndex = (Config.groupsranking.includes(userSymbol) ? Config.groupsranking.indexOf(userSymbol) : false);
	if (rankIndex && rankIndex > 0) return true;
	return false;
}

/*********************
 * Hostfilter Magic *
 * ******************/
Gold.lockedHosts = Object.create(null);

function loadHostBlacklist() {
	fs.readFile('config/lockedhosts.json', 'utf8', function (err, file) {
		if (err) return;
		Gold.lockedHosts = JSON.parse(file);
	});
}
loadHostBlacklist();

function saveHost() {
	fs.writeFileSync('config/lockedhosts.json', JSON.stringify(Gold.lockedHosts));
}

Gold.evadeMonitor = function (user, name, punished) {
	let punishments = this.punishments;
	if (punished && punished.alts) { // handles when user is unlocked
		punished.alts.forEach(alt => {
			if (Gold.punishments[toId(alt)]) delete Gold.punishments[toId(alt)];
		});
		Gold.savePunishments();
		return;
	}
	let points = 0;
	let matched = false;
	let num = Object.keys(user.connections).length - 1;
	let userAgent = user.useragent ? user.useragent : '';
	let ip = user.latestIp;

	if (punished) {
		let tarId = toId(user.name); // since user.userid can be a guest number here...
		Object.keys(punishments).forEach(punished => {
			if (punishments[punished].ip === ip) matched = true;
		});
		if (!matched && !punishments[tarId]) {
			punishments[tarId] = {
				'useragent': userAgent,
				'ip': ip,
				'iprange': Gold.getIpRange(ip)[0],
				'ipclass': Gold.getIpRange(ip)[1],
				'type': punished.type,
				'exires': punished.expires,
			};
			Gold.savePunishments();
		}
	} else {
		if (user.locked || Users.ShadowBan.checkBanned(user) || trustedHack(name)) return;

		let ipRange = Gold.getIpRange(ip)[0];
		let reasons = [];
		let evader = '';
		let alertStaff = false;
		let defaultAvatars = [1, 2, 101, 102, 169, 170, 265, 266];
		Object.keys(punishments).forEach(offender => {
			if (punishments[offender].ip === ip) return;
			if (reasons.length >= 3) return; // this should never happen
			if (punishments[offender].exires < Date.now()) {
				delete punishments[offender];
				Gold.savePunishments();
				return;
			}
			if (punishments[offender].useragent === userAgent && punishments[offender].useragent !== '') {
				points++;
				reasons.push(`have the same user agent`);
				evader = punishments[offender].type + ' user: ' + offender;
				alertStaff = true;
			}
			if (punishments[offender].iprange && ip.startsWith(punishments[offender].iprange)) {
				points++;
				reasons.push(`have the IPv4 class ${punishments[offender].ipclass} range (${ipRange}.*)`);
				evader = punishments[offender].type + ' user: ' + offender;
			}
			// this does not count AS a reason, but merely to add to the list of reasons
			if (defaultAvatars.includes(user.avatar)) {
				reasons.push(`have a default avatar`);
			}
		});
		let staff = Rooms('staff');
		if (points >= 2) {
			Users.ShadowBan.addUser(name);
			if (staff) staff.add(`[EvadeMonitor] SHADOWBANNED: ${name}, evading alt of ${evader} because they ${reasons.join(' and ')}`).update();
		} else if (alertStaff) {
			if (staff) staff.add(`[EvadeMonitor] SUSPECTED EVADER: ${name} is possibly an evading alt of ${evader} because they ${reasons.join(' and ')}.`).update();
		}
	}
};

exports.commands = {
	lockhost: function (target, room, user) {
		if (!this.can('pban')) return false;
		if (!target) return this.parse('/help lockhost');
		if (Gold.lockedHosts[target]) return this.errorReply("The host '" + target + "' is was already locked by " + Gold.lockedHosts[target].by + ".");

		Gold.lockedHosts[target] = {
			by: user.name,
			on: Date.now(),
		};
		saveHost();

		this.privateModCommand("(" + user.name + " has blacklisted host: " + target + ")");
	},
	lockhosthelp: ["/lockhost [host] - Adds host to server blacklist.  Users connecting with these hosts will be automatically locked from connection, so use this carefully! Requires & ~"],

	unlockhost: function (target, room, user) {
		if (!this.can('pban')) return false;
		if (!target) return this.parse('/help unlockhost');
		if (!Gold.lockedHosts[target]) return this.errorReply("The host '" + target + "' is not currently blacklisted.");

		delete Gold.lockedHosts[target];
		saveHost();

		this.privateModCommand("(" + user.name + " has unblacklisted host: " + target + ")");
	},
	unlockhosthelp: ["/unlockhost [host] - Removes a host from the server's blacklist.  Requires & ~"],

	proxylist: function (target, room, user) {
		if (!this.can('pban')) return false;
		let badHosts = Object.keys(Gold.lockedHosts);
		if (badHosts.length < 0) return this.errorReply("Weird, no proxies have been blacklisted (yet).");

		let buff = '

Proxy:	Blacklisted By:	Blacklisted:
';
		badHosts.forEach(proxy => {
			buff += '
' + proxy + '	' + Gold.nameColor(Gold.lockedHosts[proxy].by, false) + '	' + Chat.toDurationString(Date.now() - Gold.lockedHosts[proxy].on) + ' ago
';
		});
		buff += '
';

		return this.sendReplyBox(buff);
	},
};
<commit_msg>Remove unused variable in filters<commit_after>/**
 * Filters
 * Gold Server - http://gold.psim.us/
 *
 * Manually sets filters for chatting and names.
 * In this, we also handle proxy connections with a blacklist feature.
 * Credits: jd, panpawn
 *
 * @license MIT license
 */
'use strict';

const fs = require('fs');

let adWhitelist = Config.adWhitelist ? Config.adWhitelist : [];
let adRegex = new RegExp("(play.pokemonshowdown.com\\/~~)(?!(" + adWhitelist.join('|') + "))", "g");

let bannedMessages = Config.bannedMessages ? Config.bannedMessages : [];

let watchPhrases = Config.watchPhrases ? Config.watchPhrases : [];
let watchUsers = Config.watchUsers ? Config.watchUsers : [];

let proxyWhitelist = Config.proxyWhitelist || false;

/*********************
 * Chatfilter Magic *
 * ******************/

exports.chatfilter = function (message, user, room, connection, targetUser) {
	user.lastActiveTime = Date.now();
	if (!room && !Users(targetUser)) targetUser = {name: 'unknown user'};

	// watch phrases and watch users
	let watchRoom = Rooms('watchroom') ? Rooms('watchroom') : false;
	if (watchRoom) {
		let watchWords = watchPhrases.filter(phrase => { return ~toId(message).indexOf(phrase); }).length;
		let watchUserslist = watchUsers.filter(name => { return ~user.userid.indexOf(name); }).length;
		if (!user.can('hotpatch') && watchWords >= 1 || watchUserslist >= 1) {
			watchRoom.add('|c|' + user.getIdentity() + '| __(' + (room ? "To " + room.id : "Private message to " + targetUser.name) + ")__ " + message).update();
		}
	}

	// global banned messages
	for (let x in bannedMessages) {
		if (message.toLowerCase().indexOf(bannedMessages[x]) > -1 && bannedMessages[x] !== '' && message.substr(0, 1) !== '/') {
			if (user.locked) return false;
			Punishments.lock(user, Date.now() + 7 * 24 * 60 * 60 * 1000, null, "Said a banned word: " + bannedMessages[x]);
			user.popup('You have been automatically locked for sending a message containing a banned word.');
			Monitor.log('[PornMonitor] ' + user.name + ' __(' + (room ? 'In ' + room.id : 'Private message to ' + targetUser.name) + ')__ was automatically locked (and shadow banned) for trying to say "' + message + '"');
			fs.appendFile('logs/modlog/modlog_staff.txt', '[' + (new Date().toJSON()) + '] (staff) ' + user.name + ' was locked from talking by the Server (' +
			bannedMessages[x] + ') (' + connection.ip + ')\n');
			Gold.pmUpperStaff(user.name + ' has been automatically locked for sending a message containing a banned word' +
			(room ? ". **Room:**" + room.id : " in a private message to " + targetUser.name + ".") + ' **Message:** ' + message, '~Server');
			Users.ShadowBan.addUser(user);
		}
	}

	// advertising
	let pre_matches = (message.match(/psim|psim.us|psim us|psm.us|psm us/g) || []).length;
	let final_check = (pre_matches >= 1 ? adWhitelist.filter(server => { return ~message.indexOf(server); }).length : 0);
	if (!user.can('hotpatch') && (pre_matches >= 1 && final_check === 0 || pre_matches >= 2 && final_check >= 1 || message.match(adRegex))) {
		if (user.locked) return false;
		if (!Users.ShadowBan.checkBanned(user)) {
			Users.ShadowBan.addUser(user);
			fs.appendFile('logs/modlog/modlog_staff.txt', '[' + (new Date().toJSON()) + '] (staff) ' + user.name + ' was shadow banned by the Server. (Advertising) (' + connection.ip + ')\n');
			Gold.pmUpperStaff(user.name + " has been sbanned for attempting to advertise" + (room ? ". **Room:**" + room.id : " in a private message to " + targetUser.name + ".") + " **Message:** " + message, "~Server");
			Monitor.log("[AdvMonitor] " + user.name + " has been sbanned for attempting to advertise" + (room ? ". **Room:** " + room.id : " in a private message to " + targetUser.name + ".") + " **Message:** " + message);
		}
	}
	return message;
};
Config.chatfilter = exports.chatfilter;

/*********************
 * Namefilter Magic *
 * ******************/
try {
	Config.bannedNames = fs.readFileSync('config/bannednames.txt', 'utf8').toLowerCase().split('\n');
} catch (e) {
	Config.bannedNames = [];
}

function loadBannedNames() {
	try {
		Config.bannedNames = fs.readFileSync('config/bannednames.txt', 'utf8').toLowerCase().split('\n');
	} catch (e) {
		Config.bannedNames = [];
	}
}
loadBannedNames();

Config.namefilter = function (name, user) {
	let badNames = Config.bannedNames, badHosts = Object.keys(Gold.lockedHosts), nameId = toId(name);
	for (let x in badNames) {
		if (nameId.indexOf(badNames[x]) > -1 && badNames[x] !== '') {
			Monitor.log('[NameFilter] should probably FR: ' + name);
		}
	}

	// Hostfilter stuff
	let conNum = Object.keys(user.connections).length - 1;
	let ip = user.connections[conNum].ip;
	let trusted = trustedHack(nameId);

	Dnsbl.reverse(ip).then(host => {
		if (!host) return;
		if (badHosts.length < 0) return; // there are no blacklisted hosts (yet)

		// handling "trusted" users...
		if (trusted) return;
		if (Gold.userData[toId(name)] && Gold.userData[toId(name)].proxywhitelist) return;
		if (proxyWhitelist && proxyWhitelist.includes(nameId)) return;

		badHosts.forEach(badHost => {
			if (host.includes(badHost)) {
				user.locked = '#hostfilter';
				user.updateIdentity();
				user.popup("|modal|You have been automatically locked due to being on a proxy known for spam and abuse.\n\nLog off PS! and try reconnecting without a proxy to be unlocked.");
				// Monitor.log("[ProxyMonitor] " + name + " (" + ip + ") has been automatically locked. (" + host + ")");
				return;
			}
		});
	});

	if (user.connections[conNum].headers && user.connections[conNum].headers['user-agent']) {
		user.useragent = user.connections[conNum].headers['user-agent'];
	}

	Gold.evadeMonitor(user, name);

	return name;
};

// deal with global ranked user's manually...
function trustedHack(name) {
	let nameId = toId(name);
	let userSymbol = (Users.usergroups[nameId] ? Users.usergroups[nameId].substr(0, 1) : ' ');
	let rankIndex = (Config.groupsranking.includes(userSymbol) ? Config.groupsranking.indexOf(userSymbol) : false);
	if (rankIndex && rankIndex > 0) return true;
	return false;
}

/*********************
 * Hostfilter Magic *
 * ******************/
Gold.lockedHosts = Object.create(null);

function loadHostBlacklist() {
	fs.readFile('config/lockedhosts.json', 'utf8', function (err, file) {
		if (err) return;
		Gold.lockedHosts = JSON.parse(file);
	});
}
loadHostBlacklist();

function saveHost() {
	fs.writeFileSync('config/lockedhosts.json', JSON.stringify(Gold.lockedHosts));
}

Gold.evadeMonitor = function (user, name, punished) {
	let punishments = this.punishments;
	if (punished && punished.alts) { // handles when user is unlocked
		punished.alts.forEach(alt => {
			if (Gold.punishments[toId(alt)]) delete Gold.punishments[toId(alt)];
		});
		Gold.savePunishments();
		return;
	}
	let points = 0;
	let matched = false;
	let userAgent = user.useragent ? user.useragent : '';
	let ip = user.latestIp;

	if (punished) {
		let tarId = toId(user.name); // since user.userid can be a guest number here...
		Object.keys(punishments).forEach(punished => {
			if (punishments[punished].ip === ip) matched = true;
		});
		if (!matched && !punishments[tarId]) {
			punishments[tarId] = {
				'useragent': userAgent,
				'ip': ip,
				'iprange': Gold.getIpRange(ip)[0],
				'ipclass': Gold.getIpRange(ip)[1],
				'type': punished.type,
				'exires': punished.expires,
			};
			Gold.savePunishments();
		}
	} else {
		if (user.locked || Users.ShadowBan.checkBanned(user) || trustedHack(name)) return;

		let ipRange = Gold.getIpRange(ip)[0];
		let reasons = [];
		let evader = '';
		let alertStaff = false;
		let defaultAvatars = [1, 2, 101, 102, 169, 170, 265, 266];
		Object.keys(punishments).forEach(offender => {
			if (punishments[offender].ip === ip) return;
			if (reasons.length >= 3) return; // this should never happen
			if (punishments[offender].exires < Date.now()) {
				delete punishments[offender];
				Gold.savePunishments();
				return;
			}
			if (punishments[offender].useragent === userAgent && punishments[offender].useragent !== '') {
				points++;
				reasons.push(`have the same user agent`);
				evader = punishments[offender].type + ' user: ' + offender;
				alertStaff = true;
			}
			if (punishments[offender].iprange && ip.startsWith(punishments[offender].iprange)) {
				points++;
				reasons.push(`have the IPv4 class ${punishments[offender].ipclass} range (${ipRange}.*)`);
				evader = punishments[offender].type + ' user: ' + offender;
			}
			// this does not count AS a reason, but merely to add to the list of reasons
			if (defaultAvatars.includes(user.avatar)) {
				reasons.push(`have a default avatar`);
			}
		});
		let staff = Rooms('staff');
		if (points >= 2) {
			Users.ShadowBan.addUser(name);
			if (staff) staff.add(`[EvadeMonitor] SHADOWBANNED: ${name}, evading alt of ${evader} because they ${reasons.join(' and ')}`).update();
		} else if (alertStaff) {
			if (staff) staff.add(`[EvadeMonitor] SUSPECTED EVADER: ${name} is possibly an evading alt of ${evader} because they ${reasons.join(' and ')}.`).update();
		}
	}
};

exports.commands = {
	lockhost: function (target, room, user) {
		if (!this.can('pban')) return false;
		if (!target) return this.parse('/help lockhost');
		if (Gold.lockedHosts[target]) return this.errorReply("The host '" + target + "' is was already locked by " + Gold.lockedHosts[target].by + ".");

		Gold.lockedHosts[target] = {
			by: user.name,
			on: Date.now(),
		};
		saveHost();

		this.privateModCommand("(" + user.name + " has blacklisted host: " + target + ")");
	},
	lockhosthelp: ["/lockhost [host] - Adds host to server blacklist.  Users connecting with these hosts will be automatically locked from connection, so use this carefully! Requires & ~"],

	unlockhost: function (target, room, user) {
		if (!this.can('pban')) return false;
		if (!target) return this.parse('/help unlockhost');
		if (!Gold.lockedHosts[target]) return this.errorReply("The host '" + target + "' is not currently blacklisted.");

		delete Gold.lockedHosts[target];
		saveHost();

		this.privateModCommand("(" + user.name + " has unblacklisted host: " + target + ")");
	},
	unlockhosthelp: ["/unlockhost [host] - Removes a host from the server's blacklist.  Requires & ~"],

	proxylist: function (target, room, user) {
		if (!this.can('pban')) return false;
		let badHosts = Object.keys(Gold.lockedHosts);
		if (badHosts.length < 0) return this.errorReply("Weird, no proxies have been blacklisted (yet).");

		let buff = '

Proxy:	Blacklisted By:	Blacklisted:
';
		badHosts.forEach(proxy => {
			buff += '
' + proxy + '	' + Gold.nameColor(Gold.lockedHosts[proxy].by, false) + '	' + Chat.toDurationString(Date.now() - Gold.lockedHosts[proxy].on) + ' ago
';
		});
		buff += '
';

		return this.sendReplyBox(buff);
	},
};
<|endoftext|>


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

/**
 * Type for loading and playing sounds, works on multiple platforms and supports embedded base64 data.  This uses Web
 * Audio when available, primarily because the webkit platforms were failing with cross-domain errors when attempting to
 * load audio data from embedded data URIs.  This was occurring in mid-September 2013.  Simplification may be possible
 * if the cross-domain issue goes away at some point in the future.
 */
define( function( require ) {
  'use strict';

  // modules
  var Display = require( 'SCENERY/display/Display' );
  var inherit = require( 'PHET_CORE/inherit' );
  var platform = require( 'PHET_CORE/platform' );
  var Property = require( 'AXON/Property' );
  var vibe = require( 'VIBE/vibe' );

  // sounds
  var empty = require( 'sound!VIBE/empty.mp3' );

  // global property that allows all audio to be turned on/off, see #11
  var audioEnabledProperty = new Property( true );

  // set up a single audio context that will be used by all sounds when using Web Audio API
  var audioContext;
  if ( 'AudioContext' in window ) {
    audioContext = new AudioContext();
  }
  else if ( 'webkitAudioContext' in window ) {
    audioContext = new webkitAudioContext(); // eslint-disable-line no-undef
  }

  // controls volume if Web Audio API supported
  if ( audioContext ) {
    var gainNode = audioContext.createGain();
    gainNode.connect( audioContext.destination );
    gainNode.gain.setValueAtTime( phet.chipper.queryParameters.audioVolume, audioContext.currentTime );
  }

  /**
   * @param {Object} soundInfo - An object that includes *either* a url that points to the sound to be played *or* a
   * base64-encoded version of the sound data.  The former is generally used when a sim is running in RequireJS mode,
   * the latter is used in built versions.
   * @constructor
   */
  function Sound( soundInfo ) {

    var self = this;

    // parameter checking
    if ( typeof soundInfo !== 'object' ||
         ( typeof soundInfo.base64 === 'undefined' && typeof soundInfo.url === 'undefined' ) ) {
      throw new Error( 'Error with soundInfo object: Does not contain a necessary value.' );
    }

    this.sound = document.createElement( 'audio' );
    var supportedFormatFound = false;

    // Identify the audio format.  The URL is generally used when running in RequieJS mode, base64 is used when running
    // single-html-file (aka "built") simulations.
    var audioFormat;
    if ( soundInfo.url ) {
      audioFormat = 'audio/' +
                    soundInfo.url.slice( soundInfo.url.lastIndexOf( '.' ) + 1,
                      soundInfo.url.lastIndexOf( '?' ) >= 0 ? soundInfo.url.lastIndexOf( '?' ) : soundInfo.url.length
                    );
    }
    else {
      audioFormat = soundInfo.base64.slice( soundInfo.base64.indexOf( ':' ) + 1, soundInfo.base64.indexOf( ';' ) );
    }

    // determine whether this audio format is supported (doesn't exist for phantomjs, which is used in automated testing)
    if ( this.sound.canPlayType && this.sound.canPlayType( audioFormat ) ) {
      supportedFormatFound = true;
    }
    else {
      console.log( 'Warning: audio format not supported, sound will not be played.' );
    }

    // load the sound into memory
    if ( supportedFormatFound ) {
      if ( audioContext ) {
        var arrayBuff;

        if ( soundInfo.base64 ) {

          // We're working with base64 data, so we need to decode it. The regular expression removes the mime header.
          var soundData = ( soundInfo.base64 ? soundInfo.base64 : this.sound.getAttribute( 'src' ) ).replace( new RegExp( '^.*,' ), '' );
          var byteChars = window.atob( soundData );
          var byteArray = new window.Uint8Array( byteChars.length );
          for ( var j = 0; j < byteArray.length; j++ ) {
            byteArray[ j ] = byteChars.charCodeAt( j ); // need check to make sure this cast doesn't give problems?
          }
          arrayBuff = byteArray.buffer;

          audioContext.decodeAudioData( arrayBuff,
            function( audioData ) {
              self.audioBuffer = audioData;
            },
            function() {
              console.log( 'Error: Unable to decode audio data.' );
            }
          );
        }
        else {

          // load the sound via URL
          var request = new XMLHttpRequest();
          request.open( 'GET', soundInfo.url, true );
          request.responseType = 'arraybuffer';
          request.onload = function() {

            // decode the audio data asynchronously
            audioContext.decodeAudioData( request.response,
              function( audioData ) {
                self.audioBuffer = audioData;
              },
              function() { console.log( 'Error loading and decoding sound, sound name: ' + soundInfo.url ); }
            );
          };
          request.onerror = function() {
            console.log( 'Error occurred on attempt to load sound data.' );
          };
          request.send();
        }
      }
      else {

        // web Audio API is not available, so insert the sound into the DOM and use HTML5 audio
        this.sound.setAttribute( 'src', soundInfo.base64 ? soundInfo.base64 : soundInfo.url );
        this.sound.load();
      }
    }
  }

  vibe.register( 'Sound', Sound );

  inherit( Object, Sound, {

    /**
     * Plays the sound using the Web Audio API if available or HTML5 audio if not.
     * @public
     */
    play: function() {
      if ( !Sound.audioEnabledProperty.get() ) {
        return;
      }
      if ( audioContext ) {

        // Use the Web Audio API.  The following 'if' clause is necessary to be sure that the audio has finished
        // loading, see https://github.com/phetsims/vibe/issues/20.
        if ( this.audioBuffer ) {
          this.soundSource = audioContext.createBufferSource();
          this.soundSource.buffer = this.audioBuffer;
          this.soundSource.connect( gainNode );

          if ( typeof this.soundSource.start === 'function' ) {
            this.soundSource.start( 0 );
          }
          else if ( typeof this.soundSource.noteOn === 'function' ) {
            this.soundSource.noteOn( 0 );
          }
        }
      }
      else {

        // Use the HTML5 API (doesn't exist for phantomjs)
        this.sound.play && this.sound.play();
      }
    },

    stop: function() {
      if ( audioContext && this.soundSource ) {

        // use Web Audio API
        this.soundSource.stop();
      }
      else {

        // use HTML5 audio (doesn't exist for phantomjs)
        this.sound.pause && this.sound.pause();
        this.sound.currentTime = 0;
      }
    }
  }, {

    // @public, @static, global control for audio on/off
    audioEnabledProperty: audioEnabledProperty
  } );

  // Below is some platform-specific code for handling a number of issues related to audio.  It may be possible to
  // remove some or all of this as Web Audio becomes more consistently implemented.
  if ( audioContext ) {

    if ( !platform.mobileSafari ) {

      // In some browsers the audio context is not allowed to run before the user interacts with the simulation.  The
      // motivation for this is to prevent auto-play of sound (mostly videos) when users land on websites, but it ends
      // up preventing PhET sims from being able to play sound.  To deal with this, we add a listener that can check the
      // state of the audio context and "resume" it if necessary when the user starts interacting with the sim.  See
      // https://github.com/phetsims/vibe/issues/32 for more information.
      if ( audioContext.state !== 'running' ) {

        Display.userGestureEmitter.addListener( function resumeAudioContext() {
          if ( audioContext.state !== 'running' ) {

            // the audio context isn't running, so tell it to resume
            audioContext.resume().catch( function( err ) {
              assert && assert( false, 'error when trying to resume audio context, err = ' + err );
            } );
          }
          Display.userGestureEmitter.removeListener( resumeAudioContext ); // only do this once
        } );
      }
    }
    else {

      // There is a different issue for audio on iOS+Safari: On this platform, we must play an audio file from a thread
      // initiated by a user event such as touchstart before any sounds will play.  This requires the user to touch the
      // screen before audio can be played. See
      // http://stackoverflow.com/questions/12517000/no-sound-on-ios-6-web-audio-api
      var silence = new Sound( empty );
      var playSilence = function() {
        silence.play();
        window.removeEventListener( 'touchstart', playSilence, false );
      };
      window.addEventListener( 'touchstart', playSilence, false );
    }
  }

  return Sound;

} );<commit_msg>changed .log message into .warn<commit_after>// Copyright 2013-2018, University of Colorado Boulder

/**
 * Type for loading and playing sounds, works on multiple platforms and supports embedded base64 data.  This uses Web
 * Audio when available, primarily because the webkit platforms were failing with cross-domain errors when attempting to
 * load audio data from embedded data URIs.  This was occurring in mid-September 2013.  Simplification may be possible
 * if the cross-domain issue goes away at some point in the future.
 */
define( function( require ) {
  'use strict';

  // modules
  var Display = require( 'SCENERY/display/Display' );
  var inherit = require( 'PHET_CORE/inherit' );
  var platform = require( 'PHET_CORE/platform' );
  var Property = require( 'AXON/Property' );
  var vibe = require( 'VIBE/vibe' );

  // sounds
  var empty = require( 'sound!VIBE/empty.mp3' );

  // global property that allows all audio to be turned on/off, see #11
  var audioEnabledProperty = new Property( true );

  // set up a single audio context that will be used by all sounds when using Web Audio API
  var audioContext;
  if ( 'AudioContext' in window ) {
    audioContext = new AudioContext();
  }
  else if ( 'webkitAudioContext' in window ) {
    audioContext = new webkitAudioContext(); // eslint-disable-line no-undef
  }

  // controls volume if Web Audio API supported
  if ( audioContext ) {
    var gainNode = audioContext.createGain();
    gainNode.connect( audioContext.destination );
    gainNode.gain.setValueAtTime( phet.chipper.queryParameters.audioVolume, audioContext.currentTime );
  }

  /**
   * @param {Object} soundInfo - An object that includes *either* a url that points to the sound to be played *or* a
   * base64-encoded version of the sound data.  The former is generally used when a sim is running in RequireJS mode,
   * the latter is used in built versions.
   * @constructor
   */
  function Sound( soundInfo ) {

    var self = this;

    // parameter checking
    if ( typeof soundInfo !== 'object' ||
         ( typeof soundInfo.base64 === 'undefined' && typeof soundInfo.url === 'undefined' ) ) {
      throw new Error( 'Error with soundInfo object: Does not contain a necessary value.' );
    }

    this.sound = document.createElement( 'audio' );
    var supportedFormatFound = false;

    // Identify the audio format.  The URL is generally used when running in RequieJS mode, base64 is used when running
    // single-html-file (aka "built") simulations.
    var audioFormat;
    if ( soundInfo.url ) {
      audioFormat = 'audio/' +
                    soundInfo.url.slice( soundInfo.url.lastIndexOf( '.' ) + 1,
                      soundInfo.url.lastIndexOf( '?' ) >= 0 ? soundInfo.url.lastIndexOf( '?' ) : soundInfo.url.length
                    );
    }
    else {
      audioFormat = soundInfo.base64.slice( soundInfo.base64.indexOf( ':' ) + 1, soundInfo.base64.indexOf( ';' ) );
    }

    // determine whether this audio format is supported (doesn't exist for phantomjs, which is used in automated testing)
    if ( this.sound.canPlayType && this.sound.canPlayType( audioFormat ) ) {
      supportedFormatFound = true;
    }
    else {
      console.warn( 'audio format not supported, sound will not be played.' );
    }

    // load the sound into memory
    if ( supportedFormatFound ) {
      if ( audioContext ) {
        var arrayBuff;

        if ( soundInfo.base64 ) {

          // We're working with base64 data, so we need to decode it. The regular expression removes the mime header.
          var soundData = ( soundInfo.base64 ? soundInfo.base64 : this.sound.getAttribute( 'src' ) ).replace( new RegExp( '^.*,' ), '' );
          var byteChars = window.atob( soundData );
          var byteArray = new window.Uint8Array( byteChars.length );
          for ( var j = 0; j < byteArray.length; j++ ) {
            byteArray[ j ] = byteChars.charCodeAt( j ); // need check to make sure this cast doesn't give problems?
          }
          arrayBuff = byteArray.buffer;

          audioContext.decodeAudioData( arrayBuff,
            function( audioData ) {
              self.audioBuffer = audioData;
            },
            function() {
              console.log( 'Error: Unable to decode audio data.' );
            }
          );
        }
        else {

          // load the sound via URL
          var request = new XMLHttpRequest();
          request.open( 'GET', soundInfo.url, true );
          request.responseType = 'arraybuffer';
          request.onload = function() {

            // decode the audio data asynchronously
            audioContext.decodeAudioData( request.response,
              function( audioData ) {
                self.audioBuffer = audioData;
              },
              function() { console.log( 'Error loading and decoding sound, sound name: ' + soundInfo.url ); }
            );
          };
          request.onerror = function() {
            console.log( 'Error occurred on attempt to load sound data.' );
          };
          request.send();
        }
      }
      else {

        // web Audio API is not available, so insert the sound into the DOM and use HTML5 audio
        this.sound.setAttribute( 'src', soundInfo.base64 ? soundInfo.base64 : soundInfo.url );
        this.sound.load();
      }
    }
  }

  vibe.register( 'Sound', Sound );

  inherit( Object, Sound, {

    /**
     * Plays the sound using the Web Audio API if available or HTML5 audio if not.
     * @public
     */
    play: function() {
      if ( !Sound.audioEnabledProperty.get() ) {
        return;
      }
      if ( audioContext ) {

        // Use the Web Audio API.  The following 'if' clause is necessary to be sure that the audio has finished
        // loading, see https://github.com/phetsims/vibe/issues/20.
        if ( this.audioBuffer ) {
          this.soundSource = audioContext.createBufferSource();
          this.soundSource.buffer = this.audioBuffer;
          this.soundSource.connect( gainNode );

          if ( typeof this.soundSource.start === 'function' ) {
            this.soundSource.start( 0 );
          }
          else if ( typeof this.soundSource.noteOn === 'function' ) {
            this.soundSource.noteOn( 0 );
          }
        }
      }
      else {

        // Use the HTML5 API (doesn't exist for phantomjs)
        this.sound.play && this.sound.play();
      }
    },

    stop: function() {
      if ( audioContext && this.soundSource ) {

        // use Web Audio API
        this.soundSource.stop();
      }
      else {

        // use HTML5 audio (doesn't exist for phantomjs)
        this.sound.pause && this.sound.pause();
        this.sound.currentTime = 0;
      }
    }
  }, {

    // @public, @static, global control for audio on/off
    audioEnabledProperty: audioEnabledProperty
  } );

  // Below is some platform-specific code for handling a number of issues related to audio.  It may be possible to
  // remove some or all of this as Web Audio becomes more consistently implemented.
  if ( audioContext ) {

    if ( !platform.mobileSafari ) {

      // In some browsers the audio context is not allowed to run before the user interacts with the simulation.  The
      // motivation for this is to prevent auto-play of sound (mostly videos) when users land on websites, but it ends
      // up preventing PhET sims from being able to play sound.  To deal with this, we add a listener that can check the
      // state of the audio context and "resume" it if necessary when the user starts interacting with the sim.  See
      // https://github.com/phetsims/vibe/issues/32 for more information.
      if ( audioContext.state !== 'running' ) {

        Display.userGestureEmitter.addListener( function resumeAudioContext() {
          if ( audioContext.state !== 'running' ) {

            // the audio context isn't running, so tell it to resume
            audioContext.resume().catch( function( err ) {
              assert && assert( false, 'error when trying to resume audio context, err = ' + err );
            } );
          }
          Display.userGestureEmitter.removeListener( resumeAudioContext ); // only do this once
        } );
      }
    }
    else {

      // There is a different issue for audio on iOS+Safari: On this platform, we must play an audio file from a thread
      // initiated by a user event such as touchstart before any sounds will play.  This requires the user to touch the
      // screen before audio can be played. See
      // http://stackoverflow.com/questions/12517000/no-sound-on-ios-6-web-audio-api
      var silence = new Sound( empty );
      var playSilence = function() {
        silence.play();
        window.removeEventListener( 'touchstart', playSilence, false );
      };
      window.addEventListener( 'touchstart', playSilence, false );
    }
  }

  return Sound;

} );<|endoftext|>


<commit_before>
// node modules
import readline from 'readline';
// npm modules
import moment from 'moment';
import notifier from 'node-notifier';
import chalk from 'chalk';
import Table from 'cli-table2';
import PushBullet from 'pushbullet';
import opn from 'opn';
// our modules
import {api, config} from '../utils';

const {token, tokenAge, languages, certs} = config;
const requestBody = {};
const startTime = moment();
// The wait between calling submissionRequests().
const tickrate = 30000; // 30 seconds
const infoInterval = 10; // 10 * 30 seconds === 5 minutes

let options;

let error = '';
let accessToken = '';
let tick = 0;
let assignedTotal = 0;
let requestId = 0;
let assigned = [];
let unreadFeedbacks = [];
let positions = [];
let projectIds = [];

// Create a new table for projects that the user is queued up for
const projectDetails = new Table({
  head: [
    {hAlign: 'center', content: 'pos'},
    {hAlign: 'center', content: 'id'},
    {hAlign: 'left', content: 'project name'},
    {hAlign: 'center', content: 'lang'}],
  colWidths: [5, 7, 40, 7],
});

function createProjectDetailsTable() {
  // Push projects, sorted by queue position, into the projectDetails table
  projectDetails.length = 0;
  positions
    .sort((p1, p2) => p1.position - p2.position)
    .forEach((project) => {
      projectDetails.push([
        {hAlign: 'center', content: project.position},
        {hAlign: 'center', content: project.project_id},
        {hAlign: 'left', content: certs[project.project_id].name},
        {hAlign: 'center', content: project.language},
      ]);
    });
  return projectDetails.toString();
}

// Shows assigned projects in a table
const submissionDetails = new Table({
  head: [
    {hAlign: 'center', content: 'key'},
    {hAlign: 'left', content: 'project name'},
    {hAlign: 'center', content: 'expires'},
    {hAlign: 'center', content: 'price'}],
  colWidths: [5, 40, 15, 8],
});

function createSubmissionDetailsTable() {
  // Push assigned, sorted by time left, into the submissionDetails table
  submissionDetails.length = 0;
  assigned
    .forEach((submission, idx) => {
      const assignedAt = moment.utc(submission.assigned_at);
      const completeTime = assignedAt.add(12, 'hours');
      submissionDetails.push([
        {hAlign: 'center', content: idx + 1},
        {hAlign: 'left', content: submission.project.name},
        {hAlign: 'center', content: completeTime.fromNow()},
        {hAlign: 'center', content: submission.price},
      ]);
    });
  return submissionDetails.toString();
}

function setPrompt() {
  // Clearing the screen.
  readline.cursorTo(process.stdout, 0, 0);
  readline.clearScreenDown(process.stdout);

  // Warnings on error
  if (error) {
    console.log(chalk.red('The API is currently not responding, or very slow to respond.'));
    console.log(chalk.red('The script will continue to run and try to get a connection.'));
    console.log(chalk.red(`Error Code: ${error}`));
  }
  error = '';

  // Token expiry warning
  const tokenExpiryWarning = moment(tokenAge).diff(moment(), 'days') < 5;
  const tokenExpires = moment(tokenAge).fromNow();
  console.log(chalk[tokenExpiryWarning ? 'red' : 'green'](`Token expires ${tokenExpires}`));

  // General info.
  console.log(chalk.green(`Uptime: ${chalk.white(startTime.fromNow(true))}\n`));
  // Positions in request queues.
  console.log(chalk.blue('You are queued up for:\n'));

  // console.log a warning if max number of submissions are assigned, otherwise
  // console.log the projectDetails table
  if (assigned.length === 2) {
    console.log(chalk.yellow(`    You have ${chalk.white(assigned.length)} (max) submissions assigned.\n`));
  } else if (!positions.length) {
    console.log(chalk.yellow('Waiting for project details...\n'));
  } else {
    console.log(`${createProjectDetailsTable()}\n`);
  }

  // Assigned info.
  if (assigned.length) {
    const count = assigned.length === 1 ? 'one submission' : 'two submissions';
    console.log(chalk.yellow(`You currently have ${count} assigned:\n`));
    console.log(`${createSubmissionDetailsTable()}\n`);
  } else {
    console.log(chalk.yellow('No submissions are currently assigned.\n'));
  }

  if (!options.silent) {
    // Shows the number of projects that were assigned since the start of urcli
    console.log(chalk.green(`Total assigned: ${
      chalk.white(assignedTotal)} since ${startTime.format('dddd, MMMM Do YYYY, HH:mm')}\n`));

    // Info on when the next check will occur for queue position and feedbacks.
    if (tick % infoInterval === 0) {
      if (options.feedbacks) {
        console.log(chalk.blue('Checked for new feedbacks a few seconds ago...'));
      }
      console.log(chalk.blue('Checked the queue a few seconds ago...\n'));
    } else {
      const remainingSeconds = (infoInterval - (tick % infoInterval)) * (tickrate / 1000);
      const infoIsCheckedAt = moment().add(remainingSeconds, 'seconds');
      const humanReadableMessage = moment().to(infoIsCheckedAt);
      if (options.feedbacks) {
        console.log(chalk.blue(`Checking feedbacks ${humanReadableMessage}`));
      }
      console.log(chalk.blue(`Updating queue information ${humanReadableMessage}\n`));
    }

    // Keyboard shortcuts helptext
    console.log('KEYBOARD SHORTCUTS:\n');
    console.log(chalk.green.dim(`  Press ${
      chalk.white('0')} to open the review dashboard.`));
    console.log(chalk.green.dim(`  Press ${
      chalk.white('1')} or ${chalk.white('2')} to open your assigned submissions.\n`));
    console.log(chalk.green.dim(`  Press ${
      chalk.white('ctrl+c')} to exit the queue cleanly by deleting the submission_request.`));
    console.log(chalk.green.dim(`  Press ${
      chalk.white('ESC')} to suspend the script without deleting the submission_request.\n`));
  }
}

function exit(key) {
  if (key === 'escape') {
    // Suspend on ESC and refresh the submission_request rather than deleting it.
    api({token, task: 'refresh', id: requestId});
    console.log(chalk.green('Exited without deleting the submission_request...'));
    console.log(chalk.green('The current submission_request will expire in an hour.'));
    process.exit(0);
  } else if (key === 'SIGINT') {
    // Delete submission_request object and exit on CTRL-C
    api({token, task: 'delete', id: requestId}).then(() => {
      console.log(chalk.green('Successfully deleted request and exited..'));
      process.exit(0);
    });
  } else {
    process.exit(0);
  }
}

function setEventListeners() {
  const baseReviewURL = 'https://review.udacity.com/#!/submissions/';
  readline.emitKeypressEvents(process.stdin);
  process.stdin.setRawMode(true);
  process.stdin.on('keypress', (str, key) => {
    switch (key.sequence) {
      case '\u001b': // ESCAPE
        exit('escape');
        break;
      case '\u0003': // CTRL-C
        exit('SIGINT');
        break;
      case '0':
        opn(`${baseReviewURL}dashboard`);
        break;
      case '1':
        if (assigned[0]) opn(`${baseReviewURL}${assigned[0].id}`);
        break;
      case '2':
        if (assigned[1]) opn(`${baseReviewURL}${assigned[1].id}`);
        break;
      default:
        break;
    }
  });
}

function validateProjectIds(ids) {
  const certIds = Object.keys(certs);
  if (ids[0] === 'all') {
    return certIds;
  }
  const invalidIds = ids.filter(id => certIds.indexOf(id) === -1);
  if (invalidIds.length) {
    throw new Error(`Illegal Action: Not certified for project(s) ${[...invalidIds].join(', ')}`);
  }
  return ids;
}

function validateAccessToken() {
  if (options.push) {
    accessToken = options.push;
    const pusher = new PushBullet(accessToken);
    // Throw an error if we find no active devices to push to.
    pusher.devices((err, res) => {
      if (err) throw new Error(`Pushbullet error: ${err}`);
      if (!res.devices.length) throw new Error('Found no active devices to push to.');
    });
  }
}

function createRequestBody() {
  // Create a list of project/language pairs
  requestBody.projects = [];
  languages.forEach((language) => {
    /* eslint-disable camelcase */
    projectIds.forEach((project_id) => {
      requestBody.projects.push({project_id, language});
    });
  });
}

function assignmentNotification(projectInfo, submissionId) {
  const {name, id} = projectInfo;
  const title = `New Review Assigned! (${assigned.length})`;
  const message = `${moment().format('HH:mm')} - ${name} (${id})`;
  const sound = 'Ping';
  const open = `https://review.udacity.com/#!/submissions/${submissionId}`;
  notifier.notify({title, message, sound, open});

  // If the --push flag is set we push to active PushBullet devices
  if (accessToken) {
    const pusher = new PushBullet(accessToken);
    pusher.note({}, title, `${message}\n\n${open}`, (err) => {
      if (err) throw new Error(`Pushbullet error: ${err}`);
    });
  }
}

async function checkAssigned() {
  const assignedResponse = await api({token, task: 'assigned'});
  const oldAssignedIds = assigned.map(s => s.id);
  assigned = assignedResponse.body;

  if (assignedResponse.body.length) {
    assigned
      .filter(s => oldAssignedIds.indexOf(s.id) === -1)
      .forEach((s) => {
        // Only add it to the total number of assigned if it's been assigned
        // after the command was initiated.
        if (Date.parse(s.assigned_at) > Date.parse(startTime)) {
          assignedTotal += 1;
        }
        assignmentNotification(s.project, s.id);
      });
  }
}

async function checkPositions() {
  const positionResponse = await api({token, task: 'position', id: requestId});
  positions = positionResponse.body.error ? [] : positionResponse.body;
  setPrompt();
}

async function createSubmissionRequest() {
  const createResponse = await api({token, task: 'create', body: requestBody});
  requestId = createResponse.body.id;
  checkPositions();
  // Reset tick to reset the timers.
  tick = 0;
}

// This gets called if a submission_request is already active when the assign
// command is run. If the new project ids, input by the user, do not equal the
// project ids in the existing submission_request, we update the request.
let needToUpdate = (submissionRequest) => {
  needToUpdate = () => false;
  const submissionRequestProjectIds = submissionRequest.submission_request_projects
    .map(p => p.project_id);

  if (submissionRequestProjectIds.length !== projectIds.length) return true;

  const invalidIds = submissionRequestProjectIds
    .map(id => id.toString())
    .filter(id => projectIds.indexOf(id) === -1);
  return invalidIds.length;
};

async function updateSubmissionRequest() {
  const updateResponse = await api({token, task: 'update', id: requestId, body: requestBody});
  requestId = updateResponse.body.id;
  checkPositions();
  // Reset tick to reset the timers.
  tick = 0;
}

async function checkRefresh(closedAt) {
  const closingIn = Date.parse(closedAt) - Date.now();
  // If it expires in less than 5 minutes we refresh.
  if (closingIn < 300000) {
    const task = 'refresh';
    const id = requestId;
    api({token, task, id});
  }
}

function feedbackNotification(rating, name, id) {
  const title = `New ${rating}-star Feedback!`;
  const message = `Project: ${name}`;
  const sound = 'Pop';
  const open = `https://review.udacity.com/#!/reviews/${id}`;
  notifier.notify({title, message, sound, open});
}

async function checkFeedbacks() {
  const stats = await api({token, task: 'stats'});
  const diff = stats.body.unread_count - unreadFeedbacks.length;
  if (diff > 0) {
    const feedbacksResponse = await api({token, task: 'feedbacks'});
    unreadFeedbacks = feedbacksResponse.body.filter(fb => fb.read_at === null);
    // Notify the user of the new feedbacks.
    unreadFeedbacks.slice(-diff).forEach((fb) => {
      feedbackNotification(fb.rating, fb.project.name, fb.submission_id);
    });
  } else if (diff < 0) {
    // Note: If you check your feedbacks in the Reviews dashboard the unread
    // count always goes to 0. Therefore we can assume that a negative
    // difference between the current unread_count and the number of elements
    // in unreadFeedbacks, will mean that the new unread_count is 0.
    unreadFeedbacks = [];
  }
}

async function submissionRequests() {
  // Call API to check how many submissions are currently assigned.
  try {
    const count = await api({token, task: 'count'});
    if (assigned.length !== count.body.assigned_count) {
      checkAssigned();
    }
    // If then the assigned.length is less than the maximum number of assignments
    // allowed, we go through checking the submission_request.
    if (assigned.length < 2) {
      const getResponse = await api({token, task: 'get'});
      const submissionRequest = getResponse.body[0];
      // If there is no current submission_request we create a new one.
      if (!submissionRequest) {
        createSubmissionRequest();
      } else {
        requestId = submissionRequest.id;

        if (needToUpdate(submissionRequest)) {
          updateSubmissionRequest();
        } else {
          // Refresh the submission_request if it's is about to expire.
          checkRefresh(submissionRequest.closed_at);
          // Check the queue positions and for new feedbacks.
          if (tick % infoInterval === 0) {
            checkPositions();
            if (options.feedbacks) {
              checkFeedbacks();
            }
          }
        }
      }
    }
  } catch (e) {
    error = e.error.code;
  }
  setTimeout(() => {
    tick += 1;
    setPrompt();
    submissionRequests();
  }, tickrate);
}

export const assignCmd = (ids, opts) => {
  projectIds = validateProjectIds(ids);
  options = opts;
  validateAccessToken();
  setEventListeners();
  createRequestBody();
  // Start the request loop.
  submissionRequests();
  setPrompt();
};
<commit_msg>Add keyboard shortcut 'r' to the assign command to refresh the UI<commit_after>
// node modules
import readline from 'readline';
// npm modules
import moment from 'moment';
import notifier from 'node-notifier';
import chalk from 'chalk';
import Table from 'cli-table2';
import PushBullet from 'pushbullet';
import opn from 'opn';
// our modules
import {api, config} from '../utils';

const {token, tokenAge, languages, certs} = config;
const requestBody = {};
const startTime = moment();
// The wait between calling submissionRequests().
const tickrate = 30000; // 30 seconds
const infoInterval = 10; // 10 * 30 seconds === 5 minutes

let options;

let error = '';
let accessToken = '';
let tick = 0;
let assignedTotal = 0;
let requestId = 0;
let assigned = [];
let unreadFeedbacks = [];
let positions = [];
let projectIds = [];

// Create a new table for projects that the user is queued up for
const projectDetails = new Table({
  head: [
    {hAlign: 'center', content: 'pos'},
    {hAlign: 'center', content: 'id'},
    {hAlign: 'left', content: 'project name'},
    {hAlign: 'center', content: 'lang'}],
  colWidths: [5, 7, 40, 7],
});

function createProjectDetailsTable() {
  // Push projects, sorted by queue position, into the projectDetails table
  projectDetails.length = 0;
  positions
    .sort((p1, p2) => p1.position - p2.position)
    .forEach((project) => {
      projectDetails.push([
        {hAlign: 'center', content: project.position},
        {hAlign: 'center', content: project.project_id},
        {hAlign: 'left', content: certs[project.project_id].name},
        {hAlign: 'center', content: project.language},
      ]);
    });
  return projectDetails.toString();
}

// Shows assigned projects in a table
const submissionDetails = new Table({
  head: [
    {hAlign: 'center', content: 'key'},
    {hAlign: 'left', content: 'project name'},
    {hAlign: 'center', content: 'expires'},
    {hAlign: 'center', content: 'price'}],
  colWidths: [5, 40, 15, 8],
});

function createSubmissionDetailsTable() {
  // Push assigned, sorted by time left, into the submissionDetails table
  submissionDetails.length = 0;
  assigned
    .forEach((submission, idx) => {
      const assignedAt = moment.utc(submission.assigned_at);
      const completeTime = assignedAt.add(12, 'hours');
      submissionDetails.push([
        {hAlign: 'center', content: idx + 1},
        {hAlign: 'left', content: submission.project.name},
        {hAlign: 'center', content: completeTime.fromNow()},
        {hAlign: 'center', content: submission.price},
      ]);
    });
  return submissionDetails.toString();
}

function setPrompt() {
  // Clearing the screen.
  readline.cursorTo(process.stdout, 0, 0);
  readline.clearScreenDown(process.stdout);

  // Warnings on error
  if (error) {
    console.log(chalk.red('The API is currently not responding, or very slow to respond.'));
    console.log(chalk.red('The script will continue to run and try to get a connection.'));
    console.log(chalk.red(`Error Code: ${error}`));
  }
  error = '';

  // Token expiry warning
  const tokenExpiryWarning = moment(tokenAge).diff(moment(), 'days') < 5;
  const tokenExpires = moment(tokenAge).fromNow();
  console.log(chalk[tokenExpiryWarning ? 'red' : 'green'](`Token expires ${tokenExpires}`));

  // General info.
  console.log(chalk.green(`Uptime: ${chalk.white(startTime.fromNow(true))}\n`));
  // Positions in request queues.
  console.log(chalk.blue('You are queued up for:\n'));

  // console.log a warning if max number of submissions are assigned, otherwise
  // console.log the projectDetails table
  if (assigned.length === 2) {
    console.log(chalk.yellow(`    You have ${chalk.white(assigned.length)} (max) submissions assigned.\n`));
  } else if (!positions.length) {
    console.log(chalk.yellow('Waiting for project details...\n'));
  } else {
    console.log(`${createProjectDetailsTable()}\n`);
  }

  // Assigned info.
  if (assigned.length) {
    const count = assigned.length === 1 ? 'one submission' : 'two submissions';
    console.log(chalk.yellow(`You currently have ${count} assigned:\n`));
    console.log(`${createSubmissionDetailsTable()}\n`);
  } else {
    console.log(chalk.yellow('No submissions are currently assigned.\n'));
  }

  if (!options.silent) {
    // Shows the number of projects that were assigned since the start of urcli
    console.log(chalk.green(`Total assigned: ${
      chalk.white(assignedTotal)} since ${startTime.format('dddd, MMMM Do YYYY, HH:mm')}\n`));

    // Info on when the next check will occur for queue position and feedbacks.
    if (tick % infoInterval === 0) {
      if (options.feedbacks) {
        console.log(chalk.blue('Checked for new feedbacks a few seconds ago...'));
      }
      console.log(chalk.blue('Checked the queue a few seconds ago...\n'));
    } else {
      const remainingSeconds = (infoInterval - (tick % infoInterval)) * (tickrate / 1000);
      const infoIsCheckedAt = moment().add(remainingSeconds, 'seconds');
      const humanReadableMessage = moment().to(infoIsCheckedAt);
      if (options.feedbacks) {
        console.log(chalk.blue(`Checking feedbacks ${humanReadableMessage}`));
      }
      console.log(chalk.blue(`Updating queue information ${humanReadableMessage}\n`));
    }

    // Keyboard shortcuts helptext
    console.log('KEYBOARD SHORTCUTS:\n');
    console.log(chalk.green.dim(`  Press ${
      chalk.white('0')} to open the review dashboard.`));
    console.log(chalk.green.dim(`  Press ${
      chalk.white('1')} or ${chalk.white('2')} to open your assigned submissions.\n`));
    console.log(chalk.green.dim(`  Press ${
      chalk.white('ctrl+c')} to exit the queue cleanly by deleting the submission_request.`));
    console.log(chalk.green.dim(`  Press ${
      chalk.white('ESC')} to suspend the script without deleting the submission_request.\n`));
  }
}

function exit(key) {
  if (key === 'escape') {
    // Suspend on ESC and refresh the submission_request rather than deleting it.
    api({token, task: 'refresh', id: requestId});
    console.log(chalk.green('Exited without deleting the submission_request...'));
    console.log(chalk.green('The current submission_request will expire in an hour.'));
    process.exit(0);
  } else if (key === 'SIGINT') {
    // Delete submission_request object and exit on CTRL-C
    api({token, task: 'delete', id: requestId}).then(() => {
      console.log(chalk.green('Successfully deleted request and exited..'));
      process.exit(0);
    });
  } else {
    process.exit(0);
  }
}

function setEventListeners() {
  const baseReviewURL = 'https://review.udacity.com/#!/submissions/';
  readline.emitKeypressEvents(process.stdin);
  process.stdin.setRawMode(true);
  process.stdin.on('keypress', (str, key) => {
    switch (key.sequence) {
      case '\u001b': // ESCAPE
        exit('escape');
        break;
      case '\u0003': // CTRL-C
        exit('SIGINT');
        break;
      case 'r':
        checkAssigned();
        checkPositions();
        break;
      case '0':
        opn(`${baseReviewURL}dashboard`);
        break;
      case '1':
        if (assigned[0]) opn(`${baseReviewURL}${assigned[0].id}`);
        break;
      case '2':
        if (assigned[1]) opn(`${baseReviewURL}${assigned[1].id}`);
        break;
      default:
        break;
    }
  });
}

function validateProjectIds(ids) {
  const certIds = Object.keys(certs);
  if (ids[0] === 'all') {
    return certIds;
  }
  const invalidIds = ids.filter(id => certIds.indexOf(id) === -1);
  if (invalidIds.length) {
    throw new Error(`Illegal Action: Not certified for project(s) ${[...invalidIds].join(', ')}`);
  }
  return ids;
}

function validateAccessToken() {
  if (options.push) {
    accessToken = options.push;
    const pusher = new PushBullet(accessToken);
    // Throw an error if we find no active devices to push to.
    pusher.devices((err, res) => {
      if (err) throw new Error(`Pushbullet error: ${err}`);
      if (!res.devices.length) throw new Error('Found no active devices to push to.');
    });
  }
}

function createRequestBody() {
  // Create a list of project/language pairs
  requestBody.projects = [];
  languages.forEach((language) => {
    /* eslint-disable camelcase */
    projectIds.forEach((project_id) => {
      requestBody.projects.push({project_id, language});
    });
  });
}

function assignmentNotification(projectInfo, submissionId) {
  const {name, id} = projectInfo;
  const title = `New Review Assigned! (${assigned.length})`;
  const message = `${moment().format('HH:mm')} - ${name} (${id})`;
  const sound = 'Ping';
  const open = `https://review.udacity.com/#!/submissions/${submissionId}`;
  notifier.notify({title, message, sound, open});

  // If the --push flag is set we push to active PushBullet devices
  if (accessToken) {
    const pusher = new PushBullet(accessToken);
    pusher.note({}, title, `${message}\n\n${open}`, (err) => {
      if (err) throw new Error(`Pushbullet error: ${err}`);
    });
  }
}

async function checkAssigned() {
  const assignedResponse = await api({token, task: 'assigned'});
  const oldAssignedIds = assigned.map(s => s.id);
  assigned = assignedResponse.body;

  if (assignedResponse.body.length) {
    assigned
      .filter(s => oldAssignedIds.indexOf(s.id) === -1)
      .forEach((s) => {
        // Only add it to the total number of assigned if it's been assigned
        // after the command was initiated.
        if (Date.parse(s.assigned_at) > Date.parse(startTime)) {
          assignedTotal += 1;
        }
        assignmentNotification(s.project, s.id);
      });
  }
}

async function checkPositions() {
  const positionResponse = await api({token, task: 'position', id: requestId});
  positions = positionResponse.body.error ? [] : positionResponse.body;
  setPrompt();
}

async function createSubmissionRequest() {
  const createResponse = await api({token, task: 'create', body: requestBody});
  requestId = createResponse.body.id;
  checkPositions();
  // Reset tick to reset the timers.
  tick = 0;
}

// This gets called if a submission_request is already active when the assign
// command is run. If the new project ids, input by the user, do not equal the
// project ids in the existing submission_request, we update the request.
let needToUpdate = (submissionRequest) => {
  needToUpdate = () => false;
  const submissionRequestProjectIds = submissionRequest.submission_request_projects
    .map(p => p.project_id);

  if (submissionRequestProjectIds.length !== projectIds.length) return true;

  const invalidIds = submissionRequestProjectIds
    .map(id => id.toString())
    .filter(id => projectIds.indexOf(id) === -1);
  return invalidIds.length;
};

async function updateSubmissionRequest() {
  const updateResponse = await api({token, task: 'update', id: requestId, body: requestBody});
  requestId = updateResponse.body.id;
  checkPositions();
  // Reset tick to reset the timers.
  tick = 0;
}

async function checkRefresh(closedAt) {
  const closingIn = Date.parse(closedAt) - Date.now();
  // If it expires in less than 5 minutes we refresh.
  if (closingIn < 300000) {
    const task = 'refresh';
    const id = requestId;
    api({token, task, id});
  }
}

function feedbackNotification(rating, name, id) {
  const title = `New ${rating}-star Feedback!`;
  const message = `Project: ${name}`;
  const sound = 'Pop';
  const open = `https://review.udacity.com/#!/reviews/${id}`;
  notifier.notify({title, message, sound, open});
}

async function checkFeedbacks() {
  const stats = await api({token, task: 'stats'});
  const diff = stats.body.unread_count - unreadFeedbacks.length;
  if (diff > 0) {
    const feedbacksResponse = await api({token, task: 'feedbacks'});
    unreadFeedbacks = feedbacksResponse.body.filter(fb => fb.read_at === null);
    // Notify the user of the new feedbacks.
    unreadFeedbacks.slice(-diff).forEach((fb) => {
      feedbackNotification(fb.rating, fb.project.name, fb.submission_id);
    });
  } else if (diff < 0) {
    // Note: If you check your feedbacks in the Reviews dashboard the unread
    // count always goes to 0. Therefore we can assume that a negative
    // difference between the current unread_count and the number of elements
    // in unreadFeedbacks, will mean that the new unread_count is 0.
    unreadFeedbacks = [];
  }
}

async function submissionRequests() {
  // Call API to check how many submissions are currently assigned.
  try {
    const count = await api({token, task: 'count'});
    if (assigned.length !== count.body.assigned_count) {
      checkAssigned();
    }
    // If then the assigned.length is less than the maximum number of assignments
    // allowed, we go through checking the submission_request.
    if (assigned.length < 2) {
      const getResponse = await api({token, task: 'get'});
      const submissionRequest = getResponse.body[0];
      // If there is no current submission_request we create a new one.
      if (!submissionRequest) {
        createSubmissionRequest();
      } else {
        requestId = submissionRequest.id;

        if (needToUpdate(submissionRequest)) {
          updateSubmissionRequest();
        } else {
          // Refresh the submission_request if it's is about to expire.
          checkRefresh(submissionRequest.closed_at);
          // Check the queue positions and for new feedbacks.
          if (tick % infoInterval === 0) {
            checkPositions();
            if (options.feedbacks) {
              checkFeedbacks();
            }
          }
        }
      }
    }
  } catch (e) {
    error = e.error.code;
  }
  setTimeout(() => {
    tick += 1;
    setPrompt();
    submissionRequests();
  }, tickrate);
}

export const assignCmd = (ids, opts) => {
  projectIds = validateProjectIds(ids);
  options = opts;
  validateAccessToken();
  setEventListeners();
  createRequestBody();
  // Start the request loop.
  submissionRequests();
  setPrompt();
};
<|endoftext|>


<commit_before>function ready(fn) {
  if (document.readyState != 'loading') {
    fn();
  } else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}

function loadReusableComponents() {
  loadUQFavicon();

  //show notification bar if user is not logged in
  loadSigninNotification();

  rewriteLicenceContent();

  //first element of the original document
  var firstElement = document.body.children[0];

  // insert alerts after body-tag
  var alerts = document.querySelector('uqlibrary-alerts');
  if (!alerts) {
    //as a back up insert header if it's not defined already
    alerts = document.createElement('uqlibrary-alerts');
    document.body.insertBefore(alerts, firstElement);
  }

  // insert header after alerts
  var header = document.createElement('uq-minimal-header');
  document.body.insertBefore(header, firstElement);

  // insert sub footer before body-tag
  var subFooter = document.createElement('uql-connect-footer');
  document.body.appendChild(subFooter);

  // insert footer before body-tag
  var footer = document.createElement('uq-minimal-footer');
  document.body.appendChild(footer);

  window.addEventListener('WebComponentsReady', function () {
    // when polymer is ready - configure elements
    header.showLoginButton = false;
  });
}

function loadUQFavicon() {
  var link = document.createElement('link');
  link.type = 'image/x-icon';
  link.rel = 'shortcut icon';
  link.href = '//assets.library.uq.edu.au/reusable-components/resources/favicon.ico';
  document.getElementsByTagName('head')[0].appendChild(link);
  link.rel = 'icon'; //for IE
  document.getElementsByTagName('head')[0].appendChild(link);
}

/* Call primo defined method isUserLoggedIn() to check users login status, show the notification if the user is not logged in */
function loadSigninNotification() {
  if (isUserLoggedIn())
    return;
  if ($('#signInPrompt').length > 0) /* prevents a double sign-in on some pages */
    return;
  if ($('body').hasClass('MyAccount')) /* don't repeat sign-in prompt on E-Shelf page */
    return;

  /* get the login address from the signin link already generated by primo */
  var signInLink = $('#exlidSignIn a').attr('href');

  //put the notification bar in the feedback area which is sitting right after search bar
  var container = $('#exlidHeaderSystemFeedback');
  container.prepend('

Log in to access full text, more search results and more services
');
}

function rewriteLicenceContent() {

  // only needed in the result page
  if ($('#exlidResultsContainer .EXLResultsList')) {
    var resultList = $('#exlidResultsContainer .EXLResultsList');
    var summary = resultList.find('.EXLSummary');
    var viewItLinks = summary.find('.EXLResultTabs .EXLResultFirstTab a');
    if (viewItLinks) {
      viewItLinks.on('click', function(e) {
        console.log('link clicked');
        setTimeout(function(){
          console.log(summary.find('.EXLContainer-viewOnlineTab .EXLDetailsTabContent iframe'));
          if (summary.find('.EXLContainer-viewOnlineTab .EXLDetailsTabContent iframe').contents().find('.viewIt')){
            console.log('able to find content in the iframe');
          }
        }, 1000);

      });
    }
  }

  // Show license - a id = $id_show, div id = div_$id
}

ready(loadReusableComponents);
<commit_msg>remove testing code<commit_after>function ready(fn) {
  if (document.readyState != 'loading') {
    fn();
  } else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}

function loadReusableComponents() {
  loadUQFavicon();

  //show notification bar if user is not logged in
  loadSigninNotification();

  //first element of the original document
  var firstElement = document.body.children[0];

  // insert alerts after body-tag
  var alerts = document.querySelector('uqlibrary-alerts');
  if (!alerts) {
    //as a back up insert header if it's not defined already
    alerts = document.createElement('uqlibrary-alerts');
    document.body.insertBefore(alerts, firstElement);
  }

  // insert header after alerts
  var header = document.createElement('uq-minimal-header');
  document.body.insertBefore(header, firstElement);

  // insert sub footer before body-tag
  var subFooter = document.createElement('uql-connect-footer');
  document.body.appendChild(subFooter);

  // insert footer before body-tag
  var footer = document.createElement('uq-minimal-footer');
  document.body.appendChild(footer);

  window.addEventListener('WebComponentsReady', function () {
    // when polymer is ready - configure elements
    header.showLoginButton = false;
  });
}

function loadUQFavicon() {
  var link = document.createElement('link');
  link.type = 'image/x-icon';
  link.rel = 'shortcut icon';
  link.href = '//assets.library.uq.edu.au/reusable-components/resources/favicon.ico';
  document.getElementsByTagName('head')[0].appendChild(link);
  link.rel = 'icon'; //for IE
  document.getElementsByTagName('head')[0].appendChild(link);
}

/* Call primo defined method isUserLoggedIn() to check users login status, show the notification if the user is not logged in */
function loadSigninNotification() {
  if (isUserLoggedIn())
    return;
  if ($('#signInPrompt').length > 0) /* prevents a double sign-in on some pages */
    return;
  if ($('body').hasClass('MyAccount')) /* don't repeat sign-in prompt on E-Shelf page */
    return;

  /* get the login address from the signin link already generated by primo */
  var signInLink = $('#exlidSignIn a').attr('href');

  //put the notification bar in the feedback area which is sitting right after search bar
  var container = $('#exlidHeaderSystemFeedback');
  container.prepend('

Log in to access full text, more search results and more services
');
}

ready(loadReusableComponents);
<|endoftext|>


<commit_before>function initMap() {
    // add the map layer
    map = new L.Map("localmap");
    var min = 14;
    var max = 17;
    var myLayer = new L.TileLayer('http://tiles.buergerbautstadt.de/berlin/{z}/{x}/{y}.png', {
        minZoom: min,
        maxZoom: max,
        attribution: 'Map data © 2012 OpenStreetMap contributors',
        zIndex: 0,
        errorTileUrl: 'http://tiles.buergerbautstadt.de/error.png',
        reuseTiles: true
    });
    map.addLayer(myLayer);

    // center map
    var center = new L.LatLng(lat, lon);
    map.setView(center, 15);

    var icon = L.icon({
        iconUrl: staticUrl + '/img/Baustellenschilder/klein/schild_gruen_klein.png',
        iconSize:     [26, 45], // size of the icon width,height
        iconAnchor:   [13, 45], // point of the icon which will correspond to marker's location
    });

    var polygonOptions = {
        weight: 3,
        color: '#de6a00',
        opacity: 1,
        fill: false,
        fillColor: '#646464',
        fillOpacity: 0.5
    }

    if (typeof(polygon) !== 'undefined') {
        if (polygontype === 'Polygon') {
            L.polygon(polygon).setStyle(polygonOptions).addTo(map);
        } else if (polygontype === 'MultiPolygon') {
            L.multiPolygon(polygon).setStyle(polygonOptions).addTo(map);
        }
    }
    L.marker([lat,lon],{icon: icon}).addTo(map);

}

$(document).ready(function() {
    setTimeout('initMap()',100);
});<commit_msg>color<commit_after>function initMap() {
    // add the map layer
    map = new L.Map("localmap");
    var min = 14;
    var max = 17;
    var myLayer = new L.TileLayer('http://tiles.buergerbautstadt.de/berlin/{z}/{x}/{y}.png', {
        minZoom: min,
        maxZoom: max,
        attribution: 'Map data © 2012 OpenStreetMap contributors',
        zIndex: 0,
        errorTileUrl: 'http://tiles.buergerbautstadt.de/error.png',
        reuseTiles: true
    });
    map.addLayer(myLayer);

    // center map
    var center = new L.LatLng(lat, lon);
    map.setView(center, 15);

    var icon = L.icon({
        iconUrl: staticUrl + '/img/Baustellenschilder/klein/schild_gruen_klein.png',
        iconSize:     [26, 45], // size of the icon width,height
        iconAnchor:   [13, 45], // point of the icon which will correspond to marker's location
    });

    var polygonOptions = {
        weight: 3,
        color: '#de6a00',
        opacity: 1,
        fill: true,
        fillColor: '#de6a00',
        fillOpacity: 0.05
    }

    if (typeof(polygon) !== 'undefined') {
        if (polygontype === 'Polygon') {
            L.polygon(polygon).setStyle(polygonOptions).addTo(map);
        } else if (polygontype === 'MultiPolygon') {
            L.multiPolygon(polygon).setStyle(polygonOptions).addTo(map);
        }
    }
    L.marker([lat,lon],{icon: icon}).addTo(map);

}

$(document).ready(function() {
    setTimeout('initMap()',100);
});<|endoftext|>