<commit_before>$(function() {
	try {
		Notification.requestPermission();
	} catch (e) {

	}

	// オートセーブがあるなら復元
	if ($.cookie('post-autosave')) {
		$('#postForm textarea').val($.cookie('post-autosave'));
	}

	socket = io.connect('https://api.misskey.xyz:1207/streaming/home', { port: 1207 });

	socket.on('connected', function() {
		console.log('Connected');
	});

	socket.on('disconnect', function(client) {
	});

	socket.on('post', function(post) {
		console.log('post', post);
		var currentPath = location.pathname;
		if (currentPath != "i/mention") {
			new Audio('/resources/sounds/pop.mp3').play();
			var $post = TIMELINE.generatePostElement(post, conf).hide();
			TIMELINE.setEventPost($post);
			$post.prependTo($('#timeline .timeline > .statuses')).show(200);
		}
	});

	socket.on('repost', function(post) {
		console.log('repost', post);
		new Audio('/resources/sounds/pop.mp3').play();
		var $post = TIMELINE.generatePostElement(post, conf).hide();
		TIMELINE.setEventPost($post);
		$post.prependTo($('#timeline .timeline > .statuses')).show(200);
	});

	socket.on('reply', function(post) {
		console.log('reply', post);
		var n = new Notification(post.user.name, {
			body: post.text,
			icon: conf.url + '/img/icon/' + post.user.screenName
		});
		n.onshow = function() {
			setTimeout(function() {
				n.close();
			}, 10000);
		};
		n.onclick = function() {
			window.open(conf.url + '/' + post.user.screenName + '/post/' + post.id);
		};
	});

	socket.on('talkMessage', function(message) {
		console.log('talkMessage', message);
		var windowId = 'misskey-window-talk-' + message.user.id;
		if ($('#' + windowId)[0]) {
			return;
		}
		var n = new Notification(message.user.name, {
			body: message.text,
			icon: conf.url + '/img/icon/' + message.user.screenName
		});
		n.onshow = function() {
			setTimeout(function() {
				n.close();
			}, 10000);
		};
		n.onclick = function() {
			var url = 'https://misskey.xyz/' + message.user.screenName + '/talk?noheader=true';
			var $content = $("").attr({ src: url, seamless: true });
			openWindow(windowId, $content, '' + escapeHTML(message.user.name), 300, 450, true, url);
		};
	});

	$('#postForm').find('.imageAttacher input[name=image]').change(function() {
		var $input = $(this);
		var file = $(this).prop('files')[0];
		if (!file.type.match('image.*')) return;
		var reader = new FileReader();
		reader.onload = function() {
			var $img = $('').attr('src', reader.result);
			$input.parent('.imageAttacher').find('p, img').remove();
			$input.parent('.imageAttacher').append($img);
		};
		reader.readAsDataURL(file);
	});

	$('#postForm').submit(function(event) {
		event.preventDefault();
		var $form = $(this);
		var $submitButton = $form.find('[type=submit]');

		$submitButton.attr('disabled', true);
		$submitButton.text('Updating...');

		$.ajax('https://api.misskey.xyz/post/create', {
			type: 'post',
			processData: false,
			contentType: false,
			data: new FormData($form[0]),
			dataType: 'json',
			xhrFields: {
				withCredentials: true
			}
		}).done(function(data) {
			$form[0].reset();
			$form.find('textarea').focus();
			$form.find('.imageAttacher').find('p, img').remove();
			$form.find('.imageAttacher').append($('

'));
			$submitButton.attr('disabled', false);
			$submitButton.text('Update');
			$.removeCookie('post-autosave');
		}).fail(function(data) {
			$form[0].reset();
			$form.find('textarea').focus();
			/*alert('error');*/
			$submitButton.attr('disabled', false);
			$submitButton.text('Update');
		});
	});

	$('#postForm textarea').bind('input', function() {
		var text = $('#postForm textarea').val();

		// オートセーブ
		$.cookie('post-autosave', text, { path: '/', expires: 365 });
	});

	$('#timeline .loadMore').click(function() {
		$button = $(this);
		$button.attr('disabled', true);
		$button.text('Loading...');
		$.ajax('https://api.misskey.xyz/post/timeline', {
			type: 'get',
			data: { max_id: $('#timeline .timeline .statuses > .status:last-child').attr('data-id') },
			dataType: 'json',
			xhrFields: { withCredentials: true }
		}).done(function(data) {
			$button.attr('disabled', false);
			$button.text('Read more!');
			data.forEach(function(post) {
				var $post = TIMELINE.generatePostElement(post, conf).hide();
				TIMELINE.setEventPost($post);
				$post.appendTo($('#timeline .timeline > .statuses')).show(200);
			});
		}).fail(function(data) {
			$button.attr('disabled', false);
			$button.text('Failed...');
		});
	});
});
<commit_msg>Fix typo.<commit_after>$(function() {
	try {
		Notification.requestPermission();
	} catch (e) {

	}

	// オートセーブがあるなら復元
	if ($.cookie('post-autosave')) {
		$('#postForm textarea').val($.cookie('post-autosave'));
	}

	socket = io.connect('https://api.misskey.xyz:1207/streaming/home', { port: 1207 });

	socket.on('connected', function() {
		console.log('Connected');
	});

	socket.on('disconnect', function(client) {
	});

	socket.on('post', function(post) {
		console.log('post', post);
		var currentPath = location.pathname;
		if (currentPath != "/i/mention") {
			new Audio('/resources/sounds/pop.mp3').play();
			var $post = TIMELINE.generatePostElement(post, conf).hide();
			TIMELINE.setEventPost($post);
			$post.prependTo($('#timeline .timeline > .statuses')).show(200);
		}
	});

	socket.on('repost', function(post) {
		console.log('repost', post);
		new Audio('/resources/sounds/pop.mp3').play();
		var $post = TIMELINE.generatePostElement(post, conf).hide();
		TIMELINE.setEventPost($post);
		$post.prependTo($('#timeline .timeline > .statuses')).show(200);
	});

	socket.on('reply', function(post) {
		console.log('reply', post);
		var n = new Notification(post.user.name, {
			body: post.text,
			icon: conf.url + '/img/icon/' + post.user.screenName
		});
		n.onshow = function() {
			setTimeout(function() {
				n.close();
			}, 10000);
		};
		n.onclick = function() {
			window.open(conf.url + '/' + post.user.screenName + '/post/' + post.id);
		};
	});

	socket.on('talkMessage', function(message) {
		console.log('talkMessage', message);
		var windowId = 'misskey-window-talk-' + message.user.id;
		if ($('#' + windowId)[0]) {
			return;
		}
		var n = new Notification(message.user.name, {
			body: message.text,
			icon: conf.url + '/img/icon/' + message.user.screenName
		});
		n.onshow = function() {
			setTimeout(function() {
				n.close();
			}, 10000);
		};
		n.onclick = function() {
			var url = 'https://misskey.xyz/' + message.user.screenName + '/talk?noheader=true';
			var $content = $("").attr({ src: url, seamless: true });
			openWindow(windowId, $content, '' + escapeHTML(message.user.name), 300, 450, true, url);
		};
	});

	$('#postForm').find('.imageAttacher input[name=image]').change(function() {
		var $input = $(this);
		var file = $(this).prop('files')[0];
		if (!file.type.match('image.*')) return;
		var reader = new FileReader();
		reader.onload = function() {
			var $img = $('').attr('src', reader.result);
			$input.parent('.imageAttacher').find('p, img').remove();
			$input.parent('.imageAttacher').append($img);
		};
		reader.readAsDataURL(file);
	});

	$('#postForm').submit(function(event) {
		event.preventDefault();
		var $form = $(this);
		var $submitButton = $form.find('[type=submit]');

		$submitButton.attr('disabled', true);
		$submitButton.text('Updating...');

		$.ajax('https://api.misskey.xyz/post/create', {
			type: 'post',
			processData: false,
			contentType: false,
			data: new FormData($form[0]),
			dataType: 'json',
			xhrFields: {
				withCredentials: true
			}
		}).done(function(data) {
			$form[0].reset();
			$form.find('textarea').focus();
			$form.find('.imageAttacher').find('p, img').remove();
			$form.find('.imageAttacher').append($('

'));
			$submitButton.attr('disabled', false);
			$submitButton.text('Update');
			$.removeCookie('post-autosave');
		}).fail(function(data) {
			$form[0].reset();
			$form.find('textarea').focus();
			/*alert('error');*/
			$submitButton.attr('disabled', false);
			$submitButton.text('Update');
		});
	});

	$('#postForm textarea').bind('input', function() {
		var text = $('#postForm textarea').val();

		// オートセーブ
		$.cookie('post-autosave', text, { path: '/', expires: 365 });
	});

	$('#timeline .loadMore').click(function() {
		$button = $(this);
		$button.attr('disabled', true);
		$button.text('Loading...');
		$.ajax('https://api.misskey.xyz/post/timeline', {
			type: 'get',
			data: { max_id: $('#timeline .timeline .statuses > .status:last-child').attr('data-id') },
			dataType: 'json',
			xhrFields: { withCredentials: true }
		}).done(function(data) {
			$button.attr('disabled', false);
			$button.text('Read more!');
			data.forEach(function(post) {
				var $post = TIMELINE.generatePostElement(post, conf).hide();
				TIMELINE.setEventPost($post);
				$post.appendTo($('#timeline .timeline > .statuses')).show(200);
			});
		}).fail(function(data) {
			$button.attr('disabled', false);
			$button.text('Failed...');
		});
	});
});
<|endoftext|>


<commit_before>/**
 * util.js
 *
 * @auteur     marc laville
 * @Copyleft 2014
 * @date       31/01/2014
 * @version    0.1
 * @revision   $0$
 *
 * Quelques additions utiles en Javascript
 *
 *
 * Licensed under the GPL license:
 *   http://www.opensource.org/licenses/mit-license.php
 */

/*
 * Surcharge des objets Javascript
 */
/*
  * Classique troncature des espaces de part et d'autre d'une chaine
  */
String.prototype.trim = function(){return this.replace(/^\s+|\s+$/g, "");};
//                               -------

/*
 * L'Objet Date sait revoyer la liste des noms de mois
 */
Date.monthNames = Date.monthNames || function( ) {
//       ---------------
	var arrMonth = [],
		dateRef = new Date(),
		year = dateRef.getFullYear(),
        // Firefox don't support parametres, so we construct option to conform to Firefox format
        options = {weekday: "long", year: "numeric", month: "long", day: "numeric"};

	dateRef.setMonth(0);
	dateRef.setDate(10);
	while (year == dateRef.getFullYear()) {
		/* push le mois en lettre et passe au mois suivant */
		arrMonth.push( (dateRef.toLocaleString("fr-FR", options).split(' '))[2] );
		dateRef.setMonth( dateRef.getMonth() + 1);
	}

	return arrMonth;
}

/*
  * Espace de nommage
  */
var pxUtil = { };

/*
  * Gère la sélection d'un element input
  */
pxUtil.select = function( inputElt ) { return inputElt.setSelectionRange(0, inputElt.value.length); }

// Selection d'un champ sur un click
pxUtil.selectOnClick = function( event ) { return pxUtil.select( event.currentTarget ); };
//           --------------------
<commit_msg>Amelioration du support de la langue<commit_after>/**
 * util.js
 *
 * @auteur     marc laville
 * @Copyleft 2014
 * @date       31/01/2014
 * @version    0.1
 * @revision   $0$
 *
 * Quelques additions utiles en Javascript
 *
 *
 * Licensed under the GPL license:
 *   http://www.opensource.org/licenses/mit-license.php
 */

/*
  * Espace de nommage
  */
var pxUtil = { };

/*
  * Gère la sélection d'un element input
  */
pxUtil.select = function( inputElt ) { return inputElt.setSelectionRange(0, inputElt.value.length); }

// Selection d'un champ sur un click
pxUtil.selectOnClick = function( event ) { return pxUtil.select( event.currentTarget ); };
//           --------------------

pxUtil.toLocaleDateStringSupportsLocales = function () {
    try {
        new Date().toLocaleDateString("i");
    } catch (e) {
        return e.name === "RangeError";
    }
    return false;
}

/*
 * Surcharge des objets Javascript
 */
/*
  * Classique troncature des espaces de part et d'autre d'une chaine
  */
String.prototype.trim = function(){return this.replace(/^\s+|\s+$/g, "");};
//                               -------

/*
 * L'Objet Date sait revoyer la liste des noms de mois
 */

 Date.monthNames = Date.monthNames || function( locales, optMonth ) {
//       ---------------
	var arrMonth = [],
		dateRef = new Date(),
		year = dateRef.getFullYear(),
        // Firefox don't support parametres, so we construct option to conform to Firefox format
        options = {weekday: "long", year: "numeric", month: optMonth || "long", day: "numeric"},
		lang = pxUtil.toLocaleDateStringSupportsLocales() ? locales || window.navigator.language : window.navigator.language,
		indexMonth = 2; // Month position in the String returned to toLocaleString

	switch( (lang.split('-'))[0] ) {
		case 'en' : ;
		case 'ko' : indexMonth = 1;
			break ;
		case 'es' : indexMonth = 3;
			break ;
		default :
	}

	dateRef.setMonth(0);
	dateRef.setDate(10);
	while (year == dateRef.getFullYear()) {
		/* push le mois en lettre et passe au mois suivant */
		arrMonth.push( dateRef.toLocaleDateString( lang, options ).split(' ')[indexMonth] );
//		arrMonth.push( dateRef.toLocaleDateString( lang, options ) );
		dateRef.setMonth( dateRef.getMonth() + 1);
	}

	return arrMonth;
}

/*
 * L'Objet Date sait revoyer la liste des noms de jour
 * locales n'est pas supporté par FireFox
 */
Date.dayNames = Date.dayNames || function( locales ) {
//       ---------------
	var arrDay = [],
		dateRef = new Date(),
        // Firefox don't support parametres, so we construct option to conform to Firefox format
        options = {weekday: "long", year: "numeric", month: "long", day: "numeric"},
		lang = pxUtil.toLocaleDateStringSupportsLocales() ? locales || window.navigator.language : window.navigator.language,
		indexDay = 0; // Day position in the String returned to toLocaleString

	switch( (lang.split('-'))[0] ) {
		case 'ko' : indexDay = 3;
			break ;
		default :
	}

	dateRef.setDate( dateRef.getDate() - dateRef.getDay() ); // Now, dateRef.getDay() return 0
	for( var j = 0 ; j < 7 ; j++ ) {
		/* push le jour en lettre et passe au jour suivant */
		arrDay.push( dateRef.toLocaleString( lang, options).split(' ')[indexDay] );
		dateRef.setDate( dateRef.getDate() + 1);
	}

	return arrDay;
}

<|endoftext|>


<commit_before>$(function() {
	updateRelativeTimes();

	setInterval(function() {
		updateRelativeTimes();
	}, 100);

	function updateRelativeTimes() {
		var now = new Date();
		$('time').each(function() {
			function pad2(n) { return n < 10 ? '0' + n : n }
			var date = new Date($(this).attr('datetime'));
			var ago = ~~((now - date) / 1000);
			var timeText =
				ago >= 31536000 ? ~~(ago / 31536000) + "NO" :
				ago >= 2592000 ? ~~(ago / 2592000) + "O" :
				ago >= 604800 ? ~~(ago / 604800) + "TԑO" :
				ago >= 86400 ? ~~(ago / 86400) + "O" :
				ago >= 3600 ? ~~(ago / 3600) + "ԑO" :
				ago >= 60 ? ~~(ago / 60) + "O" :
				ago >= 5 ? ~~(ago % 60) + "bO" :
				ago < 5 ? '' : "";
			$(this).text(timeText);
		});
	}
});

function openWindow($content, title, width, height) {
	var $window = $("\

\
			\

"+ title + "
\
				<button class=\"close\"></button>\
			\

\

\
	").css({
		width: width,
		height: height
	});
	$window.find(".content").append($content);
	$("body").prepend($window);

	function top() {
		var z = 0;
		$(".window").each(function() {
			if ($(this).css("z-index") > z)
				z = Number($(this).css("z-index"));
		});
		$window.css("z-index", z + 1);
	}

	function close() {
		$window.css({
			"transform": "perspective(512px) rotateX(22.5deg) scale(0.9)",
			"opacity": "0",
			"transition": "all ease-in 0.3s"
		});
		setTimeout(function() {
			$window.remove();
		}, 300);
	}

	$window.ready(function() {
		top();

		/*$window.css({
			"top": ($(window).scrollTop() + (($(window).height() / 2) - ($window.outerHeight() / 2) + ((Math.random() * 128) - 64))) + "px",
			"left": (($(window).width() / 2) - ($window.outerWidth() / 2) + ((Math.random() * 128) - 64)) + "px",
		});*/
		$window.css({
			"bottom": (($(window).height() / 2) - ($window.height() / 2) + ((Math.random() * 128) - 64)) + "px",
			"right": (($(window).width() / 2) - ($window.width() / 2) + ((Math.random() * 128) - 64)) + "px",
		});
		$window.animate({
			"opacity": "1",
			"transform": "scale(1)",
		}, 200);
	});

	$window.find("header > .close").click(function() {
		close();
	});

	$window.mousedown(function() {
		top();
	});

	$window.find("header").mousedown(function(e) {
		$window.find(".content").css({
			'pointer-events': 'none',
			'user-select': 'none'
		});

		var position = $window.position();

		var clickX = e.clientX;
		var clickY = e.clientY;
		var moveBaseX = clickX - position.left;
		var moveBaseY = clickY - position.top;
		var browserWidth = $(window).width();
		var browserHeight = $(window).height();
		var windowWidth = $window.outerWidth();
		var windowHeight = $window.outerHeight();

		$("html").mousemove(function(me) {
			var moveRight = browserWidth - ((windowWidth + me.clientX) - moveBaseX);
			var moveBottom = browserHeight - ((windowHeight + me.clientY) - moveBaseY);
			if (moveRight < 0) moveRight = 0;
			if (moveBottom < 0) moveBottom = 0;
			if (moveRight + windowWidth > browserWidth) moveRight = browserWidth - windowWidth;
			if (moveBottom + windowHeight > browserHeight) moveBottom = browserHeight - windowHeight;

			$window.css({
				"right": moveRight + "px",
				"bottom": moveBottom + "px",
			});
		});
		$("html").mouseleave(function() {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});
		$("html").mouseup(function() {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});
		$("html").bind("dragstart", function(e) {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});
		$("html").bind("dragend", function(e) {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});

		function endMove() {
			$window.find(".content").css({
				'pointer-events': 'auto',
				'user-select': 'auto'
			});
		}
	});

	$(window).resize(function() {
		var position = $window.position();
		var browserWidth = $(window).width();
		var browserHeight = $(window).height();
		var windowWidth = $window.outerWidth();
		var windowHeight = $window.outerHeight();

		if (position.left < 0) {
			$window.css({
				"right": (browserWidth - windowWidth) + "px"
			});
		}
		if (position.top < 0) {
			$window.css({
				"bottom": (browserHeight - windowHeight) + "px"
			});
		}
		if (position.left + windowWidth > browserWidth) {
			$window.css({
				"right": "0px"
			});
		}
		if (position.top + windowHeight > browserHeight) {
			$window.css({
				"bottom": "0px"
			});
		}
	});
}<commit_msg>Fix file encoding<commit_after>$(function() {
	updateRelativeTimes();

	setInterval(function() {
		updateRelativeTimes();
	}, 100);

	function updateRelativeTimes() {
		var now = new Date();
		$('time').each(function() {
			function pad2(n) { return n < 10 ? '0' + n : n }
			var date = new Date($(this).attr('datetime'));
			var ago = ~~((now - date) / 1000);
			var timeText =
				ago >= 31536000 ? ~~(ago / 31536000) + "年前" :
				ago >= 2592000 ? ~~(ago / 2592000) + "ヶ月前" :
				ago >= 604800 ? ~~(ago / 604800) + "週間前" :
				ago >= 86400 ? ~~(ago / 86400) + "日前" :
				ago >= 3600 ? ~~(ago / 3600) + "時間前" :
				ago >= 60 ? ~~(ago / 60) + "分前" :
				ago >= 5 ? ~~(ago % 60) + "秒前" :
				ago < 5 ? 'いま' : "";
			$(this).text(timeText);
		});
	}
});

function openWindow($content, title, width, height) {
	var $window = $("\

\
			\

"+ title + "
\
				<button class=\"close\"></button>\
			\

\

\
	").css({
		width: width,
		height: height
	});
	$window.find(".content").append($content);
	$("body").prepend($window);

	function top() {
		var z = 0;
		$(".window").each(function() {
			if ($(this).css("z-index") > z)
				z = Number($(this).css("z-index"));
		});
		$window.css("z-index", z + 1);
	}

	function close() {
		$window.css({
			"transform": "perspective(512px) rotateX(22.5deg) scale(0.9)",
			"opacity": "0",
			"transition": "all ease-in 0.3s"
		});
		setTimeout(function() {
			$window.remove();
		}, 300);
	}

	$window.ready(function() {
		top();

		/*$window.css({
			"top": ($(window).scrollTop() + (($(window).height() / 2) - ($window.outerHeight() / 2) + ((Math.random() * 128) - 64))) + "px",
			"left": (($(window).width() / 2) - ($window.outerWidth() / 2) + ((Math.random() * 128) - 64)) + "px",
		});*/
		$window.css({
			"bottom": (($(window).height() / 2) - ($window.height() / 2) + ((Math.random() * 128) - 64)) + "px",
			"right": (($(window).width() / 2) - ($window.width() / 2) + ((Math.random() * 128) - 64)) + "px",
		});
		$window.animate({
			"opacity": "1",
			"transform": "scale(1)",
		}, 200);
	});

	$window.find("header > .close").click(function() {
		close();
	});

	$window.mousedown(function() {
		top();
	});

	$window.find("header").mousedown(function(e) {
		$window.find(".content").css({
			'pointer-events': 'none',
			'user-select': 'none'
		});

		var position = $window.position();

		var clickX = e.clientX;
		var clickY = e.clientY;
		var moveBaseX = clickX - position.left;
		var moveBaseY = clickY - position.top;
		var browserWidth = $(window).width();
		var browserHeight = $(window).height();
		var windowWidth = $window.outerWidth();
		var windowHeight = $window.outerHeight();

		$("html").mousemove(function(me) {
			var moveRight = browserWidth - ((windowWidth + me.clientX) - moveBaseX);
			var moveBottom = browserHeight - ((windowHeight + me.clientY) - moveBaseY);
			if (moveRight < 0) moveRight = 0;
			if (moveBottom < 0) moveBottom = 0;
			if (moveRight + windowWidth > browserWidth) moveRight = browserWidth - windowWidth;
			if (moveBottom + windowHeight > browserHeight) moveBottom = browserHeight - windowHeight;

			$window.css({
				"right": moveRight + "px",
				"bottom": moveBottom + "px",
			});
		});
		$("html").mouseleave(function() {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});
		$("html").mouseup(function() {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});
		$("html").bind("dragstart", function(e) {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});
		$("html").bind("dragend", function(e) {
			$(this).unbind("mouseup mousemove mouseleave");
			endMove();
		});

		function endMove() {
			$window.find(".content").css({
				'pointer-events': 'auto',
				'user-select': 'auto'
			});
		}
	});

	$(window).resize(function() {
		var position = $window.position();
		var browserWidth = $(window).width();
		var browserHeight = $(window).height();
		var windowWidth = $window.outerWidth();
		var windowHeight = $window.outerHeight();

		if (position.left < 0) {
			$window.css({
				"right": (browserWidth - windowWidth) + "px"
			});
		}
		if (position.top < 0) {
			$window.css({
				"bottom": (browserHeight - windowHeight) + "px"
			});
		}
		if (position.left + windowWidth > browserWidth) {
			$window.css({
				"right": "0px"
			});
		}
		if (position.top + windowHeight > browserHeight) {
			$window.css({
				"bottom": "0px"
			});
		}
	});
}<|endoftext|>


<commit_before>var gmail3 = function () {
  var gmail3 = {};
  var msgConsumers = [];

  function addMsgConsumer(msgConsumer) {
    msgConsumers.push(msgConsumer);
  }

  function isEmpty(db, sheet) {
    var rows = objDB.getRows(db, sheet, [], {}, 1);
    if (rows.length === 0) {
      return true;
    } else {
      return false;
    }
  }

  function emailReport(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Sending last months report via Email');
    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        msgConsumers[i].graphFunction(scriptState);
      }
    }
    var htmlBody = "";
    var inlineImages = {};
    var blob;

    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        htmlBody += "
\n";
        blob = msgConsumers[i].chart.getAs('image/png').setName(msgConsumers[i].name + ".png");
        inlineImages[i] = blob;
      }
    }

    MailApp.sendEmail({
      to: Session.getEffectiveUser().getEmail(),
      subject: "[gmail3] - " + "Gmail usage analysis for " + scriptState.monthToProcess.format('MMMM YYYY'),
      htmlBody: htmlBody,
      inlineImages: inlineImages
    });
    scriptState.reportEmailed = true;
  }

  function createSpreadSheet(monthToProcess) {
    var ssNew = SpreadsheetApp.create("gmail3 Data - " + monthToProcess.format('MMM YYYY'));
    var ssID = ssNew.getUrl().split('/')[7];
    // This is needed so we can use the Google Visualization API Query Language
    // to set the data source URL for graphs
    allowPublicViewing(ssID);
    return ssID;
  }

  function initializeSpreadSheet(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    for(i = 0; i < msgConsumers.length; i += 1) {
      msgConsumers[i].initSpreadSheet(ss);
    }
  }

  function populateData(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Populating Data');
    // Process up to 100 gmail threads at once
    var batchSize = 100;
    if (scriptState.debugEnabled) {
      // If we have debugEnabled only process 20, so runs are quicker
      batchSize = 20;
    }
    var after = scriptState.monthToProcess.clone().startOf('month').format('YYYY/MM/DD');
    var before = scriptState.monthToProcess.clone().endOf('month').add(1, 'days').format('YYYY/MM/DD');
    var query = "in:anywhere -label:sms -label:call-log -label:chats -label:spam -filename:ics";
    query += " -from:maestro.bounces.google.com -from:unified-notifications.bounces.google.com -from:docs.google.com";
    query += " -from:group.calendar.google.com -from:apps-scripts-notifications@google.com";
    query += " -from:sites.bounces.google.com";
    query += " after:" + after + " before:" + before;

    Logger.log('Searching: ' + query);
    var threads = GmailApp.search(query, scriptState.threadIndex, batchSize);
    if (scriptState.debugEnabled && scriptState.threadIndex >= 40) {
      scriptState.dataPopulated = true;
    } else if (threads.length === 0) {
      scriptState.dataPopulated = true;
    } else {
      for (thread = 0; thread < threads.length; thread += 1) {
        var msg = threads[thread].getMessages()[0];
        for(var i = 0; i < msgConsumers.length; i += 1) {
          if (scriptState.debugEnabled) {
            Logger.log('Calling Message function for, ' + msgConsumers[i].name);
          }
          try {
            msgConsumers[i].msgFunction(msg, db);
          } catch (e) {
            throw new Error("msgConsumer, " + msgConsumers[i].name + ", threw error: " + e.message);
          }
        }
      }
    }
    scriptState.threadIndex += batchSize;
    Logger.log('PopulateData run complete: threads: ' + threads.length + ', threadIndex: ' + scriptState.threadIndex);
  }

  function setScriptState(scriptState) {
    var scriptStateString = JSON.stringify(scriptState);
    PropertiesService.getUserProperties().setProperty("scriptState", scriptStateString);
    Logger.log("New scriptState set: " + scriptStateString);
  }

  function getScriptState() {
    var scriptState = null;
    var nextMonthToProcess;
    var scriptStateString = PropertiesService.getUserProperties().getProperty("scriptState");
    var debugEnabledUserProperty = PropertiesService.getUserProperties().getProperty("debugEnabled");
    if (debugEnabledUserProperty === "true") {
      debugEnabled = true;
      Logger.log("Debugging enabled");
    } else {
      debugEnabled = false;
      Logger.log("Debugging disabled");
    }
    Logger.log("Retrieved scriptState: " + scriptStateString);
    if (scriptStateString === null) {
      Logger.log('No stored scriptState, creating an empty one');
      scriptState = {};
      scriptState.debugEnabled = debugEnabled;
      var currentMonth = moment().startOf('month');
      if (scriptState.debugEnabled) {
        nextMonthToProcess = currentMonth.clone().startOf('year');
      } else {
        nextMonthToProcess = currentMonth.clone().subtract(1, 'days').startOf('month');
      }
      scriptState.ssID = createSpreadSheet(nextMonthToProcess);
      initializeSpreadSheet(scriptState);
      scriptState.monthToProcess = nextMonthToProcess;
      scriptState.dataPopulated = false;
      scriptState.reportEmailed = false;
      scriptState.threadIndex = 0;
      scriptState.triggers = [];
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    } else {
      scriptState = JSON.parse(scriptStateString);
      scriptState.debugEnabled = debugEnabled;
      scriptState.monthToProcess = moment(scriptState.monthToProcess);
      scriptState.currentDay = moment(scriptState.currentDay);
    }

    // Reset the running total, if it is a new day
    if (! moment().startOf('day').isSame(scriptState.currentDay)) {
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    }

    // Create a daily trigger if it does not exist
    dailyTrigger = _.find(scriptState.triggers, function (trigger) {
      return trigger.type === "atHour";
    });
    if (dailyTrigger === undefined) {
      Logger.log("No daily trigger, adding one");
      trigger = ScriptApp.newTrigger("main")
      .timeBased()
      .atHour(1)
      .everyDays(1)
      .create();
      scriptState.triggers.push({
        type: "atHour",
        uniqueId: trigger.getUniqueId(),
      });
    }

    return scriptState;
  }

  function deleteSheet1(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    if (ss.getSheetByName("Sheet1") !== null) {
      ss.deleteSheet(ss.getSheetByName("Sheet1"));
    }
  }

  function incrementMonth(scriptState) {
    Logger.log('Processing done for the current month, incrementing the month');
    var nextMonthToProcess = scriptState.monthToProcess.clone()
    .endOf('month')
    .add(1, 'days')
    .startOf('month');
    // This may not suceeed so do it first, before changing the script state
    scriptState.ssID = createSpreadSheet(nextMonthToProcess);
    initializeSpreadSheet(scriptState);
    scriptState.monthToProcess = nextMonthToProcess;
    scriptState.dataPopulated = false;
    scriptState.reportEmailed = false;
    scriptState.threadIndex = 0;
    scriptState.dailyRunningTimeTotalSeconds = 0;
    scriptState.currentDay = moment().startOf('day');

    return scriptState;
  }

  function deleteAtTriggers(scriptState) {
    Logger.log("Deleting all existing at triggers");
    var trigger;
    function uniqueIdMatchProjectTrigger(projectTrigger) {
      return function (trigger) {
        return trigger.uniqueId === projectTrigger.getUniqueId();
      };
    }
    function uniqueIdMatch(uniqueId) {
      return function (trigger) {
        return trigger.uniqueId === uniqueId;
      };
    }
    var projectTriggers = ScriptApp.getProjectTriggers();
    for (var i = 0; i < projectTriggers.length; i++) {
      trigger = _.find(scriptState.triggers, uniqueIdMatchProjectTrigger(projectTriggers[i]));
      if (trigger === undefined) {
        // We have no record of this trigger, so delete it
        ScriptApp.deleteTrigger(projectTriggers[i]);
      } else {
        if (trigger.type === "at") {
          var uniqueId = projectTriggers[i].getUniqueId();
          ScriptApp.deleteTrigger(projectTriggers[i]);
          scriptState.triggers = _.reject(scriptState.triggers, uniqueIdMatch(uniqueId));
        }
      }
    }
  }

  function setupNextAtTrigger(scriptState) {
    // Delete any previous at triggers, so we don't exceed the trigger quota
    deleteAtTriggers(scriptState);

    // If less than daily run time, 3 hours, and there is still time left in the
    // day, add a new trigger
    if (scriptState.dailyRunningTimeTotalSeconds < (170 * 60) &&
      moment().add(35, 'minutes').date() === moment().date()) {
      Logger.log("Scheduling a new at trigger at: " + moment().add(15, "minutes").toISOString());
      trigger = ScriptApp.newTrigger("main").timeBased().after(15 * 60 * 1000).create();
      scriptState.triggers.push({
        type: "at",
        uniqueId: trigger.getUniqueId(),
      });
    } else {
      Logger.log("Not scheduling any further triggers for today to avoid exceeding execution quota of 3 hours");
      Logger.log("Running time total today " + (scriptState.dailyRunningTimeTotalSeconds / 60) + " minutes");
    }
  }

  function extractEmailAddress(emailAddress) {
    var emailAddressRegex = /^(?:.*<)?([^<>]+)(?:>)?$/;
    return emailAddressRegex.exec(emailAddress)[1].toLowerCase();
  }

  function unfoldHeaders(foldedHeaders) {
    var unfoldRegex = /\r\n[ \t]+/g;
    return foldedHeaders.replace(unfoldRegex, " ");
  }

  function extractHeaders(rawMessage) {
    var headers = {};
    var parseHeaderRegex = /^[ \n\t]*([^:]+):[ \t]*(.*)$/;
    var extractHeadersRegex = /\r\n\r\n/;
    var printableRegex = /[^ -~]+/g;
    var foldedHeaders = rawMessage.split(extractHeadersRegex)[0];
    var unfoldedHeaders = unfoldHeaders(foldedHeaders);
    var headerLines = unfoldedHeaders.split(/\r\n/);
    for (var i = 0; i < headerLines.length; i++) {
      var printableHeader = headerLines[i].replace(printableRegex, "");
      var headerMatch = parseHeaderRegex.exec(printableHeader);
      if (headerMatch === null) {
        throw new Error("headerMatch returned null");
      }
      if (headerMatch[1] !== "") {
        var headerField = headerMatch[1].toLowerCase();
        if (headers.hasOwnProperty(headerField)) {
          headers[headerField].push(headerMatch[2]);
        } else {
          headers[headerField] = [headerMatch[2]];
        }
      }
    }
    return headers;
  }

  function allowPublicViewing(ID) {
    file = DriveApp.getFileById(ID);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  }

  // These are admittedly swiss cheese tests for robots, but I am at loss to
  // find better methods.
  function emailSentByRobot(rawMessage) {
    var msgFromRobot = false;
    var headers = extractHeaders(rawMessage);
    var from = extractEmailAddress(headers.from);
    var i;
    var robotMailers = [
      /\s\(Postfix/,
      /mailgun.net/,
    ];
    var fromRobots = [
      /^no-?reply@.*$/,
      /^do-?not-?reply@.*$/,
      /^alert@pingdom.com$/,
    ];
    if (headers.hasOwnProperty("received")) {
      var firstHop = headers.received[headers.received.length - 1];
      for (i = 0; i < robotMailers.length; i++) {
        if (robotMailers[i].test(firstHop)) {
          msgFromRobot = true;
          break;
        }
      }
    }
    for (i = 0; i < fromRobots.length; i++) {
      if (fromRobots[i].test(from)) {
        msgFromRobot = true;
        break;
      }
    }
    return msgFromRobot;
  }

  function main() {
    var lock = LockService.getUserLock();
    if (!lock.tryLock(1000)) {
      Logger.log('Could not obtain lock after a second');
      return;
    }
    var currentMonth = moment().startOf('month');
    var scriptStartTime = moment();
    var scriptState = null;

    try {
      scriptState = getScriptState();
    } catch (e) {
      Logger.log('Error: ' + e.message);
      return;
    }

    if (scriptState.monthToProcess.isBefore(currentMonth)) {
      Logger.log("Beginning processing of: gmail3 - " + scriptState.monthToProcess.format('MMM YYYY'));
      if (scriptState.dataPopulated) {
        if (scriptState.reportEmailed) {
          try {
            deleteSheet1(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to delete Sheet1 for the previous month: ' + e.message);
          }
          try {
            incrementMonth(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to increment month: ' + e.message);
          }
        } else {
          try {
            emailReport(scriptState, msgConsumers);
          } catch (e) {
            Logger.log('Error: Unable to email report for the previous month: ' + e.message);
          }
        }
      } else {
        try {
          populateData(scriptState, msgConsumers);
        } catch (e) {
          Logger.log('Error: Unable to populate data: ' + e.message);
        }
      }
      scriptState.dailyRunningTimeTotalSeconds += moment().diff(scriptStartTime, 'seconds');
      try {
        setupNextAtTrigger(scriptState);
      } catch (e) {
        Logger.log('Error: Unable to schedule next at trigger: ' + e.message);
      }
    } else {
      Logger.log('Previous month processed complete, nothing todo');
    }

    for (var i = 0; i < 5; i++) {
      try {
        setScriptState(scriptState);
        break;
      } catch (e) {
        Logger.log('Error: Unable to set script state: ' + e.message);
      }
    }
    lock.releaseLock();
  }

  gmail3.emailReport = emailReport;
  gmail3.addMsgConsumer = addMsgConsumer;
  gmail3.createSpreadSheet = createSpreadSheet;
  gmail3.initializeSpreadSheet = initializeSpreadSheet;
  gmail3.isEmpty = isEmpty;
  gmail3.populateData = populateData;
  gmail3.setScriptState = setScriptState;
  gmail3.getScriptState = getScriptState;
  gmail3.deleteSheet1 = deleteSheet1;
  gmail3.incrementMonth = incrementMonth;
  gmail3.deleteAtTriggers = deleteAtTriggers;
  gmail3.setupNextAtTrigger = setupNextAtTrigger;
  gmail3.extractEmailAddress = extractEmailAddress;
  gmail3.unfoldHeaders = unfoldHeaders;
  gmail3.extractHeaders = extractHeaders;
  gmail3.allowPublicViewing = allowPublicViewing;
  gmail3.emailSentByRobot = emailSentByRobot;
  gmail3.main = main;

  return gmail3;
}();
<commit_msg>Add sendgrid.net to robots<commit_after>var gmail3 = function () {
  var gmail3 = {};
  var msgConsumers = [];

  function addMsgConsumer(msgConsumer) {
    msgConsumers.push(msgConsumer);
  }

  function isEmpty(db, sheet) {
    var rows = objDB.getRows(db, sheet, [], {}, 1);
    if (rows.length === 0) {
      return true;
    } else {
      return false;
    }
  }

  function emailReport(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Sending last months report via Email');
    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        msgConsumers[i].graphFunction(scriptState);
      }
    }
    var htmlBody = "";
    var inlineImages = {};
    var blob;

    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        htmlBody += "
\n";
        blob = msgConsumers[i].chart.getAs('image/png').setName(msgConsumers[i].name + ".png");
        inlineImages[i] = blob;
      }
    }

    MailApp.sendEmail({
      to: Session.getEffectiveUser().getEmail(),
      subject: "[gmail3] - " + "Gmail usage analysis for " + scriptState.monthToProcess.format('MMMM YYYY'),
      htmlBody: htmlBody,
      inlineImages: inlineImages
    });
    scriptState.reportEmailed = true;
  }

  function createSpreadSheet(monthToProcess) {
    var ssNew = SpreadsheetApp.create("gmail3 Data - " + monthToProcess.format('MMM YYYY'));
    var ssID = ssNew.getUrl().split('/')[7];
    // This is needed so we can use the Google Visualization API Query Language
    // to set the data source URL for graphs
    allowPublicViewing(ssID);
    return ssID;
  }

  function initializeSpreadSheet(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    for(i = 0; i < msgConsumers.length; i += 1) {
      msgConsumers[i].initSpreadSheet(ss);
    }
  }

  function populateData(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Populating Data');
    // Process up to 100 gmail threads at once
    var batchSize = 100;
    if (scriptState.debugEnabled) {
      // If we have debugEnabled only process 20, so runs are quicker
      batchSize = 20;
    }
    var after = scriptState.monthToProcess.clone().startOf('month').format('YYYY/MM/DD');
    var before = scriptState.monthToProcess.clone().endOf('month').add(1, 'days').format('YYYY/MM/DD');
    var query = "in:anywhere -label:sms -label:call-log -label:chats -label:spam -filename:ics";
    query += " -from:maestro.bounces.google.com -from:unified-notifications.bounces.google.com -from:docs.google.com";
    query += " -from:group.calendar.google.com -from:apps-scripts-notifications@google.com";
    query += " -from:sites.bounces.google.com";
    query += " after:" + after + " before:" + before;

    Logger.log('Searching: ' + query);
    var threads = GmailApp.search(query, scriptState.threadIndex, batchSize);
    if (scriptState.debugEnabled && scriptState.threadIndex >= 40) {
      scriptState.dataPopulated = true;
    } else if (threads.length === 0) {
      scriptState.dataPopulated = true;
    } else {
      for (thread = 0; thread < threads.length; thread += 1) {
        var msg = threads[thread].getMessages()[0];
        for(var i = 0; i < msgConsumers.length; i += 1) {
          if (scriptState.debugEnabled) {
            Logger.log('Calling Message function for, ' + msgConsumers[i].name);
          }
          try {
            msgConsumers[i].msgFunction(msg, db);
          } catch (e) {
            throw new Error("msgConsumer, " + msgConsumers[i].name + ", threw error: " + e.message);
          }
        }
      }
    }
    scriptState.threadIndex += batchSize;
    Logger.log('PopulateData run complete: threads: ' + threads.length + ', threadIndex: ' + scriptState.threadIndex);
  }

  function setScriptState(scriptState) {
    var scriptStateString = JSON.stringify(scriptState);
    PropertiesService.getUserProperties().setProperty("scriptState", scriptStateString);
    Logger.log("New scriptState set: " + scriptStateString);
  }

  function getScriptState() {
    var scriptState = null;
    var nextMonthToProcess;
    var scriptStateString = PropertiesService.getUserProperties().getProperty("scriptState");
    var debugEnabledUserProperty = PropertiesService.getUserProperties().getProperty("debugEnabled");
    if (debugEnabledUserProperty === "true") {
      debugEnabled = true;
      Logger.log("Debugging enabled");
    } else {
      debugEnabled = false;
      Logger.log("Debugging disabled");
    }
    Logger.log("Retrieved scriptState: " + scriptStateString);
    if (scriptStateString === null) {
      Logger.log('No stored scriptState, creating an empty one');
      scriptState = {};
      scriptState.debugEnabled = debugEnabled;
      var currentMonth = moment().startOf('month');
      if (scriptState.debugEnabled) {
        nextMonthToProcess = currentMonth.clone().startOf('year');
      } else {
        nextMonthToProcess = currentMonth.clone().subtract(1, 'days').startOf('month');
      }
      scriptState.ssID = createSpreadSheet(nextMonthToProcess);
      initializeSpreadSheet(scriptState);
      scriptState.monthToProcess = nextMonthToProcess;
      scriptState.dataPopulated = false;
      scriptState.reportEmailed = false;
      scriptState.threadIndex = 0;
      scriptState.triggers = [];
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    } else {
      scriptState = JSON.parse(scriptStateString);
      scriptState.debugEnabled = debugEnabled;
      scriptState.monthToProcess = moment(scriptState.monthToProcess);
      scriptState.currentDay = moment(scriptState.currentDay);
    }

    // Reset the running total, if it is a new day
    if (! moment().startOf('day').isSame(scriptState.currentDay)) {
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    }

    // Create a daily trigger if it does not exist
    dailyTrigger = _.find(scriptState.triggers, function (trigger) {
      return trigger.type === "atHour";
    });
    if (dailyTrigger === undefined) {
      Logger.log("No daily trigger, adding one");
      trigger = ScriptApp.newTrigger("main")
      .timeBased()
      .atHour(1)
      .everyDays(1)
      .create();
      scriptState.triggers.push({
        type: "atHour",
        uniqueId: trigger.getUniqueId(),
      });
    }

    return scriptState;
  }

  function deleteSheet1(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    if (ss.getSheetByName("Sheet1") !== null) {
      ss.deleteSheet(ss.getSheetByName("Sheet1"));
    }
  }

  function incrementMonth(scriptState) {
    Logger.log('Processing done for the current month, incrementing the month');
    var nextMonthToProcess = scriptState.monthToProcess.clone()
    .endOf('month')
    .add(1, 'days')
    .startOf('month');
    // This may not suceeed so do it first, before changing the script state
    scriptState.ssID = createSpreadSheet(nextMonthToProcess);
    initializeSpreadSheet(scriptState);
    scriptState.monthToProcess = nextMonthToProcess;
    scriptState.dataPopulated = false;
    scriptState.reportEmailed = false;
    scriptState.threadIndex = 0;
    scriptState.dailyRunningTimeTotalSeconds = 0;
    scriptState.currentDay = moment().startOf('day');

    return scriptState;
  }

  function deleteAtTriggers(scriptState) {
    Logger.log("Deleting all existing at triggers");
    var trigger;
    function uniqueIdMatchProjectTrigger(projectTrigger) {
      return function (trigger) {
        return trigger.uniqueId === projectTrigger.getUniqueId();
      };
    }
    function uniqueIdMatch(uniqueId) {
      return function (trigger) {
        return trigger.uniqueId === uniqueId;
      };
    }
    var projectTriggers = ScriptApp.getProjectTriggers();
    for (var i = 0; i < projectTriggers.length; i++) {
      trigger = _.find(scriptState.triggers, uniqueIdMatchProjectTrigger(projectTriggers[i]));
      if (trigger === undefined) {
        // We have no record of this trigger, so delete it
        ScriptApp.deleteTrigger(projectTriggers[i]);
      } else {
        if (trigger.type === "at") {
          var uniqueId = projectTriggers[i].getUniqueId();
          ScriptApp.deleteTrigger(projectTriggers[i]);
          scriptState.triggers = _.reject(scriptState.triggers, uniqueIdMatch(uniqueId));
        }
      }
    }
  }

  function setupNextAtTrigger(scriptState) {
    // Delete any previous at triggers, so we don't exceed the trigger quota
    deleteAtTriggers(scriptState);

    // If less than daily run time, 3 hours, and there is still time left in the
    // day, add a new trigger
    if (scriptState.dailyRunningTimeTotalSeconds < (170 * 60) &&
      moment().add(35, 'minutes').date() === moment().date()) {
      Logger.log("Scheduling a new at trigger at: " + moment().add(15, "minutes").toISOString());
      trigger = ScriptApp.newTrigger("main").timeBased().after(15 * 60 * 1000).create();
      scriptState.triggers.push({
        type: "at",
        uniqueId: trigger.getUniqueId(),
      });
    } else {
      Logger.log("Not scheduling any further triggers for today to avoid exceeding execution quota of 3 hours");
      Logger.log("Running time total today " + (scriptState.dailyRunningTimeTotalSeconds / 60) + " minutes");
    }
  }

  function extractEmailAddress(emailAddress) {
    var emailAddressRegex = /^(?:.*<)?([^<>]+)(?:>)?$/;
    return emailAddressRegex.exec(emailAddress)[1].toLowerCase();
  }

  function unfoldHeaders(foldedHeaders) {
    var unfoldRegex = /\r\n[ \t]+/g;
    return foldedHeaders.replace(unfoldRegex, " ");
  }

  function extractHeaders(rawMessage) {
    var headers = {};
    var parseHeaderRegex = /^[ \n\t]*([^:]+):[ \t]*(.*)$/;
    var extractHeadersRegex = /\r\n\r\n/;
    var printableRegex = /[^ -~]+/g;
    var foldedHeaders = rawMessage.split(extractHeadersRegex)[0];
    var unfoldedHeaders = unfoldHeaders(foldedHeaders);
    var headerLines = unfoldedHeaders.split(/\r\n/);
    for (var i = 0; i < headerLines.length; i++) {
      var printableHeader = headerLines[i].replace(printableRegex, "");
      var headerMatch = parseHeaderRegex.exec(printableHeader);
      if (headerMatch === null) {
        throw new Error("headerMatch returned null");
      }
      if (headerMatch[1] !== "") {
        var headerField = headerMatch[1].toLowerCase();
        if (headers.hasOwnProperty(headerField)) {
          headers[headerField].push(headerMatch[2]);
        } else {
          headers[headerField] = [headerMatch[2]];
        }
      }
    }
    return headers;
  }

  function allowPublicViewing(ID) {
    file = DriveApp.getFileById(ID);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  }

  // These are admittedly swiss cheese tests for robots, but I am at loss to
  // find better methods.
  function emailSentByRobot(rawMessage) {
    var msgFromRobot = false;
    var headers = extractHeaders(rawMessage);
    var from = extractEmailAddress(headers.from);
    var i;
    var robotMailers = [
      /\s\(Postfix/,
      /mailgun.net/,
      /sendgrid.net/,
    ];
    var fromRobots = [
      /^no-?reply@.*$/,
      /^do-?not-?reply@.*$/,
      /^alert@pingdom.com$/,
    ];
    if (headers.hasOwnProperty("received")) {
      var firstHop = headers.received[headers.received.length - 1];
      for (i = 0; i < robotMailers.length; i++) {
        if (robotMailers[i].test(firstHop)) {
          msgFromRobot = true;
          break;
        }
      }
    }
    for (i = 0; i < fromRobots.length; i++) {
      if (fromRobots[i].test(from)) {
        msgFromRobot = true;
        break;
      }
    }
    return msgFromRobot;
  }

  function main() {
    var lock = LockService.getUserLock();
    if (!lock.tryLock(1000)) {
      Logger.log('Could not obtain lock after a second');
      return;
    }
    var currentMonth = moment().startOf('month');
    var scriptStartTime = moment();
    var scriptState = null;

    try {
      scriptState = getScriptState();
    } catch (e) {
      Logger.log('Error: ' + e.message);
      return;
    }

    if (scriptState.monthToProcess.isBefore(currentMonth)) {
      Logger.log("Beginning processing of: gmail3 - " + scriptState.monthToProcess.format('MMM YYYY'));
      if (scriptState.dataPopulated) {
        if (scriptState.reportEmailed) {
          try {
            deleteSheet1(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to delete Sheet1 for the previous month: ' + e.message);
          }
          try {
            incrementMonth(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to increment month: ' + e.message);
          }
        } else {
          try {
            emailReport(scriptState, msgConsumers);
          } catch (e) {
            Logger.log('Error: Unable to email report for the previous month: ' + e.message);
          }
        }
      } else {
        try {
          populateData(scriptState, msgConsumers);
        } catch (e) {
          Logger.log('Error: Unable to populate data: ' + e.message);
        }
      }
      scriptState.dailyRunningTimeTotalSeconds += moment().diff(scriptStartTime, 'seconds');
      try {
        setupNextAtTrigger(scriptState);
      } catch (e) {
        Logger.log('Error: Unable to schedule next at trigger: ' + e.message);
      }
    } else {
      Logger.log('Previous month processed complete, nothing todo');
    }

    for (var i = 0; i < 5; i++) {
      try {
        setScriptState(scriptState);
        break;
      } catch (e) {
        Logger.log('Error: Unable to set script state: ' + e.message);
      }
    }
    lock.releaseLock();
  }

  gmail3.emailReport = emailReport;
  gmail3.addMsgConsumer = addMsgConsumer;
  gmail3.createSpreadSheet = createSpreadSheet;
  gmail3.initializeSpreadSheet = initializeSpreadSheet;
  gmail3.isEmpty = isEmpty;
  gmail3.populateData = populateData;
  gmail3.setScriptState = setScriptState;
  gmail3.getScriptState = getScriptState;
  gmail3.deleteSheet1 = deleteSheet1;
  gmail3.incrementMonth = incrementMonth;
  gmail3.deleteAtTriggers = deleteAtTriggers;
  gmail3.setupNextAtTrigger = setupNextAtTrigger;
  gmail3.extractEmailAddress = extractEmailAddress;
  gmail3.unfoldHeaders = unfoldHeaders;
  gmail3.extractHeaders = extractHeaders;
  gmail3.allowPublicViewing = allowPublicViewing;
  gmail3.emailSentByRobot = emailSentByRobot;
  gmail3.main = main;

  return gmail3;
}();
<|endoftext|>


<commit_before>/* global Player, Projectile, Zombie, Explosion, ObjectPoolMaker, FlashMessage */

(function () {
  'use strict';

  var Game = {
    init: function () {
      Game.canvas = document.getElementById('game-canvas');
      Game.context = Game.canvas.getContext('2d');

      Game.loadBackground();
      Game.bind();
      Game.reset();
    },

    hasLeveled: function (level) {
      return Game.score > Game.neededScoreForLevel(level + 1);
    },

    neededScoreForLevel: function (level) {
      return Math.floor(Math.pow(level, 3) * 100) + 1000;
    },

    bind: function () {
      document.addEventListener('keydown', function (e) {
        Game.keysDown[e.keyCode] = true;
      }, false);

      document.addEventListener('keyup', function (e) {
        delete Game.keysDown[e.keyCode];
      }, false);

      document.getElementById('play-again').addEventListener('click', function () {
        Game.reset();
      });
    },

    reset: function () {
      // Current game difficulty
      Game.difficulty = 0.01;

      // Max difficulty that Game can reach at any given time
      // It represents the chance percentage of spawning a new Zombie
      Game.maxDifficulty = 0.42;

      // How much difficulty increases after its cooldown elapsed
      Game.difficultyIncrement = 0.005;

      // How long it takes (in seconds) to difficulty to increase
      Game.difficultyCooldown = 4;

      // Last time difficulty was increased
      Game.lastDifficultyIncrease = 0;

      // Time elapsed since Game began
      Game.gameTime = 0;

      // Total score from killing zombies
      Game.score = 0;

      // Maps which keys as currently pressed by e.keyCode
      Game.keysDown = {};

      // Last time a projectile was shot by the player
      Game.lastProjectileTime = 0;

      // How long it takes (in ms) for another projectile to be fired
      Game.projectileCooldown = 225;

      // How much projectile speed improves after each level
      Game.projectileCooldownImprove = 25;

      // Maximum projectile speed (less is faster)
      Game.minProjectileCooldown = 100;

      // Define how many levels it takes to increase projectile power by 1
      // (huge difference)
      Game.powerUpProjectileIn = 10;

      // How many pixels Player and Zombies are constrained by vertically/horizontally
      // (this is interesting according to the background image used)
      Game.verticalBoundary = 35;
      Game.horizontalBoundary = 5;

      Game.isGameOver = false;

      Game.player = new Player(
        Game.canvas.width / 2 - Player.width / 2,
        Game.canvas.height / 2 - Player.height / 2
      );

      // Store pool of objects to be reused for each Entity
      Game.zombiePool = new ObjectPoolMaker(Zombie, 100);
      Game.projectilePool = new ObjectPoolMaker(Projectile, 100);

      // Got lazy here and just used an array.. soon to be updated :p
      Game.explosions = [];
      Game.flashMessages = [];

      document.getElementById('game-over-overlay').style.display = 'none';
      document.getElementById('game-over').style.display = 'none';
    },

    loadBackground: function () {
      Game.backgroundReady = false;

      Game.backgroundImg = new Image();
      Game.backgroundImg.src = 'public/images/background.png';

      Game.backgroundImg.onload = function () {
        Game.backgroundReady = true;
      };
    },

    update: function (modifier) {
      Game.gameTime += modifier;

      Game.handleInput(modifier);
      Game.handleDifficulty();
      Game.spawnZombies();
      Game.updateEntities(modifier);
    },

    handleInput: function (modifier) {
      if (Game.isGameOver) {
        return;
      }

      var now = Date.now();

      // UP
      if (Game.keysDown[38] && Game.player.y > Game.verticalBoundary) {
        Game.player.setY(Game.player.y - (Game.player.speed * modifier));
      }

      // DOWN
      if (Game.keysDown[40] && Game.player.y < Game.canvas.height - Game.verticalBoundary - Player.height) {
        Game.player.setY(Game.player.y + (Game.player.speed * modifier));
      }

      // RIGHT
      if (Game.keysDown[39] && Game.player.x < Game.canvas.width - Game.horizontalBoundary - Player.width) {
        Game.player.setX(Game.player.x + (Game.player.speed * modifier));
      }

      // LEFT
      if (Game.keysDown[37] && Game.player.x > Game.horizontalBoundary) {
        Game.player.setX(Game.player.x - (Game.player.speed * modifier));
      }

      // SPACE - shoot!
      if (Game.keysDown[32]) {
        // prevent spamming projectiles
        if (now - Game.lastProjectileTime > Game.projectileCooldown) {
          Game.projectilePool.create(
            Game.player.x,
            Game.player.y + (Player.height / 2) - (Projectile.height / 2),
            400,
            Math.floor(Game.player.level / Game.powerUpProjectileIn) + 1
          );
          Game.lastProjectileTime = now;
        }
      }
    },

    handleDifficulty: function () {
      // It gets harder as time goes by..
      if (Game.difficulty < Game.maxDifficulty &&
          Game.gameTime - Game.lastDifficultyIncrease > Game.difficultyCooldown) {
        Game.difficulty += Game.difficultyIncrement;
        Game.lastDifficultyIncrease = Game.gameTime;
      }
    },

    spawnZombies: function () {
      // Create some enemies
      if (Math.random() < Game.difficulty) {
        // resets x coordinate
        // randomize new y coordinate (constrained in available map space)
        Game.zombiePool.create(
          Game.canvas.width,
          Math.random() * (Game.canvas.height - Zombie.height - (2 * Game.verticalBoundary)) + Game.verticalBoundary
        );
      }
    },

    updateEntities: function (modifier) {
      Game.updateProjectilesAndKillZombies(modifier);
      Game.updateZombies(modifier);
      Game.updateExplosions();
      Game.updatePlayer();
      Game.updateFlashMessages(modifier);
    },

    updateProjectilesAndKillZombies: function (modifier) {
      var i,
          j,
          zombie,
          projectile;

      // update all projectiles
      for (i = 0; i < Game.projectilePool.size(); i++) {
        projectile = Game.projectilePool.objectPool()[i];

        projectile.setX(projectile.x + (projectile.speed * modifier));

        // delete projectile if out of the scene
        if (projectile.x > Game.canvas.width) {
          Game.projectilePool.destroy(projectile);
          i--;
        }
        else {
          // kill zombies!
          for (j = 0; j < Game.zombiePool.size(); j++) {
            zombie = Game.zombiePool.objectPool()[j];
            if (projectile.isCollided(zombie)) {
              Game.explosions.push(new Explosion(zombie.x, zombie.y));

              // hundred points for each zombie!
              Game.score += 100;

              // Decrease projectile power and
              projectile.power--;
              // destroy projectile if it has no more power
              if (projectile.power === 0) {
                Game.projectilePool.destroy(projectile);
                i--;
              }

              Game.flashMessages.push(new FlashMessage(
                '+ 100',
                zombie.x,
                zombie.y,
                500
              ));
              Game.zombiePool.destroy(zombie);
              j--;

              break;
            }
          }
        }
      }
    },

    updateZombies: function (modifier) {
      var zombie,
          i,
          now = Date.now();

      // update all enemies
      for (i = 0; i < Game.zombiePool.size(); i++) {
        zombie = Game.zombiePool.objectPool()[i];
        zombie.update(now);
        zombie.setX(zombie.x - (zombie.speed * modifier));

        // delete zombie if out of the scene
        if (zombie.x + Zombie.width < 0) {
          Game.zombiePool.destroy(zombie);
          i--;
        }
      }
    },

    updateExplosions: function () {
      var i;

      for (i = 0; i < Game.explosions.length; ++i) {
        Game.explosions[i].update();
        if (Game.explosions[i].currentFrame >= Explosion.framesPosition.length - 1) {
          Game.explosions.splice(i--, 1);
        }
      }
    },

    updatePlayer: function () {
      var zombie,
          i;

      if (!Game.isGameOver) {
        for (i = 0; i < Game.zombiePool.size(); ++i) {
          zombie = Game.zombiePool.objectPool()[i];
          if (zombie.isCollided(Game.player)) {
            Game.gameOver();
            break;
          }
        }
      }

      // Level up!
      if (Game.hasLeveled(Game.player.level)) {
        Game.player.level += 1;
        Game.flashMessages.push(new FlashMessage(
          'Level ' + Game.player.level,
          Game.player.x,
          Game.player.y,
          700,
          '#E2E215',
          '18px'
        ));

        // power up ?
        if (Game.player.level % 10 === 0) {
          Game.flashMessages.push(new FlashMessage(
            'Power Up!',
            Game.player.x,
            Game.player.y - Player.height / 2,
            1000,
            '#34FAC3',
            '16px'
          ));
        }

        Game.projectileCooldown -= Game.projectileCooldownImprove;
        if (Game.projectileCooldown < Game.minProjectileCooldown) {
          Game.projectileCooldown = Game.minProjectileCooldown;
        }
      }
    },

    updateFlashMessages: function (modifier) {
      var i,
          now = Date.now();

      for (i = 0; i < Game.flashMessages.length; ++i) {
        Game.flashMessages[i].y -= modifier * 10;
        if (now - Game.flashMessages[i].createdAt > Game.flashMessages[i].duration) {
          Game.flashMessages.splice(i--, 1);
        }
      }
    },

    gameOver: function () {
      document.getElementById('game-over-overlay').style.display = 'block';
      document.getElementById('game-over').style.display = 'block';
      Game.isGameOver = true;
    },

    render: function () {
      var i;

      // Render Background
      if (Game.backgroundReady) {
        Game.context.drawImage(Game.backgroundImg, 0, 0);
      }

      // Render Projectiles
      for (i = 0; i < Game.projectilePool.size(); ++i) {
        Game.projectilePool.objectPool()[i].render(Game.context);
      }

      // Render Explosions
      for (i = 0; i < Game.explosions.length; ++i) {
        Game.explosions[i].render(Game.context);
      }

      // Render Zombies
      for (i = 0; i < Game.zombiePool.size(); ++i) {
        Game.zombiePool.objectPool()[i].render(Game.context);
      }

      // Render Player
      if (!Game.isGameOver) {
        Game.player.render(Game.context);
      }

      // Render Flash messages
      for (i = 0; i < Game.flashMessages.length; ++i) {
        Game.flashMessages[i].render(Game.context);
      }

      // Render score
      Game.renderStatus();
    },

    renderStatus: function () {
      Game.context.fillStyle = '#fff';
      Game.context.font = '20px Helvetica';
      Game.context.textAlign = 'left';
      Game.context.textBaseline = 'top';

      Game.context.fillText('Score: ' + Game.score, 10, 5);
      Game.context.fillText('Level: ' + Game.player.level, Game.canvas.width - 110, 5);
    }
  };

  // expose Game globally
  window.Game = Game;
}());
<commit_msg>Update explosion array to an explosionPool as Zombies and projectiles<commit_after>/* global Player, Projectile, Zombie, Explosion, ObjectPoolMaker, FlashMessage */

(function () {
  'use strict';

  var Game = {
    init: function () {
      Game.canvas = document.getElementById('game-canvas');
      Game.context = Game.canvas.getContext('2d');

      Game.loadBackground();
      Game.bind();
      Game.reset();
    },

    hasLeveled: function (level) {
      return Game.score > Game.neededScoreForLevel(level + 1);
    },

    neededScoreForLevel: function (level) {
      return Math.floor(Math.pow(level, 3) * 100) + 1000;
    },

    bind: function () {
      document.addEventListener('keydown', function (e) {
        Game.keysDown[e.keyCode] = true;
      }, false);

      document.addEventListener('keyup', function (e) {
        delete Game.keysDown[e.keyCode];
      }, false);

      document.getElementById('play-again').addEventListener('click', function () {
        Game.reset();
      });
    },

    reset: function () {
      // Current game difficulty
      Game.difficulty = 0.01;

      // Max difficulty that Game can reach at any given time
      // It represents the chance percentage of spawning a new Zombie
      Game.maxDifficulty = 0.42;

      // How much difficulty increases after its cooldown elapsed
      Game.difficultyIncrement = 0.005;

      // How long it takes (in seconds) to difficulty to increase
      Game.difficultyCooldown = 4;

      // Last time difficulty was increased
      Game.lastDifficultyIncrease = 0;

      // Time elapsed since Game began
      Game.gameTime = 0;

      // Total score from killing zombies
      Game.score = 0;

      // Maps which keys as currently pressed by e.keyCode
      Game.keysDown = {};

      // Last time a projectile was shot by the player
      Game.lastProjectileTime = 0;

      // How long it takes (in ms) for another projectile to be fired
      Game.projectileCooldown = 225;

      // How much projectile speed improves after each level
      Game.projectileCooldownImprove = 25;

      // Maximum projectile speed (less is faster)
      Game.minProjectileCooldown = 100;

      // Define how many levels it takes to increase projectile power by 1
      // (huge difference)
      Game.powerUpProjectileIn = 10;

      // How many pixels Player and Zombies are constrained by vertically/horizontally
      // (this is interesting according to the background image used)
      Game.verticalBoundary = 35;
      Game.horizontalBoundary = 5;

      Game.isGameOver = false;

      Game.player = new Player(
        Game.canvas.width / 2 - Player.width / 2,
        Game.canvas.height / 2 - Player.height / 2
      );

      // Store pool of objects to be reused for each Entity
      Game.zombiePool = new ObjectPoolMaker(Zombie, 100);
      Game.projectilePool = new ObjectPoolMaker(Projectile, 100);
      Game.explosionPool = new ObjectPoolMaker(Explosion, 50);
      Game.flashMessages = [];

      document.getElementById('game-over-overlay').style.display = 'none';
      document.getElementById('game-over').style.display = 'none';
    },

    loadBackground: function () {
      Game.backgroundReady = false;

      Game.backgroundImg = new Image();
      Game.backgroundImg.src = 'public/images/background.png';

      Game.backgroundImg.onload = function () {
        Game.backgroundReady = true;
      };
    },

    update: function (modifier) {
      Game.gameTime += modifier;

      Game.handleInput(modifier);
      Game.handleDifficulty();
      Game.spawnZombies();
      Game.updateEntities(modifier);
    },

    handleInput: function (modifier) {
      if (Game.isGameOver) {
        return;
      }

      var now = Date.now();

      // UP
      if (Game.keysDown[38] && Game.player.y > Game.verticalBoundary) {
        Game.player.setY(Game.player.y - (Game.player.speed * modifier));
      }

      // DOWN
      if (Game.keysDown[40] && Game.player.y < Game.canvas.height - Game.verticalBoundary - Player.height) {
        Game.player.setY(Game.player.y + (Game.player.speed * modifier));
      }

      // RIGHT
      if (Game.keysDown[39] && Game.player.x < Game.canvas.width - Game.horizontalBoundary - Player.width) {
        Game.player.setX(Game.player.x + (Game.player.speed * modifier));
      }

      // LEFT
      if (Game.keysDown[37] && Game.player.x > Game.horizontalBoundary) {
        Game.player.setX(Game.player.x - (Game.player.speed * modifier));
      }

      // SPACE - shoot!
      if (Game.keysDown[32]) {
        // prevent spamming projectiles
        if (now - Game.lastProjectileTime > Game.projectileCooldown) {
          Game.projectilePool.create(
            Game.player.x,
            Game.player.y + (Player.height / 2) - (Projectile.height / 2),
            400,
            Math.floor(Game.player.level / Game.powerUpProjectileIn) + 1
          );
          Game.lastProjectileTime = now;
        }
      }
    },

    handleDifficulty: function () {
      // It gets harder as time goes by..
      if (Game.difficulty < Game.maxDifficulty &&
          Game.gameTime - Game.lastDifficultyIncrease > Game.difficultyCooldown) {
        Game.difficulty += Game.difficultyIncrement;
        Game.lastDifficultyIncrease = Game.gameTime;
      }
    },

    spawnZombies: function () {
      // Create some enemies
      if (Math.random() < Game.difficulty) {
        // resets x coordinate
        // randomize new y coordinate (constrained in available map space)
        Game.zombiePool.create(
          Game.canvas.width,
          Math.random() * (Game.canvas.height - Zombie.height - (2 * Game.verticalBoundary)) + Game.verticalBoundary
        );
      }
    },

    updateEntities: function (modifier) {
      Game.updateProjectilesAndKillZombies(modifier);
      Game.updateZombies(modifier);
      Game.updateExplosions();
      Game.updatePlayer();
      Game.updateFlashMessages(modifier);
    },

    updateProjectilesAndKillZombies: function (modifier) {
      var i,
          j,
          zombie,
          projectile;

      // update all projectiles
      for (i = 0; i < Game.projectilePool.size(); i++) {
        projectile = Game.projectilePool.objectPool()[i];

        projectile.setX(projectile.x + (projectile.speed * modifier));

        // delete projectile if out of the scene
        if (projectile.x > Game.canvas.width) {
          Game.projectilePool.destroy(projectile);
          i--;
        }
        else {
          // kill zombies!
          for (j = 0; j < Game.zombiePool.size(); j++) {
            zombie = Game.zombiePool.objectPool()[j];
            if (projectile.isCollided(zombie)) {
              Game.explosionPool.create(zombie.x, zombie.y);

              // hundred points for each zombie!
              Game.score += 100;

              // Decrease projectile power and
              projectile.power--;
              // destroy projectile if it has no more power
              if (projectile.power === 0) {
                Game.projectilePool.destroy(projectile);
                i--;
              }

              Game.flashMessages.push(new FlashMessage(
                '+ 100',
                zombie.x,
                zombie.y,
                500
              ));
              Game.zombiePool.destroy(zombie);
              j--;

              break;
            }
          }
        }
      }
    },

    updateZombies: function (modifier) {
      var zombie,
          i,
          now = Date.now();

      // update all enemies
      for (i = 0; i < Game.zombiePool.size(); i++) {
        zombie = Game.zombiePool.objectPool()[i];
        zombie.update(now);
        zombie.setX(zombie.x - (zombie.speed * modifier));

        // delete zombie if out of the scene
        if (zombie.x + Zombie.width < 0) {
          Game.zombiePool.destroy(zombie);
          i--;
        }
      }
    },

    updateExplosions: function () {
      var i,
          explosion;

      for (i = 0; i < Game.explosionPool.size(); ++i) {
        explosion = Game.explosionPool.objectPool()[i];
        explosion.update();
        if (explosion.currentFrame >= Explosion.framesPosition.length - 1) {
          Game.explosionPool.destroy(explosion);
        }
      }
    },

    updatePlayer: function () {
      var zombie,
          i;

      if (!Game.isGameOver) {
        for (i = 0; i < Game.zombiePool.size(); ++i) {
          zombie = Game.zombiePool.objectPool()[i];
          if (zombie.isCollided(Game.player)) {
            Game.gameOver();
            break;
          }
        }
      }

      // Level up!
      if (Game.hasLeveled(Game.player.level)) {
        Game.player.level += 1;
        Game.flashMessages.push(new FlashMessage(
          'Level ' + Game.player.level,
          Game.player.x,
          Game.player.y,
          700,
          '#E2E215',
          '18px'
        ));

        // power up ?
        if (Game.player.level % 10 === 0) {
          Game.flashMessages.push(new FlashMessage(
            'Power Up!',
            Game.player.x,
            Game.player.y - Player.height / 2,
            1000,
            '#34FAC3',
            '16px'
          ));
        }

        Game.projectileCooldown -= Game.projectileCooldownImprove;
        if (Game.projectileCooldown < Game.minProjectileCooldown) {
          Game.projectileCooldown = Game.minProjectileCooldown;
        }
      }
    },

    updateFlashMessages: function (modifier) {
      var i,
          now = Date.now();

      for (i = 0; i < Game.flashMessages.length; ++i) {
        Game.flashMessages[i].y -= modifier * 10;
        if (now - Game.flashMessages[i].createdAt > Game.flashMessages[i].duration) {
          Game.flashMessages.splice(i--, 1);
        }
      }
    },

    gameOver: function () {
      document.getElementById('game-over-overlay').style.display = 'block';
      document.getElementById('game-over').style.display = 'block';
      Game.isGameOver = true;
    },

    render: function () {
      var i;

      // Render Background
      if (Game.backgroundReady) {
        Game.context.drawImage(Game.backgroundImg, 0, 0);
      }

      // Render Projectiles
      for (i = 0; i < Game.projectilePool.size(); ++i) {
        Game.projectilePool.objectPool()[i].render(Game.context);
      }

      // Render Explosions
      for (i = 0; i < Game.explosionPool.size(); ++i) {
        Game.explosionPool.objectPool()[i].render(Game.context);
      }

      // Render Zombies
      for (i = 0; i < Game.zombiePool.size(); ++i) {
        Game.zombiePool.objectPool()[i].render(Game.context);
      }

      // Render Player
      if (!Game.isGameOver) {
        Game.player.render(Game.context);
      }

      // Render Flash messages
      for (i = 0; i < Game.flashMessages.length; ++i) {
        Game.flashMessages[i].render(Game.context);
      }

      // Render score
      Game.renderStatus();
    },

    renderStatus: function () {
      Game.context.fillStyle = '#fff';
      Game.context.font = '20px Helvetica';
      Game.context.textAlign = 'left';
      Game.context.textBaseline = 'top';

      Game.context.fillText('Score: ' + Game.score, 10, 5);
      Game.context.fillText('Level: ' + Game.player.level, Game.canvas.width - 110, 5);
    }
  };

  // expose Game globally
  window.Game = Game;
}());
<|endoftext|>