ScuttleBot

Merge pull request #134 from ConflictHQ/feature/86-wire-bot-commands feat: wire command router into all 10 system bots

noreply 2026-04-05 17:51 trunk merge
Commit e8d318d9d3a13a71c484ad84a549efe3d4f716b53a2d19dda0ff7abae6950cd3
--- internal/bots/auditbot/auditbot.go
+++ internal/bots/auditbot/auditbot.go
@@ -20,10 +20,11 @@
2020
"strings"
2121
"time"
2222
2323
"github.com/lrstanley/girc"
2424
25
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2526
"github.com/conflicthq/scuttlebot/pkg/protocol"
2627
)
2728
2829
const botNick = "auditbot"
2930
@@ -133,17 +134,30 @@
133134
if ch := e.Last(); strings.HasPrefix(ch, "#") {
134135
cl.Cmd.Join(ch)
135136
}
136137
})
137138
138
- c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
139
+ router := cmdparse.NewRouter(botNick)
140
+ router.Register(cmdparse.Command{
141
+ Name: "query",
142
+ Usage: "QUERY <nick|#channel>",
143
+ Description: "show recent audit events for a nick or channel",
144
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
145
+ })
146
+
147
+ c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
139148
if len(e.Params) < 1 {
140149
return
150
+ }
151
+ // Dispatch commands (DMs and channel messages).
152
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
153
+ cl.Cmd.Message(reply.Target, reply.Text)
154
+ return
141155
}
142156
channel := e.Params[0]
143157
if !strings.HasPrefix(channel, "#") {
144
- return // ignore DMs
158
+ return // non-command DMs ignored
145159
}
146160
text := e.Last()
147161
env, err := protocol.Unmarshal([]byte(text))
148162
if err != nil {
149163
return // non-envelope PRIVMSG ignored
150164
--- internal/bots/auditbot/auditbot.go
+++ internal/bots/auditbot/auditbot.go
@@ -20,10 +20,11 @@
20 "strings"
21 "time"
22
23 "github.com/lrstanley/girc"
24
 
25 "github.com/conflicthq/scuttlebot/pkg/protocol"
26 )
27
28 const botNick = "auditbot"
29
@@ -133,17 +134,30 @@
133 if ch := e.Last(); strings.HasPrefix(ch, "#") {
134 cl.Cmd.Join(ch)
135 }
136 })
137
138 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
 
 
 
 
 
 
 
 
139 if len(e.Params) < 1 {
140 return
 
 
 
 
 
141 }
142 channel := e.Params[0]
143 if !strings.HasPrefix(channel, "#") {
144 return // ignore DMs
145 }
146 text := e.Last()
147 env, err := protocol.Unmarshal([]byte(text))
148 if err != nil {
149 return // non-envelope PRIVMSG ignored
150
--- internal/bots/auditbot/auditbot.go
+++ internal/bots/auditbot/auditbot.go
@@ -20,10 +20,11 @@
20 "strings"
21 "time"
22
23 "github.com/lrstanley/girc"
24
25 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
26 "github.com/conflicthq/scuttlebot/pkg/protocol"
27 )
28
29 const botNick = "auditbot"
30
@@ -133,17 +134,30 @@
134 if ch := e.Last(); strings.HasPrefix(ch, "#") {
135 cl.Cmd.Join(ch)
136 }
137 })
138
139 router := cmdparse.NewRouter(botNick)
140 router.Register(cmdparse.Command{
141 Name: "query",
142 Usage: "QUERY <nick|#channel>",
143 Description: "show recent audit events for a nick or channel",
144 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
145 })
146
147 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
148 if len(e.Params) < 1 {
149 return
150 }
151 // Dispatch commands (DMs and channel messages).
152 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
153 cl.Cmd.Message(reply.Target, reply.Text)
154 return
155 }
156 channel := e.Params[0]
157 if !strings.HasPrefix(channel, "#") {
158 return // non-command DMs ignored
159 }
160 text := e.Last()
161 env, err := protocol.Unmarshal([]byte(text))
162 if err != nil {
163 return // non-envelope PRIVMSG ignored
164
--- internal/bots/herald/herald.go
+++ internal/bots/herald/herald.go
@@ -17,10 +17,12 @@
1717
"strings"
1818
"sync"
1919
"time"
2020
2121
"github.com/lrstanley/girc"
22
+
23
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2224
)
2325
2426
const botNick = "herald"
2527
2628
// Event is a notification pushed to herald for delivery.
@@ -165,10 +167,35 @@
165167
c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
166168
if ch := e.Last(); strings.HasPrefix(ch, "#") {
167169
cl.Cmd.Join(ch)
168170
}
169171
})
172
+
173
+ router := cmdparse.NewRouter(botNick)
174
+ router.Register(cmdparse.Command{
175
+ Name: "status",
176
+ Usage: "STATUS",
177
+ Description: "show webhook endpoint status and recent events",
178
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
179
+ })
180
+ router.Register(cmdparse.Command{
181
+ Name: "test",
182
+ Usage: "TEST #channel",
183
+ Description: "send a test event to a channel",
184
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
185
+ })
186
+
187
+ c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
188
+ if len(e.Params) < 1 || e.Source == nil {
189
+ return
190
+ }
191
+ // Dispatch commands (DMs and channel messages).
192
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
193
+ cl.Cmd.Message(reply.Target, reply.Text)
194
+ return
195
+ }
196
+ })
170197
171198
b.client = c
172199
173200
errCh := make(chan error, 1)
174201
go func() {
175202
--- internal/bots/herald/herald.go
+++ internal/bots/herald/herald.go
@@ -17,10 +17,12 @@
17 "strings"
18 "sync"
19 "time"
20
21 "github.com/lrstanley/girc"
 
 
22 )
23
24 const botNick = "herald"
25
26 // Event is a notification pushed to herald for delivery.
@@ -165,10 +167,35 @@
165 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
166 if ch := e.Last(); strings.HasPrefix(ch, "#") {
167 cl.Cmd.Join(ch)
168 }
169 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
171 b.client = c
172
173 errCh := make(chan error, 1)
174 go func() {
175
--- internal/bots/herald/herald.go
+++ internal/bots/herald/herald.go
@@ -17,10 +17,12 @@
17 "strings"
18 "sync"
19 "time"
20
21 "github.com/lrstanley/girc"
22
23 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
24 )
25
26 const botNick = "herald"
27
28 // Event is a notification pushed to herald for delivery.
@@ -165,10 +167,35 @@
167 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
168 if ch := e.Last(); strings.HasPrefix(ch, "#") {
169 cl.Cmd.Join(ch)
170 }
171 })
172
173 router := cmdparse.NewRouter(botNick)
174 router.Register(cmdparse.Command{
175 Name: "status",
176 Usage: "STATUS",
177 Description: "show webhook endpoint status and recent events",
178 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
179 })
180 router.Register(cmdparse.Command{
181 Name: "test",
182 Usage: "TEST #channel",
183 Description: "send a test event to a channel",
184 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
185 })
186
187 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
188 if len(e.Params) < 1 || e.Source == nil {
189 return
190 }
191 // Dispatch commands (DMs and channel messages).
192 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
193 cl.Cmd.Message(reply.Target, reply.Text)
194 return
195 }
196 })
197
198 b.client = c
199
200 errCh := make(chan error, 1)
201 go func() {
202
--- internal/bots/oracle/oracle.go
+++ internal/bots/oracle/oracle.go
@@ -21,10 +21,11 @@
2121
"sync"
2222
"time"
2323
2424
"github.com/lrstanley/girc"
2525
26
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2627
"github.com/conflicthq/scuttlebot/pkg/chathistory"
2728
"github.com/conflicthq/scuttlebot/pkg/toon"
2829
)
2930
3031
const (
@@ -188,14 +189,26 @@
188189
if ch := e.Last(); strings.HasPrefix(ch, "#") {
189190
cl.Cmd.Join(ch)
190191
}
191192
})
192193
193
- // Only handle DMs — oracle ignores channel messages.
194
+ router := cmdparse.NewRouter(botNick)
195
+ router.Register(cmdparse.Command{
196
+ Name: "summarize",
197
+ Usage: "SUMMARIZE [#channel] [duration]",
198
+ Description: "summarize recent channel activity",
199
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
200
+ })
201
+
194202
c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
195203
if len(e.Params) < 1 || e.Source == nil {
196204
return
205
+ }
206
+ // Dispatch commands (DMs and channel messages).
207
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
208
+ cl.Cmd.Message(reply.Target, reply.Text)
209
+ return
197210
}
198211
target := e.Params[0]
199212
if strings.HasPrefix(target, "#") {
200213
return // channel message — ignore
201214
}
202215
--- internal/bots/oracle/oracle.go
+++ internal/bots/oracle/oracle.go
@@ -21,10 +21,11 @@
21 "sync"
22 "time"
23
24 "github.com/lrstanley/girc"
25
 
