ScuttleBot

scuttlebot / cmd / fleet-cmd / main.go
Blame History Raw 189 lines
1
package main
2
3
import (
4
"encoding/json"
5
"fmt"
6
"log"
7
"net/http"
8
"os"
9
"sort"
10
"strings"
11
"text/tabwriter"
12
"time"
13
)
14
15
type Agent struct {
16
Nick string `json:"nick"`
17
Type string `json:"type"`
18
CreatedAt time.Time `json:"created_at"`
19
}
20
21
type Message struct {
22
Nick string `json:"nick"`
23
Text string `json:"text"`
24
At time.Time `json:"at"`
25
}
26
27
func main() {
28
token := os.Getenv("SCUTTLEBOT_TOKEN")
29
url := os.Getenv("SCUTTLEBOT_URL")
30
if url == "" {
31
url = "http://localhost:8080"
32
}
33
34
if token == "" {
35
log.Fatal("SCUTTLEBOT_TOKEN is required")
36
}
37
38
if len(os.Args) < 2 {
39
usage()
40
}
41
42
// Parse optional --channel flag before the subcommand.
43
channel := "general"
44
args := os.Args[1:]
45
for i := 0; i < len(args); i++ {
46
if args[i] == "--channel" && i+1 < len(args) {
47
channel = strings.TrimPrefix(args[i+1], "#")
48
args = append(args[:i], args[i+2:]...)
49
break
50
}
51
if strings.HasPrefix(args[i], "--channel=") {
52
channel = strings.TrimPrefix(strings.TrimPrefix(args[i], "--channel="), "#")
53
args = append(args[:i], args[i+1:]...)
54
break
55
}
56
}
57
58
if len(args) == 0 {
59
usage()
60
}
61
62
switch args[0] {
63
case "map":
64
mapFleet(url, token, channel)
65
case "broadcast":
66
if len(args) < 2 {
67
log.Fatal("usage: fleet-cmd broadcast <message>")
68
}
69
broadcast(url, token, channel, strings.Join(args[1:], " "))
70
default:
71
usage()
72
}
73
}
74
75
func usage() {
76
fmt.Println("Usage: fleet-cmd [--channel <channel>] <command> [args]")
77
fmt.Println("Commands:")
78
fmt.Println(" map Show all agents and their last activity")
79
fmt.Println(" broadcast Send a message to the channel")
80
os.Exit(1)
81
}
82
83
func mapFleet(url, token, channel string) {
84
agents := fetchAgents(url, token)
85
messages := fetchMessages(url, token, channel)
86
87
// Filter for actual session nicks (ones with suffixes)
88
sessions := make(map[string]Message)
89
for _, m := range messages {
90
if strings.Contains(m.Nick, "-") {
91
sessions[m.Nick] = m
92
}
93
}
94
95
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
96
fmt.Fprintln(w, "NICK\tTYPE\tLAST ACTIVITY\tTIME")
97
98
// Sort nicks for stable output
99
var nicks []string
100
for n := range sessions {
101
nicks = append(nicks, n)
102
}
103
sort.Strings(nicks)
104
105
for _, nick := range nicks {
106
m := sessions[nick]
107
nickType := "unknown"
108
for _, a := range agents {
109
if strings.HasPrefix(nick, a.Nick) {
110
nickType = a.Type
111
break
112
}
113
}
114
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", nick, nickType, truncate(m.Text, 40), timeSince(m.At))
115
}
116
w.Flush()
117
}
118
119
func broadcast(url, token, channel, msg string) {
120
body, _ := json.Marshal(map[string]string{
121
"nick": "commander",
122
"text": msg,
123
})
124
req, _ := http.NewRequest("POST", url+"/v1/channels/"+channel+"/messages", strings.NewReader(string(body)))
125
req.Header.Set("Authorization", "Bearer "+token)
126
req.Header.Set("Content-Type", "application/json")
127
128
resp, err := http.DefaultClient.Do(req)
129
if err != nil {
130
log.Fatal(err)
131
}
132
defer resp.Body.Close()
133
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
134
log.Fatalf("broadcast failed: %s", resp.Status)
135
}
136
fmt.Printf("Broadcast sent to #%s: %s\n", channel, msg)
137
}
138
139
func fetchAgents(url, token string) []Agent {
140
req, _ := http.NewRequest("GET", url+"/v1/agents", nil)
141
req.Header.Set("Authorization", "Bearer "+token)
142
resp, _ := http.DefaultClient.Do(req)
143
var data struct {
144
Agents []Agent `json:"agents"`
145
}
146
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
147
log.Printf("fetchAgents: decode: %v", err)
148
}
149
return data.Agents
150
}
151
152
func fetchMessages(url, token, channel string) []Message {
153
req, _ := http.NewRequest("GET", url+"/v1/channels/"+channel+"/messages", nil)
154
req.Header.Set("Authorization", "Bearer "+token)
155
resp, _ := http.DefaultClient.Do(req)
156
var data struct {
157
Messages []struct {
158
Nick string `json:"nick"`
159
Text string `json:"text"`
160
At string `json:"at"`
161
} `json:"messages"`
162
}
163
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
164
log.Printf("fetchMessages: decode: %v", err)
165
}
166
167
var out []Message
168
for _, m := range data.Messages {
169
at, _ := time.Parse(time.RFC3339Nano, m.At)
170
out = append(out, Message{Nick: m.Nick, Text: m.Text, At: at})
171
}
172
return out
173
}
174
175
func truncate(s string, n int) string {
176
if len(s) <= n {
177
return s
178
}
179
return s[:n-3] + "..."
180
}
181
182
func timeSince(t time.Time) string {
183
if t.IsZero() {
184
return "never"
185
}
186
d := time.Since(t).Round(time.Second)
187
return d.String() + " ago"
188
}
189

Keyboard Shortcuts

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