<commit_before>const { escapeMarkdown } = require('discord.js');
const { oneLine, stripIndents } = require('common-tags');
const ArgumentUnionType = require('../types/union');

/** A fancy argument */
class Argument {
	/**
	 * @typedef {Object} ArgumentInfo
	 * @property {string} key - Key for the argument
	 * @property {string} [label=key] - Label for the argument
	 * @property {string} prompt - First prompt for the argument when it wasn't specified
	 * @property {string} [error] - Predefined error message to output for the argument when it isn't valid
	 * @property {string} [type] - Type of the argument (must be the ID of one of the registered argument types -
	 * see {@link CommandRegistry#registerDefaultTypes} for the built-in types)
	 * @property {number} [max] - If type is `integer` or `float`, this is the maximum value of the number.
	 * If type is `string`, this is the maximum length of the string.
	 * @property {number} [min] - If type is `integer` or `float`, this is the minimum value of the number.
	 * If type is `string`, this is the minimum length of the string.
	 * @property {ArgumentDefault} [default] - Default value for the argument (makes the arg optional - cannot be `null`)
	 * @property {boolean} [infinite=false] - Whether the argument accepts infinite values
	 * @property {Function} [validate] - Validator function for the argument (see {@link ArgumentType#validate})
	 * @property {Function} [parse] - Parser function for the argument (see {@link ArgumentType#parse})
	 * @property {Function} [isEmpty] - Empty checker for the argument (see {@link ArgumentType#isEmpty})
	 * @property {number} [wait=30] - How long to wait for input (in seconds)
	 */

	/**
	 * Either a value or a function that returns a value. The function is passed the CommandMessage and the Argument.
	 * @typedef {*|Function} ArgumentDefault
	 */

	/**
	 * @param {CommandoClient} client - Client the argument is for
	 * @param {ArgumentInfo} info - Information for the command argument
	 */
	constructor(client, info) {
		this.constructor.validateInfo(client, info);

		/**
		 * Key for the argument
		 * @type {string}
		 */
		this.key = info.key;

		/**
		 * Label for the argument
		 * @type {string}
		 */
		this.label = info.label || info.key;

		/**
		 * Question prompt for the argument
		 * @type {string}
		 */
		this.prompt = info.prompt;

		/**
		 * Error message for when a value is invalid
		 * @type {?string}
		 */
		this.error = info.error || null;

		/**
		 * Type of the argument
		 * @type {?ArgumentType}
		 */
		this.type = this.constructor.determineType(client, info.type);

		/**
		 * If type is `integer` or `float`, this is the maximum value of the number.
		 * If type is `string`, this is the maximum length of the string.
		 * @type {?number}
		 */
		this.max = typeof info.max !== 'undefined' ? info.max : null;

		/**
		 * If type is `integer` or `float`, this is the minimum value of the number.
		 * If type is `string`, this is the minimum length of the string.
		 * @type {?number}
		 */
		this.min = typeof info.min !== 'undefined' ? info.min : null;

		/**
		 * The default value for the argument
		 * @type {?ArgumentDefault}
		 */
		this.default = typeof info.default !== 'undefined' ? info.default : null;

		/**
		 * Whether the argument accepts an infinite number of values
		 * @type {boolean}
		 */
		this.infinite = Boolean(info.infinite);

		/**
		 * Validator function for validating a value for the argument
		 * @type {?Function}
		 * @see {@link ArgumentType#validate}
		 */
		this.validator = info.validate || null;

		/**
		 * Parser function for parsing a value for the argument
		 * @type {?Function}
		 * @see {@link ArgumentType#parse}
		 */
		this.parser = info.parse || null;

		/**
		 * Function to check whether a raw value is considered empty
		 * @type {?Function}
		 * @see {@link ArgumentType#isEmpty}
		 */
		this.emptyChecker = info.isEmpty || null;

		/**
		 * How long to wait for input (in seconds)
		 * @type {number}
		 */
		this.wait = typeof info.wait !== 'undefined' ? info.wait : 30;
	}

	/**
	 * Result object from obtaining a single {@link Argument}'s value(s)
	 * @typedef {Object} ArgumentResult
	 * @property {?*|?Array<*>} value - Final value(s) for the argument
	 * @property {?string} cancelled - One of:
	 * - `user` (user cancelled)
	 * - `time` (wait time exceeded)
	 * - `promptLimit` (prompt limit exceeded)
	 * @property {Message[]} prompts - All messages that were sent to prompt the user
	 * @property {Message[]} answers - All of the user's messages that answered a prompt
	 */

	/**
	 * Prompts the user and obtains the value for the argument
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @param {string} [value] - Pre-provided value for the argument
	 * @param {number} [promptLimit=Infinity] - Maximum number of times to prompt for the argument
	 * @return {Promise<ArgumentResult>}
	 */
	async obtain(msg, value, promptLimit = Infinity) {
		let empty = this.isEmpty(value, msg);
		if(empty && this.default !== null) {
			return {
				value: typeof this.default === 'function' ? await this.default(msg, this) : this.default,
				cancelled: null,
				prompts: [],
				answers: []
			};
		}
		if(this.infinite) return this.obtainInfinite(msg, value, promptLimit);

		const wait = this.wait > 0 && this.wait !== Infinity ? this.wait * 1000 : undefined;
		const prompts = [];
		const answers = [];
		let valid = !empty ? await this.validate(value, msg) : false;

		while(!valid || typeof valid === 'string') {
			/* eslint-disable no-await-in-loop */
			if(prompts.length >= promptLimit) {
				return {
					value: null,
					cancelled: 'promptLimit',
					prompts,
					answers
				};
			}

			// Prompt the user for a new value
			prompts.push(await msg.reply(stripIndents`
				${empty ? this.prompt : valid ? valid : `You provided an invalid ${this.label}. Please try again.`}
				${oneLine`
					Respond with \`cancel\` to cancel the command.
					${wait ? `The command will automatically be cancelled in ${this.wait} seconds.` : ''}
				`}
			`));

			// Get the user's response
			const responses = await msg.channel.awaitMessages(msg2 => msg2.author.id === msg.author.id, {
				max: 1,
				time: wait
			});

			// Make sure they actually answered
			if(responses && responses.size === 1) {
				answers.push(responses.first());
				value = answers[answers.length - 1].content;
			} else {
				return {
					value: null,
					cancelled: 'time',
					prompts,
					answers
				};
			}

			// See if they want to cancel
			if(value.toLowerCase() === 'cancel') {
				return {
					value: null,
					cancelled: 'user',
					prompts,
					answers
				};
			}

			empty = this.isEmpty(value, msg);
			valid = await this.validate(value, msg);
			/* eslint-enable no-await-in-loop */
		}

		return {
			value: await this.parse(value, msg),
			cancelled: null,
			prompts,
			answers
		};
	}

	/**
	 * Prompts the user and obtains multiple values for the argument
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @param {string[]} [values] - Pre-provided values for the argument
	 * @param {number} [promptLimit=Infinity] - Maximum number of times to prompt for the argument
	 * @return {Promise<ArgumentResult>}
	 * @private
	 */
	async obtainInfinite(msg, values, promptLimit = Infinity) { // eslint-disable-line complexity
		const wait = this.wait > 0 && this.wait !== Infinity ? this.wait * 1000 : undefined;
		const results = [];
		const prompts = [];
		const answers = [];
		let currentVal = 0;

		while(true) { // eslint-disable-line no-constant-condition
			/* eslint-disable no-await-in-loop */
			let value = values && values[currentVal] ? values[currentVal] : null;
			let valid = value ? await this.validate(value, msg) : false;
			let attempts = 0;

			while(!valid || typeof valid === 'string') {
				attempts++;
				if(attempts > promptLimit) {
					return {
						value: null,
						cancelled: 'promptLimit',
						prompts,
						answers
					};
				}

				// Prompt the user for a new value
				if(value) {
					const escaped = escapeMarkdown(value).replace(/@/g, '@\u200b');
					prompts.push(await msg.reply(stripIndents`
						${valid ? valid : oneLine`
							You provided an invalid ${this.label},
							"${escaped.length < 1850 ? escaped : '[too long to show]'}".
							Please try again.
						`}
						${oneLine`
							Respond with \`cancel\` to cancel the command, or \`finish\` to finish entry up to this point.
							${wait ? `The command will automatically be cancelled in ${this.wait} seconds.` : ''}
						`}
					`));
				} else if(results.length === 0) {
					prompts.push(await msg.reply(stripIndents`
						${this.prompt}
						${oneLine`
							Respond with \`cancel\` to cancel the command, or \`finish\` to finish entry.
							${wait ? `The command will automatically be cancelled in ${this.wait} seconds, unless you respond.` : ''}
						`}
					`));
				}

				// Get the user's response
				const responses = await msg.channel.awaitMessages(msg2 => msg2.author.id === msg.author.id, {
					max: 1,
					time: wait
				});

				// Make sure they actually answered
				if(responses && responses.size === 1) {
					answers.push(responses.first());
					value = answers[answers.length - 1].content;
				} else {
					return {
						value: null,
						cancelled: 'time',
						prompts,
						answers
					};
				}

				// See if they want to finish or cancel
				const lc = value.toLowerCase();
				if(lc === 'finish') {
					return {
						value: results.length > 0 ? results : null,
						cancelled: results.length > 0 ? null : 'user',
						prompts,
						answers
					};
				}
				if(lc === 'cancel') {
					return {
						value: null,
						cancelled: 'user',
						prompts,
						answers
					};
				}

				valid = await this.validate(value, msg);
			}

			results.push(await this.parse(value, msg));

			if(values) {
				currentVal++;
				if(currentVal === values.length) {
					return {
						value: results,
						cancelled: null,
						prompts,
						answers
					};
				}
			}
			/* eslint-enable no-await-in-loop */
		}
	}

	/**
	 * Checks if a value is valid for the argument
	 * @param {string} value - Value to check
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @return {boolean|string|Promise<boolean|string>}
	 */
	validate(value, msg) {
		const valid = this.validator ? this.validator(value, msg, this) : this.type.validate(value, msg, this);
		if(!valid || typeof valid === 'string') return this.error || valid;
		return valid;
	}

	/**
	 * Parses a value string into a proper value for the argument
	 * @param {string} value - Value to parse
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @return {*|Promise<*>}
	 */
	parse(value, msg) {
		if(this.parser) return this.parser(value, msg, this);
		return this.type.parse(value, msg, this);
	}

	/**
	 * Checks whether a value for the argument is considered to be empty
	 * @param {string} value - Value to check for emptiness
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @return {boolean}
	 */
	isEmpty(value, msg) {
		if(this.emptyChecker) return this.emptyChecker(value, msg, this);
		if(this.type) return this.type.isEmpty(value, msg, this);
		return !value;
	}

	/**
	 * Validates the constructor parameters
	 * @param {CommandoClient} client - Client to validate
	 * @param {ArgumentInfo} info - Info to validate
	 * @private
	 */
	static validateInfo(client, info) {
		if(!client) throw new Error('The argument client must be specified.');
		if(typeof info !== 'object') throw new TypeError('Argument info must be an Object.');
		if(typeof info.key !== 'string') throw new TypeError('Argument key must be a string.');
		if(info.label && typeof info.label !== 'string') throw new TypeError('Argument label must be a string.');
		if(typeof info.prompt !== 'string') throw new TypeError('Argument prompt must be a string.');
		if(info.error && typeof info.error !== 'string') throw new TypeError('Argument error must be a string.');
		if(info.type && typeof info.type !== 'string') throw new TypeError('Argument type must be a string.');
		if(info.type && !info.type.includes('|') && !client.registry.types.has(info.type)) {
			throw new RangeError(`Argument type "${info.type}" isn't registered.`);
		}
		if(!info.type && !info.validate) {
			throw new Error('Argument must have either "type" or "validate" specified.');
		}
		if(info.validate && typeof info.validate !== 'function') {
			throw new TypeError('Argument validate must be a function.');
		}
		if(info.parse && typeof info.parse !== 'function') {
			throw new TypeError('Argument parse must be a function.');
		}
		if(!info.type && (!info.validate || !info.parse)) {
			throw new Error('Argument must have both validate and parse since it doesn\'t have a type.');
		}
		if(typeof info.wait !== 'undefined' && (typeof info.wait !== 'number' || Number.isNaN(info.wait))) {
			throw new TypeError('Argument wait must be a number.');
		}
	}

	/**
	 * Gets the argument type to use from an ID
	 * @param {string} id - ID of the type to use
	 * @returns {?ArgumentType}
	 * @private
	 */
	static determineType(client, id) {
		if(!id) return null;
		if(!id.includes('|')) return client.registry.types.get(id);

		let type = client.registry.types.get(id);
		if(type) return type;
		type = new ArgumentUnionType(client, id);
		client.registry.registerType(type);
		return type;
	}
}

module.exports = Argument;
<commit_msg>Add brief union type info<commit_after>const { escapeMarkdown } = require('discord.js');
const { oneLine, stripIndents } = require('common-tags');
const ArgumentUnionType = require('../types/union');

/** A fancy argument */
class Argument {
	/**
	 * @typedef {Object} ArgumentInfo
	 * @property {string} key - Key for the argument
	 * @property {string} [label=key] - Label for the argument
	 * @property {string} prompt - First prompt for the argument when it wasn't specified
	 * @property {string} [error] - Predefined error message to output for the argument when it isn't valid
	 * @property {string} [type] - Type of the argument (must be the ID of one of the registered argument types
	 * or multiple IDs in order of priority separated by `|` for a union type - see
	 * {@link CommandRegistry#registerDefaultTypes} for the built-in types)
	 * @property {number} [max] - If type is `integer` or `float`, this is the maximum value of the number.
	 * If type is `string`, this is the maximum length of the string.
	 * @property {number} [min] - If type is `integer` or `float`, this is the minimum value of the number.
	 * If type is `string`, this is the minimum length of the string.
	 * @property {ArgumentDefault} [default] - Default value for the argument (makes the arg optional - cannot be `null`)
	 * @property {boolean} [infinite=false] - Whether the argument accepts infinite values
	 * @property {Function} [validate] - Validator function for the argument (see {@link ArgumentType#validate})
	 * @property {Function} [parse] - Parser function for the argument (see {@link ArgumentType#parse})
	 * @property {Function} [isEmpty] - Empty checker for the argument (see {@link ArgumentType#isEmpty})
	 * @property {number} [wait=30] - How long to wait for input (in seconds)
	 */