26 "github.com/conflicthq/scuttlebot/pkg/chathistory"
27 "github.com/conflicthq/scuttlebot/pkg/toon"
28 )
29
30 const (
@@ -188,14 +189,26 @@
188 if ch := e.Last(); strings.HasPrefix(ch, "#") {
189 cl.Cmd.Join(ch)
190 }
191 })
192
193 // Only handle DMs — oracle ignores channel messages.
 
 
 
 
 
 
 
194 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
195 if len(e.Params) < 1 || e.Source == nil {
196 return
 
 
 
 
 
197 }
198 target := e.Params[0]
199 if strings.HasPrefix(target, "#") {
200 return // channel message — ignore
201 }
202
--- internal/bots/oracle/oracle.go
+++ internal/bots/oracle/oracle.go
@@ -21,10 +21,11 @@
21 "sync"
22 "time"
23
24 "github.com/lrstanley/girc"
25
26 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
27 "github.com/conflicthq/scuttlebot/pkg/chathistory"
28 "github.com/conflicthq/scuttlebot/pkg/toon"
29 )
30
31 const (
@@ -188,14 +189,26 @@
189 if ch := e.Last(); strings.HasPrefix(ch, "#") {
190 cl.Cmd.Join(ch)
191 }
192 })
193
194 router := cmdparse.NewRouter(botNick)
195 router.Register(cmdparse.Command{
196 Name: "summarize",
197 Usage: "SUMMARIZE [#channel] [duration]",
198 Description: "summarize recent channel activity",
199 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
200 })
201
202 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
203 if len(e.Params) < 1 || e.Source == nil {
204 return
205 }
206 // Dispatch commands (DMs and channel messages).
207 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
208 cl.Cmd.Message(reply.Target, reply.Text)
209 return
210 }
211 target := e.Params[0]
212 if strings.HasPrefix(target, "#") {
213 return // channel message — ignore
214 }
215
--- internal/bots/scribe/scribe.go
+++ internal/bots/scribe/scribe.go
@@ -15,10 +15,11 @@
1515
"strings"
1616
"time"
1717
1818
"github.com/lrstanley/girc"
1919
20
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2021
"github.com/conflicthq/scuttlebot/pkg/protocol"
2122
)
2223
2324
const botNick = "scribe"
2425
@@ -76,19 +77,38 @@
7677
c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
7778
if ch := e.Last(); strings.HasPrefix(ch, "#") {
7879
cl.Cmd.Join(ch)
7980
}
8081
})
82
+
83
+ router := cmdparse.NewRouter(botNick)
84
+ router.Register(cmdparse.Command{
85
+ Name: "search",
86
+ Usage: "SEARCH <term>",
87
+ Description: "search channel logs",
88
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
89
+ })
90
+ router.Register(cmdparse.Command{
91
+ Name: "stats",
92
+ Usage: "STATS",
93
+ Description: "show channel message statistics",
94
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
95
+ })
8196
8297
// Log PRIVMSG — the agent message stream.
8398
c.Handlers.AddBg(girc.PRIVMSG, func(client *girc.Client, e girc.Event) {
8499
if len(e.Params) < 1 || e.Source == nil {
85100
return
101
+ }
102
+ // Dispatch commands (DMs and channel messages).
103
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
104
+ client.Cmd.Message(reply.Target, reply.Text)
105
+ return
86106
}
87107
channel := e.Params[0]
88108
if !strings.HasPrefix(channel, "#") {
89
- return // ignore DMs to scribe itself
109
+ return // non-command DMs ignored
90110
}
91111
text := e.Last()
92112
nick := e.Source.Name
93113
b.writeEntry(channel, nick, text)
94114
})
95115
--- internal/bots/scribe/scribe.go
+++ internal/bots/scribe/scribe.go
@@ -15,10 +15,11 @@
15 "strings"
16 "time"
17
18 "github.com/lrstanley/girc"
19
 
