twitch-core
Version:
Twitch bot command client
470 lines (469 loc) • 16.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TwitchCommandClient = void 0;
const path_1 = __importDefault(require("path"));
const events_1 = __importDefault(require("events"));
const tmi_js_1 = __importDefault(require("tmi.js"));
const recursive_readdir_sync_1 = __importDefault(require("recursive-readdir-sync"));
const Server_1 = require("../server/Server");
const ClientLogger_1 = require("./ClientLogger");
const EmotesManager_1 = require("../emotes/EmotesManager");
const CommandConstants_1 = require("./CommandConstants");
const CommandParser_1 = require("../commands/CommandParser");
const TwitchChatChannel_1 = require("../channels/TwitchChatChannel");
const TwitchChatMessage_1 = require("../messages/TwitchChatMessage");
const SettingsProvider_1 = require("../settings/SettingsProvider");
const TextCommand_1 = require("../commands/TextCommand");
class TwitchCommandClient extends events_1.default {
constructor(options) {
super();
const defaultOptions = {
prefix: '!',
serverPort: 8080,
greetOnJoin: false,
enableServer: false,
verboseLogging: false,
autoJoinBotChannel: false,
enableRateLimitingControl: true,
botType: CommandConstants_1.CommandConstants.BOT_TYPE_NORMAL
};
this.options = Object.assign(defaultOptions, options);
this.checkOptions();
this.provider = new SettingsProvider_1.SettingsProvider();
this.logger = new ClientLogger_1.ClientLogger().getLogger('main');
this.commands = [];
this.channelsWithMod = [];
this.messagesCount = 0;
}
checkOptions() {
if (this.options.prefix === '/') {
throw new Error('Invalid prefix. Cannot be /');
}
if (this.options.channels === undefined) {
throw new Error('Channels not specified');
}
if (this.options.username === undefined) {
throw new Error('Username not specified');
}
if (this.options.oauth === undefined) {
throw new Error('Oauth password not specified');
}
this.options.username = this.options.username.toLowerCase();
this.options.channels = this.toLowerArray(this.options.channels);
if (this.options.botOwners === undefined) {
this.options.botOwners = [this.options.username];
}
else {
this.options.botOwners = this.toLowerArray(this.options.botOwners);
}
}
/**
* Connect the bot to Twitch Chat
*/
connect() {
return __awaiter(this, void 0, void 0, function* () {
if (this.options.enableServer) {
this.server = new Server_1.Server(this, this.options.serverPort);
this.server.start();
}
this.parser = new CommandParser_1.CommandParser(this);
this.emotesManager = new EmotesManager_1.EmotesManager(this);
yield this.emotesManager.getGlobalEmotes();
this.logger.info('Current default prefix is ' + this.options.prefix);
this.logger.info('Connecting to Twitch Chat');
const channels = [...this.options.channels];
if (this.options.autoJoinBotChannel) {
channels.push(this.options.username);
}
this.logger.info('Autojoining ' + channels.length + ' channels');
this.tmi = new tmi_js_1.default.client({
options: {
debug: this.options.verboseLogging
},
connection: {
secure: true,
reconnect: true
},
identity: {
username: this.options.username,
password: 'oauth:' + this.options.oauth
},
channels: channels,
logger: this.logger
});
this.tmi.on('connected', this.onConnect.bind(this));
this.tmi.on('disconnected', this.onDisconnect.bind(this));
this.tmi.on('join', this.onJoin.bind(this));
this.tmi.on('reconnect', this.onReconnect.bind(this));
this.tmi.on('timeout', this.onTimeout.bind(this));
this.tmi.on('mod', this.onMod.bind(this));
this.tmi.on('unmod', this.onUnmod.bind(this));
this.tmi.on('message', this.onMessage.bind(this));
yield this.tmi.connect();
});
}
/**
* Send a text message in the channel
*
* @param channel
* @param message
* @param addRandomEmote
*/
say(channel, message, addRandomEmote = false) {
return __awaiter(this, void 0, void 0, function* () {
if (this.checkRateLimit()) {
if (addRandomEmote) {
message += ' ' + this.emotesManager.getRandomEmote().code;
}
const serverResponse = yield this.tmi.say(channel, message);
if (this.messagesCount === 0) {
this.startMessagesCounterInterval();
}
this.messagesCount = this.messagesCount + 1;
return serverResponse;
}
else {
this.logger.warn('Rate limit excedeed. Wait for timer reset.');
}
});
}
/**
* Send an action message in the channel
*
* @param channel
* @param message
* @param addRandomEmote
*/
action(channel, message, addRandomEmote = false) {
return __awaiter(this, void 0, void 0, function* () {
if (this.checkRateLimit()) {
if (addRandomEmote) {
message += ' ' + this.emotesManager.getRandomEmote().code;
}
const serverResponse = yield this.tmi.action(channel, message);
if (this.messagesCount === 0) {
this.startMessagesCounterInterval();
}
this.messagesCount = this.messagesCount + 1;
return serverResponse;
}
else {
this.logger.warn('Rate limit excedeed. Wait for timer reset.');
}
});
}
/**
* Send a private message to the user with given text
*
* @param username
* @param message
*/
whisper(username, message) {
return __awaiter(this, void 0, void 0, function* () {
const serverResponse = yield this.tmi.whisper(username, message);
return serverResponse;
});
}
/**
* Register commands in given path (recursive)
*
* @param path
* @param options
*/
registerCommandsIn(path) {
const files = recursive_readdir_sync_1.default(path);
const commandProvider = this.provider
.get('commands');
files.forEach((file) => {
if (!file.match('.*(?<!\.d\.ts)$'))
return;
let commandFile = require(file);
if (typeof commandFile.default === 'function') {
commandFile = commandFile.default;
}
if (typeof commandFile === 'function') {
let options;
const commandName = commandFile.name;
if (commandProvider) {
options = commandProvider
.get(commandName)
.value();
}
this.commands.push(new commandFile(this, options));
this.logger.info(`Register command ${commandName} ${options ? '(external option)' : '(internal option)'}`);
}
else {
this.logger.warn('You are not export default class correctly!');
}
}, this);
}
/**
* Register text commands
*/
registerTextCommands() {
const provider = this.provider
.get('text-commands');
if (provider) {
provider
.get('commands')
.value()
.forEach(options => {
if (!options.messageType) {
options = Object.assign({ messageType: 'reply' }, options);
}
this.commands.push(new TextCommand_1.TextCommand(this, options));
this.logger.info(`Register text command ${options.name}`);
});
}
else {
this.logger.warn('Text command provider text-commands.json is not registered!');
}
}
/**
* Register default commands
*/
registerDefaultCommands() {
this.registerCommandsIn(path_1.default.join(__dirname, '../commands/default'));
}
findCommand(parserResult) {
return this.commands.find(command => {
var _a;
if ((_a = command.options.aliases) === null || _a === void 0 ? void 0 : _a.includes(parserResult.command)) {
return command;
}
if (parserResult.command === command.options.name) {
return command;
}
});
}
/**
* Execute command method
*
* @param command
* @param msg
*/
executeCommandMethod(command, msg) {
var _a;
(_a = this.findCommand({ command })) === null || _a === void 0 ? void 0 : _a.execute(msg);
}
/**
* Bot connected
*/
onConnect() {
this.emit('connected');
}
/**
* Channel joined or someone join the channel
*
* @param channel
* @param username
*/
onJoin(channel, username) {
var _a;
const channelObject = new TwitchChatChannel_1.TwitchChatChannel({ channel }, this);
if (this.options.greetOnJoin &&
this.getUsername() === username &&
((_a = this.options) === null || _a === void 0 ? void 0 : _a.onJoinMessage) !== '') {
this.action(channel, this.options.onJoinMessage);
}
this.emit('join', channelObject, username);
}
/**
* Bot disconnects
*/
onDisconnect() {
this.emit('disconnected');
}
/**
* Command executed
*
* @param channel
* @param userstate
* @param messageText
* @param self
*/
onMessage(channel, userstate, messageText, self) {
return __awaiter(this, void 0, void 0, function* () {
if (self)
return;
const chatter = Object.assign(Object.assign({}, userstate), { message: messageText });
const msg = new TwitchChatMessage_1.TwitchChatMessage(chatter, channel, this);
if (msg.author.username === this.getUsername()) {
if (!(msg.author.isBroadcaster ||
msg.author.isModerator ||
msg.author.isVip)) {
yield new Promise((resolve) => setTimeout(resolve, 1000));
}
}
this.emit('message', msg);
const parserResult = this.parser.parse(messageText, this.options.prefix);
if (parserResult) {
const command = this.findCommand(parserResult);
if (command) {
const preValidateResponse = command.preValidate(msg);
if (typeof preValidateResponse !== 'string') {
command
.prepareRun(msg, parserResult.args)
.then(commandResult => {
this.emit('commandExecuted', commandResult);
})
.catch((err) => {
msg.reply('Unexpected error: ' + err);
this.emit('commandError', err);
});
}
else {
msg.reply(preValidateResponse, false);
}
}
}
});
}
/**
* Connection timeout
*
* @param channel
* @param username
* @param reason
* @param duration
*/
onTimeout(channel, username, reason, duration) {
this.emit('timeout', channel, username, reason, duration);
}
/**
* Reconnection
*/
onReconnect() {
this.emit('reconnect');
}
/**
* Request the bot to join a channel
*
* @param channel
*/
join(channel) {
return __awaiter(this, void 0, void 0, function* () {
return this.tmi.join(channel);
});
}
/**
* Request the bot to leave a channel
*
* @param channel
*/
part(channel) {
return __awaiter(this, void 0, void 0, function* () {
return this.tmi.part(channel);
});
}
/**
* Gets the bot username
*/
getUsername() {
return this.tmi.getUsername();
}
/**
* Gets the bot channels
*/
getChannels() {
return this.tmi.getChannels();
}
/**
* Checks if the message author is one of bot owners
*
* @param author
*/
isOwner(author) {
return this.options.botOwners.includes(author.username);
}
/**
* Received mod role
*
* @param channel
* @param username
*/
onMod(channel, username) {
if (username === this.getUsername() &&
!this.channelsWithMod.includes(channel)) {
this.logger.debug('Bot has received mod role');
this.channelsWithMod.push(channel);
}
this.emit('mod', channel, username);
}
/**
* Emit error
*
* @param error
*/
onError(error) {
this.logger.error(error);
this.emit('error', error);
}
/**
* Unmod bot
*
* @param channel
* @param username
*/
onUnmod(channel, username) {
if (username === this.getUsername()) {
this.logger.debug('Bot has received unmod');
this.channelsWithMod = this.channelsWithMod.filter(v => {
return v !== channel;
});
}
this.emit('onumod', channel, username);
}
/**
* Start messages counting
*/
startMessagesCounterInterval() {
if (this.options.enableRateLimitingControl) {
if (this.options.verboseLogging) {
this.logger.verbose('Starting messages counter interval');
}
const messageLimits = CommandConstants_1.CommandConstants.MESSAGE_LIMITS[this.options.botType];
this.messagesCounterInterval = setInterval(this.resetMessageCounter.bind(this), messageLimits.timespan * 1000);
}
}
/**
* Reset message counter
*/
resetMessageCounter() {
if (this.options.verboseLogging) {
this.logger.verbose('Resetting messages count');
}
this.messagesCount = 0;
}
/**
* Check if the bot sent too many messages in timespan limit
*/
checkRateLimit() {
if (this.options.enableRateLimitingControl) {
const messageLimits = CommandConstants_1.CommandConstants.MESSAGE_LIMITS[this.options.botType];
if (this.options.verboseLogging) {
this.logger.verbose('Messages count: ' + this.messagesCount);
}
return this.messagesCount < messageLimits.messages;
}
else {
return true;
}
}
toLowerArray(arr) {
return arr.map(v => v.toLowerCase());
}
}
exports.TwitchCommandClient = TwitchCommandClient;