From f7c27ea5008a7fd1b34d5a702f6579a517c362f4 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 3 Aug 2023 15:51:51 +0200 Subject: [PATCH 01/17] Create structs for manifest endpoint --- manifests.go | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 manifests.go diff --git a/manifests.go b/manifests.go new file mode 100644 index 000000000..e82c1cb41 --- /dev/null +++ b/manifests.go @@ -0,0 +1,112 @@ +package slack + +// Manifest is an application manifest schema +type Manifest struct { + Metadata ManifestMetadata `json:"_metadata,omitempty" yaml:"_metadata,omitempty"` + Display Display `json:"display_information" yaml:"display_information"` + Settings Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Features Features `json:"features,omitempty" yaml:"features,omitempty"` + OAuthConfig OAuthConfig `json:"oauth_config,omitempty" yaml:"oauth_config,omitempty"` +} + +// ManifestMetadata is a group of settings that describe the manifest +type ManifestMetadata struct { + MajorVersion int `json:"major_version,omitempty" yaml:"major_version,omitempty"` + MinorVersion int `json:"minor_version,omitempty" yaml:"minor_version,omitempty"` +} + +// Display is a group of settings that describe parts of an app's appearance within Slack +type Display struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + LongDescription string `json:"long_description,omitempty" yaml:"long_description,omitempty"` + BackgroundColor string `json:"background_color,omitempty" yaml:"background_color,omitempty"` +} + +// Settings is a group of settings corresponding to the Settings section of the app config pages. +type Settings struct { + AllowedIPAddressRanges []string `json:"allowed_ip_address_ranges,omitempty" yaml:"allowed_ip_address_ranges,omitempty"` + EventSubscriptions EventSubscriptions `json:"event_subscriptions,omitempty" yaml:"event_subscriptions,omitempty"` + Interactivity Interactivity `json:"interactivity,omitempty" yaml:"interactivity,omitempty"` + OrgDeployEnabled bool `json:"org_deploy_enabled,omitempty" yaml:"org_deploy_enabled,omitempty"` + SocketModeEnabled bool `json:"socket_mode_enabled,omitempty" yaml:"socket_mode_enabled,omitempty"` +} + +// EventSubscriptions is a group of settings that describe the Events API configuration +type EventSubscriptions struct { + RequestUrl string `json:"request_url,omitempty" yaml:"request_url,omitempty"` + BotEvents []string `json:"bot_events,omitempty" yaml:"bot_events,omitempty"` + UserEvents []string `json:"user_events,omitempty" yaml:"user_events,omitempty"` +} + +// Interactivity is a group of settings that describe the interactivity configuration +type Interactivity struct { + IsEnabled bool `json:"is_enabled" yaml:"is_enabled"` + RequestUrl string `json:"request_url,omitempty" yaml:"request_url,omitempty"` + MessageMenuOptionsUrl string `json:"message_menu_options_url,omitempty" yaml:"message_menu_options_url,omitempty"` +} + +// Features is a group of settings corresponding to the Features section of the app config pages +type Features struct { + AppHome AppHome `json:"app_home,omitempty" yaml:"app_home,omitempty"` + BotUser BotUser `json:"bot_user,omitempty" yaml:"bot_user,omitempty"` + Shortcuts []Shortcut `json:"shortcuts,omitempty" yaml:"shortcuts,omitempty"` + SlashCommands []ManifestSlashCommand `json:"slash_commands,omitempty" yaml:"slash_commands,omitempty"` + WorkflowSteps []WorkflowStep `json:"workflow_steps,omitempty" yaml:"workflow_steps,omitempty"` +} + +// AppHome is a group of settings that describe the App Home configuration +type AppHome struct { + HomeTabEnabled bool `json:"home_tab_enabled,omitempty" yaml:"home_tab_enabled,omitempty"` + MessagesTabEnabled bool `json:"messages_tab_enabled,omitempty" yaml:"messages_tab_enabled,omitempty"` + MessagesTabReadOnlyEnabled bool `json:"messages_tab_read_only_enabled,omitempty" yaml:"messages_tab_read_only_enabled,omitempty"` +} + +// BotUser is a group of settings that describe bot user configuration +type BotUser struct { + DisplayName string `json:"display_name" yaml:"display_name"` + AlwaysOnline bool `json:"always_online,omitempty" yaml:"always_online,omitempty"` +} + +// Shortcut is a group of settings that describes shortcut configuration +type Shortcut struct { + Name string `json:"name" yaml:"name"` + CallbackID string `json:"callback_id" yaml:"callback_id"` + Description string `json:"description" yaml:"description"` + Type ShortcutType `json:"type" yaml:"type"` +} + +// ShortcutType is a new string type for the available types of shortcuts +type ShortcutType string + +const ( + MessageShortcut ShortcutType = "message" + GlobalShortcut ShortcutType = "global" +) + +// ManifestSlashCommand is a group of settings that describes slash command configuration +type ManifestSlashCommand struct { + Command string `json:"command" yaml:"command"` + Description string `json:"description" yaml:"description"` + ShouldEscape bool `json:"should_escape,omitempty" yaml:"should_escape,omitempty"` + Url string `json:"url,omitempty" yaml:"url,omitempty"` + UsageHint string `json:"usage_hint,omitempty" yaml:"usage_hint,omitempty"` +} + +// WorkflowStep is a group of settings that describes workflow steps configuration +type WorkflowStep struct { + Name string `json:"name" yaml:"name"` + CallbackID string `json:"callback_id", yaml:"callback_id"` +} + +// OAuthConfig is a group of settings that describe OAuth configuration for the app +type OAuthConfig struct { + RedirectUrls []string `json:"redirect_urls,omitempty" yaml:"redirect_urls,omitempty"` + Scopes OAuthScopes `json:"scopes,omitempty" yaml:"scopes,omitempty"` +} + +// OAuthScopes is a group of settings that describe permission scopes configuration +type OAuthScopes struct { + Bot []string `json:"bot,omitempty" yaml:"bot,omitempty"` + User []string `json:"user,omitempty" yaml:"user,omitempty"` +} From 00233edafb216429809b26a85d7b5a3d816b2c46 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 3 Aug 2023 19:01:55 +0200 Subject: [PATCH 02/17] Add requests to validate manifests --- manifests.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/manifests.go b/manifests.go index e82c1cb41..93ecba6d9 100644 --- a/manifests.go +++ b/manifests.go @@ -1,5 +1,11 @@ package slack +import ( + "context" + "encoding/json" + "net/url" +) + // Manifest is an application manifest schema type Manifest struct { Metadata ManifestMetadata `json:"_metadata,omitempty" yaml:"_metadata,omitempty"` @@ -9,6 +15,41 @@ type Manifest struct { OAuthConfig OAuthConfig `json:"oauth_config,omitempty" yaml:"oauth_config,omitempty"` } +// ValidateManifest sends a request to apps.manifest.validate to validate your app manifest +func (api *Client) ValidateManifest(manifest *Manifest, token string, appId string) (*ValidateManifestResponse, error) { + return api.ValidateManifestContext(context.Background(), manifest, token, appId) +} + +// ValidateManifestContext sends a request to apps.manifest.validate to validate your app manifest with context +func (api *Client) ValidateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*ValidateManifestResponse, error) { + if token == "" { + token = api.appLevelToken + } + + // Marshal manifest into string + jsonBytes, err := json.Marshal(manifest) + if err != nil { + return nil, err + } + + values := url.Values{ + "token": {token}, + "manifest": {string(jsonBytes)}, + } + + if appId != "" { + values.Add("app_id", appId) + } + + response := &ValidateManifestResponse{} + err = api.postMethod(ctx, "apps.manifest.validate", values, response) + if err != nil { + return nil, err + } + + return response, nil +} + // ManifestMetadata is a group of settings that describe the manifest type ManifestMetadata struct { MajorVersion int `json:"major_version,omitempty" yaml:"major_version,omitempty"` @@ -110,3 +151,16 @@ type OAuthScopes struct { Bot []string `json:"bot,omitempty" yaml:"bot,omitempty"` User []string `json:"user,omitempty" yaml:"user,omitempty"` } + +// ValidateManifestResponse is the response returned by the API +type ValidateManifestResponse struct { + Ok bool `json:"ok"` + Error string `json:"error,omitempty"` + Errors []ManifestValidationError `json:"errors,omitempty"` +} + +// ManifestValidationError is an error message returned for invalid manifests +type ManifestValidationError struct { + Message string `json:"message"` + Pointer string `json:"pointer"` +} From 3b8d5b7d4b72ecc5fad6f8837215ca0eaf753235 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 08:48:09 +0200 Subject: [PATCH 03/17] Add create and delete methods, implement common interface --- manifests.go | 79 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/manifests.go b/manifests.go index 93ecba6d9..3f0484232 100644 --- a/manifests.go +++ b/manifests.go @@ -15,17 +15,56 @@ type Manifest struct { OAuthConfig OAuthConfig `json:"oauth_config,omitempty" yaml:"oauth_config,omitempty"` } +func (api *Client) CreateManifest(manifest *Manifest, token string) (*ManifestResponse, error) { + return api.CreateManifestContext(context.Background(), manifest, token) +} + +func (api *Client) CreateManifestContext(ctx context.Context, manifest *Manifest, token string) (*ManifestResponse, error) { + jsonBytes, err := json.Marshal(manifest) + if err != nil { + return nil, err + } + + values := url.Values{ + "token": {token}, + "manifest": {string(jsonBytes)}, + } + + response := &ManifestResponse{} + err = api.postMethod(ctx, "apps.manifest.create", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +func (api *Client) DeleteManifest(token string, appId string) (*SlackResponse, error) { + return api.DeleteManifestContext(context.Background(), token, appId) +} + +func (api *Client) DeleteManifestContext(ctx context.Context, token string, appId string) (*SlackResponse, error) { + values := url.Values{ + "token": {token}, + "app_id": {appId}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "apps.manifest.delete", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + // ValidateManifest sends a request to apps.manifest.validate to validate your app manifest -func (api *Client) ValidateManifest(manifest *Manifest, token string, appId string) (*ValidateManifestResponse, error) { +func (api *Client) ValidateManifest(manifest *Manifest, token string, appId string) (*ManifestResponse, error) { return api.ValidateManifestContext(context.Background(), manifest, token, appId) } // ValidateManifestContext sends a request to apps.manifest.validate to validate your app manifest with context -func (api *Client) ValidateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*ValidateManifestResponse, error) { - if token == "" { - token = api.appLevelToken - } - +func (api *Client) ValidateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*ManifestResponse, error) { // Marshal manifest into string jsonBytes, err := json.Marshal(manifest) if err != nil { @@ -41,13 +80,13 @@ func (api *Client) ValidateManifestContext(ctx context.Context, manifest *Manife values.Add("app_id", appId) } - response := &ValidateManifestResponse{} + response := &ManifestResponse{} err = api.postMethod(ctx, "apps.manifest.validate", values, response) if err != nil { return nil, err } - return response, nil + return response, response.Err() } // ManifestMetadata is a group of settings that describe the manifest @@ -152,15 +191,35 @@ type OAuthScopes struct { User []string `json:"user,omitempty" yaml:"user,omitempty"` } -// ValidateManifestResponse is the response returned by the API -type ValidateManifestResponse struct { +// ManifestResponse is the response returned by the API +// this is a different format than SlackResponse, so we can't use that here +// However, it intentionally has an Err() method for similar usage +type ManifestResponse struct { Ok bool `json:"ok"` Error string `json:"error,omitempty"` Errors []ManifestValidationError `json:"errors,omitempty"` } +func (m ManifestResponse) Err() error { + if m.Ok { + return nil + } + + return ManifestErrorResponse{Err: m.Error, Errors: m.Errors} +} + // ManifestValidationError is an error message returned for invalid manifests type ManifestValidationError struct { Message string `json:"message"` Pointer string `json:"pointer"` } + +// ManifestErrorResponse is a helper struct to contain an error from a ManifestResponse +type ManifestErrorResponse struct { + Err string + Errors []ManifestValidationError +} + +func (m ManifestErrorResponse) Error() string { + return m.Err +} From 7fceb91debe075a8fa0426836facac3edb9bbe60 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 09:04:09 +0200 Subject: [PATCH 04/17] Add tokens --- manifests.go | 43 +++++++++++++++++++------------------------ slack.go | 14 ++++++++------ tokens.go | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 tokens.go diff --git a/manifests.go b/manifests.go index 3f0484232..4a53448a2 100644 --- a/manifests.go +++ b/manifests.go @@ -15,11 +15,17 @@ type Manifest struct { OAuthConfig OAuthConfig `json:"oauth_config,omitempty" yaml:"oauth_config,omitempty"` } +// CreateManifest creates an app from an app manifest func (api *Client) CreateManifest(manifest *Manifest, token string) (*ManifestResponse, error) { return api.CreateManifestContext(context.Background(), manifest, token) } +// CreateManifestContext creates an app from an app manifest with a custom context func (api *Client) CreateManifestContext(ctx context.Context, manifest *Manifest, token string) (*ManifestResponse, error) { + if token == "" { + token = api.configToken + } + jsonBytes, err := json.Marshal(manifest) if err != nil { return nil, err @@ -39,11 +45,17 @@ func (api *Client) CreateManifestContext(ctx context.Context, manifest *Manifest return response, response.Err() } +// DeleteManifest permanently deletes an app created through app manifests func (api *Client) DeleteManifest(token string, appId string) (*SlackResponse, error) { return api.DeleteManifestContext(context.Background(), token, appId) } +// DeleteManifestContext permanently deletes an app created through app manifests with a custom context func (api *Client) DeleteManifestContext(ctx context.Context, token string, appId string) (*SlackResponse, error) { + if token == "" { + token = api.configToken + } + values := url.Values{ "token": {token}, "app_id": {appId}, @@ -63,8 +75,12 @@ func (api *Client) ValidateManifest(manifest *Manifest, token string, appId stri return api.ValidateManifestContext(context.Background(), manifest, token, appId) } -// ValidateManifestContext sends a request to apps.manifest.validate to validate your app manifest with context +// ValidateManifestContext sends a request to apps.manifest.validate to validate your app manifest with a custom context func (api *Client) ValidateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*ManifestResponse, error) { + if token == "" { + token = api.configToken + } + // Marshal manifest into string jsonBytes, err := json.Marshal(manifest) if err != nil { @@ -191,21 +207,10 @@ type OAuthScopes struct { User []string `json:"user,omitempty" yaml:"user,omitempty"` } -// ManifestResponse is the response returned by the API -// this is a different format than SlackResponse, so we can't use that here -// However, it intentionally has an Err() method for similar usage +// ManifestResponse is the response returned by the API for app.manifest.x endpoints type ManifestResponse struct { - Ok bool `json:"ok"` - Error string `json:"error,omitempty"` Errors []ManifestValidationError `json:"errors,omitempty"` -} - -func (m ManifestResponse) Err() error { - if m.Ok { - return nil - } - - return ManifestErrorResponse{Err: m.Error, Errors: m.Errors} + SlackResponse } // ManifestValidationError is an error message returned for invalid manifests @@ -213,13 +218,3 @@ type ManifestValidationError struct { Message string `json:"message"` Pointer string `json:"pointer"` } - -// ManifestErrorResponse is a helper struct to contain an error from a ManifestResponse -type ManifestErrorResponse struct { - Err string - Errors []ManifestValidationError -} - -func (m ManifestErrorResponse) Error() string { - return m.Err -} diff --git a/slack.go b/slack.go index ea3aab6d6..361e849cb 100644 --- a/slack.go +++ b/slack.go @@ -57,12 +57,14 @@ type authTestResponseFull struct { type ParamOption func(*url.Values) type Client struct { - token string - appLevelToken string - endpoint string - debug bool - log ilogger - httpclient httpClient + token string + appLevelToken string + configToken string + configRefreshToken string + endpoint string + debug bool + log ilogger + httpclient httpClient } // Option defines an option for a Client diff --git a/tokens.go b/tokens.go new file mode 100644 index 000000000..567a5eec2 --- /dev/null +++ b/tokens.go @@ -0,0 +1,40 @@ +package slack + +import ( + "context" + "net/url" +) + +// RotateTokens exchanges a refresh token for a new app configuration token +func (api *Client) RotateTokens(refreshToken string) (*TokenResponse, error) { + return api.RotateTokensContext(context.Background(), refreshToken) +} + +// RotateTokensContext exchanges a refresh token for a new app configuration token with a custom context +func (api *Client) RotateTokensContext(ctx context.Context, refreshToken string) (*TokenResponse, error) { + if refreshToken == "" { + refreshToken = api.configRefreshToken + } + + values := url.Values{ + "refresh_token": {refreshToken}, + } + + response := &TokenResponse{} + err := api.postMethod(ctx, "tooling.tokens.rotate", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +type TokenResponse struct { + Token string `json:"token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + TeamId string `json:"team_id,omitempty"` + UserId string `json:"user_id,omitempty"` + CreatedAt uint64 `json:"iat,omitempty"` + ExpiresAt uint64 `json:"exp,omitempty"` + SlackResponse +} From 4b09d4185459f69df4c1d0adfb4ba32183a21e85 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 09:14:34 +0200 Subject: [PATCH 05/17] Finish manifest methods --- manifests.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/manifests.go b/manifests.go index 4a53448a2..f64c45365 100644 --- a/manifests.go +++ b/manifests.go @@ -70,6 +70,62 @@ func (api *Client) DeleteManifestContext(ctx context.Context, token string, appI return response, response.Err() } +// ExportManifest exports an app manifest from an existing app +func (api *Client) ExportManifest(token string, appId string) (*ExportManifestResponse, error) { + return api.ExportManifestContext(context.Background(), token, appId) +} + +// ExportManifestContext exports an app manifest from an existing app with a custom context +func (api *Client) ExportManifestContext(ctx context.Context, token string, appId string) (*ExportManifestResponse, error) { + if token == "" { + token = api.configToken + } + + values := url.Values{ + "token": {token}, + "app_id": {appId}, + } + + response := &ExportManifestResponse{} + err := api.postMethod(ctx, "apps.manifest.export", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// UpdateManifest updates an app from an app manifest +func (api *Client) UpdateManifest(manifest *Manifest, token string, appId string) (*UpdateManifestResponse, error) { + return api.UpdateManifestContext(context.Background(), manifest, token, appId) +} + +// UpdateManifestContext updates an app from an app manifest with a custom context +func (api *Client) UpdateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*UpdateManifestResponse, error) { + if token == "" { + token = api.configToken + } + + jsonBytes, err := json.Marshal(manifest) + if err != nil { + return nil, err + } + + values := url.Values{ + "token": {token}, + "app_id": {appId}, + "manifest": {string(jsonBytes)}, + } + + response := &UpdateManifestResponse{} + err = api.postMethod(ctx, "apps.manifest.update", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + // ValidateManifest sends a request to apps.manifest.validate to validate your app manifest func (api *Client) ValidateManifest(manifest *Manifest, token string, appId string) (*ManifestResponse, error) { return api.ValidateManifestContext(context.Background(), manifest, token, appId) @@ -207,7 +263,7 @@ type OAuthScopes struct { User []string `json:"user,omitempty" yaml:"user,omitempty"` } -// ManifestResponse is the response returned by the API for app.manifest.x endpoints +// ManifestResponse is the response returned by the API for apps.manifest.x endpoints type ManifestResponse struct { Errors []ManifestValidationError `json:"errors,omitempty"` SlackResponse @@ -218,3 +274,14 @@ type ManifestValidationError struct { Message string `json:"message"` Pointer string `json:"pointer"` } + +type ExportManifestResponse struct { + Manifest Manifest `json:"manifest,omitempty"` + SlackResponse +} + +type UpdateManifestResponse struct { + AppId string `json:"app_id,omitempty"` + PermissionsUpdated bool `json:"permissions_updated,omitempty"` + SlackResponse +} From 1abbd7a75d90e7f142d8ee8c26e2fab03956c518 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 09:18:28 +0200 Subject: [PATCH 06/17] Fix linting --- manifests.go | 2 +- tokens_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 tokens_test.go diff --git a/manifests.go b/manifests.go index f64c45365..6d3f5c10c 100644 --- a/manifests.go +++ b/manifests.go @@ -248,7 +248,7 @@ type ManifestSlashCommand struct { // WorkflowStep is a group of settings that describes workflow steps configuration type WorkflowStep struct { Name string `json:"name" yaml:"name"` - CallbackID string `json:"callback_id", yaml:"callback_id"` + CallbackID string `json:"callback_id" yaml:"callback_id"` } // OAuthConfig is a group of settings that describe OAuth configuration for the app diff --git a/tokens_test.go b/tokens_test.go new file mode 100644 index 000000000..578351f3c --- /dev/null +++ b/tokens_test.go @@ -0,0 +1 @@ +package slack From 6c46b7c91c1589c31736ed4f9a43e5501fff0447 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 10:14:14 +0200 Subject: [PATCH 07/17] Rename value --- tokens.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tokens.go b/tokens.go index 567a5eec2..0fa9201ae 100644 --- a/tokens.go +++ b/tokens.go @@ -34,7 +34,7 @@ type TokenResponse struct { RefreshToken string `json:"refresh_token,omitempty"` TeamId string `json:"team_id,omitempty"` UserId string `json:"user_id,omitempty"` - CreatedAt uint64 `json:"iat,omitempty"` + IssuedAt uint64 `json:"iat,omitempty"` ExpiresAt uint64 `json:"exp,omitempty"` SlackResponse } From dd3d9cc0a5e042ff2640a84b05dc28734ecfc16e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 10:54:07 +0200 Subject: [PATCH 08/17] More tests, return manifest itself when exporting --- manifests.go | 8 ++++---- manifests_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ tokens_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 manifests_test.go diff --git a/manifests.go b/manifests.go index 6d3f5c10c..5922e6b74 100644 --- a/manifests.go +++ b/manifests.go @@ -71,12 +71,12 @@ func (api *Client) DeleteManifestContext(ctx context.Context, token string, appI } // ExportManifest exports an app manifest from an existing app -func (api *Client) ExportManifest(token string, appId string) (*ExportManifestResponse, error) { +func (api *Client) ExportManifest(token string, appId string) (*Manifest, error) { return api.ExportManifestContext(context.Background(), token, appId) } // ExportManifestContext exports an app manifest from an existing app with a custom context -func (api *Client) ExportManifestContext(ctx context.Context, token string, appId string) (*ExportManifestResponse, error) { +func (api *Client) ExportManifestContext(ctx context.Context, token string, appId string) (*Manifest, error) { if token == "" { token = api.configToken } @@ -92,7 +92,7 @@ func (api *Client) ExportManifestContext(ctx context.Context, token string, appI return nil, err } - return response, response.Err() + return response.Manifest, response.Err() } // UpdateManifest updates an app from an app manifest @@ -276,7 +276,7 @@ type ManifestValidationError struct { } type ExportManifestResponse struct { - Manifest Manifest `json:"manifest,omitempty"` + Manifest *Manifest `json:"manifest,omitempty"` SlackResponse } diff --git a/manifests_test.go b/manifests_test.go new file mode 100644 index 000000000..f68e70294 --- /dev/null +++ b/manifests_test.go @@ -0,0 +1,49 @@ +package slack + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" +) + +func TestCreateManifest(t *testing.T) { + http.HandleFunc("/apps.manifest.create", handleCreateManifest) + once.Do(startServer) + + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + resp, err := api.CreateManifest(getTestManifest(), "token") + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !reflect.DeepEqual(resp, getTestManifestResponse()) { + t.Fatal(ErrIncorrectResponse) + } +} + +func handleCreateManifest(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + response, _ := json.Marshal(getTestManifestResponse()) + rw.Write(response) +} + +func getTestManifest() *Manifest { + return &Manifest{ + Display: Display{ + Name: "test", + Description: "this is a test", + }, + } +} + +func getTestManifestResponse() *ManifestResponse { + return &ManifestResponse{ + SlackResponse: SlackResponse{ + Ok: true, + }, + } +} diff --git a/tokens_test.go b/tokens_test.go index 578351f3c..61b17bdfa 100644 --- a/tokens_test.go +++ b/tokens_test.go @@ -1 +1,45 @@ package slack + +import ( + "encoding/json" + "net/http" + "reflect" + "testing" +) + +func TestRotateTokens(t *testing.T) { + http.HandleFunc("/tooling.tokens.rotate", handleRotateToken) + expected := getTestTokenResponse() + + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + tok, err := api.RotateTokens("old-refresh") + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !reflect.DeepEqual(expected, *tok) { + t.Fatal(ErrIncorrectResponse) + } +} + +func getTestTokenResponse() TokenResponse { + return TokenResponse{ + Token: "token", + RefreshToken: "refresh", + UserId: "uid", + TeamId: "tid", + IssuedAt: 1, + ExpiresAt: 1, + SlackResponse: SlackResponse{Ok: true}, + } +} + +func handleRotateToken(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + response, _ := json.Marshal(getTestTokenResponse()) + rw.Write(response) +} From 6bfd3934bf4a41ca86c97689953c707d0611b3e4 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 10:54:37 +0200 Subject: [PATCH 09/17] Undo pointer and take reference manually --- manifests.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifests.go b/manifests.go index 5922e6b74..810bd4f6a 100644 --- a/manifests.go +++ b/manifests.go @@ -92,7 +92,7 @@ func (api *Client) ExportManifestContext(ctx context.Context, token string, appI return nil, err } - return response.Manifest, response.Err() + return &response.Manifest, response.Err() } // UpdateManifest updates an app from an app manifest @@ -276,7 +276,7 @@ type ManifestValidationError struct { } type ExportManifestResponse struct { - Manifest *Manifest `json:"manifest,omitempty"` + Manifest Manifest `json:"manifest,omitempty"` SlackResponse } From c33746bd2c5a147e44dc02553edc521ec77e6188 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 11:20:14 +0200 Subject: [PATCH 10/17] More tests --- manifests_test.go | 106 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/manifests_test.go b/manifests_test.go index f68e70294..9133e68c8 100644 --- a/manifests_test.go +++ b/manifests_test.go @@ -13,7 +13,8 @@ func TestCreateManifest(t *testing.T) { api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) - resp, err := api.CreateManifest(getTestManifest(), "token") + manif := getTestManifest() + resp, err := api.CreateManifest(&manif, "token") if err != nil { t.Errorf("Unexpected error: %v", err) return @@ -31,8 +32,107 @@ func handleCreateManifest(rw http.ResponseWriter, r *http.Request) { rw.Write(response) } -func getTestManifest() *Manifest { - return &Manifest{ +func TestDeleteManifest(t *testing.T) { + http.HandleFunc("/apps.manifest.delete", handleDeleteManifest) + expectedResponse := SlackResponse{Ok: true} + + once.Do(startServer) + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + resp, err := api.DeleteManifest("token", "app id") + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !reflect.DeepEqual(expectedResponse, *resp) { + t.Fatal(ErrIncorrectResponse) + } +} + +func handleDeleteManifest(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + response, _ := json.Marshal(SlackResponse{Ok: true}) + rw.Write(response) +} + +func TestExportManifest(t *testing.T) { + http.HandleFunc("/apps.manifest.export", handleExportManifest) + expectedResponse := getTestManifest() + + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + resp, err := api.ExportManifest("token", "app id") + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !reflect.DeepEqual(expectedResponse, *resp) { + t.Fatal(ErrIncorrectResponse) + } +} + +func handleExportManifest(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + response, _ := json.Marshal(ExportManifestResponse{Manifest: getTestManifest()}) + rw.Write(response) +} + +func TestUpdateManifest(t *testing.T) { + http.HandleFunc("/apps.manifest.update", handleUpdateManifest) + expectedResponse := UpdateManifestResponse{AppId: "app id"} + + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + manif := getTestManifest() + resp, err := api.UpdateManifest(&manif, "token", "app id") + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !reflect.DeepEqual(expectedResponse, *resp) { + t.Fatal(ErrIncorrectResponse) + } +} + +func handleUpdateManifest(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + response, _ := json.Marshal(UpdateManifestResponse{AppId: "app id"}) + rw.Write(response) +} + +func TestValidateManifest(t *testing.T) { + http.HandleFunc("/apps.manifest.validate", handleValidateManifest) + expectedResponse := ManifestResponse{SlackResponse: SlackResponse{Ok: true}} + + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + manif := getTestManifest() + resp, err := api.ValidateManifest(&manif, "token", "app id") + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if !reflect.DeepEqual(expectedResponse, *resp) { + t.Fatal(ErrIncorrectResponse) + } +} + +func handleValidateManifest(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + response, _ := json.Marshal(ManifestResponse{SlackResponse: SlackResponse{Ok: true}}) + rw.Write(response) +} + +func getTestManifest() Manifest { + return Manifest{ Display: Display{ Name: "test", Description: "this is a test", From f7d479fe47bf13629def5a72cbb9a46f3223c2b5 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 11:24:43 +0200 Subject: [PATCH 11/17] Add options for tokens --- slack.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/slack.go b/slack.go index 361e849cb..756106fe4 100644 --- a/slack.go +++ b/slack.go @@ -101,6 +101,16 @@ func OptionAppLevelToken(token string) func(*Client) { return func(c *Client) { c.appLevelToken = token } } +// OptionConfigToken sets a configuration token for the client. +func OptionConfigToken(token string) func(*Client) { + return func(c *Client) { c.configToken = token } +} + +// OptionConfigRefreshToken sets a configuration refresh token for the client. +func OptionConfigRefreshToken(token string) func(*Client) { + return func(c *Client) { c.configRefreshToken = token } +} + // New builds a slack client from the provided token and options. func New(token string, options ...Option) *Client { s := &Client{ From 14d4607b891eed33657de55d43fbe8669327873e Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 11:31:50 +0200 Subject: [PATCH 12/17] Use correct method --- tokens.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tokens.go b/tokens.go index 0fa9201ae..8f678cc05 100644 --- a/tokens.go +++ b/tokens.go @@ -21,7 +21,7 @@ func (api *Client) RotateTokensContext(ctx context.Context, refreshToken string) } response := &TokenResponse{} - err := api.postMethod(ctx, "tooling.tokens.rotate", values, response) + err := api.getMethod(ctx, "tooling.tokens.rotate", "", values, response) if err != nil { return nil, err } From a33e5ae10f9bf51dc5e014cf515676ff25717083 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 11:37:08 +0200 Subject: [PATCH 13/17] use config token --- tokens.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tokens.go b/tokens.go index 8f678cc05..602db1b76 100644 --- a/tokens.go +++ b/tokens.go @@ -21,7 +21,7 @@ func (api *Client) RotateTokensContext(ctx context.Context, refreshToken string) } response := &TokenResponse{} - err := api.getMethod(ctx, "tooling.tokens.rotate", "", values, response) + err := api.getMethod(ctx, "tooling.tokens.rotate", api.configToken, values, response) if err != nil { return nil, err } From b9a30c0d15654169122b6fe1fe8a0f6f77e647a7 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 4 Aug 2023 11:39:48 +0200 Subject: [PATCH 14/17] Pass config token as param --- tokens.go | 12 ++++++++---- tokens_test.go | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tokens.go b/tokens.go index 602db1b76..5968704d7 100644 --- a/tokens.go +++ b/tokens.go @@ -6,12 +6,16 @@ import ( ) // RotateTokens exchanges a refresh token for a new app configuration token -func (api *Client) RotateTokens(refreshToken string) (*TokenResponse, error) { - return api.RotateTokensContext(context.Background(), refreshToken) +func (api *Client) RotateTokens(configToken string, refreshToken string) (*TokenResponse, error) { + return api.RotateTokensContext(context.Background(), configToken, refreshToken) } // RotateTokensContext exchanges a refresh token for a new app configuration token with a custom context -func (api *Client) RotateTokensContext(ctx context.Context, refreshToken string) (*TokenResponse, error) { +func (api *Client) RotateTokensContext(ctx context.Context, configToken string, refreshToken string) (*TokenResponse, error) { + if configToken == "" { + configToken = api.configToken + } + if refreshToken == "" { refreshToken = api.configRefreshToken } @@ -21,7 +25,7 @@ func (api *Client) RotateTokensContext(ctx context.Context, refreshToken string) } response := &TokenResponse{} - err := api.getMethod(ctx, "tooling.tokens.rotate", api.configToken, values, response) + err := api.getMethod(ctx, "tooling.tokens.rotate", configToken, values, response) if err != nil { return nil, err } diff --git a/tokens_test.go b/tokens_test.go index 61b17bdfa..621174598 100644 --- a/tokens_test.go +++ b/tokens_test.go @@ -14,7 +14,7 @@ func TestRotateTokens(t *testing.T) { once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) - tok, err := api.RotateTokens("old-refresh") + tok, err := api.RotateTokens("expired-config", "old-refresh") if err != nil { t.Errorf("Unexpected error: %v", err) return From 2f5b296c9073a41d25f19f7aae1836188e574f2d Mon Sep 17 00:00:00 2001 From: stijndcl Date: Thu, 10 Aug 2023 16:31:31 +0200 Subject: [PATCH 15/17] Small bugfix --- manifests.go | 2 +- tokens.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/manifests.go b/manifests.go index 810bd4f6a..0041bf244 100644 --- a/manifests.go +++ b/manifests.go @@ -283,5 +283,5 @@ type ExportManifestResponse struct { type UpdateManifestResponse struct { AppId string `json:"app_id,omitempty"` PermissionsUpdated bool `json:"permissions_updated,omitempty"` - SlackResponse + ManifestResponse } diff --git a/tokens.go b/tokens.go index 5968704d7..4b83beebb 100644 --- a/tokens.go +++ b/tokens.go @@ -33,6 +33,12 @@ func (api *Client) RotateTokensContext(ctx context.Context, configToken string, return response, response.Err() } +// UpdateConfigTokens replaces the configuration tokens in the client with those returned by the API +func (api *Client) UpdateConfigTokens(response *TokenResponse) { + api.configToken = response.Token + api.configRefreshToken = response.RefreshToken +} + type TokenResponse struct { Token string `json:"token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` From 217f2de7e7a42428d7b3e7d03b2072fff65410fc Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Fri, 18 Aug 2023 23:47:58 +0200 Subject: [PATCH 16/17] Add examples --- examples/manifests/README.md | 38 ++++++++++++++++++++++++++++ examples/manifests/manifest.go | 45 ++++++++++++++++++++++++++++++++++ examples/tokens/README.md | 10 ++++++++ examples/tokens/tokens.go | 33 +++++++++++++++++++++++++ 4 files changed, 126 insertions(+) create mode 100644 examples/manifests/README.md create mode 100644 examples/manifests/manifest.go create mode 100644 examples/tokens/README.md create mode 100644 examples/tokens/tokens.go diff --git a/examples/manifests/README.md b/examples/manifests/README.md new file mode 100644 index 000000000..e2a8e2e3d --- /dev/null +++ b/examples/manifests/README.md @@ -0,0 +1,38 @@ +# Manifest examples + +This example tries to show how to interact with the +new [manifest endpoints](https://api.slack.com/reference/manifests#manifest_apis). These endpoints require a special set +of tokens: **configuration tokens**. Refer to +the [relevant documentation](https://api.slack.com/authentication/config-tokens) for how to create these tokens. + +For examples on how to use configuration tokens, see the [tokens example](../tokens). + +## Usage info + +The manifest endpoints allow you to configure your application programmatically instead of manually creating +a `manifest.yaml` file and uploading it on your Slack application's dashboard. + +A manifest should follow a specific structure and has a handful of required fields. These are describe in +the [manifest documentation](https://api.slack.com/reference/manifests#fields), but Slack additionally returns very +informative error messages for malformed templates to help you pin down what the issue is. The library itself does not +attempt to perform any form of validation on your manifest. + +**Note that each configuration token may only be used once before being invalidated. Again refer to the tokens example +for more information.** + +## Available methods + +- ``Slack.CreateManifest()`` +- ``Slack.DeleteManifest()`` +- ``Slack.ExportManifest()`` +- ``Slack.UpdateManifest()`` + +## Example details + +The example code here only shows how to _update_ an application using a manifest. The other available methods are either +identical in usage or trivial to use, so no full example is provided for them. + +The example doesn't rotate the configuration tokens after updating the manifest. **You should almost always do this**. +Your access token is invalidated after sending a request, and rotating your tokens will allow you to make another +request in the future. This example does not do this explicitly as it would just repeat the tokens example. For sake of +simplicity, it only focuses on the manifest part. diff --git a/examples/manifests/manifest.go b/examples/manifests/manifest.go new file mode 100644 index 000000000..bfcfa9cab --- /dev/null +++ b/examples/manifests/manifest.go @@ -0,0 +1,45 @@ +package manifests + +import ( + "fmt" + "github.com/slack-go/slack" +) + +// createManifest programmatically creates a Slack app manifest +func createManifest() *slack.Manifest { + return &slack.Manifest{ + Display: slack.Display{ + Name: "Your Application", + }, + // ... other configuration here + } +} + +func main() { + api := slack.New( + "YOUR_TOKEN_HERE", + // You may choose to provide your access token when creating your Slack client + // or when invoking the method calls + slack.OptionConfigToken("YOUR_CONFIG_ACCESS_TOKEN_HERE"), + ) + + // Create a new Manifest object + manifest := createManifest() + + // Update your application using the new manifest + // You may pass your token as a parameter here as well, if you didn't do it above + response, err := api.UpdateManifest(manifest, "", "YOUR_APP_ID_HERE") + if err != nil { + fmt.Printf("error updating Slack application: %v\n", err) + return + } + + if !response.Ok { + fmt.Printf("unable to update Slack application: %v\n", response.Errors) + } + + fmt.Println("successfully updated Slack application") + + // The access token is now invalid, so it should be rotated for future use + // Refer to the examples about tokens for more details +} diff --git a/examples/tokens/README.md b/examples/tokens/README.md new file mode 100644 index 000000000..7e8e163e7 --- /dev/null +++ b/examples/tokens/README.md @@ -0,0 +1,10 @@ +# Tokens examples + +The refresh token endpoint can be used to update +your [configuration tokenset](https://api.slack.com/authentication/config-tokens). These tokens may only be used **once +** before being invalidated, and are only valid for up to **12 hours**. + +Once a token has been used, or before it expires, you can use the `RotateTokens()` method to obtain a fresh set to use +for the next request. Depending on your use-case you may want to store these somewhere for a future run, so they are +only returned by the method call. If you wish to update the tokens inside the active Slack client, this can be done +using `UpdateConfigTokens()`. diff --git a/examples/tokens/tokens.go b/examples/tokens/tokens.go new file mode 100644 index 000000000..63d46d25f --- /dev/null +++ b/examples/tokens/tokens.go @@ -0,0 +1,33 @@ +package tokens + +import ( + "fmt" + "github.com/slack-go/slack" +) + +func main() { + api := slack.New( + "YOUR_TOKEN_HERE", + // You may choose to provide your config tokens when creating your Slack client + // or when invoking the method calls + slack.OptionConfigToken("YOUR_CONFIG_ACCESS_TOKEN_HERE"), + slack.OptionConfigRefreshToken("YOUR_REFRESH_TOKEN_HERE"), + ) + + // Obtain a fresh set of tokens + // You may pass your tokens as a parameter here as well, if you didn't do it above + freshTokens, err := api.RotateTokens("", "") + if err != nil { + fmt.Printf("error rotating tokens: %v\n", err) + return + } + + fmt.Printf("new access token: %s\n", freshTokens.Token) + fmt.Printf("new refresh token: %s\n", freshTokens.RefreshToken) + fmt.Printf("new tokenset expires at: %d\n", freshTokens.ExpiresAt) + + // Optionally: update the tokens inside the running Slack client + // This isn't necessary if you restart the application after storing the tokens elsewhere, + // or pass them as parameters to RotateTokens() explicitly + api.UpdateConfigTokens(freshTokens) +} From 3dc3db8986522e1a4b87b9b5250098e90c0bbc05 Mon Sep 17 00:00:00 2001 From: Stijn De Clercq Date: Sat, 19 Aug 2023 19:19:35 +0200 Subject: [PATCH 17/17] Re-phrase example docs --- examples/manifests/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/manifests/README.md b/examples/manifests/README.md index e2a8e2e3d..ba6db2332 100644 --- a/examples/manifests/README.md +++ b/examples/manifests/README.md @@ -1,8 +1,8 @@ # Manifest examples -This example tries to show how to interact with the +This example shows how to interact with the new [manifest endpoints](https://api.slack.com/reference/manifests#manifest_apis). These endpoints require a special set -of tokens: **configuration tokens**. Refer to +of tokens called `configuration tokens`. Refer to the [relevant documentation](https://api.slack.com/authentication/config-tokens) for how to create these tokens. For examples on how to use configuration tokens, see the [tokens example](../tokens).