'use strict'; const config = require('config'); const { Client } = require('irc-framework'); const { argv } = require('yargs'); const logger = require('./utils/logger')(); const { onMessage, getGames, } = require('./play'); logger.setLevel(argv.level || 'info'); function reclaimNick(client, currentNick) { logger.info(`Connected as '${currentNick}' because '${config.user.id}' was taken, asking NickServ to release it`); client.say('NickServ', `GHOST ${config.user.id}`); // NickServ disconnects the stale session, give it a moment before taking the nick back setTimeout(() => { if (client.connected && client.user.nick !== config.user.id) { client.changeNick(config.user.id); } }, 3000); } async function init() { const client = new Client(); const bot = { client, rooms: config.channels.map((channel) => ({ id: channel, name: channel, })), }; const games = await getGames(bot, config.user.id); // client.connected is true as soon as the socket opens, not after registration as documented let registered = false; client.on('socket connected', () => { registered = false; }); // only plain messages, replying to notices is bad IRC bot etiquette and risks loops with services and other bots client.on('privmsg', (event) => onMessage({ from: event.nick, to: event.target, recipient: /^#/.test(event.target) ? null : event.target, body: event.message, type: 'message', }, bot, games)); client.on('nick in use', (event) => { // registration stalls until we pick another nick if (!registered) { logger.warn(`Nick '${event.nick}' is in use, trying '${event.nick}_'`); client.changeNick(`${event.nick}_`); return; } logger.warn(`Nick '${event.nick}' is still in use: ${event.reason}`); }); client.on('nick', (event) => event.new_nick === config.user.id && logger.info(`Reclaimed nick '${config.user.id}'`)); client.on('loggedin', (event) => logger.info(`Logged in to account '${event.account}'`)); client.on('sasl failed', (event) => logger.error(`SASL authentication failed: ${event.reason} ${event.message || ''}`)); client.on('irc error', (event) => logger.error(`IRC error: ${JSON.stringify(event)}`)); client.on('kick', (event) => event.kicked === client.user.nick && logger.warn(`Kicked from ${event.channel} by ${event.nick}: ${event.message}`)); client.on('reconnecting', (event) => logger.warn(`Disconnected, reconnect attempt ${event.attempt} in ${Math.round(event.wait / 1000)} seconds`)); // the library only auto-reconnects after a successful registration, cover failed (initial) connections too client.on('close', () => { logger.error(`Connection closed, reconnecting in ${config.reconnectDelay} seconds`); setTimeout(() => client.connect(), config.reconnectDelay * 1000); }); client.on('join', (event) => { if (event.nick !== client.user.nick) { return; } logger.info(`Joined ${event.channel}`); if (config.greeting) { client.say(event.channel, config.greeting); } }); // fires again on every reconnect client.on('registered', (event) => { registered = true; // client.user.nick is only updated after this listener runs, use the nick from the event logger.info(`Connected to ${config.server} as '${event.nick}'`); if (event.nick !== config.user.id && config.user.password) { reclaimNick(client, event.nick); } config.channels.forEach((channel) => { logger.info(`Joining ${channel}`); client.join(channel); }); }); client.connect({ host: config.server, port: config.port, tls: config.tls, nick: config.user.id, username: config.user.username, gecos: config.user.realName, // SASL logs in before registration completes, so channels requiring an identified user can be joined straight away account: config.user.password ? { account: config.user.username, password: config.user.password } : {}, auto_reconnect: true, auto_reconnect_max_retries: Infinity, auto_reconnect_max_wait: 60000, }); } module.exports = init;