<commit_before>'use strict';

var	async = require('async');
var winston = module.parent.require('winston');
var XRegExp = require('xregexp');
var validator = require('validator');
var nconf = module.parent.require('nconf');

var db = require.main.require('./src/database');
var Topics = require.main.require('./src/topics');
var User = require.main.require('./src/user');
var Groups = require.main.require('./src/groups');
var Notifications = require.main.require('./src/notifications');
var Privileges = require.main.require('./src/privileges');
var Meta = require.main.require('./src/meta');
var Utils = require.main.require('./public/src/utils');
var batch = require.main.require('./src/batch');

var SocketPlugins = require.main.require('./src/socket.io/plugins');

const utility = require('./lib/utility');

var regex = XRegExp('(?:^|\\s|\\>|;)(@[\\p{L}\\d\\-_.]+)', 'g');
var isLatinMention = /@[\w\d\-_.]+$/;
var removePunctuationSuffix = function(string) {
	return string.replace(/[!?.]*$/, '');
};
var Entities = require('html-entities').XmlEntities;
var entities = new Entities();

var Mentions = {
	_settings: {},
	_defaults: {
		disableFollowedTopics: 'off',
		autofillGroups: 'off',
		disableGroupMentions: '[]',
		overrideIgnores: 'off',
		display: '',
	}
};
SocketPlugins.mentions = {};

