2999 lines
83 KiB
Plaintext
2999 lines
83 KiB
Plaintext
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||
function bareStdin(ctx) {
|
||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||
}
|
||
|
||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||
function bareFormatModeString(mode, type) {
|
||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||
const perm = mode & 0o777
|
||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||
return (
|
||
typeChar +
|
||
r(0o400) +
|
||
w(0o200) +
|
||
x(0o100) +
|
||
r(0o040) +
|
||
w(0o020) +
|
||
x(0o010) +
|
||
r(0o004) +
|
||
w(0o002) +
|
||
x(0o001)
|
||
)
|
||
}
|
||
|
||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||
const now = nowMs != null ? nowMs : Date.now()
|
||
const d = new Date(mtimeMs)
|
||
const months = [
|
||
'Jan',
|
||
'Feb',
|
||
'Mar',
|
||
'Apr',
|
||
'May',
|
||
'Jun',
|
||
'Jul',
|
||
'Aug',
|
||
'Sep',
|
||
'Oct',
|
||
'Nov',
|
||
'Dec'
|
||
]
|
||
const mon = months[d.getMonth()]
|
||
const day = String(d.getDate()).padStart(2, ' ')
|
||
const sixMo = 180 * 24 * 3600 * 1000
|
||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||
return mon + ' ' + day + ' ' + yr
|
||
}
|
||
const hh = String(d.getHours()).padStart(2, '0')
|
||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||
}
|
||
|
||
/** @param {number} size */
|
||
function barePosixBlocks(size) {
|
||
return Math.ceil(Number(size) / 512) || 0
|
||
}
|
||
|
||
/**
|
||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string | Uint8Array} chunk
|
||
* @returns {boolean}
|
||
*/
|
||
function bareOsEmitRaw(ctx, chunk) {
|
||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||
const b4 = ctx.b4a
|
||
const u8 =
|
||
typeof chunk === 'string'
|
||
? b4 && typeof b4.from === 'function'
|
||
? b4.from(chunk)
|
||
: new TextEncoder().encode(chunk)
|
||
: chunk
|
||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||
return true
|
||
}
|
||
const w = globalThis.process?.stdout?.write
|
||
if (typeof w === 'function') {
|
||
w.call(globalThis.process.stdout, chunk)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/** Session env map (`vfs.env`, then `ctx.env`). Never throws. */
|
||
function bareOsEnv(ctx) {
|
||
const v = ctx && ctx.vfs && ctx.vfs.env
|
||
if (v && typeof v === 'object') return v
|
||
const e = ctx && ctx.env
|
||
if (e && typeof e === 'object') return e
|
||
return {}
|
||
}
|
||
|
||
/**
|
||
* Strict POSIX-ish decimal integer (no octal, no exponent, no empty).
|
||
* @param {unknown} s
|
||
* @returns {number}
|
||
*/
|
||
function bareOsParseDecInt(s) {
|
||
const t = String(s == null ? '' : s).trim()
|
||
if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN
|
||
const n = Number.parseInt(t, 10)
|
||
return Number.isSafeInteger(n) ? n : NaN
|
||
}
|
||
|
||
/** @param {unknown} s */
|
||
function bareOsParseNonNegInt(s) {
|
||
const n = bareOsParseDecInt(s)
|
||
return n >= 0 ? n : NaN
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} name
|
||
* @param {number} fallback
|
||
* @param {number} [min]
|
||
* @param {number} [max]
|
||
*/
|
||
function bareOsEnvInt(ctx, name, fallback, min, max) {
|
||
const raw = bareOsEnv(ctx)[name]
|
||
if (raw == null || raw === '') return fallback
|
||
const n = Number.parseInt(String(raw), 10)
|
||
if (!Number.isFinite(n)) return fallback
|
||
let v = n
|
||
if (min != null && v < min) v = min
|
||
if (max != null && v > max) v = max
|
||
return v
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} msg
|
||
* @param {number} [code]
|
||
*/
|
||
function bareOsFail(ctx, msg, code) {
|
||
if (msg) ctx.console.error(msg)
|
||
ctx.exitCode = code == null ? 1 : code
|
||
}
|
||
|
||
/** @param {unknown} e */
|
||
function bareOsIsNotFoundErr(e) {
|
||
const code = e && typeof e === 'object' ? e.code : ''
|
||
if (code === 'ENOENT') return true
|
||
const msg = String((e && e.message) || e || '')
|
||
return /ENOENT|No such file|not found/i.test(msg)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {unknown} buf
|
||
* @returns {Uint8Array}
|
||
*/
|
||
function bareOsToU8(ctx, buf) {
|
||
if (!buf) return new Uint8Array(0)
|
||
if (buf instanceof Uint8Array) return buf
|
||
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf)
|
||
return new Uint8Array(buf)
|
||
}
|
||
|
||
/** @param {string} dir @param {string} name */
|
||
function bareOsJoinPath(dir, name) {
|
||
const d = String(dir || '').replace(/\/+$/, '')
|
||
const n = String(name || '').replace(/^\/+/, '')
|
||
if (!d || d === '/') return '/' + n
|
||
return d + '/' + n
|
||
}
|
||
|
||
/** @param {string} p */
|
||
function bareOsBaseName(p) {
|
||
const t = String(p || '').replace(/\/+$/, '')
|
||
if (!t || t === '/') return t === '/' ? '/' : ''
|
||
const i = t.lastIndexOf('/')
|
||
return i < 0 ? t : t.slice(i + 1) || t
|
||
}
|
||
|
||
/** @param {string} p */
|
||
function bareOsParentDir(p) {
|
||
const t = String(p || '').replace(/\/+$/, '') || '/'
|
||
if (t === '/') return '/'
|
||
const i = t.lastIndexOf('/')
|
||
return i <= 0 ? '/' : t.slice(0, i) || '/'
|
||
}
|
||
|
||
/** @param {string} p */
|
||
function bareOsNormPath(p) {
|
||
return String(p || '').replace(/\/+$/, '') || '/'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} p
|
||
*/
|
||
function bareOsResolvePath(ctx, p) {
|
||
if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') {
|
||
try {
|
||
return String(ctx.vfs.resolveLogical(p) || p)
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
}
|
||
return String(p || '')
|
||
}
|
||
|
||
/**
|
||
* True when dest is src or lives under src (self-copy / self-move).
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} src
|
||
* @param {string} dest
|
||
*/
|
||
function bareOsDestInsideSrc(ctx, src, dest) {
|
||
const s = bareOsNormPath(bareOsResolvePath(ctx, src))
|
||
const d = bareOsNormPath(bareOsResolvePath(ctx, dest))
|
||
if (s === d) return true
|
||
if (s === '/') return d !== '/'
|
||
return d === s || d.startsWith(s + '/')
|
||
}
|
||
|
||
const BARE_OS_B64_ALPH =
|
||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||
|
||
/** @param {Uint8Array} u8 */
|
||
function bareOsB64Encode(u8) {
|
||
let out = ''
|
||
let i = 0
|
||
for (; i + 2 < u8.length; i += 3) {
|
||
const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2]
|
||
out +=
|
||
BARE_OS_B64_ALPH[(n >> 18) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 12) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 6) & 63] +
|
||
BARE_OS_B64_ALPH[n & 63]
|
||
}
|
||
const rest = u8.length - i
|
||
if (rest === 1) {
|
||
const n = u8[i] << 16
|
||
out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '=='
|
||
} else if (rest === 2) {
|
||
const n = (u8[i] << 16) | (u8[i + 1] << 8)
|
||
out +=
|
||
BARE_OS_B64_ALPH[(n >> 18) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 12) & 63] +
|
||
BARE_OS_B64_ALPH[(n >> 6) & 63] +
|
||
'='
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk.
|
||
* @param {string} s
|
||
* @returns {Uint8Array}
|
||
*/
|
||
function bareOsB64Decode(s) {
|
||
const t = String(s).replace(/\s+/g, '')
|
||
if (!t) return new Uint8Array(0)
|
||
if (t.length % 4 === 1) throw new Error('invalid base64 length')
|
||
let pad = 0
|
||
if (t.endsWith('==')) pad = 2
|
||
else if (t.endsWith('=')) pad = 1
|
||
const body = pad ? t.slice(0, t.length - pad) : t
|
||
const bytes = []
|
||
let buf = 0
|
||
let bits = 0
|
||
for (let i = 0; i < body.length; i++) {
|
||
const c = body[i]
|
||
let v = BARE_OS_B64_ALPH.indexOf(c)
|
||
if (v < 0) {
|
||
if (c === '-') v = 62
|
||
else if (c === '_') v = 63
|
||
else throw new Error('invalid base64 character')
|
||
}
|
||
buf = (buf << 6) | v
|
||
bits += 6
|
||
if (bits >= 8) {
|
||
bits -= 8
|
||
bytes.push((buf >> bits) & 255)
|
||
}
|
||
}
|
||
if (pad) {
|
||
const want = Math.floor((body.length * 6) / 8)
|
||
if (bytes.length > want) bytes.length = want
|
||
}
|
||
return new Uint8Array(bytes)
|
||
}
|
||
|
||
/**
|
||
* @param {string} s
|
||
* @returns {Uint8Array}
|
||
*/
|
||
function bareOsHexDecode(s) {
|
||
const t = String(s).replace(/\s+/g, '')
|
||
if (t.length % 2 !== 0) throw new Error('odd hex length')
|
||
const out = new Uint8Array(t.length / 2)
|
||
for (let i = 0; i < out.length; i++) {
|
||
const pair = t.slice(i * 2, i * 2 + 2)
|
||
if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex')
|
||
out[i] = Number.parseInt(pair, 16)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** @param {Uint8Array} u8 */
|
||
function bareOsHexEncode(u8) {
|
||
let s = ''
|
||
for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0')
|
||
return s
|
||
}
|
||
|
||
/** 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
|
||
}
|
||
|
||
/** RPL_ISUPPORT (005) token map. */
|
||
|
||
function bareIrcParseIsupport(params) {
|
||
var tokens = {}
|
||
var list = Array.isArray(params) ? params : []
|
||
var last = list.length
|
||
if (last && String(list[last - 1]).indexOf('are supported') >= 0) last--
|
||
var start = 0
|
||
if (
|
||
list.length &&
|
||
!/=/.test(String(list[0])) &&
|
||
!/^[A-Z]+$/.test(String(list[0]))
|
||
) {
|
||
start = 1
|
||
}
|
||
for (var i = start; i < last; i++) {
|
||
var raw = String(list[i] || '')
|
||
if (!raw) continue
|
||
var eq = raw.indexOf('=')
|
||
if (eq < 0) tokens[raw] = true
|
||
else tokens[raw.slice(0, eq)] = raw.slice(eq + 1)
|
||
}
|
||
return tokens
|
||
}
|
||
|
||
function bareIrcIsupportDefaults() {
|
||
return {
|
||
CASEMAPPING: 'rfc1459',
|
||
CHANTYPES: '#&',
|
||
PREFIX: '(ov)@+',
|
||
NICKLEN: '16',
|
||
CHANNELLEN: '50',
|
||
TOPICLEN: '390',
|
||
NETWORK: ''
|
||
}
|
||
}
|
||
|
||
function bareIrcMergeIsupport(cur, incoming) {
|
||
var out = cur && typeof cur === 'object' ? cur : bareIrcIsupportDefaults()
|
||
var next = incoming && typeof incoming === 'object' ? incoming : {}
|
||
var k
|
||
for (k in next) {
|
||
if (Object.prototype.hasOwnProperty.call(next, k)) out[k] = next[k]
|
||
}
|
||
return out
|
||
}
|
||
|
||
function bareIrcPrefixMap(isupport) {
|
||
var raw =
|
||
isupport && isupport.PREFIX != null ? String(isupport.PREFIX) : '(ov)@+'
|
||
var m = /^\(([^)]+)\)(.*)$/.exec(raw)
|
||
var modes = m ? m[1] : 'ov'
|
||
var prefs = m ? m[2] : '@+'
|
||
var byMode = {}
|
||
var byPref = {}
|
||
var i
|
||
for (i = 0; i < modes.length && i < prefs.length; i++) {
|
||
byMode[modes.charAt(i)] = prefs.charAt(i)
|
||
byPref[prefs.charAt(i)] = modes.charAt(i)
|
||
}
|
||
return { modes: modes, prefixes: prefs, byMode: byMode, byPref: byPref }
|
||
}
|
||
|
||
function bareIrcNicklen(isupport) {
|
||
var n = parseInt(isupport && isupport.NICKLEN, 10)
|
||
return Number.isFinite(n) && n > 0 ? n : 16
|
||
}
|
||
|
||
function bareIrcChantypes(isupport) {
|
||
return (isupport && isupport.CHANTYPES) || '#'
|
||
}
|
||
|
||
function bareIrcCasemapping(isupport) {
|
||
return (isupport && isupport.CASEMAPPING) || 'rfc1459'
|
||
}
|
||
|
||
/** SASL PLAIN (IRCv3). No import. */
|
||
|
||
function bareIrcB64Encode(bytes) {
|
||
var table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||
var s = ''
|
||
var i = 0
|
||
while (i < bytes.length) {
|
||
var a = bytes[i++]
|
||
var b = i < bytes.length ? bytes[i++] : NaN
|
||
var c = i < bytes.length ? bytes[i++] : NaN
|
||
var triple =
|
||
((a & 255) << 16) |
|
||
((Number.isFinite(b) ? b & 255 : 0) << 8) |
|
||
(Number.isFinite(c) ? c & 255 : 0)
|
||
s += table.charAt((triple >> 18) & 63)
|
||
s += table.charAt((triple >> 12) & 63)
|
||
s += Number.isFinite(b) ? table.charAt((triple >> 6) & 63) : '='
|
||
s += Number.isFinite(c) ? table.charAt(triple & 63) : '='
|
||
}
|
||
return s
|
||
}
|
||
|
||
function bareIrcUtf8Bytes(str) {
|
||
var s = String(str == null ? '' : str)
|
||
if (typeof TextEncoder !== 'undefined')
|
||
return Array.from(new TextEncoder().encode(s))
|
||
var out = []
|
||
for (var i = 0; i < s.length; i++) out.push(s.charCodeAt(i) & 255)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* AUTHENTICATE PLAIN payload (account \0 account \0 password), chunked ≤400.
|
||
* @returns {string[]}
|
||
*/
|
||
function bareIrcSaslPlainChunks(user, pass) {
|
||
var u = String(user || '')
|
||
var p = String(pass || '')
|
||
var raw = bareIrcUtf8Bytes('\0' + u + '\0' + p)
|
||
var b64 = bareIrcB64Encode(raw)
|
||
var chunks = []
|
||
var i = 0
|
||
if (!b64) return ['+']
|
||
while (i < b64.length) {
|
||
chunks.push(b64.slice(i, i + 400))
|
||
i += 400
|
||
}
|
||
return chunks
|
||
}
|
||
|
||
/** IRC session buffers and members. */
|
||
|
||
var BARE_IRC_MAX_TRANSCRIPT = 2000
|
||
|
||
function bareIrcCreateState(opts) {
|
||
opts = opts || {}
|
||
var max =
|
||
opts.maxTranscript > 0 ? opts.maxTranscript : BARE_IRC_MAX_TRANSCRIPT
|
||
return {
|
||
connection: 'idle',
|
||
nick: opts.nick || '',
|
||
requestedNick: opts.nick || '',
|
||
account: '',
|
||
away: false,
|
||
network: opts.network || '',
|
||
isupport: bareIrcIsupportDefaults(),
|
||
capsOffered: {},
|
||
capsEnabled: {},
|
||
buffers: {},
|
||
currentId: 'server',
|
||
maxTranscript: max,
|
||
registered: false,
|
||
tls: opts.tls !== false,
|
||
saslOk: false,
|
||
channelList: [],
|
||
listBusy: false,
|
||
ignore: {}
|
||
}
|
||
}
|
||
|
||
function bareIrcBufferId(kind, name, mapping) {
|
||
if (kind === 'server') return 'server'
|
||
return kind + ':' + bareIrcCasemap(name, mapping)
|
||
}
|
||
|
||
function bareIrcEnsureBuffer(state, kind, name) {
|
||
var mapping = bareIrcCasemapping(state.isupport)
|
||
var id = bareIrcBufferId(kind, name, mapping)
|
||
if (!state.buffers[id]) {
|
||
state.buffers[id] = {
|
||
id: id,
|
||
kind: kind,
|
||
name: name || '*server',
|
||
topic: '',
|
||
members: {},
|
||
lines: [],
|
||
unread: 0,
|
||
highlight: false
|
||
}
|
||
}
|
||
return state.buffers[id]
|
||
}
|
||
|
||
function bareIrcPushLine(state, buf, rec) {
|
||
if (!buf) return
|
||
buf.lines.push(rec)
|
||
while (buf.lines.length > state.maxTranscript) buf.lines.shift()
|
||
if (buf.id !== state.currentId) {
|
||
buf.unread++
|
||
if (rec && rec.highlight) buf.highlight = true
|
||
}
|
||
}
|
||
|
||
function bareIrcListBuffers(state) {
|
||
var ids = Object.keys(state.buffers)
|
||
ids.sort(function (a, b) {
|
||
if (a === 'server') return -1
|
||
if (b === 'server') return 1
|
||
return a < b ? -1 : a > b ? 1 : 0
|
||
})
|
||
return ids.map(function (id) {
|
||
return state.buffers[id]
|
||
})
|
||
}
|
||
|
||
function bareIrcMemberPrefixes(state, nickRaw) {
|
||
var pref = bareIrcPrefixMap(state.isupport)
|
||
var nick = String(nickRaw || '')
|
||
var modes = ''
|
||
while (nick && pref.byPref[nick.charAt(0)]) {
|
||
modes += pref.byPref[nick.charAt(0)]
|
||
nick = nick.slice(1)
|
||
}
|
||
return { nick: nick, modes: modes }
|
||
}
|
||
|
||
/** Libera defaults + ~/.irc config/secrets. */
|
||
|
||
function bareIrcRandomSuffix(n) {
|
||
var alphabet = 'abcdefghijkmnpqrstuvwxyz23456789'
|
||
var out = ''
|
||
var i
|
||
var len = n > 0 ? n : 4
|
||
for (i = 0; i < len; i++) {
|
||
out += alphabet.charAt(Math.floor(Math.random() * alphabet.length))
|
||
}
|
||
return out
|
||
}
|
||
|
||
function bareIrcRandomNick() {
|
||
return ('os' + bareIrcRandomSuffix(5)).slice(0, 16)
|
||
}
|
||
|
||
function bareIrcNickLooksMachine(nick) {
|
||
var s = String(nick || '')
|
||
if (!s) return true
|
||
if (/^[0-9a-f]{8,16}$/i.test(s)) return true
|
||
if (/^[0-9a-f]{12}$/i.test(s)) return true
|
||
if (/^[a-z0-9]{10,}-[a-z0-9]{4,}$/i.test(s)) return true
|
||
return false
|
||
}
|
||
|
||
function bareIrcHostLooksLibera(host) {
|
||
var h = String(host || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
return h === 'libera.chat' || h.endsWith('.libera.chat')
|
||
}
|
||
|
||
var BARE_IRC_LIBERA = {
|
||
name: 'libera',
|
||
host: 'irc.libera.chat',
|
||
port: 6697,
|
||
tls: true,
|
||
regional: {
|
||
eu: 'irc.eu.libera.chat',
|
||
us: 'irc.us.libera.chat',
|
||
au: 'irc.au.libera.chat',
|
||
ea: 'irc.ea.libera.chat',
|
||
ipv4: 'irc.ipv4.libera.chat',
|
||
ipv6: 'irc.ipv6.libera.chat'
|
||
}
|
||
}
|
||
|
||
function bareIrcHome(ctx) {
|
||
var env = ctx && ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||
if (env.HOME) return String(env.HOME)
|
||
if (typeof ctx.bareOsResolveHome === 'function') {
|
||
try {
|
||
return String(ctx.bareOsResolveHome() || '')
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return '/home/guest'
|
||
}
|
||
|
||
function bareIrcConfigPaths(ctx) {
|
||
var home = bareIrcHome(ctx).replace(/\/+$/, '')
|
||
return {
|
||
dir: home + '/.irc',
|
||
config: home + '/.irc/config.json',
|
||
secrets: home + '/.irc/secrets.json',
|
||
bindings: home + '/.irc/bindings.json'
|
||
}
|
||
}
|
||
|
||
function bareIrcReadJsonFile(ctx, path) {
|
||
if (!ctx || !ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
|
||
try {
|
||
var buf = ctx.vfs.readFile(path)
|
||
if (buf && typeof buf.then === 'function') return buf
|
||
if (!buf) return null
|
||
var t =
|
||
ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(buf)
|
||
t = String(t || '').trim()
|
||
if (!t) return null
|
||
return JSON.parse(t)
|
||
} catch (e) {
|
||
return null
|
||
}
|
||
}
|
||
|
||
async function bareIrcLoadConfig(ctx) {
|
||
var paths = bareIrcConfigPaths(ctx)
|
||
var raw = bareIrcReadJsonFile(ctx, paths.config)
|
||
if (raw && typeof raw.then === 'function') {
|
||
try {
|
||
var buf = await raw
|
||
if (!buf) raw = null
|
||
else {
|
||
var t =
|
||
ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(buf)
|
||
: String(buf)
|
||
raw = t && String(t).trim() ? JSON.parse(String(t).trim()) : null
|
||
}
|
||
} catch (e) {
|
||
raw = null
|
||
}
|
||
}
|
||
var sec = bareIrcReadJsonFile(ctx, paths.secrets)
|
||
if (sec && typeof sec.then === 'function') {
|
||
try {
|
||
var sb = await sec
|
||
if (!sb) sec = null
|
||
else {
|
||
var st =
|
||
ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||
? ctx.b4a.toString(sb)
|
||
: String(sb)
|
||
sec = st && String(st).trim() ? JSON.parse(String(st).trim()) : null
|
||
}
|
||
} catch (e2) {
|
||
sec = null
|
||
}
|
||
}
|
||
return {
|
||
paths: paths,
|
||
config: raw && typeof raw === 'object' ? raw : {},
|
||
secrets: sec && typeof sec === 'object' ? sec : {}
|
||
}
|
||
}
|
||
|
||
function bareIrcResolveConnect(ctx, flags, loaded) {
|
||
var env = ctx && ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||
var cfg = (loaded && loaded.config) || {}
|
||
var sec = (loaded && loaded.secrets) || {}
|
||
var servers = Array.isArray(cfg.servers) ? cfg.servers : []
|
||
var named = flags.network || flags.host || ''
|
||
var srv = servers[0] || {}
|
||
if (named) {
|
||
for (var i = 0; i < servers.length; i++) {
|
||
if (servers[i] && servers[i].name === named) srv = servers[i]
|
||
}
|
||
if (named === 'libera' || named === 'irc.libera.chat') {
|
||
srv = Object.assign({}, BARE_IRC_LIBERA, srv)
|
||
} else if (BARE_IRC_LIBERA.regional[named]) {
|
||
srv = Object.assign({}, BARE_IRC_LIBERA, srv, {
|
||
host: BARE_IRC_LIBERA.regional[named]
|
||
})
|
||
}
|
||
}
|
||
var host = flags.host || srv.host || BARE_IRC_LIBERA.host
|
||
if (host === 'libera') host = BARE_IRC_LIBERA.host
|
||
var port = flags.port != null ? flags.port : srv.port || BARE_IRC_LIBERA.port
|
||
var tls = flags.tls != null ? flags.tls : srv.tls !== false
|
||
var nick = flags.nick || cfg.nick || env.BARE_IRC_NICK || ''
|
||
var generatedNick = false
|
||
if (!nick) {
|
||
var candidate = env.USER || env.LOGNAME || ''
|
||
if (candidate && !bareIrcNickLooksMachine(candidate)) nick = candidate
|
||
}
|
||
if (!nick) {
|
||
nick = bareIrcRandomNick()
|
||
generatedNick = true
|
||
}
|
||
var saslUser = flags.saslUser || cfg.saslUser || sec.saslUser || ''
|
||
var saslPass = flags.saslPass != null ? flags.saslPass : sec.saslPass || ''
|
||
var autojoin = Array.isArray(flags.autojoin)
|
||
? flags.autojoin
|
||
: Array.isArray(cfg.autojoin)
|
||
? cfg.autojoin
|
||
: []
|
||
return {
|
||
host: host,
|
||
port: port,
|
||
tls: tls,
|
||
insecurePlain: !!flags.insecurePlain,
|
||
nick: String(nick).slice(0, 16),
|
||
generatedNick: generatedNick,
|
||
username: flags.username || cfg.username || 'bareos',
|
||
realname: flags.realname || cfg.realname || 'Bare OS irc',
|
||
pass: flags.pass || sec.serverPass || '',
|
||
saslUser: saslUser,
|
||
saslPass: saslPass,
|
||
autojoin: autojoin,
|
||
shareChannels: cfg.shareChannels === true,
|
||
network: srv.name || (bareIrcHostLooksLibera(host) ? 'libera' : host)
|
||
}
|
||
}
|
||
|
||
function bareIrcParseFlags(argv) {
|
||
var args = argv.slice(1)
|
||
var flags = {
|
||
help: false,
|
||
json: false,
|
||
tls: null,
|
||
insecurePlain: false,
|
||
host: '',
|
||
port: null,
|
||
nick: '',
|
||
saslUser: '',
|
||
network: '',
|
||
rest: []
|
||
}
|
||
for (var i = 0; i < args.length; i++) {
|
||
var a = args[i]
|
||
if (a === '-h' || a === '--help') flags.help = true
|
||
else if (a === '--json') flags.json = true
|
||
else if (a === '--tls') flags.tls = true
|
||
else if (a === '--plain') flags.tls = false
|
||
else if (a === '--insecure-plain') flags.insecurePlain = true
|
||
else if (a === '--host' && args[i + 1]) flags.host = args[++i]
|
||
else if (a === '--port' && args[i + 1]) flags.port = parseInt(args[++i], 10)
|
||
else if (a === '--nick' && args[i + 1]) flags.nick = args[++i]
|
||
else if (a === '--sasl-user' && args[i + 1]) flags.saslUser = args[++i]
|
||
else if (a === '--network' && args[i + 1]) flags.network = args[++i]
|
||
else if (a && a.charAt(0) !== '-') flags.rest.push(a)
|
||
}
|
||
if (flags.rest[0] && !flags.host && !flags.network) {
|
||
var r0 = flags.rest[0]
|
||
if (
|
||
r0 === 'libera' ||
|
||
r0 === 'eu' ||
|
||
r0 === 'us' ||
|
||
r0 === 'au' ||
|
||
r0 === 'ea' ||
|
||
r0.indexOf('.') >= 0
|
||
) {
|
||
flags.network = r0
|
||
flags.rest = flags.rest.slice(1)
|
||
}
|
||
}
|
||
return flags
|
||
}
|
||
|
||
async function bareIrcRememberNick(ctx, nick) {
|
||
var n = String(nick || '').trim()
|
||
if (!n) return false
|
||
if (!ctx || !ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
|
||
var paths = bareIrcConfigPaths(ctx)
|
||
var loaded = await bareIrcLoadConfig(ctx)
|
||
var cfg = loaded.config || {}
|
||
if (cfg.nick) return false
|
||
cfg.nick = n
|
||
try {
|
||
if (typeof ctx.vfs.mkdir === 'function') {
|
||
var mk = ctx.vfs.mkdir(paths.dir, { recursive: true })
|
||
if (mk && typeof mk.then === 'function') await mk
|
||
}
|
||
var body = JSON.stringify(cfg, null, 2)
|
||
var w = ctx.vfs.writeFile(paths.config, body)
|
||
if (w && typeof w.then === 'function') await w
|
||
return true
|
||
} catch (e) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/** 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
|
||
}
|
||
|
||
/** Resolve a TCP/TLS socket for /bin/irc without requiring ctx.bareOsTlsConnect. */
|
||
|
||
function bareIrcTryHostTls(ctx) {
|
||
var req = null
|
||
try {
|
||
if (typeof globalThis.require === 'function') req = globalThis.require
|
||
} catch (e) {
|
||
req = null
|
||
}
|
||
if (
|
||
!req &&
|
||
typeof Bare !== 'undefined' &&
|
||
Bare &&
|
||
typeof Bare.require === 'function'
|
||
) {
|
||
req = Bare.require
|
||
}
|
||
if (!req) return null
|
||
var names = ['bare-tls', 'tls', 'node:tls']
|
||
var i
|
||
for (i = 0; i < names.length; i++) {
|
||
try {
|
||
var m = req(names[i])
|
||
if (m) return m
|
||
} catch (e2) {
|
||
/* next */
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
function bareIrcTlsLib(ctx) {
|
||
if (ctx && ctx.bare) {
|
||
if (ctx.bare.bareTls) return ctx.bare.bareTls
|
||
if (ctx.bare.tls) return ctx.bare.tls
|
||
}
|
||
return bareIrcTryHostTls(ctx)
|
||
}
|
||
|
||
function bareIrcOpenPlain(ctx, spec) {
|
||
if (typeof ctx.bareOsTelnetConnect === 'function') {
|
||
return ctx.bareOsTelnetConnect(spec.host, spec.port, {})
|
||
}
|
||
var net =
|
||
ctx && ctx.bare && (ctx.bare.bareTcp || ctx.bare.bareNet || ctx.bare.net)
|
||
if (net && typeof net.createConnection === 'function') {
|
||
return net.createConnection({ host: spec.host, port: spec.port })
|
||
}
|
||
throw new Error('irc: no plaintext connector (ctx.bareOsTelnetConnect)')
|
||
}
|
||
|
||
function bareIrcOpenTls(ctx, spec) {
|
||
var opts = {
|
||
host: spec.host,
|
||
port: spec.port,
|
||
servername: spec.host,
|
||
rejectUnauthorized: true
|
||
}
|
||
if (typeof ctx.bareOsTlsConnect === 'function') {
|
||
return ctx.bareOsTlsConnect(spec.host, spec.port, opts)
|
||
}
|
||
var tls = bareIrcTlsLib(ctx)
|
||
if (tls && typeof tls.connect === 'function') {
|
||
return tls.connect(opts)
|
||
}
|
||
if (tls && typeof tls.createConnection === 'function') {
|
||
return tls.createConnection(opts)
|
||
}
|
||
var Sock = tls && (tls.Socket || tls.TLSSocket)
|
||
if (typeof Sock === 'function') {
|
||
var tcp = bareIrcOpenPlain(ctx, spec)
|
||
return new Sock(tcp, { host: spec.host })
|
||
}
|
||
throw new Error(
|
||
'irc: no TLS connector (need ctx.bareOsTlsConnect or ctx.bare.bareTls)'
|
||
)
|
||
}
|
||
|
||
function bareIrcDial(ctx, spec) {
|
||
if (typeof ctx.bareOsIrcHostAllowed === 'function') {
|
||
var gate = ctx.bareOsIrcHostAllowed(spec.host)
|
||
if (gate && gate.ok === false) {
|
||
throw new Error(
|
||
'irc: host blocked (' + (gate.reason || 'policy') + '): ' + spec.host
|
||
)
|
||
}
|
||
}
|
||
if (spec.tls === false) {
|
||
var plainOk =
|
||
typeof ctx.bareOsIrcPlaintextAllowed === 'function'
|
||
? ctx.bareOsIrcPlaintextAllowed(spec.host, {
|
||
tls: false,
|
||
insecurePlain: spec.insecurePlain
|
||
})
|
||
: { ok: !bareIrcHostLooksLibera(spec.host) || spec.insecurePlain }
|
||
if (plainOk && plainOk.ok === false) {
|
||
throw new Error('irc: ' + (plainOk.reason || 'plaintext blocked'))
|
||
}
|
||
return bareIrcOpenPlain(ctx, spec)
|
||
}
|
||
return bareIrcOpenTls(ctx, spec)
|
||
}
|
||
|
||
/** Slash-command table for /bin/irc (modern client + common aliases). */
|
||
|
||
var BARE_IRC_FEATURED_ROOMS = [
|
||
{ name: '#libera', users: '', topic: 'Official Libera.Chat help' },
|
||
{ name: '#linux', users: '', topic: 'Linux discussion' },
|
||
{ name: '#programming', users: '', topic: 'Programming' },
|
||
{ name: '#javascript', users: '', topic: 'JavaScript' },
|
||
{ name: '#python', users: '', topic: 'Python' },
|
||
{ name: '#rust', users: '', topic: 'Rust' },
|
||
{ name: '#archlinux', users: '', topic: 'Arch Linux' },
|
||
{ name: '#debian', users: '', topic: 'Debian' },
|
||
{ name: '#ubuntu', users: '', topic: 'Ubuntu' },
|
||
{ name: '#vim', users: '', topic: 'Vim' },
|
||
{ name: '#emacs', users: '', topic: 'Emacs' },
|
||
{ name: '#git', users: '', topic: 'Git' }
|
||
]
|
||
|
||
function bareIrcNeedTarget(buf) {
|
||
return buf && buf.kind && buf.kind !== 'server' ? buf.name : ''
|
||
}
|
||
|
||
function bareIrcModeNicks(client, chan, flag, nicks) {
|
||
if (!chan || !nicks.length) return false
|
||
var i
|
||
for (i = 0; i < nicks.length; i++) {
|
||
client.sendRaw('MODE', [chan, flag, nicks[i]])
|
||
}
|
||
return true
|
||
}
|
||
|
||
function bareIrcRunSlash(client, rawLine, opts) {
|
||
opts = opts || {}
|
||
var buf = opts.buf || null
|
||
var ctx = opts.ctx
|
||
var line = String(rawLine || '').trim()
|
||
var out = {
|
||
ok: true,
|
||
help: false,
|
||
rooms: false,
|
||
roomsQuery: '',
|
||
quit: false,
|
||
close: false,
|
||
clear: false,
|
||
usage: '',
|
||
note: ''
|
||
}
|
||
if (!line || line.charAt(0) !== '/') {
|
||
out.ok = false
|
||
return out
|
||
}
|
||
var body = line.slice(1)
|
||
var sp = body.search(/\s/)
|
||
var cmd = (sp < 0 ? body : body.slice(0, sp)).toLowerCase()
|
||
var rest = sp < 0 ? '' : body.slice(sp + 1).trim()
|
||
var parts = rest ? rest.split(/\s+/) : []
|
||
var chan = bareIrcNeedTarget(buf)
|
||
|
||
if (cmd === 'help' || cmd === 'h' || cmd === '?') {
|
||
out.help = true
|
||
return out
|
||
}
|
||
if (cmd === 'quit' || cmd === 'exit') {
|
||
client.quit(rest || 'Leaving')
|
||
out.quit = true
|
||
return out
|
||
}
|
||
if (cmd === 'disconnect') {
|
||
client.quit(rest || 'Disconnect')
|
||
return out
|
||
}
|
||
if (cmd === 'reconnect') {
|
||
if (opts.reconnect) opts.reconnect()
|
||
else out.note = 'reconnect unavailable'
|
||
return out
|
||
}
|
||
if (cmd === 'join' || cmd === 'j') {
|
||
if (!parts[0]) {
|
||
out.usage = '/join #channel [key]'
|
||
return out
|
||
}
|
||
client.join(parts[0], parts[1])
|
||
return out
|
||
}
|
||
if (cmd === 'part' || cmd === 'leave') {
|
||
var pchan = parts[0] && parts[0].charAt(0) === '#' ? parts.shift() : chan
|
||
if (!pchan) {
|
||
out.usage = '/part [#channel] [reason]'
|
||
return out
|
||
}
|
||
client.part(pchan, parts.join(' '))
|
||
return out
|
||
}
|
||
if (cmd === 'cycle' || cmd === 'hop') {
|
||
var cchan = parts[0] || chan
|
||
if (!cchan) {
|
||
out.usage = '/cycle [#channel]'
|
||
return out
|
||
}
|
||
client.part(cchan, 'cycling')
|
||
client.join(cchan)
|
||
return out
|
||
}
|
||
if (cmd === 'query' || cmd === 'q') {
|
||
if (!parts[0]) {
|
||
out.usage = '/query NICK [text]'
|
||
return out
|
||
}
|
||
var qb = bareIrcEnsureBuffer(client.state, 'query', parts[0])
|
||
client.state.currentId = qb.id
|
||
if (parts.slice(1).join(' '))
|
||
client.sendPrivmsg(parts[0], parts.slice(1).join(' '))
|
||
return out
|
||
}
|
||
if (cmd === 'msg' || cmd === 'privmsg' || cmd === 'm') {
|
||
if (!parts[0] || !parts.slice(1).join(' ')) {
|
||
out.usage = '/msg TARGET text'
|
||
return out
|
||
}
|
||
client.sendPrivmsg(parts[0], parts.slice(1).join(' '))
|
||
return out
|
||
}
|
||
if (cmd === 'notice') {
|
||
if (!parts[0] || !parts.slice(1).join(' ')) {
|
||
out.usage = '/notice TARGET text'
|
||
return out
|
||
}
|
||
client.sendRaw('NOTICE', [parts[0], parts.slice(1).join(' ')])
|
||
return out
|
||
}
|
||
if (cmd === 'nick') {
|
||
if (!parts[0]) {
|
||
out.usage = '/nick NEWNICK'
|
||
return out
|
||
}
|
||
client.sendRaw('NICK', [parts[0]])
|
||
return out
|
||
}
|
||
if (cmd === 'whois') {
|
||
if (!parts[0]) {
|
||
out.usage = '/whois NICK'
|
||
return out
|
||
}
|
||
client.sendRaw(
|
||
'WHOIS',
|
||
parts.length > 1 ? [parts[0], parts[1]] : [parts[0]]
|
||
)
|
||
return out
|
||
}
|
||
if (cmd === 'whowas') {
|
||
if (!parts[0]) {
|
||
out.usage = '/whowas NICK'
|
||
return out
|
||
}
|
||
client.sendRaw('WHOWAS', [parts[0]])
|
||
return out
|
||
}
|
||
if (cmd === 'who') {
|
||
client.sendRaw('WHO', [parts[0] || chan || '*'])
|
||
return out
|
||
}
|
||
if (cmd === 'names') {
|
||
client.sendRaw('NAMES', [parts[0] || chan || ''].filter(Boolean))
|
||
return out
|
||
}
|
||
if (cmd === 'topic') {
|
||
var tchan = chan
|
||
var topic = rest
|
||
if (
|
||
parts[0] &&
|
||
(parts[0].charAt(0) === '#' || parts[0].charAt(0) === '&')
|
||
) {
|
||
tchan = parts[0]
|
||
topic = parts.slice(1).join(' ')
|
||
}
|
||
if (!tchan) {
|
||
out.usage = '/topic [#channel] [text]'
|
||
return out
|
||
}
|
||
if (topic) client.sendRaw('TOPIC', [tchan, topic])
|
||
else client.sendRaw('TOPIC', [tchan])
|
||
return out
|
||
}
|
||
if (cmd === 'me' || cmd === 'action') {
|
||
if (!rest || !chan) {
|
||
out.usage = '/me action text (in a channel or query)'
|
||
return out
|
||
}
|
||
client.sendPrivmsg(chan, '\x01ACTION ' + rest + '\x01')
|
||
return out
|
||
}
|
||
if (cmd === 'away') {
|
||
client.sendRaw('AWAY', rest ? [rest] : [])
|
||
return out
|
||
}
|
||
if (cmd === 'back') {
|
||
client.sendRaw('AWAY', [])
|
||
return out
|
||
}
|
||
if (cmd === 'quote' || cmd === 'raw') {
|
||
if (!rest) {
|
||
out.usage = '/quote COMMAND [params]'
|
||
return out
|
||
}
|
||
var qparts = rest.split(/\s+/)
|
||
client.sendRaw(qparts[0], qparts.slice(1))
|
||
return out
|
||
}
|
||
if (cmd === 'list' || cmd === 'rooms' || cmd === 'browse') {
|
||
out.rooms = true
|
||
out.roomsQuery = rest
|
||
return out
|
||
}
|
||
if (cmd === 'motd') {
|
||
client.sendRaw('MOTD', parts[0] ? [parts[0]] : [])
|
||
return out
|
||
}
|
||
if (cmd === 'lusers') {
|
||
client.sendRaw('LUSERS', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'admin') {
|
||
client.sendRaw('ADMIN', parts[0] ? [parts[0]] : [])
|
||
return out
|
||
}
|
||
if (cmd === 'info') {
|
||
client.sendRaw('INFO', parts[0] ? [parts[0]] : [])
|
||
return out
|
||
}
|
||
if (cmd === 'time') {
|
||
client.sendRaw('TIME', parts[0] ? [parts[0]] : [])
|
||
return out
|
||
}
|
||
if (cmd === 'links') {
|
||
client.sendRaw('LINKS', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'map') {
|
||
client.sendRaw('MAP', [])
|
||
return out
|
||
}
|
||
if (cmd === 'rules') {
|
||
client.sendRaw('RULES', [])
|
||
return out
|
||
}
|
||
if (cmd === 'stats') {
|
||
client.sendRaw('STATS', parts[0] ? parts : ['u'])
|
||
return out
|
||
}
|
||
if (cmd === 'version') {
|
||
if (parts[0] && parts[0].charAt(0) !== '#') {
|
||
client.sendRaw('PRIVMSG', [parts[0], '\x01VERSION\x01'])
|
||
return out
|
||
}
|
||
client.sendRaw('VERSION', parts[0] ? [parts[0]] : [])
|
||
return out
|
||
}
|
||
if (cmd === 'userhost') {
|
||
if (!parts[0]) {
|
||
out.usage = '/userhost NICK [NICK...]'
|
||
return out
|
||
}
|
||
client.sendRaw('USERHOST', parts.slice(0, 5))
|
||
return out
|
||
}
|
||
if (cmd === 'ison') {
|
||
if (!parts[0]) {
|
||
out.usage = '/ison NICK [NICK...]'
|
||
return out
|
||
}
|
||
client.sendRaw('ISON', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'mode') {
|
||
if (!parts[0]) {
|
||
out.usage = '/mode TARGET [modes] [args]'
|
||
return out
|
||
}
|
||
client.sendRaw('MODE', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'umode') {
|
||
client.sendRaw(
|
||
'MODE',
|
||
[client.state.nick].concat(parts.length ? parts : [])
|
||
)
|
||
return out
|
||
}
|
||
if (cmd === 'kick') {
|
||
var kchan = chan
|
||
var knick = parts[0]
|
||
var kreason = parts.slice(1).join(' ')
|
||
if (
|
||
parts[0] &&
|
||
(parts[0].charAt(0) === '#' || parts[0].charAt(0) === '&')
|
||
) {
|
||
kchan = parts[0]
|
||
knick = parts[1]
|
||
kreason = parts.slice(2).join(' ')
|
||
}
|
||
if (!kchan || !knick) {
|
||
out.usage = '/kick [channel] NICK [reason]'
|
||
return out
|
||
}
|
||
client.sendRaw('KICK', kreason ? [kchan, knick, kreason] : [kchan, knick])
|
||
return out
|
||
}
|
||
if (cmd === 'invite') {
|
||
if (!parts[0]) {
|
||
out.usage = '/invite NICK [#channel]'
|
||
return out
|
||
}
|
||
var ichan = parts[1] || chan
|
||
if (!ichan) {
|
||
out.usage = '/invite NICK #channel'
|
||
return out
|
||
}
|
||
client.sendRaw('INVITE', [parts[0], ichan])
|
||
return out
|
||
}
|
||
if (cmd === 'ban') {
|
||
if (!chan) {
|
||
out.usage = '/ban [mask] (in a channel)'
|
||
return out
|
||
}
|
||
if (!parts[0]) client.sendRaw('MODE', [chan, '+b'])
|
||
else client.sendRaw('MODE', [chan, '+b', parts[0]])
|
||
return out
|
||
}
|
||
if (cmd === 'unban') {
|
||
if (!chan || !parts[0]) {
|
||
out.usage = '/unban mask (in a channel)'
|
||
return out
|
||
}
|
||
client.sendRaw('MODE', [chan, '-b', parts[0]])
|
||
return out
|
||
}
|
||
if (cmd === 'op') {
|
||
if (!chan || !parts[0]) {
|
||
out.usage = '/op NICK [NICK...] (in a channel)'
|
||
return out
|
||
}
|
||
bareIrcModeNicks(client, chan, '+o', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'deop') {
|
||
if (!chan || !parts[0]) {
|
||
out.usage = '/deop NICK [NICK...]'
|
||
return out
|
||
}
|
||
bareIrcModeNicks(client, chan, '-o', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'voice') {
|
||
if (!chan || !parts[0]) {
|
||
out.usage = '/voice NICK [NICK...]'
|
||
return out
|
||
}
|
||
bareIrcModeNicks(client, chan, '+v', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'devoice') {
|
||
if (!chan || !parts[0]) {
|
||
out.usage = '/devoice NICK [NICK...]'
|
||
return out
|
||
}
|
||
bareIrcModeNicks(client, chan, '-v', parts)
|
||
return out
|
||
}
|
||
if (cmd === 'knock') {
|
||
if (!parts[0]) {
|
||
out.usage = '/knock #channel [message]'
|
||
return out
|
||
}
|
||
client.sendRaw(
|
||
'KNOCK',
|
||
parts.slice(1).join(' ')
|
||
? [parts[0], parts.slice(1).join(' ')]
|
||
: [parts[0]]
|
||
)
|
||
return out
|
||
}
|
||
if (cmd === 'ctcp') {
|
||
if (!parts[0] || !parts[1]) {
|
||
out.usage = '/ctcp NICK CMD [args]'
|
||
return out
|
||
}
|
||
var ctcp =
|
||
parts[1].toUpperCase() +
|
||
(parts.slice(2).join(' ') ? ' ' + parts.slice(2).join(' ') : '')
|
||
client.sendRaw('PRIVMSG', [parts[0], '\x01' + ctcp + '\x01'])
|
||
return out
|
||
}
|
||
if (cmd === 'ping') {
|
||
if (parts[0]) {
|
||
client.sendRaw('PRIVMSG', [parts[0], '\x01PING ' + Date.now() + '\x01'])
|
||
} else {
|
||
client.sendRaw('PING', [client.state.nick || 'irc'])
|
||
}
|
||
return out
|
||
}
|
||
if (cmd === 'ignore') {
|
||
if (!parts[0]) {
|
||
var keys = Object.keys(client.state.ignore || {})
|
||
out.note = keys.length
|
||
? 'ignoring: ' + keys.join(', ')
|
||
: 'ignore list empty'
|
||
return out
|
||
}
|
||
if (!client.state.ignore) client.state.ignore = {}
|
||
client.state.ignore[
|
||
bareIrcCasemap(parts[0], bareIrcCasemapping(client.state.isupport))
|
||
] = parts[0]
|
||
out.note = 'ignoring ' + parts[0]
|
||
return out
|
||
}
|
||
if (cmd === 'unignore') {
|
||
if (!parts[0]) {
|
||
out.usage = '/unignore NICK'
|
||
return out
|
||
}
|
||
if (client.state.ignore) {
|
||
delete client.state.ignore[
|
||
bareIrcCasemap(parts[0], bareIrcCasemapping(client.state.isupport))
|
||
]
|
||
}
|
||
out.note = 'unignored ' + parts[0]
|
||
return out
|
||
}
|
||
if (cmd === 'clear') {
|
||
out.clear = true
|
||
return out
|
||
}
|
||
if (cmd === 'close' || cmd === 'wc') {
|
||
out.close = true
|
||
return out
|
||
}
|
||
if (cmd === 'win' || cmd === 'buffer' || cmd === 'window') {
|
||
var n = parseInt(parts[0], 10)
|
||
var list = bareIrcListBuffers(client.state)
|
||
if (n >= 1 && n <= list.length) client.state.currentId = list[n - 1].id
|
||
else out.usage = '/win N (1–' + list.length + ')'
|
||
return out
|
||
}
|
||
if (cmd === 'p2p') {
|
||
if (parts[0] === 'open' && parts[1]) {
|
||
var name = String(parts[1])
|
||
if (name.indexOf('[p2p]') < 0) name = name + ' [p2p]'
|
||
var pb = bareIrcEnsureBuffer(client.state, 'p2p', name)
|
||
client.state.currentId = pb.id
|
||
bareIrcPushLine(client.state, pb, {
|
||
atMs: Date.now(),
|
||
kind: 'info',
|
||
text: 'Shadow room — not Libera. Messages stay on the swarm sideband.'
|
||
})
|
||
if (ctx && typeof ctx.bareOsIrcShadowOpen === 'function') {
|
||
try {
|
||
ctx.bareOsIrcShadowOpen(parts[1])
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
out.usage = '/p2p open #name'
|
||
return out
|
||
}
|
||
if (cmd === 'oper') {
|
||
if (parts.length < 2) {
|
||
out.usage = '/oper USER PASS'
|
||
return out
|
||
}
|
||
client.sendRaw('OPER', [parts[0], parts[1]])
|
||
return out
|
||
}
|
||
if (cmd === 'squery') {
|
||
if (!parts[0] || !parts.slice(1).join(' ')) {
|
||
out.usage = '/squery SERVICE text'
|
||
return out
|
||
}
|
||
client.sendRaw('SQUERY', [parts[0], parts.slice(1).join(' ')])
|
||
return out
|
||
}
|
||
if (cmd === 'set') {
|
||
out.note = 'config is ~/.irc/config.json (nick, autojoin, saslUser)'
|
||
return out
|
||
}
|
||
if (cmd === 'server') {
|
||
out.note =
|
||
(client.state.network || 'irc') +
|
||
' ' +
|
||
(client.opts.host || '') +
|
||
':' +
|
||
(client.opts.port || '')
|
||
return out
|
||
}
|
||
|
||
out.ok = false
|
||
out.usage = 'unknown command /' + cmd + ' try /help'
|
||
return out
|
||
}
|
||
|
||
/** TEA IRC TUI. Cell buffer; protocol lives in irc-client.js. */
|
||
|
||
function bareIrcPad(s, w) {
|
||
s = String(s == null ? '' : s)
|
||
if (w <= 0) return ''
|
||
if (s.length > w) return s.slice(0, w)
|
||
while (s.length < w) s += ' '
|
||
return s
|
||
}
|
||
|
||
function bareIrcNickColWidth(cols, buf, roomsOpen) {
|
||
if (roomsOpen || !buf || buf.kind !== 'channel') return 0
|
||
if (cols < 80) return 0
|
||
return cols >= 140 ? 20 : 16
|
||
}
|
||
|
||
function bareIrcMemberLabel(m, key) {
|
||
var pref =
|
||
m && m.modes && m.modes.indexOf('o') >= 0
|
||
? '@'
|
||
: m && m.modes && m.modes.indexOf('v') >= 0
|
||
? '+'
|
||
: ' '
|
||
return pref + (m && m.nick ? m.nick : key)
|
||
}
|
||
|
||
function bareIrcFmtClock(ms) {
|
||
var d = new Date(typeof ms === 'number' ? ms : Date.now())
|
||
function z(n) {
|
||
return (n < 10 ? '0' : '') + n
|
||
}
|
||
return z(d.getHours()) + ':' + z(d.getMinutes())
|
||
}
|
||
|
||
function bareIrcCreateTuiApp(ctx, client) {
|
||
var tui = ctx.tui
|
||
var size0 = tui && typeof tui.size === 'function' ? tui.size() : {}
|
||
return {
|
||
client: client,
|
||
width: size0.width || 80,
|
||
height: size0.height || 24,
|
||
help: false,
|
||
quitConfirm: false,
|
||
roomsOpen: false,
|
||
roomsCursor: 0,
|
||
roomsFilterEdit: false,
|
||
openedRoomsOnce: false,
|
||
filterOpen: false,
|
||
status: 'connecting…',
|
||
input: tui.textinput.create({ prompt: '', focused: true, charLimit: 450 }),
|
||
viewport: tui.viewport.create({
|
||
width: size0.width || 80,
|
||
height: Math.max(6, (size0.height || 24) - 6)
|
||
}),
|
||
init: function () {
|
||
var self = this
|
||
client.on('registered', function () {
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('privmsg', function () {
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('notice', function () {
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('sasl', function (p) {
|
||
self.status = p && p.ok ? 'sasl ok' : 'sasl failed'
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('error', function (p) {
|
||
self.status =
|
||
'error: ' +
|
||
((p && (p.text || (p.error && p.error.message))) || 'error')
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('close', function () {
|
||
self.status = 'disconnected'
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('join', function () {
|
||
self.roomsOpen = false
|
||
self.roomsFilterEdit = false
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('list', function () {
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('listend', function (p) {
|
||
self.status = 'rooms ' + ((p && p.count) || 0)
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
client.on('motd_end', function () {
|
||
var joins = (client.opts && client.opts.autojoin) || []
|
||
if (!self.openedRoomsOnce && (!joins || !joins.length)) {
|
||
self.openedRoomsOnce = true
|
||
self.roomsOpen = true
|
||
self.roomsCursor = 0
|
||
self.status = 'pick a room (Enter join / filter Esc close)'
|
||
}
|
||
var srv = bareIrcEnsureBuffer(client.state, 'server', '*server')
|
||
bareIrcPushLine(client.state, srv, {
|
||
atMs: Date.now(),
|
||
kind: 'info',
|
||
text:
|
||
'You are ' +
|
||
(client.state.nick || 'guest') +
|
||
'. /rooms browse /join #libera help /query nick ? keys'
|
||
})
|
||
if (tui && typeof tui.send === 'function')
|
||
tui.send({ type: 'irc.paint' })
|
||
})
|
||
this._sync()
|
||
return null
|
||
},
|
||
_roomRows: function () {
|
||
var listed = this.client.state.channelList || []
|
||
if (listed.length) return listed
|
||
return BARE_IRC_FEATURED_ROOMS
|
||
},
|
||
_openRooms: function (query) {
|
||
this.roomsOpen = true
|
||
this.roomsCursor = 0
|
||
this.roomsFilterEdit = false
|
||
if (query) this.client.list(query)
|
||
this.status = query
|
||
? 'listing ' + query
|
||
: 'featured rooms (type /rooms linux to search)'
|
||
},
|
||
_joinRoomAtCursor: function () {
|
||
var rows = this._roomRows()
|
||
var hit = rows[this.roomsCursor]
|
||
if (!hit || !hit.name) return
|
||
this.roomsOpen = false
|
||
this.roomsFilterEdit = false
|
||
this.client.join(hit.name)
|
||
this.status = 'joining ' + hit.name
|
||
},
|
||
_current: function () {
|
||
var st = this.client.state
|
||
return (
|
||
st.buffers[st.currentId] || bareIrcEnsureBuffer(st, 'server', '*server')
|
||
)
|
||
},
|
||
_sync: function () {
|
||
var buf = this._current()
|
||
buf.unread = 0
|
||
buf.highlight = false
|
||
var cols = Math.max(40, this.width || 80)
|
||
var nickW = bareIrcNickColWidth(cols, buf, this.roomsOpen)
|
||
this.viewport.width = Math.max(20, cols - (nickW ? nickW + 1 : 0))
|
||
this.viewport.height = Math.max(4, (this.height || 24) - 6)
|
||
if (this.roomsOpen) {
|
||
var rows = this._roomRows()
|
||
if (this.roomsCursor >= rows.length)
|
||
this.roomsCursor = Math.max(0, rows.length - 1)
|
||
var rlines = []
|
||
var ri
|
||
for (ri = 0; ri < rows.length; ri++) {
|
||
var rr = rows[ri]
|
||
var mark = ri === this.roomsCursor ? '>' : ' '
|
||
var users =
|
||
rr.users === '' || rr.users == null
|
||
? ' '
|
||
: String(rr.users).padStart
|
||
? String(rr.users).padStart(4, ' ')
|
||
: String(rr.users)
|
||
rlines.push(
|
||
mark +
|
||
' ' +
|
||
String(rr.name || '')
|
||
.slice(0, 18)
|
||
.padEnd(18, ' ') +
|
||
' ' +
|
||
users +
|
||
' ' +
|
||
String(rr.topic || '')
|
||
)
|
||
}
|
||
if (!rlines.length) rlines.push('(no rooms — /rooms linux to search)')
|
||
this.viewport.setContent(rlines.join('\n'))
|
||
var mid = Math.floor((this.viewport.height || 8) / 2)
|
||
this.viewport.setYOffset(Math.max(0, this.roomsCursor - mid))
|
||
return
|
||
}
|
||
var lines = []
|
||
for (var i = 0; i < buf.lines.length; i++) {
|
||
var rec = buf.lines[i]
|
||
var clock = bareIrcFmtClock(rec.atMs)
|
||
if (rec.kind === 'action') {
|
||
lines.push(clock + ' * ' + rec.from + ' ' + rec.text)
|
||
} else if (rec.kind === 'notice') {
|
||
lines.push(clock + ' -' + rec.from + '- ' + rec.text)
|
||
} else if (rec.kind === 'privmsg') {
|
||
lines.push(clock + ' <' + rec.from + '> ' + rec.text)
|
||
} else {
|
||
lines.push(clock + ' ' + (rec.text || rec.command || ''))
|
||
}
|
||
}
|
||
this.viewport.setContent(lines.join('\n'))
|
||
this.viewport.gotoBottom()
|
||
},
|
||
_nextBuffer: function (dir) {
|
||
var list = bareIrcListBuffers(this.client.state)
|
||
if (!list.length) return
|
||
var cur = 0
|
||
for (var i = 0; i < list.length; i++) {
|
||
if (list[i].id === this.client.state.currentId) cur = i
|
||
}
|
||
var n = (cur + dir + list.length) % list.length
|
||
this.client.state.currentId = list[n].id
|
||
this._sync()
|
||
},
|
||
_completeNick: function () {
|
||
var buf = this._current()
|
||
var val = this.input.value || ''
|
||
var m = /(?:^|\s)(\S+)$/.exec(val)
|
||
var prefix = m ? m[1] : ''
|
||
if (!prefix) return
|
||
var mapping = bareIrcCasemapping(this.client.state.isupport)
|
||
var keys = Object.keys(buf.members || {})
|
||
var hit = ''
|
||
for (var i = 0; i < keys.length; i++) {
|
||
var mem = buf.members[keys[i]]
|
||
var nick = mem && mem.nick ? mem.nick : keys[i]
|
||
if (
|
||
bareIrcCasemap(nick, mapping).indexOf(
|
||
bareIrcCasemap(prefix, mapping)
|
||
) === 0
|
||
) {
|
||
hit = nick
|
||
break
|
||
}
|
||
}
|
||
if (!hit) return
|
||
this.input.setValue(val.slice(0, val.length - prefix.length) + hit)
|
||
},
|
||
_applySlash: function (res) {
|
||
var buf = this._current()
|
||
if (res.help) this.help = true
|
||
if (res.rooms) this._openRooms(res.roomsQuery || '')
|
||
if (res.note) {
|
||
bareIrcPushLine(this.client.state, buf, {
|
||
atMs: Date.now(),
|
||
kind: 'info',
|
||
text: res.note
|
||
})
|
||
}
|
||
if (res.usage) {
|
||
bareIrcPushLine(this.client.state, buf, {
|
||
atMs: Date.now(),
|
||
kind: 'info',
|
||
text: res.usage
|
||
})
|
||
}
|
||
if (res.clear && buf) buf.lines = []
|
||
if (res.close) {
|
||
if (buf.kind === 'channel') this.client.part(buf.name, '')
|
||
var id = buf.id
|
||
if (id !== 'server') {
|
||
delete this.client.state.buffers[id]
|
||
this.client.state.currentId = 'server'
|
||
}
|
||
}
|
||
if (res.quit) return tui.quit
|
||
this._sync()
|
||
return null
|
||
},
|
||
_runSlash: function (line) {
|
||
var self = this
|
||
var res = bareIrcRunSlash(this.client, line, {
|
||
buf: this._current(),
|
||
ctx: ctx,
|
||
reconnect: function () {
|
||
try {
|
||
if (self.client.opts) {
|
||
self.client.attach(bareIrcDial(ctx, self.client.opts))
|
||
self.status = 'reconnecting\u2026'
|
||
}
|
||
} catch (err) {
|
||
self.status = 'reconnect failed'
|
||
}
|
||
}
|
||
})
|
||
return this._applySlash(res)
|
||
},
|
||
update: function (msg) {
|
||
if (msg && msg.type === 'resize') {
|
||
this.width = msg.width || this.width
|
||
this.height = msg.height || this.height
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
if (msg && msg.type === 'irc.paint') {
|
||
this.status = this.client.state.registered
|
||
? this.client.state.saslOk
|
||
? 'sasl ok'
|
||
: 'registered'
|
||
: this.client.state.connection
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
if (this.help) {
|
||
if (msg && msg.type === 'key') this.help = false
|
||
return [this, null]
|
||
}
|
||
if (this.roomsOpen) {
|
||
if (this.roomsFilterEdit) {
|
||
if (tui.key.matches(msg, 'escape')) {
|
||
this.roomsFilterEdit = false
|
||
this.input.reset()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'enter')) {
|
||
var q = String(this.input.value || '').trim()
|
||
this.input.reset()
|
||
this.roomsFilterEdit = false
|
||
if (q) this.client.list(q)
|
||
this.status = q ? 'listing ' + q : 'rooms'
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
var rpair = this.input.update(msg)
|
||
this.input = rpair[0]
|
||
return [this, rpair[1]]
|
||
}
|
||
if (tui.key.matches(msg, 'escape', 'q')) {
|
||
this.roomsOpen = false
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, '/', 'f')) {
|
||
this.roomsFilterEdit = true
|
||
this.input.reset()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'enter')) {
|
||
this._joinRoomAtCursor()
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'j', 'down')) {
|
||
this.roomsCursor++
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'k', 'up')) {
|
||
this.roomsCursor = Math.max(0, this.roomsCursor - 1)
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'pageup', 'pagedown')) {
|
||
var step = this.viewport.height || 10
|
||
if (tui.key.matches(msg, 'pageup'))
|
||
this.roomsCursor = Math.max(0, this.roomsCursor - step)
|
||
else this.roomsCursor += step
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
}
|
||
if (this.quitConfirm) {
|
||
if (tui.key.matches(msg, 'y', 'Y')) {
|
||
this.client.quit('Quit')
|
||
return [this, tui.quit]
|
||
}
|
||
if (msg && msg.type === 'key') this.quitConfirm = false
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'ctrl+c')) {
|
||
this.quitConfirm = true
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'f10')) {
|
||
this.client.quit('Quit')
|
||
return [this, tui.quit]
|
||
}
|
||
if (tui.key.matches(msg, '?')) {
|
||
this.help = true
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'ctrl+l')) {
|
||
this._openRooms('')
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'ctrl+n', 'alt+right')) {
|
||
this._nextBuffer(1)
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'ctrl+p', 'alt+left')) {
|
||
this._nextBuffer(-1)
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'tab')) {
|
||
this._completeNick()
|
||
return [this, null]
|
||
}
|
||
if (tui.key.matches(msg, 'pageup', 'pagedown', 'up', 'down')) {
|
||
var vp = this.viewport.update(msg)
|
||
this.viewport = vp[0]
|
||
return [this, vp[1]]
|
||
}
|
||
if (tui.key.matches(msg, 'enter')) {
|
||
var line = String(this.input.value || '').trim()
|
||
this.input.reset()
|
||
if (!line) return [this, null]
|
||
if (line.charAt(0) === '/') {
|
||
var cmd = this._runSlash(line)
|
||
return [this, cmd]
|
||
}
|
||
var cur = this._current()
|
||
if (cur.kind === 'server') {
|
||
bareIrcPushLine(this.client.state, cur, {
|
||
atMs: Date.now(),
|
||
kind: 'info',
|
||
text: 'join a channel or /query someone first'
|
||
})
|
||
} else if (cur.kind === 'p2p') {
|
||
bareIrcPushLine(this.client.state, cur, {
|
||
atMs: Date.now(),
|
||
kind: 'privmsg',
|
||
from: this.client.state.nick || 'me',
|
||
text: line,
|
||
local: true
|
||
})
|
||
if (typeof ctx.bareOsChatSend === 'function') {
|
||
try {
|
||
ctx.bareOsChatSend(
|
||
'[bare-os-irc-shadow] ' + cur.name + ' ' + line
|
||
)
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} else {
|
||
this.client.sendPrivmsg(cur.name, line)
|
||
}
|
||
this._sync()
|
||
return [this, null]
|
||
}
|
||
var pair = this.input.update(msg)
|
||
this.input = pair[0]
|
||
return [this, pair[1]]
|
||
},
|
||
view: function () {
|
||
var cols = Math.max(40, this.width || 80)
|
||
var rows = Math.max(12, this.height || 24)
|
||
var st = tui.style
|
||
var buf = this._current()
|
||
var net = this.client.state.network || 'irc'
|
||
var tls = this.client.state.tls ? 'tls' : 'plain'
|
||
var titleRaw =
|
||
' ' +
|
||
net +
|
||
' ' +
|
||
buf.name +
|
||
(buf.unread ? ' ● ' + buf.unread : '') +
|
||
' ' +
|
||
tls +
|
||
(this.client.state.saslOk ? ' sasl' : '') +
|
||
' ' +
|
||
this.status +
|
||
' ' +
|
||
cols +
|
||
'x' +
|
||
rows +
|
||
' '
|
||
var title = st
|
||
? st()
|
||
.foreground('brightwhite')
|
||
.background('blue')
|
||
.width(cols)
|
||
.render(titleRaw)
|
||
: titleRaw
|
||
var tabs = bareIrcListBuffers(this.client.state)
|
||
.map(function (b) {
|
||
var mark = b.id === buf.id ? '[' + b.name + ']' : b.name
|
||
if (b.unread) mark += '(' + b.unread + ')'
|
||
return mark
|
||
})
|
||
.join(' ')
|
||
var tabBar = st ? st().dim().width(cols).render(tabs) : tabs
|
||
var nickW = bareIrcNickColWidth(cols, buf, this.roomsOpen)
|
||
var bodyH = this.viewport.height || Math.max(4, rows - 6)
|
||
var body = this.viewport.view()
|
||
if (nickW && buf.kind === 'channel') {
|
||
var keys = Object.keys(buf.members || {})
|
||
keys.sort(function (a, b) {
|
||
var ma = buf.members[a]
|
||
var mb = buf.members[b]
|
||
var pa = ma && ma.modes && ma.modes.indexOf('o') >= 0 ? 0 : 1
|
||
var pb = mb && mb.modes && mb.modes.indexOf('o') >= 0 ? 0 : 1
|
||
if (pa !== pb) return pa - pb
|
||
var na = ma && ma.nick ? ma.nick : a
|
||
var nb = mb && mb.nick ? mb.nick : b
|
||
return na < nb ? -1 : na > nb ? 1 : 0
|
||
})
|
||
var nicks = keys.map(function (k) {
|
||
return bareIrcMemberLabel(buf.members[k], k)
|
||
})
|
||
var left = String(body).split('\n')
|
||
var chatW = Math.max(8, cols - nickW - 1)
|
||
var joined = []
|
||
var r
|
||
for (r = 0; r < bodyH; r++) {
|
||
var L = bareIrcPad(
|
||
st ? st.truncate(left[r] || '', chatW) : left[r] || '',
|
||
chatW
|
||
)
|
||
var R = bareIrcPad(nicks[r] || '', nickW)
|
||
joined.push(L + '\u2502' + R)
|
||
}
|
||
body = joined.join('\n')
|
||
}
|
||
var prompt
|
||
if (this.roomsOpen && this.roomsFilterEdit) {
|
||
prompt =
|
||
'Filter: ' +
|
||
(this.input.view ? this.input.view() : this.input.value || '')
|
||
} else {
|
||
prompt =
|
||
(buf.kind === 'p2p' ? '#p2p ' : '') +
|
||
'[' +
|
||
(this.roomsOpen ? 'rooms' : buf.name) +
|
||
'] ' +
|
||
(this.input.view ? this.input.view() : this.input.value || '')
|
||
}
|
||
var footRaw = this.roomsOpen
|
||
? this.roomsFilterEdit
|
||
? 'Filter + Enter search LIST Esc back'
|
||
: 'Enter join j/k move / filter Esc close featured or /rooms linux'
|
||
: 'Enter send /rooms /join /query Ctrl+L rooms Ctrl+n/p Tab nick ?'
|
||
var foot = st ? st().dim().width(cols).render(footRaw) : footRaw
|
||
var rule = st
|
||
? st()
|
||
.dim()
|
||
.width(cols)
|
||
.render('\u2500'.repeat(Math.min(cols, 120)))
|
||
: ''
|
||
var lines = [title, tabBar, rule]
|
||
.concat(String(body).split('\n'))
|
||
.concat([rule, prompt, foot])
|
||
while (lines.length < rows) lines.push('')
|
||
var out = []
|
||
for (var i = 0; i < rows; i++) {
|
||
out.push(
|
||
st
|
||
? st.truncate(lines[i] || '', cols)
|
||
: String(lines[i] || '').slice(0, cols)
|
||
)
|
||
}
|
||
return out.join('\n')
|
||
},
|
||
overlay: function (size) {
|
||
if (!this.help && !this.quitConfirm) return null
|
||
var cols = (size && size.width) || this.width || 80
|
||
var rows = (size && size.height) || this.height || 24
|
||
var st = tui.style
|
||
var body = this.quitConfirm
|
||
? 'Disconnect and quit?\n\nY yes any other key cancel'
|
||
: 'Bare OS irc\n\n' +
|
||
'/rooms [pat] browse/list /join #chan /part /query nick\n' +
|
||
'/msg /notice /me /nick /whois /whowas /who /names /topic\n' +
|
||
'/kick /invite /op /deop /voice /mode /ban /away /back\n' +
|
||
'/list /motd /quote /ctcp /ping /ignore /clear /close /quit\n' +
|
||
'/p2p open #chan shadow room (not Libera)\n' +
|
||
'Ctrl+L rooms Ctrl+n/p buffers Tab nick\n\n' +
|
||
'Default: irc.libera.chat:6697 TLS random nick if unset\n' +
|
||
'Press any key to close.'
|
||
var boxed = st
|
||
? st()
|
||
.border(st.borders.rounded)
|
||
.padding(1, 2)
|
||
.background('black')
|
||
.render(body)
|
||
: body
|
||
var h = st ? st.height(boxed) : boxed.split('\n').length
|
||
var w = st ? st.width(boxed) : 48
|
||
return {
|
||
row: Math.max(0, Math.floor((rows - h) / 2)),
|
||
col: Math.max(0, Math.floor((cols - w) / 2)),
|
||
text: boxed
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function bareIrcTuiRunOpts() {
|
||
return { buffer: 'cell' }
|
||
}
|
||
|
||
/**
|
||
* Modern IRC client. Default: Libera.Chat TLS irc.libera.chat:6697.
|
||
* TTY → ctx.tui; otherwise scriptable: status | send | join | query | whois.
|
||
*/
|
||
function ircUsage(argv0) {
|
||
return (
|
||
'usage: ' +
|
||
(argv0 || 'irc') +
|
||
' [libera|HOST] [--tls|--plain] [--insecure-plain] [--nick N] [--sasl-user U]\n' +
|
||
' ' +
|
||
(argv0 || 'irc') +
|
||
' send TARGET TEXT | join #chan | query NICK [TEXT] | whois NICK | status\n' +
|
||
'Default network: irc.libera.chat:6697 (TLS). Config: ~/.irc/config.json\n' +
|
||
'See man irc.'
|
||
)
|
||
}
|
||
|
||
async function bareIrcEnsureLiveClient(ctx, spec) {
|
||
if (
|
||
ctx._bareIrcClient &&
|
||
ctx._bareIrcClient.state.connection === 'registered'
|
||
) {
|
||
return ctx._bareIrcClient
|
||
}
|
||
var sock = bareIrcDial(ctx, spec)
|
||
var client = bareIrcCreateClient(spec)
|
||
client.attach(sock)
|
||
ctx._bareIrcClient = client
|
||
return client
|
||
}
|
||
|
||
function bareIrcWaitRegistered(client, ms) {
|
||
return new Promise(function (resolve, reject) {
|
||
if (client.state.registered) {
|
||
resolve(client)
|
||
return
|
||
}
|
||
var t = setTimeout(function () {
|
||
reject(new Error('irc: register timeout'))
|
||
}, ms || 20000)
|
||
client.on('registered', function () {
|
||
clearTimeout(t)
|
||
resolve(client)
|
||
})
|
||
client.on('error', function (p) {
|
||
clearTimeout(t)
|
||
reject(
|
||
p && p.error ? p.error : new Error(p && p.text ? p.text : 'irc error')
|
||
)
|
||
})
|
||
})
|
||
}
|
||
|
||
async function run(ctx, argv) {
|
||
var flags = bareIrcParseFlags(argv)
|
||
if (flags.help) {
|
||
ctx.console.log(ircUsage(argv[0]))
|
||
return
|
||
}
|
||
|
||
var loaded = await bareIrcLoadConfig(ctx)
|
||
var spec = bareIrcResolveConnect(ctx, flags, loaded)
|
||
var sub = flags.rest[0] || ''
|
||
|
||
if (sub === 'status') {
|
||
var c = ctx._bareIrcClient
|
||
var info = {
|
||
connection: c ? c.state.connection : 'idle',
|
||
nick: c ? c.state.nick : spec.nick,
|
||
host: spec.host,
|
||
port: spec.port,
|
||
tls: spec.tls,
|
||
network: spec.network
|
||
}
|
||
ctx.console.log(
|
||
flags.json ? JSON.stringify(info) : JSON.stringify(info, null, 2)
|
||
)
|
||
return
|
||
}
|
||
|
||
if (sub === 'send' || sub === 'join' || sub === 'query' || sub === 'whois') {
|
||
try {
|
||
var client = await bareIrcEnsureLiveClient(ctx, spec)
|
||
await bareIrcWaitRegistered(client, 25000)
|
||
if (sub === 'send') {
|
||
var target = flags.rest[1]
|
||
var text = flags.rest.slice(2).join(' ')
|
||
if (!target || !text) {
|
||
ctx.console.error('irc: send TARGET TEXT')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
if (!client.sendPrivmsg(target, text)) {
|
||
ctx.console.error('irc: send failed (rate limit or no socket)')
|
||
ctx.exitCode = 1
|
||
}
|
||
return
|
||
}
|
||
if (sub === 'join') {
|
||
if (!flags.rest[1]) {
|
||
ctx.console.error('irc: join #channel')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
client.join(flags.rest[1])
|
||
return
|
||
}
|
||
if (sub === 'query') {
|
||
if (!flags.rest[1]) {
|
||
ctx.console.error('irc: query NICK [TEXT]')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
if (flags.rest[2])
|
||
client.sendPrivmsg(flags.rest[1], flags.rest.slice(2).join(' '))
|
||
return
|
||
}
|
||
if (sub === 'whois') {
|
||
if (!flags.rest[1]) {
|
||
ctx.console.error('irc: whois NICK')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
client.sendRaw('WHOIS', [flags.rest[1]])
|
||
return
|
||
}
|
||
} catch (e) {
|
||
ctx.console.error('irc: ' + ((e && e.message) || String(e)))
|
||
ctx.exitCode = 1
|
||
}
|
||
return
|
||
}
|
||
|
||
var stdin = ctx.replStdin
|
||
var isTTY = Boolean(stdin && stdin.isTTY)
|
||
if (!isTTY || !ctx.tui || typeof ctx.tui.run !== 'function') {
|
||
ctx.console.error(
|
||
'irc: TTY + ctx.tui required for full-screen mode; use: irc send TARGET TEXT'
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
try {
|
||
if (typeof ctx.bareOsIrcHostAllowed === 'function') {
|
||
var allow = ctx.bareOsIrcHostAllowed(spec.host)
|
||
if (allow && allow.ok === false) {
|
||
ctx.console.error(
|
||
'irc: host blocked (' + (allow.reason || 'policy') + '): ' + spec.host
|
||
)
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
}
|
||
var socket = bareIrcDial(ctx, spec)
|
||
var live = bareIrcCreateClient(spec)
|
||
live.attach(socket)
|
||
live.on('registered', function () {
|
||
if (spec.generatedNick && live.state.nick) {
|
||
bareIrcRememberNick(ctx, live.state.nick)
|
||
}
|
||
var joins = spec.autojoin || []
|
||
for (var i = 0; i < joins.length; i++) live.join(joins[i])
|
||
if (typeof ctx.bareOsIrcPresencePublish === 'function') {
|
||
try {
|
||
ctx.bareOsIrcPresencePublish({
|
||
schema: 1,
|
||
network: spec.network || 'libera',
|
||
nick: live.state.nick,
|
||
account: live.state.account || '',
|
||
channels: spec.shareChannels ? joins : [],
|
||
atMs: Date.now()
|
||
})
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
})
|
||
var app = bareIrcCreateTuiApp(ctx, live)
|
||
try {
|
||
await ctx.tui.run(app, bareIrcTuiRunOpts())
|
||
} finally {
|
||
try {
|
||
live.quit('Client closed')
|
||
} catch (e2) {
|
||
/* ignore */
|
||
}
|
||
}
|
||
} catch (err) {
|
||
ctx.console.error('irc: ' + ((err && err.message) || String(err)))
|
||
ctx.exitCode = 1
|
||
}
|
||
}
|