	/**
	 * Either a value or a function that returns a value. The function is passed the CommandMessage and the Argument.
	 * @typedef {*|Function} ArgumentDefault
	 */

	/**
	 * @param {CommandoClient} client - Client the argument is for
	 * @param {ArgumentInfo} info - Information for the command argument
	 */
	constructor(client, info) {
		this.constructor.validateInfo(client, info);

		/**
		 * Key for the argument
		 * @type {string}
		 */
		this.key = info.key;

		/**
		 * Label for the argument
		 * @type {string}
		 */
		this.label = info.label || info.key;

		/**
		 * Question prompt for the argument
		 * @type {string}
		 */
		this.prompt = info.prompt;

		/**
		 * Error message for when a value is invalid
		 * @type {?string}
		 */
		this.error = info.error || null;

		/**
		 * Type of the argument
		 * @type {?ArgumentType}
		 */
		this.type = this.constructor.determineType(client, info.type);

		/**
		 * If type is `integer` or `float`, this is the maximum value of the number.
		 * If type is `string`, this is the maximum length of the string.
		 * @type {?number}
		 */
		this.max = typeof info.max !== 'undefined' ? info.max : null;

		/**
		 * If type is `integer` or `float`, this is the minimum value of the number.
		 * If type is `string`, this is the minimum length of the string.
		 * @type {?number}
		 */
		this.min = typeof info.min !== 'undefined' ? info.min : null;

		/**
		 * The default value for the argument
		 * @type {?ArgumentDefault}
		 */
		this.default = typeof info.default !== 'undefined' ? info.default : null;

		/**
		 * Whether the argument accepts an infinite number of values
		 * @type {boolean}
		 */
		this.infinite = Boolean(info.infinite);

		/**
		 * Validator function for validating a value for the argument
		 * @type {?Function}
		 * @see {@link ArgumentType#validate}
		 */
		this.validator = info.validate || null;

		/**
		 * Parser function for parsing a value for the argument
		 * @type {?Function}
		 * @see {@link ArgumentType#parse}
		 */
		this.parser = info.parse || null;

		/**
		 * Function to check whether a raw value is considered empty
		 * @type {?Function}
		 * @see {@link ArgumentType#isEmpty}
		 */
		this.emptyChecker = info.isEmpty || null;

		/**
		 * How long to wait for input (in seconds)
		 * @type {number}
		 */
		this.wait = typeof info.wait !== 'undefined' ? info.wait : 30;
	}

	/**
	 * Result object from obtaining a single {@link Argument}'s value(s)
	 * @typedef {Object} ArgumentResult
	 * @property {?*|?Array<*>} value - Final value(s) for the argument
	 * @property {?string} cancelled - One of:
	 * - `user` (user cancelled)
	 * - `time` (wait time exceeded)
	 * - `promptLimit` (prompt limit exceeded)
	 * @property {Message[]} prompts - All messages that were sent to prompt the user
	 * @property {Message[]} answers - All of the user's messages that answered a prompt
	 */

