├── .gitignore ├── README.md ├── command.go ├── daryl.go └── slack.go /.gitignore: -------------------------------------------------------------------------------- 1 | daryl 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # mybot 3 | 4 | `mybot` is an working Slack bot written in Go. Fork it and use it to build 5 | your very own cool Slack bot! 6 | 7 | Check the [blog post](https://www.opsdash.com/blog/slack-bot-in-golang.html) 8 | for a description of mybot internals. 9 | 10 | Follow us on Twitter today! [@therapidloop](https://twitter.com/therapidloop) 11 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/csv" 5 | "encoding/json" 6 | "fmt" 7 | "math/rand" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "golang.org/x/net/websocket" 14 | ) 15 | 16 | var cmdList *CommandList 17 | 18 | func init() { 19 | cmdList = &CommandList{ 20 | commands: make(map[string]Command), 21 | } 22 | f := Command{ 23 | Name: "coinflip", 24 | Description: "Flip a Coin - 'coinflip'", 25 | Usage: "coinflip", 26 | Run: coinFlip, 27 | } 28 | cmdList.AddCommand(f) 29 | c := Command{ 30 | Name: "stock", 31 | Description: "Get a Stock Quote - 'quote AAPL'", 32 | Usage: "quote TICKER", 33 | Run: getQuote, 34 | } 35 | cmdList.AddCommand(c) 36 | 37 | k := Command{ 38 | Name: "kudos", 39 | Description: "Send kudos to a teammate- 'quote @teammate'", 40 | Usage: "kudos @teammate", 41 | Run: kudos, 42 | } 43 | cmdList.AddCommand(k) 44 | 45 | i := Command{ 46 | Name: "image", 47 | Description: "Returns the first google image for query- 'image '", 48 | Usage: "image kittens", 49 | Run: image, 50 | } 51 | cmdList.AddCommand(i) 52 | } 53 | 54 | type Command struct { 55 | Name string 56 | Description string 57 | Usage string 58 | Run CommandFunc 59 | } 60 | 61 | type CommandFunc func(args []string) string 62 | 63 | type CommandList struct { 64 | commands map[string]Command 65 | } 66 | 67 | func (cl *CommandList) AddCommand(c Command) { 68 | cl.commands[c.Name] = c 69 | } 70 | 71 | func (cl *CommandList) Process(ws *websocket.Conn, m Message, id string) { 72 | logger.Info("processing", m.Text) 73 | 74 | if strings.HasPrefix(m.Text, "<@"+id+">") { 75 | 76 | parts := strings.Fields(m.Text) 77 | 78 | cmd, ok := cl.commands[parts[1]] 79 | if !ok { 80 | logger.Info("error", "no command found", 81 | "args", parts[1], 82 | "full text", m.Text) 83 | m.Text = cl.ListCommands() 84 | postMessage(ws, m) 85 | return 86 | } 87 | // looks good, get the quote and reply with the result 88 | logger.Info("action", "start processing", 89 | "args", parts[1], 90 | "full text", m.Text) 91 | go func(m Message) { 92 | logger.Info("action", "executing", 93 | "full text", m.Text) 94 | m.Text = cmd.Run(parts[2:]) 95 | postMessage(ws, m) 96 | }(m) 97 | } else { 98 | // casual mention. What should we do about that? 99 | go func(m Message) { 100 | m.Text = "You rang?" 101 | postMessage(ws, m) 102 | }(m) 103 | 104 | } 105 | 106 | } 107 | 108 | func (cl *CommandList) ListCommands() string { 109 | out := "Here's what I can do:\n" 110 | for _, cmd := range cl.commands { 111 | txt := fmt.Sprintf("\t %s - %s\n", cmd.Name, cmd.Description) 112 | out = out + txt 113 | } 114 | 115 | return out 116 | 117 | } 118 | 119 | // send a kudo to a team member 120 | func kudos(args []string) string { 121 | if len(args) < 1 { 122 | return "Please tell me who to thank!" 123 | } 124 | teammate := args[0] 125 | return fmt.Sprintf("Hey %s, thanks for being awesome!", teammate) 126 | } 127 | 128 | // send a kudo to a team member 129 | func image(args []string) string { 130 | if len(args) < 1 { 131 | return "You don't really want me searching for random images, do you?" 132 | } 133 | url := fmt.Sprintf("https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q=%s", url.QueryEscape(strings.Join(args, " "))) 134 | resp, err := http.Get(url) 135 | if err != nil { 136 | return fmt.Sprintf("Google doesn't like you. %s", err) 137 | } 138 | defer resp.Body.Close() 139 | r := make(map[string]interface{}) 140 | if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { 141 | return fmt.Sprintf("Google doesn't like you. %s", err) 142 | } 143 | rd := r["responseData"].(map[string]interface{}) 144 | res := rd["results"].([]interface{}) 145 | con := res[0].(map[string]interface{}) 146 | u := con["url"].(string) 147 | return fmt.Sprintf("%v", u) 148 | } 149 | 150 | // Get the quote via Yahoo. You should replace this method to something 151 | // relevant to your team! 152 | func getQuote(args []string) string { 153 | if len(args) < 1 { 154 | return "Please tell me which stock to quote next time!" 155 | } 156 | sym := args[0] 157 | 158 | sym = strings.ToUpper(sym) 159 | url := fmt.Sprintf("http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=nsl1op&e=.csv", sym) 160 | resp, err := http.Get(url) 161 | if err != nil { 162 | return fmt.Sprintf("error: %v", err) 163 | } 164 | rows, err := csv.NewReader(resp.Body).ReadAll() 165 | if err != nil { 166 | return fmt.Sprintf("error: %v", err) 167 | } 168 | if len(rows) >= 1 && len(rows[0]) == 5 { 169 | return fmt.Sprintf("%s (%s) is trading at $%s", rows[0][0], rows[0][1], rows[0][2]) 170 | } 171 | return fmt.Sprintf("unknown response format (symbol was \"%s\")", sym) 172 | } 173 | 174 | // Get the quote via Yahoo. You should replace this method to something 175 | // relevant to your team! 176 | func coinFlip(args []string) string { 177 | 178 | var heads bool 179 | rand.Seed(time.Now().UnixNano()) 180 | switch rand.Intn(2) { 181 | case 0: 182 | heads = true 183 | case 1: 184 | heads = false 185 | } 186 | if heads { 187 | return fmt.Sprintf("the gods of fortune say 'heads'") 188 | } 189 | return fmt.Sprintf("'tails' is the result") 190 | 191 | } 192 | -------------------------------------------------------------------------------- /daryl.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | mybot - Illustrative Slack bot in Go 4 | 5 | Copyright (c) 2015 RapidLoop 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | "os" 31 | "strings" 32 | 33 | "github.com/Sirupsen/logrus" 34 | ) 35 | 36 | var logger *logrus.Entry 37 | 38 | func init() { 39 | logger = logrus.WithFields(logrus.Fields{ 40 | "slack": "gophers", 41 | }) 42 | } 43 | 44 | func main() { 45 | if len(os.Args) != 2 { 46 | fmt.Fprintf(os.Stderr, "usage: daryl slack-bot-token\n") 47 | os.Exit(1) 48 | } 49 | 50 | // start a websocket-based Real Time API session 51 | ws, id := slackConnect(os.Args[1]) 52 | fmt.Println("d.a.r.y.l. ready, ^C exits") 53 | 54 | for { 55 | // read each incoming message 56 | m, err := getMessage(ws) 57 | if err != nil { 58 | logger.Error("error", err) 59 | } 60 | // see if we're mentioned 61 | if m.Type == "message" && strings.Contains(m.Text, "<@"+id+">") { 62 | logger.Info("message", m.Text) 63 | cmdList.Process(ws, m, id) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /slack.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | mybot - Illustrative Slack bot in Go 4 | 5 | Copyright (c) 2015 RapidLoop 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | package main 27 | 28 | import ( 29 | "encoding/json" 30 | "fmt" 31 | "io/ioutil" 32 | "log" 33 | "net/http" 34 | "sync/atomic" 35 | 36 | "golang.org/x/net/websocket" 37 | ) 38 | 39 | // These two structures represent the response of the Slack API rtm.start. 40 | // Only some fields are included. The rest are ignored by json.Unmarshal. 41 | 42 | type responseRtmStart struct { 43 | Ok bool `json:"ok"` 44 | Error string `json:"error"` 45 | Url string `json:"url"` 46 | Self responseSelf `json:"self"` 47 | } 48 | 49 | type responseSelf struct { 50 | Id string `json:"id"` 51 | } 52 | 53 | // slackStart does a rtm.start, and returns a websocket URL and user ID. The 54 | // websocket URL can be used to initiate an RTM session. 55 | func slackStart(token string) (wsurl, id string, err error) { 56 | url := fmt.Sprintf("https://slack.com/api/rtm.start?token=%s", token) 57 | resp, err := http.Get(url) 58 | if err != nil { 59 | return 60 | } 61 | if resp.StatusCode != 200 { 62 | err = fmt.Errorf("API request failed with code %d", resp.StatusCode) 63 | return 64 | } 65 | body, err := ioutil.ReadAll(resp.Body) 66 | resp.Body.Close() 67 | if err != nil { 68 | return 69 | } 70 | var respObj responseRtmStart 71 | err = json.Unmarshal(body, &respObj) 72 | if err != nil { 73 | return 74 | } 75 | 76 | if !respObj.Ok { 77 | err = fmt.Errorf("Slack error: %s", respObj.Error) 78 | return 79 | } 80 | 81 | wsurl = respObj.Url 82 | id = respObj.Self.Id 83 | return 84 | } 85 | 86 | // These are the messages read off and written into the websocket. Since this 87 | // struct serves as both read and write, we include the "Id" field which is 88 | // required only for writing. 89 | 90 | type Message struct { 91 | Id uint64 `json:"id"` 92 | Type string `json:"type"` 93 | Channel string `json:"channel"` 94 | Text string `json:"text"` 95 | } 96 | 97 | func getMessage(ws *websocket.Conn) (m Message, err error) { 98 | err = websocket.JSON.Receive(ws, &m) 99 | return 100 | } 101 | 102 | var counter uint64 103 | 104 | func postMessage(ws *websocket.Conn, m Message) error { 105 | m.Id = atomic.AddUint64(&counter, 1) 106 | return websocket.JSON.Send(ws, m) 107 | } 108 | 109 | // Starts a websocket-based Real Time API session and return the websocket 110 | // and the ID of the (bot-)user whom the token belongs to. 111 | func slackConnect(token string) (*websocket.Conn, string) { 112 | wsurl, id, err := slackStart(token) 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | 117 | ws, err := websocket.Dial(wsurl, "", "https://api.slack.com/") 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | 122 | return ws, id 123 | } 124 | --------------------------------------------------------------------------------