ScuttleBot

scuttlebot / cmd / fleet-cmd / main.go
Source Blame History 188 lines
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 }

Keyboard Shortcuts

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