	/**
	 * Prompts the user and obtains the value for the argument
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @param {string} [value] - Pre-provided value for the argument
	 * @param {number} [promptLimit=Infinity] - Maximum number of times to prompt for the argument
	 * @return {Promise<ArgumentResult>}
	 */
	async obtain(msg, value, promptLimit = Infinity) {
		let empty = this.isEmpty(value, msg);
		if(empty && this.default !== null) {
			return {
				value: typeof this.default === 'function' ? await this.default(msg, this) : this.default,
				cancelled: null,
				prompts: [],
				answers: []
			};
		}
		if(this.infinite) return this.obtainInfinite(msg, value, promptLimit);

		const wait = this.wait > 0 && this.wait !== Infinity ? this.wait * 1000 : undefined;
		const prompts = [];
		const answers = [];
		let valid = !empty ? await this.validate(value, msg) : false;

		while(!valid || typeof valid === 'string') {
			/* eslint-disable no-await-in-loop */
			if(prompts.length >= promptLimit) {
				return {
					value: null,
					cancelled: 'promptLimit',
					prompts,
					answers
				};
			}

			// Prompt the user for a new value
			prompts.push(await msg.reply(stripIndents`
				${empty ? this.prompt : valid ? valid : `You provided an invalid ${this.label}. Please try again.`}
				${oneLine`
					Respond with \`cancel\` to cancel the command.
					${wait ? `The command will automatically be cancelled in ${this.wait} seconds.` : ''}
				`}
			`));

			// Get the user's response
			const responses = await msg.channel.awaitMessages(msg2 => msg2.author.id === msg.author.id, {
				max: 1,
				time: wait
			});

			// Make sure they actually answered
			if(responses && responses.size === 1) {
				answers.push(responses.first());
				value = answers[answers.length - 1].content;
			} else {
				return {
					value: null,
					cancelled: 'time',
					prompts,
					answers
				};
			}

			// See if they want to cancel
			if(value.toLowerCase() === 'cancel') {
				return {
					value: null,
					cancelled: 'user',
					prompts,
					answers
				};
			}

			empty = this.isEmpty(value, msg);
			valid = await this.validate(value, msg);
			/* eslint-enable no-await-in-loop */
		}

		return {
			value: await this.parse(value, msg),
			cancelled: null,
			prompts,
			answers
		};
	}

	/**
	 * Prompts the user and obtains multiple values for the argument
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @param {string[]} [values] - Pre-provided values for the argument
	 * @param {number} [promptLimit=Infinity] - Maximum number of times to prompt for the argument
	 * @return {Promise<ArgumentResult>}
	 * @private
	 */
	async obtainInfinite(msg, values, promptLimit = Infinity) { // eslint-disable-line complexity
		const wait = this.wait > 0 && this.wait !== Infinity ? this.wait * 1000 : undefined;
		const results = [];
		const prompts = [];
		const answers = [];
		let currentVal = 0;

		while(true) { // eslint-disable-line no-constant-condition
			/* eslint-disable no-await-in-loop */
			let value = values && values[currentVal] ? values[currentVal] : null;
			let valid = value ? await this.validate(value, msg) : false;
			let attempts = 0;

			while(!valid || typeof valid === 'string') {
				attempts++;
				if(attempts > promptLimit) {
					return {
						value: null,
						cancelled: 'promptLimit',
						prompts,
						answers
					};
				}

				// Prompt the user for a new value
				if(value) {
					const escaped = escapeMarkdown(value).replace(/@/g, '@\u200b');
					prompts.push(await msg.reply(stripIndents`
						${valid ? valid : oneLine`
							You provided an invalid ${this.label},
							"${escaped.length < 1850 ? escaped : '[too long to show]'}".
							Please try again.
						`}
						${oneLine`
							Respond with \`cancel\` to cancel the command, or \`finish\` to finish entry up to this point.
							${wait ? `The command will automatically be cancelled in ${this.wait} seconds.` : ''}
						`}
					`));
				} else if(results.length === 0) {
					prompts.push(await msg.reply(stripIndents`
						${this.prompt}
						${oneLine`
							Respond with \`cancel\` to cancel the command, or \`finish\` to finish entry.
							${wait ? `The command will automatically be cancelled in ${this.wait} seconds, unless you respond.` : ''}
						`}
					`));
				}

				// Get the user's response
				const responses = await msg.channel.awaitMessages(msg2 => msg2.author.id === msg.author.id, {
					max: 1,
					time: wait
				});

				// Make sure they actually answered
				if(responses && responses.size === 1) {
					answers.push(responses.first());
					value = answers[answers.length - 1].content;
				} else {
					return {
						value: null,
						cancelled: 'time',
						prompts,
						answers
					};
				}

				// See if they want to finish or cancel
				const lc = value.toLowerCase();
				if(lc === 'finish') {
					return {
						value: results.length > 0 ? results : null,
						cancelled: results.length > 0 ? null : 'user',
						prompts,
						answers
					};
				}
				if(lc === 'cancel') {
					return {
						value: null,
						cancelled: 'user',
						prompts,
						answers
					};
				}

				valid = await this.validate(value, msg);
			}

			results.push(await this.parse(value, msg));

			if(values) {
				currentVal++;
				if(currentVal === values.length) {
					return {
						value: results,
						cancelled: null,
						prompts,
						answers
					};
				}
			}
			/* eslint-enable no-await-in-loop */
		}
	}

	/**
	 * Checks if a value is valid for the argument
	 * @param {string} value - Value to check
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @return {boolean|string|Promise<boolean|string>}
	 */
	validate(value, msg) {
		const valid = this.validator ? this.validator(value, msg, this) : this.type.validate(value, msg, this);
		if(!valid || typeof valid === 'string') return this.error || valid;
		return valid;
	}

	/**
	 * Parses a value string into a proper value for the argument
	 * @param {string} value - Value to parse
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @return {*|Promise<*>}
	 */
	parse(value, msg) {
		if(this.parser) return this.parser(value, msg, this);
		return this.type.parse(value, msg, this);
	}

	/**
	 * Checks whether a value for the argument is considered to be empty
	 * @param {string} value - Value to check for emptiness
	 * @param {CommandMessage} msg - Message that triggered the command
	 * @return {boolean}
	 */
	isEmpty(value, msg) {
		if(this.emptyChecker) return this.emptyChecker(value, msg, this);
		if(this.type) return this.type.isEmpty(value, msg, this);
		return !value;
	}

	/**
	 * Validates the constructor parameters
	 * @param {CommandoClient} client - Client to validate
	 * @param {ArgumentInfo} info - Info to validate
	 * @private
	 */
	static validateInfo(client, info) {
		if(!client) throw new Error('The argument client must be specified.');
		if(typeof info !== 'object') throw new TypeError('Argument info must be an Object.');
		if(typeof info.key !== 'string') throw new TypeError('Argument key must be a string.');
		if(info.label && typeof info.label !== 'string') throw new TypeError('Argument label must be a string.');
		if(typeof info.prompt !== 'string') throw new TypeError('Argument prompt must be a string.');
		if(info.error && typeof info.error !== 'string') throw new TypeError('Argument error must be a string.');
		if(info.type && typeof info.type !== 'string') throw new TypeError('Argument type must be a string.');
		if(info.type && !info.type.includes('|') && !client.registry.types.has(info.type)) {
			throw new RangeError(`Argument type "${info.type}" isn't registered.`);
		}
		if(!info.type && !info.validate) {
			throw new Error('Argument must have either "type" or "validate" specified.');
		}
		if(info.validate && typeof info.validate !== 'function') {
			throw new TypeError('Argument validate must be a function.');
		}
		if(info.parse && typeof info.parse !== 'function') {
			throw new TypeError('Argument parse must be a function.');
		}
		if(!info.type && (!info.validate || !info.parse)) {
			throw new Error('Argument must have both validate and parse since it doesn\'t have a type.');
		}
		if(typeof info.wait !== 'undefined' && (typeof info.wait !== 'number' || Number.isNaN(info.wait))) {
			throw new TypeError('Argument wait must be a number.');
		}
	}

	/**
	 * Gets the argument type to use from an ID
	 * @param {string} id - ID of the type to use
	 * @returns {?ArgumentType}
	 * @private
	 */
	static determineType(client, id) {
		if(!id) return null;
		if(!id.includes('|')) return client.registry.types.get(id);

		let type = client.registry.types.get(id);
		if(type) return type;
		type = new ArgumentUnionType(client, id);
		client.registry.registerType(type);
		return type;
	}
}

module.exports = Argument;
<|endoftext|>


<commit_before>// Code goes here

/*
 * To avoid polluting the global scope, suppose I simply create an
 * anonymous function and then immediately invoke it?
 *
 * Although it seems this should work, it actually generates a syntax
 * error. (I do not understand why. This error differs from the issue
 * describe in the Pluralsight video.)
 *
 * On refreshing the page, the console has the error:
 *
 *     Uncaught SyntaxError: Unexpected token (
 *
 * The console function indicates that this error occurs at line 7 of
 * the script:
 *
 * 		`function() {`
 */
function() {
	var createWorker = function() {

		/*
		 * Remember that any variables defined in this function are
		 * only visible within this function; that is, they are private. But
		 * they **are** visible to functions defined within this function.
		 */
		var workerCount = 0;

		/*
		 * `task1` is the name of an **internal** function. It is **not**
		 * exposed to consumers of the returned value.
		 *
		 * `task1` increments the local (private) variable `workerCount` and
		 * then logs its value.
		 */
		var task1 = function() {
			workerCount++;
			console.log("task1", workerCount);
		}

		/*
		 * Similarly, `task2` is another internal function. Because of
		 * JavaScript scoping rules, its name is only available in the scope
		 * of `createWorker` (and any enclosed scopes).
		 *
		 * Similarly, `task2` increments the private variable `workerCount`.
		 *
		 */
		var task2 = function() {
			workerCount++;
			console.log("task2", workerCount);
		}

		/*
		 * Finally, we return an object. The returned object exposes two
		 * members, `job1` and `job2` which are bound to the internal
		 * functions that do our work.
		 */
		return {
			job1: task1,
			job2: task2
		}
	}

	var worker = createWorker();

	/*
	 * We invoke `task1` and `task2`, from the global scope but we
	 * "see" neither `task1 nor `task2`. But we do see their result. :)
	 *
	 * Invoking `job1` and `job2` increments the variable `workerCount`
	 * which is only local to the `createWorker` function.
	 */
	worker.job1();
	worker.job2();
	worker.job2();
	worker.job2();
}();

<commit_msg>Our solution: an IIFE<commit_after>// Code goes here

/*
 * One solution to not polluting the global scope is the "immediately
 * invoked function expression" (IIFE - prononced "iffy").
 *
 * In this construct, one create an expression (inside the outermost
 * parthenses) that defines a anonymous function and then immediately
 * executes that function.
 *
 * Because the function is anonymous, it does not add any name to the
 * global scope. Because it is invoked immediately, it performs its work
 * (a "side effect") and then immediately goes out of scope (and the
 * function is eligible for garbage collection).
 */
(function() {
	var createWorker = function() {

		/*
		 * Remember that any variables defined in this function are
		 * only visible within this function; that is, they are private. But
		 * they **are** visible to functions defined within this function.
		 */
		var workerCount = 0;

		/*
		 * `task1` is the name of an **internal** function. It is **not**
		 * exposed to consumers of the returned value.
		 *
		 * `task1` increments the local (private) variable `workerCount` and
		 * then logs its value.
		 */
		var task1 = function() {
			workerCount++;
			console.log("task1", workerCount);
		}

		/*
		 * Similarly, `task2` is another internal function. Because of
		 * JavaScript scoping rules, its name is only available in the scope
		 * of `createWorker` (and any enclosed scopes).
		 *
		 * Similarly, `task2` increments the private variable `workerCount`.
		 *
		 */
		var task2 = function() {
			workerCount++;
			console.log("task2", workerCount);
		}

		/*
		 * Finally, we return an object. The returned object exposes two
		 * members, `job1` and `job2` which are bound to the internal
		 * functions that do our work.
		 */
		return {
			job1: task1,
			job2: task2
		}
	}

	var worker = createWorker();

	/*
	 * We invoke `task1` and `task2`, from the global scope but we
	 * "see" neither `task1 nor `task2`. But we do see their result. :)
	 *
	 * Invoking `job1` and `job2` increments the variable `workerCount`
	 * which is only local to the `createWorker` function.
	 */
	worker.job1();
	worker.job2();
	worker.job2();
	worker.job2();
}());

<|endoftext|>


<commit_before>// This technical data was produced for the U. S. Government under Contract No. W15P7T-13-C-F600, and
// is subject to the Rights in Technical Data-Noncommercial Items clause at DFARS 252.227-7013 (FEB 2012)

//requires leaflet_helper.js, underscore, jquery, leaflet, log4javascript

var aoi_feature_edit = {};

var feature_hash = {};

aoi_feature_edit.options = {
    drawControlLocation: "topleft"
};

aoi_feature_edit.init = function () {

    aoi_feature_edit.drawcontrol = null;
    aoi_feature_edit.featureLayers = [];
    aoi_feature_edit.icons = [];

    _.each(aoi_feature_edit.feature_types, function (ftype) {
        var featureLayer = L.geoJson(null,{
            style: function (ftype) {
                var feature_type = aoi_feature_edit.feature_types[ftype.properties.template];
                if (feature_type && feature_type.hasOwnProperty("style")) {
                    return feature_type.style;
                }
            },
            onEachFeature: function(feature, layer) {
            	if (feature.properties) {
                	feature_hash[feature.properties.id] = {layerGroup: featureLayer, layer: layer};

                	var popupContent = '
Feature #'+feature.properties.id+'
';
                	if (feature.properties.template){
                		var template = aoi_feature_edit.feature_types[parseInt(feature.properties.template)];
                		popupContent += ''+template.name+'
';
                	}
                	popupContent += 'Analyst: '+feature.properties.analyst;
                	popupContent += '
Created: '+feature.properties.created_at;
                	popupContent += '
Updated: '+feature.properties.updated_at;
                	popupContent += '
Delete Feature';
                	layer.bindPopup(popupContent);
                }
            }
        });
        aoi_feature_edit.featureLayers[ftype.id] = featureLayer;
    });
};

aoi_feature_edit.get_feature_type = function (i) {
    return aoi_feature_edit.feature_types[i] || {style:{"weight": 2, "color": "yellow", "fillColor": "orange", "fillOpacity": .9, "opacity": 1}};
};
aoi_feature_edit.map_resize = function(){
    var toLower = parseInt($('div.navbar-inner').css('height'));
    var newHeight = $(window).height()-toLower;
    $(map).height(newHeight);
    $(map).css('top',toLower+'px');

    $('.navbar-fixed-top').css('margin-bottom', 0);
    $('body').css({'padding-left':0, 'padding-right':0});

};
aoi_feature_edit.map_init = function (map, bounds) {
    var custom_map = aoi_feature_edit.aoi_map_json;
    aoi_feature_edit.map = map;

    var baseLayers = {};
    var layerSwitcher = {};
    //var editableLayers = new L.FeatureGroup();
    // Only layer in here should be base OSM layer
    _.each(aoi_feature_edit.map._layers, function (l) {
        baseLayers["OpenStreetMap"] = l;
    });

    if (custom_map.hasOwnProperty("layers")) {
        _.each(custom_map.layers, function (l) {
            var n = leaflet_helper.layer_conversion(l);
            if (n !== undefined) {
                if (l.isBaseLayer) {
                    baseLayers[l.name] = n;
                    log.info ("Added " + l.name + " as a base layer.")
                } else {
                    layerSwitcher[l.name] = n;
                    log.info ("Added " + l.name + " as a layer.")
                }
            }
        });
    }

    var layercontrol = L.control.layers(baseLayers, layerSwitcher).addTo(aoi_feature_edit.map);

    aoi_feature_edit.addMapControlButtons(aoi_feature_edit.map);

    var aoi_extents = L.geoJson(aoi_feature_edit.aoi_extents_geojson,
        {
            style: leaflet_helper.styles.extentStyle,
            zIndex: 1000
        });
    aoi_extents.addTo(aoi_feature_edit.map);

    //Build a reset button that zooms to the extents of the AOI
    function locateBounds () {
        return aoi_extents.getBounds();
    }
    (new L.Control.ResetView(locateBounds)).addTo(aoi_feature_edit.map);

    // for each feature template, add features to map and layer control
    _.each(aoi_feature_edit.feature_types, function (ftype) {
        var tnum = ftype.id;
        var featuretype = ftype.type;
        var featureCollection = aoi_feature_edit.createFeatureCollection(tnum);

        _.each(aoi_feature_edit.job_features_geojson.features, function (feature) {
            if (feature.properties.template == tnum && feature.geometry.type == featuretype) {
                //TODO: Change icon here depending on feature type, Issue #27
                featureCollection.features.push(feature);
            }
        });

        aoi_feature_edit.featureLayers[tnum].addData(featureCollection);
        aoi_feature_edit.featureLayers[tnum].addTo(aoi_feature_edit.map);
        layercontrol.addOverlay(aoi_feature_edit.featureLayers[tnum], aoi_feature_edit.feature_types[tnum].name);
    });

    // add one for points
//    var pointcollection = _.filter(aoi_feature_edit.job_features_geojson.features, function(feature) {
//         return feature.geometry.type == "Point";
//    });
//
//    if ( pointcollection.length > 0 ) {
//        var fid = aoi_feature_edit.featureLayers.length + 1;
//        var featureCollection = aoi_feature_edit.createFeatureCollection(fid);
//
//        for ( var i = 0; i < pointcollection.length; i++ ) {
//            featureCollection.features.push(pointcollection[i]);
//        }
//
//        aoi_feature_edit.featureLayers[fid] = L.geoJson(featureCollection);
//        aoi_feature_edit.featureLayers[fid].addTo(aoi_feature_edit.map);
//        layercontrol.addOverlay(aoi_feature_edit.featureLayers[fid], "Points");
//    }

    setTimeout(function () {
        aoi_feature_edit.map.fitBounds(aoi_extents.getBounds());
    }, 1);

    aoi_feature_edit.buildDrawingControl();

    function onSuccess(data, textStatus, jqXHR) {
    	if (data[0] && data[0].geojson) {
    		var tnum = data[0].fields.template;
	        var featureCollection = aoi_feature_edit.createFeatureCollection(tnum);
	        featureCollection.features.push($.parseJSON(data[0].geojson));
	    	aoi_feature_edit.featureLayers[tnum].addData(featureCollection);
    	}
    }

    function onError(jqXHR, textStatus, errorThrown) {
    	alert("Error while adding feature: " + errorThrown);
    }

    map.on('draw:created', function (e) {
        var type = e.layerType;
        var layer = e.layer;

        var geojson = e.layer.toGeoJSON();
        geojson.properties.template = aoi_feature_edit.current_feature_type_id;
        geojson = JSON.stringify(geojson);

        $.ajax({
            type: "POST",
            url: aoi_feature_edit.create_feature_url,
            data: { aoi: aoi_feature_edit.aoi_id,
                geometry: geojson
            },
            success: onSuccess,
            error: onError,
            dataType: "json"
        });

        //layer.bindPopup('Feature Created!');
    });

    //Resize the map
    aoi_feature_edit.map_resize();
    //Resize it on screen resize, but no more than every .3 seconds
    var lazyResize = _.debounce(aoi_feature_edit.map_resize,300);
    $(window).resize(lazyResize);
};

aoi_feature_edit.buildDrawingControl = function(feature_id){

    if (aoi_feature_edit.drawcontrol) {
        //If a current control exists, delete it
        aoi_feature_edit.map.removeControl(aoi_feature_edit.drawcontrol);
    }

    //Find the Feature ID from the drop-down box (or use the one passed in)
    feature_id = feature_id || aoi_feature_edit.current_feature_type_id || 1;
    var feature = aoi_feature_edit.get_feature_type(feature_id);

    //Start building the draw options object
    var drawOptions = { draw:{position: aoi_feature_edit.options.drawControlLocation} };
    drawOptions.edit = false;
    //TODO: Add editing back in - currently is not catching edits, as features are saved
    // to server as soon as they are entered

    if (feature.type == "Polygon") {
        drawOptions.draw.polygon = {
            title: 'Add a feature Polygon: '+ feature.name || "Polygon",
            allowIntersection: false,
            drawError: {
                color: '#b00b00',
                timeout: 1000
            },
            shapeOptions: feature.style || {borderColor: "black", backgroundColor:"brown"},
            showArea: true
        };
        drawOptions.draw.polyline = false;
        drawOptions.draw.rectangle = false;
        drawOptions.draw.circle = false;
        drawOptions.draw.marker = false;
    } else {
        drawOptions.draw.marker = {
            title: 'Add a feature point: ' + feature.name || "Point"
        };
        drawOptions.draw.polyline = false;
        drawOptions.draw.rectangle = false;
        drawOptions.draw.circle = false;
        drawOptions.draw.polygon = false;
    }

    //Create the drawing objects control
    var drawControl = new L.Control.Draw(drawOptions);
    aoi_feature_edit.map.addControl(drawControl);
    aoi_feature_edit.drawcontrol = drawControl;

};

aoi_feature_edit.addMapControlButtons = function (map) {

    function complete_button_onClick() {
         $.ajax({
              type: "POST",
              url: aoi_feature_edit.complete_url,
              dataType: "json",
              success: function(response){
                  geoq.redirect(aoi_feature_edit.complete_redirect_url);
              }
         });
    }
    var completeButtonOptions = {
      'html': 'Mark as Complete',  // string
      'onClick': complete_button_onClick,  // callback function
      'hideText': false,  // bool
      'maxWidth': 60,  // number
      'doToggle': false,  // bool
      'toggleStatus': false  // bool
    }
    var completeButton = new L.Control.Button(completeButtonOptions).addTo(map);

    var featuresButtonOptions = {
      'html': '<select id="features"></select>',  // string
      'hideText': false,  // bool
      'maxWidth': 60,  // number
      'doToggle': false,  // bool
      'toggleStatus': false  // bool
    }
    var featuresButton = new L.Control.Button(featuresButtonOptions).addTo(map);

    var title = "
"+aoi_feature_edit.job_name+" > AOI #"+aoi_feature_edit.aoi_id+" > ";
    title+= ""+aoi_feature_edit.percent_complete+"% Complete > "+ aoi_feature_edit.description+"
";

    var titleInfoOptions = {
      'html': title,  // string
      'hideText': false,  // bool
      'maxWidth': 60,  // number
      'doToggle': false,  // bool
      'toggleStatus': false  // bool
    }
    var titleInfoButton = new L.Control.ButtonLeft(titleInfoOptions).addTo(map);

    //Quick work-around for moving header to top of the page
    var $c = $($(".leaflet-control-button.leaflet-control")[0]);
    $c.css({backgroundColor:'white',border:'solid black 1px', borderRadius:4, padding:'0px 6px 0px 6px'});
    $c.prependTo($c.parent());

    var feature_type_div = "features";
    _.each(aoi_feature_edit.feature_types, function(feature_type){
        aoi_feature_edit.addOptions(feature_type, feature_type_div);
    });

    var $features = $("#features");
    $features.select2();
    aoi_feature_edit.current_feature_type_id = parseInt($features.val());

    $features.on("change", function(e) {
        log.info("Selected feature type: " + e.val + ".");
        aoi_feature_edit.current_feature_type_id = e.val;
        aoi_feature_edit.updateDrawOptions(e.val);
        //aoi_feature_edit.filterDrawConsole();
    });

};

// Changes current features to match the selected style.
aoi_feature_edit.updateDrawOptions = function (i) {
    aoi_feature_edit.drawcontrol.setDrawingOptions({ polygon: { shapeOptions: aoi_feature_edit.feature_types[i].style },
        rectangle: { shapeOptions: aoi_feature_edit.feature_types[i].style}
    });
    aoi_feature_edit.buildDrawingControl(i);
};

aoi_feature_edit.getDrawConsole = function () {

    var geometry_type = null;

    if (aoi_feature_edit.hasOwnProperty("current_feature_type_id")) {
        var feature_id = aoi_feature_edit.current_feature_type_id;
        geometry_type = aoi_feature_edit.feature_types[feature_id].type;
    }

    if (geometry_type) {
        if (geometry_type != "Polygon") {
            var marker = false;
        }
        if (geometry_type === "Point") {
            var polygon = false;
            var rectangle = false;
        }
    }

};

// Filters the draw control to allow acceptable feature types.
aoi_feature_edit.filterDrawConsole = function () {
    var control = aoi_feature_edit.drawcontrol;
    var geometry_type = aoi_feature_edit.feature_types[aoi_feature_edit.current_feature_type_id].type;
    var map = control._map;
    map.removeControl(control);
};

aoi_feature_edit.addOptions = function (feature, div) {
    var t = _.template("<option value='{{id}}'>{{name}}</option>");
    $("#" + div).append(t(feature));
};

aoi_feature_edit.createFeatureCollection = function (id) {
    var featureCollection = {};
    featureCollection.type = "FeatureCollection";
    featureCollection.features = [];
    featureCollection.properties = {};
    featureCollection.properties.id = id;

    return featureCollection;
};

aoi_feature_edit.deleteFeature = function (id) {
	var feature = feature_hash[id];
	if (feature) {
		feature.layerGroup.removeLayer(feature.layer);
		delete feature_hash[id];
	}
};
<commit_msg>changes for displaying colored icons<commit_after>// This technical data was produced for the U. S. Government under Contract No. W15P7T-13-C-F600, and
// is subject to the Rights in Technical Data-Noncommercial Items clause at DFARS 252.227-7013 (FEB 2012)

//requires leaflet_helper.js, underscore, jquery, leaflet, log4javascript

var aoi_feature_edit = {};

var feature_hash = {};

aoi_feature_edit.options = {
    drawControlLocation: "topleft"
};

aoi_feature_edit.all_polygons = [];
aoi_feature_edit.all_markers = [];

aoi_feature_edit.MapMarker = L.Icon.extend({
    options: {
        id: 0,
        shadowUrl: null,
        iconAnchor: new L.Point(12, 41),
        iconSize: new L.Point(25, 41),
        repeatMode: true,
        text: 'Draw a marker',
        iconUrl: null
    }
});

aoi_feature_edit.init = function () {

    aoi_feature_edit.drawcontrol = null;
    aoi_feature_edit.featureLayers = [];
    aoi_feature_edit.icons = {};

    _.each(aoi_feature_edit.feature_types, function (ftype) {
        // if this is a point, create icon for it first
        if (ftype.type == 'Point') {
            aoi_feature_edit.icons[ftype.id] = {
                iconUrl: ftype.style.iconUrl,
                text: ftype.name
            };
        }

        var featureLayer = L.geoJson(null,{
            style: function (ftype) {
                var feature_type = aoi_feature_edit.feature_types[ftype.properties.template];
                if (feature_type && feature_type.hasOwnProperty("style")) {
                    return feature_type.style;
                }
            },
            onEachFeature: function(feature, layer) {
            	if (feature.properties) {
                	feature_hash[feature.properties.id] = {layerGroup: featureLayer, layer: layer};

                	var popupContent = '
Feature #'+feature.properties.id+'
';
                	if (feature.properties.template){
                		var template = aoi_feature_edit.feature_types[parseInt(feature.properties.template)];
                		popupContent += ''+template.name+'
';
                	}
                	popupContent += 'Analyst: '+feature.properties.analyst;
                	popupContent += '
Created: '+feature.properties.created_at;
                	popupContent += '
Updated: '+feature.properties.updated_at;
                	popupContent += '
Delete Feature';
                	layer.bindPopup(popupContent);
                }
            },
            pointToLayer: function( feature, latlng) {
                return new L.Marker(latlng, {
                    icon: new aoi_feature_edit.MapMarker(aoi_feature_edit.icons[feature.properties.template])
                });
            }
        });
        aoi_feature_edit.featureLayers[ftype.id] = featureLayer;
    });
};

aoi_feature_edit.get_feature_type = function (i) {
    return aoi_feature_edit.feature_types[i] || {style:{"weight": 2, "color": "yellow", "fillColor": "orange", "fillOpacity": .9, "opacity": 1}};
};
aoi_feature_edit.map_resize = function(){
    var toLower = parseInt($('div.navbar-inner').css('height'));
    var newHeight = $(window).height()-toLower;
    $(map).height(newHeight);
    $(map).css('top',toLower+'px');

    $('.navbar-fixed-top').css('margin-bottom', 0);
    $('body').css({'padding-left':0, 'padding-right':0});

};
aoi_feature_edit.map_init = function (map, bounds) {
    var custom_map = aoi_feature_edit.aoi_map_json;
    aoi_feature_edit.map = map;

    // SRJ: move zoom control
    aoi_feature_edit.map.removeControl(aoi_feature_edit.map.zoomControl);
    aoi_feature_edit.map.addControl(L.control.zoom({position:'bottomright'}));

    var baseLayers = {};
    var layerSwitcher = {};
    //var editableLayers = new L.FeatureGroup();
    // Only layer in here should be base OSM layer
    _.each(aoi_feature_edit.map._layers, function (l) {
        baseLayers["OpenStreetMap"] = l;
    });

    if (custom_map.hasOwnProperty("layers")) {
        _.each(custom_map.layers, function (l) {
            var n = leaflet_helper.layer_conversion(l);
            if (n !== undefined) {
                if (l.isBaseLayer) {
                    baseLayers[l.name] = n;
                    log.info ("Added " + l.name + " as a base layer.")
                } else {
                    layerSwitcher[l.name] = n;
                    log.info ("Added " + l.name + " as a layer.")
                }
            }
        });
    }

    var layercontrol = L.control.layers(baseLayers, layerSwitcher).addTo(aoi_feature_edit.map);

    aoi_feature_edit.addMapControlButtons(aoi_feature_edit.map);

    var aoi_extents = L.geoJson(aoi_feature_edit.aoi_extents_geojson,
        {
            style: leaflet_helper.styles.extentStyle,
            zIndex: 1000
        });
    aoi_extents.addTo(aoi_feature_edit.map);

    //Build a reset button that zooms to the extents of the AOI
    function locateBounds () {
        return aoi_extents.getBounds();
    }
    (new L.Control.ResetView(locateBounds)).addTo(aoi_feature_edit.map);

    // for each feature template, add features to map and layer control
    _.each(aoi_feature_edit.feature_types, function (ftype) {

        if (ftype.type == 'Polygon') {
            aoi_feature_edit.all_polygons.push(aoi_feature_edit.createPolygonOptions(ftype));
        } else if (ftype.type == 'Point') {
            aoi_feature_edit.all_markers.push(aoi_feature_edit.createPointOptions(ftype));
        }
        var tnum = ftype.id;
        var featuretype = ftype.type;
        var featureCollection = aoi_feature_edit.createFeatureCollection(tnum);

        _.each(aoi_feature_edit.job_features_geojson.features, function (feature) {
            if (feature.properties.template == tnum && feature.geometry.type == featuretype) {
                //TODO: Change icon here depending on feature type, Issue #27
                featureCollection.features.push(feature);
            }
        });

        aoi_feature_edit.featureLayers[tnum].addData(featureCollection);
        aoi_feature_edit.featureLayers[tnum].addTo(aoi_feature_edit.map);
        layercontrol.addOverlay(aoi_feature_edit.featureLayers[tnum], aoi_feature_edit.feature_types[tnum].name);
    });

    // add one for points
//    var pointcollection = _.filter(aoi_feature_edit.job_features_geojson.features, function(feature) {
//         return feature.geometry.type == "Point";
//    });
//
//    if ( pointcollection.length > 0 ) {
//        var fid = aoi_feature_edit.featureLayers.length + 1;
//        var featureCollection = aoi_feature_edit.createFeatureCollection(fid);
//
//        for ( var i = 0; i < pointcollection.length; i++ ) {
//            featureCollection.features.push(pointcollection[i]);
//        }
//
//        aoi_feature_edit.featureLayers[fid] = L.geoJson(featureCollection);
//        aoi_feature_edit.featureLayers[fid].addTo(aoi_feature_edit.map);
//        layercontrol.addOverlay(aoi_feature_edit.featureLayers[fid], "Points");
//    }

    setTimeout(function () {
        aoi_feature_edit.map.fitBounds(aoi_extents.getBounds());
    }, 1);

    var drawnItems = new L.FeatureGroup();
    aoi_feature_edit.map.addLayer(drawnItems);
    aoi_feature_edit.drawnItems = drawnItems;

    aoi_feature_edit.buildDrawingControl(drawnItems);

    function onSuccess(data, textStatus, jqXHR) {
    	if (data[0] && data[0].geojson) {
    		var tnum = data[0].fields.template;
	        var featureCollection = aoi_feature_edit.createFeatureCollection(tnum);
	        featureCollection.features.push($.parseJSON(data[0].geojson));
	    	aoi_feature_edit.featureLayers[tnum].addData(featureCollection);
    	}
    }

    function onError(jqXHR, textStatus, errorThrown) {
    	alert("Error while adding feature: " + errorThrown);
    }

    map.on('draw:created', function (e) {
        var type = e.layerType;
        var layer = e.layer;

        var geojson = e.layer.toGeoJSON();
        geojson.properties.template = aoi_feature_edit.current_feature_type_id;
        geojson = JSON.stringify(geojson);

        $.ajax({
            type: "POST",
            url: aoi_feature_edit.create_feature_url,
            data: { aoi: aoi_feature_edit.aoi_id,
                geometry: geojson
            },
            success: onSuccess,
            error: onError,
            dataType: "json"
        });

        //layer.bindPopup('Feature Created!');
        drawnItems.addLayer(layer);
    });

    map.on('draw:drawstart', function (e) {
        var id = e.layerType.slice(-1);
        aoi_feature_edit.current_feature_type_id = parseInt(id);
    });

    //Resize the map
    aoi_feature_edit.map_resize();
    //Resize it on screen resize, but no more than every .3 seconds
    var lazyResize = _.debounce(aoi_feature_edit.map_resize,300);
    $(window).resize(lazyResize);
};

aoi_feature_edit.buildDrawingControl = function(drawnItems){

    if (aoi_feature_edit.drawcontrol) {
        //If a current control exists, delete it
        aoi_feature_edit.map.removeControl(aoi_feature_edit.drawcontrol);
    }

    //Find the Feature ID from the drop-down box (or use the one passed in)
    //feature_id = feature_id || aoi_feature_edit.current_feature_type_id || 1;
    //var feature = aoi_feature_edit.get_feature_type(feature_id);

    //Start building the draw options object
    var drawOptions = { draw:{position: aoi_feature_edit.options.drawControlLocation} };
    drawOptions.edit = false;
    //TODO: Add editing back in - currently is not catching edits, as features are saved
    // to server as soon as they are entered

    var drawControl = new L.Control.Draw({
        draw: {
            position: aoi_feature_edit.options.drawControlLocation,
            polyline: false,
            circle: false,
            rectangle: false,

            markers: aoi_feature_edit.all_markers,
            polygons: aoi_feature_edit.all_polygons
            // rectangle: {
            //    shapeOptions: aoi_feature_edit.feature_types[aoi_feature_edit.current_feature_type_id].style
            //}
        },
        edit: {
            featureGroup: drawnItems
        }
    });

    //Create the drawing objects control
    //var drawControl = new L.Control.Draw(drawOptions);
    aoi_feature_edit.map.addControl(drawControl);
    aoi_feature_edit.drawcontrol = drawControl;

};

aoi_feature_edit.addMapControlButtons = function (map) {

    function complete_button_onClick() {
         $.ajax({
              type: "POST",
              url: aoi_feature_edit.complete_url,
              dataType: "json",
              success: function(response){
                  geoq.redirect(aoi_feature_edit.complete_redirect_url);
              }
         });
    }
    var completeButtonOptions = {
      'html': 'Mark as Complete',  // string
      'onClick': complete_button_onClick,  // callback function
      'hideText': false,  // bool
      'maxWidth': 60,  // number
      'doToggle': false,  // bool
      'toggleStatus': false  // bool
    }
    var completeButton = new L.Control.Button(completeButtonOptions).addTo(map);

//    var featuresButtonOptions = {
//      'html': '<select id="features"></select>',  // string
//      'hideText': false,  // bool
//      'maxWidth': 60,  // number
//      'doToggle': false,  // bool
//      'toggleStatus': false  // bool
//    }
//    var featuresButton = new L.Control.Button(featuresButtonOptions).addTo(map);

    var title = "
"+aoi_feature_edit.job_name+" > AOI #"+aoi_feature_edit.aoi_id+" > ";
    title+= ""+aoi_feature_edit.percent_complete+"% Complete > "+ aoi_feature_edit.description+"
";

    var titleInfoOptions = {
      'html': title,  // string
      'hideText': false,  // bool
      'maxWidth': 60,  // number
      'doToggle': false,  // bool
      'toggleStatus': false  // bool
    }
    var titleInfoButton = new L.Control.ButtonLeft(titleInfoOptions).addTo(map);

    //Quick work-around for moving header to top of the page
    var $c = $($(".leaflet-control-button.leaflet-control")[0]);
    $c.css({backgroundColor:'white',border:'solid black 1px', borderRadius:4, padding:'0px 6px 0px 6px'});
    $c.prependTo($c.parent());

//    var feature_type_div = "features";
//    _.each(aoi_feature_edit.feature_types, function(feature_type){
//        aoi_feature_edit.addOptions(feature_type, feature_type_div);
//    });
//
//    var $features = $("#features");
//    $features.select2();
//    aoi_feature_edit.current_feature_type_id = parseInt($features.val());
//
//    $features.on("change", function(e) {
//        log.info("Selected feature type: " + e.val + ".");
//        aoi_feature_edit.current_feature_type_id = e.val;
//        aoi_feature_edit.updateDrawOptions(e.val);
//        //aoi_feature_edit.filterDrawConsole();
//    });

};

// Changes current features to match the selected style.
//aoi_feature_edit.updateDrawOptions = function (i) {
//    aoi_feature_edit.drawcontrol.setDrawingOptions({ polygon: { shapeOptions: aoi_feature_edit.feature_types[i].style },
//        rectangle: { shapeOptions: aoi_feature_edit.feature_types[i].style}
//    });
//    aoi_feature_edit.buildDrawingControl(i);
//};

aoi_feature_edit.getDrawConsole = function () {

    var geometry_type = null;

    if (aoi_feature_edit.hasOwnProperty("current_feature_type_id")) {
        var feature_id = aoi_feature_edit.current_feature_type_id;
        geometry_type = aoi_feature_edit.feature_types[feature_id].type;
    }

    if (geometry_type) {
        if (geometry_type != "Polygon") {
            var marker = false;
        }
        if (geometry_type === "Point") {
            var polygon = false;
            var rectangle = false;
        }
    }

};

// Filters the draw control to allow acceptable feature types.
aoi_feature_edit.filterDrawConsole = function () {
    var control = aoi_feature_edit.drawcontrol;
    var geometry_type = aoi_feature_edit.feature_types[aoi_feature_edit.current_feature_type_id].type;
    var map = control._map;
    map.removeControl(control);
};

aoi_feature_edit.addOptions = function (feature, div) {
    var t = _.template("<option value='{{id}}'>{{name}}</option>");
    $("#" + div).append(t(feature));
};

aoi_feature_edit.createFeatureCollection = function (id) {
    var featureCollection = {};
    featureCollection.type = "FeatureCollection";
    featureCollection.features = [];
    featureCollection.properties = {};
    featureCollection.properties.id = id;

    return featureCollection;
};

aoi_feature_edit.deleteFeature = function (id) {
	var feature = feature_hash[id];
	if (feature) {
		feature.layerGroup.removeLayer(feature.layer);
		delete feature_hash[id];
	}
};

aoi_feature_edit.createPolygonOptions = function(opts) {
    var options = {};

    if (opts.name) {
        options.title = opts.name;
    }

    options.allowIntersection = false;
    options.drawError = { color: '#b00b00', timeout: 1000};

    options.shapeOptions = opts.style || {borderColor: "black", backgroundColor:"brown"},
    options.showArea = true;
    options.id = opts.id;

    return options;
};

aoi_feature_edit.createPointOptions = function(opts) {
    var options = {};

    var marker = new aoi_feature_edit.MapMarker({
        iconUrl: opts.style.iconUrl,
        text: opts.name
    });

    options.icon = marker;
    options.repeatMode = true;
    options.id = opts.id;

    return options;
};<|endoftext|>


<commit_before>if(typeof String.prototype.trim !== 'function') {
    String.prototype.trim = function() {
        return this.replace(/^\s+|\s+$/g, '');
    }
}

function getCookie(cname) {
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++) {
        var c = ca[i].trim();
        if (c.indexOf(name) == 0) return c.substring(name.length,c.length);
    }
    return "";
}

function harvestAll(){
    $.ajax({
        type: "POST",
        url: "http://www.sexgangsters.com/api/",
        "data": {
            "data": "{\"method\":\"city.business.collectAll\"}"
        },
        headers: {
            "X-CSRFToken":getCookie("csrftoken")
        }
    });
}

function getRivalsList(){
    var returnVar = "DTC";
    $.ajax({
        type:       "POST",
        url:        "http://www.sexgangsters.com/api/",
        async:      false,
        "data":     {
            "data": "{\"method\":\"pvp.rivals.get\",\"args\":{\"cSetId\":0,\"cItemId\":0}}"
        },
        headers:    {
            "X-CSRFToken":getCookie("csrftoken")
        },
        success:    function(data) {
            returnVar=data;
        }
    });
    return returnVar;
}

function makeFightStep1(rivalId){
    var returnVar = "DTC";
    var dataVar = "{\"method\":\"pvp.fight.start\",\"args\":{\"rivalId\":" + rivalId +"}}";
    $.ajax({
        type:       "POST",
        url:        "http://www.sexgangsters.com/api/",
        async:      false,
        "data":     {
            "data": dataVar
        },
        headers:    {
            "X-CSRFToken":getCookie("csrftoken")
        },
        success:    function(data) {
            returnVar = data;
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            returnVar = textStatus;
        }

    });
    makeFightStep2();
    return returnVar;
}

function makeFightStep2(){
    var returnVar = "DTC";
    // Fight exec
    $.ajax({
        type:       "POST",
        url:        "http://www.sexgangsters.com/api/",
        async:      false,
        "data":     {
            "data": "{\"method\":\"pvp.fight.finish\",\"args\":{\"boosters\":null}}"
        },
        headers:    {
            "X-CSRFToken":getCookie("csrftoken")
        },
        success:    function(data) {
            returnVar=data;
        }
    });
    return returnVar;
}

function testFunction() {
    return "hello from js";
}

<commit_msg>Implemented a generic version of the Stupid Game Ajax Call<commit_after>if(typeof String.prototype.trim !== 'function') {
    String.prototype.trim = function() {
        return this.replace(/^\s+|\s+$/g, '');
    }
}

function getCookie(cname) {
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++) {
        var c = ca[i].trim();
        if (c.indexOf(name) == 0) return c.substring(name.length,c.length);
    }
    return "";
}

