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