20 "github.com/conflicthq/scuttlebot/pkg/protocol"
21 )
22
23 const botNick = "scribe"
24
@@ -76,19 +77,38 @@
76 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
77 if ch := e.Last(); strings.HasPrefix(ch, "#") {
78 cl.Cmd.Join(ch)
79 }
80 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
82 // Log PRIVMSG — the agent message stream.
83 c.Handlers.AddBg(girc.PRIVMSG, func(client *girc.Client, e girc.Event) {
84 if len(e.Params) < 1 || e.Source == nil {
85 return
 
 
 
 
 
86 }
87 channel := e.Params[0]
88 if !strings.HasPrefix(channel, "#") {
89 return // ignore DMs to scribe itself
90 }
91 text := e.Last()
92 nick := e.Source.Name
93 b.writeEntry(channel, nick, text)
94 })
95
--- internal/bots/scribe/scribe.go
+++ internal/bots/scribe/scribe.go
@@ -15,10 +15,11 @@
15 "strings"
16 "time"
17
18 "github.com/lrstanley/girc"
19
20 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
21 "github.com/conflicthq/scuttlebot/pkg/protocol"
22 )
23
24 const botNick = "scribe"
25
@@ -76,19 +77,38 @@
77 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
78 if ch := e.Last(); strings.HasPrefix(ch, "#") {
79 cl.Cmd.Join(ch)
80 }
81 })
82
83 router := cmdparse.NewRouter(botNick)
84 router.Register(cmdparse.Command{
85 Name: "search",
86 Usage: "SEARCH <term>",
87 Description: "search channel logs",
88 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
89 })
90 router.Register(cmdparse.Command{
91 Name: "stats",
92 Usage: "STATS",
93 Description: "show channel message statistics",
94 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
95 })
96
97 // Log PRIVMSG — the agent message stream.
98 c.Handlers.AddBg(girc.PRIVMSG, func(client *girc.Client, e girc.Event) {
99 if len(e.Params) < 1 || e.Source == nil {
100 return
101 }
102 // Dispatch commands (DMs and channel messages).
103 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
104 client.Cmd.Message(reply.Target, reply.Text)
105 return
106 }
107 channel := e.Params[0]
108 if !strings.HasPrefix(channel, "#") {
109 return // non-command DMs ignored
110 }
111 text := e.Last()
112 nick := e.Source.Name
113 b.writeEntry(channel, nick, text)
114 })
115
--- internal/bots/scroll/scroll.go
+++ internal/bots/scroll/scroll.go
@@ -20,10 +20,11 @@
2020
"sync"
2121
"time"
2222
2323
"github.com/lrstanley/girc"
2424
25
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2526
"github.com/conflicthq/scuttlebot/internal/bots/scribe"
2627
"github.com/conflicthq/scuttlebot/pkg/chathistory"
2728
"github.com/conflicthq/scuttlebot/pkg/toon"
2829
)
2930
@@ -93,14 +94,32 @@
9394
}
9495
hasCH := cl.HasCapability("chathistory") || cl.HasCapability("draft/chathistory")
9596
b.log.Info("scroll connected", "channels", b.channels, "chathistory", hasCH)
9697
})
9798
98
- // Only respond to DMs — ignore anything in a channel.
99
+ router := cmdparse.NewRouter(botNick)
100
+ router.Register(cmdparse.Command{
101
+ Name: "replay",
102
+ Usage: "REPLAY [#channel] [count]",
103
+ Description: "replay recent channel messages",
104
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
105
+ })
106
+ router.Register(cmdparse.Command{
107
+ Name: "search",
108
+ Usage: "SEARCH [#channel] <term>",
109
+ Description: "search channel history",
110
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
111
+ })
112
+
99113
c.Handlers.AddBg(girc.PRIVMSG, func(client *girc.Client, e girc.Event) {
100114
if len(e.Params) < 1 || e.Source == nil {
101115
return
116
+ }
117
+ // Dispatch commands (DMs and channel messages).
118
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
119
+ client.Cmd.Message(reply.Target, reply.Text)
120
+ return
102121
}
103122
target := e.Params[0]
104123
if strings.HasPrefix(target, "#") {
105124
return // channel message, ignore
106125
}
107126
--- internal/bots/scroll/scroll.go
+++ internal/bots/scroll/scroll.go
@@ -20,10 +20,11 @@
20 "sync"
21 "time"
22
23 "github.com/lrstanley/girc"
24
 
25 "github.com/conflicthq/scuttlebot/internal/bots/scribe"
26 "github.com/conflicthq/scuttlebot/pkg/chathistory"
27 "github.com/conflicthq/scuttlebot/pkg/toon"
28 )
29
@@ -93,14 +94,32 @@
93 }
94 hasCH := cl.HasCapability("chathistory") || cl.HasCapability("draft/chathistory")
95 b.log.Info("scroll connected", "channels", b.channels, "chathistory", hasCH)
96 })
97
98 // Only respond to DMs — ignore anything in a channel.
 
 
 
 
 
 
 
 
 
 
 
 
 
