/* global WebSocket */ import { EventEmitter } from 'events' import parse from './parser' import { format } from 'util' let translatorSocket class FakeSocket { constructor (server, socket) { this.open = true this.server = server this.socket = socket this.handlers = [] this.socket.addEventListener('message', (msg) => { let tm = msg.split(' ') if (tm[0] === 'TRANSLATOR') { if ((tm[1] === 'CLOSE' || tm[1] === 'ERROR') && tm[2] === this.server) { return this._socketClosed() } } if (tm[0] === server) { this._handle('message', tm.slice(1).join(' ')) } }) this.socket.addEventListener('close', () => { this._socketClosed() }) } send () { if (!this.open) return this.socket.send(this.server + ' ' + format.apply(Array.prototype.slice.call(arguments))) } addEventListener (type, fn) { this.handlers.push({ target: type, fn }) } _socketClosed () { this.open = false for (let a in this.handlers) { let at = this.handlers[a] if (at.target === 'close') { at.fn.apply(this, []) } } } _handle (ev) { if (!this.open) return for (let a in this.handlers) { let ap = this.handlers[a] if (ap.target === ev) { ap.fn.apply(this.socket, Array.prototype.slice.call(arguments, 1)) } } } close () { this.open = false this.socket.send('TRANSLATOR DISCONNECT ' + this.server) } static addListenerPassthrough (fs, type) { fs.socket.addEventListener(type, () => { this._handle.apply(this, [type].concat(arguments)) }) } } async function _waitOpen (s) { let o let c try { await new Promise((resolve, reject) => { o = function () { console.log('removing events', o, c) s.removeEventListener('close', c) s.removeEventListener('open', o) resolve() } s.addEventListener('open', o) c = function (err) { reject(err) } s.addEventListener('close', c) }) } catch (e) { throw e } return true } async function _trySocket (address, port, ssl, useTranslator) { // Attempt a direct ws connection let proto = 'ws' if (ssl) proto = 'wss' if (!useTranslator) { let conn = address if (port && (port < 6000 || port > 7000)) { conn = address + ':' + port } let tSock = new WebSocket(proto + '://' + conn) try { console.log('Waiting to open') await _waitOpen(tSock) } catch (e) { console.error(e) console.error('Waiting to open failed') return _trySocket(address, port, ssl, true) } tSock.open = true tSock.addEventListener('close', () => { tSock.open = false }) return tSock } if (translatorSocket) { translatorSocket.send('TRANSLATOR SERVER ' + address + ' ' + port + (ssl ? ' SSL' : '')) return new FakeSocket(address, translatorSocket) } translatorSocket = new WebSocket(`ws${window.location.protocol === 'https:' ? 's' : ''}://${window.location.host}`) console.log('Opening translator socket') await _waitOpen(translatorSocket) return new FakeSocket(address, translatorSocket) } 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 = format(resp, line.slice(1).join(' ')) } else if (line[0] === 'CLIENTINFO') { resp = 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 = 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': case '489': 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: format('%s!%s@%s', 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 () { async function wrapped (argument) { try { this.socket = await _trySocket(this.config.server, this.config.port, this.config.secure) } catch (e) { this.emit('connerror', { type: 'sock_error', message: 'A socket error occured.' }) throw e } this.socket.addEventListener('message', (e) => { let line = e.data if (line.indexOf('PING') === 0) { console.log('%s server pinged us (%s)', new Date(), line) this.write('PONG %s', line.substring(4)) return } this.emit('raw', line) let parsed = parse(line) this.emit('line', parsed) try { this.handler.handleServerLine(parsed) } catch (e) { console.error('An error occured while handling parsed server line', parsed) console.error(e.stack) } }) this.socket.addEventListener('close', (data) => { if (!this.queue['close']) { this.emit('closed', { type: 'sock_closed', raw: data, message: 'Connection closed.' }) } this.connected = false this.authenticated = false }) this.connected = true this.authenticate() } return wrapped.call(this) } authenticate () { if (this.config.password) { this.write('PASS %s', this.config.password) } if (this.extras.authenticationSteps) { for (let i in this.extras.authenticationSteps) { let step = this.extras.authenticationSteps[i] step.authenticate(this) } } this.write('USER %s 8 * :%s', this.config.username, this.config.realname) this.write('NICK %s', this.config.nickname) this.bindFinal() this.sendPing() } bindFinal () { this.on('userinput', (data) => { return this.handler.handleUserLine(data) }) } sendPing () { if (!this.connected) return this.write('PING :' + this.data.actualServer) setTimeout(() => this.sendPing(), 5000) } disconnect (message) { if (!this.connected) { this.emit('connerror', { type: 'sock_closed', message: 'Connection already closed.' }) return } this.queue['close'] = true this.write('QUIT :%s', (message != null ? message : this.globalConfig.default_quit_msg)) } write () { let message = format.apply(null, arguments) if (!this.connected) { return this.emit('connerror', { type: 'sock_closed', message: 'Connection is closed.' }) } this.socket.send(message + '\r\n') } } export { IRCConnection }