function harvestAll(){
    $.ajax({
        type: "POST",
        url: "http://www.sexgangsters.com/api/",
        "data": {
            "data": "{\"method\":\"city.business.collectAll\"}"
        },
        headers: {
            "X-CSRFToken":getCookie("csrftoken")
        }
    });
}

function getRivalsList(){
    var returnVar = "DTC";
    $.ajax({
        type:       "POST",
        url:        "http://www.sexgangsters.com/api/",
        async:      false,
        "data":     {
            "data": "{\"method\":\"pvp.rivals.get\",\"args\":{\"cSetId\":0,\"cItemId\":0}}"
        },
        headers:    {
            "X-CSRFToken":getCookie("csrftoken")
        },
        success:    function(data) {
            returnVar=data;
        }
    });
    return returnVar;
}

function makeFightStep1(rivalId){
    var returnVar = "DTC";
    var dataVar = "{\"method\":\"pvp.fight.start\",\"args\":{\"rivalId\":" + rivalId +"}}";
    $.ajax({
        type:       "POST",
        url:        "http://www.sexgangsters.com/api/",
        async:      false,
        "data":     {
            "data": dataVar
        },
        headers:    {
            "X-CSRFToken":getCookie("csrftoken")
        },
        success:    function(data) {
            returnVar = data;
        },
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            returnVar = textStatus;
        }

    });
    makeFightStep2();
    return returnVar;
}

