/* 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} 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} 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} 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} 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} 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} 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, 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, 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 } }