<commit_before>const ClientBase = require('./ClientBase');
const _ = require('./utils');

// Add commands to ClientBase
class Client extends ClientBase {
	// Send action message (/me <message>) on a channel
	action(channel, message, tags) {
		message = `\u0001ACTION ${message}\u0001`;
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ _.channel(channel), message ])
		);
	}

	// Ban username on channel
	ban(channel, username, reason) {
		username = _.username(username);
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/ban ${username} ${reason}` }, (res, rej) =>
			this.once('_promiseBan', err => !err ? res([ _.channel(channel), username, reason ]) : rej(err))
		);
	}

	// Clear all messages on a channel
	clear(channel) {
		return this._sendCommand({ channel, command: '/clear' }, (res, rej) =>
			this.once('_promiseClear', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Change the color of your username
	color(channel, newColor) {
		newColor = newColor ?? channel;
		return this._sendCommand({ channel: this._globalDefaultChannel, command: `/color ${newColor}` }, (res, rej) =>
			this.once('_promiseColor', err => !err ? res([ newColor ]) : rej(err))
		);
	}

	// Run commercial on a channel for X seconds
	commercial(channel, seconds) {
		seconds = seconds ?? 30;
		return this._sendCommand({ channel, command: `/commercial ${seconds}` }, (res, rej) =>
			this.once('_promiseCommercial', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Delete a specific message on a channel
	deletemessage(channel, messageUUID) {
		return this._sendCommand({ channel, command: `/delete ${messageUUID}` }, (res, rej) =>
			this.once('_promiseDeletemessage', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable emote-only mode on a channel
	emoteonly(channel) {
		return this._sendCommand({ channel, command: '/emoteonly' }, (res, rej) =>
			this.once('_promiseEmoteonly', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable emote-only mode on a channel
	emoteonlyoff(channel) {
		return this._sendCommand({ channel, command: '/emoteonlyoff' }, (res, rej) =>
			this.once('_promiseEmoteonlyoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable followers-only mode on a channel
	followersonly(channel, minutes) {
		minutes = minutes ?? 30;
		return this._sendCommand({ channel, command: `/followers ${minutes}` }, (res, rej) =>
			this.once('_promiseFollowers', err => !err ? res([ _.channel(channel), ~~minutes ]) : rej(err))
		);
	}

	// Disable followers-only mode on a channel
	followersonlyoff(channel) {
		return this._sendCommand({ channel, command: '/followersoff' }, (res, rej) =>
			this.once('_promiseFollowersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Host a channel
	host(channel, target) {
		target = _.username(target);
		return this._sendCommand({ delay: 2000, channel, command: `/host ${target}` }, (res, rej) =>
			this.once('_promiseHost', (err, remaining) => !err ? res([ _.channel(channel), target, ~~remaining ]) : rej(err))
		);
	}

	// Join a channel
	join(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ delay: undefined, channel: null, command: `JOIN ${channel}` }, (res, rej) => {
			const eventName = '_promiseJoin';
			let hasFulfilled = false;
			const listener = (err, joinedChannel) => {
				if(channel === _.channel(joinedChannel)) {
					// Received _promiseJoin event for the target channel
					this.removeListener(eventName, listener);
					hasFulfilled = true;
					!err ? res([ channel ]) : rej(err);
				}
			};
			this.on(eventName, listener);
			// Race the Promise against a delay
			const delay = this._getPromiseDelay();
			_.promiseDelay(delay).then(() => {
				if(!hasFulfilled) {
					this.emit(eventName, 'No response from Twitch.', channel);
				}
			});
		});
	}

	// Mod username on channel
	mod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/mod ${username}` }, (res, rej) =>
			this.once('_promiseMod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of mods on a channel
	mods(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ channel, command: '/mods' }, (resolve, reject) => {
			this.once('_promiseMods', (err, mods) => {
				if(!err) {
					// Update the internal list of moderators
					mods.forEach(username => {
						if(!this.moderators[channel]) {
							this.moderators[channel] = [];
						}
						if(!this.moderators[channel].includes(username)) {
							this.moderators[channel].push(username);
						}
					});
					resolve(mods);
				}
				else {
					reject(err);
				}
			});
		});
	}

	// Leave a channel
	part(channel) {
		return this._sendCommand({ delay: null, channel: null, command: `PART ${channel}` }, (res, rej) =>
			this.once('_promisePart', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a ping to the server
	ping() {
		return this._sendCommand({ delay: null, command: 'PING' }, (res, _rej) => {
			// Update the internal ping timeout check interval
			this.latency = new Date();
			this.pingTimeout = setTimeout(() => {
				if(this.ws !== null) {
					this.wasCloseCalled = false;
					this.log.error('Ping timeout.');
					this.ws.close();

					clearInterval(this.pingLoop);
					clearTimeout(this.pingTimeout);
				}
			}, this.opts.connection.timeout ?? 9999);
			this.once('_promisePing', latency => res([ parseFloat(latency) ]));
		});
	}

	// Enable R9KBeta mode on a channel
	r9kbeta(channel) {
		return this._sendCommand({ channel, command: '/r9kbeta' }, (res, rej) =>
			this.once('_promiseR9kbeta', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable R9KBeta mode on a channel
	r9kbetaoff(channel) {
		return this._sendCommand({ channel, command: '/r9kbetaoff' }, (res, rej) =>
			this.once('_promiseR9kbetaoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a raw message to the server
	raw(command, tags) {
		return this._sendCommand({ channel: null, command, tags }, (res, _rej) => res([ command ]));
	}

	// Send a message to a channel in reply to a message
	reply(channel, message, replyParentMsgId) {
		if(typeof replyParentMsgId === 'object') {
			replyParentMsgId = replyParentMsgId.id;
		}
		if(!replyParentMsgId || typeof replyParentMsgId !== 'string') {
			throw new Error('replyParentMsgId is required.');
		}
		return this.say(channel, message, { 'reply-parent-msg-id': replyParentMsgId });
	}

	// Send a message on a channel
	say(channel, message, tags) {
		channel = _.channel(channel);
		if((message.startsWith('.') && !message.startsWith('..')) || message.startsWith('/') || message.startsWith('\\')) {
			// Check if the message is an action message
			if(message.slice(1, 4) === 'me ') {
				return this.action(channel, message.slice(4));
			}
			else {
				return this._sendCommand({ channel, message, tags }, (res, _rej) =>
					// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
					// detect a response on a USERNOTICE command, but it's not yet implemented here.
					res([ channel, message ])
				);
			}
		}
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ channel, message ])
		);
	}

	// Enable slow mode on a channel
	slow(channel, seconds) {
		seconds = seconds ?? 300;
		return this._sendCommand({ channel, command: `/slow ${seconds}` }, (res, rej) =>
			this.once('_promiseSlow', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Disable slow mode on a channel
	slowoff(channel) {
		return this._sendCommand({ channel, command: '/slowoff' }, (res, rej) =>
			this.once('_promiseSlowoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable subscribers mode on a channel
	subscribers(channel) {
		return this._sendCommand({ channel, command: '/subscribers' }, (res, rej) =>
			this.once('_promiseSubscribers', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable subscribers mode on a channel
	subscribersoff(channel) {
		return this._sendCommand({ channel, command: '/subscribersoff' }, (res, rej) =>
			this.once('_promiseSubscribersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Timeout username on channel for X seconds
	timeout(channel, username, seconds, reason) {
		username = _.username(username);

		if((seconds ?? false) && !_.isInteger(seconds)) {
			reason = seconds;
			seconds = 300;
		}

		seconds = seconds ?? 300;
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/timeout ${username} ${seconds} ${reason}` }, (res, rej) =>
			this.once('_promiseTimeout', err => !err ? res([ _.channel(channel), username, ~~seconds, reason ]) : rej(err))
		);
	}

	// Unban username on channel
	unban(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unban ${username}` }, (res, rej) =>
			this.once('_promiseUnban', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// End the current hosting
	unhost(channel) {
		return this._sendCommand({ delay: 2000, channel, command: '/unhost' }, (res, rej) =>
			this.once('_promiseUnhost', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Unmod username on channel
	unmod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unmod ${username}` }, (res, rej) =>
			this.once('_promiseUnmod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Unvip username on channel
	unvip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unvip ${username}` }, (res, rej) =>
			this.once('_promiseUnvip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Add username to VIP list on channel
	vip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/vip ${username}` }, (res, rej) =>
			this.once('_promiseVip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of VIPs on a channel
	vips(channel) {
		return this._sendCommand({ channel, command: '/vips' }, (res, rej) =>
			this.once('_promiseVips', (err, vips) => !err ? res(vips) : rej(err))
		);
	}

	// Send an whisper message to a user
	whisper(username, message) {
		username = _.username(username);

		// The server will not send a whisper to the account that sent it.
		if(username === this.getUsername()) {
			return Promise.reject('Cannot send a whisper to the same account.');
		}
		return this._sendCommand({ delay: null, channel: this._globalDefaultChannel, command: `/w ${username} ${message}` }, (_res, rej) =>
			this.once('_promiseWhisper', err => err && rej(err))
		).catch(err => {
			// Either an "actual" error occured or the timeout triggered
			// the latter means no errors have occured and we can resolve
			// else just elevate the error
			if(err && typeof err === 'string' && err.indexOf('No response from Twitch.') !== 0) {
				throw err;
			}
			const from = _.channel(username);
			const userstate = Object.assign({
				'message-type': 'whisper',
				'message-id': null,
				'thread-id': null,
				username: this.getUsername()
			}, this.globaluserstate);

			// Emit for both, whisper and message
			this.emits([ 'whisper', 'message' ], [
				[ from, userstate, message, true ],
				[ from, userstate, message, true ]
			]);
			return [ username, message ];
		});
	}
}

// Alias for followersonly()
Client.prototype.followersmode = Client.prototype.followersonly;

// Alias for followersonlyoff()
Client.prototype.followersmodeoff = Client.prototype.followersonlyoff;

// Alias for part()
Client.prototype.leave = Client.prototype.part;

// Alias for slow()
Client.prototype.slowmode = Client.prototype.slow;

// Alias for r9kbeta()
Client.prototype.r9kmode = Client.prototype.r9kbeta;

// Alias for r9kbetaoff()
Client.prototype.r9kmodeoff = Client.prototype.r9kbetaoff;

// Alias for slowoff()
Client.prototype.slowmodeoff = Client.prototype.slowoff;

module.exports = Client;
<commit_msg>Client: allow extra tags for reply<commit_after>const ClientBase = require('./ClientBase');
const _ = require('./utils');

// Add commands to ClientBase
class Client extends ClientBase {
	// Send action message (/me <message>) on a channel
	action(channel, message, tags) {
		message = `\u0001ACTION ${message}\u0001`;
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ _.channel(channel), message ])
		);
	}

	// Ban username on channel
	ban(channel, username, reason) {
		username = _.username(username);
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/ban ${username} ${reason}` }, (res, rej) =>
			this.once('_promiseBan', err => !err ? res([ _.channel(channel), username, reason ]) : rej(err))
		);
	}

	// Clear all messages on a channel
	clear(channel) {
		return this._sendCommand({ channel, command: '/clear' }, (res, rej) =>
			this.once('_promiseClear', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Change the color of your username
	color(channel, newColor) {
		newColor = newColor ?? channel;
		return this._sendCommand({ channel: this._globalDefaultChannel, command: `/color ${newColor}` }, (res, rej) =>
			this.once('_promiseColor', err => !err ? res([ newColor ]) : rej(err))
		);
	}

	// Run commercial on a channel for X seconds
	commercial(channel, seconds) {
		seconds = seconds ?? 30;
		return this._sendCommand({ channel, command: `/commercial ${seconds}` }, (res, rej) =>
			this.once('_promiseCommercial', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Delete a specific message on a channel
	deletemessage(channel, messageUUID) {
		return this._sendCommand({ channel, command: `/delete ${messageUUID}` }, (res, rej) =>
			this.once('_promiseDeletemessage', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable emote-only mode on a channel
	emoteonly(channel) {
		return this._sendCommand({ channel, command: '/emoteonly' }, (res, rej) =>
			this.once('_promiseEmoteonly', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable emote-only mode on a channel
	emoteonlyoff(channel) {
		return this._sendCommand({ channel, command: '/emoteonlyoff' }, (res, rej) =>
			this.once('_promiseEmoteonlyoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable followers-only mode on a channel
	followersonly(channel, minutes) {
		minutes = minutes ?? 30;
		return this._sendCommand({ channel, command: `/followers ${minutes}` }, (res, rej) =>
			this.once('_promiseFollowers', err => !err ? res([ _.channel(channel), ~~minutes ]) : rej(err))
		);
	}

	// Disable followers-only mode on a channel
	followersonlyoff(channel) {
		return this._sendCommand({ channel, command: '/followersoff' }, (res, rej) =>
			this.once('_promiseFollowersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Host a channel
	host(channel, target) {
		target = _.username(target);
		return this._sendCommand({ delay: 2000, channel, command: `/host ${target}` }, (res, rej) =>
			this.once('_promiseHost', (err, remaining) => !err ? res([ _.channel(channel), target, ~~remaining ]) : rej(err))
		);
	}

	// Join a channel
	join(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ delay: undefined, channel: null, command: `JOIN ${channel}` }, (res, rej) => {
			const eventName = '_promiseJoin';
			let hasFulfilled = false;
			const listener = (err, joinedChannel) => {
				if(channel === _.channel(joinedChannel)) {
					// Received _promiseJoin event for the target channel
					this.removeListener(eventName, listener);
					hasFulfilled = true;
					!err ? res([ channel ]) : rej(err);
				}
			};
			this.on(eventName, listener);
			// Race the Promise against a delay
			const delay = this._getPromiseDelay();
			_.promiseDelay(delay).then(() => {
				if(!hasFulfilled) {
					this.emit(eventName, 'No response from Twitch.', channel);
				}
			});
		});
	}

	// Mod username on channel
	mod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/mod ${username}` }, (res, rej) =>
			this.once('_promiseMod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of mods on a channel
	mods(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ channel, command: '/mods' }, (resolve, reject) => {
			this.once('_promiseMods', (err, mods) => {
				if(!err) {
					// Update the internal list of moderators
					mods.forEach(username => {
						if(!this.moderators[channel]) {
							this.moderators[channel] = [];
						}
						if(!this.moderators[channel].includes(username)) {
							this.moderators[channel].push(username);
						}
					});
					resolve(mods);
				}
				else {
					reject(err);
				}
			});
		});
	}

	// Leave a channel
	part(channel) {
		return this._sendCommand({ delay: null, channel: null, command: `PART ${channel}` }, (res, rej) =>
			this.once('_promisePart', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a ping to the server
	ping() {
		return this._sendCommand({ delay: null, command: 'PING' }, (res, _rej) => {
			// Update the internal ping timeout check interval
			this.latency = new Date();
			this.pingTimeout = setTimeout(() => {
				if(this.ws !== null) {
					this.wasCloseCalled = false;
					this.log.error('Ping timeout.');
					this.ws.close();

					clearInterval(this.pingLoop);
					clearTimeout(this.pingTimeout);
				}
			}, this.opts.connection.timeout ?? 9999);
			this.once('_promisePing', latency => res([ parseFloat(latency) ]));
		});
	}

	// Enable R9KBeta mode on a channel
	r9kbeta(channel) {
		return this._sendCommand({ channel, command: '/r9kbeta' }, (res, rej) =>
			this.once('_promiseR9kbeta', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable R9KBeta mode on a channel
	r9kbetaoff(channel) {
		return this._sendCommand({ channel, command: '/r9kbetaoff' }, (res, rej) =>
			this.once('_promiseR9kbetaoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a raw message to the server
	raw(command, tags) {
		return this._sendCommand({ channel: null, command, tags }, (res, _rej) => res([ command ]));
	}

	// Send a message to a channel in reply to a message
	reply(channel, message, replyParentMsgId, tags = {}) {
		if(typeof replyParentMsgId === 'object') {
			replyParentMsgId = replyParentMsgId.id;
		}
		if(!replyParentMsgId || typeof replyParentMsgId !== 'string') {
			throw new Error('replyParentMsgId is required.');
		}
		return this.say(channel, message, { ...tags, 'reply-parent-msg-id': replyParentMsgId });
	}

	// Send a message on a channel
	say(channel, message, tags) {
		channel = _.channel(channel);
		if((message.startsWith('.') && !message.startsWith('..')) || message.startsWith('/') || message.startsWith('\\')) {
			// Check if the message is an action message
			if(message.slice(1, 4) === 'me ') {
				return this.action(channel, message.slice(4));
			}
			else {
				return this._sendCommand({ channel, message, tags }, (res, _rej) =>
					// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
					// detect a response on a USERNOTICE command, but it's not yet implemented here.
					res([ channel, message ])
				);
			}
		}
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ channel, message ])
		);
	}

	// Enable slow mode on a channel
	slow(channel, seconds) {
		seconds = seconds ?? 300;
		return this._sendCommand({ channel, command: `/slow ${seconds}` }, (res, rej) =>
			this.once('_promiseSlow', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Disable slow mode on a channel
	slowoff(channel) {
		return this._sendCommand({ channel, command: '/slowoff' }, (res, rej) =>
			this.once('_promiseSlowoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable subscribers mode on a channel
	subscribers(channel) {
		return this._sendCommand({ channel, command: '/subscribers' }, (res, rej) =>
			this.once('_promiseSubscribers', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable subscribers mode on a channel
	subscribersoff(channel) {
		return this._sendCommand({ channel, command: '/subscribersoff' }, (res, rej) =>
			this.once('_promiseSubscribersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Timeout username on channel for X seconds
	timeout(channel, username, seconds, reason) {
		username = _.username(username);

		if((seconds ?? false) && !_.isInteger(seconds)) {
			reason = seconds;
			seconds = 300;
		}

		seconds = seconds ?? 300;
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/timeout ${username} ${seconds} ${reason}` }, (res, rej) =>
			this.once('_promiseTimeout', err => !err ? res([ _.channel(channel), username, ~~seconds, reason ]) : rej(err))
		);
	}

	// Unban username on channel
	unban(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unban ${username}` }, (res, rej) =>
			this.once('_promiseUnban', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// End the current hosting
	unhost(channel) {
		return this._sendCommand({ delay: 2000, channel, command: '/unhost' }, (res, rej) =>
			this.once('_promiseUnhost', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Unmod username on channel
	unmod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unmod ${username}` }, (res, rej) =>
			this.once('_promiseUnmod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Unvip username on channel
	unvip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unvip ${username}` }, (res, rej) =>
			this.once('_promiseUnvip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Add username to VIP list on channel
	vip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/vip ${username}` }, (res, rej) =>
			this.once('_promiseVip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of VIPs on a channel
	vips(channel) {
		return this._sendCommand({ channel, command: '/vips' }, (res, rej) =>
			this.once('_promiseVips', (err, vips) => !err ? res(vips) : rej(err))
		);
	}

	// Send an whisper message to a user
	whisper(username, message) {
		username = _.username(username);

		// The server will not send a whisper to the account that sent it.
		if(username === this.getUsername()) {
			return Promise.reject('Cannot send a whisper to the same account.');
		}
		return this._sendCommand({ delay: null, channel: this._globalDefaultChannel, command: `/w ${username} ${message}` }, (_res, rej) =>
			this.once('_promiseWhisper', err => err && rej(err))
		).catch(err => {
			// Either an "actual" error occured or the timeout triggered
			// the latter means no errors have occured and we can resolve
			// else just elevate the error
			if(err && typeof err === 'string' && err.indexOf('No response from Twitch.') !== 0) {
				throw err;
			}
			const from = _.channel(username);
			const userstate = Object.assign({
				'message-type': 'whisper',
				'message-id': null,
				'thread-id': null,
				username: this.getUsername()
			}, this.globaluserstate);

			// Emit for both, whisper and message
			this.emits([ 'whisper', 'message' ], [
				[ from, userstate, message, true ],
				[ from, userstate, message, true ]
			]);
			return [ username, message ];
		});
	}
}

// Alias for followersonly()
Client.prototype.followersmode = Client.prototype.followersonly;

// Alias for followersonlyoff()
Client.prototype.followersmodeoff = Client.prototype.followersonlyoff;

// Alias for part()
Client.prototype.leave = Client.prototype.part;

// Alias for slow()
Client.prototype.slowmode = Client.prototype.slow;

// Alias for r9kbeta()
Client.prototype.r9kmode = Client.prototype.r9kbeta;

// Alias for r9kbetaoff()
Client.prototype.r9kmodeoff = Client.prototype.r9kbetaoff;

// Alias for slowoff()
Client.prototype.slowmodeoff = Client.prototype.slowoff;

module.exports = Client;
<|endoftext|>


<commit_before>const ClientBase = require('./ClientBase');
const _ = require('./utils');

// Add commands to ClientBase
class Client extends ClientBase {
	// Send action message (/me <message>) on a channel
	action(channel, message, tags) {
		message = `\u0001ACTION ${message}\u0001`;
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ _.channel(channel), message ])
		);
	}

	// Ban username on channel
	ban(channel, username, reason) {
		username = _.username(username);
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/ban ${username} ${reason}` }, (res, rej) =>
			this.once('_promiseBan', err => !err ? res([ _.channel(channel), username, reason ]) : rej(err))
		);
	}

	// Clear all messages on a channel
	clear(channel) {
		return this._sendCommand({ channel, command: '/clear' }, (res, rej) =>
			this.once('_promiseClear', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Change the color of your username
	color(channel, newColor) {
		newColor = newColor ?? channel;
		return this._sendCommand({ channel: this._globalDefaultChannel, command: `/color ${newColor}` }, (res, rej) =>
			this.once('_promiseColor', err => !err ? res([ newColor ]) : rej(err))
		);
	}

	// Run commercial on a channel for X seconds
	commercial(channel, seconds) {
		seconds = seconds ?? 30;
		return this._sendCommand({ channel, command: `/commercial ${seconds}` }, (res, rej) =>
			this.once('_promiseCommercial', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Delete a specific message on a channel
	deletemessage(channel, messageUUID) {
		return this._sendCommand({ channel, command: `/delete ${messageUUID}` }, (res, rej) =>
			this.once('_promiseDeletemessage', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable emote-only mode on a channel
	emoteonly(channel) {
		return this._sendCommand({ channel, command: '/emoteonly' }, (res, rej) =>
			this.once('_promiseEmoteonly', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable emote-only mode on a channel
	emoteonlyoff(channel) {
		return this._sendCommand({ channel, command: '/emoteonlyoff' }, (res, rej) =>
			this.once('_promiseEmoteonlyoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable followers-only mode on a channel
	followersonly(channel, minutes) {
		minutes = minutes ?? 30;
		return this._sendCommand({ channel, command: `/followers ${minutes}` }, (res, rej) =>
			this.once('_promiseFollowers', err => !err ? res([ _.channel(channel), ~~minutes ]) : rej(err))
		);
	}

	// Disable followers-only mode on a channel
	followersonlyoff(channel) {
		return this._sendCommand({ channel, command: '/followersoff' }, (res, rej) =>
			this.once('_promiseFollowersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Host a channel
	host(channel, target) {
		target = _.username(target);
		return this._sendCommand({ delay: 2000, channel, command: `/host ${target}` }, (res, rej) =>
			this.once('_promiseHost', (err, remaining) => !err ? res([ _.channel(channel), target, ~~remaining ]) : rej(err))
		);
	}

	// Join a channel
	join(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ delay: undefined, channel: null, command: `JOIN ${channel}` }, (res, rej) => {
			const eventName = '_promiseJoin';
			let hasFulfilled = false;
			const listener = (err, joinedChannel) => {
				if(channel === _.channel(joinedChannel)) {
					// Received _promiseJoin event for the target channel
					this.removeListener(eventName, listener);
					hasFulfilled = true;
					!err ? res([ channel ]) : rej(err);
				}
			};
			this.on(eventName, listener);
			// Race the Promise against a delay
			const delay = this._getPromiseDelay();
			_.promiseDelay(delay).then(() => {
				if(!hasFulfilled) {
					this.emit(eventName, 'No response from Twitch.', channel);
				}
			});
		});
	}

	// Mod username on channel
	mod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/mod ${username}` }, (res, rej) =>
			this.once('_promiseMod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of mods on a channel
	mods(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ channel, command: '/mods' }, (resolve, reject) => {
			this.once('_promiseMods', (err, mods) => {
				if(!err) {
					// Update the internal list of moderators
					mods.forEach(username => {
						if(!this.moderators[channel]) {
							this.moderators[channel] = [];
						}
						if(!this.moderators[channel].includes(username)) {
							this.moderators[channel].push(username);
						}
					});
					resolve(mods);
				}
				else {
					reject(err);
				}
			});
		});
	}

	// Leave a channel
	part(channel) {
		return this._sendCommand({ delay: null, channel: null, command: `PART ${channel}` }, (res, rej) =>
			this.once('_promisePart', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a ping to the server
	ping() {
		return this._sendCommand({ delay: null, command: 'PING' }, (res, _rej) => {
			// Update the internal ping timeout check interval
			this.latency = new Date();
			this.pingTimeout = setTimeout(() => {
				if(this.ws !== null) {
					this.wasCloseCalled = false;
					this.log.error('Ping timeout.');
					this.ws.close();

					clearInterval(this.pingLoop);
					clearTimeout(this.pingTimeout);
				}
			}, this.opts.connection.timeout ?? 9999);
			this.once('_promisePing', latency => res([ parseFloat(latency) ]));
		});
	}

	// Enable R9KBeta mode on a channel
	r9kbeta(channel) {
		return this._sendCommand({ channel, command: '/r9kbeta' }, (res, rej) =>
			this.once('_promiseR9kbeta', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable R9KBeta mode on a channel
	r9kbetaoff(channel) {
		return this._sendCommand({ channel, command: '/r9kbetaoff' }, (res, rej) =>
			this.once('_promiseR9kbetaoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a raw message to the server
	raw(command, tags) {
		return this._sendCommand({ channel: null, command, tags }, (res, _rej) => res([ command ]));
	}

	// Send a message to a channel in reply to a message
	reply(channel, message, replyParentMsgId, tags = {}) {
		if(typeof replyParentMsgId === 'object') {
			replyParentMsgId = replyParentMsgId.id;
		}
		if(!replyParentMsgId || typeof replyParentMsgId !== 'string') {
			throw new Error('replyParentMsgId is required.');
		}
		return this.say(channel, message, { ...tags, 'reply-parent-msg-id': replyParentMsgId });
	}

	// Send a message on a channel
	say(channel, message, tags) {
		channel = _.channel(channel);
		if((message.startsWith('.') && !message.startsWith('..')) || message.startsWith('/') || message.startsWith('\\')) {
			// Check if the message is an action message
			if(message.slice(1, 4) === 'me ') {
				return this.action(channel, message.slice(4));
			}
			else {
				return this._sendCommand({ channel, message, tags }, (res, _rej) =>
					// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
					// detect a response on a USERNOTICE command, but it's not yet implemented here.
					res([ channel, message ])
				);
			}
		}
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ channel, message ])
		);
	}

	// Enable slow mode on a channel
	slow(channel, seconds) {
		seconds = seconds ?? 300;
		return this._sendCommand({ channel, command: `/slow ${seconds}` }, (res, rej) =>
			this.once('_promiseSlow', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Disable slow mode on a channel
	slowoff(channel) {
		return this._sendCommand({ channel, command: '/slowoff' }, (res, rej) =>
			this.once('_promiseSlowoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable subscribers mode on a channel
	subscribers(channel) {
		return this._sendCommand({ channel, command: '/subscribers' }, (res, rej) =>
			this.once('_promiseSubscribers', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable subscribers mode on a channel
	subscribersoff(channel) {
		return this._sendCommand({ channel, command: '/subscribersoff' }, (res, rej) =>
			this.once('_promiseSubscribersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Timeout username on channel for X seconds
	timeout(channel, username, seconds, reason) {
		username = _.username(username);

		if((seconds ?? false) && !_.isInteger(seconds)) {
			reason = seconds;
			seconds = 300;
		}

		seconds = seconds ?? 300;
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/timeout ${username} ${seconds} ${reason}` }, (res, rej) =>
			this.once('_promiseTimeout', err => !err ? res([ _.channel(channel), username, ~~seconds, reason ]) : rej(err))
		);
	}

	// Unban username on channel
	unban(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unban ${username}` }, (res, rej) =>
			this.once('_promiseUnban', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// End the current hosting
	unhost(channel) {
		return this._sendCommand({ delay: 2000, channel, command: '/unhost' }, (res, rej) =>
			this.once('_promiseUnhost', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Unmod username on channel
	unmod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unmod ${username}` }, (res, rej) =>
			this.once('_promiseUnmod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Unvip username on channel
	unvip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unvip ${username}` }, (res, rej) =>
			this.once('_promiseUnvip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Add username to VIP list on channel
	vip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/vip ${username}` }, (res, rej) =>
			this.once('_promiseVip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of VIPs on a channel
	vips(channel) {
		return this._sendCommand({ channel, command: '/vips' }, (res, rej) =>
			this.once('_promiseVips', (err, vips) => !err ? res(vips) : rej(err))
		);
	}

	// Send an whisper message to a user
	whisper(username, message) {
		username = _.username(username);

		// The server will not send a whisper to the account that sent it.
		if(username === this.getUsername()) {
			return Promise.reject('Cannot send a whisper to the same account.');
		}
		return this._sendCommand({ delay: null, channel: this._globalDefaultChannel, command: `/w ${username} ${message}` }, (_res, rej) =>
			this.once('_promiseWhisper', err => err && rej(err))
		).catch(err => {
			// Either an "actual" error occured or the timeout triggered
			// the latter means no errors have occured and we can resolve
			// else just elevate the error
			if(err && typeof err === 'string' && err.indexOf('No response from Twitch.') !== 0) {
				throw err;
			}
			const from = _.channel(username);
			const userstate = Object.assign({
				'message-type': 'whisper',
				'message-id': null,
				'thread-id': null,
				username: this.getUsername()
			}, this.globaluserstate);

			// Emit for both, whisper and message
			this.emits([ 'whisper', 'message' ], [
				[ from, userstate, message, true ],
				[ from, userstate, message, true ]
			]);
			return [ username, message ];
		});
	}
}

// Alias for followersonly()
Client.prototype.followersmode = Client.prototype.followersonly;

// Alias for followersonlyoff()
Client.prototype.followersmodeoff = Client.prototype.followersonlyoff;

// Alias for part()
Client.prototype.leave = Client.prototype.part;

// Alias for slow()
Client.prototype.slowmode = Client.prototype.slow;

// Alias for r9kbeta()
Client.prototype.r9kmode = Client.prototype.r9kbeta;

// Alias for r9kbetaoff()
Client.prototype.r9kmodeoff = Client.prototype.r9kbetaoff;

// Alias for slowoff()
Client.prototype.slowmodeoff = Client.prototype.slowoff;

module.exports = Client;
<commit_msg>Client: switch color paramters preferred order<commit_after>const ClientBase = require('./ClientBase');
const _ = require('./utils');

// Add commands to ClientBase
class Client extends ClientBase {
	// Send action message (/me <message>) on a channel
	action(channel, message, tags) {
		message = `\u0001ACTION ${message}\u0001`;
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ _.channel(channel), message ])
		);
	}

	// Ban username on channel
	ban(channel, username, reason) {
		username = _.username(username);
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/ban ${username} ${reason}` }, (res, rej) =>
			this.once('_promiseBan', err => !err ? res([ _.channel(channel), username, reason ]) : rej(err))
		);
	}

	// Clear all messages on a channel
	clear(channel) {
		return this._sendCommand({ channel, command: '/clear' }, (res, rej) =>
			this.once('_promiseClear', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Change the color of your username
	color(newColor, _oldNewColor) {
		newColor = _oldNewColor ?? newColor;
		return this._sendCommand({ channel: this._globalDefaultChannel, command: `/color ${newColor}` }, (res, rej) =>
			this.once('_promiseColor', err => !err ? res([ newColor ]) : rej(err))
		);
	}

	// Run commercial on a channel for X seconds
	commercial(channel, seconds) {
		seconds = seconds ?? 30;
		return this._sendCommand({ channel, command: `/commercial ${seconds}` }, (res, rej) =>
			this.once('_promiseCommercial', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Delete a specific message on a channel
	deletemessage(channel, messageUUID) {
		return this._sendCommand({ channel, command: `/delete ${messageUUID}` }, (res, rej) =>
			this.once('_promiseDeletemessage', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable emote-only mode on a channel
	emoteonly(channel) {
		return this._sendCommand({ channel, command: '/emoteonly' }, (res, rej) =>
			this.once('_promiseEmoteonly', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable emote-only mode on a channel
	emoteonlyoff(channel) {
		return this._sendCommand({ channel, command: '/emoteonlyoff' }, (res, rej) =>
			this.once('_promiseEmoteonlyoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable followers-only mode on a channel
	followersonly(channel, minutes) {
		minutes = minutes ?? 30;
		return this._sendCommand({ channel, command: `/followers ${minutes}` }, (res, rej) =>
			this.once('_promiseFollowers', err => !err ? res([ _.channel(channel), ~~minutes ]) : rej(err))
		);
	}

	// Disable followers-only mode on a channel
	followersonlyoff(channel) {
		return this._sendCommand({ channel, command: '/followersoff' }, (res, rej) =>
			this.once('_promiseFollowersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Host a channel
	host(channel, target) {
		target = _.username(target);
		return this._sendCommand({ delay: 2000, channel, command: `/host ${target}` }, (res, rej) =>
			this.once('_promiseHost', (err, remaining) => !err ? res([ _.channel(channel), target, ~~remaining ]) : rej(err))
		);
	}

	// Join a channel
	join(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ delay: undefined, channel: null, command: `JOIN ${channel}` }, (res, rej) => {
			const eventName = '_promiseJoin';
			let hasFulfilled = false;
			const listener = (err, joinedChannel) => {
				if(channel === _.channel(joinedChannel)) {
					// Received _promiseJoin event for the target channel
					this.removeListener(eventName, listener);
					hasFulfilled = true;
					!err ? res([ channel ]) : rej(err);
				}
			};
			this.on(eventName, listener);
			// Race the Promise against a delay
			const delay = this._getPromiseDelay();
			_.promiseDelay(delay).then(() => {
				if(!hasFulfilled) {
					this.emit(eventName, 'No response from Twitch.', channel);
				}
			});
		});
	}

	// Mod username on channel
	mod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/mod ${username}` }, (res, rej) =>
			this.once('_promiseMod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of mods on a channel
	mods(channel) {
		channel = _.channel(channel);
		return this._sendCommand({ channel, command: '/mods' }, (resolve, reject) => {
			this.once('_promiseMods', (err, mods) => {
				if(!err) {
					// Update the internal list of moderators
					mods.forEach(username => {
						if(!this.moderators[channel]) {
							this.moderators[channel] = [];
						}
						if(!this.moderators[channel].includes(username)) {
							this.moderators[channel].push(username);
						}
					});
					resolve(mods);
				}
				else {
					reject(err);
				}
			});
		});
	}

	// Leave a channel
	part(channel) {
		return this._sendCommand({ delay: null, channel: null, command: `PART ${channel}` }, (res, rej) =>
			this.once('_promisePart', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a ping to the server
	ping() {
		return this._sendCommand({ delay: null, command: 'PING' }, (res, _rej) => {
			// Update the internal ping timeout check interval
			this.latency = new Date();
			this.pingTimeout = setTimeout(() => {
				if(this.ws !== null) {
					this.wasCloseCalled = false;
					this.log.error('Ping timeout.');
					this.ws.close();

					clearInterval(this.pingLoop);
					clearTimeout(this.pingTimeout);
				}
			}, this.opts.connection.timeout ?? 9999);
			this.once('_promisePing', latency => res([ parseFloat(latency) ]));
		});
	}

	// Enable R9KBeta mode on a channel
	r9kbeta(channel) {
		return this._sendCommand({ channel, command: '/r9kbeta' }, (res, rej) =>
			this.once('_promiseR9kbeta', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable R9KBeta mode on a channel
	r9kbetaoff(channel) {
		return this._sendCommand({ channel, command: '/r9kbetaoff' }, (res, rej) =>
			this.once('_promiseR9kbetaoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Send a raw message to the server
	raw(command, tags) {
		return this._sendCommand({ channel: null, command, tags }, (res, _rej) => res([ command ]));
	}

	// Send a message to a channel in reply to a message
	reply(channel, message, replyParentMsgId, tags = {}) {
		if(typeof replyParentMsgId === 'object') {
			replyParentMsgId = replyParentMsgId.id;
		}
		if(!replyParentMsgId || typeof replyParentMsgId !== 'string') {
			throw new Error('replyParentMsgId is required.');
		}
		return this.say(channel, message, { ...tags, 'reply-parent-msg-id': replyParentMsgId });
	}

	// Send a message on a channel
	say(channel, message, tags) {
		channel = _.channel(channel);
		if((message.startsWith('.') && !message.startsWith('..')) || message.startsWith('/') || message.startsWith('\\')) {
			// Check if the message is an action message
			if(message.slice(1, 4) === 'me ') {
				return this.action(channel, message.slice(4));
			}
			else {
				return this._sendCommand({ channel, message, tags }, (res, _rej) =>
					// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
					// detect a response on a USERNOTICE command, but it's not yet implemented here.
					res([ channel, message ])
				);
			}
		}
		return this._sendMessage({ delay: this._getPromiseDelay(), channel, message, tags }, (res, _rej) =>
			// The message is assumed to be sent and recieved by Twitch. The "client-nonce" tag can be used to
			// detect a response on a USERNOTICE command, but it's not yet implemented here.
			res([ channel, message ])
		);
	}

	// Enable slow mode on a channel
	slow(channel, seconds) {
		seconds = seconds ?? 300;
		return this._sendCommand({ channel, command: `/slow ${seconds}` }, (res, rej) =>
			this.once('_promiseSlow', err => !err ? res([ _.channel(channel), ~~seconds ]) : rej(err))
		);
	}

	// Disable slow mode on a channel
	slowoff(channel) {
		return this._sendCommand({ channel, command: '/slowoff' }, (res, rej) =>
			this.once('_promiseSlowoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Enable subscribers mode on a channel
	subscribers(channel) {
		return this._sendCommand({ channel, command: '/subscribers' }, (res, rej) =>
			this.once('_promiseSubscribers', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Disable subscribers mode on a channel
	subscribersoff(channel) {
		return this._sendCommand({ channel, command: '/subscribersoff' }, (res, rej) =>
			this.once('_promiseSubscribersoff', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Timeout username on channel for X seconds
	timeout(channel, username, seconds, reason) {
		username = _.username(username);

		if((seconds ?? false) && !_.isInteger(seconds)) {
			reason = seconds;
			seconds = 300;
		}

		seconds = seconds ?? 300;
		reason = reason ?? '';
		return this._sendCommand({ channel, command: `/timeout ${username} ${seconds} ${reason}` }, (res, rej) =>
			this.once('_promiseTimeout', err => !err ? res([ _.channel(channel), username, ~~seconds, reason ]) : rej(err))
		);
	}

	// Unban username on channel
	unban(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unban ${username}` }, (res, rej) =>
			this.once('_promiseUnban', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// End the current hosting
	unhost(channel) {
		return this._sendCommand({ delay: 2000, channel, command: '/unhost' }, (res, rej) =>
			this.once('_promiseUnhost', err => !err ? res([ _.channel(channel) ]) : rej(err))
		);
	}

	// Unmod username on channel
	unmod(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unmod ${username}` }, (res, rej) =>
			this.once('_promiseUnmod', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Unvip username on channel
	unvip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/unvip ${username}` }, (res, rej) =>
			this.once('_promiseUnvip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Add username to VIP list on channel
	vip(channel, username) {
		username = _.username(username);
		return this._sendCommand({ channel, command: `/vip ${username}` }, (res, rej) =>
			this.once('_promiseVip', err => !err ? res([ _.channel(channel), username ]) : rej(err))
		);
	}

	// Get list of VIPs on a channel
	vips(channel) {
		return this._sendCommand({ channel, command: '/vips' }, (res, rej) =>
			this.once('_promiseVips', (err, vips) => !err ? res(vips) : rej(err))
		);
	}

	// Send an whisper message to a user
	whisper(username, message) {
		username = _.username(username);

		// The server will not send a whisper to the account that sent it.
		if(username === this.getUsername()) {
			return Promise.reject('Cannot send a whisper to the same account.');
		}
		return this._sendCommand({ delay: null, channel: this._globalDefaultChannel, command: `/w ${username} ${message}` }, (_res, rej) =>
			this.once('_promiseWhisper', err => err && rej(err))
		).catch(err => {
			// Either an "actual" error occured or the timeout triggered
			// the latter means no errors have occured and we can resolve
			// else just elevate the error
			if(err && typeof err === 'string' && err.indexOf('No response from Twitch.') !== 0) {
				throw err;
			}
			const from = _.channel(username);
			const userstate = Object.assign({
				'message-type': 'whisper',
				'message-id': null,
				'thread-id': null,
				username: this.getUsername()
			}, this.globaluserstate);

			// Emit for both, whisper and message
			this.emits([ 'whisper', 'message' ], [
				[ from, userstate, message, true ],
				[ from, userstate, message, true ]
			]);
			return [ username, message ];
		});
	}
}

// Alias for followersonly()
Client.prototype.followersmode = Client.prototype.followersonly;

// Alias for followersonlyoff()
Client.prototype.followersmodeoff = Client.prototype.followersonlyoff;

// Alias for part()
Client.prototype.leave = Client.prototype.part;

// Alias for slow()
Client.prototype.slowmode = Client.prototype.slow;

// Alias for r9kbeta()
Client.prototype.r9kmode = Client.prototype.r9kbeta;

// Alias for r9kbetaoff()
Client.prototype.r9kmodeoff = Client.prototype.r9kbetaoff;

// Alias for slowoff()
Client.prototype.slowmodeoff = Client.prototype.slowoff;

module.exports = Client;
<|endoftext|>


<commit_before>/**
 * Logger will allow the use of any logger as well as support multiple log levels
 * depending on a defined propery called `loglevel`.
 */

function Logger(Framework) {

  // If log is specified, use it, otherwise use the console
  if (Framework.logger) {
    Framework.error = Framework.logger.error;
    Framework.warn = Framework.logger.warn;
    Framework.log = Framework.logger.debug || Framework.logger;
    Framework.verbose = Framework.logger.verbose;
  }
  // In IE, we can't default to the browser console because there IS NO BROWSER CONSOLE
  else if (typeof console !== 'undefined') {
    Framework.error = console && console.error && console.error.bind(console);
    Framework.warn = console && console.warn && console.warn.bind(console);
    Framework.log = console && console.log && console.log.bind(console);
    Framework.verbose = console && console.debug && console.debug.bind(console);
  }

  // Support for `debug` for backwards compatibility
  Framework.debug = Framework.log;

  // Turn off all logs in production
  if (Framework.production) {
    Framework.logLevel = 'silent';
  }

  // Use log level config if provided
  var noop = function() {};
  switch (Framework.logLevel) {

    case 'verbose':
      break;

    case 'debug':
      Framework.verbose = noop;
      break;

    case 'warn':
      Framework.verbose = Framework.log = Framework.debug = noop;
      break;

    case 'error':
      Framework.verbose = Framework.log = Framework.debug = Framework.warn = noop;
      break;

    case 'silent':
      Framework.verbose = Framework.log = Framework.debug = Framework.warn =
      Framework.error = noop;
      break;

    default:
      throw new Error('Unrecognized logging level config ' +
        '(' + Framework.id + '.logLevel = "' + Framework.logLevel + '")');
  }
}
<commit_msg>Revert "Add Framework.debug so that defining Framework.logLevel works correctly now"<commit_after>/**
 *
 *
 */

function Logger (Framework) {

	// If log is specified, use it, otherwise use the console
	if (Framework.logger) {
		Framework.error		= Framework.logger.error;
		Framework.warn		= Framework.logger.warn;
		Framework.log		= Framework.logger.debug || Framework.logger;
		Framework.verbose	= Framework.logger.verbose;
	}
	// In IE, we can't default to the browser console because there IS NO BROWSER CONSOLE
	else if (typeof console !== 'undefined') {
		Framework.error		= console && console.error && console.error.bind(console);
		Framework.warn		= console && console.warn && console.warn.bind(console);
		Framework.log		= console && console.log && console.log.bind(console);
		Framework.verbose	= console && console.debug && console.debug.bind(console);
	}

	// Support for `debug` for backwards compatibility
	Framework.debug = Framework.log;

	// Turn off all logs in production
	if (Framework.production) {
		Framework.logLevel = 'silent';
	}

	// Use log level config if provided
	var noop = function () {};
	switch ( Framework.logLevel ) {

		case 'verbose':	break;

		case 'debug':	Framework.verbose = noop;
						break;

		case 'warn':	Framework.verbose = Framework.log = noop;
						break;

		case 'error':	Framework.verbose = Framework.log =
						Framework.warn = noop;
						break;

		case 'silent':	Framework.verbose = Framework.log =
						Framework.warn = Framework.error = noop;
						break;

		default:		throw new Error ('Unrecognized logging level config ' +
						'(' + Framework.id + '.logLevel = "' + Framework.logLevel + '")');

	}

}
<|endoftext|>


<commit_before>var ElementType = require("./ElementType.js");

function Parser(handler, options) {
    if (options) this._options = options;

    validateHandler(handler);
    this._handler = handler;

    this._buffer = "";
    this._prevTagSep = "";
    this._contentFlags = 0;
    this._done = false;
    this._parseState = ElementType.Text;
}

//**"Static"**//
//Regular expressions used for cleaning up and parsing (stateless)
var _reWhitespace = /\s/; //Used to find any whitespace to split on
var _reTagName = /^\s*(\/?)\s*([^\s\/]+)/; //Used to find the tag name for an element

//Find attributes in a tag
var _reAttrib = /([^=<>\"\'\s]+)\s*=\s*"([^"]*)"|([^=<>\"\'\s]+)\s*=\s*'([^']*)'|([^=<>\"\'\s]+)\s*=\s*([^'"\s]+)|([^=<>\"\'\s\/]+)/g; //"

var tagTypes = {};
tagTypes[ElementType.Script] = true;
tagTypes[ElementType.Style] = true;
tagTypes[ElementType.Tag] = true;

Parser.prototype._options = {
    includeLocation: false, //Do not track element position in document by default
    xmlMode: false //Special behaviour for script/style tags by default
};

//**Public**//
//Methods//
//Parses a complete HTML and pushes it to the handler
Parser.prototype.parseComplete = function(data) {
    this.reset();
    this.parseChunk(data);
    this.done();
};

//Parses a piece of an HTML document
Parser.prototype.parseChunk = function(data) {
    if (this._done)
        this.handleError(
            Error("Attempted to parse chunk after parsing already done")
        );
    this._buffer += data; //FIXME: this can be a bottleneck
    this.parseTags();
};

//Tells the parser that the HTML being parsed is complete
Parser.prototype.done = function() {
    if (this._done) return;
    this._done = true;

    //Push any unparsed text into a final element in the element list
    if (this._buffer) {
        var data = this._buffer;
        if (this._parseState === ElementType.Tag) {
            data = data.trim();
            var name = parseTagName(data);
            if (name.charAt(0) === "/") this._handler.closeTag(name.substr(1));
            else
                this._handler.openTag({
                    name: name,
                    raw: data,
                    data: data,
                    attribs: parseAttributes(data)
                });
        } else
            this._handler.writeText({
                raw: data,
                data: data,
                type: ElementType.Text
            });

        this._buffer = "";
    }
    this._handler.done();
};

//Resets the parser to a blank state, ready to parse a new HTML document
Parser.prototype.reset = function() {
    Parser.call(this, this._handler);
    this._handler.reset();
};

//**Private**//
//Takes an element and adds an "attribs" property for any element attributes found
var parseAttributes = function(data) {
    var pos = data.search(_reWhitespace);
    if (pos === -1) return;
    var attribRaw = data.substr(pos);

    _reAttrib.lastIndex = 0;
    var match,
        attrs = {};

    while ((match = _reAttrib.exec(attribRaw))) {
        if (match[1]) attrs[match[1]] = match[2];
        else if (match[3]) attrs[match[3]] = match[4];
        else if (match[5]) attrs[match[5]] = match[6];
        else if (match[7]) attrs[match[7]] = match[7];
    }

    return attrs;
};

//Extracts the base tag name from the data value of an element
var parseTagName = function(data) {
    var match = data.match(_reTagName);
    if (match === null) return "";
    return match[1] + match[2];
};

//Special tags that are threated differently
var SpecialTags = {};
SpecialTags[ElementType.Tag] = 0;
SpecialTags[ElementType.Style] = 1; //2^0
SpecialTags[ElementType.Script] = 2; //2^1
SpecialTags.w = 4; //2^2 - if set, append prev tag sep to data
SpecialTags[ElementType.Comment] = 8; //2^3

//Parses through HTML text and returns an array of found elements
Parser.prototype.parseTags = function() {
    var buffer = this._buffer,
        current = 0;

    var next,
        tagSep,
        rawData,
        elementName,
        prevElement,
        elementType,
        elementData,
        attributes;

    var opening = buffer.indexOf("<"),
        closing = buffer.indexOf(">");

    while (opening !== closing) {
        //just false if both are -1
        if (closing === -1 || (opening !== -1 && opening < closing)) {
            next = opening;
            tagSep = "<";
            opening = buffer.indexOf(tagSep, next + 1);
        } else {
            next = closing;
            tagSep = ">";
            closing = buffer.indexOf(tagSep, next + 1);
        }
        rawData = buffer.substring(current, next); //The next chunk of data to parse
        current = next + 1;
        elementType = this._parseState;
        this._parseState = tagSep === "<" ? ElementType.Tag : ElementType.Text;

        if (elementType === ElementType.Tag) {
            elementData = rawData.trim();
            elementName = parseTagName(elementData);
        } else {
            elementData = rawData;
            elementName = "";
        }

        //This section inspects the current tag stack and modifies the current
        //element if we're actually parsing a special area (script/comment/style tag)
        if (this._contentFlags === 0) {
            /*do nothing*/
        } else if (this._contentFlags >= SpecialTags[ElementType.Comment]) {
            //We're currently in a comment tag
            this._processComment(rawData, tagSep);
            continue;
        }
        //if it's a closing tag, remove the flag
        else if (
            this._contentFlags >= SpecialTags[ElementType.Script] &&
            elementName === "/script"
        ) {
            //remove the script flag (also removes the written flag)
            this._contentFlags %= SpecialTags[ElementType.Script];
        } else if (
            this._contentFlags >= SpecialTags[ElementType.Style] &&
            elementName === "/style"
        ) {
            //remove the style flag (also removes the written flag)
            this._contentFlags %= SpecialTags[ElementType.Style];
        }
        //special behaviour for script & style tags
        //Make sure we're not in a comment
        else if (!this._options.xmlMode && rawData.substring(0, 3) !== "!--") {
            //All data from here to style close is now a text element
            elementType = ElementType.Text;
            //If the previous element is text, append the last tag sep to element
            if (this._contentFlags >= SpecialTags.w) {
                this._handler.writeText(this._prevTagSep + rawData);
            } else {
                //The previous element was not text
                this._contentFlags += SpecialTags.w;
                this._handler.writeText(rawData);
            }
            this._prevTagSep = tagSep;
            continue;
        }

        //Processing of non-special tags
        if (elementType === ElementType.Tag) {
            if (rawData.substring(0, 3) === "!--") {
                //This tag is a comment
                this._contentFlags += SpecialTags[ElementType.Comment];
                this._processComment(rawData.substr(3), tagSep);
            } else if (rawData.charAt(0) === "!" || rawData.charAt(0) === "?") {
                //ElementType.Directive
                //TODO: what about CDATA?
                this._handler.writeDirective(elementName, elementData);
            } else this._processTag(elementName, elementData, tagSep, rawData);
        } else if (elementType === ElementType.Text && rawData !== "") {
            this._handler.writeText(elementData);
        }
    }

    this._buffer = buffer.substring(current);
};

Parser.prototype._processComment = function(rawData, tagSep) {
    if (tagSep === ">" && rawData.substr(-2) === "--") {
        //comment ends
        //remove the written flag (also removes the comment flag)
        this._contentFlags %= SpecialTags.w;
        rawData = rawData.slice(0, -2);
    } else rawData += tagSep;
    this._prevTagSep = tagSep;

    this._handler.writeComment(rawData);
};

Parser.prototype._processTag = function(name, data, tagSep, raw) {
    if (name.charAt(0) === "/") {
        this._handler.closeTag(name.substring(1));
        return;
    }

    var type = ElementType.Tag;
    if (this._options.xmlMode) {
        /*do nothing*/
    } else if (name === "script") type = ElementType.Script;
    else if (name === "style") type = ElementType.Style;

    this._handler.openTag(name, parseAttributes(data), type);

    //If tag self-terminates, add an explicit, separate closing tag
    if (data.substr(-1) === "/") {
        this._handler.closeTag(name);
    } else {
        this._contentFlags += SpecialTags[type];
        this._prevTagSep = tagSep;
    }
};

//Checks the handler to ensure it is an object with the right interface
var validateHandler = function(handler) {
    if (typeof handler !== "object") throw Error("Handler is not an object");
    [
        "reset",
        "done",
        "openTag",
        "closeTag",
        "writeText",
        "writeComment",
        "writeDirective"
    ].forEach(function(name) {
        if (typeof handler[name] !== "function")
            throw Error("Handler method '" + name + "' is invalid");
    });
};

Parser.prototype.handleError = function(error) {
    if (typeof this._handler.error === "function") this._handler.error(error);
    else throw error;
};

module.exports = Parser;
<commit_msg>Fixed two bugs<commit_after>var ElementType = require("./ElementType.js");

function Parser(handler, options) {
    if (options) this._options = options;

    validateHandler(handler);
    this._handler = handler;

    this._buffer = "";
    this._prevTagSep = "";
    this._contentFlags = 0;
    this._done = false;
    this._parseState = ElementType.Text;
}

//**"Static"**//
//Regular expressions used for cleaning up and parsing (stateless)
var _reWhitespace = /\s/; //Used to find any whitespace to split on
var _reTagName = /^\s*(\/?)\s*([^\s\/]+)/; //Used to find the tag name for an element

//Find attributes in a tag
var _reAttrib = /([^=<>\"\'\s]+)\s*=\s*"([^"]*)"|([^=<>\"\'\s]+)\s*=\s*'([^']*)'|([^=<>\"\'\s]+)\s*=\s*([^'"\s]+)|([^=<>\"\'\s\/]+)/g; //"

var tagTypes = {};
tagTypes[ElementType.Script] = true;
tagTypes[ElementType.Style] = true;
tagTypes[ElementType.Tag] = true;

Parser.prototype._options = {
    includeLocation: false, //Do not track element position in document by default
    xmlMode: false //Special behaviour for script/style tags by default
};

//**Public**//
//Methods//
//Parses a complete HTML and pushes it to the handler
Parser.prototype.parseComplete = function(data) {
    this.reset();
    this.parseChunk(data);
    this.done();
};

//Parses a piece of an HTML document
Parser.prototype.parseChunk = function(data) {
    if (this._done)
        this.handleError(
            Error("Attempted to parse chunk after parsing already done")
        );
    this._buffer += data; //FIXME: this can be a bottleneck
    this.parseTags();
};

//Tells the parser that the HTML being parsed is complete
Parser.prototype.done = function() {
    if (this._done) return;
    this._done = true;

    //Push any unparsed text into a final element in the element list
    if (this._buffer) {
        var data = this._buffer;
        if (this._parseState === ElementType.Tag) {
            data = data.trim();
            var name = parseTagName(data);
            if (name.charAt(0) === "/") this._handler.closeTag(name.substr(1));
            else
                this._handler.openTag(
                    name,
                    parseAttributes(data),
                    ElementType.Tag
                );
        } else this._handler.writeText(data);

        this._buffer = "";
    }
    this._handler.done();
};

//Resets the parser to a blank state, ready to parse a new HTML document
Parser.prototype.reset = function() {
    Parser.call(this, this._handler);
    this._handler.reset();
};

//**Private**//
//Takes an element and adds an "attribs" property for any element attributes found
var parseAttributes = function(data) {
    var pos = data.search(_reWhitespace);
    if (pos === -1) return;
    var attribRaw = data.substr(pos);

    _reAttrib.lastIndex = 0;
    var match,
        attrs = {};

    while ((match = _reAttrib.exec(attribRaw))) {
        if (match[1]) attrs[match[1]] = match[2];
        else if (match[3]) attrs[match[3]] = match[4];
        else if (match[5]) attrs[match[5]] = match[6];
        else if (match[7]) attrs[match[7]] = match[7];
    }

    return attrs;
};

//Extracts the base tag name from the data value of an element
var parseTagName = function(data) {
    var match = data.match(_reTagName);
    if (match === null) return "";
    return match[1] + match[2];
};

//Special tags that are threated differently
var SpecialTags = {};
SpecialTags[ElementType.Tag] = 0;
SpecialTags[ElementType.Style] = 1; //2^0
SpecialTags[ElementType.Script] = 2; //2^1
SpecialTags.w = 4; //2^2 - if set, append prev tag sep to data
SpecialTags[ElementType.Comment] = 8; //2^3

//Parses through HTML text and returns an array of found elements
Parser.prototype.parseTags = function() {
    var buffer = this._buffer,
        current = 0;

    var next,
        tagSep,
        rawData,
        elementName,
        prevElement,
        elementType,
        elementData,
        attributes;

    var opening = buffer.indexOf("<"),
        closing = buffer.indexOf(">");

    while (opening !== closing) {
        //just false if both are -1
        if (closing === -1 || (opening !== -1 && opening < closing)) {
            next = opening;
            tagSep = "<";
            opening = buffer.indexOf(tagSep, next + 1);
        } else {
            next = closing;
            tagSep = ">";
            closing = buffer.indexOf(tagSep, next + 1);
        }
        rawData = buffer.substring(current, next); //The next chunk of data to parse
        current = next + 1;
        elementType = this._parseState;
        this._parseState = tagSep === "<" ? ElementType.Tag : ElementType.Text;

        if (elementType === ElementType.Tag) {
            elementData = rawData.trim();
            elementName = parseTagName(elementData);
        } else {
            elementData = rawData;
            elementName = "";
        }

        //This section inspects the current tag stack and modifies the current
        //element if we're actually parsing a special area (script/comment/style tag)
        if (this._contentFlags === 0) {
            /*do nothing*/
        } else if (this._contentFlags >= SpecialTags[ElementType.Comment]) {
            //We're currently in a comment tag
            this._processComment(rawData, tagSep);
            continue;
        }
        //if it's a closing tag, remove the flag
        else if (
            this._contentFlags >= SpecialTags[ElementType.Script] &&
            elementName === "/script"
        ) {
            //remove the script flag (also removes the written flag)
            this._contentFlags %= SpecialTags[ElementType.Script];
        } else if (
            this._contentFlags >= SpecialTags[ElementType.Style] &&
            elementName === "/style"
        ) {
            //remove the style flag (also removes the written flag)
            this._contentFlags %= SpecialTags[ElementType.Style];
        }
        //special behaviour for script & style tags
        //Make sure we're not in a comment
        else if (!this._options.xmlMode && rawData.substring(0, 3) !== "!--") {
            //If the previous element is text, append the last tag sep to element
            if (this._contentFlags >= SpecialTags.w) {
                this._handler.writeText(this._prevTagSep + rawData);
            } else {
                //The previous element was not text
                this._contentFlags += SpecialTags.w;
                if (rawData !== "") this._handler.writeText(rawData);
            }
            this._prevTagSep = tagSep;
            continue;
        }

        //Processing of non-special tags
        if (elementType === ElementType.Tag) {
            if (rawData.substring(0, 3) === "!--") {
                //This tag is a comment
                this._contentFlags += SpecialTags[ElementType.Comment];
                this._processComment(rawData.substr(3), tagSep);
            } else if (rawData.charAt(0) === "!" || rawData.charAt(0) === "?") {
                //ElementType.Directive
                //TODO: what about CDATA?
                this._handler.writeDirective(elementName, elementData);
            } else this._processTag(elementName, elementData, tagSep, rawData);
        } else if (elementType === ElementType.Text && rawData !== "") {
            this._handler.writeText(elementData);
        }
    }

    this._buffer = buffer.substring(current);
};

Parser.prototype._processComment = function(rawData, tagSep) {
    if (tagSep === ">" && rawData.substr(-2) === "--") {
        //comment ends
        //remove the written flag (also removes the comment flag)
        this._contentFlags %= SpecialTags.w;
        rawData = rawData.slice(0, -2);
    } else rawData += tagSep;
    this._prevTagSep = tagSep;

    this._handler.writeComment(rawData);
};

Parser.prototype._processTag = function(name, data, tagSep, raw) {
    if (name.charAt(0) === "/") {
        this._handler.closeTag(name.substring(1));
        return;
    }

    var type = ElementType.Tag;
    if (this._options.xmlMode) {
        /*do nothing*/
    } else if (name === "script") type = ElementType.Script;
    else if (name === "style") type = ElementType.Style;

    this._handler.openTag(name, parseAttributes(data), type);

    //If tag self-terminates, add an explicit, separate closing tag
    if (data.substr(-1) === "/") {
        this._handler.closeTag(name);
    } else {
        this._contentFlags += SpecialTags[type];
        this._prevTagSep = tagSep;
    }
};

//Checks the handler to ensure it is an object with the right interface
var validateHandler = function(handler) {
    if (typeof handler !== "object") throw Error("Handler is not an object");
    [
        "reset",
        "done",
        "openTag",
        "closeTag",
        "writeText",
        "writeComment",
        "writeDirective"
    ].forEach(function(name) {
        if (typeof handler[name] !== "function")
            throw Error("Handler method '" + name + "' is invalid");
    });
};

Parser.prototype.handleError = function(error) {
    if (typeof this._handler.error === "function") this._handler.error(error);
    else throw error;
};

module.exports = Parser;
<|endoftext|>


<commit_before>'use strict';
var protobufjs = protobufjs || require('protobufjs/dist/ProtoBuf');
var Message = Message || require('./Message');

// var KJUR = require('jsrsasign');
var RootCerts = require('./RootCerts');

// BIP 70 - payment protocol
function PayPro() {
  this.messageType = null;
  this.message = null;
}

PayPro.PAYMENT_REQUEST_MAX_SIZE = 50000;
PayPro.PAYMENT_MAX_SIZE = 50000;
PayPro.PAYMENT_ACK_MAX_SIZE = 60000;
PayPro.PAYMENT_REQUEST_CONTENT_TYPE = "application/bitcoin-paymentrequest";
PayPro.PAYMENT_CONTENT_TYPE = "application/bitcoin-payment";
PayPro.PAYMENT_ACK_CONTENT_TYPE = "application/bitcoin-paymentack";

PayPro.proto = {};

PayPro.proto.Output = "message Output {\
  optional uint64 amount = 1 [default = 0];\
  optional bytes script = 2;\
}\n";

PayPro.proto.PaymentDetails = "message PaymentDetails {\
  optional string network = 1 [default = \"main\"];\
  repeated Output outputs = 2;\
  required uint64 time = 3;\
  optional uint64 expires = 4;\
  optional string memo = 5;\
  optional string payment_url = 6;\
  optional bytes merchant_data = 7;\
}\n";

PayPro.proto.PaymentRequest = "message PaymentRequest {\
  optional uint32 payment_details_version = 1 [default = 1];\
  optional string pki_type = 2 [default = \"none\"];\
  optional bytes pki_data = 3;\
  required bytes serialized_payment_details = 4;\
  optional bytes signature = 5;\
}\n";

PayPro.proto.Payment = "message Payment {\
  optional bytes merchant_data = 1;\
  repeated bytes transactions = 2;\
  repeated Output refund_to = 3;\
  optional string memo = 4;\
}\n";

PayPro.proto.PaymentACK = "message PaymentACK {\
  required Payment payment = 1;\
  optional string memo = 2;\
}\n";

PayPro.proto.X509Certificates = "message X509Certificates {\
  repeated bytes certificate = 1;\
}\n";

PayPro.proto.all = "";
PayPro.proto.all = PayPro.proto.all + PayPro.proto.Output;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.PaymentDetails;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.PaymentRequest;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.Payment;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.PaymentACK;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.X509Certificates;

PayPro.builder = protobufjs.loadProto(PayPro.proto.all);

PayPro.Output = PayPro.builder.build("Output");
PayPro.PaymentDetails = PayPro.builder.build("PaymentDetails");
PayPro.PaymentRequest = PayPro.builder.build("PaymentRequest");
PayPro.Payment = PayPro.builder.build("Payment");
PayPro.PaymentACK = PayPro.builder.build("PaymentACK");
PayPro.X509Certificates = PayPro.builder.build("X509Certificates");

PayPro.prototype.makeOutput = function(obj) {
  this.messageType = 'Output';
  this.message = new PayPro.Output();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePaymentDetails = function(obj) {
  this.messageType = 'PaymentDetails';
  this.message = new PayPro.PaymentDetails();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePaymentRequest = function(obj) {
  this.messageType = 'PaymentRequest';
  this.message = new PayPro.PaymentRequest();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePayment = function(obj) {
  this.messageType = 'Payment';
  this.message = new PayPro.Payment();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePaymentACK = function(obj) {
  this.messageType = 'Payment';
  this.message = new PayPro.PaymentACK();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makeX509Certificates = function(obj) {
  this.messageType = 'X509Certificates';
  this.message = new PayPro.X509Certificates();
  this.setObj(obj);
  return this;
};

PayPro.prototype.isValidSize = function() {
  var s = this.serialize();
  if (this.messageType == 'PaymentRequest')
    return s.length < PayPro.PAYMENT_REQUEST_MAX_SIZE;
  if (this.messageType == 'Payment')
    return s.length < PayPro.PAYMENT_MAX_SIZE;
  if (this.messageType == 'PaymentACK')
    return s.length < PayPro.PAYMENT_ACK_MAX_SIZE;
  return true;
};

PayPro.prototype.getContentType = function() {
  if (this.messageType == 'PaymentRequest')
    return PayPro.PAYMENT_REQUEST_CONTENT_TYPE;

  if (this.messageType == 'Payment')
    return PayPro.PAYMENT_CONTENT_TYPE;

  if (this.messageType == 'PaymentACK')
    return PayPro.PAYMENT_ACK_CONTENT_TYPE;

  throw new Error('No known content type for this message type');
};

PayPro.prototype.set = function(key, val) {
  this.message.set(key, val);
  return this;
};

PayPro.prototype.get = function(key) {
  var v = this.message.get(key);

  if (v === null)
    return v;

  //protobuf supports longs, javascript naturally does not
  //convert longs (see long.js, e.g. require('long')) to Numbers
  if (typeof v.low !== 'undefined' && typeof v.high !== 'undefined')
    return v.toInt();

  if (typeof v.toBuffer !== 'undefined') {
    var maybebuf = v.toBuffer();
    return Buffer.isBuffer(maybebuf) ? maybebuf : new Buffer(new Uint8Array(maybebuf));
  }

  return v;
};

PayPro.prototype.setObj = function(obj) {
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      var val = obj[key];
      this.message.set(key, val);
    }
  }
  return this;
};

PayPro.prototype.serializeForSig = function() {
  if (this.messageType !== 'PaymentRequest')
    throw new Error('serializeForSig is only for PaymentRequest');

  var save = this.message.get('signature');
  this.message.set('signature', new Buffer([]));
  var buf = this.serialize();
  this.message.set('signature', save);
  return buf;
};

PayPro.prototype.serialize = function() {
  //protobufjs returns either a Buffer or an ArrayBuffer
  //but we always want a Buffer (which browserify understands, browser or no)
  var maybebuf = this.message.toBuffer();
  var buf = (Buffer.isBuffer(maybebuf)) ? maybebuf : new Buffer(new Uint8Array(maybebuf));
  return buf;
};

PayPro.prototype.deserialize = function(buf, messageType) {
  this.messageType = messageType || this.messageType;
  if (!this.messageType)
    throw new Error('Must specify messageType');
  this.message = PayPro[this.messageType].decode(buf);
  return this;
};

PayPro.prototype.sign = function(key) {
  if (this.messageType !== 'PaymentRequest')
    throw new Error('Signing can only be performed on a PaymentRequest');

  var pki_type = this.get('pki_type');

  if (pki_type === 'SIN') {
    var sig = this.sinSign(key);
  } else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') {
    var sig = this.x509Sign(key);
  } else if (pki_type === 'none') {
    return this;
  } else {
    throw new Error('Unsupported pki_type');
  }

  this.set('signature', sig);

  return this;
};

PayPro.prototype.verify = function() {
  if (this.messageType !== 'PaymentRequest')
    throw new Error('Verifying can only be performed on a PaymentRequest');

  var pki_type = this.get('pki_type');

  if (pki_type === 'SIN') {
    return this.sinVerify();
  } else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') {
    return this.x509Verify();
  } else if (pki_type === 'none') {
    return true;
  }

  throw new Error('Unsupported pki_type');
};

PayPro.prototype.x509Sign = function(key) {
  var self = this;
  var crypto = require('crypto');
  var pki_type = this.get('pki_type');
  var pki_data = this.get('pki_data'); // contains one or more x509 certs
  var details = this.get('serialized_payment_details');
  var type = pki_type.split('+')[1].toUpperCase();

  var trusted = [].concat(pki_data).every(function(cert) {
    var der = cert.toString('hex');
    // var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE');
    var pem = self._DERtoPEM(der, 'CERTIFICATE');
    return !!RootCerts[pem.replace(/\s+/g, '')];
  });

  if (!trusted) {
    // throw new Error('Unstrusted certificate.');
  }

  var signature = crypto.createSign('RSA-' + type);
  var buf = this.serializeForSig();
  signature.update(buf);
  var sig = signature.sign(key);
  return sig;
};

PayPro.prototype.x509Verify = function() {
  var self = this;
  var crypto = require('crypto');
  var pki_type = this.get('pki_type');
  var sig = this.get('signature');
  var pki_data = this.get('pki_data');
  var details = this.get('serialized_payment_details');
  var buf = this.serializeForSig();
  var type = pki_type.split('+')[1].toUpperCase();

  var verifier = crypto.createVerify('RSA-' + type);
  verifier.update(buf);

  return [].concat(pki_data).every(function(cert) {
    var der = cert.toString('hex');
    // var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex(der, 'CERTIFICATE');
    var pem = self._DERtoPEM(der, 'CERTIFICATE');

    if (!RootCerts[pem.replace(/\s+/g, '')]) {
      // throw new Error('Unstrusted certificate.');
    }

    return verifier.verify(pem, sig);
  });
};

//default signing function for prototype.sign
PayPro.prototype.sinSign = function(key) {
  this.set('pki_data', key.public)
  var buf = this.serializeForSig();
  return Message.sign(buf, key);
};

//default verify function
PayPro.prototype.sinVerify = function() {
  var sig = this.get('signature');
  var pubkey = this.get('pki_data');
  var buf = this.serializeForSig();
  return Message.verifyWithPubKey(pubkey, buf, sig);
};

// Helpers

PayPro.prototype._PEMtoDER = function(pem) {
  return this._PEMtoDERParam(pem);
};

PayPro.prototype._PEMtoDERParam = function(pem, param) {
  if (Buffer.isBuffer(pem)) {
    pem = pem.toString();
  }
  var start = new RegExp('(?=-----BEGIN ' + (param || '[^-]+') + '-----)', 'i');
  var end = new RegExp('^-----END ' + (param || '[^-]+') + '-----$', 'gmi');
  pem = pem.replace(end, '');
  var parts = pem.split(start);
  return parts.map(function(part) {
    var type = /-----BEGIN ([^-]+)-----/.exec(part)[1];
    part = part.replace(/-----BEGIN ([^-]+)-----/g, '');
    part = part.replace(/\s+/g, '');
    if (!param || type !== param) return;
    return new Buffer(part, 'base64');
  }).filter(Boolean);
};

PayPro.prototype._DERtoPEM = function(der, type) {
  if (typeof der === 'string') {
    der = new Buffer(der, 'hex');
  }
  var type = type || 'UNKNOWN';
  der = der.toString('base64');
  der = der.replace(/(.{64})/g, '$1\r\n');
  der = der.replace(/\r\n$/, '');
  return ''
    + '-----BEGIN ' + type + '-----\r\n'
    + der
    + '\r\n-----END ' + type + '-----\r\n';
};

module.exports = PayPro;
<commit_msg>paypro: remove all references of KJUR.<commit_after>'use strict';
var protobufjs = protobufjs || require('protobufjs/dist/ProtoBuf');
var Message = Message || require('./Message');

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

// BIP 70 - payment protocol
function PayPro() {
  this.messageType = null;
  this.message = null;
}

PayPro.PAYMENT_REQUEST_MAX_SIZE = 50000;
PayPro.PAYMENT_MAX_SIZE = 50000;
PayPro.PAYMENT_ACK_MAX_SIZE = 60000;
PayPro.PAYMENT_REQUEST_CONTENT_TYPE = "application/bitcoin-paymentrequest";
PayPro.PAYMENT_CONTENT_TYPE = "application/bitcoin-payment";
PayPro.PAYMENT_ACK_CONTENT_TYPE = "application/bitcoin-paymentack";

PayPro.proto = {};

PayPro.proto.Output = "message Output {\
  optional uint64 amount = 1 [default = 0];\
  optional bytes script = 2;\
}\n";

PayPro.proto.PaymentDetails = "message PaymentDetails {\
  optional string network = 1 [default = \"main\"];\
  repeated Output outputs = 2;\
  required uint64 time = 3;\
  optional uint64 expires = 4;\
  optional string memo = 5;\
  optional string payment_url = 6;\
  optional bytes merchant_data = 7;\
}\n";

PayPro.proto.PaymentRequest = "message PaymentRequest {\
  optional uint32 payment_details_version = 1 [default = 1];\
  optional string pki_type = 2 [default = \"none\"];\
  optional bytes pki_data = 3;\
  required bytes serialized_payment_details = 4;\
  optional bytes signature = 5;\
}\n";

PayPro.proto.Payment = "message Payment {\
  optional bytes merchant_data = 1;\
  repeated bytes transactions = 2;\
  repeated Output refund_to = 3;\
  optional string memo = 4;\
}\n";

PayPro.proto.PaymentACK = "message PaymentACK {\
  required Payment payment = 1;\
  optional string memo = 2;\
}\n";

PayPro.proto.X509Certificates = "message X509Certificates {\
  repeated bytes certificate = 1;\
}\n";

PayPro.proto.all = "";
PayPro.proto.all = PayPro.proto.all + PayPro.proto.Output;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.PaymentDetails;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.PaymentRequest;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.Payment;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.PaymentACK;
PayPro.proto.all = PayPro.proto.all + PayPro.proto.X509Certificates;

PayPro.builder = protobufjs.loadProto(PayPro.proto.all);

PayPro.Output = PayPro.builder.build("Output");
PayPro.PaymentDetails = PayPro.builder.build("PaymentDetails");
PayPro.PaymentRequest = PayPro.builder.build("PaymentRequest");
PayPro.Payment = PayPro.builder.build("Payment");
PayPro.PaymentACK = PayPro.builder.build("PaymentACK");
PayPro.X509Certificates = PayPro.builder.build("X509Certificates");

PayPro.prototype.makeOutput = function(obj) {
  this.messageType = 'Output';
  this.message = new PayPro.Output();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePaymentDetails = function(obj) {
  this.messageType = 'PaymentDetails';
  this.message = new PayPro.PaymentDetails();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePaymentRequest = function(obj) {
  this.messageType = 'PaymentRequest';
  this.message = new PayPro.PaymentRequest();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePayment = function(obj) {
  this.messageType = 'Payment';
  this.message = new PayPro.Payment();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makePaymentACK = function(obj) {
  this.messageType = 'Payment';
  this.message = new PayPro.PaymentACK();
  this.setObj(obj);
  return this;
};

PayPro.prototype.makeX509Certificates = function(obj) {
  this.messageType = 'X509Certificates';
  this.message = new PayPro.X509Certificates();
  this.setObj(obj);
  return this;
};

PayPro.prototype.isValidSize = function() {
  var s = this.serialize();
  if (this.messageType == 'PaymentRequest')
    return s.length < PayPro.PAYMENT_REQUEST_MAX_SIZE;
  if (this.messageType == 'Payment')
    return s.length < PayPro.PAYMENT_MAX_SIZE;
  if (this.messageType == 'PaymentACK')
    return s.length < PayPro.PAYMENT_ACK_MAX_SIZE;
  return true;
};

PayPro.prototype.getContentType = function() {
  if (this.messageType == 'PaymentRequest')
    return PayPro.PAYMENT_REQUEST_CONTENT_TYPE;

  if (this.messageType == 'Payment')
    return PayPro.PAYMENT_CONTENT_TYPE;

  if (this.messageType == 'PaymentACK')
    return PayPro.PAYMENT_ACK_CONTENT_TYPE;

  throw new Error('No known content type for this message type');
};

PayPro.prototype.set = function(key, val) {
  this.message.set(key, val);
  return this;
};

PayPro.prototype.get = function(key) {
  var v = this.message.get(key);

  if (v === null)
    return v;

  //protobuf supports longs, javascript naturally does not
  //convert longs (see long.js, e.g. require('long')) to Numbers
  if (typeof v.low !== 'undefined' && typeof v.high !== 'undefined')
    return v.toInt();

  if (typeof v.toBuffer !== 'undefined') {
    var maybebuf = v.toBuffer();
    return Buffer.isBuffer(maybebuf) ? maybebuf : new Buffer(new Uint8Array(maybebuf));
  }

  return v;
};

PayPro.prototype.setObj = function(obj) {
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      var val = obj[key];
      this.message.set(key, val);
    }
  }
  return this;
};

PayPro.prototype.serializeForSig = function() {
  if (this.messageType !== 'PaymentRequest')
    throw new Error('serializeForSig is only for PaymentRequest');

  var save = this.message.get('signature');
  this.message.set('signature', new Buffer([]));
  var buf = this.serialize();
  this.message.set('signature', save);
  return buf;
};

PayPro.prototype.serialize = function() {
  //protobufjs returns either a Buffer or an ArrayBuffer
  //but we always want a Buffer (which browserify understands, browser or no)
  var maybebuf = this.message.toBuffer();
  var buf = (Buffer.isBuffer(maybebuf)) ? maybebuf : new Buffer(new Uint8Array(maybebuf));
  return buf;
};

PayPro.prototype.deserialize = function(buf, messageType) {
  this.messageType = messageType || this.messageType;
  if (!this.messageType)
    throw new Error('Must specify messageType');
  this.message = PayPro[this.messageType].decode(buf);
  return this;
};

PayPro.prototype.sign = function(key) {
  if (this.messageType !== 'PaymentRequest')
    throw new Error('Signing can only be performed on a PaymentRequest');

  var pki_type = this.get('pki_type');

  if (pki_type === 'SIN') {
    var sig = this.sinSign(key);
  } else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') {
    var sig = this.x509Sign(key);
  } else if (pki_type === 'none') {
    return this;
  } else {
    throw new Error('Unsupported pki_type');
  }

  this.set('signature', sig);

  return this;
};

PayPro.prototype.verify = function() {
  if (this.messageType !== 'PaymentRequest')
    throw new Error('Verifying can only be performed on a PaymentRequest');

  var pki_type = this.get('pki_type');

  if (pki_type === 'SIN') {
    return this.sinVerify();
  } else if (pki_type === 'x509+sha1' || pki_type === 'x509+sha256') {
    return this.x509Verify();
  } else if (pki_type === 'none') {
    return true;
  }

  throw new Error('Unsupported pki_type');
};

PayPro.prototype.x509Sign = function(key) {
  var self = this;
  var crypto = require('crypto');
  var pki_type = this.get('pki_type');
  var pki_data = this.get('pki_data'); // contains one or more x509 certs
  var details = this.get('serialized_payment_details');
  var type = pki_type.split('+')[1].toUpperCase();

  var trusted = [].concat(pki_data).every(function(cert) {
    var der = cert.toString('hex');
    var pem = self._DERtoPEM(der, 'CERTIFICATE');
    return !!RootCerts[pem.replace(/\s+/g, '')];
  });

  if (!trusted) {
    // throw new Error('Unstrusted certificate.');
  }

  var signature = crypto.createSign('RSA-' + type);
  var buf = this.serializeForSig();
  signature.update(buf);
  var sig = signature.sign(key);
  return sig;
};

PayPro.prototype.x509Verify = function() {
  var self = this;
  var crypto = require('crypto');
  var pki_type = this.get('pki_type');
  var sig = this.get('signature');
  var pki_data = this.get('pki_data');
  var details = this.get('serialized_payment_details');
  var buf = this.serializeForSig();
  var type = pki_type.split('+')[1].toUpperCase();

  var verifier = crypto.createVerify('RSA-' + type);
  verifier.update(buf);

  return [].concat(pki_data).every(function(cert) {
    var der = cert.toString('hex');
    var pem = self._DERtoPEM(der, 'CERTIFICATE');

    if (!RootCerts[pem.replace(/\s+/g, '')]) {
      // throw new Error('Unstrusted certificate.');
    }

    return verifier.verify(pem, sig);
  });
};

//default signing function for prototype.sign
PayPro.prototype.sinSign = function(key) {
  this.set('pki_data', key.public)
  var buf = this.serializeForSig();
  return Message.sign(buf, key);
};

//default verify function
PayPro.prototype.sinVerify = function() {
  var sig = this.get('signature');
  var pubkey = this.get('pki_data');
  var buf = this.serializeForSig();
  return Message.verifyWithPubKey(pubkey, buf, sig);
};

// Helpers

PayPro.prototype._PEMtoDER = function(pem) {
  return this._PEMtoDERParam(pem);
};

PayPro.prototype._PEMtoDERParam = function(pem, param) {
  if (Buffer.isBuffer(pem)) {
    pem = pem.toString();
  }
  var start = new RegExp('(?=-----BEGIN ' + (param || '[^-]+') + '-----)', 'i');
  var end = new RegExp('^-----END ' + (param || '[^-]+') + '-----$', 'gmi');
  pem = pem.replace(end, '');
  var parts = pem.split(start);
  return parts.map(function(part) {
    var type = /-----BEGIN ([^-]+)-----/.exec(part)[1];
    part = part.replace(/-----BEGIN ([^-]+)-----/g, '');
    part = part.replace(/\s+/g, '');
    if (!param || type !== param) return;
    return new Buffer(part, 'base64');
  }).filter(Boolean);
};

PayPro.prototype._DERtoPEM = function(der, type) {
  if (typeof der === 'string') {
    der = new Buffer(der, 'hex');
  }
  var type = type || 'UNKNOWN';
  der = der.toString('base64');
  der = der.replace(/(.{64})/g, '$1\r\n');
  der = der.replace(/\r\n$/, '');
  return ''
    + '-----BEGIN ' + type + '-----\r\n'
    + der
    + '\r\n-----END ' + type + '-----\r\n';
};

module.exports = PayPro;
<|endoftext|>