/* global WebSocket */ /* eslint-disable no-control-regex */ 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 waitForSocketOpen (s) { let o let c try { await new Promise((resolve, reject) => { o = function () { 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 createSocket (address, port, ssl, useTranslator) { // Attempt a direct ws connection let proto = 'ws' if (ssl) proto = 'wss' if (!useTranslator) { let conn = address if (port) conn = address + ':' + port let tSock = new WebSocket(proto + '://' + conn) try { await waitForSocketOpen(tSock) } catch (e) { console.error(e) return createSocket(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 waitForSocketOpen(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.defaultParams.default_quit_msg : data.message)) break case 'privmsg': this.conn.write('PRIVMSG %s :%s', data.arguments[0], data.message) this.conn.emit('fromServer', { 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('fromServer', { 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('fromServer', { type: 'message', messageType: 'ctcpRequest', 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('fromServer', { 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('connectionError', new Error('IRCError ' + line.message)) break case 'PONG': this.conn.ping = Date.now() - this.conn.pingSent this.conn.pingSent = 0 this.conn.emit('ping', this.conn.ping) 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('fromServer', { type: 'joinChannel', user: line.user, channel: line.trailing, server: serverName }) } else { for (let i in line.arguments) { this.conn.emit('fromServer', { type: 'joinChannel', user: line.user, channel: line.arguments[i], server: serverName }) } } break case 'PART': this.conn.emit('fromServer', { type: 'partChannel', user: line.user, channel: line.arguments[0], reason: line.trailing, server: serverName }) break case 'QUIT': this.conn.emit('fromServer', { type: '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('fromServer', { type: '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('fromServer', { type: 'message', messageType: 'privmsg', to: line.arguments[0], user: line.user, message: line.trailing, server: serverName }) } else { this.conn.emit('fromServer', { type: 'serverMessage', 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('fromServer', { type: 'message', messageType: 'ctcpResponse', to: line.arguments[0], user: line.user, message: message, server: serverName }) return } if (line.user.nickname !== '') { this.conn.emit('fromServer', { type: 'message', messageType: 'notice', to: line.arguments[0], user: line.user, message: line.trailing, server: serverName }) } else { this.conn.emit('fromServer', { type: 'serverMessage', messageType: 'notice', message: line.trailing, server: serverName, from: realServerName }) } break case 'NICK': let newNickA = line.arguments[0] || line.trailing if (line.user.nickname === this.conn.config.nickname) { this.conn.config.nickname = newNickA } this.conn.emit('fromServer', { type: 'nick', nick: line.user.nickname, newNick: newNickA, server: serverName }) break case 'KICK': this.conn.emit('fromServer', { type: 'kickedFromChannel', user: line.user, channel: line.arguments[0], reason: line.trailing, kickee: line.arguments[1], server: serverName }) break case 'TOPIC': this.conn.emit('fromServer', { type: 'topic', channel: line.arguments[0], setBy: line.user.nickname, topic: line.trailing, server: serverName }) break case '332': this.conn.emit('fromServer', { type: 'topic', channel: line.arguments[1], topic: line.trailing, server: serverName }) break case '333': this.conn.emit('fromServer', { type: 'topic', channel: line.arguments[1], setBy: line.arguments[2], time: (line.arguments[3] || line.trailing), server: serverName }) break case '375': case '372': case '376': this.conn.emit('fromServer', { type: 'serverMessage', 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('fromServer', { type: 'serverMessage', messageType: 'regular', message: line.trailing, server: serverName, from: realServerName }) break case '252': case '254': case '396': case '042': this.conn.emit('fromServer', { type: 'serverMessage', 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('fromServer', { 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 = '+' let lts = line.trailing ? line.trailing.split(' ') : [] 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] let modei = parseInt(i) if (this.conn.data.supportedModes[mode]) { this.conn.emit('fromServer', { type: 'mode' + (method === '+' ? 'Add' : 'Del'), target: line.arguments[0], mode: mode, modeTarget: line.arguments[2] ? (line.arguments[2 + modei] ? line.arguments[2 + modei] : lts[modei - 1]) : lts[modei], server: serverName, user: { nickname: sender } }) } else { pass.push(mode) } } } else { pass = modes } if (pass.length > 0) { this.conn.emit('fromServer', { 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 '352': // who query response // [*][@|+] : if (!this.conn.queue.who) { this.conn.queue.who = [] } this.conn.queue.who.push({ channel: line.arguments[1], server: line.arguments[4], modes: line.arguments[6], hopcount: line.trailing.split(' ')[0], hostmask: format('%s@%s', line.arguments[2], line.arguments[3]), nickname: line.arguments[5], realname: line.trailing.split(' ').slice(1).join(' ') }) break case '315': this.conn.emit('fromServer', { type: 'whoResponse', who: this.conn.queue.who, target: line.arguments[1], message: line.trailing, server: serverName, from: realServerName }) delete this.conn.queue.who break case '311': // start whois queue list = { nickname: line.arguments[1], hostmask: format('%s@%s', 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], serverName: 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('fromServer', { 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('fromServer', { type: 'listedChannel', channel: 'Channel', users: 'Users', topic: 'Topic', server: serverName, from: realServerName }) break case '322': this.conn.emit('fromServer', { type: 'listedChannel', 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 default: let argc = line.arguments if (argc.indexOf(this.conn.config.nickname) === 0) argc = argc.slice(1) this.conn.emit('fromServer', { type: 'serverMessage', messageType: 'unknown', message: (argc.length ? argc.join(' ') + ' :' : '') + line.trailing, server: serverName, from: realServerName }) } } } class IRCConnection extends EventEmitter { constructor (providedInfo, defaultParams, extras) { super() this.defaultParams = defaultParams this.extras = extras || { authenticationSteps: [], ctcps: {} } this.config = { nickname: 'teemant', username: defaultParams.username, realname: defaultParams.realname, server: 'localhost', port: 6667, autojoin: [], secure: defaultParams.secure_by_default, password: '', address: providedInfo.server, rejectUnauthorized: defaultParams.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.queue = {} this.pingSent = 0 this.ping = 0 } connect () { async function wrapped (argument) { this.socket = await createSocket(this.config.server, this.config.port, this.config.secure) this.socket.addEventListener('message', (e) => { let line = e.data // Handle from-server pings if (line.indexOf('PING') === 0) { 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('connectionClosed', new Error('Socket has been 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() } bindFinal () { this.on('userInput', (data) => { return this.handler.handleUserLine(data) }) this.once('authenticated', () => this.sendPing()) } sendPing () { if (!this.connected) return this.write('PING :' + this.data.actualServer) this.pingSent = Date.now() setTimeout(() => this.sendPing(), 5000) } disconnect (message) { if (!this.connected) { this.emit('connectionError', new Error('SocketError: Socket is already closed.')) return } this.queue['close'] = true this.write('QUIT :%s', (message != null ? message : this.defaultParams.default_quit_msg)) } write () { let message = format.apply(null, arguments) if (!this.connected) { return this.emit('connectionError', new Error('SocketError: Socket is closed.')) } this.socket.send(message + '\r\n') } } export { IRCConnection }