Add irc
Release rolling / release (push) Successful in 9m31s

This commit is contained in:
Raven Scott
2026-08-12 23:50:49 -04:00
parent bbfe873414
commit 1187e30cf6
53 changed files with 6693 additions and 32 deletions
+449
View File
@@ -0,0 +1,449 @@
/** TEA IRC TUI. Cell buffer; protocol lives in irc-client.js. */
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,
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('join', 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' })
})
this._sync()
return null
},
_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 = cols >= 100 ? 16 : 0
this.viewport.width = Math.max(20, cols - nickW - 2)
this.viewport.height = Math.max(4, (this.height || 24) - 6)
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)
},
_runSlash: function (line) {
var parts = line.slice(1).split(/\s+/)
var cmd = (parts[0] || '').toLowerCase()
var rest = line.slice(1 + (parts[0] || '').length).trim()
var buf = this._current()
if (cmd === 'join' && parts[1]) this.client.join(parts[1], parts[2])
else if (cmd === 'part')
this.client.part(parts[1] || buf.name, rest.replace(/^\S+\s*/, ''))
else if (cmd === 'query' && parts[1]) {
var q = bareIrcEnsureBuffer(this.client.state, 'query', parts[1])
this.client.state.currentId = q.id
if (parts.slice(2).join(' ')) {
this.client.sendPrivmsg(parts[1], parts.slice(2).join(' '))
}
} else if ((cmd === 'msg' || cmd === 'privmsg') && parts[1]) {
this.client.sendPrivmsg(parts[1], parts.slice(2).join(' '))
} else if (cmd === 'notice' && parts[1]) {
this.client.sendRaw('NOTICE', [parts[1], parts.slice(2).join(' ')])
} else if (cmd === 'nick' && parts[1])
this.client.sendRaw('NICK', [parts[1]])
else if (cmd === 'whois' && parts[1])
this.client.sendRaw('WHOIS', [parts[1]])
else if (cmd === 'who' && parts[1]) this.client.sendRaw('WHO', [parts[1]])
else if (cmd === 'topic') {
if (rest) this.client.sendRaw('TOPIC', [buf.name, rest])
else this.client.sendRaw('TOPIC', [buf.name])
} else if (cmd === 'me' && rest) {
this.client.sendPrivmsg(buf.name, '\x01ACTION ' + rest + '\x01')
} else if (cmd === 'away') this.client.sendRaw('AWAY', rest ? [rest] : [])
else if (cmd === 'quit') {
this.client.quit(rest || 'Leaving')
return tui.quit
} else if (cmd === 'quote' && rest) {
var qparts = rest.split(/\s+/)
this.client.sendRaw(qparts[0], qparts.slice(1))
} else if (cmd === 'disconnect') this.client.quit('Disconnect')
else if (cmd === 'reconnect') {
try {
if (typeof ctx.bareOsTlsConnect === 'function' && this.client.opts) {
var spec = this.client.opts
var sock =
spec.tls === false
? ctx.bareOsTelnetConnect(spec.host, spec.port, {})
: ctx.bareOsTlsConnect(spec.host, spec.port, {
servername: spec.host,
rejectUnauthorized: true
})
this.client.attach(sock)
this.status = 'reconnecting\u2026'
}
} catch (err) {
this.status = 'reconnect failed'
}
} else if (cmd === 'win' || cmd === 'buffer') {
var n = parseInt(parts[1], 10)
var list = bareIrcListBuffers(this.client.state)
if (n >= 1 && n <= list.length) {
this.client.state.currentId = list[n - 1].id
}
} else if (cmd === 'p2p' && parts[1] === 'open' && parts[2]) {
var name = String(parts[2])
if (name.indexOf('[p2p]') < 0) name = name + ' [p2p]'
var pb = bareIrcEnsureBuffer(this.client.state, 'p2p', name)
this.client.state.currentId = pb.id
bareIrcPushLine(this.client.state, pb, {
atMs: Date.now(),
kind: 'info',
text: 'Shadow room — not Libera. Messages stay on the swarm sideband.'
})
if (typeof ctx.bareOsIrcShadowOpen === 'function') {
try {
ctx.bareOsIrcShadowOpen(parts[2])
} catch (e) {
/* ignore */
}
}
} else if (cmd === 'help') {
this.help = true
} else {
bareIrcPushLine(this.client.state, buf, {
atMs: Date.now(),
kind: 'info',
text: 'unknown command /' + cmd
})
}
this._sync()
return null
},
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.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+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 = cols >= 100 ? 16 : 0
var body = this.viewport.view()
if (nickW) {
var nicks = Object.keys(buf.members || {})
.map(function (k) {
var m = buf.members[k]
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 : k)
})
.slice(0, this.viewport.height || 10)
while (nicks.length < (this.viewport.height || 0)) nicks.push('')
var left = String(body).split('\n')
var joined = []
var h = Math.max(left.length, nicks.length)
for (var r = 0; r < h; r++) {
var L = st
? st.truncate(left[r] || '', cols - nickW - 1)
: (left[r] || '').slice(0, cols - nickW - 1)
var R = st
? st.truncate(nicks[r] || '', nickW)
: (nicks[r] || '').slice(0, nickW)
joined.push(L + ' ' + R)
}
body = joined.join('\n')
}
var prompt =
(buf.kind === 'p2p' ? '#p2p ' : '') +
'[' +
buf.name +
'] ' +
(this.input.view ? this.input.view() : this.input.value || '')
var footRaw =
'Enter send /join /query /quit Ctrl+n/p buffers Tab nick ? help'
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' +
'/join #chan /part /query nick /msg nick text\n' +
'/nick /whois /me /away /quote /quit\n' +
'/p2p open #chan shadow room (not Libera)\n' +
'Ctrl+n / Ctrl+p next buffer Tab complete\n\n' +
'Default network: irc.libera.chat:6697 TLS\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' }
}