Mentions.init = function (data, callback) {
	var hostMiddleware = require.main.require('./src/middleware');
	var controllers = require('./controllers');

	data.router.get('/admin/plugins/mentions', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
	data.router.get('/api/admin/plugins/mentions', controllers.renderAdminPage);

	// Retrieve settings
	Meta.settings.get('mentions', function (err, settings) {
		Object.assign(Mentions._settings, Mentions._defaults, settings);
		callback();
	});
};

Mentions.addAdminNavigation = function (header, callback) {
	header.plugins.push({
		route: '/plugins/mentions',
		name: 'Mentions'
	});

	callback(null, header);
};

function getNoMentionGroups() {
	var noMentionGroups = ['registered-users', 'guests'];
	try {
		noMentionGroups = noMentionGroups.concat(JSON.parse(Mentions._settings.disableGroupMentions));
	} catch (err) {
		winston.error(err);
	}
	return noMentionGroups;
}

Mentions.notify = function(data) {
	var postData = data.post;
	var cleanedContent = Mentions.clean(postData.content, true, true, true);
	var matches = cleanedContent.match(regex);

	if (!matches) {
		return;
	}

	var noMentionGroups = getNoMentionGroups();

	matches = matches.map(function(match) {
		return Utils.slugify(match);
	}).filter(function(match, index, array) {
		return match && array.indexOf(match) === index && noMentionGroups.indexOf(match) === -1;
	});

	if (!matches.length) {
		return;
	}

	async.parallel({
		userRecipients: function(next) {
			async.filter(matches, User.existsBySlug, next);
		},
		groupRecipients: function(next) {
			async.filter(matches, Groups.existsBySlug, next);
		}
	}, function(err, results) {
		if (err) {
			return;
		}

		if (!results.userRecipients.length && !results.groupRecipients.length) {
			return;
		}

		async.parallel({
			topic: function(next) {
				Topics.getTopicFields(postData.tid, ['title', 'cid'], next);
			},
			author: function(next) {
				User.getUserField(postData.uid, 'username', next);
			},
			uids: function(next) {
				async.map(results.userRecipients, function(slug, next) {
					User.getUidByUserslug(slug, next);
				}, next);
			},
			groupData: function(next) {
				getGroupMemberUids(results.groupRecipients, next);
			},
			topicFollowers: function(next) {
				if (Mentions._settings.disableFollowedTopics === 'on') {
					Topics.getFollowers(postData.tid, next);
				} else {
					next(null, []);
				}
			}
		}, function(err, results) {
			if (err) {
				return;
			}

			var title = entities.decode(results.topic.title);
			var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');

			var uids = results.uids.filter(function(uid, index, array) {
				return array.indexOf(uid) === index && parseInt(uid, 10) !== parseInt(postData.uid, 10) && !results.topicFollowers.includes(uid.toString());
			});

			var groupMemberUids = {};
			results.groupData.groupNames.forEach(function(groupName, index) {
				results.groupData.groupMembers[index] = results.groupData.groupMembers[index].filter(function(uid) {
					if (!uid || groupMemberUids[uid]) {
						return false;
					}
					groupMemberUids[uid] = 1;
					return !uids.includes(uid) &&
						parseInt(uid, 10) !== parseInt(postData.uid, 10) &&
						!results.topicFollowers.includes(uid.toString());
				});
			});

			sendNotificationToUids(postData, uids, 'user', '[[notifications:user_mentioned_you_in, ' + results.author + ', ' + titleEscaped + ']]');

			results.groupData.groupNames.forEach(function(groupName, index) {
				var memberUids = results.groupData.groupMembers[index];
				sendNotificationToUids(postData, memberUids, groupName, '[[notifications:user_mentioned_group_in, ' + results.author + ', ' + groupName + ', ' + titleEscaped + ']]');
			});
		});
	});
};

Mentions.addFilters = function (data, callback) {
	data.regularFilters.push({ name: '[[notifications:mentions]]', filter: 'mention' });
	callback(null, data);
};

Mentions.notificationTypes = function (data, callback) {
	data.types.push('notificationType_mention');
	callback(null, data);
};

function sendNotificationToUids(postData, uids, nidType, notificationText) {
	if (!uids.length) {
		return;
	}

	var filteredUids = [];
	var notification;
	async.waterfall([
		function (next) {
			createNotification(postData, nidType, notificationText, next);
		},
		function (_notification, next) {
			notification = _notification;
			if (!notification) {
				return next();
			}

			batch.processArray(uids, function (uids, next) {
				async.waterfall([
					function(next) {
						Privileges.topics.filterUids('read', postData.tid, uids, next);
					},
					function(_uids, next) {
						if (Mentions._settings.overrideIgnores === 'on') {
							return setImmediate(next, null, _uids);
						}

						Topics.filterIgnoringUids(postData.tid, _uids, next);
					},
					function (_uids, next) {
						// Filter out uids that have already been notified for this pid
						db.isSortedSetMembers('mentions:sent:' + postData.pid, _uids, function (err, exists) {
							next(err, _uids.filter((uid, idx) => !exists[idx]))
						});
					},
					function(_uids, next) {
						if (!_uids.length) {
							return next();
						}

						filteredUids = filteredUids.concat(_uids);

						next();
					}
				], next);
			}, {
				interval: 1000,
				batch: 500,
			}, next);
		},
	], function (err) {
		if (err) {
			return winston.error(err);
		}

		if (notification && filteredUids.length) {
			Notifications.push(notification, filteredUids, function () {
				const dates = filteredUids.map(() => Date.now());
				db.sortedSetAdd('mentions:sent:' + postData.pid, dates, filteredUids);
			});
		}
	});
}

function createNotification(postData, nidType, notificationText, callback) {
	Notifications.create({
		type: 'mention',
		bodyShort: notificationText,
		bodyLong: postData.content,
		nid: 'tid:' + postData.tid + ':pid:' + postData.pid + ':uid:' + postData.uid + ':' + nidType,
		pid: postData.pid,
		tid: postData.tid,
		from: postData.uid,
		path: '/post/' + postData.pid,
		importance: 6
	}, callback);
}

function getGroupMemberUids(groupRecipients, callback) {
	async.map(groupRecipients, function(slug, next) {
		Groups.getGroupNameByGroupSlug(slug, next);
	}, function(err, groupNames) {
		if (err) {
			return callback(err);
		}
		async.map(groupNames, function(groupName, next) {
			Groups.getMembers(groupName, 0, -1, next);
		}, function(err, groupMembers) {
			if (err) {
				return callback(err);
			}
			callback(null, {groupNames: groupNames, groupMembers: groupMembers});
		});
	});
}

Mentions.parsePost = function(data, callback) {
	if (!data || !data.postData || !data.postData.content) {
		return callback(null, data);
	}

	Mentions.parseRaw(data.postData.content, function(err, content) {
		if (err) {
			return callback(err);
		}

		data.postData.content = content;
		callback(null, data);
	});
};

Mentions.parseRaw = function(content, callback) {
	var splitContent = utility.split(content, false, false, true);
	var matches = [];
	splitContent.forEach(function(cleanedContent, i) {
		if ((i & 1) === 0) {
			matches = matches.concat(cleanedContent.match(regex) || []);
		}
	});

	if (!matches.length) {
		return callback(null, content);
	}

	matches = matches.filter(function(cur, idx) {
		// Eliminate duplicates
		return idx === matches.indexOf(cur);
	}).map(function(match) {
		/**
		 *	Javascript-favour of regex does not support lookaround,
		 *	so need to clean up the cruft by discarding everthing
		 *	before the @
		 */
		var atIndex = match.indexOf('@');
		return atIndex !== 0 ? match.slice(atIndex) : match;
	});

	async.each(matches, function(match, next) {
		var slug = Utils.slugify(match.slice(1));

		match = removePunctuationSuffix(match);

		async.parallel({
			groupExists: async.apply(Groups.existsBySlug, slug),
			user: async () => {
				const uid = await User.getUidByUserslug(slug);
				return await User.getUserFields(uid, ['username', 'fullname']);
			},
		}, function(err, results) {
			if (err) {
				return next(err);
			}

			if (results.user || results.groupExists) {
				var regex = isLatinMention.test(match)
					? new RegExp('(?:^|\\s|\>|;)' + match + '\\b', 'g')
					: new RegExp('(?:^|\\s|\>|;)' + match, 'g');

				let skip = false;

				splitContent = splitContent.map(function(c, i) {
					// *Might* not be needed anymore? Check pls...
					if (skip || (i & 1) === 1) {
						skip = c === '';	// if code block detected, skip the content inside of it
						return c;
					}
					return c.replace(regex, function(match) {
						// Again, cleaning up lookaround leftover bits
						var atIndex = match.indexOf('@');
						var plain = match.slice(0, atIndex);
						match = match.slice(atIndex);

						switch (Mentions._settings.display) {
							case 'fullname':
								match = results.user.fullname || match;
								break;
							case 'username':
								match = results.user.username;
								break;
						}

						var str = results.user
								? '' + match + ''
								: '' + match + '';

						return plain + str;
					});
				});
			}

			next();
		});
	}, function(err) {
		callback(err, splitContent.join(''));
	});
};

Mentions.clean = function(input, isMarkdown, stripBlockquote, stripCode) {
	var split = utility.split(input, isMarkdown, stripBlockquote, stripCode);
	split = split.filter(function(e, i) {
		// only keep non-code/non-blockquote
		return (i & 1) === 0;
	});
	return split.join('');
};

/*
	WebSocket methods
*/

SocketPlugins.mentions.listGroups = function(socket, data, callback) {
	if (Mentions._settings.autofillGroups === 'off') {
		return callback(null, []);
	}

	Groups.getGroups('groups:visible:createtime', 0, -1, function(err, groups) {
		if (err) {
			return callback(err);
		}
		var noMentionGroups = getNoMentionGroups();
		groups = groups.filter(function(groupName) {
			return groupName && !noMentionGroups.includes(groupName);
		}).map(function(groupName) {
			return validator.escape(groupName);
		});
		callback(null, groups);
	});
};

module.exports = Mentions;
<commit_msg>fix: #116<commit_after>'use strict';

var	async = require('async');
var winston = module.parent.require('winston');
var XRegExp = require('xregexp');
var validator = require('validator');
var nconf = module.parent.require('nconf');

var db = require.main.require('./src/database');
var Topics = require.main.require('./src/topics');
var User = require.main.require('./src/user');
var Groups = require.main.require('./src/groups');
var Notifications = require.main.require('./src/notifications');
var Privileges = require.main.require('./src/privileges');
var Meta = require.main.require('./src/meta');
var Utils = require.main.require('./public/src/utils');
var batch = require.main.require('./src/batch');

var SocketPlugins = require.main.require('./src/socket.io/plugins');

const utility = require('./lib/utility');

var regex = XRegExp('(?:^|\\s|\\>|;)(@[\\p{L}\\d\\-_.]+)', 'g');
var isLatinMention = /@[\w\d\-_.]+$/;
var removePunctuationSuffix = function(string) {
	return string.replace(/[!?.]*$/, '');
};
var Entities = require('html-entities').XmlEntities;
var entities = new Entities();

var Mentions = {
	_settings: {},
	_defaults: {
		disableFollowedTopics: 'off',
		autofillGroups: 'off',
		disableGroupMentions: '[]',
		overrideIgnores: 'off',
		display: '',
	}
};
SocketPlugins.mentions = {};

Mentions.init = function (data, callback) {
	var hostMiddleware = require.main.require('./src/middleware');
	var controllers = require('./controllers');

	data.router.get('/admin/plugins/mentions', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
	data.router.get('/api/admin/plugins/mentions', controllers.renderAdminPage);

	// Retrieve settings
	Meta.settings.get('mentions', function (err, settings) {
		Object.assign(Mentions._settings, Mentions._defaults, settings);
		callback();
	});
};

Mentions.addAdminNavigation = function (header, callback) {
	header.plugins.push({
		route: '/plugins/mentions',
		name: 'Mentions'
	});

	callback(null, header);
};

function getNoMentionGroups() {
	var noMentionGroups = ['registered-users', 'guests'];
	try {
		noMentionGroups = noMentionGroups.concat(JSON.parse(Mentions._settings.disableGroupMentions));
	} catch (err) {
		winston.error(err);
	}
	return noMentionGroups;
}

Mentions.notify = function(data) {
	var postData = data.post;
	var cleanedContent = Mentions.clean(postData.content, true, true, true);
	var matches = cleanedContent.match(regex);

	if (!matches) {
		return;
	}

	var noMentionGroups = getNoMentionGroups();

	matches = matches.map(function(match) {
		return Utils.slugify(match);
	}).filter(function(match, index, array) {
		return match && array.indexOf(match) === index && noMentionGroups.indexOf(match) === -1;
	});

	if (!matches.length) {
		return;
	}

	async.parallel({
		userRecipients: function(next) {
			async.filter(matches, User.existsBySlug, next);
		},
		groupRecipients: function(next) {
			async.filter(matches, Groups.existsBySlug, next);
		}
	}, function(err, results) {
		if (err) {
			return;
		}

		if (!results.userRecipients.length && !results.groupRecipients.length) {
			return;
		}

		async.parallel({
			topic: function(next) {
				Topics.getTopicFields(postData.tid, ['title', 'cid'], next);
			},
			author: function(next) {
				User.getUserField(postData.uid, 'username', next);
			},
			uids: function(next) {
				async.map(results.userRecipients, function(slug, next) {
					User.getUidByUserslug(slug, next);
				}, next);
			},
			groupData: function(next) {
				getGroupMemberUids(results.groupRecipients, next);
			},
			topicFollowers: function(next) {
				if (Mentions._settings.disableFollowedTopics === 'on') {
					Topics.getFollowers(postData.tid, next);
				} else {
					next(null, []);
				}
			}
		}, function(err, results) {
			if (err) {
				return;
			}

			var title = entities.decode(results.topic.title);
			var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');

			var uids = results.uids.filter(function(uid, index, array) {
				return array.indexOf(uid) === index && parseInt(uid, 10) !== parseInt(postData.uid, 10) && !results.topicFollowers.includes(uid.toString());
			});

			var groupMemberUids = {};
			results.groupData.groupNames.forEach(function(groupName, index) {
				results.groupData.groupMembers[index] = results.groupData.groupMembers[index].filter(function(uid) {
					if (!uid || groupMemberUids[uid]) {
						return false;
					}
					groupMemberUids[uid] = 1;
					return !uids.includes(uid) &&
						parseInt(uid, 10) !== parseInt(postData.uid, 10) &&
						!results.topicFollowers.includes(uid.toString());
				});
			});

			sendNotificationToUids(postData, uids, 'user', '[[notifications:user_mentioned_you_in, ' + results.author + ', ' + titleEscaped + ']]');

			results.groupData.groupNames.forEach(function(groupName, index) {
				var memberUids = results.groupData.groupMembers[index];
				sendNotificationToUids(postData, memberUids, groupName, '[[notifications:user_mentioned_group_in, ' + results.author + ', ' + groupName + ', ' + titleEscaped + ']]');
			});
		});
	});
};

Mentions.addFilters = function (data, callback) {
	data.regularFilters.push({ name: '[[notifications:mentions]]', filter: 'mention' });
	callback(null, data);
};

Mentions.notificationTypes = function (data, callback) {
	data.types.push('notificationType_mention');
	callback(null, data);
};

function sendNotificationToUids(postData, uids, nidType, notificationText) {
	if (!uids.length) {
		return;
	}

	var filteredUids = [];
	var notification;
	async.waterfall([
		function (next) {
			createNotification(postData, nidType, notificationText, next);
		},
		function (_notification, next) {
			notification = _notification;
			if (!notification) {
				return next();
			}

			batch.processArray(uids, function (uids, next) {
				async.waterfall([
					function(next) {
						Privileges.topics.filterUids('read', postData.tid, uids, next);
					},
					function(_uids, next) {
						if (Mentions._settings.overrideIgnores === 'on') {
							return setImmediate(next, null, _uids);
						}

						Topics.filterIgnoringUids(postData.tid, _uids, next);
					},
					function (_uids, next) {
						// Filter out uids that have already been notified for this pid
						db.isSortedSetMembers('mentions:sent:' + postData.pid, _uids, function (err, exists) {
							next(err, _uids.filter((uid, idx) => !exists[idx]))
						});
					},
					function(_uids, next) {
						if (!_uids.length) {
							return next();
						}

						filteredUids = filteredUids.concat(_uids);

						next();
					}
				], next);
			}, {
				interval: 1000,
				batch: 500,
			}, next);
		},
	], function (err) {
		if (err) {
			return winston.error(err);
		}

		if (notification && filteredUids.length) {
			Notifications.push(notification, filteredUids, function () {
				const dates = filteredUids.map(() => Date.now());
				db.sortedSetAdd('mentions:sent:' + postData.pid, dates, filteredUids);
			});
		}
	});
}

function createNotification(postData, nidType, notificationText, callback) {
	Notifications.create({
		type: 'mention',
		bodyShort: notificationText,
		bodyLong: postData.content,
		nid: 'tid:' + postData.tid + ':pid:' + postData.pid + ':uid:' + postData.uid + ':' + nidType,
		pid: postData.pid,
		tid: postData.tid,
		from: postData.uid,
		path: '/post/' + postData.pid,
		importance: 6
	}, callback);
}

function getGroupMemberUids(groupRecipients, callback) {
	async.map(groupRecipients, function(slug, next) {
		Groups.getGroupNameByGroupSlug(slug, next);
	}, function(err, groupNames) {
		if (err) {
			return callback(err);
		}
		async.map(groupNames, function(groupName, next) {
			Groups.getMembers(groupName, 0, -1, next);
		}, function(err, groupMembers) {
			if (err) {
				return callback(err);
			}
			callback(null, {groupNames: groupNames, groupMembers: groupMembers});
		});
	});
}

Mentions.parsePost = function(data, callback) {
	if (!data || !data.postData || !data.postData.content) {
		return callback(null, data);
	}

	Mentions.parseRaw(data.postData.content, function(err, content) {
		if (err) {
			return callback(err);
		}

		data.postData.content = content;
		callback(null, data);
	});
};

Mentions.parseRaw = function(content, callback) {
	var splitContent = utility.split(content, false, false, true);
	var matches = [];
	splitContent.forEach(function(cleanedContent, i) {
		if ((i & 1) === 0) {
			matches = matches.concat(cleanedContent.match(regex) || []);
		}
	});

	if (!matches.length) {
		return callback(null, content);
	}

	matches = matches.filter(function(cur, idx) {
		// Eliminate duplicates
		return idx === matches.indexOf(cur);
	}).map(function(match) {
		/**
		 *	Javascript-favour of regex does not support lookaround,
		 *	so need to clean up the cruft by discarding everthing
		 *	before the @
		 */
		var atIndex = match.indexOf('@');
		return atIndex !== 0 ? match.slice(atIndex) : match;
	});

	async.each(matches, function(match, next) {
		var slug = Utils.slugify(match.slice(1));

		match = removePunctuationSuffix(match);

		async.parallel({
			groupExists: async.apply(Groups.existsBySlug, slug),
			user: async () => {
				const uid = await User.getUidByUserslug(slug);
				return await User.getUserFields(uid, ['uid', 'username', 'fullname']);
			},
		}, function(err, results) {
			if (err) {
				return next(err);
			}

			if (results.user.uid || results.groupExists) {
				var regex = isLatinMention.test(match)
					? new RegExp('(?:^|\\s|\>|;)' + match + '\\b', 'g')
					: new RegExp('(?:^|\\s|\>|;)' + match, 'g');

				let skip = false;

				splitContent = splitContent.map(function(c, i) {
					// *Might* not be needed anymore? Check pls...
					if (skip || (i & 1) === 1) {
						skip = c === '';	// if code block detected, skip the content inside of it
						return c;
					}
					return c.replace(regex, function(match) {
						// Again, cleaning up lookaround leftover bits
						var atIndex = match.indexOf('@');
						var plain = match.slice(0, atIndex);
						match = match.slice(atIndex);

						switch (Mentions._settings.display) {
							case 'fullname':
								match = results.user.fullname || match;
								break;
							case 'username':
								match = results.user.username;
								break;
						}

						var str = results.user.uid
								? '' + match + ''
								: '' + match + '';

						return plain + str;
					});
				});
			}

			next();
		});
	}, function(err) {
		callback(err, splitContent.join(''));
	});
};

Mentions.clean = function(input, isMarkdown, stripBlockquote, stripCode) {
	var split = utility.split(input, isMarkdown, stripBlockquote, stripCode);
	split = split.filter(function(e, i) {
		// only keep non-code/non-blockquote
		return (i & 1) === 0;
	});
	return split.join('');
};

/*
	WebSocket methods
*/

SocketPlugins.mentions.listGroups = function(socket, data, callback) {
	if (Mentions._settings.autofillGroups === 'off') {
		return callback(null, []);
	}

	Groups.getGroups('groups:visible:createtime', 0, -1, function(err, groups) {
		if (err) {
			return callback(err);
		}
		var noMentionGroups = getNoMentionGroups();
		groups = groups.filter(function(groupName) {
			return groupName && !noMentionGroups.includes(groupName);
		}).map(function(groupName) {
			return validator.escape(groupName);
		});
		callback(null, groups);
	});
};

module.exports = Mentions;
<|endoftext|>


<commit_before>(function(module) {
	"use strict";

	var marked = require('marked'),
		async = require('async'),
		fs = require('fs'),
		path = require('path'),
		categories = require('../../src/categories'),
		user = require('../../src/user'),
		translator = require('../../public/src/translator'),
		templates = require('../../public/src/templates');

	var Widget = {
		templates: {}
	};

	Widget.renderHTMLWidget = function(widget, callback) {
		var html = widget.data.html;

		callback(null, {
			title: "HTML",
			html: html
		});
	};

	Widget.renderTextWidget = function(widget, callback) {
		var markdown = !!widget.data.markdown,
			text = widget.data.text;

		if (markdown) {
			text = marked(text);
		} else {
			text = text.replace(/\r\n/g, "
");
		}

		callback(null, {
			title: "Text",
			html: text
		});
	};

	Widget.renderForumStatsWidget = function(widget, callback) {
		var html = Widget.templates['forumstats.tpl'];

		translator.translate(html, function(translatedHTML) {
			callback(null, {
				title: widget.data.title || "Forum Stats",
				html: translatedHTML
			});
		});
	};

	Widget.renderRecentRepliesWidget = function(widget, callback) {
		var html = Widget.templates['recentreplies.tpl'];

		html = templates.prepare(html).parse({cid: widget.data.cid || false});

		callback(null, {
			title: widget.data.title || "Recent Replies",
			html: html
		});
	};

	Widget.renderActiveUsersWidget = function(widget, callback) {
		var html = Widget.templates['activeusers.tpl'], cid;

		if (widget.data.cid) {
			cid = widget.data.cid;
		} else {
			var match = widget.area.url.match('[0-9]+');
			cid = match ? match[0] : 1;
		}

		categories.getActiveUsers(cid, function(err, uids) {
			user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture'], function(err, users) {
				html = templates.prepare(html).parse({active_users: users});

				callback(err, {
					title: widget.data.title || "Active Participants",
					html: html
				});
			});
		});
	};

	Widget.renderModeratorsWidget = function(widget, callback) {
		var html = Widget.templates['moderators.tpl'], cid;

		if (widget.data.cid) {
			cid = widget.data.cid;
		} else {
			var match = widget.area.url.match('[0-9]+');
			cid = match ? match[0] : 1;
		}

		categories.getModerators(cid, function(err, moderators) {
			html = templates.prepare(html).parse({moderators: moderators});

			callback(err, {
				title: widget.data.title || "Moderators",
				html: html
			});
		});
	};

	Widget.defineWidgets = function(widgets, callback) {
		// todo: move content into template files

		widgets = widgets.concat([
			{
				widget: "html",
				name: "HTML",
				description: "Any text, html, or embedded script.",
				content: "<textarea class=\"form-control\" rows=\"6\" name=\"html\" placeholder=\"Enter HTML\"></textarea>"
			},
			{
				widget: "text",
				name: "Text",
				description: "Markdown formatted text.",
				content: "<textarea class=\"form-control\" rows=\"6\" name=\"text\" placeholder=\"Enter Text / Markdown\"></textarea>"
							+ "
"
							+ "
"
							+ 	"<input type=\"checkbox\" name=\"markdown\" checked /> Parse as Markdown?"
							+ "
"
			},
			{
				widget: "recentreplies",
				name: "Recent Replies",
				description: "List of recent replies in a category.",
				content: "Custom Category:
Leave blank to to dynamically pull from current category<input type=\"text\" class=\"form-control\" name=\"cid\" placeholder=\"0\" />
Custom Title:<input type=\"text\" class=\"form-control\" name=\"title\" placeholder=\"Recent Replies\" />"
			},
			{
				widget: "activeusers",
				name: "Active Users",
				description: "List of active users in a category.",
				content: "Custom Category:
Leave blank to to dynamically pull from current category<input type=\"text\" class=\"form-control\" name=\"cid\" placeholder=\"0\" />
Custom Title:<input type=\"text\" class=\"form-control\" name=\"title\" placeholder=\"Active Users\" />"
			},
			{
				widget: "moderators",
				name: "Moderators",
				description: "List of moderators in a category.",
				content: "Custom Category:
Leave blank to to dynamically pull from current category<input type=\"text\" class=\"form-control\" name=\"cid\" placeholder=\"0\" />
Custom Title:<input type=\"text\" class=\"form-control\" name=\"title\" placeholder=\"Moderators\" />"
			},
			{
				widget: "forumstats",
				name: "Forum Stats",
				description: "Lists user, topics, and post count.",
				content: "Custom Title:<input type=\"text\" class=\"form-control\" name=\"title\" placeholder=\"Forum Stats\" />"
			}
		]);

		callback(null, widgets);
	};

	Widget.addRoutes = function(custom_routes, callback) {
		var templatesToLoad = ["recentreplies.tpl", "activeusers.tpl", "moderators.tpl", "forumstats.tpl"];

		function loadTemplate(template, next) {
			fs.readFile(path.resolve(__dirname, './public/templates/' + template), function (err, data) {
				Widget.templates[template] = data.toString();
				next(err);
			});
		}

		async.each(templatesToLoad, loadTemplate, function(err) {
			if (err) {
				throw new Error("Error loading templates: "  + err);
			}

			custom_routes.templates = custom_routes.templates.concat(
				[
					{
						"template": "recentreplies.tpl",
						"content": Widget.templates['recentreplies.tpl']
					}
				]
			);

			callback(err, custom_routes);
		});
	};

	module.exports = Widget;
}(module));<commit_msg>updated essentials to latest container changes in widgets<commit_after>(function(module) {
	"use strict";

	var marked = require('marked'),
		async = require('async'),
		fs = require('fs'),
		path = require('path'),
		categories = require('../../src/categories'),
		user = require('../../src/user'),
		translator = require('../../public/src/translator'),
		templates = require('../../public/src/templates');

	var Widget = {
		templates: {}
	};

	Widget.renderHTMLWidget = function(widget, callback) {
		var html = widget.data.html;

		callback(null, html);
	};

	Widget.renderTextWidget = function(widget, callback) {
		var markdown = !!widget.data.markdown,
			text = widget.data.text;

		if (markdown) {
			text = marked(text);
		} else {
			text = text.replace(/\r\n/g, "
");
		}

		callback(null, text);
	};

	Widget.renderRecentRepliesWidget = function(widget, callback) {
		var html = Widget.templates['recentreplies.tpl'];

		html = templates.prepare(html).parse({cid: widget.data.cid || false});

		callback(null, html);
	};

	Widget.renderActiveUsersWidget = function(widget, callback) {
		var html = Widget.templates['activeusers.tpl'], cid;

		if (widget.data.cid) {
			cid = widget.data.cid;
		} else {
			var match = widget.area.url.match('[0-9]+');
			cid = match ? match[0] : 1;
		}

		categories.getActiveUsers(cid, function(err, uids) {
			user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture'], function(err, users) {
				html = templates.prepare(html).parse({active_users: users});

				callback(err, html);
			});
		});
	};

	Widget.renderModeratorsWidget = function(widget, callback) {
		var html = Widget.templates['moderators.tpl'], cid;

		if (widget.data.cid) {
			cid = widget.data.cid;
		} else {
			var match = widget.area.url.match('[0-9]+');
			cid = match ? match[0] : 1;
		}

		categories.getModerators(cid, function(err, moderators) {
			html = templates.prepare(html).parse({moderators: moderators});

			callback(err, html);
		});
	};

	Widget.renderForumStatsWidget = function(widget, callback) {
		var html = Widget.templates['forumstats.tpl'];

		translator.translate(html, function(translatedHTML) {
			callback(null, translatedHTML);
		});
	};

	Widget.defineWidgets = function(widgets, callback) {
		// todo: move content into template files

		widgets = widgets.concat([
			{
				widget: "html",
				name: "HTML",
				description: "Any text, html, or embedded script.",
				content: Widget.templates['admin/html.tpl']
			},
			{
				widget: "text",
				name: "Text",
				description: "Markdown formatted text.",
				content: Widget.templates['admin/text.tpl']
			},
			{
				widget: "recentreplies",
				name: "Recent Replies",
				description: "List of recent replies in a category.",
				content: Widget.templates['admin/categorywidget.tpl']
			},
			{
				widget: "activeusers",
				name: "Active Users",
				description: "List of active users in a category.",
				content: Widget.templates['admin/categorywidget.tpl']
			},
			{
				widget: "moderators",
				name: "Moderators",
				description: "List of moderators in a category.",
				content: Widget.templates['admin/categorywidget.tpl']
			},
			{
				widget: "forumstats",
				name: "Forum Stats",
				description: "Lists user, topics, and post count.",
				content: Widget.templates['admin/forumstats.tpl']
			}
		]);

		callback(null, widgets);
	};

	Widget.addRoutes = function(custom_routes, callback) {
		var templatesToLoad = [
			"recentreplies.tpl", "activeusers.tpl", "moderators.tpl", "forumstats.tpl",
			"admin/categorywidget.tpl", "admin/forumstats.tpl", "admin/html.tpl", "admin/text.tpl"
		];

		function loadTemplate(template, next) {
			fs.readFile(path.resolve(__dirname, './public/templates/' + template), function (err, data) {
				Widget.templates[template] = data.toString();
				next(err);
			});
		}

		async.each(templatesToLoad, loadTemplate, function(err) {
			if (err) {
				throw new Error("Error loading templates: "  + err);
			}

			custom_routes.templates = custom_routes.templates.concat(
				[
					{
						"template": "recentreplies.tpl",
						"content": Widget.templates['recentreplies.tpl']
					}
				]
			);

			callback(err, custom_routes);
		});
	};

	module.exports = Widget;
}(module));<|endoftext|>


