import { EventEmitter } from 'events' import net from 'net' import tls from 'tls' import util from 'util' import parse from './parser' class IRCConnectionHandler { constructor (connection) { this.conn = connection } handleUserLine (data) { switch (data.command) { case 'topic': this.conn.write('%s %s', data.command.toUpperCase(), data.arguments[0], (data.message !== '' ? ' :' + data.message : '')) break case 'kick': this.conn.write('%s %s :%s', data.command.toUpperCase(), data.arguments.join(' '), data.message) break case 'part': this.conn.write('%s %s :%s', data.command.toUpperCase(), data.arguments[0], data.message) break case 'nick': case 'whois': case 'who': case 'names': case 'join': this.conn.write('%s %s', data.command.toUpperCase(), data.arguments[0]) break case 'quit': this.conn.write('%s :%s', data.command.toUpperCase(), (data.message === '' ? this.conn.globalConfig.default_quit_msg : data.message)) break case 'privmsg': this.conn.write('PRIVMSG %s :%s', data.arguments[0], data.message) this.conn.emit('pass_to_client', { type: 'message', messageType: 'privmsg', to: data.arguments[0], user: { nickname: this.conn.config.nickname }, message: data.message, server: data.server }) break case 'notice': this.conn.write('NOTICE %s :%s', data.arguments[0], data.message) this.conn.emit('pass_to_client', { type: 'message', messageType: 'notice', to: data.arguments[0], user: { nickname: this.conn.config.nickname }, message: data.message, server: data.server }) break case 'list': this.conn.write(data.command.toUpperCase()) break case 'ctcp': let ctcpmsg = '' if (data.arguments[1].toLowerCase() === 'ping') { ctcpmsg = 'PING ' + Math.floor(Date.now() / 1000) } else { ctcpmsg = data.message } this.conn.write('PRIVMSG %s :\x01%s\x01', data.arguments[0], ctcpmsg) this.conn.emit('pass_to_client', { type: 'message', messageType: 'ctcp_request', to: this.conn.config.nickname, user: { nickname: data.arguments[0] }, message: ctcpmsg, server: data.server }) break default: this.conn.write(data.command.toUpperCase(), data.message) } if (data.targetType === 'channel' || data.targetType === 'message') { this.conn.write('PRIVMSG %s :%s', data.target, data.message) this.conn.emit('pass_to_client', { type: 'message', messageType: 'privmsg', to: data.target, user: { nickname: this.conn.config.nickname }, message: data.message, server: data.server }) } } whoisManage (whom, list) { if (!this.conn.queue.whois) { this.conn.queue.whois = {} } if (!this.conn.queue.whois[whom]) { this.conn.queue.whois[whom] = list } else { for (let a in list) { this.conn.queue.whois[whom][a] = list[a] } } } ctcpManage (data) { let line = data.trailing.replace(/\x01/g, '').trim().split(' ') /* ignore no-control-regex */ if (!line[0]) return line[0] = line[0].toUpperCase() let resp = '\x01' + line[0] + ' %s\x01' if (line[0] === 'PING' && line[1] != null && line[1] !== '') { resp = util.format(resp, line.slice(1).join(' ')) } else if (line[0] === 'CLIENTINFO') { resp = util.format(resp, 'CLIENTINFO PING ' + Object.keys(this.conn.extras.ctcps).join(' ')) } else if (this.conn.extras.ctcps && this.conn.extras.ctcps[line[0]] != null) { resp = util.format(resp, this.conn.extras.ctcps[line[0]](data, this.conn)) } else { resp = null } if (resp != null) { this.conn.write('NOTICE %s :%s', data.user.nickname, resp) } return resp != null } handleServerLine (line) { if (this.conn.queue['supportsmsg'] && line.command !== '005') { delete this.conn.queue['supportsmsg'] if (this.conn.config.autojoin.length > 0) { for (let t in this.conn.config.autojoin) { this.conn.write('JOIN', this.conn.config.autojoin[t]) } } this.conn.emit('authenticated', {}) } let serverName = this.conn.config.server let realServerName = this.conn.data.actualServer if (line.user.nickname === '') { realServerName = line.user.hostname } let list = null switch (line.command) { case 'error': this.conn.emit('connerror', { type: 'irc_error', raw: line.raw }) break case '001': this.conn.data.actualServer = line.user.hostname break case '005': if (!this.conn.queue['supportsmsg']) { this.conn.queue['supportsmsg'] = true } this.conn.authenticated = true let argv = line.arguments.slice(1) for (let a in argv) { let t = argv[a] if (t.indexOf('=') !== -1) { t = t.split('=') if (t[0] === 'PREFIX') { let d = t[1].match(/\((\w+)\)(.*)/) let r = d[1].split('') let aa = d[2].split('') for (let b in r) { this.conn.data.supportedModes[r[b]] = aa[b] } } else if (t[0] === 'NETWORK') { this.conn.data.network = t[1] } else if (t[0] === 'CHANNELLEN') { this.conn.data.maxChannelLength = parseInt(t[1]) } this.conn.data.serverSupports[t[0]] = t[1] } else { this.conn.data.serverSupports[t] = true } } break case 'JOIN': if (line.trailing) { this.conn.emit('pass_to_client', { type: 'event_join_channel', user: line.user, channel: line.trailing, server: serverName }) } else { for (let i in line.arguments) { this.conn.emit('pass_to_client', { type: 'event_join_channel', user: line.user, channel: line.arguments[i], server: serverName }) } } break case 'PART': this.conn.emit('pass_to_client', { type: 'event_part_channel', user: line.user, channel: line.arguments[0], reason: line.trailing, server: serverName }) break case 'QUIT': this.conn.emit('pass_to_client', { type: 'event_quit', user: line.user, reason: line.trailing, server: serverName }) break case '353': if (!this.conn.queue['names']) { this.conn.queue['names'] = {} } let splittrail = line.trailing.split(' ') for (let a in splittrail) { let nick = splittrail[a] if (nick.trim() === '') continue if (this.conn.queue['names'][line.arguments[2]]) { this.conn.queue['names'][line.arguments[2]].push(nick) } else { this.conn.queue['names'][line.arguments[2]] = [nick] } } break case '366': if (!this.conn.queue['names']) break if (this.conn.queue['names'][line.arguments[1]]) { this.conn.emit('pass_to_client', { type: 'channel_nicks', channel: line.arguments[1], nicks: this.conn.queue['names'][line.arguments[1]], server: serverName }) delete this.conn.queue['names'][line.arguments[1]] } if (Object.keys(this.conn.queue['names']).length === 0) { delete this.conn.queue['names'] } break case 'PRIVMSG': if (line.trailing.indexOf('\x01') === 0 && line.trailing.indexOf('\x01ACTION') !== 0) { return this.ctcpManage(line) } if (line.user.nickname !== '') { this.conn.emit('pass_to_client', { type: 'message', messageType: 'privmsg', to: line.arguments[0], user: line.user, message: line.trailing, server: serverName }) } else { this.conn.emit('pass_to_client', { type: 'server_message', messageType: 'privmsg', message: line.trailing, server: serverName, from: realServerName }) } break case 'NOTICE': if (line.trailing.indexOf('\x01') === 0 && line.trailing.indexOf('\x01ACTION') !== 0) { let composethis = line.trailing.replace(/\x01/g, '').trim().split(' ') composethis[0] = composethis[0].toUpperCase() let message = composethis.join(' ') if (composethis[0] === 'PING') { message = Math.floor(Date.now() / 1000) - composethis[1] + 's' } this.conn.emit('pass_to_client', { type: 'message', messageType: 'ctcp_response', to: line.arguments[0], user: line.user, message: message, server: serverName }) return } if (line.user.nickname !== '') { this.conn.emit('pass_to_client', { type: 'message', messageType: 'notice', to: line.arguments[0], user: line.user, message: line.trailing, server: serverName }) } else { this.conn.emit('pass_to_client', { type: 'server_message', messageType: 'notice', message: line.trailing, server: serverName, from: realServerName }) } break case 'NICK': if (line.user.nickname === this.conn.config.nickname) { this.conn.config.nickname = line.arguments[0] } this.conn.emit('pass_to_client', { type: 'nick_change', nick: line.user.nickname, newNick: line.arguments[0], server: serverName }) break case 'KICK': this.conn.emit('pass_to_client', { type: 'event_kick_channel', user: line.user, channel: line.arguments[0], reason: line.trailing, kickee: line.arguments[1], server: serverName }) break case 'TOPIC': this.conn.emit('pass_to_client', { type: 'channel_topic', channel: line.arguments[0], set_by: line.user.nickname, topic: line.trailing, server: serverName }) break case '332': this.conn.emit('pass_to_client', { type: 'channel_topic', channel: line.arguments[1], topic: line.trailing, server: serverName }) break case '333': this.conn.emit('pass_to_client', { type: 'channel_topic', channel: line.arguments[1], set_by: line.arguments[2], time: line.arguments[3], server: serverName }) break case '375': case '372': case '376': this.conn.emit('pass_to_client', { type: 'server_message', messageType: 'motd', message: line.trailing, server: serverName, from: realServerName }) break case '006': case '007': case '251': case '255': case '270': case '290': case '292': case '323': case '351': case '381': this.conn.emit('pass_to_client', { type: 'server_message', messageType: 'regular', message: line.trailing, server: serverName, from: realServerName }) break case '252': case '254': case '396': case '042': this.conn.emit('pass_to_client', { type: 'server_message', messageType: 'regular', message: line.arguments[1] + ' ' + line.trailing, server: serverName, from: realServerName }) break case '501': case '401': case '402': case '421': case '482': case '331': case '432': this.conn.emit('pass_to_client', { type: 'message', to: null, message: line.arguments[1] + ': ' + line.trailing, server: serverName, user: { nickname: realServerName }, messageType: 'error' }) break case 'MODE': let isChannelMode = false let method = '+' if (line.arguments[0].indexOf('#') !== -1) { isChannelMode = true } let modes = line.arguments[1] if (!modes && line.trailing !== '') { modes = line.trailing } let sender = line.user.nickname if (sender === '') { sender = line.user.hostname } method = modes.substring(0, 1) modes = modes.substring(1).split('') let pass = [] if (isChannelMode) { for (let i in modes) { let mode = modes[i] if (this.conn.data.supportedModes[mode]) { this.conn.emit('pass_to_client', { type: 'mode_' + (method === '+' ? 'add' : 'del'), target: line.arguments[0], mode: mode, modeTarget: line.arguments[2 + parseInt(i)], server: serverName, user: { nickname: sender } }) } else { pass.push(mode) } } } else { pass = modes } if (pass.length > 0) { this.conn.emit('pass_to_client', { type: 'mode', target: line.arguments[0], message: method + pass.join(''), server: serverName, user: { nickname: sender } }) } break case '433': let newNick = this.conn.config.nickname + '_' this.conn.write('NICK ' + newNick) this.conn.config.nickname = newNick break case '311': // start whois queue list = { nickname: line.arguments[1], hostmask: '%s!%s@%s'.format(line.arguments[1], line.arguments[2], line.arguments[3]), realname: line.trailing || '' } this.whoisManage(line.arguments[1], list) break case '319': // whois: channels list = { channels: line.trailing.split(' ') } this.whoisManage(line.arguments[1], list) break case '378': list = { connectingFrom: line.trailing } this.whoisManage(line.arguments[1], list) break case '379': list = { usingModes: line.trailing } this.whoisManage(line.arguments[1], list) break case '312': list = { server: line.arguments[2], server_name: line.trailing || '' } this.whoisManage(line.arguments[1], list) break case '313': list = { title: line.trailing } this.whoisManage(line.arguments[1], list) break case '330': list = { loggedIn: line.trailing + ' ' + line.arguments[2] } this.whoisManage(line.arguments[1], list) break case '335': list = { bot: true } this.whoisManage(line.arguments[1], list) break case '307': list = { registered: line.trailing } this.whoisManage(line.arguments[1], list) break case '671': list = { secure: true } this.whoisManage(line.arguments[1], list) break case '317': list = { signonTime: line.arguments[3], idleSeconds: line.arguments[2] } this.whoisManage(line.arguments[1], list) break case '318': if (!this.conn.queue.whois || !this.conn.queue.whois[line.arguments[1]]) return this.conn.emit('pass_to_client', { type: 'whoisResponse', whois: this.conn.queue.whois[line.arguments[1]], server: serverName, from: realServerName }) delete this.conn.queue.whois[line.arguments[1]] break case '321': this.conn.emit('pass_to_client', { type: 'listedchan', channel: 'Channel', users: 'Users', topic: 'Topic', server: serverName, from: realServerName }) break case '322': this.conn.emit('pass_to_client', { type: 'listedchan', channel: line.arguments[1], users: line.arguments[2], topic: line.trailing, server: serverName, from: realServerName }) break case 'CAP': // might come in the future, who knows this.conn.write('CAP END') break } } } class IRCConnection extends EventEmitter { constructor (providedInfo, globalConfig, extras) { super() this.globalConfig = globalConfig this.extras = extras || { authenticationSteps: [], ctcps: {} } this.config = { nickname: 'teemant', username: globalConfig.username, realname: globalConfig.realname, server: 'localhost', port: 6667, autojoin: [], secure: globalConfig.secure_by_default, password: '', address: providedInfo.server, rejectUnauthorized: globalConfig.rejectUnauthorizedCertificates } for (let a in providedInfo) { this.config[a] = providedInfo[a] } this.socket = null this.connected = false this.authenticated = false this.handler = new IRCConnectionHandler(this) this.data = { serverSupports: {}, network: this.config.server, actualServer: this.config.server, maxChannelLength: 64, supportedModes: {} } this.authorizationError = '' this.queue = {} } connect () { this.socket = (this.config.secure ? tls : net).connect({ port: this.config.port, host: this.config.server, rejectUnauthorized: this.config.rejectUnauthorized }, () => { this.connected = true this.authenticate() }) this.socket.setEncoding(this.globalConfig.encoding) this.socket.setTimeout(this.globalConfig.timeout) this.socket.on('error', (data) => { this.emit('connerror', { type: 'sock_error', message: 'A socket error occured.', raw: data }) }) this.socket.on('lookup', (err, address, family, host) => { if (err) { this.emit('connerror', { type: 'resolve_error', message: 'Failed to resolve host.' }) } else { this.emit('lookup', { address: address, family: address, host: host }) this.config.address = address } }) let buffer = '' this.socket.on('data', (chunk) => { buffer += chunk let data = buffer.split('\r\n') buffer = data.pop() data.forEach((line) => { if (line.indexOf('PING') === 0) { this.socket.write('PONG %s\r\n', line.substring(4)) return } this.emit('raw', line) let parsed = parse(line) this.emit('line', parsed) this.handler.handleServerLine(parsed) }) }) this.socket.on('close', (data) => { if (!this.queue['close']) { this.emit('closed', { type: 'sock_closed', raw: data, message: 'Connection closed.' }) } this.connected = false this.authenticated = false }) } authenticate () { if (this.config.password) { this.socket.write('PASS %s\r\n', this.config.password) } if (this.extras.authenticationSteps) { for (let i in this.extras.authenticationSteps) { let step = this.extras.authenticationSteps[i] step.authenticate(this) } } this.socket.write('USER %s 8 * :%s\r\n', this.config.username, this.config.realname) this.socket.write('NICK %s\r\n', this.config.nickname) } disconnect (message) { if (!this.connected) { this.emit('connerror', { type: 'sock_closed', message: 'Connection already closed.' }) return } this.queue['close'] = true this.socket.write('QUIT :%s\r\n', (message != null ? message : this.globalConfig.default_quit_msg)) } write () { let message = util.format.apply(null, arguments) if (!this.connected) { return this.emit('connerror', { type: 'sock_closed', message: 'Connection is closed.' }) } this.socket.write(message + '\r\n') } } module.exports = IRCConnection