/** IRC connection + registration + dispatch (modern client protocol). */ var BARE_IRC_WANT_CAPS = [ 'cap-notify', 'multi-prefix', 'extended-join', 'account-tag', 'account-notify', 'away-notify', 'chghost', 'server-time', 'message-tags', 'echo-message', 'sasl' ] function bareIrcNow() { return Date.now() } function bareIrcCreateClient(opts) { opts = opts || {} var listeners = {} var sock = null var lines = bareIrcCreateLineBuffer() var sendTimes = [] var nickTries = 0 var capLsDone = false var awaitingCapAck = false var saslPending = false var client = { opts: opts, state: bareIrcCreateState(opts), on: function (ev, fn) { if (!listeners[ev]) listeners[ev] = [] listeners[ev].push(fn) return client }, off: function (ev, fn) { var arr = listeners[ev] if (!arr) return listeners[ev] = arr.filter(function (x) { return x !== fn }) } } function emit(ev, payload) { var arr = listeners[ev] || [] for (var i = 0; i < arr.length; i++) { try { arr[i](payload) } catch (e) { /* ignore */ } } if (typeof opts.onEvent === 'function') { try { opts.onEvent(ev, payload) } catch (e2) { /* ignore */ } } } function writeRaw(s) { if (!sock || typeof sock.write !== 'function') return false try { sock.write(s) return true } catch (e) { return false } } client.sendRaw = function (command, params) { var msg = typeof command === 'string' && params ? { command: command, params: params } : typeof command === 'object' ? command : { command: String(command || ''), params: [] } var line = bareIrcSerializeMessage(msg) return writeRaw(line) } client.rateOk = function () { var now = bareIrcNow() sendTimes = sendTimes.filter(function (t) { return now - t < 1000 }) if (sendTimes.length >= 5) return false sendTimes.push(now) return true } client.sendPrivmsg = function (target, text) { if (!client.rateOk()) return false var ok = client.sendRaw('PRIVMSG', [target, text]) if (ok && !client.state.capsEnabled['echo-message']) { client._ingestLocalEcho(target, text) } return ok } client.join = function (chan, key) { return client.sendRaw('JOIN', key ? [chan, key] : [chan]) } client.list = function (pattern) { client.state.channelList = [] client.state.listBusy = true var pat = String(pattern || '').trim() if (pat && pat.indexOf('*') < 0 && pat.charAt(0) !== '#') pat = '*' + pat + '*' return client.sendRaw('LIST', pat ? [pat] : []) } client.part = function (chan, reason) { return client.sendRaw('PART', reason ? [chan, reason] : [chan]) } client.quit = function (reason) { client.state.connection = 'closing' client.sendRaw('QUIT', [reason || 'Leaving']) try { if (sock && typeof sock.end === 'function') sock.end() } catch (e) { /* ignore */ } } client._ingestLocalEcho = function (target, text) { var mapping = bareIrcCasemapping(client.state.isupport) var kind = bareIrcIsChannel(target, bareIrcChantypes(client.state.isupport)) ? 'channel' : 'query' var buf = bareIrcEnsureBuffer(client.state, kind, target) bareIrcPushLine(client.state, buf, { atMs: bareIrcNow(), kind: 'privmsg', from: client.state.nick, text: text, local: true }) emit('privmsg', { target: target, nick: client.state.nick, text: text, local: true }) } function requestCaps() { var offered = client.state.capsOffered var want = [] for (var i = 0; i < BARE_IRC_WANT_CAPS.length; i++) { var c = BARE_IRC_WANT_CAPS[i] if (offered[c] != null) want.push(c) } if (!want.length) { finishCap() return } awaitingCapAck = true client.sendRaw('CAP', ['REQ', want.join(' ')]) } function finishCap() { awaitingCapAck = false if ( client.state.capsEnabled.sasl && opts.saslUser && opts.saslPass != null && String(opts.saslPass) !== '' ) { saslPending = true client.sendRaw('AUTHENTICATE', ['PLAIN']) return } client.sendRaw('CAP', ['END']) } function applyCapList(text, ack) { var bits = String(text || '').split(/\s+/) for (var i = 0; i < bits.length; i++) { if (!bits[i]) continue var name = bits[i] var val = true var eq = name.indexOf('=') if (eq >= 0) { val = name.slice(eq + 1) name = name.slice(0, eq) } client.state.capsOffered[name] = val if (ack) client.state.capsEnabled[name] = val } } function handleNumeric(msg) { var n = parseInt(msg.command, 10) var p = msg.params || [] if (n === 1) { if (p[0]) client.state.nick = p[0] client.state.registered = true client.state.connection = 'registered' emit('registered', { nick: client.state.nick }) } else if (n === 5) { client.state.isupport = bareIrcMergeIsupport( client.state.isupport, bareIrcParseIsupport(p) ) if (client.state.isupport.NETWORK) { client.state.network = String(client.state.isupport.NETWORK) } } else if (n === 332) { var ch = p[1] || p[0] var buf = bareIrcEnsureBuffer(client.state, 'channel', ch) buf.topic = p[p.length - 1] || '' } else if (n === 353) { var chan = p.length >= 3 ? p[p.length - 2] : p[1] var names = String(p[p.length - 1] || '').split(/\s+/) var b = bareIrcEnsureBuffer(client.state, 'channel', chan) for (var i = 0; i < names.length; i++) { if (!names[i]) continue var mem = bareIrcMemberPrefixes(client.state, names[i]) b.members[ bareIrcCasemap(mem.nick, bareIrcCasemapping(client.state.isupport)) ] = mem } } else if (n === 366) { emit('names', { channel: p[1] || p[0] }) } else if (n === 321) { client.state.channelList = [] client.state.listBusy = true emit('liststart', {}) } else if (n === 322) { var types = bareIrcChantypes(client.state.isupport) var lchan = p[1] || '' var lcount = parseInt(p[2], 10) var ltopic = p[p.length - 1] || '' if (!bareIrcIsChannel(lchan, types) && bareIrcIsChannel(p[0], types)) { lchan = p[0] lcount = parseInt(p[1], 10) } if (bareIrcIsChannel(lchan, types)) { if (!client.state.channelList) client.state.channelList = [] if (client.state.channelList.length < 400) { client.state.channelList.push({ name: lchan, users: Number.isFinite(lcount) ? lcount : 0, topic: ltopic }) } } emit('list', { channel: lchan, users: lcount, topic: ltopic }) } else if (n === 323) { client.state.listBusy = false if (client.state.channelList) { client.state.channelList.sort(function (a, b) { return (b.users || 0) - (a.users || 0) }) } emit('listend', { count: (client.state.channelList || []).length }) } else if (n === 376 || n === 422) { emit('motd_end', {}) } else if (n === 433) { nickTries++ if (nickTries <= 8) { var base = String( opts.nick || client.state.requestedNick || 'os' ).replace(/_+$/, '') var next = (base.slice(0, 10) + '_' + bareIrcRandomSuffix(3)).slice( 0, 16 ) client.state.requestedNick = next client.sendRaw('NICK', [next]) } else emit('nickinuse', { nick: p[1] || p[0] }) } else if (n === 470) { var fromCh = p[1] || '' var toCh = p[2] || '' if (toCh) { var fb = bareIrcEnsureBuffer(client.state, 'channel', toCh) client.state.currentId = fb.id bareIrcPushLine(client.state, fb, { atMs: bareIrcNow(), kind: 'info', text: 'forwarded ' + fromCh + ' → ' + toCh }) } emit('forward', { from: fromCh, to: toCh }) } else if (n === 900 || n === 903) { client.state.saslOk = true if (p.length >= 3) client.state.account = String(p[2] || p[0] || '') if (saslPending) { saslPending = false client.sendRaw('CAP', ['END']) } emit('sasl', { ok: true }) } else if (n === 904 || n === 905 || n === 906 || n === 907) { client.state.saslOk = false if (saslPending) { saslPending = false client.sendRaw('CAP', ['END']) } emit('sasl', { ok: false, numeric: n }) } if (n !== 321 && n !== 322 && n !== 323 && n !== 353 && n !== 366) { var srv = bareIrcEnsureBuffer(client.state, 'server', '*server') bareIrcPushLine(client.state, srv, { atMs: bareIrcNow(), kind: 'numeric', command: msg.command, text: (p.slice(1).join(' ') || p.join(' ')).trim() }) } } function handlePrivmsg(msg, notice) { var target = msg.params[0] || '' var text = msg.params[1] || '' var from = msg.nick || msg.source || '' var mapping = bareIrcCasemapping(client.state.isupport) if ( from && client.state.ignore && client.state.ignore[bareIrcCasemap(from, mapping)] ) { return } var types = bareIrcChantypes(client.state.isupport) var kind var name if (bareIrcIsChannel(target, types)) { kind = 'channel' name = target } else { kind = 'query' name = bareIrcEqual(target, client.state.nick, mapping) ? from : target } var buf = bareIrcEnsureBuffer(client.state, kind, name) var ctcp = /^\x01([A-Z]+)(?: ([\s\S]*))?\x01$/.exec(text) var rec = { atMs: bareIrcNow(), kind: notice ? 'notice' : ctcp && ctcp[1] === 'ACTION' ? 'action' : 'privmsg', from: from, text: ctcp && ctcp[1] === 'ACTION' ? ctcp[2] || '' : text, highlight: false, tags: msg.tags } var self = client.state.nick if ( self && !notice && rec.kind === 'privmsg' && text.toLowerCase().indexOf(String(self).toLowerCase()) >= 0 ) { rec.highlight = true } bareIrcPushLine(client.state, buf, rec) emit(notice ? 'notice' : 'privmsg', { target: target, nick: from, text: rec.text, action: rec.kind === 'action', tags: msg.tags }) if (ctcp && !notice && ctcp[1] === 'VERSION') { client.sendRaw('NOTICE', [from, '\x01VERSION Bare OS irc\x01']) } else if (ctcp && !notice && ctcp[1] === 'PING') { client.sendRaw('NOTICE', [from, text]) } } client.handleMessage = function (msg) { if (!msg || !msg.command) return emit('message', msg) var cmd = String(msg.command).toUpperCase() if (cmd === 'PING') { client.sendRaw('PONG', [msg.params[0] || '']) emit('ping', msg.params[0]) return } if (cmd === 'PONG') return if (cmd === 'CAP') { var sub = String(msg.params[1] || msg.params[0] || '').toUpperCase() var payload = msg.params[msg.params.length - 1] || '' if (sub === 'LS') { applyCapList(payload, false) var more = msg.params[2] === '*' || msg.params[1] === '*' if (!more) { capLsDone = true requestCaps() } } else if (sub === 'ACK') { applyCapList(payload, true) if (awaitingCapAck) finishCap() } else if (sub === 'NAK') { if (awaitingCapAck) finishCap() } else if (sub === 'NEW') applyCapList(payload, false) return } if (cmd === 'AUTHENTICATE') { if (String(msg.params[0] || '') === '+' && saslPending) { var chunks = bareIrcSaslPlainChunks(opts.saslUser, opts.saslPass) for (var c = 0; c < chunks.length; c++) client.sendRaw('AUTHENTICATE', [chunks[c]]) } return } if (/^\d{3}$/.test(cmd)) { handleNumeric(msg) return } if (cmd === 'PRIVMSG') { handlePrivmsg(msg, false) return } if (cmd === 'NOTICE') { handlePrivmsg(msg, true) return } if (cmd === 'JOIN') { var jc = msg.params[0] || '' var jb = bareIrcEnsureBuffer(client.state, 'channel', jc) var jm = bareIrcMemberPrefixes(client.state, msg.nick || '') var jmap = bareIrcCasemapping(client.state.isupport) if (jm.nick) { jb.members[bareIrcCasemap(jm.nick, jmap)] = jm } var selfJoin = bareIrcEqual(msg.nick || '', client.state.nick, jmap) if (selfJoin) { client.state.currentId = jb.id bareIrcPushLine(client.state, jb, { atMs: bareIrcNow(), kind: 'info', text: 'joined ' + jc }) } else { bareIrcPushLine(client.state, jb, { atMs: bareIrcNow(), kind: 'info', text: (msg.nick || '') + ' joined' }) } emit('join', { channel: jc, nick: msg.nick, self: selfJoin }) return } if (cmd === 'TOPIC') { var tc = msg.params[0] || '' var tb = bareIrcEnsureBuffer(client.state, 'channel', tc) tb.topic = msg.params[1] || '' bareIrcPushLine(client.state, tb, { atMs: bareIrcNow(), kind: 'info', text: (msg.nick || 'topic') + ': ' + tb.topic }) emit('topic', { channel: tc, topic: tb.topic, nick: msg.nick }) return } if (cmd === 'INVITE') { var invTo = msg.params[1] || '' var srvI = bareIrcEnsureBuffer(client.state, 'server', '*server') bareIrcPushLine(client.state, srvI, { atMs: bareIrcNow(), kind: 'info', text: (msg.nick || '') + ' invited you to ' + invTo + ' (/join ' + invTo + ')' }) emit('invite', { nick: msg.nick, channel: invTo }) return } if (cmd === 'PART' || cmd === 'KICK') { var pc = msg.params[0] || '' var pb = client.state.buffers[ bareIrcBufferId( 'channel', pc, bareIrcCasemapping(client.state.isupport) ) ] var who = cmd === 'KICK' ? msg.params[1] : msg.nick if (pb && who) { delete pb.members[ bareIrcCasemap(who, bareIrcCasemapping(client.state.isupport)) ] } emit(cmd.toLowerCase(), { channel: pc, nick: who }) return } if (cmd === 'NICK' && msg.nick) { var nn = msg.params[0] || '' if ( bareIrcEqual( msg.nick, client.state.nick, bareIrcCasemapping(client.state.isupport) ) ) { client.state.nick = nn } emit('nick', { from: msg.nick, to: nn }) return } if (cmd === 'ERROR') { emit('error', { text: msg.params[0] || '' }) return } var srv = bareIrcEnsureBuffer(client.state, 'server', '*server') bareIrcPushLine(client.state, srv, { atMs: bareIrcNow(), kind: 'info', command: cmd, text: (msg.params || []).join(' ') }) } client.attach = function (socket) { sock = socket client.state.connection = 'registering' bareIrcEnsureBuffer(client.state, 'server', '*server') client.state.currentId = 'server' if (sock && typeof sock.on === 'function') { sock.on('data', function (chunk) { var got = lines.push(chunk) for (var i = 0; i < got.length; i++) { var parsed = bareIrcParseMessage(got[i]) if (parsed) client.handleMessage(parsed) } }) sock.on('error', function (err) { client.state.connection = 'closed' emit('error', { error: err }) }) sock.on('close', function () { client.state.connection = 'closed' emit('close', {}) }) } var nick = opts.nick || 'bareos' client.state.nick = nick client.state.requestedNick = nick client.sendRaw('CAP', ['LS', '302']) if (opts.pass) client.sendRaw('PASS', [opts.pass]) client.sendRaw('NICK', [nick]) client.sendRaw('USER', [ opts.username || 'bareos', '0', '*', opts.realname || 'Bare OS irc' ]) return client } return client }