<commit_before>'use strict';

var Settings = module.parent.require("./settings");
var SocketAdmin = module.parent.require('./socket.io/admin');

var controllers = require('./lib/controller');

var plugin = {};

var settings;
var camoUrl;

plugin.init = function(params, callback) {
  var router = params.router;
  var hostMiddleware = params.middleware;
  var hostControllers = params.controllers;

  settings = new Settings('camo', '1.0.0', {host: "", key: "", type: "path"}, sync);

  router.get('/admin/plugins/camo', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
  router.get('/api/admin/plugins/camo', controllers.renderAdminPage);

  SocketAdmin.settings.syncCamo = function () {
    settings.sync(sync);
  };

  function sync() {
    camoUrl = require('camo-url')({
      host: settings.get('host'),
      key: settings.get('key'),
      type: settings.get('type')
    });
  }

  callback();
};

plugin.addAdminNavigation = function(header, callback) {
  header.plugins.push({
    route: '/plugins/camo',
    icon: 'fa-image',
    name: 'Camo'
  });

  callback(null, header);
};

plugin.parseRaw = function(content, callback) {
  content = content.replace(/<img[^>]+src=['"](http[^s][^'"]+)['"][^>]*>/gi, function (match, url) {
    return match.replace(url, camoUrl(url));
  });
  callback(null, content);
};

plugin.parsePost = function(data, callback) {
  plugin.parseRaw(data.postData.content, function(err, content){
    data.postData.content = content;
    callback(null, data);
  });
};

module.exports = plugin;
<commit_msg>Add internal camo proxy.<commit_after>'use strict';

var Settings = module.parent.require("./settings");
var SocketAdmin = module.parent.require('./socket.io/admin');

var controllers = require('./lib/controller');

var plugin = {};

var settings;
var camoUrl;
var camo;

// Kill worker when nodebb closes/crashes.
process.on("uncaughtException", killWorker);
process.on("SIGINT", killWorker);
process.on("SIGTERM", killWorker);

plugin.init = function(params, callback) {
  var router = params.router;
  var hostMiddleware = params.middleware;
  var hostControllers = params.controllers;

  settings = new Settings('camo', '1.0.0', {host: "", key: "", type: "path"}, sync);

  router.get('/admin/plugins/camo', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
  router.get('/api/admin/plugins/camo', controllers.renderAdminPage);

  SocketAdmin.settings.syncCamo = function () {
    settings.sync(sync);
  };

  function sync() {
    camoUrl = require('camo-url')({
      host: settings.get('host'),
      key: settings.get('key'),
      type: settings.get('type')
    });

    if (settings.get('useCamoProxy')) {
      killWorker();

      var camo = require("child_process").spawn('node', ['node_modules/camo/server'], {silent: true, env: {
        'CAMO_KEY': settings.get('key') || 'banana',
		'PORT': settings.get('port') || '8082'
      }});

      camo.stdout.on('data', function (data) { console.log('CAMO PROXY SAYS: ' + data); });
      camo.stderr.on('data', function (data) { console.log('CAMO PROXY ERROR: ' + data); });
      camo.on('close', function (code) { console.log('CAMO PROXY exited with code ' + code); });
    }
  }

  callback();
};

plugin.addAdminNavigation = function(header, callback) {
  header.plugins.push({
    route: '/plugins/camo',
    icon: 'fa-image',
    name: 'Camo'
  });

  callback(null, header);
};

plugin.parseRaw = function(content, callback) {
  content = content.replace(/<img[^>]+src=['"](http[^s][^'"]+)['"][^>]*>/gi, function (match, url) {
    return match.replace(url, camoUrl(url));
  });
  callback(null, content);
};

plugin.parsePost = function(data, callback) {
  plugin.parseRaw(data.postData.content, function(err, content){
    data.postData.content = content;
    callback(null, data);
  });
};

function killWorker() {
  if (camo && camo.pid) process.kill(camo.pid);
}

module.exports = plugin;
<|endoftext|>


<commit_before>exports.register = function (plugin, options, next) {
  plugin.route({
    path: '/{p*}',
    method:'GET',
    handler: function (request, reply) {
      return reply.view('index')
    }
  })

  plugin.route({
    path: '/assets/{filename*}',
    method: 'GET',
    handler: {
      file: function (request) {
        return request.params.filename
      }
    }
  })

  plugin.route({
    path: '/views/{filename*}',
    method: 'GET',
    handler: {
      file: function (request) {
        return 'modules/' + request.params.filename
      }
    }
  })

  next()
}

exports.register.attributes = {
    name: 'index'
}
<commit_msg>removed view path<commit_after>exports.register = function (plugin, options, next) {
  plugin.route({
    path: '/{p*}',
    method:'GET',
    handler: function (request, reply) {
      return reply.view('index')
    }
  })

  plugin.route({
    path: '/assets/{filename*}',
    method: 'GET',
    handler: {
      file: function (request) {
        return request.params.filename
      }
    }
  })

  next()
}

exports.register.attributes = {
    name: 'index'
}
<|endoftext|>


<commit_before>import can from "can";
import 'can/map/define/';

export default can.Map.extend({
	define: {
		chart: {
			type: '*',
			value: null
		},
		valueSerialized: {
			get: function(val) {
				return this.attr('value').serialize();
			}
		}
	},
	'value': null,
	'key': null,
	'dequeueKey': function() {
		var value = this.attr('value').attr(),
			key = this.attr('key');
		if(key === null || (value !== null && value.length && value[0] === key)) {
			this.attr('key', value.shift());
		}
		return value;
	},
	'updateColumn': function() {
		var value = this.dequeueKey(this.attr('value')),
			key = this.attr('key'),
			chart = this.attr('chart'),
			pushing = [key].concat(value);
		if(value.length) {
			chart.load({
				columns: [pushing]
			});
		} else {
			this.unloadColumn();
		}
	},
	'unloadColumn': function() {
		this.attr('chart').unload({
			ids: this.attr('key')
		});
	}
});<commit_msg>fixes column errors if invalid data passed<commit_after>import can from "can";
import 'can/map/define/';

export default can.Map.extend({
	define: {
		chart: {
			type: '*',
			value: null
		},
		valueSerialized: {
			get: function(val) {
				return this.attr('value') && this.attr('value').serialize();
			}
		}
	},
	'value': null,
	'key': null,
	'dequeueKey': function() {
		var value = this.attr('value') && this.attr('value').attr(),
			key = this.attr('key');
		if(key === null || (value !== null && value.length && value[0] === key)) {
			this.attr('key', value.shift());
		}
		return value;
	},
	'updateColumn': function() {
		var value = this.dequeueKey(this.attr('value')),
			key = this.attr('key'),
			chart = this.attr('chart'),
			pushing = [key].concat(value);
		if(value && value.length) {
			chart.load({
				columns: [pushing]
			});
		} else {
			this.unloadColumn();
		}
	},
	'unloadColumn': function() {
		this.attr('chart').unload({
			ids: this.attr('key')
		});
	}
});<|endoftext|>


<commit_before>var fs = require('fsplus');
var path = require('path');
var geci = require('geci');
var mkdirp = require('mkdirp');
var Player = require('player');
var color = require('colorful');
var consoler = require('consoler');
var termList = require('term-list-enhanced');
var sdk = require('./sdk');
var utils = require('./utils');
var pkg = require('../package');
var template = require('./template');

// the keypress shortcut list
var shorthands = {
  'return': 'play',
  'backspace': 'stop',
  'g': 'go',
  'l': 'loving',
  'n': 'next',
  'q': 'quit',
  's': 'share',
  'r': 'showLrc'
};

var errors = {
  'account_missing': '请先设置豆瓣账户再收听私人/红心兆赫: $ douban.fm config'
};

module.exports = Fm;

function Fm() {
  this.rc = {};
  this.syshome = utils.home();
  this.rc.profile = path.join(this.syshome, '.douban.fm.profile.json');
  this.rc.history = path.join(this.syshome, '.douban.fm.history.json');
  this.home = utils.readJSON(this.rc.profile).home || path.join(this.syshome, 'douban.fm');
  this.love = path.join(this.home, 'love');
  this.isShowLrc = false;
  mkdirp.sync(this.love); // ensure dir exists
  template.updateTab('Douban FM');
};

/**
*
* Fetch songs and add them to playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.fetch = function(channel, account, callback) {
  var self = this;
  return sdk.songs({
    kbps: 192,
    token: account.token,
    history: self.rc.history,
    channel: channel.channel_id,
    user_id: account.user_id,
    expire: account.expire,
    local: (channel.channel_id == -99) ? self.home : false
  }, utils.isFunction(callback) ? callback : cb);

  function cb(err, songs, result) {
    if (err) return;
    if (!songs || songs.length === 0) return;
    if (!self.player) return;
    songs.forEach(function(song) {
      self.player.add(song);
    });
  }
}

/**
*
* Playing songs when everything is ready
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.play = function(channel, account) {
  var self = this;
  var menu = self.menu;
  var privateMhz = (channel.channel_id == 0 || channel.channel_id == -3) && (!account || !account.token);

  // Check if this kind of mHz is private
  if (privateMhz) return menu.update(channel.index, color.yellow(errors.account_missing));

  // clear last label
  if (self.status === 'fetching' || self.status === 'downloading') return;
  if (self.status === 'playing' || self.status === 'error') {
    if (typeof(self.channel) != undefined) menu.clear(self.channel);
    if (self.player) {
      self.player.stop();
      delete self.player;
    }
  }

  // clear label status
  menu.clear(0);
  self.channel = channel.index;
  self.status = 'fetching';
  menu.update(channel.index, template.listing());
  fs.updateJSON(self.rc.profile, { lastChannel: channel });

  // start fetching songs
  self.fetch(channel, account, function(err, songs, result) {
    if (err) {
      self.status = 'error';
      return menu.update(channel.index, color.red(err.toString()));
    }
    // mark PRO account on logo
    if (result && !result.warning) menu.update(0, color.inverse(' PRO '));
    self.status = 'ready';
    self.player = new Player(songs, {
      src: 'url',
      cache: true,
      downloads: self.home
    });
    self.player.play();
    self.player.on('downloading', function(url) {
      self.status = 'downloading';
      menu.update(channel.index, template.loading());
    });
    // update template
    self.player.on('playing', function(song) {
      var isValidSong = song.title && song.sid;
      self.status = 'playing';
      // update playing label
      menu.update(0, color.green('>'));
      // update song infomation
      menu.update(channel.index, template.song(song));
      // logging songs history
      if (isValidSong) {
        var updates = {};
        updates[song.sid] = song;
        try {
          fs.updateJSON(this.rc.history, updates);
        } catch (err) {
          // error must be logged in a private place.
        }
      }
      // print LRC if needed.
      if (self.isShowLrc) {
        if (self.lrc) self.lrc.stop();
        geci.fetch(song, function(err, lrc) {
          if (err) return menu.update(0, color.grey('抱歉, 没找到歌词 ' + err.toString()));
          if (!lrc) return menu.update(0, color.grey('抱歉, 没找到歌词'));
          self.lrc = geci.print(lrc, function(line, extra) {
            menu.update(channel.index, template.song(song, line));
          });
        });
      }
      // 没有对尝试获取列表失败进行处理，如果失败2次，则不会再播放任何歌曲
      if (song._id < self.player.list.length - 1) return false;
      return self.fetch(channel, account);
    });
  });
}

/**
*
* Add current song to lovelist when pressing `L`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.loving = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (!this.player.playing.sid) return this.menu.update(0, '未知曲目无法加心');
  if (!account) return false;
  var self = this;
  var menu = self.menu;
  var song = self.player.playing;
  var query = {
    sid: song.sid,
    channel: self.channel,
    user_id: account.user_id,
    expire: account.expire,
    token: account.token
  };
  if (song.like) query.type = 'u';
  menu.update(0, '正在加载...');
  sdk.love(query, function(err, result) {
    menu.clear(0);
    if (err) menu.update(0, '出错了, 请稍后再试...');
    if (!err) self.player.playing.like = !song.like;
    return menu.update(
      self.channel,
      template.song(self.player.playing, null, true) // keep silence, do not notify
    );
  });
}

/**
*
* Play the next song in the playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.next = function(channel, account) {
  if (!this.player) return false;
  this.player.next(function(err, song) {
    if (err) menu.update(self.channel, color.yellow('这是最后一首了哦，回车以加载最新列表'));
    return false;
  });
}

/**
*
* Stop playing
* and show the stopped status on logo.
*
**/
Fm.prototype.stop = function() {
  if (!this.player) return false;
  var menu = this.menu;
  menu.clear(0);
  menu.update(0, template.pause());
  return this.player.stop();
}

/**
*
* Quit the Fm
* and kill the process when pressing `Q`
*
**/
Fm.prototype.quit = function() {
  this.menu.stop();
  return process.exit();
}

/**
*
* Goto the music album page when pressing `G`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.go = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (channel.channel_id == -99) return false;
  return utils.go(utils.album(this.player.playing.album));
}

/**
*
* Show lrc when when pressing `R`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.showLrc = function(channel, account) {
  if (channel.channel_id == -99) return false;
  this.isShowLrc = !!!this.isShowLrc;
  this.menu.clear(0);
  this.menu.update(0, this.isShowLrc ? '歌词开启' : '歌词关闭');
  return false;
}

/**
*
* Share the current playing songs to Weibo when pressing `S`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.share = function(channel, account) {
  if (!this.player || !this.player.playing) return false;
  return utils.go(template.share(this.player.playing));
}

/**
*
* Create command line interface menu
* using term-list-enhanced module
* @callback[Function]: the callback function when set down.
*
**/
Fm.prototype.createMenu = function(callback) {
  var self = this;
  // fetch channels
  sdk.fm.channels(function(err, list) {
    if (err) consoler.error('获取豆瓣电台频道出错，切换为本地电台...');
    // fetch configs, show user's infomations
    fs.readJSON(self.rc.profile, function(e, user) {
      var vaildAccount = user && user.account && user.account.user_name;
      var account = vaildAccount ? user.account : null;
      // init menu
      self.menu = new termList();
      var nav = [template.logo(account), sdk.mhz.localMhz];
      self.menu.adds(nav.concat(!err ? [sdk.mhz.privateMhz].concat(list) : []));
      // bind keypress events
      self.menu.on('keypress', function(key, index) {
        if (!shorthands[key.name]) return false;
        if (index < 1 && key.name != 'q') return utils.go(pkg.repository.url);
        return self[shorthands[key.name]](self.menu.items[index], account);
      });
      self.menu.on('empty', function() {
        self.menu.stop();
      });
      // check last played channel
      if (user && user.lastChannel) {
        self.play(user.lastChannel, account);
        self.menu.start(user.lastChannel.index);
        return false;
      }
      // start menu at line 2 (below the logo text)
      self.menu.start(1);
    });
  });
  // callback if necessary.
  return callback && callback();
}

/**
*
* Init douban.fm command line interface.
* @callback [Function]: the callback function when all set done
*
**/
Fm.prototype.init = function(callback) {
  var self = this;
  fs.exists(self.home, function(exist) {
    if (exist) return self.createMenu(callback);
    mkdirp(self.love, function(err) {
      if (err) return consoler.error('创建歌曲文件夹出错，请检查权限');
      return self.createMenu(callback);
    });
  });
}<commit_msg>0.1.6: 记录播放记录时添加 errhander<commit_after>var fs = require('fsplus');
var path = require('path');
var geci = require('geci');
var mkdirp = require('mkdirp');
var Player = require('player');
var color = require('colorful');
var consoler = require('consoler');
var termList = require('term-list-enhanced');
var sdk = require('./sdk');
var utils = require('./utils');
var pkg = require('../package');
var template = require('./template');

// the keypress shortcut list
var shorthands = {
  'return': 'play',
  'backspace': 'stop',
  'g': 'go',
  'l': 'loving',
  'n': 'next',
  'q': 'quit',
  's': 'share',
  'r': 'showLrc'
};

var errors = {
  'account_missing': '请先设置豆瓣账户再收听私人/红心兆赫: $ douban.fm config'
};

module.exports = Fm;

function Fm() {
  this.rc = {};
  this.syshome = utils.home();
  this.rc.profile = path.join(this.syshome, '.douban.fm.profile.json');
  this.rc.history = path.join(this.syshome, '.douban.fm.history.json');
  this.home = utils.readJSON(this.rc.profile).home || path.join(this.syshome, 'douban.fm');
  this.love = path.join(this.home, 'love');
  this.isShowLrc = false;
  mkdirp.sync(this.love); // ensure dir exists
  template.updateTab('Douban FM');
};

/**
*
* Fetch songs and add them to playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.fetch = function(channel, account, callback) {
  var self = this;
  return sdk.songs({
    kbps: 192,
    token: account ? account.token: null,
    history: self.rc.history,
    channel: channel.channel_id,
    user_id: account ? account.user_id : null,
    expire: account ? account.expire : null,
    local: (channel.channel_id == -99) ? self.home : false
  }, utils.isFunction(callback) ? callback : cb);

  function cb(err, songs, result) {
    if (err) return;
    if (!songs || songs.length === 0) return;
    if (!self.player) return;
    songs.forEach(function(song) {
      self.player.add(song);
    });
  }
}

/**
*
* Playing songs when everything is ready
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.play = function(channel, account) {
  var self = this;
  var menu = self.menu;
  var privateMhz = (channel.channel_id == 0 || channel.channel_id == -3) && (!account || !account.token);

  // Check if this kind of mHz is private
  if (privateMhz) return menu.update(channel.index, color.yellow(errors.account_missing));

  // clear last label
  if (self.status === 'fetching' || self.status === 'downloading') return;
  if (self.status === 'playing' || self.status === 'error') {
    if (typeof(self.channel) != undefined) menu.clear(self.channel);
    if (self.player) {
      self.player.stop();
      delete self.player;
    }
  }

  // clear label status
  menu.clear(0);
  self.channel = channel.index;
  self.status = 'fetching';
  menu.update(channel.index, template.listing());
  try {
    fs.updateJSON(self.rc.profile, { lastChannel: channel });
  } catch (err) {};

  // start fetching songs
  self.fetch(channel, account, function(err, songs, result) {
    if (err) {
      self.status = 'error';
      return menu.update(channel.index, color.red(err.toString()));
    }
    // mark PRO account on logo
    if (result && !result.warning) menu.update(0, color.inverse(' PRO '));
    self.status = 'ready';
    self.player = new Player(songs, {
      src: 'url',
      cache: true,
      downloads: self.home
    });
    self.player.play();
    self.player.on('downloading', function(url) {
      self.status = 'downloading';
      menu.update(channel.index, template.loading());
    });
    // update template
    self.player.on('playing', function(song) {
      var isValidSong = song.title && song.sid;
      self.status = 'playing';
      // update playing label
      menu.update(0, color.green('>'));
      // update song infomation
      menu.update(channel.index, template.song(song));
      // logging songs history
      if (isValidSong) {
        var updates = {};
        updates[song.sid] = song;
        try {
          fs.updateJSON(this.rc.history, updates);
        } catch (err) {
          // error must be logged in a private place.
        }
      }
      // print LRC if needed.
      if (self.isShowLrc) {
        if (self.lrc) self.lrc.stop();
        geci.fetch(song, function(err, lrc) {
          if (err) return menu.update(0, color.grey('抱歉, 没找到歌词 ' + err.toString()));
          if (!lrc) return menu.update(0, color.grey('抱歉, 没找到歌词'));
          self.lrc = geci.print(lrc, function(line, extra) {
            menu.update(channel.index, template.song(song, line));
          });
        });
      }
      // 没有对尝试获取列表失败进行处理，如果失败2次，则不会再播放任何歌曲
      if (song._id < self.player.list.length - 1) return false;
      return self.fetch(channel, account);
    });
  });
}

/**
*
* Add current song to lovelist when pressing `L`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.loving = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (!this.player.playing.sid) return this.menu.update(0, '未知曲目无法加心');
  if (!account) return false;
  var self = this;
  var menu = self.menu;
  var song = self.player.playing;
  var query = {
    sid: song.sid,
    channel: self.channel,
    user_id: account.user_id,
    expire: account.expire,
    token: account.token
  };
  if (song.like) query.type = 'u';
  menu.update(0, '正在加载...');
  sdk.love(query, function(err, result) {
    menu.clear(0);
    if (err) menu.update(0, '出错了, 请稍后再试...');
    if (!err) self.player.playing.like = !song.like;
    return menu.update(
      self.channel,
      template.song(self.player.playing, null, true) // keep silence, do not notify
    );
  });
}

/**
*
* Play the next song in the playlist
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.next = function(channel, account) {
  if (!this.player) return false;
  this.player.next(function(err, song) {
    if (err) menu.update(self.channel, color.yellow('这是最后一首了哦，回车以加载最新列表'));
    return false;
  });
}

/**
*
* Stop playing
* and show the stopped status on logo.
*
**/
Fm.prototype.stop = function() {
  if (!this.player) return false;
  var menu = this.menu;
  menu.clear(0);
  menu.update(0, template.pause());
  return this.player.stop();
}

/**
*
* Quit the Fm
* and kill the process when pressing `Q`
*
**/
Fm.prototype.quit = function() {
  this.menu.stop();
  return process.exit();
}

/**
*
* Goto the music album page when pressing `G`
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.go = function(channel, account) {
  if (!this.player) return false;
  if (!this.player.playing) return false;
  if (channel.channel_id == -99) return false;
  return utils.go(utils.album(this.player.playing.album));
}

/**
*
* Show lrc when when pressing `R`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.showLrc = function(channel, account) {
  if (channel.channel_id == -99) return false;
  this.isShowLrc = !!!this.isShowLrc;
  this.menu.clear(0);
  this.menu.update(0, this.isShowLrc ? '歌词开启' : '歌词关闭');
  return false;
}

/**
*
* Share the current playing songs to Weibo when pressing `S`.
* @channel[Object]
* @account[Object]
*
**/
Fm.prototype.share = function(channel, account) {
  if (!this.player || !this.player.playing) return false;
  return utils.go(template.share(this.player.playing));
}

/**
*
* Create command line interface menu
* using term-list-enhanced module
* @callback[Function]: the callback function when set down.
*
**/
Fm.prototype.createMenu = function(callback) {
  var self = this;
  // fetch channels
  sdk.fm.channels(function(err, list) {
    if (err) consoler.error('获取豆瓣电台频道出错，切换为本地电台...');
    // fetch configs, show user's infomations
    fs.readJSON(self.rc.profile, function(e, user) {
      var vaildAccount = user && user.account && user.account.user_name;
      var account = vaildAccount ? user.account : null;
      // init menu
      self.menu = new termList();
      var nav = [template.logo(account), sdk.mhz.localMhz];
      self.menu.adds(nav.concat(!err ? [sdk.mhz.privateMhz].concat(list) : []));
      // bind keypress events
      self.menu.on('keypress', function(key, index) {
        if (!shorthands[key.name]) return false;
        if (index < 1 && key.name != 'q') return utils.go(pkg.repository.url);
        return self[shorthands[key.name]](self.menu.items[index], account);
      });
      self.menu.on('empty', function() {
        self.menu.stop();
      });
      // check last played channel
      if (user && user.lastChannel) {
        self.play(user.lastChannel, account);
        self.menu.start(user.lastChannel.index);
        return false;
      }
      // start menu at line 2 (below the logo text)
      self.menu.start(1);
    });
  });
  // callback if necessary.
  return callback && callback();
}

/**
*
* Init douban.fm command line interface.
* @callback [Function]: the callback function when all set done
*
**/
Fm.prototype.init = function(callback) {
  var self = this;
  fs.exists(self.home, function(exist) {
    if (exist) return self.createMenu(callback);
    mkdirp(self.love, function(err) {
      if (err) return consoler.error('创建歌曲文件夹出错，请检查权限');
      return self.createMenu(callback);
    });
  });
}<|endoftext|>


<commit_before>servers['Route'].augment({
    assets: {
        styles: [
            require.resolve('../assets/css/reset.css'),
            require.resolve('../assets/css/controls.css'),
            require.resolve('../assets/css/style.css')
        ],
        scripts: [
            require.resolve('wax/ext/modestmaps.js'),
            require.resolve('wax/build/wax.mm.min.js')
        ]
    },
    initializeAssets: function(parent, app) {
        parent.call(this, app);
        this.get('/assets/tilestream/css/vendor.css',
            mirror.assets(this.assets.styles, { type: '.css' }));
        this.get('/assets/tilestream/js/vendor.js',
            mirror.assets(this.assets.scripts, { type: '.js' }));
    },
    initializeModels: function(parent, app) {
        this.models = app.models;
        _.bindAll(this, 'loadModel', 'getModel');
        this.get('/api/v1/:model/:id', this.loadModel, this.getModel);
        this.get('/api/v1/:collection', this.loadCollection.bind(this));
        parent.call(this, app);
    }
});
<commit_msg>add max-age in production<commit_after>var env = process.env.NODE_ENV || 'development';

servers['Route'].augment({
    assets: {
        styles: [
            require.resolve('../assets/css/reset.css'),
            require.resolve('../assets/css/controls.css'),
            require.resolve('../assets/css/style.css')
        ],
        scripts: [
            require.resolve('wax/ext/modestmaps.js'),
            require.resolve('wax/build/wax.mm.min.js')
        ]
    },
    initializeAssets: function(parent, app) {
        var maxAge = env == 'production' ? 3600 : 0;

        parent.call(this, app);
        this.get('/assets/tilestream/css/vendor.css',
            mirror.assets(this.assets.styles, { type: '.css', maxAge: maxAge }));
        this.get('/assets/tilestream/js/vendor.js',
            mirror.assets(this.assets.scripts, { type: '.js', maxAge: maxAge }));
    },
    initializeModels: function(parent, app) {
        this.models = app.models;
        _.bindAll(this, 'loadModel', 'getModel');
        this.get('/api/v1/:model/:id', this.loadModel, this.getModel);
        this.get('/api/v1/:collection', this.loadCollection.bind(this));
        parent.call(this, app);
    }
});
<|endoftext|>


<commit_before><commit_msg>Removed buggy line where node-ga was included<commit_after><|endoftext|>


<commit_before>const XLSX = require('xlsx');
const _ = require('underscore');
const db = require('../db');
const moment = require('moment');
const fs = require('fs');

module.exports = {
    orderList: async function(req, res, next) {
        try {
            let cno = req.get('CustomerNO') || req.params.CustomerNO;
            let data = await db.items.cfind({
                SK_BCODE: {
                    $exists: true
                },
                CreateTime: {
                    $gte: moment(0, "HH").toDate(),
                    $lte: new Date()
                },
                CustomerNO: cno
            }).sort({
                CreateTime: -1
            }).exec();

            _.each(data,function(d){
                if(!d.Price){
                    d.Price=0;
                }
            });

            let sorddt = await db.sorddt.find({
                OD_CTNO: cno
            });

            let sd = _.indexBy(sorddt, 'OD_SKNO');

            let sstock = await db.sstock.find({});

            let t = _.reduce(sstock, (ret, item, key) => {

                let mkey = `${item.OD_NO}-${item.OD_CTNO}-${item.FirstSale}
                        -${item.Price}-${item.OD_PRICE}-${item.SK_NO}-
                        -${item.SK_BCODE}-${item.SK_NAME}-${item.SK_SPEC}-
                        -${item.SK_UNIT}-${item.SK_SUPPNO}-${item.SK_SUPPNAME}-
                        -${item.SK_LOCATE}-${item.SK_LPRICE1}-${item.SK_LPRICE2}-
                        -${item.SK_IKIND}-${item.SK_NOWQTY}-${item.SK_KINDNAME}-`;

                if (ret[mkey]) {
                    ret[mkey]['Amount'] += item.Amount;
                } else {
                    ret[mkey] = item;
                }
                //join
                let join = sd[item.SK_NO];
                if (join) {
                    ret[mkey]['FirstSale'] = join.OD_PRICE!=undefined ? 0 : 1;
                    ret[mkey]['Price'] = join.OD_PRICE!=undefined ? join.OD_PRICE : join.SK_LPRICE2;
                    ret[mkey]['Amount'] = join.OD_QTY ? join.OD_QTY : 0;
                    ret[mkey]['SK_KINDNAME'] = item.SK_KINDNAME ? item.SK_KINDNAME : '';
                    ret[mkey]['OD_NO'] = join.OD_NO;
                    ret[mkey]['OD_PRICE'] = join.OD_PRICE;
                    ret[mkey]['OD_CTNO'] = join.OD_CTNO;
                } else {
                    ret[mkey]['FirstSale'] = 0;
                    ret[mkey]['Price'] = item.SK_LPRICE2;
                    ret[mkey]['Amount'] = 0;
                    ret[mkey]['SK_KINDNAME'] = item.SK_KINDNAME ? item.SK_KINDNAME : '';
                    ret[mkey]['OD_NO'] = null;
                    ret[mkey]['OD_PRICE'] = null;
                    ret[mkey]['OD_CTNO'] = null;
                }

                return ret;
            }, {});

            let result = _.values(t);

            let indexData = _.indexBy(data, 'SK_NO');

            let ret = _.reduce(result, (sum, value, key) => {
                let v = indexData[value['SK_NO']];
                if (v) {
                    value = _.extend(value, v, {
                        ordered: 1
                    });
                    //data = _.without(data,v);
                } else {
                    sum.push(value);
                }
                return sum;
            }, []);
            res.send(_.union(data, ret));
        } catch (e) {
            console.error(e);
            res.status(500).send(e);
        }

    },
    orderUpload: function(req, res, next) {
        let datas = req.body;
        _.each(datas, data => {
            data.CustomerNO = req.get('CustomerNO');
            data.CreateTime = new Date(data.CreateTime);
        })
        db.items.insert(datas, function(err, datas) {
            if (err) {
                res.send(err);
            } else {
                res.send(datas);
            }
        });
    },
    orderCreate: function(req, res, next) {
        let data = req.body;
        data.CustomerNO = req.get('CustomerNO');
        data.CreateTime = new Date();
        db.items.insert(data, function(err, data) {
            if (err) {
                res.status(500).send(err);
            } else {
                res.send(data);
            }
        });
    },
    orderUpdate: function(req, res, next) {
        let id = req.params.id;
        let amount = req.params.amount;
        let price = req.params.price;
        let memo = req.params.memo;
        db.items.update({
                _id: id
            }, {
                $set: {
                    Amount: amount,
                    Price: price,
                    Memo: memo
                }
            }, {},
            function(err, data) {
                if (err) {
                    res.status(500).send(err);
                } else {
                    res.send({
                        numUpdated: data
                    });
                }
            });
    },
    orderDelete: function(req, res, next) {
        db.items.remove({
            _id: req.params.id
        }, {}, function(err, numRemoved) {
            if (err) {
                res.status(500).send(err);
            } else {
                res.send({
                    numRemoved: numRemoved
                });
            }
        });
    },
    orderDaily: async function(req, res, next) {
        try {
            let result = await db.items.cfind({
                SK_BCODE: {
                    $exists: true
                }
            }).sort({
                CreateTime: -1
            }).exec();
            let v = _.reduce(result, (sum, data) => {
                let key = moment(data.CreateTime).format("YYYY-MM-DD");
                if (!sum[key]) {
                    //sum[key] = {key:key,start:moment(key).format("YYYY-MM-DD 00:00:00"),end:moment(key).format("YYYY-MM-DD 23:59:59")};
                    sum[key] = {
                        key: key,
                        start: moment(data.CreateTime),
                        end: moment(data.CreateTime),
                        Date: key,
                        Download: '下載'
                    }
                } else {
                    let dataDate = moment(data.CreateTime);
                    if (dataDate.isBefore(sum[key].start)) {
                        sum[key].start = dataDate;
                    } else if (dataDate.isAfter(sum[key].end)) {
                        sum[key].end = dataDate;
                    }
                }
                return sum;
            }, {});
            res.send(_.values(v));
        } catch (e) {
            console.error(e);
            res.status(500).send(e);
        }
    },
    orderHistory: async function(req, res, next) {
        try {
            let skno = req.params.no;
            let cno = req.get('CustomerNO') || req.params.CustomerNO;
            let result = await db.sorddt.cfind({
                OD_SKNO:skno,
                OD_CTNO:cno
            }).
            sort({
                OD_NO:1
            }).
            exec();
            res.send(result);
        } catch (e) {
            res.status(500).send(e);
        }
    },
    orderToExcle: function(req, res, next) {
        let start = req.params.start;
        let end = req.params.end;
        res.set("fileName", moment(start).format('YYYYMMDD'));
        db.items.cfind({
                $and: [{
                    CreateTime: {
                        $gte: moment(start).toDate(),
                        $lte: moment(end).toDate()
                    }
                }, {
                    SK_BCODE: {
                        $exists: true
                    }
                }]
            })
            .sort({
                CreateTime: -1
            }).exec(
                function(err, dataSet) {
                    //console.log(dataSet);
                    if (err) {
                        res.status(500).send(err);
                    } else {
                        let groupData = _.groupBy(dataSet, 'Customer');
                        let wb = {
                            SheetNames: [],
                            Sheets: {}
                        };

                        //案名稱不得使用下列字元: 大於或小於符號 (< >)、星號 ( * )、問號 ( ? )、雙引號 ( " )、分隔號或縱線字元 ( | )、冒號 ( : )、正斜線 ( / ) 或括弧 ( [] )。

                        function fixSheetName(name) {
                            name = name.replace(/[\[\]\:\<\>\*\/\|\?\"\\\：]/g, " ");
                            console.log(name);
                            return name.substring(0, 30);
                        }

                        _.each(groupData, (datas, key) => {
                            key = fixSheetName(key);
                            let _data = _.reduce(datas, function(sum, item) {
                                sum.push({
                                    '貨號': item.SK_NO,
                                    '條碼': item.SK_BCODE,
                                    '品名': item.SK_NAME,
                                    '價格': item.Price,
                                    '數量': item.Amount,
                                    '備註': `${item.SK_SPEC} ${item.Memo}`
                                });
                                return sum;
                            }, []);

                            //console.log(_data);

                            let _headers = [];
                            if (_data.length > 0) {
                                _headers = _.keys(_data[0]) || [];
                            }
                            let headers = _headers
                                .map((v, i) => Object.assign({}, {
                                    v: v,
                                    position: String.fromCharCode(65 + i) + 1
                                }))
                                .reduce((prev, next) => Object.assign({}, prev, {
                                    [next.position]: {
                                        v: next.v
                                    }
                                }), {});
                            let data = _data
                                .map((v, i) => _headers.map((k, j) => Object.assign({}, {
                                    v: v[k],
                                    position: String.fromCharCode(65 + j) + (i + 2)
                                })))
                                .reduce((prev, next) => prev.concat(next))
                                .reduce((prev, next) => Object.assign({}, prev, {
                                    [next.position]: {
                                        v: next.v
                                    }
                                }), {});
                            let output = Object.assign({}, headers, data);
                            let outputPos = Object.keys(output);
                            let ref = outputPos[0] + ':' + outputPos[outputPos.length - 1];

                            wb.SheetNames.push(key);
                            wb.Sheets[key] = Object.assign({}, output, {
                                '!ref': ref,
                                '!cols': [{
                                    wch: 8
                                }, {
                                    wch: 20
                                }, {
                                    wch: 30
                                }, {
                                    wch: 5
                                }, {
                                    wch: 5
                                }, {
                                    wch: 60
                                }, {
                                    wch: 30
                                }],
                                /*'!rows': [{
                                    hpt: 10
                                },{
                                    hpt: 20
                                },{
                                    hpt: 30
                                },{
                                    hpt: 40
                                },{
                                    hpt: 50
                                },{
                                    hpt: 60
                                },{
                                    hpt: 70
                                }],*/
                                '!images': [{
                                    /*name: 'text.png',
                                    fs.readFileSync('text.png');
                                    data: pic.toString('base64'),
                                    opts: {
                                        base64: true
                                    },
                                    position: {
                                        type: 'twoCellAnchor',
                                        attrs: {
                                            editAs: 'oneCell'
                                        },
                                        from: {
                                            col: 6,
                                            row: 1
                                        },
                                        to: {
                                            col: 7,
                                            row: 5
                                        }
                                    }*/
                                }]
                            });

                        });
                        let fileName = "./temp/" + new Date().getTime() + '.xlsx';
                        XLSX.writeFile(wb, fileName);
                        res.download(fileName);
                    }
                });
    }
};<commit_msg>price bug<commit_after>const XLSX = require('xlsx');
const _ = require('underscore');
const db = require('../db');
const moment = require('moment');
const fs = require('fs');

module.exports = {
    orderList: async function(req, res, next) {
        try {
            let cno = req.get('CustomerNO') || req.params.CustomerNO;
            let data = await db.items.cfind({
                SK_BCODE: {
                    $exists: true
                },
                CreateTime: {
                    $gte: moment(0, "HH").toDate(),
                    $lte: new Date()
                },
                CustomerNO: cno
            }).sort({
                CreateTime: -1
            }).exec();

            _.each(data,function(d){
                if(!d.Price){
                    d.Price=0;
                }
            });

            let sorddt = await db.sorddt.find({
                OD_CTNO: cno
            });

            let sd = _.indexBy(sorddt, 'OD_SKNO');

            let sstock = await db.sstock.find({});

            let t = _.reduce(sstock, (ret, item, key) => {

                let mkey = `${item.OD_NO}-${item.OD_CTNO}-${item.FirstSale}
                        -${item.Price}-${item.OD_PRICE}-${item.SK_NO}-
                        -${item.SK_BCODE}-${item.SK_NAME}-${item.SK_SPEC}-
                        -${item.SK_UNIT}-${item.SK_SUPPNO}-${item.SK_SUPPNAME}-
                        -${item.SK_LOCATE}-${item.SK_LPRICE1}-${item.SK_LPRICE2}-
                        -${item.SK_IKIND}-${item.SK_NOWQTY}-${item.SK_KINDNAME}-`;

                if (ret[mkey]) {
                    ret[mkey]['Amount'] += item.Amount;
                } else {
                    ret[mkey] = item;
                    delete ret[mkey]._id;
                }
                //join
                let join = sd[item.SK_NO];
                if (join) {
                    ret[mkey]['FirstSale'] = join.OD_PRICE!=undefined ? 0 : 1;
                    ret[mkey]['Price'] = join.OD_PRICE!=undefined ? join.OD_PRICE : join.SK_LPRICE2;
                    ret[mkey]['Amount'] = join.OD_QTY ? join.OD_QTY : 0;
                    ret[mkey]['SK_KINDNAME'] = item.SK_KINDNAME ? item.SK_KINDNAME : '';
                    ret[mkey]['OD_NO'] = join.OD_NO;
                    ret[mkey]['OD_PRICE'] = join.OD_PRICE;
                    ret[mkey]['OD_CTNO'] = join.OD_CTNO;
                } else {
                    ret[mkey]['FirstSale'] = 0;
                    ret[mkey]['Price'] = item.SK_LPRICE2;
                    ret[mkey]['Amount'] = 0;
                    ret[mkey]['SK_KINDNAME'] = item.SK_KINDNAME ? item.SK_KINDNAME : '';
                    ret[mkey]['OD_NO'] = null;
                    ret[mkey]['OD_PRICE'] = null;
                    ret[mkey]['OD_CTNO'] = null;
                }

                return ret;
            }, {});

            let result = _.values(t);

            let indexData = _.indexBy(data, 'SK_NO');

            let ret = _.reduce(result, (sum, value, key) => {
                let v = indexData[value['SK_NO']];
                if (v) {
                    value = _.extend(value, v, {
                        ordered: 1
                    });
                    //data = _.without(data,v);
                } else {
                    sum.push(value);
                }
                return sum;
            }, []);
            res.send(_.union(data, ret));
        } catch (e) {
            console.error(e);
            res.status(500).send(e);
        }

    },
    orderUpload: function(req, res, next) {
        let datas = req.body;
        _.each(datas, data => {
            data.CustomerNO = req.get('CustomerNO');
            data.CreateTime = new Date(data.CreateTime);
        })
        db.items.insert(datas, function(err, datas) {
            if (err) {
                res.send(err);
            } else {
                res.send(datas);
            }
        });
    },
    orderCreate: function(req, res, next) {
        let data = req.body;
        data.CustomerNO = req.get('CustomerNO');
        data.CreateTime = new Date();
        db.items.insert(data, function(err, data) {
            if (err) {
                res.status(500).send(err);
            } else {
                res.send(data);
            }
        });
    },
    orderUpdate: function(req, res, next) {
        let id = req.params.id;
        let amount = req.params.amount;
        let price = req.params.price;
        let memo = req.params.memo;
        db.items.update({
                _id: id
            }, {
                $set: {
                    Amount: amount,
                    Price: price,
                    Memo: memo
                }
            }, {},
            function(err, data) {
                if (err) {
                    res.status(500).send(err);
                } else {
                    res.send({
                        numUpdated: data
                    });
                }
            });
    },
    orderDelete: function(req, res, next) {
        db.items.remove({
            _id: req.params.id
        }, {}, function(err, numRemoved) {
            if (err) {
                res.status(500).send(err);
            } else {
                res.send({
                    numRemoved: numRemoved
                });
            }
        });
    },
    orderDaily: async function(req, res, next) {
        try {
            let result = await db.items.cfind({
                SK_BCODE: {
                    $exists: true
                }
            }).sort({
                CreateTime: -1
            }).exec();
            let v = _.reduce(result, (sum, data) => {
                let key = moment(data.CreateTime).format("YYYY-MM-DD");
                if (!sum[key]) {
                    //sum[key] = {key:key,start:moment(key).format("YYYY-MM-DD 00:00:00"),end:moment(key).format("YYYY-MM-DD 23:59:59")};
                    sum[key] = {
                        key: key,
                        start: moment(data.CreateTime),
                        end: moment(data.CreateTime),
                        Date: key,
                        Download: '下載'
                    }
                } else {
                    let dataDate = moment(data.CreateTime);
                    if (dataDate.isBefore(sum[key].start)) {
                        sum[key].start = dataDate;
                    } else if (dataDate.isAfter(sum[key].end)) {
                        sum[key].end = dataDate;
                    }
                }
                return sum;
            }, {});
            res.send(_.values(v));
        } catch (e) {
            console.error(e);
            res.status(500).send(e);
        }
    },
    orderHistory: async function(req, res, next) {
        try {
            let skno = req.params.no;
            let cno = req.get('CustomerNO') || req.params.CustomerNO;
            let result = await db.sorddt.cfind({
                OD_SKNO:skno,
                OD_CTNO:cno
            }).
            sort({
                OD_NO:1
            }).
            exec();
            res.send(result);
        } catch (e) {
            res.status(500).send(e);
        }
    },
    orderToExcle: function(req, res, next) {
        let start = req.params.start;
        let end = req.params.end;
        res.set("fileName", moment(start).format('YYYYMMDD'));
        db.items.cfind({
                $and: [{
                    CreateTime: {
                        $gte: moment(start).toDate(),
                        $lte: moment(end).toDate()
                    }
                }, {
                    SK_BCODE: {
                        $exists: true
                    }
                }]
            })
            .sort({
                CreateTime: -1
            }).exec(
                function(err, dataSet) {
                    //console.log(dataSet);
                    if (err) {
                        res.status(500).send(err);
                    } else {
                        let groupData = _.groupBy(dataSet, 'Customer');
                        let wb = {
                            SheetNames: [],
                            Sheets: {}
                        };

                        //案名稱不得使用下列字元: 大於或小於符號 (< >)、星號 ( * )、問號 ( ? )、雙引號 ( " )、分隔號或縱線字元 ( | )、冒號 ( : )、正斜線 ( / ) 或括弧 ( [] )。

                        function fixSheetName(name) {
                            name = name.replace(/[\[\]\:\<\>\*\/\|\?\"\\\：]/g, " ");
                            console.log(name);
                            return name.substring(0, 30);
                        }

                        _.each(groupData, (datas, key) => {
                            key = fixSheetName(key);
                            let _data = _.reduce(datas, function(sum, item) {
                                sum.push({
                                    '貨號': item.SK_NO,
                                    '條碼': item.SK_BCODE,
                                    '品名': item.SK_NAME,
                                    '價格': item.Price,
                                    '數量': item.Amount,
                                    '備註': `${item.SK_SPEC} ${item.Memo}`
                                });
                                return sum;
                            }, []);

                            //console.log(_data);

                            let _headers = [];
                            if (_data.length > 0) {
                                _headers = _.keys(_data[0]) || [];
                            }
                            let headers = _headers
                                .map((v, i) => Object.assign({}, {
                                    v: v,
                                    position: String.fromCharCode(65 + i) + 1
                                }))
                                .reduce((prev, next) => Object.assign({}, prev, {
                                    [next.position]: {
                                        v: next.v
                                    }
                                }), {});
                            let data = _data
                                .map((v, i) => _headers.map((k, j) => Object.assign({}, {
                                    v: v[k],
                                    position: String.fromCharCode(65 + j) + (i + 2)
                                })))
                                .reduce((prev, next) => prev.concat(next))
                                .reduce((prev, next) => Object.assign({}, prev, {
                                    [next.position]: {
                                        v: next.v
                                    }
                                }), {});
                            let output = Object.assign({}, headers, data);
                            let outputPos = Object.keys(output);
                            let ref = outputPos[0] + ':' + outputPos[outputPos.length - 1];

                            wb.SheetNames.push(key);
                            wb.Sheets[key] = Object.assign({}, output, {
                                '!ref': ref,
                                '!cols': [{
                                    wch: 8
                                }, {
                                    wch: 20
                                }, {
                                    wch: 30
                                }, {
                                    wch: 5
                                }, {
                                    wch: 5
                                }, {
                                    wch: 60
                                }, {
                                    wch: 30
                                }],
                                /*'!rows': [{
                                    hpt: 10
                                },{
                                    hpt: 20
                                },{
                                    hpt: 30
                                },{
                                    hpt: 40
                                },{
                                    hpt: 50
                                },{
                                    hpt: 60
                                },{
                                    hpt: 70
                                }],*/
                                '!images': [{
                                    /*name: 'text.png',
                                    fs.readFileSync('text.png');
                                    data: pic.toString('base64'),
                                    opts: {
                                        base64: true
                                    },
                                    position: {
                                        type: 'twoCellAnchor',
                                        attrs: {
                                            editAs: 'oneCell'
                                        },
                                        from: {
                                            col: 6,
                                            row: 1
                                        },
                                        to: {
                                            col: 7,
                                            row: 5
                                        }
                                    }*/
                                }]
                            });

                        });
                        let fileName = "./temp/" + new Date().getTime() + '.xlsx';
                        XLSX.writeFile(wb, fileName);
                        res.download(fileName);
                    }
                });
    }
};<|endoftext|>


<commit_before>const fs = require('fs');
const path = require('path');
const {promisify} = require('util');
const schedule = require('node-schedule');
const {sortBy} = require('lodash');

class Counter {
    constructor(name) {
        this.name = name;
        this.map = new Map();
        this.load();
    }

    increment(key) {
        if (this.map.has(key)) {
            this.map.set(key, this.map.get(key) + 1);
        } else {
            this.map.set(key, 1);
        }

        this.save();
    }

    clear() {
        this.map = new Map();
        this.save();
    }

    entries() {
        const keys = Array.from(this.map.keys());
        const sortedKeys = sortBy(keys, (key) => this.map.get(key)).reverse();
        return sortedKeys.map((key) => [key, this.map.get(key)]);
    }

    async save() {
        const filePath = path.join(__dirname, `${this.name}.json`);
        await promisify(fs.writeFile)(filePath, JSON.stringify(Array.from(this.map.entries())));
    }

    async load() {
        const filePath = path.join(__dirname, `${this.name}.json`);

        const exists = await new Promise((resolve) => {
            fs.access(filePath, fs.constants.F_OK, (error) => {
                resolve(!Boolean(error));
            });
        });

        if (exists) {
            const data = await promisify(fs.readFile)(filePath);
            this.map = new Map(JSON.parse(data));
        } else {
            this.map = new Map();
        }
    }
}

module.exports = (clients) => {
    const { rtmClient: rtm, webClient: slack } = clients;

    const sushiCounter = new Counter('sushi');
    const suspendCounter = new Counter('suspend');

    rtm.on('message', async (message) => {
        const { channel, text, user, ts: timestamp } = message;
        if (!text) {
            return;
        }

        let rtext = text;
        rtext = rtext.
            replace(/鮨/g, 'すし').
            replace(/(su|zu|ス|ズ|ず|寿|壽)/gi, 'す').
            replace(/(sh?i|ci|し|シ|司)/gi, 'し');

        rtext = rtext.
            replace(/(ca|(ke|け|ケ)(i|ぃ|い|ｨ|ィ|ｲ|イ|e|ぇ|え|ｪ|ェ|ｴ|エ|-|ー))(ki|ke|き|キ)/gi, 'ケーキ');

        rtext = rtext.
            replace(/akouryyy/gi, 'akkoury').
            replace(/akouryy/gi, '').
            replace(/kk/gi, 'k').
            replace(/rr/gi, 'r').
            replace(/y/gi, 'yy');

        if (rtext.includes("すし")) {
            slack.reactions.add({name: 'sushi', channel, timestamp});
            sushiCounter.increment(user);
        }
        if (rtext.includes("ケーキ")) {
            slack.reactions.add({name: 'cake', channel, timestamp});
        }
        if (rtext.includes("殺") || rtext.includes("死")) {
            slack.reactions.add({name: 'no_good', channel, timestamp});
            slack.reactions.add({name: 'shaved_ice', channel, timestamp});
            suspendCounter.increment(user);
        }
        if (rtext.includes("akouryy")) {
            slack.reactions.add({name: 'no_good', channel, timestamp});
            slack.reactions.add({name: 'akouryy', channel, timestamp});
        }
    });

    schedule.scheduleJob('0 19 * * 0', async () => {
        const {members} = await slack.users.list();

        await slack.chat.postMessage({
            channel: process.env.CHANNEL_SANDBOX,
            username: 'sushi-bot',
            text: '今週の凍結ランキング',
            icon_emoji: ':shaved_ice:',
            attachments: suspendCounter.entries().map(([user, count], index) => {
                const member = members.find(({id}) => id === user);
                const name = member.profile.display_name || member.name;

                return {
                    author_name: `${index + 1}位: ${name} (${count}回)`,
                    author_icon: member.profile.image_24,
                };
            })
        });

        suspendCounter.clear();

        await slack.chat.postMessage({
            channel: process.env.CHANNEL_SANDBOX,
            username: 'sushi-bot',
            text: '今週の寿司ランキング',
            icon_emoji: ':sushi:',
            attachments: sushiCounter.entries().map(([user, count], index) => {
                const member = members.find(({id}) => id === user);
                const name = member.profile.display_name || member.name;

                return {
                    author_name: `${index + 1}位: ${name} (${count + 1}回)`,
                    author_icon: member.profile.image_24,
                };
            })
        });

        sushiCounter.clear();
    });
};
<commit_msg>sushi-bot: 全身を得る<commit_after>const fs = require('fs');
const path = require('path');
const {promisify} = require('util');
const schedule = require('node-schedule');
const {sortBy} = require('lodash');

class Counter {
    constructor(name) {
        this.name = name;
        this.map = new Map();
        this.load();
    }

    increment(key) {
        if (this.map.has(key)) {
            this.map.set(key, this.map.get(key) + 1);
        } else {
            this.map.set(key, 1);
        }

        this.save();
    }

    clear() {
        this.map = new Map();
        this.save();
    }

    entries() {
        const keys = Array.from(this.map.keys());
        const sortedKeys = sortBy(keys, (key) => this.map.get(key)).reverse();
        return sortedKeys.map((key) => [key, this.map.get(key)]);
    }

    async save() {
        const filePath = path.join(__dirname, `${this.name}.json`);
        await promisify(fs.writeFile)(filePath, JSON.stringify(Array.from(this.map.entries())));
    }

    async load() {
        const filePath = path.join(__dirname, `${this.name}.json`);

        const exists = await new Promise((resolve) => {
            fs.access(filePath, fs.constants.F_OK, (error) => {
                resolve(!Boolean(error));
            });
        });

        if (exists) {
            const data = await promisify(fs.readFile)(filePath);
            this.map = new Map(JSON.parse(data));
        } else {
            this.map = new Map();
        }
    }
}

module.exports = (clients) => {
    const { rtmClient: rtm, webClient: slack } = clients;

    const sushiCounter = new Counter('sushi');
    const suspendCounter = new Counter('suspend');

    rtm.on('message', async (message) => {
        const { channel, text, user, ts: timestamp } = message;
        if (!text) {
            return;
        }

        let rtext = text;
        rtext = rtext.
            replace(/鮨/g, 'すし').
            replace(/(su|zu|ス|ズ|ず|寿|壽)/gi, 'す').
            replace(/(sh?i|ci|し|シ|司)/gi, 'し');

        rtext = rtext.
            replace(/(ca|(ke|け|ケ)(i|ぃ|い|ｨ|ィ|ｲ|イ|e|ぇ|え|ｪ|ェ|ｴ|エ|-|ー))(ki|ke|き|キ)/gi, 'ケーキ');

        rtext = rtext.
            replace(/akouryyy/gi, 'akkoury').
            replace(/akouryy/gi, '').
            replace(/kk/gi, 'k').
            replace(/rr/gi, 'r').
            replace(/y/gi, 'yy');

        if (rtext.includes("すし")) {
            slack.reactions.add({name: 'sushi', channel, timestamp});
            sushiCounter.increment(user);
        }
        if (rtext.includes("ケーキ")) {
            slack.reactions.add({name: 'cake', channel, timestamp});
        }
        if (rtext.includes("殺") || rtext.includes("死")) {
            slack.reactions.add({name: 'no_good', channel, timestamp});
            slack.reactions.add({name: 'shaved_ice', channel, timestamp});
            suspendCounter.increment(user);
        }
        if (rtext.includes("akouryy")) {
            slack.reactions.add({name: 'no_good', channel, timestamp});
            slack.reactions.add({name: 'akouryy', channel, timestamp});
        }
    });

    schedule.scheduleJob('0 19 * * 0', async () => {
        const {members} = await slack.users.list();

        await slack.chat.postMessage({
            channel: process.env.CHANNEL_SANDBOX,
            username: 'sushi-bot',
            text: '今週の凍結ランキング',
            icon_emoji: ':shaved_ice:',
            attachments: suspendCounter.entries().map(([user, count], index) => {
                const member = members.find(({id}) => id === user);
                const name = member.profile.display_name || member.name;

                return {
                    author_name: `${index + 1}位: ${name} (${count}回)`,
                    author_icon: member.profile.image_24,
                };
            })
        });

        suspendCounter.clear();

        await slack.chat.postMessage({
            channel: process.env.CHANNEL_SANDBOX,
            username: 'sushi-bot',
            text: '今週の寿司ランキング',
            icon_emoji: ':sushi:',
            attachments: sushiCounter.entries().map(([user, count], index) => {
                const member = members.find(({id}) => id === user);
                const name = member.profile.display_name || member.name;

                return {
                    author_name: `${index + 1}位: ${name} (${count}回)`,
                    author_icon: member.profile.image_24,
                };
            })
        });

        sushiCounter.clear();
    });
};
<|endoftext|>