function makeFightStep2(){
    var returnVar = "DTC";
    // Fight exec
    $.ajax({
        type:       "POST",
        url:        "http://www.sexgangsters.com/api/",
        async:      false,
        "data":     {
            "data": "{\"method\":\"pvp.fight.finish\",\"args\":{\"boosters\":null}}"
        },
        headers:    {
            "X-CSRFToken":getCookie("csrftoken")
        },
        success:    function(data) {
            returnVar=data;
        }
    });
    return returnVar;
}

function testFunction() {
    return "hello from js";
}

function genericStupidGameCaller(dataString){
    var returnVar = "DTC";
    // Fight exec
    $.ajax({
        type:       "POST",
        url:        "http://www.sexgangsters.com/api/",
        async:      false,
        "data":     {
            "data": dataString
        },
        headers:    {
            "X-CSRFToken":getCookie("csrftoken")
        },
        success:    function(data) {
            returnVar=data;
        }
    });
    return returnVar;
}

function harvestAll_test(){
    return genericStupidGameCaller("{\"method\":\"city.business.collectAll\"}");
}<|endoftext|>


<commit_before>describe('ChildrenCountInput', function () {
    'use strict';

    var assert = require('assert'),
        sinon = require('sinon'),
        React = require('react'),
        TestUtils = require('react/addons').addons.TestUtils,
        ChildrenCountInput = require('../src/ChildrenCountInput'),
        TestBrowser = require('jsdom-test-browser'),
        bro = new TestBrowser();

    ['value', 'onChange'].forEach(function (p) {
        it('declares the ' + p + ' property', function () {
            assert(ChildrenCountInput.propTypes[p]);
        });
    });

    describe('instance', function () {
        this.timeout(4000);

        beforeEach(function (done) { bro.setUp(done); });
        afterEach(function () { bro.tearDown(); });

        describe('element HTML', function () {
            var element;

            beforeEach(function () {
                element = TestUtils.renderIntoDocument(
                    React.createElement(ChildrenCountInput, {value: 5})
                ).getDOMNode();
            });

            it('is a select tag', function () {
                assert.strictEqual(element.tagName, 'SELECT');
            });

            it('defines 6 option values', function () {
                var values = [].slice.call(element.options).map(function (o) { return o.value; });
                assert.deepEqual(values, [0, 1, 2, 3, 4, 5]);
            });

            it('selects the option defined by the component\'s value property', function () {
                assert.strictEqual(bro.$(element).val(), '5');
            });
        });

        describe('selected option change', function () {
            var spy,
                element;

            beforeEach(function () {
                spy = sinon.spy();

                element = TestUtils.renderIntoDocument(
                    React.createElement(ChildrenCountInput, {onChange: spy})
                ).getDOMNode();

                TestUtils.Simulate.change(element, {target: {value: '2'}});
            });

            it('triggers an onChange', function () {
                assert(spy.calledOnce);
            });

            it('value is converted from a string to an integer', function () {
                assert.strictEqual(spy.args[0][0], 2);
            });
        });
    });
});
<commit_msg>Delete the redundant tests<commit_after>describe('ChildrenCountInput', function () {
    'use strict';

    var assert = require('assert'),
        React = require('react'),
        TestUtils = require('react/addons').addons.TestUtils,
        ChildrenCountInput = require('../src/ChildrenCountInput'),
        TestBrowser = require('jsdom-test-browser'),
        bro = new TestBrowser();

    describe('instance', function () {
        this.timeout(4000);

        beforeEach(function (done) { bro.setUp(done); });
        afterEach(function () { bro.tearDown(); });

        describe('element HTML', function () {
            var element;

            beforeEach(function () {
                element = TestUtils.renderIntoDocument(
                    React.createElement(ChildrenCountInput)
                ).getDOMNode();
            });

            it('defines 6 option values', function () {
                var values = [].slice.call(element.options).map(function (o) { return o.value; });
                assert.deepEqual(values, [0, 1, 2, 3, 4, 5]);
            });
        });
    });
});
<|endoftext|>


