Skip to content

Commit

Permalink
Merge pull request #1044 from winston-stripe/add-bookmarks
Browse files Browse the repository at this point in the history
Add `bookmarks.<add|edit|remove|list>` support
  • Loading branch information
kanata2 committed Apr 26, 2022
2 parents 835fab1 + 8797bb0 commit d28b536
Show file tree
Hide file tree
Showing 2 changed files with 396 additions and 0 deletions.
159 changes: 159 additions & 0 deletions bookmarks.go
@@ -0,0 +1,159 @@
package slack

import (
"context"
"net/url"
)

type Bookmark struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
Title string `json:"title"`
Link string `json:"link"`
Emoji string `json:"emoji"`
IconURL string `json:"icon_url"`
Type string `json:"type"`
Created JSONTime `json:"date_created"`
Updated JSONTime `json:"date_updated"`
Rank string `json:"rank"`

LastUpdatedByUserID string `json:"last_updated_by_user_id"`
LastUpdatedByTeamID string `json:"last_updated_by_team_id"`

ShortcutID string `json:"shortcut_id"`
EntityID string `json:"entity_id"`
AppID string `json:"app_id"`
}

type AddBookmarkParameters struct {
Title string // A required title for the bookmark
Type string // A required type for the bookmark
Link string // URL required for type:link
Emoji string // An optional emoji
EntityID string
ParentID string
}

type EditBookmarkParameters struct {
Title *string // Change the title. Set to "" to clear
Emoji *string // Change the emoji. Set to "" to clear
Link string // Change the link
}

type addBookmarkResponse struct {
Bookmark Bookmark `json:"bookmark"`
SlackResponse
}

type editBookmarkResponse struct {
Bookmark Bookmark `json:"bookmark"`
SlackResponse
}

type listBookmarksResponse struct {
Bookmarks []Bookmark `json:"bookmarks"`
SlackResponse
}

// AddBookmark adds a bookmark in a channel
func (api *Client) AddBookmark(channelID string, params AddBookmarkParameters) (Bookmark, error) {
return api.AddBookmarkContext(context.Background(), channelID, params)
}

// AddBookmarkContext adds a bookmark in a channel with a custom context
func (api *Client) AddBookmarkContext(ctx context.Context, channelID string, params AddBookmarkParameters) (Bookmark, error) {
values := url.Values{
"channel_id": {channelID},
"token": {api.token},
"title": {params.Title},
"type": {params.Type},
}
if params.Link != "" {
values.Set("link", params.Link)
}
if params.Emoji != "" {
values.Set("emoji", params.Emoji)
}
if params.EntityID != "" {
values.Set("entity_id", params.EntityID)
}
if params.ParentID != "" {
values.Set("parent_id", params.ParentID)
}

response := &addBookmarkResponse{}
if err := api.postMethod(ctx, "bookmarks.add", values, response); err != nil {
return Bookmark{}, err
}

return response.Bookmark, response.Err()
}

// RemoveBookmark removes a bookmark from a channel
func (api *Client) RemoveBookmark(channelID, bookmarkID string) error {
return api.RemoveBookmarkContext(context.Background(), channelID, bookmarkID)
}

// RemoveBookmarkContext removes a bookmark from a channel with a custom context
func (api *Client) RemoveBookmarkContext(ctx context.Context, channelID, bookmarkID string) error {
values := url.Values{
"channel_id": {channelID},
"token": {api.token},
"bookmark_id": {bookmarkID},
}

response := &SlackResponse{}
if err := api.postMethod(ctx, "bookmarks.remove", values, response); err != nil {
return err
}

return response.Err()
}

// ListBookmarks returns all bookmarks for a channel.
func (api *Client) ListBookmarks(channelID string) ([]Bookmark, error) {
return api.ListBookmarksContext(context.Background(), channelID)
}

// ListBookmarksContext returns all bookmarks for a channel with a custom context.
func (api *Client) ListBookmarksContext(ctx context.Context, channelID string) ([]Bookmark, error) {
values := url.Values{
"channel_id": {channelID},
"token": {api.token},
}

response := &listBookmarksResponse{}
err := api.postMethod(ctx, "bookmarks.list", values, response)
if err != nil {
return nil, err
}
return response.Bookmarks, response.Err()
}

