IRC Commands
Release rolling / release (push) Canceled after 8m43s

This commit is contained in:
Raven Scott
2026-08-13 00:33:57 -04:00
parent 15afc148d7
commit 8bb8254c42
22 changed files with 3799 additions and 1228 deletions
@@ -0,0 +1,500 @@
/** 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
}