<commit_before>const config = require('../config.js')
const dbOpsGuilds = require('../util/db/guilds.js')
const log = require('../util/logger.js')
const MenuUtils = require('../structs/MenuUtils.js')
const FeedSelector = require('../structs/FeedSelector.js')
const Translator = require('../structs/Translator.js')
const properties = [`message`, `embed`, `filters`, `misc-options`, `subscriptions`]

async function destSelectorFn (m, data) {
  const { guildRss, rssName, rssNameList } = data
  const sourceFeedLink = guildRss.sources[rssName].link
  const destFeedLinks = []
  for (let i = 0; i < rssNameList.length; ++i) {
    destFeedLinks.push(guildRss.sources[rssNameList[i]].link)
  }
  const cloned = []
  if (data.cloneMessage) cloned.push('message')
  if (data.cloneEmbed) cloned.push('embed')
  if (data.cloneFilters) cloned.push('filters')
  if (data.cloneMiscOptions) cloned.push('misc-options')
  if (data.cloneSubscriptions) cloned.push('subscriptions')

  return {
    ...data,
    clonedProps: cloned,
    next: {
      text: Translator.translate('commands.rssclone.confirm', guildRss.locale, { link: sourceFeedLink, cloning: cloned.join('`, `'), destinations: destFeedLinks.join('\n') })
    }
  }
}

async function confirmFn (m, data) {
  if (m.content !== 'yes') {
    throw new MenuUtils.MenuOptionError(Translator.translate('commands.rssclone.confirmError', data.guildRss ? data.guildRss.locale : null))
  }
  return { ...data, confirmed: true }
}

module.exports = async (bot, message, command) => {
  try {
    const guildRss = await dbOpsGuilds.get(message.guild.id)
    const guildLocale = guildRss ? guildRss.locale : undefined
    const translate = Translator.createLocaleTranslator(guildLocale)
    const sourceSelector = new FeedSelector(message, undefined, { command: command, prependDescription: translate('commands.rssclone.copyFrom'), globalSelect: true }, guildRss)
    const destSelector = new FeedSelector(message, destSelectorFn, { command: command, prependDescription: translate('commands.rssclone.copyTo'), multiSelect: true, globalSelect: true }, guildRss)
    const confirm = new MenuUtils.Menu(message, confirmFn, { splitOptions: { prepend: '```', append: '```' } })
    const prefix = guildRss.prefix || config.bot.prefix
    const args = MenuUtils.extractArgsAfterCommand(message.content)
    if (args.length === 0) {
      return await message.channel.send(translate('commands.rssclone.noProperties', { prefix, properties: properties.join('`, `') }))
    }
    let invalidArgs = []
    for (const arg of args) {
      if (!properties.includes(arg)) {
        invalidArgs.push(arg)
      }
    }
    if (invalidArgs.length > 0) {
      return await message.channel.send(translate('commands.rssclone.invalidProperties', { invalid: invalidArgs.join('`, `'), properties: properties.join('`, `') }))
    }

    const cloneAll = args.includes('all')
    const cloneMessage = cloneAll || args.includes('message')
    const cloneEmbed = cloneAll || args.includes('embed')
    const cloneFilters = cloneAll || args.includes('filters')
    const cloneMiscOptions = cloneAll || args.includes('misc-options')
    const cloneSubscriptions = cloneAll || args.includes('subscriptions')

    const data = await new MenuUtils.MenuSeries(message, [sourceSelector, destSelector, confirm], { cloneMessage, cloneEmbed, cloneFilters, cloneMiscOptions, cloneSubscriptions, locale: guildLocale }).start()
    if (!data || !data.confirmed) return

    const sourceFeed = guildRss.sources[data.rssName]
    const destFeeds = []
    for (let i = 0; i < data.rssNameList.length; ++i) {
      destFeeds.push(guildRss.sources[data.rssNameList[i]])
    }
    // If any of these props are empty in the source feed, then it will simply be deleted
    const emptyMessage = !sourceFeed.message
    const emptyEmbed = !sourceFeed.embeds || sourceFeed.embeds.length === 0
    const emptyFilters = !sourceFeed.filters || Object.keys(sourceFeed.filters).length === 0
    const emptySubscriptions = !sourceFeed.subscribers || sourceFeed.subscribers.length === 0
    let destLinksCount = 0

    destFeeds.forEach(destFeed => {
      // Message
      if (emptyMessage) delete destFeed.message
      else if (cloneMessage) destFeed.message = sourceFeed.message

      // Embed
      if (emptyEmbed) delete destFeed.embeds
      else if (cloneEmbed) destFeed.embeds = JSON.parse(JSON.stringify(sourceFeed.embeds))

      // Filters
      if (emptyFilters) {
        const origFilteredRoleSubs = destFeed.filters && destFeed.filters.roleSubscriptions ? JSON.parse(JSON.stringify(destFeed.filters.roleSubscriptions)) : undefined
        if (origFilteredRoleSubs) destFeed.filters = { roleSubscriptions: origFilteredRoleSubs }
        else delete destFeed.filters
      } else if (cloneFilters) {
        const origFilteredRoleSubs = destFeed.filters && destFeed.filters.roleSubscriptions ? JSON.parse(JSON.stringify(destFeed.filters.roleSubscriptions)) : undefined
        const copy = JSON.parse(JSON.stringify(sourceFeed.filters))
        delete copy.roleSubscriptions
        destFeed.filters = copy
        if (origFilteredRoleSubs) destFeed.filters.roleSubscriptions = origFilteredRoleSubs
      }

      // Options
      if (cloneMiscOptions) {
        destFeed.checkTitles = sourceFeed.checkTitles
        destFeed.imgPreviews = sourceFeed.imgPreviews
        destFeed.imgLinksExistence = sourceFeed.imgLinksExistence
        destFeed.checkDates = sourceFeed.checkDates
        destFeed.formatTables = sourceFeed.formatTables
        if (sourceFeed.splitMessage) destFeed.splitMessage = JSON.parse(JSON.stringify(sourceFeed.splitMessage))
      }

      // Global Roles
      if (emptySubscriptions) delete destFeed.subscribers
      else if (cloneSubscriptions) destFeed.subscribers = JSON.parse(JSON.stringify(sourceFeed.subscribers))

      destLinksCount++
    })

    log.command.info(`Properties ${data.clonedProps.join(',')} for the feed ${sourceFeed.link} cloning to to ${destLinksCount} feeds`, message.guild)
    await dbOpsGuilds.update(guildRss)
    await message.channel.send(`${translate('commands.rssclone.success', { cloned: data.clonedProps.join('`, `'), link: sourceFeed.link, destinationCount: destLinksCount })} ${translate('generics.backupReminder', { prefix })}`)
  } catch (err) {
    log.command.warning(`rssclone`, message.guild, err)
    if (err.code !== 50013) message.channel.send(err.message).catch(err => log.command.warning('rssclone 1', message.guild, err))
  }
}
<commit_msg>Fix rssclone all not working<commit_after>const config = require('../config.js')
const dbOpsGuilds = require('../util/db/guilds.js')
const log = require('../util/logger.js')
const MenuUtils = require('../structs/MenuUtils.js')
const FeedSelector = require('../structs/FeedSelector.js')
const Translator = require('../structs/Translator.js')
const properties = [`message`, `embed`, `filters`, `misc-options`, `subscriptions`, 'all']