func (api *Client) EditBookmark(channelID, bookmarkID string, params EditBookmarkParameters) (Bookmark, error) {
return api.EditBookmarkContext(context.Background(), channelID, bookmarkID, params)
}

func (api *Client) EditBookmarkContext(ctx context.Context, channelID, bookmarkID string, params EditBookmarkParameters) (Bookmark, error) {
values := url.Values{
"channel_id": {channelID},
"token": {api.token},
"bookmark_id": {bookmarkID},
}
if params.Link != "" {
values.Set("link", params.Link)
}
if params.Emoji != nil {
values.Set("emoji", *params.Emoji)
}
if params.Title != nil {
values.Set("title", *params.Title)
}

response := &editBookmarkResponse{}
if err := api.postMethod(ctx, "bookmarks.edit", values, response); err != nil {
return Bookmark{}, err
}

return response.Bookmark, response.Err()
}
237 changes: 237 additions & 0 deletions bookmarks_test.go
@@ -0,0 +1,237 @@
package slack

import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)

func getTestBookmark(channelID, bookmarkID string) Bookmark {
return Bookmark{
ID: bookmarkID,
ChannelID: channelID,
Title: "bookmark",
Type: "link",
Link: "https://example.com",
IconURL: "https://example.com/icon.png",
}
}

func addBookmarkLinkHandler(rw http.ResponseWriter, r *http.Request) {
channelID := r.FormValue("channel_id")
title := r.FormValue("title")
bookmarkType := r.FormValue("type")
link := r.FormValue("link")

rw.Header().Set("Content-Type", "application/json")

if bookmarkType == "link" && link != "" && channelID != "" && title != "" {
bookmark := getTestBookmark(channelID, "Bk123RBZG8GZ")
bookmark.Title = title
bookmark.Type = bookmarkType
bookmark.Link = link

resp, _ := json.Marshal(&addBookmarkResponse{
SlackResponse: SlackResponse{Ok: true},
Bookmark: bookmark})
rw.Write(resp)
} else {
rw.Write([]byte(`{ "ok": false, "error": "errored" }`))
}
}

func TestAddBookmarkLink(t *testing.T) {
http.HandleFunc("/bookmarks.add", addBookmarkLinkHandler)
once.Do(startServer)
api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
params := AddBookmarkParameters{
Title: "test",
Type: "link",
Link: "https://example.com",
}
_, err := api.AddBookmark("CXXXXXXXX", params)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
}

func listBookmarksHandler(rw http.ResponseWriter, r *http.Request) {
channelID := r.FormValue("channel_id")

rw.Header().Set("Content-Type", "application/json")

if channelID != "" {
bookmarks := []Bookmark{
getTestBookmark(channelID, "Bk001"),
getTestBookmark(channelID, "Bk002"),
getTestBookmark(channelID, "Bk003"),
getTestBookmark(channelID, "Bk004"),
}

resp, _ := json.Marshal(&listBookmarksResponse{
SlackResponse: SlackResponse{Ok: true},
Bookmarks: bookmarks})
rw.Write(resp)
} else {
rw.Write([]byte(`{ "ok": false, "error": "errored" }`))
}
}

func TestListBookmarks(t *testing.T) {
http.HandleFunc("/bookmarks.list", listBookmarksHandler)
once.Do(startServer)
api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
channel := "CXXXXXXXX"
bookmarks, err := api.ListBookmarks(channel)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}

if !reflect.DeepEqual([]Bookmark{
getTestBookmark(channel, "Bk001"),
getTestBookmark(channel, "Bk002"),
getTestBookmark(channel, "Bk003"),
getTestBookmark(channel, "Bk004"),
}, bookmarks) {
t.Fatal(ErrIncorrectResponse)
}
}

func removeBookmarkHandler(bookmark *Bookmark) func(rw http.ResponseWriter, r *http.Request) {
return func(rw http.ResponseWriter, r *http.Request) {
channelID := r.FormValue("channel_id")
bookmarkID := r.FormValue("bookmark_id")

rw.Header().Set("Content-Type", "application/json")

if channelID == bookmark.ChannelID && bookmarkID == bookmark.ID {
rw.Write([]byte(`{ "ok": true }`))
} else {
rw.Write([]byte(`{ "ok": false, "error": "errored" }`))
}
}
}

