├── .travis.yml ├── LICENSE.md ├── README.md ├── api.go ├── api_channels.go ├── api_channels_test.go ├── api_test.go ├── api_users.go ├── api_users_test.go ├── cmd └── slacker │ └── main.go ├── doc.go ├── error_map.go ├── rtm.go ├── rtm_broker.go ├── rtm_broker_test.go ├── rtm_events.go ├── rtm_publishable_events.go ├── rtm_start.go ├── rtm_start_test.go ├── slacker.go └── slacker_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - 1.5 6 | - tip 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Robert Ross 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slacker 2 | 3 | [![GoDoc](https://godoc.org/github.com/bobbytables/slacker?status.svg)](https://godoc.org/github.com/bobbytables/slacker) 4 | [![Build Status](https://travis-ci.org/bobbytables/slacker.svg?branch=master)](https://travis-ci.org/bobbytables/slacker) 5 | 6 | Slacker is a Golang package to interface with Slack's API and Real Time Messaging API. 7 | 8 | For full documentation, always check [godoc](https://godoc.org/github.com/bobbytables/slacker). 9 | 10 | ## Simple Examples 11 | 12 | It's always fun to see quick ways to use a package. Here are some examples of 13 | how to use slacker for simple things. 14 | 15 | ### Getting all channels for a team 16 | 17 | ```go 18 | c := slacker.NewAPIClient("your-slack-token", "") 19 | channels, err := c.ChannelsList() 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // Map channels so we can easily retrieve a channel by name. 25 | mappedChannels := map[string]*slacker.Channel{} 26 | for _, channel := range channels { 27 | mappedChannels[channel.Name] = channel 28 | } 29 | 30 | fmt.Printf("Channels: %+v", mappedChannels) 31 | ``` 32 | 33 | ### Getting all members for a team 34 | 35 | ```go 36 | c := slacker.NewAPIClient("your-slack-token", "") 37 | users, err := c.UsersList() 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | mappedUsers := map[string]*slacker.User{} 43 | for _, user := range users { 44 | mappedUsers[user.ID] = user 45 | } 46 | ``` 47 | 48 | ### Starting an RTM broker (real time messaging) 49 | 50 | This example starts a websocket to Slack's RTM API and displays events as they 51 | come in. 52 | 53 | ```go 54 | c := slacker.NewAPIClient("your-slack-token", "") 55 | rtmStart, err := c.RTMStart() 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | broker := slacker.NewRTMBroker(rtmStart) 61 | broker.Connect() 62 | 63 | for { 64 | event := <-broker.Events() 65 | fmt.Println(event.Type) 66 | 67 | if event.Type == "message" { 68 | msg, err := event.Message() 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | fmt.Println(msg.Text) 74 | } 75 | } 76 | ``` 77 | 78 | 79 | # License 80 | 81 | Slacker is released under the [MIT License](LICENSE.md). 82 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | // Response is the simplest representation of a Slack API response 11 | type Response struct { 12 | Ok bool `json:"ok"` 13 | Error string `json:"error"` 14 | } 15 | 16 | var ( 17 | // ErrNotAuthed is returned when an API response returns with "not_authed" 18 | // as it's error attribute. 19 | ErrNotAuthed = errors.New("slacker: Not Authed") 20 | ) 21 | 22 | // ParseResponse parses an io.Reader (usually the result of an API request), 23 | // and see's if the response actually contains error information. If it does, 24 | // it will return an error, leaving `dest` untouched. Otherwise, it will 25 | // json decode onto the destination passed in. 26 | func ParseResponse(r io.Reader, dest interface{}) error { 27 | d := json.NewDecoder(r) 28 | var rawData json.RawMessage 29 | 30 | if err := d.Decode(&rawData); err != nil { 31 | return nil 32 | } 33 | 34 | var resp Response 35 | if err := json.Unmarshal(rawData, &resp); err != nil { 36 | return err 37 | } 38 | 39 | if !resp.Ok { 40 | return responseError(resp.Error) 41 | } 42 | 43 | if err := json.Unmarshal(rawData, dest); err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func responseError(ident string) error { 51 | err, ok := errMap[ident] 52 | if !ok { 53 | return fmt.Errorf("slacker: unknown error returned: %s", ident) 54 | } 55 | 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /api_channels.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | // Channel represents a Slack channel 4 | // https://api.slack.com/types/channel 5 | type Channel struct { 6 | Created int `json:"created"` 7 | Creator string `json:"creator"` 8 | ID string `json:"id"` 9 | IsArchived bool `json:"is_archived"` 10 | IsChannel bool `json:"is_channel"` 11 | IsGeneral bool `json:"is_general"` 12 | IsMember bool `json:"is_member"` 13 | Members []string `json:"members"` 14 | Name string `json:"name"` 15 | NumMembers int `json:"num_members"` 16 | 17 | Purpose ChannelPurpose `json:"purpose"` 18 | Topic ChannelTopic `json:"topic"` 19 | } 20 | 21 | // ChannelPurpose represents a channels' purpose in Slack 22 | type ChannelPurpose struct { 23 | Creator string `json:"creator"` 24 | LastSet int `json:"last_set"` 25 | Value string `json:"value"` 26 | } 27 | 28 | // ChannelTopic represents a channel's topic in Slack 29 | type ChannelTopic struct { 30 | Creator string `json:"creator"` 31 | LastSet int `json:"last_set"` 32 | Value string `json:"value"` 33 | } 34 | 35 | // ChannelsList is a wrapper for a channels.list API call 36 | type ChannelsList struct { 37 | Channels []*Channel `json:"channels"` 38 | } 39 | 40 | // ChannelsList returns a list of Channels from Slack 41 | func (c *APIClient) ChannelsList() ([]*Channel, error) { 42 | dest := ChannelsList{} 43 | if err := c.slackMethodAndParse("channels.list", &dest); err != nil { 44 | return nil, err 45 | } 46 | 47 | return dest.Channels, nil 48 | } 49 | -------------------------------------------------------------------------------- /api_channels_test.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | var ( 12 | channelsListResp = `{"ok":true,"channels":[{"id":"C0HH54ZNV","name":"engineering","is_channel":true,"created":1451536266,"creator":"AKJSHFHFU","is_archived":false,"is_general":false,"is_member":true,"members":["AKJSHFHFU"],"topic":{"value":"","creator":"","last_set":0},"purpose":{"value":"All things engineering","creator":"AKJSHFHFU","last_set":1451536266},"num_members":1}]}` 13 | ) 14 | 15 | func TestChannelsAPI(t *testing.T) { 16 | Convey("Slack Channels API Endpoints", t, func() { 17 | Convey("channels.list", func() { 18 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | w.Write([]byte(channelsListResp)) 20 | })) 21 | defer server.Close() 22 | 23 | client := NewAPIClient("bunk", server.URL) 24 | channels, err := client.ChannelsList() 25 | So(err, ShouldBeNil) 26 | So(channels, ShouldHaveLength, 1) 27 | 28 | channel := channels[0] 29 | 30 | So(channel.ID, ShouldEqual, "C0HH54ZNV") 31 | So(channel.Name, ShouldEqual, "engineering") 32 | So(channel.IsChannel, ShouldEqual, true) 33 | So(channel.Created, ShouldEqual, 1451536266) 34 | So(channel.Creator, ShouldEqual, "AKJSHFHFU") 35 | So(channel.IsArchived, ShouldEqual, false) 36 | So(channel.IsGeneral, ShouldEqual, false) 37 | So(channel.IsMember, ShouldEqual, true) 38 | So(channel.NumMembers, ShouldEqual, 1) 39 | 40 | So(channel.Members[0], ShouldEqual, "AKJSHFHFU") 41 | 42 | So(channel.Topic.Value, ShouldEqual, "") 43 | So(channel.Topic.Creator, ShouldEqual, "") 44 | So(channel.Topic.LastSet, ShouldEqual, 0) 45 | 46 | So(channel.Purpose.Value, ShouldEqual, "All things engineering") 47 | So(channel.Purpose.Creator, ShouldEqual, "AKJSHFHFU") 48 | So(channel.Purpose.LastSet, ShouldEqual, 1451536266) 49 | }) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestAPIParsing(t *testing.T) { 11 | Convey("API Response Parsing Utilities", t, func() { 12 | Convey("When the response has failed", func() { 13 | badResponse := `{"ok":false,"error":"not_authed"}` 14 | dest := struct{}{} 15 | 16 | err := ParseResponse(bytes.NewBufferString(badResponse), &dest) 17 | So(err, ShouldEqual, ErrNotAuthed) 18 | }) 19 | 20 | Convey("When the response has failed with an error we don't know about", func() { 21 | badResponse := `{"ok":false,"error":"chicken"}` 22 | dest := struct{}{} 23 | 24 | err := ParseResponse(bytes.NewBufferString(badResponse), &dest) 25 | So(err.Error(), ShouldEqual, "slacker: unknown error returned: chicken") 26 | }) 27 | 28 | Convey("When the response is successful it decodes onto the destination", func() { 29 | goodResponse := `{"ok":true,"hello":"world"}` 30 | dest := &struct { 31 | Hello string `json:"hello"` 32 | }{} 33 | 34 | err := ParseResponse(bytes.NewBufferString(goodResponse), dest) 35 | So(err, ShouldBeNil) 36 | So(dest.Hello, ShouldEqual, "world") 37 | }) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /api_users.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | // User represents a Slack user object 4 | // https://api.slack.com/types/user 5 | type User struct { 6 | ID string `json:"id"` 7 | TeamID string `json:"team_id"` 8 | Name string `json:"name"` 9 | RealName string `json:"real_name"` 10 | Deleted bool `json:"deleted"` 11 | Color string `json:"color"` 12 | IsAdmin bool `json:"is_admin"` 13 | IsOwner bool `json:"is_owner"` 14 | Has2fa bool `json:"has_2fa"` 15 | HasFiles bool `json:"has_files"` 16 | 17 | Profile `json:"profile"` 18 | } 19 | 20 | // Profile represents a more detailed profile of a Slack user, including things 21 | // like avatars. 22 | type Profile struct { 23 | AvatarHash string `json:"avatar_hash"` 24 | Email string `json:"email"` 25 | FirstName string `json:"first_name"` 26 | LastName string `json:"last_name"` 27 | Image192 string `json:"image_192"` 28 | Image24 string `json:"image_24"` 29 | Image32 string `json:"image_32"` 30 | Image48 string `json:"image_48"` 31 | Image512 string `json:"image_512"` 32 | Image72 string `json:"image_72"` 33 | RealName string `json:"real_name"` 34 | RealNameNormalized string `json:"real_name_normalized"` 35 | } 36 | 37 | // UsersList is a response object wrapper for users.list in Slack 38 | // https://api.slack.com/methods/users.list 39 | type UsersList struct { 40 | Users []*User `json:"members"` 41 | } 42 | 43 | // UsersList returns all users in the team 44 | func (c *APIClient) UsersList() ([]*User, error) { 45 | dest := UsersList{} 46 | if err := c.slackMethodAndParse("users.list", &dest); err != nil { 47 | return nil, err 48 | } 49 | 50 | return dest.Users, nil 51 | } 52 | -------------------------------------------------------------------------------- /api_users_test.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | var ( 12 | usersListResp = `{ "ok": true, "members": [ { "team_id": "HURFFDURF", "id": "AKDOGURTUE", "name": "bobbytables", "deleted": false, "status": null, "color": "3c989f", "real_name": "Bobby Tables", "tz": "America\/Los_Angeles", "tz_label": "Pacific Standard Time", "tz_offset": -28800, "profile": { "first_name": "Bobby", "last_name": "Tables", "avatar_hash": "gd1bb47f21fb", "real_name": "Bobby Tables", "real_name_normalized": "Bobby Tables", "email": "bobbytables@dropstudents.com", "image_24": "https://fake.com/image.png", "image_32": "https://fake.com/image.png", "image_48": "https://fake.com/image.png", "image_72": "https://fake.com/image.png", "image_192": "https://fake.com/image.png", "image_512": "https://fake.com/image.png" }, "is_admin": false, "is_owner": false, "is_primary_owner": false, "is_restricted": false, "is_ultra_restricted": false, "is_bot": false, "has_2fa": false } ], "cache_ts": 1453140218 }` 13 | ) 14 | 15 | func TestUsersAPI(t *testing.T) { 16 | Convey("Slack Users API Endpoints", t, func() { 17 | Convey("users.list", func() { 18 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | w.Write([]byte(usersListResp)) 20 | })) 21 | defer server.Close() 22 | 23 | client := NewAPIClient("bunk", server.URL) 24 | users, err := client.UsersList() 25 | So(err, ShouldBeNil) 26 | So(users, ShouldHaveLength, 1) 27 | 28 | user := users[0] 29 | 30 | So(user.ID, ShouldEqual, "AKDOGURTUE") 31 | So(user.TeamID, ShouldEqual, "HURFFDURF") 32 | So(user.Name, ShouldEqual, "bobbytables") 33 | So(user.Deleted, ShouldBeFalse) 34 | So(user.Color, ShouldEqual, "3c989f") 35 | So(user.RealName, ShouldEqual, "Bobby Tables") 36 | 37 | profile := user.Profile 38 | 39 | So(profile.FirstName, ShouldEqual, "Bobby") 40 | So(profile.LastName, ShouldEqual, "Tables") 41 | So(profile.AvatarHash, ShouldEqual, "gd1bb47f21fb") 42 | So(profile.RealName, ShouldEqual, "Bobby Tables") 43 | So(profile.RealNameNormalized, ShouldEqual, "Bobby Tables") 44 | So(profile.Email, ShouldEqual, "bobbytables@dropstudents.com") 45 | So(profile.Image24, ShouldEqual, "https://fake.com/image.png") 46 | So(profile.Image32, ShouldEqual, "https://fake.com/image.png") 47 | So(profile.Image48, ShouldEqual, "https://fake.com/image.png") 48 | So(profile.Image72, ShouldEqual, "https://fake.com/image.png") 49 | So(profile.Image192, ShouldEqual, "https://fake.com/image.png") 50 | So(profile.Image512, ShouldEqual, "https://fake.com/image.png") 51 | }) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/slacker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/bobbytables/slacker" 8 | "github.com/codegangsta/cli" 9 | ) 10 | 11 | // This command line tool is mostly used to just see if you're wired up 12 | // correctly. If you're making contributions this is a good tool to modify 13 | // to do smoke testing. 14 | func main() { 15 | app := cli.NewApp() 16 | app.Usage = "runs methods against the Slack API and returns the result" 17 | app.Author = "Bobby Tables " 18 | app.Name = "slacker" 19 | app.Commands = []cli.Command{slackMethod()} 20 | 21 | app.Run(os.Args) 22 | } 23 | 24 | func slackMethod() cli.Command { 25 | return cli.Command{ 26 | Name: "run", 27 | Usage: "[method]", 28 | Flags: []cli.Flag{ 29 | cli.StringFlag{ 30 | Name: "token", 31 | Usage: "Your Slack API token", 32 | EnvVar: "SLACK_TOKEN", 33 | }, 34 | }, 35 | Description: "Hits the SlackAPI using the format: https://slack.com/api/{method}", 36 | Action: func(ctx *cli.Context) { 37 | if len(ctx.Args()) == 0 { 38 | cli.ShowSubcommandHelp(ctx) 39 | return 40 | } 41 | 42 | method := ctx.Args()[0] 43 | token := ctx.String("token") 44 | 45 | client := slacker.NewAPIClient(token, "") 46 | b, err := client.RunMethod(method) 47 | if err != nil { 48 | fmt.Printf("Error running method: %s", err.Error()) 49 | return 50 | } 51 | 52 | fmt.Println(string(b)) 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package slacker is a Go package for working with the Slack integration tools. 3 | This includes the API and RTM endpoints. 4 | 5 | To create a new slacker client, you can run 6 | 7 | client := slacker.NewAPIClient("my-slack-api-token", "") 8 | 9 | The first parameter is an OAuth2 token you should have obtained through either 10 | the Slack integrations dashboard, or the 3-legged OAuth2 token flow. 11 | 12 | The second parameter is the base URL for the Slack API. If left empty, it will 13 | use https://slack.com/api for all RPC calls. 14 | 15 | After you have created a client, you can call methods against the Slack API. 16 | For example, retrieving all user's of a team 17 | 18 | users, err := client.UsersList() 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | for _, user := range users { 24 | fmt.Println(user.RealName) 25 | } 26 | 27 | If you want to run a method that is not supported by this package, you can call 28 | generic methods by running: 29 | 30 | client.RunMethod("users.list") 31 | 32 | This will return a []byte of the JSON returned by the Slack API. 33 | 34 | */ 35 | package slacker 36 | -------------------------------------------------------------------------------- /error_map.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | var errMap = map[string]error{ 4 | "not_authed": ErrNotAuthed, 5 | } 6 | -------------------------------------------------------------------------------- /rtm.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | // Publishable is an interface that allows types to declare they are 4 | // "publishable". Meaning that you can send the RTM API the data returned by 5 | // the method Publishable 6 | type Publishable interface { 7 | Publishable() ([]byte, error) 8 | } 9 | -------------------------------------------------------------------------------- /rtm_broker.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/gorilla/websocket" 8 | ) 9 | 10 | // RTMBroker handles incoming and outgoing messages to a Slack RTM Websocket 11 | type RTMBroker struct { 12 | url string 13 | incoming chan []byte 14 | outgoing chan []byte 15 | events chan RTMEvent 16 | conn *websocket.Conn 17 | closed bool 18 | } 19 | 20 | // RTMEvent repesents a simple event received from Slack 21 | type RTMEvent struct { 22 | Type string `json:"type"` 23 | RawMessage json.RawMessage 24 | } 25 | 26 | // NewRTMBroker returns a connected broker to Slack from a rtm.start result 27 | func NewRTMBroker(s *RTMStartResult) *RTMBroker { 28 | broker := &RTMBroker{ 29 | url: s.URL, 30 | } 31 | 32 | return broker 33 | } 34 | 35 | // Connect connects to the RTM Websocket 36 | func (b *RTMBroker) Connect() error { 37 | conn, _, err := websocket.DefaultDialer.Dial(b.url, nil) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | b.conn = conn 43 | b.incoming = make(chan []byte, 0) 44 | b.events = make(chan RTMEvent, 0) 45 | 46 | go b.startRecv() 47 | go b.handleEvents() 48 | 49 | return nil 50 | } 51 | 52 | // Close Closes the connection to Slack RTM 53 | func (b *RTMBroker) Close() error { 54 | b.closed = true 55 | return b.conn.Close() 56 | } 57 | 58 | // Events returns a receive-only channel for all Events RTM API pushes 59 | // to the broker. 60 | func (b *RTMBroker) Events() <-chan RTMEvent { 61 | return b.events 62 | } 63 | 64 | // Publish pushes an event to the RTM Websocket 65 | func (b *RTMBroker) Publish(e Publishable) error { 66 | d, err := e.Publishable() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return b.conn.WriteMessage(websocket.TextMessage, d) 72 | } 73 | 74 | func (b *RTMBroker) startRecv() { 75 | for !b.closed { 76 | msgType, message, _ := b.conn.ReadMessage() 77 | if msgType == websocket.TextMessage { 78 | 79 | b.incoming <- message 80 | } 81 | 82 | time.Sleep(25 * time.Millisecond) 83 | } 84 | } 85 | 86 | func (b *RTMBroker) handleEvents() { 87 | for !b.closed { 88 | raw := json.RawMessage(<-b.incoming) 89 | 90 | rtmEvent := RTMEvent{ 91 | RawMessage: raw, 92 | } 93 | 94 | if err := json.Unmarshal(raw, &rtmEvent); err != nil { 95 | panic(err) 96 | } 97 | 98 | b.events <- rtmEvent 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /rtm_broker_test.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | var voidConnHandler = func(c *websocket.Conn) {} 15 | 16 | func TestRTMBroker(t *testing.T) { 17 | Convey("RTM Broker", t, func() { 18 | Convey("Events() returns RTMEvents on a channel", func() { 19 | server := generateServerWithResponse(`{"type":"message"}`, voidConnHandler) 20 | defer server.Close() 21 | start := &RTMStartResult{URL: server.URL} 22 | 23 | broker := NewRTMBroker(start) 24 | err := broker.Connect() 25 | 26 | So(err, ShouldBeNil) 27 | 28 | select { 29 | case event := <-broker.Events(): 30 | So(event.Type, ShouldEqual, "message") 31 | case <-time.After(1 * time.Second): 32 | So(true, ShouldBeFalse) 33 | } 34 | }) 35 | 36 | Convey("Publish() pushes an event to the server", func() { 37 | msg := RTMMessage{} 38 | done := make(chan bool) 39 | 40 | server := generateServerWithResponse(`{"type":"message"}`, func(c *websocket.Conn) { 41 | c.ReadJSON(&msg) 42 | done <- true 43 | }) 44 | defer server.Close() 45 | 46 | start := &RTMStartResult{URL: server.URL} 47 | 48 | broker := NewRTMBroker(start) 49 | err := broker.Connect() 50 | 51 | So(err, ShouldBeNil) 52 | 53 | broker.Publish(RTMMessage{Text: "hello world!"}) 54 | 55 | select { 56 | case <-done: 57 | So(msg.Type, ShouldEqual, "message") 58 | So(msg.Text, ShouldEqual, "hello world!") 59 | So(msg.ID, ShouldBeGreaterThan, 0) 60 | case <-time.After(1 * time.Second): 61 | So(true, ShouldBeFalse) 62 | } 63 | }) 64 | }) 65 | } 66 | 67 | func generateServerWithResponse(event string, f func(c *websocket.Conn)) *httptest.Server { 68 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 | upgrader := websocket.Upgrader{ 70 | CheckOrigin: func(r *http.Request) bool { return true }, 71 | } 72 | 73 | conn, err := upgrader.Upgrade(w, r, nil) 74 | if err != nil { 75 | http.Error(w, "could not create websock connection", http.StatusInternalServerError) 76 | } 77 | conn.WriteMessage(websocket.TextMessage, []byte(event)) 78 | go f(conn) 79 | })) 80 | server.URL = "ws" + strings.TrimPrefix(server.URL, "http") 81 | 82 | return server 83 | } 84 | -------------------------------------------------------------------------------- /rtm_events.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import "encoding/json" 4 | 5 | // RTMMessage represents a posted message in a channel 6 | type RTMMessage struct { 7 | Type string `json:"type"` 8 | Text string `json:"text"` 9 | Channel string `json:"channel"` 10 | User string `json:"user"` 11 | Ts string `json:"ts"` 12 | 13 | // This should only be set when publishing and is handled by Publishable() 14 | ID uint64 `json:"id"` 15 | } 16 | 17 | // Message converts an event to an RTMMessage. If the event type is not 18 | // "message", it returns an error 19 | func (e RTMEvent) Message() (*RTMMessage, error) { 20 | var msg RTMMessage 21 | if err := json.Unmarshal(e.RawMessage, &msg); err != nil { 22 | return nil, err 23 | } 24 | 25 | return &msg, nil 26 | } 27 | -------------------------------------------------------------------------------- /rtm_publishable_events.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/sdming/gosnow" 7 | ) 8 | 9 | var sf *gosnow.SnowFlake 10 | 11 | // Some events pushed to Slack require an ID that is always incrementing, 12 | // the Snowflake ID generator is perfect for this. 13 | func init() { 14 | snowflake, err := gosnow.Default() 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | sf = snowflake 20 | } 21 | 22 | // Publishable implements Publishable 23 | func (e RTMMessage) Publishable() ([]byte, error) { 24 | nextID, err := sf.Next() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | e.ID = nextID 30 | e.Type = "message" 31 | 32 | return json.Marshal(e) 33 | } 34 | -------------------------------------------------------------------------------- /rtm_start.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | // RTMStartResult contains the result of rtm.start in the Slack API 4 | type RTMStartResult struct { 5 | URL string `json:"url,omitempty"` 6 | } 7 | 8 | // RTMStart issues a start command for RTM. This is isually used for retrieving 9 | // a WebSocket URL to start listening / posting messages into Slack. 10 | func (c *APIClient) RTMStart() (*RTMStartResult, error) { 11 | var result RTMStartResult 12 | resp, err := c.slackMethod("rtm.start") 13 | if err != nil { 14 | return nil, err 15 | } 16 | defer resp.Body.Close() 17 | 18 | if err := ParseResponse(resp.Body, &result); err != nil { 19 | return nil, err 20 | } 21 | 22 | return &result, nil 23 | } 24 | -------------------------------------------------------------------------------- /rtm_start_test.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestRTM(t *testing.T) { 12 | Convey("RTM Methods", t, func() { 13 | Convey("rtm.start success", func() { 14 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | _, err := w.Write([]byte(`{"ok": true, "url": "wss:\/\/ms100.slack-msgs.com\/websocket\/bunk"}`)) 16 | if err != nil { 17 | panic(err) 18 | } 19 | })) 20 | Reset(func() { server.Close() }) 21 | 22 | client := NewAPIClient("my-token", server.URL) 23 | result, err := client.RTMStart() 24 | So(err, ShouldBeNil) 25 | So(result.URL, ShouldEqual, "wss://ms100.slack-msgs.com/websocket/bunk") 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /slacker.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | ) 8 | 9 | // DefaultAPIURL is the default URL + Path for slack API requests 10 | const DefaultAPIURL = "https://slack.com/api" 11 | 12 | // APIClient contains simple logic for starting the RTM Messaging API for Slack 13 | type APIClient struct { 14 | client *http.Client 15 | token string 16 | SlackURL string 17 | } 18 | 19 | // NewAPIClient returns a new APIClient with a token set. 20 | func NewAPIClient(token string, url string) *APIClient { 21 | if url == "" { 22 | url = DefaultAPIURL 23 | } 24 | 25 | return &APIClient{ 26 | client: http.DefaultClient, 27 | token: token, 28 | SlackURL: url, 29 | } 30 | } 31 | 32 | // RunMethod runs an RPC method and returns the response body as a byte slice 33 | func (c *APIClient) RunMethod(name string) ([]byte, error) { 34 | resp, err := c.slackMethod(name) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer resp.Body.Close() 39 | 40 | return ioutil.ReadAll(resp.Body) 41 | } 42 | 43 | func (c *APIClient) slackMethod(method string) (*http.Response, error) { 44 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s?token=%s", c.SlackURL, method, c.token), nil) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return c.client.Do(req) 51 | } 52 | 53 | func (c *APIClient) slackMethodAndParse(method string, dest interface{}) error { 54 | resp, err := c.slackMethod(method) 55 | if err != nil { 56 | return err 57 | } 58 | defer resp.Body.Close() 59 | 60 | return ParseResponse(resp.Body, dest) 61 | } 62 | -------------------------------------------------------------------------------- /slacker_test.go: -------------------------------------------------------------------------------- 1 | package slacker 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestSlacker(t *testing.T) { 10 | Convey("Slacker", t, func() { 11 | Convey("NewAPIClient()", func() { 12 | client := NewAPIClient("my-token", DefaultAPIURL) 13 | 14 | So(client.SlackURL, ShouldEqual, DefaultAPIURL) 15 | }) 16 | }) 17 | } 18 | --------------------------------------------------------------------------------