async function destSelectorFn (m, data) {
  const { guildRss, rssName, rssNameList } = data
  const sourceFeedLink = guildRss.sources[rssName].link
  const destFeedLinks = []
  for (let i = 0; i < rssNameList.length; ++i) {
    destFeedLinks.push(guildRss.sources[rssNameList[i]].link)
  }
  const cloned = []
  if (data.cloneMessage) cloned.push('message')
  if (data.cloneEmbed) cloned.push('embed')
  if (data.cloneFilters) cloned.push('filters')
  if (data.cloneMiscOptions) cloned.push('misc-options')
  if (data.cloneSubscriptions) cloned.push('subscriptions')

  return {
    ...data,
    clonedProps: cloned,
    next: {
      text: Translator.translate('commands.rssclone.confirm', guildRss.locale, { link: sourceFeedLink, cloning: cloned.join('`, `'), destinations: destFeedLinks.join('\n') })
    }
  }
}

async function confirmFn (m, data) {
  if (m.content !== 'yes') {
    throw new MenuUtils.MenuOptionError(Translator.translate('commands.rssclone.confirmError', data.guildRss ? data.guildRss.locale : null))
  }
  return { ...data, confirmed: true }
}

module.exports = async (bot, message, command) => {
  try {
    const guildRss = await dbOpsGuilds.get(message.guild.id)
    const guildLocale = guildRss ? guildRss.locale : undefined
    const translate = Translator.createLocaleTranslator(guildLocale)
    const sourceSelector = new FeedSelector(message, undefined, { command: command, prependDescription: translate('commands.rssclone.copyFrom'), globalSelect: true }, guildRss)
    const destSelector = new FeedSelector(message, destSelectorFn, { command: command, prependDescription: translate('commands.rssclone.copyTo'), multiSelect: true, globalSelect: true }, guildRss)
    const confirm = new MenuUtils.Menu(message, confirmFn, { splitOptions: { prepend: '```', append: '```' } })
    const prefix = guildRss.prefix || config.bot.prefix
    const args = MenuUtils.extractArgsAfterCommand(message.content)
    if (args.length === 0) {
      return await message.channel.send(translate('commands.rssclone.noProperties', { prefix, properties: properties.join('`, `') }))
    }
    let invalidArgs = []
    for (const arg of args) {
      if (!properties.includes(arg)) {
        invalidArgs.push(arg)
      }
    }
    if (invalidArgs.length > 0) {
      return await message.channel.send(translate('commands.rssclone.invalidProperties', { invalid: invalidArgs.join('`, `'), properties: properties.join('`, `') }))
    }

    const cloneAll = args.includes('all')
    const cloneMessage = cloneAll || args.includes('message')
    const cloneEmbed = cloneAll || args.includes('embed')
    const cloneFilters = cloneAll || args.includes('filters')
    const cloneMiscOptions = cloneAll || args.includes('misc-options')
    const cloneSubscriptions = cloneAll || args.includes('subscriptions')

    const data = await new MenuUtils.MenuSeries(message, [sourceSelector, destSelector, confirm], { cloneMessage, cloneEmbed, cloneFilters, cloneMiscOptions, cloneSubscriptions, locale: guildLocale }).start()
    if (!data || !data.confirmed) return

    const sourceFeed = guildRss.sources[data.rssName]
    const destFeeds = []
    for (let i = 0; i < data.rssNameList.length; ++i) {
      destFeeds.push(guildRss.sources[data.rssNameList[i]])
    }
    // If any of these props are empty in the source feed, then it will simply be deleted
    const emptyMessage = !sourceFeed.message
    const emptyEmbed = !sourceFeed.embeds || sourceFeed.embeds.length === 0
    const emptyFilters = !sourceFeed.filters || Object.keys(sourceFeed.filters).length === 0
    const emptySubscriptions = !sourceFeed.subscribers || sourceFeed.subscribers.length === 0
    let destLinksCount = 0

    destFeeds.forEach(destFeed => {
      // Message
      if (emptyMessage) delete destFeed.message
      else if (cloneMessage) destFeed.message = sourceFeed.message

      // Embed
      if (emptyEmbed) delete destFeed.embeds
      else if (cloneEmbed) destFeed.embeds = JSON.parse(JSON.stringify(sourceFeed.embeds))

      // Filters
      if (emptyFilters) {
        const origFilteredRoleSubs = destFeed.filters && destFeed.filters.roleSubscriptions ? JSON.parse(JSON.stringify(destFeed.filters.roleSubscriptions)) : undefined
        if (origFilteredRoleSubs) destFeed.filters = { roleSubscriptions: origFilteredRoleSubs }
        else delete destFeed.filters
      } else if (cloneFilters) {
        const origFilteredRoleSubs = destFeed.filters && destFeed.filters.roleSubscriptions ? JSON.parse(JSON.stringify(destFeed.filters.roleSubscriptions)) : undefined
        const copy = JSON.parse(JSON.stringify(sourceFeed.filters))
        delete copy.roleSubscriptions
        destFeed.filters = copy
        if (origFilteredRoleSubs) destFeed.filters.roleSubscriptions = origFilteredRoleSubs
      }

      // Options
      if (cloneMiscOptions) {
        destFeed.checkTitles = sourceFeed.checkTitles
        destFeed.imgPreviews = sourceFeed.imgPreviews
        destFeed.imgLinksExistence = sourceFeed.imgLinksExistence
        destFeed.checkDates = sourceFeed.checkDates
        destFeed.formatTables = sourceFeed.formatTables
        if (sourceFeed.splitMessage) destFeed.splitMessage = JSON.parse(JSON.stringify(sourceFeed.splitMessage))
      }

      // Global Roles
      if (emptySubscriptions) delete destFeed.subscribers
      else if (cloneSubscriptions) destFeed.subscribers = JSON.parse(JSON.stringify(sourceFeed.subscribers))

      destLinksCount++
    })

    log.command.info(`Properties ${data.clonedProps.join(',')} for the feed ${sourceFeed.link} cloning to to ${destLinksCount} feeds`, message.guild)
    await dbOpsGuilds.update(guildRss)
    await message.channel.send(`${translate('commands.rssclone.success', { cloned: data.clonedProps.join('`, `'), link: sourceFeed.link, destinationCount: destLinksCount })} ${translate('generics.backupReminder', { prefix })}`)
  } catch (err) {
    log.command.warning(`rssclone`, message.guild, err)
    if (err.code !== 50013) message.channel.send(err.message).catch(err => log.command.warning('rssclone 1', message.guild, err))
  }
}
<|endoftext|>


<commit_before>const fs = require('fs');
const async = require('async');
const path = require('path');
const proc = require('child_process');
const request = require('request');
const chalk = require('chalk');
const rssParser = require('rss-parser');
const urlParser = require('url');
const YAML = require('yamljs');

const openapiUtil = require('../util/openapi');
const logger = require('../util/logger');
const datafire = require('../index');

const SPEC_FORMATS = ['raml', 'wadl', 'swagger_1', 'api_blueprint', 'io_docs', 'google'];

const PACKAGE_PREFIX = process.env.DATAFIRE_REGISTRY_DIR ?
  process.env.DATAFIRE_REGISTRY_DIR + '/integrations' : '@datafire'

const RSS_SCHEMA = {
  type: 'object',
  properties: {
    feed: {
      type: 'object',
      properties: {
        link: {type: 'string'},
        title: {type: 'string'},
        feedUrl: {type: 'string'},
        entries: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id: {type: 'string'},
              link: {type: 'string'},
              title: {type: 'string'},
              pubDate: {type: 'string'},
              author: {type: 'string'},
              content: {type: 'string'},
              contentSnippet: {type: 'string'},
            }
          }
        }
      }
    }
  }
}

module.exports = (args) => {
  return new Promise((resolve, reject) => {
    let specFormat = SPEC_FORMATS.filter(f => args[f])[0];
    let directory = args.destination || path.join(process.cwd(), 'integrations');
    let callback = err => {
      if (err) reject(err);
      else resolve(require(path.join(directory, args.name, 'openapi.json')));
    }
    if (args.openapi) {
      integrateOpenAPI(directory, args.name, args.openapi, args.patch, callback);
    } else if (specFormat) {
      integrateSpec(directory, args.name, specFormat, args[specFormat], callback);
    } else if (args.rss) {
      integrateRSS(directory, args.name, args.rss, callback);
    } else {
      throw new Error("Must specify an API specification or RSS feed");
    }
  });
}

const DATAFIRE_LOCATION = process.env.DATAFIRE_LOCATION || 'datafire';
const integrationCode = name => `
let datafire = require('${DATAFIRE_LOCATION}');
let openapi = require('./openapi.json');
module.exports = datafire.Integration.fromOpenAPI(openapi, "${name}");
`.trim();

const addIntegration = (directory, name, type, spec, callback) => {
  name = name || getNameFromHost(spec.host);
  let baseDir = path.join(directory, name);
  let openapiFilename = path.join(baseDir, 'openapi.json');
  let integFilename = path.join(baseDir, 'index.js');
  spec.info['x-datafire'] = {name, type};
  fs.mkdir(directory, (err) => {
    if (err && err.code !== 'EEXIST') return callback(err);
    fs.mkdir(path.join(directory, name), err => {
      if (err && err.code !== 'EEXIST') return callback(err);
      fs.writeFile(openapiFilename, JSON.stringify(spec, null, 2), e => {
        if (e) return callback(e);
        fs.writeFile(integFilename, integrationCode(name), e => {
          logger.log('Created integration ' + name + ' in ' + baseDir.replace(process.cwd(), '.'));
          callback(null, spec);
        });
      });
    })
  })
}

