ScuttleBot
feat: snitch MONITOR for presence tracking and away-notify (#108, #121) Add MONITOR support to track agent online/offline status — alerts operators when a monitored nick goes offline. Config accepts MonitorNicks list, with MonitorAdd/MonitorRemove for runtime updates. Also handle away-notify: alert when agents go AWAY unexpectedly.
Commit
170b64595fd946bf7d4946900dddb1b3033bb6e7ebb07a5cfe811b89626b15e1
Parent
4dd84aa46da16a1…
1 file changed
+45
-1
+45
-1
| --- internal/bots/snitch/snitch.go | ||
| +++ internal/bots/snitch/snitch.go | ||
| @@ -48,10 +48,14 @@ | ||
| 48 | 48 | // JoinPartWindow is the rolling window for join/part cycling. Default: 30s. |
| 49 | 49 | JoinPartWindow time.Duration |
| 50 | 50 | |
| 51 | 51 | // Channels is the list of channels to join on connect. |
| 52 | 52 | Channels []string |
| 53 | + | |
| 54 | + // MonitorNicks is the list of nicks to track via IRC MONITOR. | |
| 55 | + // Snitch will alert when a monitored nick goes offline unexpectedly. | |
| 56 | + MonitorNicks []string | |
| 53 | 57 | } |
| 54 | 58 | |
| 55 | 59 | func (c *Config) setDefaults() { |
| 56 | 60 | if c.Nick == "" { |
| 57 | 61 | c.Nick = defaultNick |
| @@ -142,12 +146,38 @@ | ||
| 142 | 146 | cl.Cmd.Join(ch) |
| 143 | 147 | } |
| 144 | 148 | if b.cfg.AlertChannel != "" { |
| 145 | 149 | cl.Cmd.Join(b.cfg.AlertChannel) |
| 146 | 150 | } |
| 151 | + if len(b.cfg.MonitorNicks) > 0 { | |
| 152 | + cl.Cmd.SendRawf("MONITOR + %s", strings.Join(b.cfg.MonitorNicks, ",")) | |
| 153 | + } | |
| 147 | 154 | if b.log != nil { |
| 148 | - b.log.Info("snitch connected", "channels", b.cfg.Channels) | |
| 155 | + b.log.Info("snitch connected", "channels", b.cfg.Channels, "monitor", b.cfg.MonitorNicks) | |
| 156 | + } | |
| 157 | + }) | |
| 158 | + | |
| 159 | + // away-notify: track agents going idle or returning. | |
| 160 | + c.Handlers.AddBg(girc.AWAY, func(_ *girc.Client, e girc.Event) { | |
| 161 | + if e.Source == nil { | |
| 162 | + return | |
| 163 | + } | |
| 164 | + nick := e.Source.Name | |
| 165 | + reason := e.Last() | |
| 166 | + if reason != "" { | |
| 167 | + b.alert(fmt.Sprintf("agent away: %s (%s)", nick, reason)) | |
| 168 | + } | |
| 169 | + }) | |
| 170 | + | |
| 171 | + c.Handlers.AddBg(girc.RPL_MONOFFLINE, func(_ *girc.Client, e girc.Event) { | |
| 172 | + nicks := e.Last() | |
| 173 | + for _, nick := range strings.Split(nicks, ",") { | |
| 174 | + nick = strings.TrimSpace(nick) | |
| 175 | + if nick == "" { | |
| 176 | + continue | |
| 177 | + } | |
| 178 | + b.alert(fmt.Sprintf("monitored nick offline: %s", nick)) | |
| 149 | 179 | } |
| 150 | 180 | }) |
| 151 | 181 | |
| 152 | 182 | c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) { |
| 153 | 183 | if ch := e.Last(); strings.HasPrefix(ch, "#") { |
| @@ -203,10 +233,24 @@ | ||
| 203 | 233 | func (b *Bot) JoinChannel(channel string) { |
| 204 | 234 | if b.client != nil { |
| 205 | 235 | b.client.Cmd.Join(channel) |
| 206 | 236 | } |
| 207 | 237 | } |
| 238 | + | |
| 239 | +// MonitorAdd adds nicks to the MONITOR list at runtime. | |
| 240 | +func (b *Bot) MonitorAdd(nicks ...string) { | |
| 241 | + if b.client != nil && len(nicks) > 0 { | |
| 242 | + b.client.Cmd.SendRawf("MONITOR + %s", strings.Join(nicks, ",")) | |
| 243 | + } | |
| 244 | +} | |
| 245 | + | |
| 246 | +// MonitorRemove removes nicks from the MONITOR list at runtime. | |
| 247 | +func (b *Bot) MonitorRemove(nicks ...string) { | |
| 248 | + if b.client != nil && len(nicks) > 0 { | |
| 249 | + b.client.Cmd.SendRawf("MONITOR - %s", strings.Join(nicks, ",")) | |
| 250 | + } | |
| 251 | +} | |
| 208 | 252 | |
| 209 | 253 | func (b *Bot) window(channel, nick string) *nickWindow { |
| 210 | 254 | if b.windows[channel] == nil { |
| 211 | 255 | b.windows[channel] = make(map[string]*nickWindow) |
| 212 | 256 | } |
| 213 | 257 |
| --- internal/bots/snitch/snitch.go | |
| +++ internal/bots/snitch/snitch.go | |
| @@ -48,10 +48,14 @@ | |
| 48 | // JoinPartWindow is the rolling window for join/part cycling. Default: 30s. |
| 49 | JoinPartWindow time.Duration |
| 50 | |
| 51 | // Channels is the list of channels to join on connect. |
| 52 | Channels []string |
| 53 | } |
| 54 | |
| 55 | func (c *Config) setDefaults() { |
| 56 | if c.Nick == "" { |
| 57 | c.Nick = defaultNick |
| @@ -142,12 +146,38 @@ | |
| 142 | cl.Cmd.Join(ch) |
| 143 | } |
| 144 | if b.cfg.AlertChannel != "" { |
| 145 | cl.Cmd.Join(b.cfg.AlertChannel) |
| 146 | } |
| 147 | if b.log != nil { |
| 148 | b.log.Info("snitch connected", "channels", b.cfg.Channels) |
| 149 | } |
| 150 | }) |
| 151 | |
| 152 | c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) { |
| 153 | if ch := e.Last(); strings.HasPrefix(ch, "#") { |
| @@ -203,10 +233,24 @@ | |
| 203 | func (b *Bot) JoinChannel(channel string) { |
| 204 | if b.client != nil { |
| 205 | b.client.Cmd.Join(channel) |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | func (b *Bot) window(channel, nick string) *nickWindow { |
| 210 | if b.windows[channel] == nil { |
| 211 | b.windows[channel] = make(map[string]*nickWindow) |
| 212 | } |
| 213 |
| --- internal/bots/snitch/snitch.go | |
| +++ internal/bots/snitch/snitch.go | |
| @@ -48,10 +48,14 @@ | |
| 48 | // JoinPartWindow is the rolling window for join/part cycling. Default: 30s. |
| 49 | JoinPartWindow time.Duration |
| 50 | |
| 51 | // Channels is the list of channels to join on connect. |
| 52 | Channels []string |
| 53 | |
| 54 | // MonitorNicks is the list of nicks to track via IRC MONITOR. |
| 55 | // Snitch will alert when a monitored nick goes offline unexpectedly. |
| 56 | MonitorNicks []string |
| 57 | } |
| 58 | |
| 59 | func (c *Config) setDefaults() { |
| 60 | if c.Nick == "" { |
| 61 | c.Nick = defaultNick |
| @@ -142,12 +146,38 @@ | |
| 146 | cl.Cmd.Join(ch) |
| 147 | } |
| 148 | if b.cfg.AlertChannel != "" { |
| 149 | cl.Cmd.Join(b.cfg.AlertChannel) |
| 150 | } |
| 151 | if len(b.cfg.MonitorNicks) > 0 { |
| 152 | cl.Cmd.SendRawf("MONITOR + %s", strings.Join(b.cfg.MonitorNicks, ",")) |
| 153 | } |
| 154 | if b.log != nil { |
| 155 | b.log.Info("snitch connected", "channels", b.cfg.Channels, "monitor", b.cfg.MonitorNicks) |
| 156 | } |
| 157 | }) |
| 158 | |
| 159 | // away-notify: track agents going idle or returning. |
| 160 | c.Handlers.AddBg(girc.AWAY, func(_ *girc.Client, e girc.Event) { |
| 161 | if e.Source == nil { |
| 162 | return |
| 163 | } |
| 164 | nick := e.Source.Name |
| 165 | reason := e.Last() |
| 166 | if reason != "" { |
| 167 | b.alert(fmt.Sprintf("agent away: %s (%s)", nick, reason)) |
| 168 | } |
| 169 | }) |
| 170 | |
| 171 | c.Handlers.AddBg(girc.RPL_MONOFFLINE, func(_ *girc.Client, e girc.Event) { |
| 172 | nicks := e.Last() |
| 173 | for _, nick := range strings.Split(nicks, ",") { |
| 174 | nick = strings.TrimSpace(nick) |
| 175 | if nick == "" { |
| 176 | continue |
| 177 | } |
| 178 | b.alert(fmt.Sprintf("monitored nick offline: %s", nick)) |
| 179 | } |
| 180 | }) |
| 181 | |
| 182 | c.Handlers.AddBg(girc.INVITE, func(cl *girc.Client, e girc.Event) { |
| 183 | if ch := e.Last(); strings.HasPrefix(ch, "#") { |
| @@ -203,10 +233,24 @@ | |
| 233 | func (b *Bot) JoinChannel(channel string) { |
| 234 | if b.client != nil { |
| 235 | b.client.Cmd.Join(channel) |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | // MonitorAdd adds nicks to the MONITOR list at runtime. |
| 240 | func (b *Bot) MonitorAdd(nicks ...string) { |
| 241 | if b.client != nil && len(nicks) > 0 { |
| 242 | b.client.Cmd.SendRawf("MONITOR + %s", strings.Join(nicks, ",")) |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | // MonitorRemove removes nicks from the MONITOR list at runtime. |
| 247 | func (b *Bot) MonitorRemove(nicks ...string) { |
| 248 | if b.client != nil && len(nicks) > 0 { |
| 249 | b.client.Cmd.SendRawf("MONITOR - %s", strings.Join(nicks, ",")) |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | func (b *Bot) window(channel, nick string) *nickWindow { |
| 254 | if b.windows[channel] == nil { |
| 255 | b.windows[channel] = make(map[string]*nickWindow) |
| 256 | } |
| 257 |