Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bookmarks.<add|edit|remove|list> support #1044

Merged
merged 7 commits into from Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"`
kanata2 marked this conversation as resolved.
Show resolved Hide resolved
}

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
kanata2 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}