func TestRemoveBookmark(t *testing.T) {
channel := "CXXXXXXXX"
bookmark := getTestBookmark(channel, "BkXXXXX")
http.HandleFunc("/bookmarks.remove", removeBookmarkHandler(&bookmark))
once.Do(startServer)
api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))

err := api.RemoveBookmark(channel, bookmark.ID)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
}

func editBookmarkHandler(bookmarks []Bookmark) func(rw http.ResponseWriter, r *http.Request) {
return func(rw http.ResponseWriter, r *http.Request) {
channelID := r.FormValue("channel_id")
bookmarkID := r.FormValue("bookmark_id")

rw.Header().Set("Content-Type", "application/json")
if err := r.ParseForm(); err != nil {
httpTestErrReply(rw, true, fmt.Sprintf("err parsing form: %s", err.Error()))
return
}

for _, bookmark := range bookmarks {
if bookmark.ID == bookmarkID && bookmark.ChannelID == channelID {
if v := r.Form.Get("link"); v != "" {
bookmark.Link = v
}
// Emoji and title require special handling since empty string sets to null
if _, ok := r.Form["emoji"]; ok {
bookmark.Emoji = r.Form.Get("emoji")
}
if _, ok := r.Form["title"]; ok {
bookmark.Title = r.Form.Get("title")
}
resp, _ := json.Marshal(&editBookmarkResponse{
SlackResponse: SlackResponse{Ok: true},
Bookmark: bookmark})
rw.Write(resp)
return
}
}
// Fail if the bookmark doesn't exist
rw.Write([]byte(`{ "ok": false, "error": "not_found" }`))
}
}

func TestEditBookmark(t *testing.T) {
channel := "CXXXXXXXX"
bookmarks := []Bookmark{
getTestBookmark(channel, "Bk001"),
getTestBookmark(channel, "Bk002"),
getTestBookmark(channel, "Bk003"),
getTestBookmark(channel, "Bk004"),
}
http.HandleFunc("/bookmarks.edit", editBookmarkHandler(bookmarks))
once.Do(startServer)
api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))

smileEmoji := ":smile:"
empty := ""
title := "hello, world!"
changes := []struct {
ID string
Params EditBookmarkParameters
}{
{ // add emoji
ID: "Bk001",
Params: EditBookmarkParameters{Emoji: &smileEmoji},
},
{ // delete emoji
ID: "Bk001",
Params: EditBookmarkParameters{Emoji: &empty},
},
{ // add title
ID: "Bk002",
Params: EditBookmarkParameters{Title: &title},
},
{ // delete title
ID: "Bk002",
Params: EditBookmarkParameters{Title: &empty},
},
{ // Change multiple fields at once
ID: "Bk003",
Params: EditBookmarkParameters{
Title: &title,
Emoji: &empty,
Link: "https://example.com/changed",
},
},
{ // noop
ID: "Bk004",
},
}

for _, change := range changes {
bookmark, err := api.EditBookmark(channel, change.ID, change.Params)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
if change.ID != bookmark.ID {
t.Fatalf("expected to modify bookmark with ID = %s, got %s", change.ID, bookmark.ID)
}
if change.Params.Emoji != nil && bookmark.Emoji != *change.Params.Emoji {
t.Fatalf("expected bookmark.Emoji = %s, got %s", *change.Params.Emoji, bookmark.Emoji)
}
if change.Params.Title != nil && bookmark.Title != *change.Params.Title {
t.Fatalf("expected bookmark.Title = %s, got %s", *change.Params.Title, bookmark.Emoji)
}
if change.Params.Link != "" && change.Params.Link != bookmark.Link {
t.Fatalf("expected bookmark.Link = %s, got %s", change.Params.Link, bookmark.Link)
}
}

// Cover the final case of trying to edit a bookmark which doesn't exist
bookmark, err := api.EditBookmark(channel, "BkMissing", EditBookmarkParameters{})
if err == nil {
t.Fatalf("Expected not found error, but got bookmark %s", bookmark.ID)
}
}

0 comments on commit d28b536

Please sign in to comment.