99 c.Handlers.AddBg(girc.PRIVMSG, func(client *girc.Client, e girc.Event) {
100 if len(e.Params) < 1 || e.Source == nil {
101 return
 
 
 
 
 
102 }
103 target := e.Params[0]
104 if strings.HasPrefix(target, "#") {
105 return // channel message, ignore
106 }
107
--- internal/bots/scroll/scroll.go
+++ internal/bots/scroll/scroll.go
@@ -20,10 +20,11 @@
20 "sync"
21 "time"
22
23 "github.com/lrstanley/girc"
24
25 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
26 "github.com/conflicthq/scuttlebot/internal/bots/scribe"
27 "github.com/conflicthq/scuttlebot/pkg/chathistory"
28 "github.com/conflicthq/scuttlebot/pkg/toon"
29 )
30
@@ -93,14 +94,32 @@
94 }
95 hasCH := cl.HasCapability("chathistory") || cl.HasCapability("draft/chathistory")
96 b.log.Info("scroll connected", "channels", b.channels, "chathistory", hasCH)
97 })
98
99 router := cmdparse.NewRouter(botNick)
100 router.Register(cmdparse.Command{
101 Name: "replay",
102 Usage: "REPLAY [#channel] [count]",
103 Description: "replay recent channel messages",
104 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
105 })
106 router.Register(cmdparse.Command{
107 Name: "search",
108 Usage: "SEARCH [#channel] <term>",
109 Description: "search channel history",
110 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
111 })
112
113 c.Handlers.AddBg(girc.PRIVMSG, func(client *girc.Client, e girc.Event) {
114 if len(e.Params) < 1 || e.Source == nil {
115 return
116 }
117 // Dispatch commands (DMs and channel messages).
118 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
119 client.Cmd.Message(reply.Target, reply.Text)
120 return
121 }
122 target := e.Params[0]
123 if strings.HasPrefix(target, "#") {
124 return // channel message, ignore
125 }
126
--- internal/bots/sentinel/sentinel.go
+++ internal/bots/sentinel/sentinel.go
@@ -20,10 +20,12 @@
2020
"strings"
2121
"sync"
2222
"time"
2323
2424
"github.com/lrstanley/girc"
25
+
26
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2527
)
2628
2729
const defaultNick = "sentinel"
2830
2931
// LLMProvider calls a language model to evaluate channel content.
@@ -162,17 +164,42 @@
162164
if ch := e.Last(); strings.HasPrefix(ch, "#") {
163165
cl.Cmd.Join(ch)
164166
}
165167
})
166168
167
- c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
169
+ router := cmdparse.NewRouter(b.cfg.Nick)
170
+ router.Register(cmdparse.Command{
171
+ Name: "report",
172
+ Usage: "REPORT [#channel]",
173
+ Description: "on-demand policy review",
174
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
175
+ })
176
+ router.Register(cmdparse.Command{
177
+ Name: "status",
178
+ Usage: "STATUS",
179
+ Description: "show current incidents",
180
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
181
+ })
182
+ router.Register(cmdparse.Command{
183
+ Name: "dismiss",
184
+ Usage: "DISMISS <incident-id>",
185
+ Description: "dismiss a false positive",
186
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
187
+ })
188
+
189
+ c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
168190
if len(e.Params) < 1 || e.Source == nil {
169191
return
192
+ }
193
+ // Dispatch commands (DMs and channel messages).
194
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
195
+ cl.Cmd.Message(reply.Target, reply.Text)
196
+ return
170197
}
171198
channel := e.Params[0]
172199
if !strings.HasPrefix(channel, "#") {
173
- return // ignore DMs
200
+ return // non-command DMs ignored
174201
}
175202
if channel == b.cfg.ModChannel {
176203
return // don't analyse the mod channel itself
177204
}
178205
nick := e.Source.Name
179206
--- internal/bots/sentinel/sentinel.go
+++ internal/bots/sentinel/sentinel.go
@@ -20,10 +20,12 @@
20 "strings"
21 "sync"
22 "time"
23
24 "github.com/lrstanley/girc"
 
 
25 )
26
27 const defaultNick = "sentinel"
28
29 // LLMProvider calls a language model to evaluate channel content.
@@ -162,17 +164,42 @@
162 if ch := e.Last(); strings.HasPrefix(ch, "#") {
163 cl.Cmd.Join(ch)
164 }
165 })
166
167 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168 if len(e.Params) < 1 || e.Source == nil {
169 return
 
 
 
 
 
170 }
171 channel := e.Params[0]
172 if !strings.HasPrefix(channel, "#") {
173 return // ignore DMs
174 }
175 if channel == b.cfg.ModChannel {
176 return // don't analyse the mod channel itself
177 }
178 nick := e.Source.Name
179
--- internal/bots/sentinel/sentinel.go
+++ internal/bots/sentinel/sentinel.go
@@ -20,10 +20,12 @@
20 "strings"
21 "sync"
22 "time"
23
24 "github.com/lrstanley/girc"
25
26 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
27 )
28
29 const defaultNick = "sentinel"
30
31 // LLMProvider calls a language model to evaluate channel content.
@@ -162,17 +164,42 @@
164 if ch := e.Last(); strings.HasPrefix(ch, "#") {
165 cl.Cmd.Join(ch)
166 }
167 })
168
169 router := cmdparse.NewRouter(b.cfg.Nick)
170 router.Register(cmdparse.Command{
171 Name: "report",
172 Usage: "REPORT [#channel]",
173 Description: "on-demand policy review",
174 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
175 })
176 router.Register(cmdparse.Command{
177 Name: "status",
178 Usage: "STATUS",
179 Description: "show current incidents",
180 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
181 })
182 router.Register(cmdparse.Command{
183 Name: "dismiss",
184 Usage: "DISMISS <incident-id>",
185 Description: "dismiss a false positive",
186 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
187 })
188
189 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
190 if len(e.Params) < 1 || e.Source == nil {
191 return
192 }
193 // Dispatch commands (DMs and channel messages).
194 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
195 cl.Cmd.Message(reply.Target, reply.Text)
196 return
197 }
198 channel := e.Params[0]
199 if !strings.HasPrefix(channel, "#") {
200 return // non-command DMs ignored
201 }
202 if channel == b.cfg.ModChannel {
203 return // don't analyse the mod channel itself
204 }
205 nick := e.Source.Name
206
--- internal/bots/snitch/snitch.go
+++ internal/bots/snitch/snitch.go
@@ -17,10 +17,12 @@
1717
"strings"
1818
"sync"
1919
"time"
2020
2121
"github.com/lrstanley/girc"
22
+
23
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2224
)
2325
2426
const defaultNick = "snitch"
2527
2628
// Config controls snitch's thresholds and alert destination.
@@ -196,14 +198,33 @@
196198
if len(e.Params) < 1 || e.Source == nil {
197199
return
198200
}
199201
b.recordJoinPart(e.Params[0], e.Source.Name)
200202
})
203
+
204
+ router := cmdparse.NewRouter(b.cfg.Nick)
205
+ router.Register(cmdparse.Command{
206
+ Name: "status",
207
+ Usage: "STATUS",
208
+ Description: "show current active alerts",
209
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
210
+ })
211
+ router.Register(cmdparse.Command{
212
+ Name: "acknowledge",
213
+ Usage: "ACKNOWLEDGE <alert-id>",
214
+ Description: "acknowledge an alert",
215
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
216
+ })
201217
202218
c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
203219
if len(e.Params) < 1 || e.Source == nil {
204220
return
221
+ }
222
+ // Dispatch commands (DMs and channel messages).
223
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
224
+ c.Cmd.Message(reply.Target, reply.Text)
225
+ return
205226
}
206227
channel := e.Params[0]
207228
nick := e.Source.Name
208229
if nick == b.cfg.Nick {
209230
return
210231
--- internal/bots/snitch/snitch.go
+++ internal/bots/snitch/snitch.go
@@ -17,10 +17,12 @@
17 "strings"
18 "sync"
19 "time"
20
21 "github.com/lrstanley/girc"
 
 
22 )
23
24 const defaultNick = "snitch"
25
26 // Config controls snitch's thresholds and alert destination.
@@ -196,14 +198,33 @@
196 if len(e.Params) < 1 || e.Source == nil {
197 return
198 }
199 b.recordJoinPart(e.Params[0], e.Source.Name)
200 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
202 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
203 if len(e.Params) < 1 || e.Source == nil {
204 return
 
 
 
 
 
205 }
206 channel := e.Params[0]
207 nick := e.Source.Name
208 if nick == b.cfg.Nick {
209 return
210
--- internal/bots/snitch/snitch.go
+++ internal/bots/snitch/snitch.go
@@ -17,10 +17,12 @@
17 "strings"
18 "sync"
19 "time"
20
21 "github.com/lrstanley/girc"
22
23 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
24 )
25
26 const defaultNick = "snitch"
27
28 // Config controls snitch's thresholds and alert destination.
@@ -196,14 +198,33 @@
198 if len(e.Params) < 1 || e.Source == nil {
199 return
200 }
201 b.recordJoinPart(e.Params[0], e.Source.Name)
202 })
203
204 router := cmdparse.NewRouter(b.cfg.Nick)
205 router.Register(cmdparse.Command{
206 Name: "status",
207 Usage: "STATUS",
208 Description: "show current active alerts",
209 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
210 })
211 router.Register(cmdparse.Command{
212 Name: "acknowledge",
213 Usage: "ACKNOWLEDGE <alert-id>",
214 Description: "acknowledge an alert",
215 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
216 })
217
218 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
219 if len(e.Params) < 1 || e.Source == nil {
220 return
221 }
222 // Dispatch commands (DMs and channel messages).
223 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
224 c.Cmd.Message(reply.Target, reply.Text)
225 return
226 }
227 channel := e.Params[0]
228 nick := e.Source.Name
229 if nick == b.cfg.Nick {
230 return
231
--- internal/bots/steward/steward.go
+++ internal/bots/steward/steward.go
@@ -29,10 +29,12 @@
2929
"strings"
3030
"sync"
3131
"time"
3232
3333
"github.com/lrstanley/girc"
34
+
35
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
3436
)
3537
3638
const defaultNick = "steward"
3739
3840
// Config controls steward's behaviour.
@@ -145,14 +147,39 @@
145147
c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
146148
if ch := e.Last(); strings.HasPrefix(ch, "#") {
147149
cl.Cmd.Join(ch)
148150
}
149151
})
152
+
153
+ router := cmdparse.NewRouter(b.cfg.Nick)
154
+ router.Register(cmdparse.Command{
155
+ Name: "act",
156
+ Usage: "ACT <incident-id>",
157
+ Description: "manually trigger action on incident",
158
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
159
+ })
160
+ router.Register(cmdparse.Command{
161
+ Name: "override",
162
+ Usage: "OVERRIDE <incident-id>",
163
+ Description: "override pending action",
164
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
165
+ })
166
+ router.Register(cmdparse.Command{
167
+ Name: "status",
168
+ Usage: "STATUS",
169
+ Description: "show current pending actions",
170
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
171
+ })
150172
151173
c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
152174
if len(e.Params) < 1 || e.Source == nil {
153175
return
176
+ }
177
+ // Dispatch commands (DMs and channel messages).
178
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
179
+ c.Cmd.Message(reply.Target, reply.Text)
180
+ return
154181
}
155182
target := e.Params[0]
156183
nick := e.Source.Name
157184
text := strings.TrimSpace(e.Last())
158185
159186
--- internal/bots/steward/steward.go
+++ internal/bots/steward/steward.go
@@ -29,10 +29,12 @@
29 "strings"
30 "sync"
31 "time"
32
33 "github.com/lrstanley/girc"
 
 
34 )
35
36 const defaultNick = "steward"
37
38 // Config controls steward's behaviour.
@@ -145,14 +147,39 @@
145 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
146 if ch := e.Last(); strings.HasPrefix(ch, "#") {
147 cl.Cmd.Join(ch)
148 }
149 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
151 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
152 if len(e.Params) < 1 || e.Source == nil {
153 return
 
 
 
 
 
154 }
155 target := e.Params[0]
156 nick := e.Source.Name
157 text := strings.TrimSpace(e.Last())
158
159
--- internal/bots/steward/steward.go
+++ internal/bots/steward/steward.go
@@ -29,10 +29,12 @@
29 "strings"
30 "sync"
31 "time"
32
33 "github.com/lrstanley/girc"
34
35 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
36 )
37
38 const defaultNick = "steward"
39
40 // Config controls steward's behaviour.
@@ -145,14 +147,39 @@
147 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
148 if ch := e.Last(); strings.HasPrefix(ch, "#") {
149 cl.Cmd.Join(ch)
150 }
151 })
152
153 router := cmdparse.NewRouter(b.cfg.Nick)
154 router.Register(cmdparse.Command{
155 Name: "act",
156 Usage: "ACT <incident-id>",
157 Description: "manually trigger action on incident",
158 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
159 })
160 router.Register(cmdparse.Command{
161 Name: "override",
162 Usage: "OVERRIDE <incident-id>",
163 Description: "override pending action",
164 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
165 })
166 router.Register(cmdparse.Command{
167 Name: "status",
168 Usage: "STATUS",
169 Description: "show current pending actions",
170 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
171 })
172
173 c.Handlers.AddBg(girc.PRIVMSG, func(_ *girc.Client, e girc.Event) {
174 if len(e.Params) < 1 || e.Source == nil {
175 return
176 }
177 // Dispatch commands (DMs and channel messages).
178 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
179 c.Cmd.Message(reply.Target, reply.Text)
180 return
181 }
182 target := e.Params[0]
183 nick := e.Source.Name
184 text := strings.TrimSpace(e.Last())
185
186
--- internal/bots/systembot/systembot.go
+++ internal/bots/systembot/systembot.go
@@ -17,10 +17,12 @@
1717
"strconv"
1818
"strings"
1919
"time"
2020
2121
"github.com/lrstanley/girc"
22
+
23
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2224
)
2325
2426
const botNick = "systembot"
2527
2628
// EntryKind classifies a system event.
@@ -177,10 +179,35 @@
177179
if e.Source != nil {
178180
nick = e.Source.Name
179181
}
180182
b.write(Entry{Kind: KindMode, Channel: channel, Nick: nick, Text: strings.Join(e.Params, " ")})
181183
})
184
+
185
+ router := cmdparse.NewRouter(botNick)
186
+ router.Register(cmdparse.Command{
187
+ Name: "status",
188
+ Usage: "STATUS",
189
+ Description: "show connected users and channel counts",
190
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
191
+ })
192
+ router.Register(cmdparse.Command{
193
+ Name: "who",
194
+ Usage: "WHO [#channel]",
195
+ Description: "show detailed user list",
196
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
197
+ })
198
+
199
+ c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
200
+ if len(e.Params) < 1 || e.Source == nil {
201
+ return
202
+ }
203
+ // Dispatch commands (DMs and channel messages).
204
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
205
+ cl.Cmd.Message(reply.Target, reply.Text)
206
+ return
207
+ }
208
+ })
182209
183210
b.client = c
184211
185212
errCh := make(chan error, 1)
186213
go func() {
187214
--- internal/bots/systembot/systembot.go
+++ internal/bots/systembot/systembot.go
@@ -17,10 +17,12 @@
17 "strconv"
18 "strings"
19 "time"
20
21 "github.com/lrstanley/girc"
 
 
22 )
23
24 const botNick = "systembot"
25
26 // EntryKind classifies a system event.
@@ -177,10 +179,35 @@
177 if e.Source != nil {
178 nick = e.Source.Name
179 }
180 b.write(Entry{Kind: KindMode, Channel: channel, Nick: nick, Text: strings.Join(e.Params, " ")})
181 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
183 b.client = c
184
185 errCh := make(chan error, 1)
186 go func() {
187
--- internal/bots/systembot/systembot.go
+++ internal/bots/systembot/systembot.go
@@ -17,10 +17,12 @@
17 "strconv"
18 "strings"
19 "time"
20
21 "github.com/lrstanley/girc"
22
23 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
24 )
25
26 const botNick = "systembot"
27
28 // EntryKind classifies a system event.
@@ -177,10 +179,35 @@
179 if e.Source != nil {
180 nick = e.Source.Name
181 }
182 b.write(Entry{Kind: KindMode, Channel: channel, Nick: nick, Text: strings.Join(e.Params, " ")})
183 })
184
185 router := cmdparse.NewRouter(botNick)
186 router.Register(cmdparse.Command{
187 Name: "status",
188 Usage: "STATUS",
189 Description: "show connected users and channel counts",
190 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
191 })
192 router.Register(cmdparse.Command{
193 Name: "who",
194 Usage: "WHO [#channel]",
195 Description: "show detailed user list",
196 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
197 })
198
199 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
200 if len(e.Params) < 1 || e.Source == nil {
201 return
202 }
203 // Dispatch commands (DMs and channel messages).
204 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
205 cl.Cmd.Message(reply.Target, reply.Text)
206 return
207 }
208 })
209
210 b.client = c
211
212 errCh := make(chan error, 1)
213 go func() {
214
--- internal/bots/warden/warden.go
+++ internal/bots/warden/warden.go
@@ -18,10 +18,11 @@
1818
"sync"
1919
"time"
2020
2121
"github.com/lrstanley/girc"
2222
23
+ "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
2324
"github.com/conflicthq/scuttlebot/pkg/protocol"
2425
)
2526
2627
const botNick = "warden"
2728
@@ -216,18 +217,49 @@
216217
c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
217218
if ch := e.Last(); strings.HasPrefix(ch, "#") {
218219
cl.Cmd.Join(ch)
219220
}
220221
})
222
+
223
+ router := cmdparse.NewRouter(botNick)
224
+ router.Register(cmdparse.Command{
225
+ Name: "warn",
226
+ Usage: "WARN <nick> [reason]",
227
+ Description: "issue a warning to a user",
228
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
229
+ })
230
+ router.Register(cmdparse.Command{
231
+ Name: "mute",
232
+ Usage: "MUTE <nick> [duration]",
233
+ Description: "mute a user",
234
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
235
+ })
236
+ router.Register(cmdparse.Command{
237
+ Name: "kick",
238
+ Usage: "KICK <nick> [reason]",
239
+ Description: "kick a user from channel",
240
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
241
+ })
242
+ router.Register(cmdparse.Command{
243
+ Name: "status",
244
+ Usage: "STATUS",
245
+ Description: "show current warnings and mutes",
246
+ Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
247
+ })
221248
222249
c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
223250
if len(e.Params) < 1 || e.Source == nil {
224251
return
252
+ }
253
+ // Dispatch commands (DMs and channel messages).
254
+ if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
255
+ cl.Cmd.Message(reply.Target, reply.Text)
256
+ return
225257
}
226258
channel := e.Params[0]
227259
if !strings.HasPrefix(channel, "#") {
228
- return
260
+ return // non-command DMs ignored
229261
}
230262
nick := e.Source.Name
231263
text := e.Last()
232264
233265
cs := b.channelStateFor(channel)
234266
--- internal/bots/warden/warden.go
+++ internal/bots/warden/warden.go
@@ -18,10 +18,11 @@
18 "sync"
19 "time"
20
21 "github.com/lrstanley/girc"
22
 
