UNPKG

twitch-core

Version:
470 lines (469 loc) 16.9 kB
"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;