diff --git a/examples/pins/pins.go b/examples/pins/pins.go new file mode 100644 index 0000000..93de5c9 --- /dev/null +++ b/examples/pins/pins.go @@ -0,0 +1,123 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/nlopes/slack" +) + +/* + WARNING: This example is destructive in the sense that it create a channel called testpinning +*/ +func main() { + var ( + apiToken string + debug bool + ) + + flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") + flag.BoolVar(&debug, "debug", false, "Show JSON output") + flag.Parse() + + api := slack.New(apiToken) + if debug { + api.SetDebug(true) + } + + var ( + postAsUserName string + postAsUserID string + postToChannelID string + ) + + // Find the user to post as. + authTest, err := api.AuthTest() + if err != nil { + fmt.Printf("Error getting channels: %s\n", err) + return + } + + channelName := "testpinning" + + // Post as the authenticated user. + postAsUserName = authTest.User + postAsUserID = authTest.UserID + + // Create a temporary channel + channel, err := api.CreateChannel(channelName) + + if err != nil { + // If the channel exists, that means we just need to unarchive it + if err.Error() == "name_taken" { + err = nil + channels, err := api.GetChannels(false) + if err != nil { + fmt.Println("Could not retrieve channels") + return + } + for _, archivedChannel := range channels { + if archivedChannel.Name == channelName { + if archivedChannel.IsArchived { + err = api.UnarchiveChannel(archivedChannel.ID) + if err != nil { + fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err) + return + } + } + channel = &archivedChannel + break + } + } + } + if err != nil { + fmt.Printf("Error setting test channel for pinning: %s\n", err) + return + } + } + postToChannelID = channel.ID + + fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID) + + // Post a message. + postParams := slack.PostMessageParameters{} + channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams) + if err != nil { + fmt.Printf("Error posting message: %s\n", err) + return + } + + // Grab a reference to the message. + msgRef := slack.NewRefToMessage(channelID, timestamp) + + // Add message pin to channel + if err := api.AddPin(channelID, msgRef); err != nil { + fmt.Printf("Error adding pin: %s\n", err) + return + } + + // List all of the users pins. + listPins, _, err := api.ListPins(channelID, slack.NewListPinsParameters()) + if err != nil { + fmt.Printf("Error listing pins: %s\n", err) + return + } + fmt.Printf("\n") + fmt.Printf("All pins by %s...\n", authTest.User) + for _, item := range listPins { + fmt.Printf(" > Item type: %s\n", item.Type) + } + + // Remove the pin. + err = api.RemovePin(channelID, msgRef) + if err != nil { + fmt.Printf("Error remove pin: %s\n", err) + return + } + + if err = api.ArchiveChannel(channelID); err != nil { + fmt.Printf("Error archiving channel: %s\n", err) + return + } + +} diff --git a/pins.go b/pins.go new file mode 100644 index 0000000..acb4fb8 --- /dev/null +++ b/pins.go @@ -0,0 +1,106 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" +) + +const ( + DEFAULT_PINS_COUNT = 100 + DEFAULT_PINS_PAGE = 1 +) + +// ListPinsParameters contains all the optional parameters for the pins.list call +type ListPinsParameters struct { + Count int + Page int +} + +// NewListPinsParameters initializes the inputs to find all pins +// performed by a user. +func NewListPinsParameters() ListPinsParameters { + return ListPinsParameters{ + Count: DEFAULT_PINS_COUNT, + Page: DEFAULT_PINS_PAGE, + } +} + +type listPinsResponseFull struct { + Items []Item + Paging `json:"paging"` + SlackResponse +} + +// AddPin pins an item in a channel +func (api *Client) AddPin(channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("pins.add", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// RemovePin un-pins an item from a channel +func (api *Client) RemovePin(channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("pins.remove", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// ListPins returns information about the items a user reacted to. +func (api *Client) ListPins(channel string, params ListPinsParameters) ([]Item, *Paging, error) { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + if params.Count != DEFAULT_PINS_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_PINS_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + response := &listPinsResponseFull{} + err := post("pins.list", values, response, api.debug) + if err != nil { + return nil, nil, err + } + if !response.Ok { + return nil, nil, errors.New(response.Error) + } + return response.Items, &response.Paging, nil +} diff --git a/pins_test.go b/pins_test.go new file mode 100644 index 0000000..ee3f17c --- /dev/null +++ b/pins_test.go @@ -0,0 +1,230 @@ +package slack + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +type pinsHandler struct { + gotParams map[string]string + response string +} + +func newPinsHandler() *pinsHandler { + return &pinsHandler{ + gotParams: make(map[string]string), + response: `{ "ok": true }`, + } +} + +func (rh *pinsHandler) accumulateFormValue(k string, r *http.Request) { + if v := r.FormValue(k); v != "" { + rh.gotParams[k] = v + } +} + +func (rh *pinsHandler) handler(w http.ResponseWriter, r *http.Request) { + rh.accumulateFormValue("channel", r) + rh.accumulateFormValue("count", r) + rh.accumulateFormValue("file", r) + rh.accumulateFormValue("file_comment", r) + rh.accumulateFormValue("page", r) + rh.accumulateFormValue("timestamp", r) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(rh.response)) +} + +func TestSlack_AddPin(t *testing.T) { + once.Do(startServer) + SLACK_API = "http://" + serverAddr + "/" + api := New("testing-token") + tests := []struct { + channel string + ref ItemRef + wantParams map[string]string + }{ + { + "ChannelID", + NewRefToMessage("ChannelID", "123"), + map[string]string{ + "channel": "ChannelID", + "timestamp": "123", + }, + }, + { + "ChannelID", + NewRefToFile("FileID"), + map[string]string{ + "channel": "ChannelID", + "file": "FileID", + }, + }, + { + "ChannelID", + NewRefToComment("FileCommentID"), + map[string]string{ + "channel": "ChannelID", + "file_comment": "FileCommentID", + }, + }, + } + var rh *pinsHandler + http.HandleFunc("/pins.add", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) }) + for i, test := range tests { + rh = newPinsHandler() + err := api.AddPin(test.channel, test.ref) + if err != nil { + t.Fatalf("%d: Unexpected error: %s", i, err) + } + if !reflect.DeepEqual(rh.gotParams, test.wantParams) { + t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams) + } + } +} + +func TestSlack_RemovePin(t *testing.T) { + once.Do(startServer) + SLACK_API = "http://" + serverAddr + "/" + api := New("testing-token") + tests := []struct { + channel string + ref ItemRef + wantParams map[string]string + }{ + { + "ChannelID", + NewRefToMessage("ChannelID", "123"), + map[string]string{ + "channel": "ChannelID", + "timestamp": "123", + }, + }, + { + "ChannelID", + NewRefToFile("FileID"), + map[string]string{ + "channel": "ChannelID", + "file": "FileID", + }, + }, + { + "ChannelID", + NewRefToComment("FileCommentID"), + map[string]string{ + "channel": "ChannelID", + "file_comment": "FileCommentID", + }, + }, + } + var rh *pinsHandler + http.HandleFunc("/pins.remove", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) }) + for i, test := range tests { + rh = newPinsHandler() + err := api.RemovePin(test.channel, test.ref) + if err != nil { + t.Fatalf("%d: Unexpected error: %s", i, err) + } + if !reflect.DeepEqual(rh.gotParams, test.wantParams) { + t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams) + } + } +} + +func TestSlack_ListPins(t *testing.T) { + once.Do(startServer) + SLACK_API = "http://" + serverAddr + "/" + api := New("testing-token") + rh := newPinsHandler() + http.HandleFunc("/pins.list", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) }) + rh.response = `{"ok": true, + "items": [ + { + "type": "message", + "channel": "C1", + "message": { + "text": "hello", + "reactions": [ + { + "name": "astonished", + "count": 3, + "users": [ "U1", "U2", "U3" ] + }, + { + "name": "clock1", + "count": 3, + "users": [ "U1", "U2" ] + } + ] + } + }, + { + "type": "file", + "file": { + "name": "toy", + "reactions": [ + { + "name": "clock1", + "count": 3, + "users": [ "U1", "U2" ] + } + ] + } + }, + { + "type": "file_comment", + "file": { + "name": "toy" + }, + "comment": { + "comment": "cool toy", + "reactions": [ + { + "name": "astonished", + "count": 3, + "users": [ "U1", "U2", "U3" ] + } + ] + } + } + ], + "paging": { + "count": 100, + "total": 4, + "page": 1, + "pages": 1 + }}` + want := []Item{ + NewMessageItem("C1", &Message{Msg: Msg{Text: "hello"}}), + NewFileItem(&File{Name: "toy"}), + NewFileCommentItem(&File{Name: "toy"}, &Comment{Comment: "cool toy"}), + } + wantParams := map[string]string{ + "channel": "ChannelID", + "count": "200", + "page": "2", + } + params := NewListPinsParameters() + params.Count = 200 + params.Page = 2 + got, paging, err := api.ListPins("ChannelID", params) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Got Pins %#v, want %#v", got, want) + for i, item := range got { + fmt.Printf("Item %d, Type: %s\n", i, item.Type) + fmt.Printf("Message %#v\n", item.Message) + fmt.Printf("File %#v\n", item.File) + fmt.Printf("Comment %#v\n", item.Comment) + } + } + if !reflect.DeepEqual(rh.gotParams, wantParams) { + t.Errorf("Got params %#v, want %#v", rh.gotParams, wantParams) + } + if reflect.DeepEqual(paging, Paging{}) { + t.Errorf("Want paging data, got empty struct") + } +} diff --git a/websocket_managed_conn.go b/websocket_managed_conn.go index 9e20518..c1ecfd1 100644 --- a/websocket_managed_conn.go +++ b/websocket_managed_conn.go @@ -388,6 +388,9 @@ "file_comment_edited": FileCommentEditedEvent{}, "file_comment_deleted": FileCommentDeletedEvent{}, + "pin_added": PinAddedEvent{}, + "pin_removed": PinRemovedEvent{}, + "star_added": StarAddedEvent{}, "star_removed": StarRemovedEvent{}, diff --git a/websocket_pins.go b/websocket_pins.go new file mode 100644 index 0000000..463bf38 --- /dev/null +++ b/websocket_pins.go @@ -0,0 +1,16 @@ +package slack + +type pinEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item Item `json:"item"` + Channel string `json:"channel_id"` + EventTimestamp JSONTimeString `json:"event_ts"` + HasPins bool `json:"has_pins,omitempty"` +} + +// PinAddedEvent represents the Pin added event +type PinAddedEvent pinEvent + +// PinRemovedEvent represents the Pin removed event +type PinRemovedEvent pinEvent