FoseFx/twitch-chatbot-boilerplate-core

View on GitHub
src/core/bot/bot.ts

Summary

Maintainability
A
0 mins
Test Coverage
B
86%
import * as fs from 'fs';
import * as merge from 'deepmerge';
import { Client, Options as TmiOptions } from 'tmi.js';
import { AuthData, StartServerOptions } from '../server/server.types';
import { refreshAccessToken } from '../server/auth';
import { getClientReadyEmitter } from '../event';
import { ensureDirExists } from '../util';

/** @internal */
let _client: Client | null = null;
/** @internal */
let _channels: string[] = [];

/** @internal **/
export async function startBot(
  options: StartServerOptions,
  tmiOptions: TmiOptions,
  authData: AuthData | null,
): Promise<void> {
  if (_client !== null || authData === null) {
    return;
  }
  _client = await _this._createNewClient(options, tmiOptions, authData);
  _channels = _this._readChannelsFromDisk();

  for (const channel of _channels) {
    _client.join(channel);
  }
  getClientReadyEmitter().emit('clientReady', _client);
}

/**
 * @public
 * @return {Promise<string>} - the channel the bot has joined
 * @example
 * ```
 * import { joinChannel } from 'twitch-chatbot-boilerplate';
 * await joinChannel("fosefx");
 * ```
 */
export async function joinChannel(channel: string): Promise<string> {
  if (_channels.includes(channel)) {
    throw new Error('Bot already joined this chat');
  }

  return _client.join(channel).then(() => {
    _channels.push(channel);
    _this._storeChannelsOnDisk();
    return channel;
  });
}

/**
 * @public
 * @return {Promise<string>} - the channel the bot has left
 * @example
 * ```
 * import { leaveChannel } from 'twitch-chatbot-boilerplate';
 * await leaveChannel("fosefx");
 * ```
 */
export async function leaveChannel(channel: string): Promise<string> {
  return _client.part(channel).then(() => {
    _channels = _channels.filter((c) => c !== channel);
    _this._storeChannelsOnDisk();
    return channel;
  });
}

/** @internal */
export async function _createNewClient(
  options: StartServerOptions,
  tmiOptions: TmiOptions,
  authData: AuthData,
): Promise<Client> {
  const defaultOptions: TmiOptions = {
    options: {
      debug: true,
    },
    connection: {
      secure: true,
      reconnect: true,
    },
    identity: {
      username: options.clientId,
      password: authData.access_token,
    },
  };

  const opts = !tmiOptions
    ? defaultOptions
    : merge<TmiOptions>(defaultOptions, tmiOptions);

  const client = Client(opts);

  // Note: _handleConnectError causes recursion to this function
  return client
    .connect()
    .then(() => client)
    .catch((e) => _this._handleConnectError(options, authData, e));
}

/** @internal */
export function isBotRunning(): boolean {
  return !!_client;
}

/** @internal */
export async function _handleConnectError(
  opts: StartServerOptions,
  authData: AuthData,
  error: string,
): Promise<Client> {
  if (error === 'Login authentication failed') {
    return _this._handleAuthError(opts, authData);
  } else {
    throw new Error(error);
  }
}

/** @internal */
export function _handleAuthError(
  opts: StartServerOptions,
  authData: AuthData,
): Promise<Client> {
  return refreshAccessToken(opts, authData, true).then((newData) =>
    _this._createNewClient(opts, opts.tmiOptions, newData),
  );
}

/** @internal */
export function _storeChannelsOnDisk(): void {
  const dir = './.config';
  ensureDirExists(dir);
  fs.writeFileSync(dir + '/channels.json', JSON.stringify(_channels));
}

/** @internal */
export function _readChannelsFromDisk(): string[] {
  const dir = './.config';
  ensureDirExists(dir);
  try {
    const str = fs.readFileSync(dir + '/channels.json', 'utf-8');
    return JSON.parse(str);
  } catch {
    return [];
  }
}

/** @internal */
export function _setClient(cl: Client): void {
  _client = cl;
}

/** @internal */
export function _setChannels(ch: string[]): void {
  _channels = ch;
}

/** @internal */
export const _this = {
  startBot,
  joinChannel,
  leaveChannel,
  _createNewClient,
  _handleConnectError,
  _handleAuthError,
  _storeChannelsOnDisk,
  _readChannelsFromDisk,
  _setClient,
  _setChannels,
};