const getLocalSpec = (name) => {
  return NATIVE_INTEGRATIONS.filter(fname => fname.startsWith(name + '.'))[0];
}

const integrateFile = (dir, name, callback) => {
  let filename = getLocalSpec(name);
  let type = filename.indexOf('.rss.') === -1 ? 'openapi' : 'rss';
  if (!filename) return callback(new Error("Integration " + name + " not found"));
  fs.readFile(path.join(NATIVE_INTEGRATIONS_DIR, filename), 'utf8', (err, data) => {
    if (err) return callback(err);
    addIntegration(dir, name, type, JSON.parse(data), callback);
  });
}

const TLDs = ['.com', '.org', '.net', '.gov', '.io', '.co.uk'];
const SUBDOMAINS = ['www.', 'api.', 'developer.'];
const getNameFromHost = (host) => {
  SUBDOMAINS.forEach(sub => {
    if (host.startsWith(sub)) host = host.substring(sub.length);
  })
  TLDs.forEach(tld => {
    if (host.endsWith(tld)) host = host.substring(0, host.length - tld.length);
  })
  return host.replace(/\./, '_');
}

const integrateOpenAPI = (dir, name, url, patch, callback) => {
  request.get(url, (err, resp, body) => {
    if (err) return callback(err);
    resp.headers['content-type'] = resp.headers['content-type'] || '';
    if (resp.headers['content-type'].indexOf('yaml') !== -1) {
      body = YAML.parse(body);
    } else {
      body = JSON.parse(body);
    }
    if (!body.host) return callback(new Error("Invalid swagger:" + JSON.stringify(body, null, 2)))
    if (patch) patch(body);
    for (let path in body.paths) {
      for (let method in body.paths[path]) {
        let op = body.paths[path][method];
        op.operationId = openapiUtil.getOperationId(method, path, op);
      }
    }
    addIntegration(dir, name, 'openapi', body, callback);
  })
}

const integrateRSS = (dir, name, urls, callback) => {
  if (typeof urls === 'string') {
    urls = {
      getItems: urls,
    }
  }
  let spec = {
    swagger: '2.0',
    basePath: '/',
    paths: {},
    definitions: {Feed: RSS_SCHEMA},
    info: {},
  }
  for (let operation in urls) {
    let url = urls[operation];
    let urlObj = urlParser.parse(url);
    urlObj.pathname = urlObj.pathname.replace(/%7B/g, '{').replace(/%7D/g, '}');
    spec.host = urlObj.hostname;
    spec.schemes = [urlObj.protocol.substring(0, urlObj.protocol.length - 1)];
    if (!name) {
      name = getNameFromHost(urlObj.hostname);
    }
    spec.paths[urlObj.pathname] = {
      get: {
        operationId: operation,
        description: "Retrieve the RSS feed",
        responses: {
          '200': {description: "OK", schema: {$ref: '#/definitions/Feed'}}
        },
        parameters: (urlObj.pathname.match(/\{\w+\}/g) || [])
              .map(p => p.substring(1, p.length - 1))
              .map(p => ({
                name: p,
                in: 'path',
                type: 'string',
                required: true,
              }))
      }
    }
  }
  async.parallel(Object.keys(spec.paths).map(path => {
    let op = spec.paths[path].get;
    return acb => {
      if (op.parameters.length) return acb();
      rssParser.parseURL(spec.schemes[0] + '://' + spec.host + path, (err, feed) => {
        if (err) return acb(err);
        feed = feed.feed;
        spec.paths[path].get.summary = feed.title;
        spec.paths[path].get.description = feed.description;
        acb();
      })
    }
  }), err => {
    if (err) return callback(err);
    let paths = Object.keys(spec.paths);
    if (paths.length === 1) {
      spec.info.title = spec.paths[paths[0]].summary;
      spec.info.description = spec.paths[paths[0]].description;
    }
    addIntegration(dir, name, 'rss', spec, callback);
  })
}

const integrateSpec = (dir, name, format, url, callback) => {
  let cmd = 'api-spec-converter "' + url + '" --from ' + format + ' --to swagger_2';
  proc.exec(cmd, (err, stdout) => {
    if (err) {
      logger.logError('Please install api-spec-converter');
      logger.log('npm install -g api-spec-converter');
      return callback(err);
    }
    addIntegration(dir, name, 'openapi', JSON.parse(stdout), callback);
  })
}

<commit_msg>add 'use strict' to integrations<commit_after>const fs = require('fs');
const async = require('async');
const path = require('path');
const proc = require('child_process');
const request = require('request');
const chalk = require('chalk');
const rssParser = require('rss-parser');
const urlParser = require('url');
const YAML = require('yamljs');

const openapiUtil = require('../util/openapi');
const logger = require('../util/logger');
const datafire = require('../index');

const SPEC_FORMATS = ['raml', 'wadl', 'swagger_1', 'api_blueprint', 'io_docs', 'google'];

const PACKAGE_PREFIX = process.env.DATAFIRE_REGISTRY_DIR ?
  process.env.DATAFIRE_REGISTRY_DIR + '/integrations' : '@datafire'

const RSS_SCHEMA = {
  type: 'object',
  properties: {
    feed: {
      type: 'object',
      properties: {
        link: {type: 'string'},
        title: {type: 'string'},
        feedUrl: {type: 'string'},
        entries: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              id: {type: 'string'},
              link: {type: 'string'},
              title: {type: 'string'},
              pubDate: {type: 'string'},
              author: {type: 'string'},
              content: {type: 'string'},
              contentSnippet: {type: 'string'},
            }
          }
        }
      }
    }
  }
}

module.exports = (args) => {
  return new Promise((resolve, reject) => {
    let specFormat = SPEC_FORMATS.filter(f => args[f])[0];
    let directory = args.destination || path.join(process.cwd(), 'integrations');
    let callback = err => {
      if (err) reject(err);
      else resolve(require(path.join(directory, args.name, 'openapi.json')));
    }
    if (args.openapi) {
      integrateOpenAPI(directory, args.name, args.openapi, args.patch, callback);
    } else if (specFormat) {
      integrateSpec(directory, args.name, specFormat, args[specFormat], callback);
    } else if (args.rss) {
      integrateRSS(directory, args.name, args.rss, callback);
    } else {
      throw new Error("Must specify an API specification or RSS feed");
    }
  });
}

const DATAFIRE_LOCATION = process.env.DATAFIRE_LOCATION || 'datafire';
const integrationCode = name => `
"use strict";
let datafire = require('${DATAFIRE_LOCATION}');
let openapi = require('./openapi.json');
module.exports = datafire.Integration.fromOpenAPI(openapi, "${name}");
`.trim();

const addIntegration = (directory, name, type, spec, callback) => {
  name = name || getNameFromHost(spec.host);
  let baseDir = path.join(directory, name);
  let openapiFilename = path.join(baseDir, 'openapi.json');
  let integFilename = path.join(baseDir, 'index.js');
  spec.info['x-datafire'] = {name, type};
  fs.mkdir(directory, (err) => {
    if (err && err.code !== 'EEXIST') return callback(err);
    fs.mkdir(path.join(directory, name), err => {
      if (err && err.code !== 'EEXIST') return callback(err);
      fs.writeFile(openapiFilename, JSON.stringify(spec, null, 2), e => {
        if (e) return callback(e);
        fs.writeFile(integFilename, integrationCode(name), e => {
          logger.log('Created integration ' + name + ' in ' + baseDir.replace(process.cwd(), '.'));
          callback(null, spec);
        });
      });
    })
  })
}

const getLocalSpec = (name) => {
  return NATIVE_INTEGRATIONS.filter(fname => fname.startsWith(name + '.'))[0];
}

const integrateFile = (dir, name, callback) => {
  let filename = getLocalSpec(name);
  let type = filename.indexOf('.rss.') === -1 ? 'openapi' : 'rss';
  if (!filename) return callback(new Error("Integration " + name + " not found"));
  fs.readFile(path.join(NATIVE_INTEGRATIONS_DIR, filename), 'utf8', (err, data) => {
    if (err) return callback(err);
    addIntegration(dir, name, type, JSON.parse(data), callback);
  });
}

const TLDs = ['.com', '.org', '.net', '.gov', '.io', '.co.uk'];
const SUBDOMAINS = ['www.', 'api.', 'developer.'];
const getNameFromHost = (host) => {
  SUBDOMAINS.forEach(sub => {
    if (host.startsWith(sub)) host = host.substring(sub.length);
  })
  TLDs.forEach(tld => {
    if (host.endsWith(tld)) host = host.substring(0, host.length - tld.length);
  })
  return host.replace(/\./, '_');
}

const integrateOpenAPI = (dir, name, url, patch, callback) => {
  request.get(url, (err, resp, body) => {
    if (err) return callback(err);
    resp.headers['content-type'] = resp.headers['content-type'] || '';
    if (resp.headers['content-type'].indexOf('yaml') !== -1) {
      body = YAML.parse(body);
    } else {
      body = JSON.parse(body);
    }
    if (!body.host) return callback(new Error("Invalid swagger:" + JSON.stringify(body, null, 2)))
    if (patch) patch(body);
    for (let path in body.paths) {
      for (let method in body.paths[path]) {
        let op = body.paths[path][method];
        op.operationId = openapiUtil.getOperationId(method, path, op);
      }
    }
    addIntegration(dir, name, 'openapi', body, callback);
  })
}

const integrateRSS = (dir, name, urls, callback) => {
  if (typeof urls === 'string') {
    urls = {
      getItems: urls,
    }
  }
  let spec = {
    swagger: '2.0',
    basePath: '/',
    paths: {},
    definitions: {Feed: RSS_SCHEMA},
    info: {},
  }
  for (let operation in urls) {
    let url = urls[operation];
    let urlObj = urlParser.parse(url);
    urlObj.pathname = urlObj.pathname.replace(/%7B/g, '{').replace(/%7D/g, '}');
    spec.host = urlObj.hostname;
    spec.schemes = [urlObj.protocol.substring(0, urlObj.protocol.length - 1)];
    if (!name) {
      name = getNameFromHost(urlObj.hostname);
    }
    spec.paths[urlObj.pathname] = {
      get: {
        operationId: operation,
        description: "Retrieve the RSS feed",
        responses: {
          '200': {description: "OK", schema: {$ref: '#/definitions/Feed'}}
        },
        parameters: (urlObj.pathname.match(/\{\w+\}/g) || [])
              .map(p => p.substring(1, p.length - 1))
              .map(p => ({
                name: p,
                in: 'path',
                type: 'string',
                required: true,
              }))
      }
    }
  }
  async.parallel(Object.keys(spec.paths).map(path => {
    let op = spec.paths[path].get;
    return acb => {
      if (op.parameters.length) return acb();
      rssParser.parseURL(spec.schemes[0] + '://' + spec.host + path, (err, feed) => {
        if (err) return acb(err);
        feed = feed.feed;
        spec.paths[path].get.summary = feed.title;
        spec.paths[path].get.description = feed.description;
        acb();
      })
    }
  }), err => {
    if (err) return callback(err);
    let paths = Object.keys(spec.paths);
    if (paths.length === 1) {
      spec.info.title = spec.paths[paths[0]].summary;
      spec.info.description = spec.paths[paths[0]].description;
    }
    addIntegration(dir, name, 'rss', spec, callback);
  })
}

const integrateSpec = (dir, name, format, url, callback) => {
  let cmd = 'api-spec-converter "' + url + '" --from ' + format + ' --to swagger_2';
  proc.exec(cmd, (err, stdout) => {
    if (err) {
      logger.logError('Please install api-spec-converter');
      logger.log('npm install -g api-spec-converter');
      return callback(err);
    }
    addIntegration(dir, name, 'openapi', JSON.parse(stdout), callback);
  })
}

<|endoftext|>