From 7ff6c6d4dbdcadb324cb19051466d1bb424854a3 Mon Sep 17 00:00:00 2001 From: Jaro Spisak <61154065+jarospisak-unity@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:01:37 +0300 Subject: [PATCH] feat: Add support for Canvas API methods (#1334) This PR introduces new functionalities for managing canvases and creating channel-specific canvases. - CreateCanvas - DeleteCanvas - EditCanvas - SetCanvasAccess - DeleteCanvasAccess - LookupCanvasSections - CreateChannelCanvas Closes #1333 --- canvas.go | 264 +++++++++++++++++++++++++++++++++++++++++++ canvas_test.go | 216 +++++++++++++++++++++++++++++++++++ conversation.go | 34 ++++++ conversation_test.go | 31 +++++ 4 files changed, 545 insertions(+) create mode 100644 canvas.go create mode 100644 canvas_test.go diff --git a/canvas.go b/canvas.go new file mode 100644 index 00000000..5225afa3 --- /dev/null +++ b/canvas.go @@ -0,0 +1,264 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" +) + +type CanvasDetails struct { + CanvasID string `json:"canvas_id"` +} + +type DocumentContent struct { + Type string `json:"type"` + Markdown string `json:"markdown,omitempty"` +} + +type CanvasChange struct { + Operation string `json:"operation"` + SectionID string `json:"section_id,omitempty"` + DocumentContent DocumentContent `json:"document_content"` +} + +type EditCanvasParams struct { + CanvasID string `json:"canvas_id"` + Changes []CanvasChange `json:"changes"` +} + +type SetCanvasAccessParams struct { + CanvasID string `json:"canvas_id"` + AccessLevel string `json:"access_level"` + ChannelIDs []string `json:"channel_ids,omitempty"` + UserIDs []string `json:"user_ids,omitempty"` +} + +type DeleteCanvasAccessParams struct { + CanvasID string `json:"canvas_id"` + ChannelIDs []string `json:"channel_ids,omitempty"` + UserIDs []string `json:"user_ids,omitempty"` +} + +type LookupCanvasSectionsCriteria struct { + SectionTypes []string `json:"section_types,omitempty"` + ContainsText string `json:"contains_text,omitempty"` +} + +type LookupCanvasSectionsParams struct { + CanvasID string `json:"canvas_id"` + Criteria LookupCanvasSectionsCriteria `json:"criteria"` +} + +type CanvasSection struct { + ID string `json:"id"` +} + +type LookupCanvasSectionsResponse struct { + SlackResponse + Sections []CanvasSection `json:"sections"` +} + +// CreateCanvas creates a new canvas. +// For more details, see CreateCanvasContext documentation. +func (api *Client) CreateCanvas(title string, documentContent DocumentContent) (string, error) { + return api.CreateCanvasContext(context.Background(), title, documentContent) +} + +// CreateCanvasContext creates a new canvas with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.create +func (api *Client) CreateCanvasContext(ctx context.Context, title string, documentContent DocumentContent) (string, error) { + values := url.Values{ + "token": {api.token}, + } + if title != "" { + values.Add("title", title) + } + if documentContent.Type != "" { + documentContentJSON, err := json.Marshal(documentContent) + if err != nil { + return "", err + } + values.Add("document_content", string(documentContentJSON)) + } + + response := struct { + SlackResponse + CanvasID string `json:"canvas_id"` + }{} + + err := api.postMethod(ctx, "canvases.create", values, &response) + if err != nil { + return "", err + } + + return response.CanvasID, response.Err() +} + +// DeleteCanvas deletes an existing canvas. +// For more details, see DeleteCanvasContext documentation. +func (api *Client) DeleteCanvas(canvasID string) error { + return api.DeleteCanvasContext(context.Background(), canvasID) +} + +// DeleteCanvasContext deletes an existing canvas with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.delete +func (api *Client) DeleteCanvasContext(ctx context.Context, canvasID string) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {canvasID}, + } + + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "canvases.delete", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// EditCanvas edits an existing canvas. +// For more details, see EditCanvasContext documentation. +func (api *Client) EditCanvas(params EditCanvasParams) error { + return api.EditCanvasContext(context.Background(), params) +} + +// EditCanvasContext edits an existing canvas with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.edit +func (api *Client) EditCanvasContext(ctx context.Context, params EditCanvasParams) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + } + + changesJSON, err := json.Marshal(params.Changes) + if err != nil { + return err + } + values.Add("changes", string(changesJSON)) + + response := struct { + SlackResponse + }{} + + err = api.postMethod(ctx, "canvases.edit", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// SetCanvasAccess sets the access level to a canvas for specified entities. +// For more details, see SetCanvasAccessContext documentation. +func (api *Client) SetCanvasAccess(params SetCanvasAccessParams) error { + return api.SetCanvasAccessContext(context.Background(), params) +} + +// SetCanvasAccessContext sets the access level to a canvas for specified entities with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.access.set +func (api *Client) SetCanvasAccessContext(ctx context.Context, params SetCanvasAccessParams) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + "access_level": {params.AccessLevel}, + } + if len(params.ChannelIDs) > 0 { + channelIDsJSON, err := json.Marshal(params.ChannelIDs) + if err != nil { + return err + } + values.Add("channel_ids", string(channelIDsJSON)) + } + if len(params.UserIDs) > 0 { + userIDsJSON, err := json.Marshal(params.UserIDs) + if err != nil { + return err + } + values.Add("user_ids", string(userIDsJSON)) + } + + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "canvases.access.set", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// DeleteCanvasAccess removes access to a canvas for specified entities. +// For more details, see DeleteCanvasAccessContext documentation. +func (api *Client) DeleteCanvasAccess(params DeleteCanvasAccessParams) error { + return api.DeleteCanvasAccessContext(context.Background(), params) +} + +// DeleteCanvasAccessContext removes access to a canvas for specified entities with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.access.delete +func (api *Client) DeleteCanvasAccessContext(ctx context.Context, params DeleteCanvasAccessParams) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + } + if len(params.ChannelIDs) > 0 { + channelIDsJSON, err := json.Marshal(params.ChannelIDs) + if err != nil { + return err + } + values.Add("channel_ids", string(channelIDsJSON)) + } + if len(params.UserIDs) > 0 { + userIDsJSON, err := json.Marshal(params.UserIDs) + if err != nil { + return err + } + values.Add("user_ids", string(userIDsJSON)) + } + + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "canvases.access.delete", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// LookupCanvasSections finds sections matching the provided criteria. +// For more details, see LookupCanvasSectionsContext documentation. +func (api *Client) LookupCanvasSections(params LookupCanvasSectionsParams) ([]CanvasSection, error) { + return api.LookupCanvasSectionsContext(context.Background(), params) +} + +// LookupCanvasSectionsContext finds sections matching the provided criteria with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.sections.lookup +func (api *Client) LookupCanvasSectionsContext(ctx context.Context, params LookupCanvasSectionsParams) ([]CanvasSection, error) { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + } + + criteriaJSON, err := json.Marshal(params.Criteria) + if err != nil { + return nil, err + } + values.Add("criteria", string(criteriaJSON)) + + response := LookupCanvasSectionsResponse{} + + err = api.postMethod(ctx, "canvases.sections.lookup", values, &response) + if err != nil { + return nil, err + } + + return response.Sections, response.Err() +} diff --git a/canvas_test.go b/canvas_test.go new file mode 100644 index 00000000..c0e30103 --- /dev/null +++ b/canvas_test.go @@ -0,0 +1,216 @@ +package slack + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" +) + +func createCanvasHandler(rw http.ResponseWriter, r *http.Request) { + title := r.FormValue("title") + documentContent := r.FormValue("document_content") + + rw.Header().Set("Content-Type", "application/json") + + if title != "" && documentContent != "" { + resp, _ := json.Marshal(&struct { + SlackResponse + CanvasID string `json:"canvas_id"` + }{ + SlackResponse: SlackResponse{Ok: true}, + CanvasID: "F1234ABCD", + }) + rw.Write(resp) + } else { + rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) + } +} + +func TestCreateCanvas(t *testing.T) { + http.HandleFunc("/canvases.create", createCanvasHandler) + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + documentContent := DocumentContent{ + Type: "markdown", + Markdown: "Test Content", + } + + canvasID, err := api.CreateCanvas("Test Canvas", documentContent) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if canvasID != "F1234ABCD" { + t.Fatalf("Expected canvas ID to be F1234ABCD, got %s", canvasID) + } +} + +func deleteCanvasHandler(rw http.ResponseWriter, r *http.Request) { + canvasID := r.FormValue("canvas_id") + + rw.Header().Set("Content-Type", "application/json") + + if canvasID == "F1234ABCD" { + rw.Write([]byte(`{ "ok": true }`)) + } else { + rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) + } +} + +func TestDeleteCanvas(t *testing.T) { + http.HandleFunc("/canvases.delete", deleteCanvasHandler) + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + err := api.DeleteCanvas("F1234ABCD") + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } +} + +func editCanvasHandler(rw http.ResponseWriter, r *http.Request) { + canvasID := r.FormValue("canvas_id") + + rw.Header().Set("Content-Type", "application/json") + + if canvasID == "F1234ABCD" { + rw.Write([]byte(`{ "ok": true }`)) + } else { + rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) + } +} + +func TestEditCanvas(t *testing.T) { + http.HandleFunc("/canvases.edit", editCanvasHandler) + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + params := EditCanvasParams{ + CanvasID: "F1234ABCD", + Changes: []CanvasChange{ + { + Operation: "update", + SectionID: "S1234", + DocumentContent: DocumentContent{ + Type: "markdown", + Markdown: "Updated Content", + }, + }, + }, + } + + err := api.EditCanvas(params) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } +} + +func setCanvasAccessHandler(rw http.ResponseWriter, r *http.Request) { + canvasID := r.FormValue("canvas_id") + + rw.Header().Set("Content-Type", "application/json") + + if canvasID == "F1234ABCD" { + rw.Write([]byte(`{ "ok": true }`)) + } else { + rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) + } +} + +func TestSetCanvasAccess(t *testing.T) { + http.HandleFunc("/canvases.access.set", setCanvasAccessHandler) + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + params := SetCanvasAccessParams{ + CanvasID: "F1234ABCD", + AccessLevel: "read", + ChannelIDs: []string{"C1234ABCD"}, + UserIDs: []string{"U1234ABCD"}, + } + + err := api.SetCanvasAccess(params) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } +} + +func deleteCanvasAccessHandler(rw http.ResponseWriter, r *http.Request) { + canvasID := r.FormValue("canvas_id") + + rw.Header().Set("Content-Type", "application/json") + + if canvasID == "F1234ABCD" { + rw.Write([]byte(`{ "ok": true }`)) + } else { + rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) + } +} + +func TestDeleteCanvasAccess(t *testing.T) { + http.HandleFunc("/canvases.access.delete", deleteCanvasAccessHandler) + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + params := DeleteCanvasAccessParams{ + CanvasID: "F1234ABCD", + ChannelIDs: []string{"C1234ABCD"}, + UserIDs: []string{"U1234ABCD"}, + } + + err := api.DeleteCanvasAccess(params) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } +} + +func lookupCanvasSectionsHandler(rw http.ResponseWriter, r *http.Request) { + canvasID := r.FormValue("canvas_id") + + rw.Header().Set("Content-Type", "application/json") + + if canvasID == "F1234ABCD" { + sections := []CanvasSection{ + {ID: "S1234"}, + {ID: "S5678"}, + } + + resp, _ := json.Marshal(&LookupCanvasSectionsResponse{ + SlackResponse: SlackResponse{Ok: true}, + Sections: sections, + }) + rw.Write(resp) + } else { + rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) + } +} + +func TestLookupCanvasSections(t *testing.T) { + http.HandleFunc("/canvases.sections.lookup", lookupCanvasSectionsHandler) + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + params := LookupCanvasSectionsParams{ + CanvasID: "F1234ABCD", + Criteria: LookupCanvasSectionsCriteria{ + SectionTypes: []string{"h1", "h2"}, + ContainsText: "Test", + }, + } + + sections, err := api.LookupCanvasSections(params) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + expectedSections := []CanvasSection{ + {ID: "S1234"}, + {ID: "S5678"}, + } + + if !reflect.DeepEqual(expectedSections, sections) { + t.Fatalf("Expected sections %v, got %v", expectedSections, sections) + } +} diff --git a/conversation.go b/conversation.go index 58169d6d..d1eb07d0 100644 --- a/conversation.go +++ b/conversation.go @@ -2,6 +2,7 @@ package slack import ( "context" + "encoding/json" "errors" "net/url" "strconv" @@ -878,3 +879,36 @@ func (api *Client) InviteSharedContext(ctx context.Context, channelID string, pa func (api *Client) InviteShared(channelID string, params InviteSharedParams) error { return api.InviteSharedContext(context.Background(), channelID, params) } + +// CreateChannelCanvas creates a new canvas in a channel. +// For more details, see CreateChannelCanvasContext documentation. +func (api *Client) CreateChannelCanvas(channel string, documentContent DocumentContent) (string, error) { + return api.CreateChannelCanvasContext(context.Background(), channel, documentContent) +} + +// CreateChannelCanvasContext creates a new canvas in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.canvases.create +func (api *Client) CreateChannelCanvasContext(ctx context.Context, channel string, documentContent DocumentContent) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {channel}, + } + if documentContent.Type != "" { + documentContentJSON, err := json.Marshal(documentContent) + if err != nil { + return "", err + } + values.Add("document_content", string(documentContentJSON)) + } + + response := struct { + SlackResponse + CanvasID string `json:"canvas_id"` + }{} + err := api.postMethod(ctx, "conversations.canvases.create", values, &response) + if err != nil { + return "", err + } + + return response.CanvasID, response.Err() +} diff --git a/conversation_test.go b/conversation_test.go index 0ee6f4d4..6b3576c9 100644 --- a/conversation_test.go +++ b/conversation_test.go @@ -743,3 +743,34 @@ func TestMarkConversation(t *testing.T) { return } } + +func createChannelCanvasHandler(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(struct { + SlackResponse + CanvasID string `json:"canvas_id"` + }{ + SlackResponse: SlackResponse{Ok: true}, + CanvasID: "F05RQ01LJU0", + }) + rw.Write(response) +} + +func TestCreateChannelCanvas(t *testing.T) { + http.HandleFunc("/conversations.canvases.create", createChannelCanvasHandler) + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + documentContent := DocumentContent{ + Type: "markdown", + Markdown: "> channel canvas!", + } + + canvasID, err := api.CreateChannelCanvas("C1234567890", documentContent) + if err != nil { + t.Errorf("Failed to create channel canvas: %v", err) + return + } + + assert.Equal(t, "F05RQ01LJU0", canvasID) +}