253 lines
6.3 KiB
JavaScript
253 lines
6.3 KiB
JavaScript
/** IRC message codec + casemap (modern.ircdocs.horse). No import. */
|
|
|
|
var BARE_IRC_MAX_LINE = 512
|
|
var BARE_IRC_MAX_TAGS = 8191
|
|
|
|
function bareIrcStripCrlf(s) {
|
|
var t = String(s == null ? '' : s)
|
|
if (t.charAt(t.length - 1) === '\n') t = t.slice(0, -1)
|
|
if (t.charAt(t.length - 1) === '\r') t = t.slice(0, -1)
|
|
return t
|
|
}
|
|
|
|
function bareIrcUnescapeTagValue(v) {
|
|
var s = String(v == null ? '' : v)
|
|
var out = ''
|
|
for (var i = 0; i < s.length; i++) {
|
|
var c = s.charAt(i)
|
|
if (c !== '\\') {
|
|
out += c
|
|
continue
|
|
}
|
|
var n = s.charAt(i + 1)
|
|
if (n === ':') {
|
|
out += ';'
|
|
i++
|
|
} else if (n === 's') {
|
|
out += ' '
|
|
i++
|
|
} else if (n === '\\') {
|
|
out += '\\'
|
|
i++
|
|
} else if (n === 'r') {
|
|
out += '\r'
|
|
i++
|
|
} else if (n === 'n') {
|
|
out += '\n'
|
|
i++
|
|
} else if (n) {
|
|
out += n
|
|
i++
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
function bareIrcEscapeTagValue(v) {
|
|
var s = String(v == null ? '' : v)
|
|
var out = ''
|
|
for (var i = 0; i < s.length; i++) {
|
|
var c = s.charAt(i)
|
|
if (c === ';') out += '\\:'
|
|
else if (c === ' ') out += '\\s'
|
|
else if (c === '\\') out += '\\\\'
|
|
else if (c === '\r') out += '\\r'
|
|
else if (c === '\n') out += '\\n'
|
|
else out += c
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* @param {string} line
|
|
* @returns {{ tags: Record<string, string>, source: string, nick: string, user: string, host: string, command: string, params: string[] } | null}
|
|
*/
|
|
function bareIrcParseMessage(line) {
|
|
var raw = bareIrcStripCrlf(line)
|
|
if (!raw) return null
|
|
var tags = {}
|
|
var i = 0
|
|
if (raw.charAt(0) === '@') {
|
|
var sp = raw.indexOf(' ')
|
|
if (sp < 0) return null
|
|
var tagStr = raw.slice(1, sp)
|
|
i = sp + 1
|
|
while (i < raw.length && raw.charAt(i) === ' ') i++
|
|
var parts = tagStr.split(';')
|
|
for (var t = 0; t < parts.length; t++) {
|
|
var item = parts[t]
|
|
if (!item) continue
|
|
var eq = item.indexOf('=')
|
|
if (eq < 0) tags[item] = ''
|
|
else tags[item.slice(0, eq)] = bareIrcUnescapeTagValue(item.slice(eq + 1))
|
|
}
|
|
raw = raw.slice(i)
|
|
i = 0
|
|
}
|
|
var source = ''
|
|
var nick = ''
|
|
var user = ''
|
|
var host = ''
|
|
if (raw.charAt(0) === ':') {
|
|
var sp2 = raw.indexOf(' ')
|
|
if (sp2 < 0) return null
|
|
source = raw.slice(1, sp2)
|
|
raw = raw.slice(sp2 + 1)
|
|
while (raw.charAt(0) === ' ') raw = raw.slice(1)
|
|
var bang = source.indexOf('!')
|
|
var at = source.indexOf('@')
|
|
if (bang >= 0) {
|
|
nick = source.slice(0, bang)
|
|
if (at > bang) {
|
|
user = source.slice(bang + 1, at)
|
|
host = source.slice(at + 1)
|
|
} else user = source.slice(bang + 1)
|
|
} else if (at >= 0) {
|
|
nick = source.slice(0, at)
|
|
host = source.slice(at + 1)
|
|
} else nick = source
|
|
}
|
|
while (raw.charAt(0) === ' ') raw = raw.slice(1)
|
|
var cmdEnd = raw.indexOf(' ')
|
|
var command
|
|
var rest
|
|
if (cmdEnd < 0) {
|
|
command = raw
|
|
rest = ''
|
|
} else {
|
|
command = raw.slice(0, cmdEnd)
|
|
rest = raw.slice(cmdEnd + 1)
|
|
}
|
|
if (!command) return null
|
|
var params = []
|
|
while (rest.length) {
|
|
while (rest.charAt(0) === ' ') rest = rest.slice(1)
|
|
if (!rest) break
|
|
if (rest.charAt(0) === ':') {
|
|
params.push(rest.slice(1))
|
|
break
|
|
}
|
|
var nsp = rest.indexOf(' ')
|
|
if (nsp < 0) {
|
|
params.push(rest)
|
|
break
|
|
}
|
|
params.push(rest.slice(0, nsp))
|
|
rest = rest.slice(nsp + 1)
|
|
}
|
|
return {
|
|
tags: tags,
|
|
source: source,
|
|
nick: nick,
|
|
user: user,
|
|
host: host,
|
|
command: command,
|
|
params: params
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {{ tags?: Record<string, string>, source?: string, command: string, params?: string[] }} msg
|
|
*/
|
|
function bareIrcSerializeMessage(msg) {
|
|
var m = msg || {}
|
|
var out = ''
|
|
var tags = m.tags
|
|
if (tags && typeof tags === 'object') {
|
|
var keys = []
|
|
var k
|
|
for (k in tags) {
|
|
if (Object.prototype.hasOwnProperty.call(tags, k)) keys.push(k)
|
|
}
|
|
if (keys.length) {
|
|
out += '@'
|
|
for (var i = 0; i < keys.length; i++) {
|
|
if (i) out += ';'
|
|
var val = tags[keys[i]]
|
|
if (val == null || val === '') out += keys[i]
|
|
else out += keys[i] + '=' + bareIrcEscapeTagValue(val)
|
|
}
|
|
out += ' '
|
|
}
|
|
}
|
|
if (m.source) out += ':' + m.source + ' '
|
|
out += String(m.command || '')
|
|
var params = Array.isArray(m.params) ? m.params : []
|
|
for (var p = 0; p < params.length; p++) {
|
|
var part = String(params[p] == null ? '' : params[p])
|
|
var last = p === params.length - 1
|
|
if (last) {
|
|
out += ' :' + part
|
|
} else {
|
|
out += ' ' + part
|
|
}
|
|
}
|
|
return out + '\r\n'
|
|
}
|
|
|
|
/**
|
|
* Byte/string accumulator. Returns complete lines without the delimiter.
|
|
*/
|
|
function bareIrcCreateLineBuffer() {
|
|
var acc = ''
|
|
return {
|
|
push: function (chunk) {
|
|
if (chunk == null) return []
|
|
var s
|
|
if (typeof chunk === 'string') s = chunk
|
|
else if (typeof TextDecoder !== 'undefined') {
|
|
s = new TextDecoder('utf-8', { fatal: false }).decode(
|
|
chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk)
|
|
)
|
|
} else {
|
|
var u = chunk instanceof Uint8Array ? chunk : []
|
|
s = ''
|
|
for (var i = 0; i < u.length; i++) s += String.fromCharCode(u[i])
|
|
}
|
|
acc += s.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
var out = []
|
|
for (;;) {
|
|
var n = acc.indexOf('\n')
|
|
if (n < 0) break
|
|
var line = acc.slice(0, n)
|
|
acc = acc.slice(n + 1)
|
|
if (line) out.push(line)
|
|
}
|
|
return out
|
|
},
|
|
pending: function () {
|
|
return acc
|
|
}
|
|
}
|
|
}
|
|
|
|
function bareIrcRfc1459LowerChar(c) {
|
|
var code = c.charCodeAt(0)
|
|
if (code >= 65 && code <= 90) return String.fromCharCode(code + 32)
|
|
if (c === '[') return '{'
|
|
if (c === ']') return '}'
|
|
if (c === '\\') return '|'
|
|
if (c === '~') return '^'
|
|
return c
|
|
}
|
|
|
|
function bareIrcCasemap(s, mapping) {
|
|
var t = String(s == null ? '' : s)
|
|
var kind = String(mapping || 'rfc1459').toLowerCase()
|
|
if (kind === 'ascii') return t.toLowerCase()
|
|
var out = ''
|
|
for (var i = 0; i < t.length; i++) out += bareIrcRfc1459LowerChar(t.charAt(i))
|
|
return out
|
|
}
|
|
|
|
function bareIrcEqual(a, b, mapping) {
|
|
return bareIrcCasemap(a, mapping) === bareIrcCasemap(b, mapping)
|
|
}
|
|
|
|
function bareIrcIsChannel(name, chantypes) {
|
|
var n = String(name || '')
|
|
if (!n) return false
|
|
var types = chantypes || '#'
|
|
return types.indexOf(n.charAt(0)) >= 0
|
|
}
|