23 "github.com/conflicthq/scuttlebot/pkg/protocol"
24 )
25
26 const botNick = "warden"
27
@@ -216,18 +217,49 @@
216 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
217 if ch := e.Last(); strings.HasPrefix(ch, "#") {
218 cl.Cmd.Join(ch)
219 }
220 })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
222 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
223 if len(e.Params) < 1 || e.Source == nil {
224 return
 
 
 
 
 
225 }
226 channel := e.Params[0]
227 if !strings.HasPrefix(channel, "#") {
228 return
229 }
230 nick := e.Source.Name
231 text := e.Last()
232
233 cs := b.channelStateFor(channel)
234
--- internal/bots/warden/warden.go
+++ internal/bots/warden/warden.go
@@ -18,10 +18,11 @@
18 "sync"
19 "time"
20
21 "github.com/lrstanley/girc"
22
23 "github.com/conflicthq/scuttlebot/internal/bots/cmdparse"
24 "github.com/conflicthq/scuttlebot/pkg/protocol"
25 )
26
27 const botNick = "warden"
28
@@ -216,18 +217,49 @@
217 c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) {
218 if ch := e.Last(); strings.HasPrefix(ch, "#") {
219 cl.Cmd.Join(ch)
220 }
221 })
222
223 router := cmdparse.NewRouter(botNick)
224 router.Register(cmdparse.Command{
225 Name: "warn",
226 Usage: "WARN <nick> [reason]",
227 Description: "issue a warning to a user",
228 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
229 })
230 router.Register(cmdparse.Command{
231 Name: "mute",
232 Usage: "MUTE <nick> [duration]",
233 Description: "mute a user",
234 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
235 })
236 router.Register(cmdparse.Command{
237 Name: "kick",
238 Usage: "KICK <nick> [reason]",
239 Description: "kick a user from channel",
240 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
241 })
242 router.Register(cmdparse.Command{
243 Name: "status",
244 Usage: "STATUS",
245 Description: "show current warnings and mutes",
246 Handler: func(_ *cmdparse.Context, _ string) string { return "not implemented yet" },
247 })
248
249 c.Handlers.AddBg(girc.PRIVMSG, func(cl *girc.Client, e girc.Event) {
250 if len(e.Params) < 1 || e.Source == nil {
251 return
252 }
253 // Dispatch commands (DMs and channel messages).
254 if reply := router.Dispatch(e.Source.Name, e.Params[0], e.Last()); reply != nil {
255 cl.Cmd.Message(reply.Target, reply.Text)
256 return
257 }
258 channel := e.Params[0]
259 if !strings.HasPrefix(channel, "#") {
260 return // non-command DMs ignored
261 }
262 nick := e.Source.Name
263 text := e.Last()
264
265 cs := b.channelStateFor(channel)
266

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button