kokoirc 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/docs/commands/kill.md +24 -0
- package/docs/commands/oper.md +24 -0
- package/docs/commands/quote.md +29 -0
- package/docs/commands/server.md +6 -0
- package/docs/commands/stats.md +31 -0
- package/docs/commands/wallops.md +24 -0
- package/package.json +1 -1
- package/src/core/commands/registry.ts +72 -1
- package/src/core/config/loader.ts +1 -0
- package/src/core/irc/events.ts +61 -8
- package/src/types/config.ts +1 -0
package/README.md
CHANGED
|
@@ -36,8 +36,8 @@ A modern terminal IRC client built with [OpenTUI](https://github.com/anomalyco/o
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
git clone https://github.com/kofany/
|
|
40
|
-
cd
|
|
39
|
+
git clone https://github.com/kofany/kokoIRC.git
|
|
40
|
+
cd kokoIRC
|
|
41
41
|
bun install
|
|
42
42
|
```
|
|
43
43
|
|
|
@@ -74,7 +74,8 @@ address = "irc.libera.chat"
|
|
|
74
74
|
port = 6697
|
|
75
75
|
tls = true
|
|
76
76
|
autoconnect = true
|
|
77
|
-
channels = ["#kokoirc"]
|
|
77
|
+
channels = ["#kokoirc", "#secret mykey"] # "channel key" syntax for keyed channels
|
|
78
|
+
autosendcmd = "MSG NickServ identify pass; WAIT 2000; MODE $N +i"
|
|
78
79
|
# sasl_user = "mynick"
|
|
79
80
|
# sasl_pass = "hunter2"
|
|
80
81
|
|
|
@@ -91,16 +92,18 @@ j = "/join"
|
|
|
91
92
|
|
|
92
93
|
## Commands
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
38 built-in commands. Type `/help` for the full list, `/help <command>` for details.
|
|
95
96
|
|
|
96
97
|
| Category | Commands |
|
|
97
98
|
|----------|----------|
|
|
98
99
|
| Connection | `/connect`, `/disconnect`, `/quit`, `/server` |
|
|
99
100
|
| Channel | `/join`, `/part`, `/close`, `/topic`, `/list` |
|
|
100
|
-
| Messaging | `/msg`, `/me`, `/notice`, `/action`, `/slap` |
|
|
101
|
-
| Moderation | `/kick`, `/ban`, `/unban`, `/kb`, `/mode` |
|
|
101
|
+
| Messaging | `/msg`, `/me`, `/notice`, `/action`, `/slap`, `/wallops` |
|
|
102
|
+
| Moderation | `/kick`, `/ban`, `/unban`, `/kb`, `/kill`, `/mode` |
|
|
102
103
|
| Nick/Ops | `/nick`, `/op`, `/deop`, `/voice`, `/devoice` |
|
|
103
104
|
| User | `/whois`, `/wii`, `/ignore`, `/unignore` |
|
|
105
|
+
| Info | `/stats` |
|
|
106
|
+
| Server | `/quote` (`/raw`), `/oper` |
|
|
104
107
|
| Config | `/set`, `/alias`, `/unalias`, `/reload` |
|
|
105
108
|
| Logging | `/log status`, `/log search <query>` |
|
|
106
109
|
| Scripts | `/script load`, `/script unload`, `/script list` |
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Moderation
|
|
3
|
+
description: Disconnect a user from the network
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /kill
|
|
7
|
+
|
|
8
|
+
## Syntax
|
|
9
|
+
|
|
10
|
+
/kill <nick> [reason]
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
14
|
+
Forcefully disconnect a user from the IRC network. This is an operator-only
|
|
15
|
+
command. If no reason is given, the target's nick is used as the reason.
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
/kill spammer Spamming is not allowed
|
|
20
|
+
/kill baduser
|
|
21
|
+
|
|
22
|
+
## See Also
|
|
23
|
+
|
|
24
|
+
/oper, /kick, /ban
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Server
|
|
3
|
+
description: Authenticate as an IRC operator
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /oper
|
|
7
|
+
|
|
8
|
+
## Syntax
|
|
9
|
+
|
|
10
|
+
/oper <name> <password>
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
14
|
+
Authenticate as an IRC operator. Requires valid operator credentials
|
|
15
|
+
configured on the server. Once authenticated, you gain access to
|
|
16
|
+
operator commands like /kill and /wallops.
|
|
17
|
+
|
|
18
|
+
## Examples
|
|
19
|
+
|
|
20
|
+
/oper admin secretpass
|
|
21
|
+
|
|
22
|
+
## See Also
|
|
23
|
+
|
|
24
|
+
/kill, /wallops
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Server
|
|
3
|
+
description: Send a raw IRC command
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /quote
|
|
7
|
+
|
|
8
|
+
## Syntax
|
|
9
|
+
|
|
10
|
+
/quote <raw command>
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
14
|
+
Send a raw IRC command directly to the server. This is useful for
|
|
15
|
+
commands not yet implemented, testing, or advanced protocol interaction.
|
|
16
|
+
|
|
17
|
+
## Aliases
|
|
18
|
+
|
|
19
|
+
/raw
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
/quote VERSION
|
|
24
|
+
/raw LUSERS
|
|
25
|
+
/quote PRIVMSG #channel :hello
|
|
26
|
+
|
|
27
|
+
## See Also
|
|
28
|
+
|
|
29
|
+
/stats
|
package/docs/commands/server.md
CHANGED
|
@@ -39,6 +39,12 @@ Add a new server to the configuration.
|
|
|
39
39
|
- `-label=<name>` — Display name
|
|
40
40
|
- `-password=<pass>` — Server password (saved to .env)
|
|
41
41
|
- `-sasl=<user>:<pass>` — SASL auth (saved to .env)
|
|
42
|
+
- `-autosendcmd=<cmds>` — Commands to run on connect, before autojoin (must be last flag)
|
|
43
|
+
|
|
44
|
+
Autosendcmd uses erssi-style syntax: commands separated by `;`, with `WAIT <ms>`
|
|
45
|
+
for delays. `$N` is replaced with your nick.
|
|
46
|
+
|
|
47
|
+
/server add libera irc.libera.chat:6697 -tls -autosendcmd=MSG NickServ identify pass; WAIT 2000; MODE $N +i
|
|
42
48
|
|
|
43
49
|
### remove
|
|
44
50
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Info
|
|
3
|
+
description: Request server statistics
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /stats
|
|
7
|
+
|
|
8
|
+
## Syntax
|
|
9
|
+
|
|
10
|
+
/stats [type] [server]
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
14
|
+
Request statistics from the IRC server. Common stat types include:
|
|
15
|
+
|
|
16
|
+
- `u` — server uptime
|
|
17
|
+
- `m` — command usage counts
|
|
18
|
+
- `o` — configured operators
|
|
19
|
+
- `l` — connection information
|
|
20
|
+
|
|
21
|
+
If no type is given, the server may return a summary or help text.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
/stats
|
|
26
|
+
/stats u
|
|
27
|
+
/stats o irc.libera.chat
|
|
28
|
+
|
|
29
|
+
## See Also
|
|
30
|
+
|
|
31
|
+
/quote
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: Messaging
|
|
3
|
+
description: Send a message to all IRC operators
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /wallops
|
|
7
|
+
|
|
8
|
+
## Syntax
|
|
9
|
+
|
|
10
|
+
/wallops <message>
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
14
|
+
Send a message to all connected IRC operators. This is an operator-only
|
|
15
|
+
command typically used for server-wide announcements among staff.
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
/wallops Server maintenance in 10 minutes
|
|
20
|
+
/wallops New oper guidelines posted on the wiki
|
|
21
|
+
|
|
22
|
+
## See Also
|
|
23
|
+
|
|
24
|
+
/oper, /quote
|
package/package.json
CHANGED
|
@@ -411,7 +411,7 @@ export const commands: Record<string, CommandDef> = {
|
|
|
411
411
|
const id = args[1]?.toLowerCase()
|
|
412
412
|
const addrArg = args[2]
|
|
413
413
|
if (!id || !addrArg) {
|
|
414
|
-
addLocalEvent(`%Zf7768eUsage: /server add <id> <address>[:<port>] [-tls] [-noauto] [-bind=<ip>] [-label=<name>] [-password=<pass>] [-sasl=<user>:<pass>]%N`)
|
|
414
|
+
addLocalEvent(`%Zf7768eUsage: /server add <id> <address>[:<port>] [-tls] [-noauto] [-bind=<ip>] [-label=<name>] [-password=<pass>] [-sasl=<user>:<pass>] [-autosendcmd=<cmds>]%N`)
|
|
415
415
|
return
|
|
416
416
|
}
|
|
417
417
|
|
|
@@ -435,6 +435,7 @@ export const commands: Record<string, CommandDef> = {
|
|
|
435
435
|
let sasl_pass: string | undefined
|
|
436
436
|
let label = id
|
|
437
437
|
let tls_verify = true
|
|
438
|
+
let autosendcmd: string | undefined
|
|
438
439
|
|
|
439
440
|
for (let i = 3; i < args.length; i++) {
|
|
440
441
|
const a = args[i]
|
|
@@ -450,6 +451,11 @@ export const commands: Record<string, CommandDef> = {
|
|
|
450
451
|
sasl_user = saslParts[0]
|
|
451
452
|
sasl_pass = saslParts.slice(1).join(":")
|
|
452
453
|
}
|
|
454
|
+
else if (a.startsWith("-autosendcmd=")) {
|
|
455
|
+
// Consumes rest of args since the value contains spaces
|
|
456
|
+
autosendcmd = [a.slice(13), ...args.slice(i + 1)].join(" ").replace(/^"|"$/g, "")
|
|
457
|
+
break
|
|
458
|
+
}
|
|
453
459
|
}
|
|
454
460
|
|
|
455
461
|
const serverConfig: ServerConfig = {
|
|
@@ -463,6 +469,7 @@ export const commands: Record<string, CommandDef> = {
|
|
|
463
469
|
nick,
|
|
464
470
|
bind_ip,
|
|
465
471
|
sasl_user,
|
|
472
|
+
autosendcmd,
|
|
466
473
|
}
|
|
467
474
|
|
|
468
475
|
const s = useStore.getState()
|
|
@@ -1371,6 +1378,70 @@ export const commands: Record<string, CommandDef> = {
|
|
|
1371
1378
|
description: "Disconnect from a server",
|
|
1372
1379
|
usage: "/disconnect [server-id] [message]",
|
|
1373
1380
|
},
|
|
1381
|
+
|
|
1382
|
+
quote: {
|
|
1383
|
+
handler(args, connId) {
|
|
1384
|
+
const client = getClient(connId)
|
|
1385
|
+
if (!client || args.length === 0) {
|
|
1386
|
+
addLocalEvent(`%Zf7768eUsage: /quote <raw command>%N`)
|
|
1387
|
+
return
|
|
1388
|
+
}
|
|
1389
|
+
client.raw(args.join(" "))
|
|
1390
|
+
},
|
|
1391
|
+
aliases: ["raw"],
|
|
1392
|
+
description: "Send a raw IRC command to the server",
|
|
1393
|
+
usage: "/quote <raw command>",
|
|
1394
|
+
},
|
|
1395
|
+
|
|
1396
|
+
stats: {
|
|
1397
|
+
handler(args, connId) {
|
|
1398
|
+
const client = getClient(connId)
|
|
1399
|
+
if (!client) return
|
|
1400
|
+
client.raw("STATS" + (args.length ? " " + args.join(" ") : ""))
|
|
1401
|
+
},
|
|
1402
|
+
description: "Request server statistics",
|
|
1403
|
+
usage: "/stats [type] [server]",
|
|
1404
|
+
},
|
|
1405
|
+
|
|
1406
|
+
oper: {
|
|
1407
|
+
handler(args, connId) {
|
|
1408
|
+
const client = getClient(connId)
|
|
1409
|
+
if (!client || args.length < 2) {
|
|
1410
|
+
addLocalEvent(`%Zf7768eUsage: /oper <name> <password>%N`)
|
|
1411
|
+
return
|
|
1412
|
+
}
|
|
1413
|
+
client.raw(`OPER ${args[0]} ${args[1]}`)
|
|
1414
|
+
},
|
|
1415
|
+
description: "Authenticate as an IRC operator",
|
|
1416
|
+
usage: "/oper <name> <password>",
|
|
1417
|
+
},
|
|
1418
|
+
|
|
1419
|
+
kill: {
|
|
1420
|
+
handler(args, connId) {
|
|
1421
|
+
const client = getClient(connId)
|
|
1422
|
+
if (!client || args.length === 0) {
|
|
1423
|
+
addLocalEvent(`%Zf7768eUsage: /kill <nick> [reason]%N`)
|
|
1424
|
+
return
|
|
1425
|
+
}
|
|
1426
|
+
const reason = args.slice(1).join(" ") || args[0]
|
|
1427
|
+
client.raw(`KILL ${args[0]} :${reason}`)
|
|
1428
|
+
},
|
|
1429
|
+
description: "Disconnect a user from the network (oper only)",
|
|
1430
|
+
usage: "/kill <nick> [reason]",
|
|
1431
|
+
},
|
|
1432
|
+
|
|
1433
|
+
wallops: {
|
|
1434
|
+
handler(args, connId) {
|
|
1435
|
+
const client = getClient(connId)
|
|
1436
|
+
if (!client || args.length === 0) {
|
|
1437
|
+
addLocalEvent(`%Zf7768eUsage: /wallops <message>%N`)
|
|
1438
|
+
return
|
|
1439
|
+
}
|
|
1440
|
+
client.raw(`WALLOPS :${args.join(" ")}`)
|
|
1441
|
+
},
|
|
1442
|
+
description: "Send a message to all opers (oper only)",
|
|
1443
|
+
usage: "/wallops <message>",
|
|
1444
|
+
},
|
|
1374
1445
|
}
|
|
1375
1446
|
|
|
1376
1447
|
// ─── Built-in Alias Resolution ────────────────────────────────
|
|
@@ -120,6 +120,7 @@ function cleanServerForTOML(server: ServerConfig): Record<string, any> {
|
|
|
120
120
|
if (server.auto_reconnect === false) obj.auto_reconnect = false
|
|
121
121
|
if (server.reconnect_delay && server.reconnect_delay !== 30) obj.reconnect_delay = server.reconnect_delay
|
|
122
122
|
if (server.reconnect_max_retries != null) obj.reconnect_max_retries = server.reconnect_max_retries
|
|
123
|
+
if (server.autosendcmd) obj.autosendcmd = server.autosendcmd
|
|
123
124
|
// SASL and password stored in config only if NOT in .env — sasl_user kept, pass stripped
|
|
124
125
|
if (server.sasl_user) obj.sasl_user = server.sasl_user
|
|
125
126
|
// password and sasl_pass NOT saved to TOML — they go to .env
|
package/src/core/irc/events.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { handleNetsplitQuit, handleNetsplitJoin, destroyNetsplitState } from "./
|
|
|
7
7
|
import { shouldSuppressNickFlood, destroyAntifloodState } from "./antiflood"
|
|
8
8
|
import { shouldIgnore } from "./ignore"
|
|
9
9
|
import { eventBus } from "@/core/scripts/event-bus"
|
|
10
|
+
import { parseCommand } from "@/core/commands/parser"
|
|
11
|
+
import { executeCommand } from "@/core/commands/execution"
|
|
10
12
|
|
|
11
13
|
function isChannelTarget(target: string): boolean {
|
|
12
14
|
return target.startsWith("#") || target.startsWith("&") || target.startsWith("+") || target.startsWith("!")
|
|
@@ -53,15 +55,18 @@ export function bindEvents(client: Client, connectionId: string) {
|
|
|
53
55
|
const s = getStore()
|
|
54
56
|
s.updateConnection(connectionId, { status: "connected", nick: event.nick })
|
|
55
57
|
statusMsg(`%Z9ece6aRegistered as %Zc0caf5${event.nick}%N`)
|
|
56
|
-
// Auto-join channels from config
|
|
57
58
|
const config = s.config
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
const serverConfig = config
|
|
60
|
+
? Object.entries(config.servers).find(([id]) => id === connectionId)?.[1]
|
|
61
|
+
: undefined
|
|
62
|
+
|
|
63
|
+
// Execute autosendcmd before autojoin (erssi-style: ";"-separated, WAIT <ms> for delays)
|
|
64
|
+
if (serverConfig?.autosendcmd) {
|
|
65
|
+
runAutosendcmd(serverConfig.autosendcmd, connectionId, event.nick, () => {
|
|
66
|
+
autojoinChannels(client, serverConfig)
|
|
67
|
+
})
|
|
68
|
+
} else {
|
|
69
|
+
if (serverConfig) autojoinChannels(client, serverConfig)
|
|
65
70
|
}
|
|
66
71
|
})
|
|
67
72
|
|
|
@@ -1029,3 +1034,51 @@ function makeFormattedEvent(key: string, params: string[]): Message {
|
|
|
1029
1034
|
}
|
|
1030
1035
|
}
|
|
1031
1036
|
|
|
1037
|
+
// ─── Autosendcmd ─────────────────────────────────────────
|
|
1038
|
+
|
|
1039
|
+
/** Join channels from config, supporting "channel key" syntax. */
|
|
1040
|
+
function autojoinChannels(client: Client, serverConfig: import("@/types/config").ServerConfig) {
|
|
1041
|
+
for (const entry of serverConfig.channels) {
|
|
1042
|
+
const spaceIdx = entry.indexOf(" ")
|
|
1043
|
+
if (spaceIdx === -1) {
|
|
1044
|
+
client.join(entry)
|
|
1045
|
+
} else {
|
|
1046
|
+
client.join(entry.slice(0, spaceIdx), entry.slice(spaceIdx + 1))
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Execute autosendcmd string (erssi-style: ";"-separated commands, WAIT <ms> for delays).
|
|
1053
|
+
* Substitutes $N with current nick. Calls onDone() when all commands (including WAITs) complete.
|
|
1054
|
+
*/
|
|
1055
|
+
function runAutosendcmd(cmd: string, connectionId: string, nick: string, onDone: () => void) {
|
|
1056
|
+
const parts = cmd.split(";").map((p) => p.trim()).filter(Boolean)
|
|
1057
|
+
let i = 0
|
|
1058
|
+
|
|
1059
|
+
function next() {
|
|
1060
|
+
while (i < parts.length) {
|
|
1061
|
+
const part = parts[i++]
|
|
1062
|
+
// Variable substitution ($N = nick)
|
|
1063
|
+
const expanded = part.replace(/\$\{?N\}?/g, nick)
|
|
1064
|
+
|
|
1065
|
+
// WAIT <ms> — delay before next command
|
|
1066
|
+
const waitMatch = expanded.match(/^WAIT\s+(\d+)$/i)
|
|
1067
|
+
if (waitMatch) {
|
|
1068
|
+
setTimeout(next, parseInt(waitMatch[1], 10))
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Treat as command: prepend / if missing, parse and execute
|
|
1073
|
+
const line = expanded.startsWith("/") ? expanded : "/" + expanded
|
|
1074
|
+
const parsed = parseCommand(line)
|
|
1075
|
+
if (parsed) {
|
|
1076
|
+
executeCommand(parsed, connectionId)
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
onDone()
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
next()
|
|
1083
|
+
}
|
|
1084
|
+
|
package/src/types/config.ts
CHANGED
|
@@ -123,4 +123,5 @@ export interface ServerConfig {
|
|
|
123
123
|
auto_reconnect?: boolean // auto reconnect on disconnect (default: true)
|
|
124
124
|
reconnect_delay?: number // seconds between reconnect attempts (default: 30)
|
|
125
125
|
reconnect_max_retries?: number // max reconnect attempts (default: 10)
|
|
126
|
+
autosendcmd?: string // commands to run on connect, before autojoin (;-separated, WAIT <ms> for delays)
|
|
126
127
|
}
|