From f230f122343dfb58c4d2873e3982cf25ee709d68 Mon Sep 17 00:00:00 2001 From: "gh-worker-campaigns-3e9aa4[bot]" <244854796+gh-worker-campaigns-3e9aa4[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 13:17:41 +0000 Subject: [PATCH] =?UTF-8?q?ADMS:=20vuln=20github.com/slack-go/slack=20(uns?= =?UTF-8?q?table=20=E2=86=92=20v0.23.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- vendor/github.com/slack-go/slack/CHANGELOG.md | 580 ++++++++++++++ vendor/github.com/slack-go/slack/Makefile | 4 +- vendor/github.com/slack-go/slack/README.md | 49 +- vendor/github.com/slack-go/slack/admin.go | 12 +- .../slack-go/slack/admin_conversations.go | 722 ++++++++++++++++++ .../slack-go/slack/admin_conversations_ekm.go | 101 +++ .../admin_conversations_restrictAccess.go | 150 ++++ .../github.com/slack-go/slack/admin_roles.go | 204 +++++ .../github.com/slack-go/slack/admin_teams.go | 153 ++++ vendor/github.com/slack-go/slack/apps.go | 2 +- vendor/github.com/slack-go/slack/assistant.go | 230 +++++- .../github.com/slack-go/slack/attachments.go | 7 +- vendor/github.com/slack-go/slack/audit.go | 3 +- vendor/github.com/slack-go/slack/block.go | 31 +- .../github.com/slack-go/slack/block_alert.go | 70 ++ .../github.com/slack-go/slack/block_call.go | 69 +- .../github.com/slack-go/slack/block_card.go | 90 +++ .../slack-go/slack/block_carousel.go | 42 + .../slack-go/slack/block_context_actions.go | 31 + .../github.com/slack-go/slack/block_conv.go | 49 +- .../slack-go/slack/block_element.go | 237 +++++- .../github.com/slack-go/slack/block_header.go | 4 +- .../github.com/slack-go/slack/block_json.go | 110 +++ .../github.com/slack-go/slack/block_object.go | 1 - .../github.com/slack-go/slack/block_plan.go | 55 ++ .../slack-go/slack/block_rich_text.go | 64 +- .../github.com/slack-go/slack/block_table.go | 55 ++ .../slack-go/slack/block_task_card.go | 101 +++ .../slack-go/slack/block_unknown.go | 22 +- vendor/github.com/slack-go/slack/channels.go | 2 +- vendor/github.com/slack-go/slack/chat.go | 315 +++++++- .../slack-go/slack/chat_stream_chunks.go | 95 +++ .../github.com/slack-go/slack/conversation.go | 228 +++++- vendor/github.com/slack-go/slack/dialog.go | 3 +- vendor/github.com/slack-go/slack/dnd.go | 36 +- vendor/github.com/slack-go/slack/entity.go | 132 ++++ vendor/github.com/slack-go/slack/files.go | 109 +-- .../slack-go/slack/function_execute.go | 6 +- vendor/github.com/slack-go/slack/huddle.go | 64 ++ vendor/github.com/slack-go/slack/im.go | 21 - vendor/github.com/slack-go/slack/info.go | 27 +- vendor/github.com/slack-go/slack/manifests.go | 16 +- vendor/github.com/slack-go/slack/messages.go | 10 + vendor/github.com/slack-go/slack/metadata.go | 35 +- vendor/github.com/slack-go/slack/misc.go | 186 ++++- vendor/github.com/slack-go/slack/mise.toml | 4 + vendor/github.com/slack-go/slack/oauth.go | 188 ++++- vendor/github.com/slack-go/slack/reactions.go | 83 +- .../github.com/slack-go/slack/remotefiles.go | 23 +- vendor/github.com/slack-go/slack/retry.go | 307 ++++++++ vendor/github.com/slack-go/slack/security.go | 8 +- vendor/github.com/slack-go/slack/slack.go | 132 +++- .../github.com/slack-go/slack/socket_mode.go | 6 +- vendor/github.com/slack-go/slack/stars.go | 60 +- vendor/github.com/slack-go/slack/team.go | 36 +- .../github.com/slack-go/slack/usergroups.go | 14 + vendor/github.com/slack-go/slack/users.go | 219 ++++-- vendor/github.com/slack-go/slack/views.go | 69 +- vendor/github.com/slack-go/slack/webhooks.go | 4 +- .../slack-go/slack/websocket_groups.go | 3 - .../slack-go/slack/websocket_managed_conn.go | 15 +- .../slack-go/slack/websocket_misc.go | 142 +++- .../slack-go/slack/workflows_featured.go | 143 ++++ .../slack-go/slack/workflows_triggers.go | 8 +- vendor/modules.txt | 4 +- 67 files changed, 5422 insertions(+), 585 deletions(-) create mode 100644 vendor/github.com/slack-go/slack/CHANGELOG.md create mode 100644 vendor/github.com/slack-go/slack/admin_conversations_ekm.go create mode 100644 vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go create mode 100644 vendor/github.com/slack-go/slack/admin_roles.go create mode 100644 vendor/github.com/slack-go/slack/admin_teams.go create mode 100644 vendor/github.com/slack-go/slack/block_alert.go create mode 100644 vendor/github.com/slack-go/slack/block_card.go create mode 100644 vendor/github.com/slack-go/slack/block_carousel.go create mode 100644 vendor/github.com/slack-go/slack/block_context_actions.go create mode 100644 vendor/github.com/slack-go/slack/block_json.go create mode 100644 vendor/github.com/slack-go/slack/block_plan.go create mode 100644 vendor/github.com/slack-go/slack/block_table.go create mode 100644 vendor/github.com/slack-go/slack/block_task_card.go create mode 100644 vendor/github.com/slack-go/slack/chat_stream_chunks.go create mode 100644 vendor/github.com/slack-go/slack/entity.go create mode 100644 vendor/github.com/slack-go/slack/huddle.go delete mode 100644 vendor/github.com/slack-go/slack/im.go create mode 100644 vendor/github.com/slack-go/slack/mise.toml create mode 100644 vendor/github.com/slack-go/slack/retry.go create mode 100644 vendor/github.com/slack-go/slack/workflows_featured.go diff --git a/go.mod b/go.mod index 101a4d77ec..a433489545 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/onsi/gomega v1.39.1 github.com/opencontainers/cgroups v0.0.6 github.com/robfig/cron v1.2.0 - github.com/slack-go/slack v0.17.3 + github.com/slack-go/slack v0.23.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index 6f1df54a3b..7c607d345c 100644 --- a/go.sum +++ b/go.sum @@ -390,8 +390,8 @@ github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5Bdj github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= -github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= +github.com/slack-go/slack v0.23.1 h1:ZS5B96wxxYQRwvJ3/vJFtqtUZi3tXhsZCyT44Nv7M80= +github.com/slack-go/slack v0.23.1/go.mod h1:H0yR/YBuRJ39RkE+JpV/d/oEsbanzTRowR82bCN0cEs= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= diff --git a/vendor/github.com/slack-go/slack/CHANGELOG.md b/vendor/github.com/slack-go/slack/CHANGELOG.md new file mode 100644 index 0000000000..2f9f821e2e --- /dev/null +++ b/vendor/github.com/slack-go/slack/CHANGELOG.md @@ -0,0 +1,580 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.23.1] - 2026-05-10 + +### Fixed + +- `NewSecretsVerifier` now rejects empty signing secrets to avoid accepting forged request + signatures when applications are misconfigured. + +## [0.23.0] - 2026-04-22 + +### Added + +- **Block Kit: `CardBlock` and `CarouselBlock`** — Support for two of the new + agent-UI blocks announced in the + [April 16 Slack changelog](https://docs.slack.dev/changelog/2026/04/16/block-kit-new-blocks). + `CardBlock` is constructed via `NewCardBlock` with a functional-options + pattern and fluent `With*` builders (`WithTitle`, `WithSubtitle`, `WithBody`, + `WithIcon`, `WithHeroImage`, `WithActions`). `CarouselBlock` is constructed + via `NewCarouselBlock` with a variadic `*CardBlock` list plus `WithBlockID` + and `AddCard` helpers. Both blocks wire into `Blocks.UnmarshalJSON` for + round-trip fidelity, and reuse existing `ImageBlockElement` / + `ButtonBlockElement` / `BlockElements` types rather than introducing new + composition objects. +- **Block Kit: `AlertBlock`** — Support for the third of the new agent-UI + blocks from the + [April 16 Slack changelog](https://docs.slack.dev/changelog/2026/04/16/block-kit-new-blocks). + `AlertBlock` is constructed via `NewAlertBlock` with a `*TextBlockObject` + body and a functional-options pattern. Severity is set via + `AlertBlockOptionLevel` (`AlertLevelDefault`, `AlertLevelInfo`, + `AlertLevelWarning`, `AlertLevelError`, `AlertLevelSuccess`) and the block + ID via `AlertBlockOptionBlockID`. Wires into `Blocks.UnmarshalJSON` for + round-trip fidelity. Must be delivered via the streaming chunks API — + `chat.postMessage` rejects it as an unsupported block type. +- **Streaming-message chunks API** — `chat.startStream` / `chat.appendStream` / + `chat.stopStream` now accept a `chunks` parameter. Added `MsgOptionChunks` + along with a `StreamChunk` interface and four chunk types: + `MarkdownTextChunk`, `TaskUpdateChunk`, `PlanUpdateChunk`, and `BlocksChunk` + (each with a `New*Chunk` constructor). This is the supported transport for + streaming Block Kit content and the new agent-UI blocks in particular + (which `chat.postMessage` rejects as `Unsupported block type`). +- **`MsgOptionTaskDisplayMode`** — New option for `chat.startStream` controlling + whether task chunks render as a sequential timeline or a grouped plan. + Accepts `TaskDisplayModeTimeline` or `TaskDisplayModePlan`. +- Added `Username`, `IconURL`, and `IconEmoji` fields to + `AssistantThreadsSetStatusParameters`, forwarded by + `SetAssistantThreadsStatusContext`, matching the new optional parameters on + [`assistant.threads.setStatus`](https://docs.slack.dev/reference/methods/assistant.threads.setStatus) + for customising the status-update presentation. +- Exposed `SocketmodeHandler.DispatchEvent` (previously the unexported + `dispatcher`), enabling integration tests to exercise registered handlers + without a live WebSocket connection. The unexported `dispatcher` is kept as + a thin wrapper for backwards compatibility. Closes #1549. + +## [0.22.0] - 2026-04-12 + +### Added + +- Added missing parameters to `assistant.search.context` (`Sort`, `SortDir`, `Before`, + `After`, `Highlight`, `IncludeContextMessages`, `IncludeDeletedUsers`, + `IncludeMessageBlocks`, `IncludeArchivedChannels`, `DisableSemanticSearch`, `Modifiers`, + `TermClauses`) and new response types (`AssistantSearchContextFile`, + `AssistantSearchContextChannel`, `AssistantSearchContextMessageContext`) to match the + full Real-Time Search API surface. +- Added `Underline`, `Highlight`, `ClientHighlight`, and `Unlink` fields to + `RichTextSectionTextStyle`. Added `Style` field to `RichTextSectionUserGroupElement`. +- Added `BotOptional` and `UserOptional` fields to `OAuthScopes` for app manifests. +- Added PKCE support for OAuth: `OAuthOptionCodeVerifier` option for + `GetOAuthV2Response`, `GenerateCodeVerifier()` and `GenerateCodeChallenge()` + helper functions (RFC 7636). `client_secret` is now conditionally omitted when + empty in both `GetOAuthV2ResponseContext` and `RefreshOAuthV2TokenContext`. + +### Fixed + +- `ChannelTypes` and `ContentTypes` now send comma-separated values instead of repeated + form keys, matching the convention used by every other method in the library. +- In `socketmode` malformed JSON messages no longer force an unnecessary reconnect. + Instead the error is emitted and the connection continues as normal. + +## [0.21.1] - 2026-04-08 + +### Added + +- **`slackevents.ChannelType*` constants and `MessageEvent` helpers** — Added + `ChannelTypeChannel`, `ChannelTypeGroup`, `ChannelTypeIM`, `ChannelTypeMPIM` constants + and `IsChannel()`, `IsGroup()`, `IsIM()`, `IsMpIM()` methods on `MessageEvent` so + callers no longer need to compare raw strings. + +### Fixed + +- **Duplicate attachment/block serialization in `MsgOptionAttachments` / `MsgOptionBlocks`** — + Attachments and blocks were serialized twice: once into typed struct fields (for the JSON + response-URL path) and again into `url.Values` (for the form POST path). Serialization for + the form path now happens inside `formSender.BuildRequestContext`, so each sender owns its + own marshalling. This fixes the long-standing FIXME and eliminates redundant `json.Marshal` + calls in the option functions. ([#1547]) + + > [!NOTE] + > `UnsafeApplyMsgOptions` returns `config.values` directly. After this change, + > `attachments` and `blocks` keys are no longer present in those values since + > marshalling is deferred to send time. This function is documented as unsupported. + +## [0.21.0] - 2026-04-05 + +### Deprecated + +- **`slackevents.ParseActionEvent`** — Cannot parse `block_actions` payloads (returns + unmarshalling error). Use `slack.InteractionCallback` with `json.Unmarshal` instead, + or `slack.InteractionCallbackParse` for HTTP requests. `InteractionCallback` handles + all interaction types. ([#596]) +- **`slackevents.MessageAction`**, **`MessageActionEntity`**, **`MessageActionResponse`** — + Associated types that only support legacy `interactive_message` payloads. + +### Removed + +- **`IM` struct** — Removed the `IM` struct (and unused internal types `imChannel`, + `imResponseFull`). The `IsUserDeleted` field has been moved to `Conversation`, where it + is populated for IM-type conversations. Code using `IM` should switch to `Conversation`. + + > [!NOTE] + > In practice no user should be affected — `IM` was never returned by any public API + > method in this library, so there was no way to obtain one outside of manual construction. + +- **`Info.GetBotByID`, `GetUserByID`, `GetChannelByID`, `GetGroupByID`, `GetIMByID`** — + These methods were deprecated and returned `nil` unconditionally. They have been removed. + + > [!WARNING] + > **Breaking change.** If you are calling any of these methods, remove those calls — they + > were already no-ops. + +### Added + +- **`admin.teams.settings.*` API support** — `AdminTeamsSettingsInfo`, + `AdminTeamsSettingsSetDefaultChannels`, `AdminTeamsSettingsSetDescription`, + `AdminTeamsSettingsSetDiscoverability`, `AdminTeamsSettingsSetIcon`, and + `AdminTeamsSettingsSetName`. Includes `TeamDiscoverability` enum with `Open`, + `InviteOnly`, `Closed`, and `Unlisted` variants. ([#960]) +- **`OAuthOptionAPIURL` for package-level OAuth functions** — All package-level OAuth + functions (`GetOAuthV2Response`, `GetOpenIDConnectToken`, `RefreshOAuthV2Token`, etc.) + now accept variadic `OAuthOption` arguments. Use `OAuthOptionAPIURL(url)` to override + the default Slack API URL, enabling integration tests against a local HTTP server. + Existing callers are unaffected. ([#744]) +- **`GetOpenIDConnectUserInfo` / `GetOpenIDConnectUserInfoContext`** — Returns identity + information about the user associated with the token via `openid.connect.userInfo`. + Complements the existing `GetOpenIDConnectToken` method. ([#967]) +- **HTTP response headers** — Slack API response headers (e.g. `X-OAuth-Scopes`, + `X-Accepted-OAuth-Scopes`, `X-Ratelimit-*`) are now accessible. `AuthTestResponse` + exposes a `Header` field directly. For all other methods, use + `OptionOnResponseHeaders(func(method string, headers http.Header))` to register a + callback that fires after every API call. ([#1076]) +- **`DNDOptionTeamID`** — `GetDNDInfo` and `GetDNDTeamInfo` now accept optional + `ParamOption` arguments. Use `DNDOptionTeamID("T...")` to pass `team_id`, which is + required after workspace migration (Slack returns `missing_argument` without it). + ([#1157]) +- **`UpdateUserGroupMembersList` / `UpdateUserGroupMembersListContext`** — Convenience + wrappers around `UpdateUserGroupMembers` that accept `[]string` instead of a + comma-separated string, enabling clean chaining with `GetUserGroupMembers`. ([#1172]) +- **`SetUserProfile` / `SetUserProfileContext`** — Set multiple user profile fields in a + single API call by passing a `*UserProfile` struct to `users.profile.set`. Complements + the existing single-field methods (`SetUserRealName`, `SetUserCustomStatus`, etc.). + ([#1158]) +- **API warning callbacks** — Slack API responses may include a `warnings` field with + deprecation notices or usage hints. Use `OptionWarnings(func(warnings []string))` to + register a callback that receives these warnings. ([#1540]) +- **RTM support for `user_status_changed`, `user_huddle_changed`, `user_profile_changed` + events** — these events are now mapped in `EventMapping` with dedicated structs + (`UserStatusChangedEvent`, `UserHuddleChangedEvent`, `UserProfileChangedEvent`). + Previously they triggered `UnmarshallingErrorEvent`. ([#1541]) +- **RTM support for `sh_room_join`, `sh_room_leave`, `sh_room_update`, `channel_updated` + events** — Slack Call/Huddle room events and channel property updates are now mapped with + dedicated structs (`SHRoomJoinEvent`, `SHRoomLeaveEvent`, `SHRoomUpdateEvent`, + `ChannelUpdatedEvent`). ([#858]) +- **`CacheTS` and `EventTS` fields on `UserChangeEvent`** — these fields were sent by Slack + but silently dropped during unmarshalling. +- **`workflows.featured` API support** — add, list, remove, and set featured workflows on + channels via `WorkflowsFeaturedAdd`, `WorkflowsFeaturedList`, `WorkflowsFeaturedRemove`, + and `WorkflowsFeaturedSet` +- **`IsConnectorBot` and `IsWorkflowBot` in `User`** — boolean flags for connector and + workflow bot users +- **`GuestInvitedBy` in `UserProfile`** — user ID of whoever invited a guest user +- **`Blocks` field on `MessageEvent`** — block data from webhook payloads is now directly + accessible via `event.Blocks` instead of only through `event.Message.Blocks`. ([#1257]) +- **`Username` field on `User`** — Slack's interaction payloads (block_actions, shortcuts) + include a `username` field in the user object that was previously dropped during + unmarshalling. ([#1218]) +- **`Blocks`, `Attachments`, `Files`, `Upload` fields on `AppMentionEvent`** — these fields + are sent by Slack in `app_mention` event payloads but were silently dropped. ([#961]) +- **`HandleShortcut`, `HandleViewSubmission`, `HandleViewClosed` in socketmode handler** — + Level 3 handlers that dispatch `shortcut`/`message_action`, `view_submission`, and + `view_closed` interactions by `CallbackID`, matching the pattern of + `HandleInteractionBlockAction` and `HandleSlashCommand`. ([#1161]) +- **`BlockFromJSON` / `MustBlockFromJSON`** — Create blocks from raw JSON strings, enabling + direct use of output from Slack's [Block Kit Builder](https://app.slack.com/block-kit-builder) + or quick adoption of new block types before the library adds typed support. The original + JSON is preserved through marshalling. ([#1497]) + +### Documentation + +- **`ViewSubmissionResponse` constructors** — `NewClearViewSubmissionResponse`, + `NewUpdateViewSubmissionResponse`, `NewPushViewSubmissionResponse`, and + `NewErrorsViewSubmissionResponse` now have doc comments explaining the HTTP response + pattern (write JSON and return promptly) and the Socket Mode pattern (pass as Ack + payload). `NewErrorsViewSubmissionResponse` additionally documents that map keys must + be `BlockID`s of `InputBlock` elements. ([#726], [#1013]) +- **Socket Mode examples** — `examples/socketmode/` and `examples/socketmode_handler/` now + have doc comments explaining the two-token requirement: app-level token (`xapp-`) for the + WebSocket connection and bot token (`xoxb-`) for API calls. ([#941]) + +### Fixed + +- **`UnknownBlock` round-trip data loss** — Unrecognized block types (e.g. new Slack block + types not yet supported by this library) now preserve their full JSON through + unmarshal/marshal cycles. Previously only `type` and `block_id` were retained, silently + discarding all other fields. + +### Changed + +- Adjusted some `admin` errors that started with uppercase to be lowercase per go + conventions. + + > [!WARNING] + > **Breaking change.** If you are matching the error content in your code, this is a + > BREAKING CHANGE. +- **`WebhookMessage.UnfurlLinks` and `UnfurlMedia` are now `*bool`** — Previously these + were `bool` with `omitempty`, which meant `false` was silently stripped from the JSON + payload. Users could not explicitly disable link or media unfurling via webhooks. The + fields are now `*bool` so that `nil` (omit), `false`, and `true` all serialize correctly. + ([#1231]) + + > [!WARNING] + > **Breaking change.** Code that sets these fields directly must be updated: + > + > ```go + > // Before + > msg := slack.WebhookMessage{UnfurlLinks: true} + > + > // After — use a helper or a variable + > t := true + > msg := slack.WebhookMessage{UnfurlLinks: &t} + > ``` + > + > Leaving the fields unset (`nil`) preserves the previous default behavior — Slack's + > server-side defaults apply (`unfurl_links=false`, `unfurl_media=true`). + +- **`User.Has2FA` is now `*bool`** — When using a bot token, Slack's `users.list` API omits + `has_2fa` entirely. With a plain `bool`, this was indistinguishable from explicitly `false`. + Now `nil` means absent/unknown, `false` means explicitly disabled, `true` means enabled. + ([#1121]) + + > [!WARNING] + > **Breaking change.** Code that reads `Has2FA` must handle the pointer: + > + > ```go + > // Before + > if user.Has2FA { ... } + > + > // After + > if user.Has2FA != nil && *user.Has2FA { ... } + > ``` + +- **`ListReactions` now uses cursor-based pagination** — `ListReactionsParameters` replaces + `Count`/`Page` with `Cursor`/`Limit`, and `ListReactions`/`ListReactionsContext` now return + `([]ReactedItem, string, error)` where the string is the next cursor, instead of + `([]ReactedItem, *Paging, error)`. ([#825]) + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewListReactionsParameters() + > params.Count = 100 + > params.Page = 2 + > items, paging, err := api.ListReactions(params) + > + > // After + > params := slack.NewListReactionsParameters() + > params.Limit = 100 + > items, nextCursor, err := api.ListReactions(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +- **`ListStars`/`GetStarred` now use cursor-based pagination** — `StarsParameters` replaces + `Count`/`Page` with `Cursor`/`Limit` (and adds `TeamID`), and `ListStars`/`ListStarsContext`/ + `GetStarred`/`GetStarredContext` now return `string` (next cursor) instead of `*Paging`. + Slack's `stars.list` API no longer returns `paging` data — only `response_metadata.next_cursor`. + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewStarsParameters() + > params.Count = 100 + > params.Page = 2 + > items, paging, err := api.ListStars(params) + > + > // After + > params := slack.NewStarsParameters() + > params.Limit = 100 + > items, nextCursor, err := api.ListStars(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +- **`GetAccessLogs` now uses cursor-based pagination** — `AccessLogParameters` replaces + `Count`/`Page` with `Cursor`/`Limit` (and adds `Before`), and `GetAccessLogs`/ + `GetAccessLogsContext` now return `string` (next cursor) instead of `*Paging`. + Slack's `team.accessLogs` API warns `use_cursor_pagination_instead` when using the old + parameters. + + > [!WARNING] + > **Breaking change.** Both the parameters and return signature have changed: + > + > ```go + > // Before + > params := slack.NewAccessLogParameters() + > params.Count = 100 + > params.Page = 2 + > logins, paging, err := api.GetAccessLogs(params) + > + > // After + > params := slack.NewAccessLogParameters() + > params.Limit = 100 + > logins, nextCursor, err := api.GetAccessLogs(params) + > // Use nextCursor for the next page: params.Cursor = nextCursor + > ``` + +### Fixed + +- **Socket Mode: large Ack payloads no longer silently fail** — Two issues caused `Ack()` + payloads to be silently dropped by Slack. First, gorilla/websocket's default 4KB write + buffer fragmented messages into WebSocket continuation frames that Slack does not + reassemble. The library now uses a 32KB write buffer. Second, Slack silently drops + Socket Mode responses at or above 20KB — `Ack()`, `Send()`, and `SendCtx()` now return + an error when the serialized response reaches this limit. ([#1196]) + + > [!WARNING] + > **Breaking change.** `Ack()` and `Send()` now return `error`. Existing call sites that + > don't capture the return value continue to compile without changes. + +- **`MsgOptionBlocks()` with no arguments now sends `blocks=[]`** — Previously, calling + `MsgOptionBlocks()` with no arguments or a nil spread was a silent no-op, making it + impossible to clear blocks from a message via `chat.update`. The Slack API requires an + explicit `blocks=[]` to reliably remove blocks. ([#1214]) + + > [!WARNING] + > **Breaking change.** `MsgOptionBlocks()` with no arguments now sends `blocks=[]` instead + > of being a no-op. If you were relying on this to be a no-op, remove the option entirely: + > + > ```go + > // Before — this was a no-op, now it sends blocks=[] + > api.PostMessage(ch, slack.MsgOptionBlocks(), slack.MsgOptionText("text", false)) + > + > // After — omit MsgOptionBlocks entirely to not set blocks + > api.PostMessage(ch, slack.MsgOptionText("text", false)) + > ``` + +- **`WorkflowButtonBlockElement` missing from `UnmarshalJSON`** — `workflow_button` blocks + now unmarshal correctly through `BlockElements`, `InputBlock`, and `Accessory` paths. + Also adds missing `multi_*_select` and `file_input` cases to `BlockElements.UnmarshalJSON`, + and fixes `toBlockElement` for `RichTextInputElement` and `WorkflowButtonElement`. ([#1539]) +- **`NewBlockHeader` nil pointer dereference** — passing a nil text object no longer panics. ([#1236]) +- **`ValidateUniqueBlockID` rejects empty block IDs** — multiple input blocks with no + explicit `block_id` set (empty string) were incorrectly flagged as duplicates, causing + `OpenView` to fail. ([#1184]) + +## [0.20.0] - 2026-03-21 + +> [!WARNING] +> `trigger_id` and `workflow_id` are NOT in any documentation or in any of the official +libraries, so exercise caution if you use these. + +### Added + +- **`workflow_id` and `trigger_id` in `Message`** — It seems that some types of messages, + e.g: `bot_message`, can carry `trigger_id` and `workflow_id`. +- **`RichTextQuote.Border` field** — optional border toggle (matches the docs now) +- **`RichTextPreformatted.Language` field** — enables syntax highlighting for preformatted + blocks + +### Fixed + +- **Remove embedding of `RichTextSection`** — `RichTextQuote` and `RichTextPreformatted` + are now flattened as they should have always been. This is a breaking change for anyone + using these structs directly. + +## [0.19.0] - 2026-03-04 + +### Added + +- **Optional HTTP retry for Web API** — Retries are off by default. Enable with `OptionRetry(n)` for 429-only retries or `OptionRetryConfig(cfg)` for full control including 5xx and connection errors with exponential backoff. ([#1532]) +- **`task_card` and `plan` agent blocks** — New block types for task cards and plan agent blocks. ([#1536]) + +### Changed + +- CI: bumped `actions/stale` from 10.1.1 to 10.2.0. ([#1534]) +- Use `golangci-lint` in Makefile. ([#1533]) + +## [0.18.0] - 2026-02-21 + +### Added + +- **`focus_on_load` support for remaining block elements** — Static/external/users/conversations/channels select, multi-select variants, datepicker, timepicker, plain_text_input, checkboxes, radio_buttons, and number_input. ([#1519]) +- **`PlainText` and `PreviewPlainText` fields on `File`** — Email file objects now include the plain text body fields instead of silently discarding them. ([#1522]) +- **Missing fields on `User`, `UserProfile`, and `EnterpriseUser`** — `who_can_share_contact_card`, `always_active`, `pronouns`, `image_1024`, `is_custom_image`, `status_text_canonical`, `huddle_state`, `huddle_state_expiration_ts`, `start_date`, and `is_primary_owner`. ([#1526]) +- **Work Objects support** — Chat unfurl with Work Object metadata, entity details (flexpane), `entity_details_requested` event, and associated types (`WorkObjectMetadata`, `WorkObjectEntity`, `WorkObjectExternalRef`). ([#1529]) +- **`admin.roles.*` API methods** — `admin.roles.listAssignments`, `admin.roles.addAssignments`, and `admin.roles.removeAssignments`. ([#1520]) + +### Fixed + +- **`UserProfile.Skype` JSON tag** — Corrected typo from `"skyp"` to `"skype"`. ([#1524]) +- **`assistant.threads.setSuggestedPrompts` title parameter** — Title is now sent when non-empty. ([#1528]) + +### Changed + +- CI test matrix updated: dropped Go 1.24, added Go 1.26; bumped golangci-lint to v2.10.1. ([#1530]) + +## [0.18.0-rc2] - 2026-01-28 + +### Added + +- **Audit Logs example** - New example demonstrating how to use the Audit Logs API. ([#1144]) +- **Admin Conversations API support** - Comprehensive support for `admin.conversations.*` + methods including core operations (archive, unarchive, create, delete, rename, invite, + search, lookup, getTeams, convertToPrivate, convertToPublic, disconnectShared, setTeams), + bulk operations (bulkArchive, bulkDelete, bulkMove), preferences, retention management, + restrict access controls, and EKM channel info. ([#1329]) + +### Changed + +- **BREAKING**: Removed deprecated `UploadFile`, `UploadFileContext`, and + `FileUploadParameters`. The `files.upload` API was discontinued by Slack on November + 12, 2025. ([#1481]) +- **BREAKING**: Renamed `UploadFileV2` → `UploadFile`, `UploadFileV2Context` → + `UploadFileContext`, and `UploadFileV2Parameters` → `UploadFileParameters`. The "V2" + suffix is no longer needed now that the old API is removed. ([#1481]) + +### Fixed + +- **File upload error wrapping** - `UploadFile` now wraps errors with the step name + (`GetUploadURLExternal`, `UploadToURL`, or `CompleteUploadExternal`) so callers can + identify which of the three upload steps failed. ([#1491]) +- **Audit Logs API endpoint** - Fixed `GetAuditLogs` to use the correct endpoint + (`api.slack.com`) instead of the regular API endpoint (`slack.com/api`). The Audit + Logs API requires a different base URL. Added `OptionAuditAPIURL` for testing. ([#1144]) +- **Socket mode websocket dial debugging** - Added debug logging when a custom dialer is + used including HTTP response status on dial failures. This helps diagnose proxy/TLS + issues like "bad handshake" errors. ([#1360]) +- **`MsgOptionPostMessageParameters` now passes `MetaData`** - Previously, metadata was + silently dropped when using `PostMessageParameters`. ([#1343]) + +## [0.18.0-rc1] - 2026-01-26 + +### Added + +- **Huddle support** - New `HuddleRoom`, `HuddleParticipantEvent`, and `HuddleRecording` + types for handling Slack huddle events (`huddle_thread` subtype messages). +- **Call block data parsing** - `CallBlock` now includes full call data when retrieved + from Slack messages, with new `CallBlockData`, `CallBlockDataV1`, and `CallBlockIconURLs` + types. ([#897]) +- **Chat Streaming API support** - New streaming API for real-time chat interactions + with example usage. ([#1506]) +- **Data Access API support** - Full support for Slack's Data Access API with + example implementation. ([#1439]) +- **Cursor-based pagination for `GetUsers`** - More efficient user retrieval + with cursor pagination. ([#1465]) +- **`GetAllConversations` with pagination** - Retrieve all conversations with + automatic pagination handling, including rate limit and server error handling. ([#1463]) +- **Table blocks support** - Parse and create table blocks with proper + unmarshaling. ([#1490], [#1511]) +- **Context actions block support** - New `context_actions` block type. ([#1495]) +- **Workflow button block element** - Support for `workflow_button` in block + elements. ([#1499]) +- **`loading_messages` parameter for `SetAssistantThreadsStatus`** - Optional + parameter to customize loading state messages. ([#1489]) +- **Attachment image fields** - Added `ImageBytes`, `ImageHeight`, and `ImageWidth` + fields to attachments. ([#1516]) +- **`RecordChannel` to conversation properties** - New property for conversation + metadata. ([#1513]) +- **Title argument for `CreateChannelCanvas`** - Canvas creation now supports + custom titles. ([#1483]) +- **`PostEphemeral` handler for slacktest** - Audit outgoing ephemeral messages + in test environments. ([#1517]) +- **`PreviewImageName` for remote files** - Customize preview image filename + instead of using the default `preview.jpg`. + +### Fixed + +- **`PublishView` no longer sends empty hash** - Prevents unnecessary payload + when hash is empty. ([#1515]) +- **`ImageBlockElement` validation** - Now properly validates that either + `imageURL` or `SlackFile` is provided. ([#1488]) +- **Rich text section channel return** - Correctly returns channel for section + channel rich text elements. ([#1472]) +- **`KickUserFromConversation` error handling** - Errors are now properly parsed + as a map structure. ([#1471]) + +### Changed + +- **BREAKING**: `GetReactions` now returns `ReactedItem` instead of `[]ItemReaction`. + This aligns the response with the actual Slack API, which includes the item itself + (message, file, or file_comment) alongside reactions. To migrate, use `resp.Reactions` + to access the slice of reactions. ([#1480]) +- **BREAKING**: `Settings` struct fields `Interactivity` and `EventSubscriptions` + are now pointers, allowing them to be omitted when empty. ([#1461]) +- Minimum Go version bumped to 1.24. ([#1504]) + +## [0.17.3] - 2025-07-04 + +Previous release. See [GitHub releases](https://github.com/slack-go/slack/releases/tag/v0.17.3) +for details. + +[#897]: https://github.com/slack-go/slack/issues/897 +[#1236]: https://github.com/slack-go/slack/issues/1236 +[#1257]: https://github.com/slack-go/slack/issues/1257 +[#1144]: https://github.com/slack-go/slack/issues/1144 +[#1329]: https://github.com/slack-go/slack/issues/1329 +[#1343]: https://github.com/slack-go/slack/issues/1343 +[#1360]: https://github.com/slack-go/slack/issues/1360 +[#1439]: https://github.com/slack-go/slack/pull/1439 +[#1461]: https://github.com/slack-go/slack/pull/1461 +[#1463]: https://github.com/slack-go/slack/pull/1463 +[#1465]: https://github.com/slack-go/slack/pull/1465 +[#1471]: https://github.com/slack-go/slack/pull/1471 +[#1472]: https://github.com/slack-go/slack/pull/1472 +[#1480]: https://github.com/slack-go/slack/pull/1480 +[#1483]: https://github.com/slack-go/slack/pull/1483 +[#1488]: https://github.com/slack-go/slack/pull/1488 +[#1489]: https://github.com/slack-go/slack/pull/1489 +[#1490]: https://github.com/slack-go/slack/pull/1490 +[#1491]: https://github.com/slack-go/slack/issues/1491 +[#1495]: https://github.com/slack-go/slack/pull/1495 +[#1497]: https://github.com/slack-go/slack/pull/1497 +[#1499]: https://github.com/slack-go/slack/pull/1499 +[#1504]: https://github.com/slack-go/slack/pull/1504 +[#1506]: https://github.com/slack-go/slack/pull/1506 +[#1511]: https://github.com/slack-go/slack/pull/1511 +[#1513]: https://github.com/slack-go/slack/pull/1513 +[#1515]: https://github.com/slack-go/slack/pull/1515 +[#1516]: https://github.com/slack-go/slack/pull/1516 +[#1517]: https://github.com/slack-go/slack/pull/1517 +[#1519]: https://github.com/slack-go/slack/pull/1519 +[#1520]: https://github.com/slack-go/slack/pull/1520 +[#1522]: https://github.com/slack-go/slack/pull/1522 +[#1524]: https://github.com/slack-go/slack/pull/1524 +[#1526]: https://github.com/slack-go/slack/pull/1526 +[#1528]: https://github.com/slack-go/slack/pull/1528 +[#1529]: https://github.com/slack-go/slack/pull/1529 +[#1530]: https://github.com/slack-go/slack/pull/1530 +[#1532]: https://github.com/slack-go/slack/pull/1532 +[#1533]: https://github.com/slack-go/slack/pull/1533 +[#1534]: https://github.com/slack-go/slack/pull/1534 +[#1536]: https://github.com/slack-go/slack/pull/1536 +[#596]: https://github.com/slack-go/slack/issues/596 +[#1541]: https://github.com/slack-go/slack/issues/1541 +[#1172]: https://github.com/slack-go/slack/issues/1172 +[#1076]: https://github.com/slack-go/slack/issues/1076 +[#1157]: https://github.com/slack-go/slack/issues/1157 +[#1196]: https://github.com/slack-go/slack/issues/1196 +[#1547]: https://github.com/slack-go/slack/pull/1547 + +[Unreleased]: https://github.com/slack-go/slack/compare/v0.23.1...HEAD +[0.23.1]: https://github.com/slack-go/slack/compare/v0.23.0...v0.23.1 +[0.23.0]: https://github.com/slack-go/slack/compare/v0.22.0...v0.23.0 +[0.22.0]: https://github.com/slack-go/slack/compare/v0.21.1...0.22.0 +[0.21.1]: https://github.com/slack-go/slack/compare/v0.21.0...v0.21.1 +[0.21.0]: https://github.com/slack-go/slack/compare/v0.20.0...v0.21.0 +[0.20.0]: https://github.com/slack-go/slack/compare/v0.19.0...v0.20.0 +[0.19.0]: https://github.com/slack-go/slack/compare/v0.18.0...v0.19.0 +[0.18.0]: https://github.com/slack-go/slack/compare/v0.18.0-rc2...v0.18.0 +[0.18.0-rc2]: https://github.com/slack-go/slack/releases/tag/v0.18.0-rc2 +[0.18.0-rc1]: https://github.com/slack-go/slack/releases/tag/v0.18.0-rc1 +[0.17.3]: https://github.com/slack-go/slack/releases/tag/v0.17.3 diff --git a/vendor/github.com/slack-go/slack/Makefile b/vendor/github.com/slack-go/slack/Makefile index 7279640164..a8104014d5 100644 --- a/vendor/github.com/slack-go/slack/Makefile +++ b/vendor/github.com/slack-go/slack/Makefile @@ -7,7 +7,7 @@ help: @echo "" @echo " make deps : Fetch all dependencies" @echo " make fmt : Run go fmt to fix any formatting issues" - @echo " make lint : Use go vet to check for linting issues" + @echo " make lint : Run golangci-lint for linting issues" @echo " make test : Run all short tests" @echo " make test-race : Run all tests with race condition checking" @echo " make test-integration : Run all tests without limiting to short" @@ -22,7 +22,7 @@ fmt: @go fmt . lint: - @go vet . + @command -v golangci-lint >/dev/null 2>&1 && golangci-lint run ./... || go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 run ./... test: @go test -v -count=1 -timeout 300s -short ./... diff --git a/vendor/github.com/slack-go/slack/README.md b/vendor/github.com/slack-go/slack/README.md index 127ae253c8..53dd912d30 100644 --- a/vendor/github.com/slack-go/slack/README.md +++ b/vendor/github.com/slack-go/slack/README.md @@ -1,9 +1,7 @@ Slack API in Go [![Go Reference](https://pkg.go.dev/badge/github.com/slack-go/slack.svg)](https://pkg.go.dev/github.com/slack-go/slack) [![CI](https://github.com/slack-go/slack/actions/workflows/test.yml/badge.svg)](https://github.com/slack-go/slack/actions/workflows/test.yml) =============== -This is the original Slack library for Go created by Norberto Lopes, transferred to a GitHub organization. - -You can also chat with us on the #slack-go, #slack-go-ja Slack channel on the Gophers Slack. +You can chat with us on the [#slack-go](https://gophers.slack.com/archives/C02JQ98JHNC), [#slack-go-ja](https://gophers.slack.com/archives/C02HNL8EN3H) Slack channel on the [Gophers Slack](https://gophers.slack.com). ![logo](logo.png "icon") @@ -17,6 +15,11 @@ Therefore, minor version releases may include backward incompatible changes. See [Releases](https://github.com/slack-go/slack/releases) for more information about the changes. +## Go Versions supported + +We support the same versions of Go as the officially supported Go versions (see [Go +Release Policy](https://go.dev/doc/devel/release#policy)). + ## Installing ### *go get* @@ -29,24 +32,24 @@ See [Releases](https://github.com/slack-go/slack/releases) for more information ```golang import ( - "fmt" + "fmt" - "github.com/slack-go/slack" + "github.com/slack-go/slack" ) func main() { - api := slack.New("YOUR_TOKEN_HERE") - // If you set debugging, it will log all requests to the console - // Useful when encountering issues - // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) - groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false)) - if err != nil { - fmt.Printf("%s\n", err) - return - } - for _, group := range groups { - fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) - } + api := slack.New("YOUR_TOKEN_HERE") + // If you set debugging, it will log all requests to the console + // Useful when encountering issues + // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) + groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false)) + if err != nil { + fmt.Printf("%s\n", err) + return + } + for _, group := range groups { + fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) + } } ``` @@ -63,13 +66,21 @@ func main() { api := slack.New("YOUR_TOKEN_HERE") user, err := api.GetUserInfo("U023BECGF") if err != nil { - fmt.Printf("%s\n", err) - return + fmt.Printf("%s\n", err) + return } fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) } ``` +### HTTP retries + +Retries are off by default. Use **OptionRetry(n)** for 429-only retries, or **OptionRetryConfig(cfg)** for full control (connection, 429, opt-in 5xx). With a custom client, pass retry options after `OptionHTTPClient`. See package `slack` doc for handler details. + +```golang +api := slack.New("YOUR_TOKEN_HERE", slack.OptionRetry(3)) +``` + ## Minimal Socket Mode usage: See https://github.com/slack-go/slack/blob/master/examples/socketmode/socketmode.go diff --git a/vendor/github.com/slack-go/slack/admin.go b/vendor/github.com/slack-go/slack/admin.go index d51426b565..1b0d2178a2 100644 --- a/vendor/github.com/slack-go/slack/admin.go +++ b/vendor/github.com/slack-go/slack/admin.go @@ -59,7 +59,7 @@ func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, fi err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { - return fmt.Errorf("Failed to invite single-channel guest: %s", err) + return fmt.Errorf("failed to invite single-channel guest: %s", err) } return nil @@ -86,7 +86,7 @@ func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channe err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { - return fmt.Errorf("Failed to restricted account: %s", err) + return fmt.Errorf("failed to restricted account: %s", err) } return nil @@ -110,7 +110,7 @@ func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { - return fmt.Errorf("Failed to invite to team: %s", err) + return fmt.Errorf("failed to invite to team: %s", err) } return nil @@ -132,7 +132,7 @@ func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) err := api.adminRequest(ctx, "setRegular", teamName, values) if err != nil { - return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) + return fmt.Errorf("failed to change the user (%s) to a regular user: %s", user, err) } return nil @@ -154,7 +154,7 @@ func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, use err := api.adminRequest(ctx, "sendSSOBind", teamName, values) if err != nil { - return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) + return fmt.Errorf("failed to send SSO binding email for user (%s): %s", user, err) } return nil @@ -177,7 +177,7 @@ func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, err := api.adminRequest(ctx, "setUltraRestricted", teamName, values) if err != nil { - return fmt.Errorf("Failed to ultra-restrict account: %s", err) + return fmt.Errorf("failed to ultra-restrict account: %s", err) } return nil diff --git a/vendor/github.com/slack-go/slack/admin_conversations.go b/vendor/github.com/slack-go/slack/admin_conversations.go index 6f76568ba2..eba5d98ea3 100644 --- a/vendor/github.com/slack-go/slack/admin_conversations.go +++ b/vendor/github.com/slack-go/slack/admin_conversations.go @@ -2,11 +2,733 @@ package slack import ( "context" + "encoding/json" "net/url" "strconv" "strings" ) +// AdminConversationsInviteParams contains arguments for AdminConversationsInvite method call. +type AdminConversationsInviteParams struct { + ChannelID string + UserIDs []string +} + +// AdminConversationsInvite invites users to a channel. +// For more information see the admin.conversations.invite docs: +// https://api.slack.com/methods/admin.conversations.invite +func (api *Client) AdminConversationsInvite(ctx context.Context, params AdminConversationsInviteParams) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + "user_ids": {strings.Join(params.UserIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.invite", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsArchive archives a public or private channel. +// For more information see the admin.conversations.archive docs: +// https://api.slack.com/methods/admin.conversations.archive +func (api *Client) AdminConversationsArchive(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.archive", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsUnarchive unarchives a public or private channel. +// For more information see the admin.conversations.unarchive docs: +// https://api.slack.com/methods/admin.conversations.unarchive +func (api *Client) AdminConversationsUnarchive(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.unarchive", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsRename renames a public or private channel. +// For more information see the admin.conversations.rename docs: +// https://api.slack.com/methods/admin.conversations.rename +func (api *Client) AdminConversationsRename(ctx context.Context, channelID, name string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "name": {name}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.rename", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsDelete deletes a public or private channel. +// For more information see the admin.conversations.delete docs: +// https://api.slack.com/methods/admin.conversations.delete +func (api *Client) AdminConversationsDelete(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.delete", values, response) + if err != nil { + return err + } + + return response.Err() +} + +type adminConversationsDisconnectSharedParams struct { + leavingTeamIDs []string +} + +// AdminConversationsDisconnectSharedOption is an option for AdminConversationsDisconnectShared. +type AdminConversationsDisconnectSharedOption func(*adminConversationsDisconnectSharedParams) + +// AdminConversationsDisconnectSharedOptionLeavingTeamIDs sets the team IDs of the workspaces to disconnect. +func AdminConversationsDisconnectSharedOptionLeavingTeamIDs(teamIDs []string) AdminConversationsDisconnectSharedOption { + return func(params *adminConversationsDisconnectSharedParams) { + params.leavingTeamIDs = teamIDs + } +} + +// AdminConversationsDisconnectShared disconnects a connected channel from one or more workspaces. +// For more information see the admin.conversations.disconnectShared docs: +// https://api.slack.com/methods/admin.conversations.disconnectShared +func (api *Client) AdminConversationsDisconnectShared(ctx context.Context, channelID string, options ...AdminConversationsDisconnectSharedOption) error { + params := adminConversationsDisconnectSharedParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + if len(params.leavingTeamIDs) > 0 { + values.Add("leaving_team_ids", strings.Join(params.leavingTeamIDs, ",")) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.disconnectShared", values, response) + if err != nil { + return err + } + + return response.Err() +} + +type adminConversationsCreateParams struct { + description string + orgWide bool + teamID string +} + +// AdminConversationsCreateOption is an option for AdminConversationsCreate. +type AdminConversationsCreateOption func(*adminConversationsCreateParams) + +// AdminConversationsCreateOptionDescription sets the description of the channel. +func AdminConversationsCreateOptionDescription(description string) AdminConversationsCreateOption { + return func(params *adminConversationsCreateParams) { + params.description = description + } +} + +// AdminConversationsCreateOptionOrgWide sets whether the channel should be org-wide. +func AdminConversationsCreateOptionOrgWide(orgWide bool) AdminConversationsCreateOption { + return func(params *adminConversationsCreateParams) { + params.orgWide = orgWide + } +} + +// AdminConversationsCreateOptionTeamID sets the team ID where the channel should be created. +func AdminConversationsCreateOptionTeamID(teamID string) AdminConversationsCreateOption { + return func(params *adminConversationsCreateParams) { + params.teamID = teamID + } +} + +// AdminConversationsCreateResponse represents the response from admin.conversations.create. +type AdminConversationsCreateResponse struct { + SlackResponse + ChannelID string `json:"channel_id"` +} + +// AdminConversationsCreate creates a public or private channel-based conversation. +// For more information see the admin.conversations.create docs: +// https://api.slack.com/methods/admin.conversations.create +func (api *Client) AdminConversationsCreate(ctx context.Context, name string, isPrivate bool, options ...AdminConversationsCreateOption) (string, error) { + params := adminConversationsCreateParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "is_private": {strconv.FormatBool(isPrivate)}, + "name": {name}, + } + + if params.description != "" { + values.Add("description", params.description) + } + + if params.orgWide { + values.Add("org_wide", "true") + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &AdminConversationsCreateResponse{} + err := api.postMethod(ctx, "admin.conversations.create", values, response) + if err != nil { + return "", err + } + + return response.ChannelID, response.Err() +} + +// AdminConversationsGetTeamsParams contains arguments for AdminConversationsGetTeams method call. +type AdminConversationsGetTeamsParams struct { + ChannelID string + Cursor string + Limit int +} + +// AdminConversationsGetTeamsResponse represents the response from admin.conversations.getTeams. +type AdminConversationsGetTeamsResponse struct { + SlackResponse + TeamIDs []string `json:"team_ids"` +} + +// AdminConversationsGetTeams gets all the workspaces a given public or private channel is connected to within this Enterprise org. +// For more information see the admin.conversations.getTeams docs: +// https://api.slack.com/methods/admin.conversations.getTeams +func (api *Client) AdminConversationsGetTeams(ctx context.Context, params AdminConversationsGetTeamsParams) ([]string, string, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + } + + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + if params.Limit > 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + + response := &AdminConversationsGetTeamsResponse{} + err := api.postMethod(ctx, "admin.conversations.getTeams", values, response) + if err != nil { + return nil, "", err + } + + return response.TeamIDs, response.ResponseMetadata.Cursor, response.Err() +} + +type adminConversationsSearchParams struct { + cursor string + limit int + query string + searchChannelType []string + sort string + sortDir string + teamIDs []string + connectedTeamIDs []string + totalCountOnly bool +} + +// AdminConversationsSearchOption is an option for AdminConversationsSearch. +type AdminConversationsSearchOption func(*adminConversationsSearchParams) + +// AdminConversationsSearchOptionCursor sets the cursor for pagination. +func AdminConversationsSearchOptionCursor(cursor string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.cursor = cursor + } +} + +// AdminConversationsSearchOptionLimit sets the maximum number of results to return. +func AdminConversationsSearchOptionLimit(limit int) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.limit = limit + } +} + +// AdminConversationsSearchOptionQuery sets the search query. +func AdminConversationsSearchOptionQuery(query string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.query = query + } +} + +// AdminConversationsSearchOptionSearchChannelTypes sets the channel types to search. +// Valid values: "private", "public", "private_exclude", "multi_workspace", "org_wide", "external_shared_exclude", "external_shared" +func AdminConversationsSearchOptionSearchChannelTypes(types []string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.searchChannelType = types + } +} + +// AdminConversationsSearchOptionSort sets the sort field. +// Valid values: "name", "member_count", "created" +func AdminConversationsSearchOptionSort(sort string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.sort = sort + } +} + +// AdminConversationsSearchOptionSortDir sets the sort direction. +// Valid values: "asc", "desc" +func AdminConversationsSearchOptionSortDir(sortDir string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.sortDir = sortDir + } +} + +// AdminConversationsSearchOptionTeamIDs filters results to channels in the specified teams. +func AdminConversationsSearchOptionTeamIDs(teamIDs []string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.teamIDs = teamIDs + } +} + +// AdminConversationsSearchOptionConnectedTeamIDs filters results to channels connected to the specified teams. +func AdminConversationsSearchOptionConnectedTeamIDs(teamIDs []string) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.connectedTeamIDs = teamIDs + } +} + +// AdminConversationsSearchOptionTotalCountOnly when true, only returns the total count of matching channels. +func AdminConversationsSearchOptionTotalCountOnly(totalCountOnly bool) AdminConversationsSearchOption { + return func(params *adminConversationsSearchParams) { + params.totalCountOnly = totalCountOnly + } +} + +// ChannelEmailAddress represents an email address associated with a channel. +type ChannelEmailAddress struct { + Address string `json:"address"` + CreatorID string `json:"creator_id"` + TeamID string `json:"team_id"` +} + +// AdminConversationOwnershipDetail represents ownership details for lists/canvas. +type AdminConversationOwnershipDetail struct { + Count int `json:"count,omitempty"` + TeamID string `json:"team_id,omitempty"` +} + +// AdminConversationLists represents lists/canvas information in admin conversations. +type AdminConversationLists struct { + OwnershipDetails []AdminConversationOwnershipDetail `json:"ownership_details,omitempty"` + TotalCount int `json:"total_count,omitempty"` +} + +// AdminConversation represents a conversation in admin API responses. +type AdminConversation struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Purpose string `json:"purpose,omitempty"` + MemberCount int `json:"member_count,omitempty"` + Created int64 `json:"created,omitempty"` + CreatorID string `json:"creator_id,omitempty"` + IsPrivate bool `json:"is_private,omitempty"` + IsArchived bool `json:"is_archived,omitempty"` + IsGeneral bool `json:"is_general,omitempty"` + LastActivityTimestamp int64 `json:"last_activity_ts,omitempty"` + IsFrozen bool `json:"is_frozen,omitempty"` + IsOrgDefault bool `json:"is_org_default,omitempty"` + IsOrgMandatory bool `json:"is_org_mandatory,omitempty"` + IsOrgShared bool `json:"is_org_shared,omitempty"` + IsExtShared bool `json:"is_ext_shared,omitempty"` + IsGlobalShared bool `json:"is_global_shared,omitempty"` + IsPendingExtShared bool `json:"is_pending_ext_shared,omitempty"` + IsDisconnectInProgress bool `json:"is_disconnect_in_progress,omitempty"` + ConnectedTeamIDs []string `json:"connected_team_ids,omitempty"` + ConnectedLimitedTeamIDs []string `json:"connected_limited_team_ids,omitempty"` + PendingConnectedTeamIDs []string `json:"pending_connected_team_ids,omitempty"` + InternalTeamIDs []string `json:"internal_team_ids,omitempty"` + InternalTeamIDsCount int `json:"internal_team_ids_count,omitempty"` + InternalTeamIDsSampleTeam string `json:"internal_team_ids_sample_team,omitempty"` + ContextTeamID string `json:"context_team_id,omitempty"` + ConversationHostID string `json:"conversation_host_id,omitempty"` + ChannelEmailAddresses []ChannelEmailAddress `json:"channel_email_addresses,omitempty"` + ChannelManagerCount int `json:"channel_manager_count,omitempty"` + ExternalUserCount int `json:"external_user_count,omitempty"` + Canvas *AdminConversationLists `json:"canvas,omitempty"` + Lists *AdminConversationLists `json:"lists,omitempty"` + Properties *Properties `json:"properties,omitempty"` +} + +// AdminConversationsSearchResponse represents the response from admin.conversations.search. +type AdminConversationsSearchResponse struct { + SlackResponse + Conversations []AdminConversation `json:"conversations"` + TotalCount int `json:"total_count"` + NextCursor string `json:"next_cursor"` +} + +// AdminConversationsSearch searches for public or private channels in an Enterprise organization. +// For more information see the admin.conversations.search docs: +// https://api.slack.com/methods/admin.conversations.search +func (api *Client) AdminConversationsSearch(ctx context.Context, options ...AdminConversationsSearchOption) (*AdminConversationsSearchResponse, error) { + params := adminConversationsSearchParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + if params.query != "" { + values.Add("query", params.query) + } + + if len(params.searchChannelType) > 0 { + values.Add("search_channel_types", strings.Join(params.searchChannelType, ",")) + } + + if params.sort != "" { + values.Add("sort", params.sort) + } + + if params.sortDir != "" { + values.Add("sort_dir", params.sortDir) + } + + if len(params.teamIDs) > 0 { + values.Add("team_ids", strings.Join(params.teamIDs, ",")) + } + + if len(params.connectedTeamIDs) > 0 { + values.Add("connected_team_ids", strings.Join(params.connectedTeamIDs, ",")) + } + + if params.totalCountOnly { + values.Add("total_count_only", "true") + } + + response := &AdminConversationsSearchResponse{} + err := api.postMethod(ctx, "admin.conversations.search", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +type adminConversationsLookupParams struct { + cursor string + limit int + maxMemberCount int +} + +// AdminConversationsLookupOption is an option for AdminConversationsLookup. +type AdminConversationsLookupOption func(*adminConversationsLookupParams) + +// AdminConversationsLookupOptionCursor sets the cursor for pagination. +func AdminConversationsLookupOptionCursor(cursor string) AdminConversationsLookupOption { + return func(params *adminConversationsLookupParams) { + params.cursor = cursor + } +} + +// AdminConversationsLookupOptionLimit sets the maximum number of results to return. +func AdminConversationsLookupOptionLimit(limit int) AdminConversationsLookupOption { + return func(params *adminConversationsLookupParams) { + params.limit = limit + } +} + +// AdminConversationsLookupOptionMaxMemberCount filters to channels with at most this many members. +func AdminConversationsLookupOptionMaxMemberCount(maxMemberCount int) AdminConversationsLookupOption { + return func(params *adminConversationsLookupParams) { + params.maxMemberCount = maxMemberCount + } +} + +// AdminConversationsLookupResponse represents the response from admin.conversations.lookup. +type AdminConversationsLookupResponse struct { + SlackResponse + Channels []string `json:"channels"` +} + +// AdminConversationsLookup returns channels on the given team matching the specified filters. +// For more information see the admin.conversations.lookup docs: +// https://api.slack.com/methods/admin.conversations.lookup +func (api *Client) AdminConversationsLookup(ctx context.Context, teamIDs []string, lastMessageActivityBefore int64, options ...AdminConversationsLookupOption) ([]string, string, error) { + params := adminConversationsLookupParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "last_message_activity_before": {strconv.FormatInt(lastMessageActivityBefore, 10)}, + "team_ids": {strings.Join(teamIDs, ",")}, + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + if params.maxMemberCount > 0 { + values.Add("max_member_count", strconv.Itoa(params.maxMemberCount)) + } + + response := &AdminConversationsLookupResponse{} + err := api.postMethod(ctx, "admin.conversations.lookup", values, response) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetadata.Cursor, response.Err() +} + +// AdminConversationsBulkArchive archives public or private channels in bulk. +// For more information see the admin.conversations.bulkArchive docs: +// https://api.slack.com/methods/admin.conversations.bulkArchive +func (api *Client) AdminConversationsBulkArchive(ctx context.Context, channelIDs []string) error { + values := url.Values{ + "token": {api.token}, + "channel_ids": {strings.Join(channelIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.bulkArchive", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsBulkDelete deletes public or private channels in bulk. +// For more information see the admin.conversations.bulkDelete docs: +// https://api.slack.com/methods/admin.conversations.bulkDelete +func (api *Client) AdminConversationsBulkDelete(ctx context.Context, channelIDs []string) error { + values := url.Values{ + "token": {api.token}, + "channel_ids": {strings.Join(channelIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.bulkDelete", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsBulkMoveParams contains arguments for AdminConversationsBulkMove method call. +type AdminConversationsBulkMoveParams struct { + ChannelIDs []string + TargetTeamID string +} + +// AdminConversationsBulkMove moves public or private channels in bulk. +// For more information see the admin.conversations.bulkMove docs: +// https://api.slack.com/methods/admin.conversations.bulkMove +func (api *Client) AdminConversationsBulkMove(ctx context.Context, params AdminConversationsBulkMoveParams) error { + values := url.Values{ + "token": {api.token}, + "channel_ids": {strings.Join(params.ChannelIDs, ",")}, + "target_team_id": {params.TargetTeamID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.bulkMove", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationPrefs represents conversation preferences. +type AdminConversationPrefs struct { + WhoCanPost *AdminConversationPref `json:"who_can_post,omitempty"` + CanThread *AdminConversationPref `json:"can_thread,omitempty"` + CanHuddle *AdminConversationPref `json:"can_huddle,omitempty"` + EnableAtHere *AdminConversationPrefEnabled `json:"enable_at_here,omitempty"` + EnableAtChannel *AdminConversationPrefEnabled `json:"enable_at_channel,omitempty"` +} + +// AdminConversationPrefEnabled represents an enabled/disabled preference. +type AdminConversationPrefEnabled struct { + Enabled bool `json:"enabled"` +} + +// AdminConversationPref represents a single conversation preference. +type AdminConversationPref struct { + Type []string `json:"type,omitempty"` + User []string `json:"user,omitempty"` +} + +// AdminConversationsGetConversationPrefsResponse represents the response from admin.conversations.getConversationPrefs. +type AdminConversationsGetConversationPrefsResponse struct { + SlackResponse + Prefs AdminConversationPrefs `json:"prefs"` +} + +// AdminConversationsGetConversationPrefs gets conversation preferences for a public or private channel. +// For more information see the admin.conversations.getConversationPrefs docs: +// https://api.slack.com/methods/admin.conversations.getConversationPrefs +func (api *Client) AdminConversationsGetConversationPrefs(ctx context.Context, channelID string) (*AdminConversationPrefs, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &AdminConversationsGetConversationPrefsResponse{} + err := api.postMethod(ctx, "admin.conversations.getConversationPrefs", values, response) + if err != nil { + return nil, err + } + + return &response.Prefs, response.Err() +} + +// AdminConversationsSetConversationPrefsParams contains arguments for AdminConversationsSetConversationPrefs method call. +type AdminConversationsSetConversationPrefsParams struct { + ChannelID string + Prefs AdminConversationPrefs +} + +// AdminConversationsSetConversationPrefs sets conversation preferences for a public or private channel. +// For more information see the admin.conversations.setConversationPrefs docs: +// https://api.slack.com/methods/admin.conversations.setConversationPrefs +func (api *Client) AdminConversationsSetConversationPrefs(ctx context.Context, params AdminConversationsSetConversationPrefsParams) error { + prefsJSON, err := json.Marshal(params.Prefs) + if err != nil { + return err + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + "prefs": {string(prefsJSON)}, + } + + response := &SlackResponse{} + err = api.postMethod(ctx, "admin.conversations.setConversationPrefs", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsGetCustomRetentionResponse represents the response from admin.conversations.getCustomRetention. +type AdminConversationsGetCustomRetentionResponse struct { + SlackResponse + DurationDays int `json:"duration_days"` + IsPolicyEnabled bool `json:"is_policy_enabled"` +} + +// AdminConversationsGetCustomRetention gets a conversation's custom retention policy. +// For more information see the admin.conversations.getCustomRetention docs: +// https://api.slack.com/methods/admin.conversations.getCustomRetention +func (api *Client) AdminConversationsGetCustomRetention(ctx context.Context, channelID string) (*AdminConversationsGetCustomRetentionResponse, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &AdminConversationsGetCustomRetentionResponse{} + err := api.postMethod(ctx, "admin.conversations.getCustomRetention", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// AdminConversationsSetCustomRetention sets a conversation's custom retention policy. +// For more information see the admin.conversations.setCustomRetention docs: +// https://api.slack.com/methods/admin.conversations.setCustomRetention +func (api *Client) AdminConversationsSetCustomRetention(ctx context.Context, channelID string, durationDays int) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "duration_days": {strconv.Itoa(durationDays)}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.setCustomRetention", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsRemoveCustomRetention removes a conversation's custom retention policy. +// For more information see the admin.conversations.removeCustomRetention docs: +// https://api.slack.com/methods/admin.conversations.removeCustomRetention +func (api *Client) AdminConversationsRemoveCustomRetention(ctx context.Context, channelID string) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.removeCustomRetention", values, response) + if err != nil { + return err + } + + return response.Err() +} + // AdminConversationsSetTeamsParams contains arguments for AdminConversationsSetTeams // method calls. type AdminConversationsSetTeamsParams struct { diff --git a/vendor/github.com/slack-go/slack/admin_conversations_ekm.go b/vendor/github.com/slack-go/slack/admin_conversations_ekm.go new file mode 100644 index 0000000000..f4b4e14efd --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_conversations_ekm.go @@ -0,0 +1,101 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +type adminConversationsEKMListOriginalConnectedChannelInfoParams struct { + channelIDs []string + teamIDs []string + cursor string + limit int +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOption is an option for +// AdminConversationsEKMListOriginalConnectedChannelInfo. +type AdminConversationsEKMListOriginalConnectedChannelInfoOption func(*adminConversationsEKMListOriginalConnectedChannelInfoParams) + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionChannelIDs filters results to specific channels. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionChannelIDs(channelIDs []string) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.channelIDs = channelIDs + } +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionTeamIDs filters results to specific teams. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionTeamIDs(teamIDs []string) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.teamIDs = teamIDs + } +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionCursor sets the cursor for pagination. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionCursor(cursor string) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.cursor = cursor + } +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoOptionLimit sets the maximum number of results to return. +func AdminConversationsEKMListOriginalConnectedChannelInfoOptionLimit(limit int) AdminConversationsEKMListOriginalConnectedChannelInfoOption { + return func(params *adminConversationsEKMListOriginalConnectedChannelInfoParams) { + params.limit = limit + } +} + +// AdminConversationsEKMOriginalConnectedChannelInfo represents channel info for EKM response. +type AdminConversationsEKMOriginalConnectedChannelInfo struct { + ID string `json:"id"` + OriginalConnectedHostID string `json:"original_connected_host_id"` + OriginalConnectedChannelID string `json:"original_connected_channel_id"` + InternalTeamIDs []string `json:"internal_team_ids_count"` +} + +// AdminConversationsEKMListOriginalConnectedChannelInfoResponse represents the response from +// admin.conversations.ekm.listOriginalConnectedChannelInfo. +type AdminConversationsEKMListOriginalConnectedChannelInfoResponse struct { + SlackResponse + Channels []AdminConversationsEKMOriginalConnectedChannelInfo `json:"channels"` +} + +// AdminConversationsEKMListOriginalConnectedChannelInfo lists the original connected channel +// information for Slack Connect channels. +// For more information see the admin.conversations.ekm.listOriginalConnectedChannelInfo docs: +// https://api.slack.com/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo +func (api *Client) AdminConversationsEKMListOriginalConnectedChannelInfo(ctx context.Context, options ...AdminConversationsEKMListOriginalConnectedChannelInfoOption) (*AdminConversationsEKMListOriginalConnectedChannelInfoResponse, error) { + params := adminConversationsEKMListOriginalConnectedChannelInfoParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + } + + if len(params.channelIDs) > 0 { + values.Add("channel_ids", strings.Join(params.channelIDs, ",")) + } + + if len(params.teamIDs) > 0 { + values.Add("team_ids", strings.Join(params.teamIDs, ",")) + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + response := &AdminConversationsEKMListOriginalConnectedChannelInfoResponse{} + err := api.postMethod(ctx, "admin.conversations.ekm.listOriginalConnectedChannelInfo", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go b/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go new file mode 100644 index 0000000000..344579972f --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_conversations_restrictAccess.go @@ -0,0 +1,150 @@ +package slack + +import ( + "context" + "net/url" +) + +// AdminConversationsRestrictAccessAddGroup + +type adminConversationsRestrictAccessAddGroupParams struct { + teamID string +} + +// AdminConversationsRestrictAccessAddGroupOption is an option for AdminConversationsRestrictAccessAddGroup. +type AdminConversationsRestrictAccessAddGroupOption func(*adminConversationsRestrictAccessAddGroupParams) + +// AdminConversationsRestrictAccessAddGroupOptionTeamID sets the workspace where the channel exists. +// Required if using an org token. +func AdminConversationsRestrictAccessAddGroupOptionTeamID(teamID string) AdminConversationsRestrictAccessAddGroupOption { + return func(params *adminConversationsRestrictAccessAddGroupParams) { + params.teamID = teamID + } +} + +// AdminConversationsRestrictAccessAddGroup adds an allowlist of IDP groups +// for accessing a channel. +// For more information see the admin.conversations.restrictAccess.addGroup docs: +// https://api.slack.com/methods/admin.conversations.restrictAccess.addGroup +func (api *Client) AdminConversationsRestrictAccessAddGroup(ctx context.Context, channelID, groupID string, options ...AdminConversationsRestrictAccessAddGroupOption) error { + params := adminConversationsRestrictAccessAddGroupParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "group_id": {groupID}, + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.restrictAccess.addGroup", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// AdminConversationsRestrictAccessListGroups + +type adminConversationsRestrictAccessListGroupsParams struct { + teamID string +} + +// AdminConversationsRestrictAccessListGroupsOption is an option for AdminConversationsRestrictAccessListGroups. +type AdminConversationsRestrictAccessListGroupsOption func(*adminConversationsRestrictAccessListGroupsParams) + +// AdminConversationsRestrictAccessListGroupsOptionTeamID sets the workspace where the channel exists. +// Required if using an org token. +func AdminConversationsRestrictAccessListGroupsOptionTeamID(teamID string) AdminConversationsRestrictAccessListGroupsOption { + return func(params *adminConversationsRestrictAccessListGroupsParams) { + params.teamID = teamID + } +} + +// AdminConversationsRestrictAccessListGroupsResponse represents the response from +// admin.conversations.restrictAccess.listGroups. +type AdminConversationsRestrictAccessListGroupsResponse struct { + SlackResponse + GroupIDs []string `json:"group_ids"` +} + +// AdminConversationsRestrictAccessListGroups lists the allowlist of IDP groups +// for a private channel. +// For more information see the admin.conversations.restrictAccess.listGroups docs: +// https://api.slack.com/methods/admin.conversations.restrictAccess.listGroups +func (api *Client) AdminConversationsRestrictAccessListGroups(ctx context.Context, channelID string, options ...AdminConversationsRestrictAccessListGroupsOption) ([]string, error) { + params := adminConversationsRestrictAccessListGroupsParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &AdminConversationsRestrictAccessListGroupsResponse{} + err := api.postMethod(ctx, "admin.conversations.restrictAccess.listGroups", values, response) + if err != nil { + return nil, err + } + + return response.GroupIDs, response.Err() +} + +// AdminConversationsRestrictAccessRemoveGroup + +type adminConversationsRestrictAccessRemoveGroupParams struct { + teamID string +} + +// AdminConversationsRestrictAccessRemoveGroupOption is an option for AdminConversationsRestrictAccessRemoveGroup. +type AdminConversationsRestrictAccessRemoveGroupOption func(*adminConversationsRestrictAccessRemoveGroupParams) + +// AdminConversationsRestrictAccessRemoveGroupOptionTeamID sets the workspace where the channel exists. +// Required if using an org token. +func AdminConversationsRestrictAccessRemoveGroupOptionTeamID(teamID string) AdminConversationsRestrictAccessRemoveGroupOption { + return func(params *adminConversationsRestrictAccessRemoveGroupParams) { + params.teamID = teamID + } +} + +// AdminConversationsRestrictAccessRemoveGroup removes an IDP group from the +// allowlist of a private channel. +// For more information see the admin.conversations.restrictAccess.removeGroup docs: +// https://api.slack.com/methods/admin.conversations.restrictAccess.removeGroup +func (api *Client) AdminConversationsRestrictAccessRemoveGroup(ctx context.Context, channelID, groupID string, options ...AdminConversationsRestrictAccessRemoveGroupOption) error { + params := adminConversationsRestrictAccessRemoveGroupParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channelID}, + "group_id": {groupID}, + } + + if params.teamID != "" { + values.Add("team_id", params.teamID) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.restrictAccess.removeGroup", values, response) + if err != nil { + return err + } + + return response.Err() +} diff --git a/vendor/github.com/slack-go/slack/admin_roles.go b/vendor/github.com/slack-go/slack/admin_roles.go new file mode 100644 index 0000000000..676acf0bd4 --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_roles.go @@ -0,0 +1,204 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +// AdminRolesAddAssignmentsParams contains arguments for AdminRolesAddAssignments method call. +type AdminRolesAddAssignmentsParams struct { + RoleID string + EntityIDs []string + UserIDs []string +} + +// AdminRolesRejectedUser represents a user that could not be assigned a role. +type AdminRolesRejectedUser struct { + ID string `json:"id"` + Error string `json:"error"` +} + +// AdminRolesRejectedEntity represents an entity that could not be assigned a role. +type AdminRolesRejectedEntity struct { + ID string `json:"id"` + Error string `json:"error"` +} + +// AdminRolesAddAssignmentsResponse represents the response from admin.roles.addAssignments. +type AdminRolesAddAssignmentsResponse struct { + SlackResponse + RejectedUsers []AdminRolesRejectedUser `json:"rejected_users"` + RejectedEntities []AdminRolesRejectedEntity `json:"rejected_entities"` +} + +// AdminRolesAddAssignments adds members to a specified role. +// For more information see the admin.roles.addAssignments docs: +// https://api.slack.com/methods/admin.roles.addAssignments +func (api *Client) AdminRolesAddAssignments(ctx context.Context, params AdminRolesAddAssignmentsParams) (*AdminRolesAddAssignmentsResponse, error) { + values := url.Values{ + "token": {api.token}, + "role_id": {params.RoleID}, + } + + if len(params.EntityIDs) > 0 { + values.Add("entity_ids", strings.Join(params.EntityIDs, ",")) + } + + if len(params.UserIDs) > 0 { + values.Add("user_ids", strings.Join(params.UserIDs, ",")) + } + + response := &AdminRolesAddAssignmentsResponse{} + err := api.postMethod(ctx, "admin.roles.addAssignments", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +type adminRolesListAssignmentsParams struct { + roleIDs []string + entityIDs []string + limit int + cursor string + sortDirection string +} + +// AdminRolesListAssignmentsOption is an option for AdminRolesListAssignments. +type AdminRolesListAssignmentsOption func(*adminRolesListAssignmentsParams) + +// AdminRolesListAssignmentsOptionRoleIDs filters results to the specified role IDs. +func AdminRolesListAssignmentsOptionRoleIDs(roleIDs []string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.roleIDs = roleIDs + } +} + +// AdminRolesListAssignmentsOptionEntityIDs filters results to the specified entity IDs. +func AdminRolesListAssignmentsOptionEntityIDs(entityIDs []string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.entityIDs = entityIDs + } +} + +// AdminRolesListAssignmentsOptionLimit sets the maximum number of results to return. +func AdminRolesListAssignmentsOptionLimit(limit int) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.limit = limit + } +} + +// AdminRolesListAssignmentsOptionCursor sets the cursor for pagination. +func AdminRolesListAssignmentsOptionCursor(cursor string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.cursor = cursor + } +} + +// AdminRolesListAssignmentsOptionSortDir sets the sort direction. +// Valid values: "asc", "desc". +func AdminRolesListAssignmentsOptionSortDir(sortDir string) AdminRolesListAssignmentsOption { + return func(params *adminRolesListAssignmentsParams) { + params.sortDirection = sortDir + } +} + +// RoleAssignment represents a single role assignment. +type RoleAssignment struct { + RoleID string `json:"role_id"` + EntityID string `json:"entity_id,omitempty"` + UserID string `json:"user_id,omitempty"` + DateCreate int64 `json:"date_create,omitempty"` +} + +// AdminRolesListAssignmentsResponse represents the response from admin.roles.listAssignments. +type AdminRolesListAssignmentsResponse struct { + SlackResponse + RoleAssignments []RoleAssignment `json:"role_assignments"` + ResponseMetadata ResponseMetadata `json:"response_metadata"` +} + +// AdminRolesListAssignments lists assignments for roles. +// For more information see the admin.roles.listAssignments docs: +// https://api.slack.com/methods/admin.roles.listAssignments +func (api *Client) AdminRolesListAssignments(ctx context.Context, options ...AdminRolesListAssignmentsOption) (*AdminRolesListAssignmentsResponse, error) { + params := adminRolesListAssignmentsParams{} + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + } + + if len(params.roleIDs) > 0 { + values.Add("role_ids", strings.Join(params.roleIDs, ",")) + } + + if len(params.entityIDs) > 0 { + values.Add("entity_ids", strings.Join(params.entityIDs, ",")) + } + + if params.limit > 0 { + values.Add("limit", strconv.Itoa(params.limit)) + } + + if params.cursor != "" { + values.Add("cursor", params.cursor) + } + + if params.sortDirection != "" { + values.Add("sort_dir", params.sortDirection) + } + + response := &AdminRolesListAssignmentsResponse{} + err := api.postMethod(ctx, "admin.roles.listAssignments", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// AdminRolesRemoveAssignmentsParams contains arguments for AdminRolesRemoveAssignments method call. +type AdminRolesRemoveAssignmentsParams struct { + RoleID string + EntityIDs []string + UserIDs []string +} + +// AdminRolesRemoveAssignmentsResponse represents the response from admin.roles.removeAssignments. +type AdminRolesRemoveAssignmentsResponse struct { + SlackResponse + RejectedUsers []AdminRolesRejectedUser `json:"rejected_users"` + RejectedEntities []AdminRolesRejectedEntity `json:"rejected_entities"` +} + +// AdminRolesRemoveAssignments removes members from a specified role. +// For more information see the admin.roles.removeAssignments docs: +// https://api.slack.com/methods/admin.roles.removeAssignments +func (api *Client) AdminRolesRemoveAssignments(ctx context.Context, params AdminRolesRemoveAssignmentsParams) (*AdminRolesRemoveAssignmentsResponse, error) { + values := url.Values{ + "token": {api.token}, + "role_id": {params.RoleID}, + } + + if len(params.EntityIDs) > 0 { + values.Add("entity_ids", strings.Join(params.EntityIDs, ",")) + } + + if len(params.UserIDs) > 0 { + values.Add("user_ids", strings.Join(params.UserIDs, ",")) + } + + response := &AdminRolesRemoveAssignmentsResponse{} + err := api.postMethod(ctx, "admin.roles.removeAssignments", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/admin_teams.go b/vendor/github.com/slack-go/slack/admin_teams.go new file mode 100644 index 0000000000..17ae3df333 --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_teams.go @@ -0,0 +1,153 @@ +package slack + +import ( + "context" + "net/url" + "strings" +) + +// AdminTeamSettings contains workspace settings returned by admin.teams.settings.info. +type AdminTeamSettings struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Domain string `json:"domain"` + EmailDomain string `json:"email_domain"` + AvatarBaseURL string `json:"avatar_base_url"` + IsVerified bool `json:"is_verified"` + Icon TeamSettingsIcon `json:"icon"` + EnterpriseID string `json:"enterprise_id"` + EnterpriseName string `json:"enterprise_name"` + EnterpriseDomain string `json:"enterprise_domain"` + DefaultChannels []string `json:"default_channels"` +} + +// TeamSettingsIcon contains team icon URLs and a default flag. +type TeamSettingsIcon struct { + ImageDefault bool `json:"image_default"` + Image34 string `json:"image_34"` + Image44 string `json:"image_44"` + Image68 string `json:"image_68"` + Image88 string `json:"image_88"` + Image102 string `json:"image_102"` + Image132 string `json:"image_132"` + Image230 string `json:"image_230"` +} + +// TeamDiscoverability represents the discoverability setting for a workspace. +type TeamDiscoverability string + +const ( + TeamDiscoverabilityOpen TeamDiscoverability = "open" + TeamDiscoverabilityInviteOnly TeamDiscoverability = "invite_only" + TeamDiscoverabilityClosed TeamDiscoverability = "closed" + TeamDiscoverabilityUnlisted TeamDiscoverability = "unlisted" +) + +type adminTeamSettingsInfoResponse struct { + Team AdminTeamSettings `json:"team"` + SlackResponse +} + +// AdminTeamsSettingsInfo returns workspace settings. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.info +func (api *Client) AdminTeamsSettingsInfo(ctx context.Context, teamID string) (*AdminTeamSettings, error) { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + } + + response := &adminTeamSettingsInfoResponse{} + err := api.postMethod(ctx, "admin.teams.settings.info", values, response) + if err != nil { + return nil, err + } + + return &response.Team, response.Err() +} + +// AdminTeamsSettingsSetDefaultChannels sets the default channels for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels +func (api *Client) AdminTeamsSettingsSetDefaultChannels(ctx context.Context, teamID string, channelIDs ...string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "channel_ids": {strings.Join(channelIDs, ",")}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setDefaultChannels", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetDescription sets the description for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription +func (api *Client) AdminTeamsSettingsSetDescription(ctx context.Context, teamID, description string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "description": {description}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setDescription", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetDiscoverability sets the discoverability for a workspace. +// The discoverability parameter must be one of: open, invite_only, closed, or unlisted. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability +func (api *Client) AdminTeamsSettingsSetDiscoverability(ctx context.Context, teamID string, discoverability TeamDiscoverability) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "discoverability": {string(discoverability)}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setDiscoverability", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetIcon sets the icon for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon +func (api *Client) AdminTeamsSettingsSetIcon(ctx context.Context, teamID, imageURL string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "image_url": {imageURL}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setIcon", values, response) + if err != nil { + return err + } + return response.Err() +} + +// AdminTeamsSettingsSetName sets the name for a workspace. +// Slack API docs: https://docs.slack.dev/reference/methods/admin.teams.settings.setName +func (api *Client) AdminTeamsSettingsSetName(ctx context.Context, teamID, name string) error { + values := url.Values{ + "token": {api.token}, + "team_id": {teamID}, + "name": {name}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.teams.settings.setName", values, response) + if err != nil { + return err + } + return response.Err() +} diff --git a/vendor/github.com/slack-go/slack/apps.go b/vendor/github.com/slack-go/slack/apps.go index 7322b15b9e..c75569fb68 100644 --- a/vendor/github.com/slack-go/slack/apps.go +++ b/vendor/github.com/slack-go/slack/apps.go @@ -35,7 +35,7 @@ func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventCont "event_context": eventContext, }) - err := postJSON(ctx, api.httpclient, api.endpoint+"apps.event.authorizations.list", api.appLevelToken, request, &resp, api) + err := api.postJSONMethod(ctx, "apps.event.authorizations.list", api.appLevelToken, request, &resp) if err != nil { return nil, err diff --git a/vendor/github.com/slack-go/slack/assistant.go b/vendor/github.com/slack-go/slack/assistant.go index 8432f89b3d..fb82cb64aa 100644 --- a/vendor/github.com/slack-go/slack/assistant.go +++ b/vendor/github.com/slack-go/slack/assistant.go @@ -4,13 +4,19 @@ import ( "context" "encoding/json" "net/url" + "strconv" + "strings" ) // AssistantThreadSetStatusParameters are the parameters for AssistantThreadSetStatus type AssistantThreadsSetStatusParameters struct { - ChannelID string `json:"channel_id"` - Status string `json:"status"` - ThreadTS string `json:"thread_ts"` + ChannelID string `json:"channel_id"` + Status string `json:"status"` + ThreadTS string `json:"thread_ts"` + LoadingMessages []string `json:"loading_messages,omitempty"` + Username string `json:"username,omitempty"` + IconURL string `json:"icon_url,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` } // AssistantThreadSetTitleParameters are the parameters for AssistantThreadSetTitle @@ -34,6 +40,95 @@ type AssistantThreadsPrompt struct { Message string `json:"message"` } +// AssistantSearchContextParameters are the parameters for AssistantSearchContext +type AssistantSearchContextParameters struct { + Query string `json:"query"` + ActionToken string `json:"action_token,omitempty"` + ChannelTypes []string `json:"channel_types,omitempty"` + ContentTypes []string `json:"content_types,omitempty"` + ContextChannelID string `json:"context_channel_id,omitempty"` + Cursor string `json:"cursor,omitempty"` + IncludeBots bool `json:"include_bots,omitempty"` + Limit int `json:"limit,omitempty"` + IncludeDeletedUsers bool `json:"include_deleted_users,omitempty"` + Before int64 `json:"before,omitempty"` + After int64 `json:"after,omitempty"` + IncludeContextMessages bool `json:"include_context_messages,omitempty"` + Sort string `json:"sort,omitempty"` + SortDir string `json:"sort_dir,omitempty"` + IncludeMessageBlocks bool `json:"include_message_blocks,omitempty"` + Highlight bool `json:"highlight,omitempty"` + TermClauses []string `json:"term_clauses,omitempty"` + Modifiers string `json:"modifiers,omitempty"` + IncludeArchivedChannels bool `json:"include_archived_channels,omitempty"` + DisableSemanticSearch bool `json:"disable_semantic_search,omitempty"` +} + +// AssistantSearchContextMessage represents a search result message +type AssistantSearchContextMessage struct { + AuthorUserID string `json:"author_user_id"` + AuthorName string `json:"author_name,omitempty"` + TeamID string `json:"team_id"` + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name,omitempty"` + MessageTS string `json:"message_ts"` + Content string `json:"content"` + IsAuthorBot bool `json:"is_author_bot"` + Permalink string `json:"permalink"` + Blocks Blocks `json:"blocks,omitempty"` + ContextMessages *AssistantSearchContextMessageContext `json:"context_messages,omitempty"` +} + +// AssistantSearchContextMessageContext contains context messages surrounding a search result +type AssistantSearchContextMessageContext struct { + Before []AssistantSearchContextMessage `json:"before"` + After []AssistantSearchContextMessage `json:"after"` +} + +// AssistantSearchContextFile represents a search result file +type AssistantSearchContextFile struct { + UploaderUserID string `json:"uploader_user_id"` + AuthorUserID string `json:"author_user_id"` + AuthorName string `json:"author_name"` + TeamID string `json:"team_id"` + FileID string `json:"file_id"` + DateCreated int64 `json:"date_created"` + DateUpdated int64 `json:"date_updated"` + Title string `json:"title"` + FileType string `json:"file_type"` + Permalink string `json:"permalink"` + Content string `json:"content"` +} + +// AssistantSearchContextChannel represents a search result channel +type AssistantSearchContextChannel struct { + TeamID string `json:"team_id"` + CreatorUserID string `json:"creator_user_id"` + CreatorName string `json:"creator_name"` + DateCreated int64 `json:"date_created"` + DateUpdated int64 `json:"date_updated"` + Name string `json:"name"` + Topic string `json:"topic"` + Purpose string `json:"purpose"` + Permalink string `json:"permalink"` +} + +// AssistantSearchContextResults contains the search results +type AssistantSearchContextResults struct { + Messages []AssistantSearchContextMessage `json:"messages,omitempty"` + Files []AssistantSearchContextFile `json:"files,omitempty"` + Channels []AssistantSearchContextChannel `json:"channels,omitempty"` +} + +// AssistantSearchContextResponse is the response from assistant.search.context +type AssistantSearchContextResponse struct { + SlackResponse + Results AssistantSearchContextResults `json:"results"` + ResponseMetadata struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` +} + // AssistantThreadSetSuggestedPrompts sets the suggested prompts for a thread func (p *AssistantThreadsSetSuggestedPromptsParameters) AddPrompt(title, message string) { p.Prompts = append(p.Prompts, AssistantThreadsPrompt{ @@ -62,6 +157,10 @@ func (api *Client) SetAssistantThreadsSuggestedPromptsContext(ctx context.Contex values.Add("channel_id", params.ChannelID) + if params.Title != "" { + values.Add("title", params.Title) + } + // Send Prompts as JSON prompts, err := json.Marshal(params.Prompts) if err != nil { @@ -82,13 +181,17 @@ func (api *Client) SetAssistantThreadsSuggestedPromptsContext(ctx context.Contex return response.Err() } -// SetAssistantThreadStatus sets the status of a thread +// SetAssistantThreadsStatus sets the status of a thread. +// This method accepts either the chat:write or assistant:write scope. +// Note: the assistant:write scope is being deprecated in favor of chat:write. // @see https://api.slack.com/methods/assistant.threads.setStatus func (api *Client) SetAssistantThreadsStatus(params AssistantThreadsSetStatusParameters) (err error) { return api.SetAssistantThreadsStatusContext(context.Background(), params) } -// SetAssistantThreadStatusContext sets the status of a thread with a custom context +// SetAssistantThreadsStatusContext sets the status of a thread with a custom context. +// This method accepts either the chat:write or assistant:write scope. +// Note: the assistant:write scope is being deprecated in favor of chat:write. // @see https://api.slack.com/methods/assistant.threads.setStatus func (api *Client) SetAssistantThreadsStatusContext(ctx context.Context, params AssistantThreadsSetStatusParameters) (err error) { @@ -105,6 +208,22 @@ func (api *Client) SetAssistantThreadsStatusContext(ctx context.Context, params // Always send the status parameter, if empty, it will clear any existing status values.Add("status", params.Status) + if len(params.LoadingMessages) > 0 { + values.Add("loading_messages", strings.Join(params.LoadingMessages, ",")) + } + + if params.Username != "" { + values.Add("username", params.Username) + } + + if params.IconURL != "" { + values.Add("icon_url", params.IconURL) + } + + if params.IconEmoji != "" { + values.Add("icon_emoji", params.IconEmoji) + } + response := struct { SlackResponse }{} @@ -155,3 +274,104 @@ func (api *Client) SetAssistantThreadsTitleContext(ctx context.Context, params A return response.Err() } + +// SearchAssistantContext searches messages across the Slack organization +// @see https://api.slack.com/methods/assistant.search.context +func (api *Client) SearchAssistantContext(params AssistantSearchContextParameters) (*AssistantSearchContextResponse, error) { + return api.SearchAssistantContextContext(context.Background(), params) +} + +// SearchAssistantContextContext searches messages across the Slack organization with a custom context +// @see https://api.slack.com/methods/assistant.search.context +func (api *Client) SearchAssistantContextContext(ctx context.Context, params AssistantSearchContextParameters) (*AssistantSearchContextResponse, error) { + values := url.Values{ + "token": {api.token}, + } + + values.Add("query", params.Query) + + if params.ActionToken != "" { + values.Add("action_token", params.ActionToken) + } + + if len(params.ChannelTypes) > 0 { + values.Add("channel_types", strings.Join(params.ChannelTypes, ",")) + } + + if len(params.ContentTypes) > 0 { + values.Add("content_types", strings.Join(params.ContentTypes, ",")) + } + + if params.ContextChannelID != "" { + values.Add("context_channel_id", params.ContextChannelID) + } + + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + if params.IncludeBots { + values.Add("include_bots", "true") + } + + if params.Limit > 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + + if params.IncludeDeletedUsers { + values.Add("include_deleted_users", "true") + } + + if params.Before > 0 { + values.Add("before", strconv.FormatInt(params.Before, 10)) + } + + if params.After > 0 { + values.Add("after", strconv.FormatInt(params.After, 10)) + } + + if params.IncludeContextMessages { + values.Add("include_context_messages", "true") + } + + if params.Sort != "" { + values.Add("sort", params.Sort) + } + + if params.SortDir != "" { + values.Add("sort_dir", params.SortDir) + } + + if params.IncludeMessageBlocks { + values.Add("include_message_blocks", "true") + } + + if params.Highlight { + values.Add("highlight", "true") + } + + if len(params.TermClauses) > 0 { + values.Add("term_clauses", strings.Join(params.TermClauses, ",")) + } + + if params.Modifiers != "" { + values.Add("modifiers", params.Modifiers) + } + + if params.IncludeArchivedChannels { + values.Add("include_archived_channels", "true") + } + + if params.DisableSemanticSearch { + values.Add("disable_semantic_search", "true") + } + + response := &AssistantSearchContextResponse{} + + err := api.postMethod(ctx, "assistant.search.context", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/attachments.go b/vendor/github.com/slack-go/slack/attachments.go index f4eb9b932d..be04e9049e 100644 --- a/vendor/github.com/slack-go/slack/attachments.go +++ b/vendor/github.com/slack-go/slack/attachments.go @@ -77,8 +77,11 @@ type Attachment struct { Pretext string `json:"pretext,omitempty"` Text string `json:"text,omitempty"` - ImageURL string `json:"image_url,omitempty"` - ThumbURL string `json:"thumb_url,omitempty"` + ImageURL string `json:"image_url,omitempty"` + ImageBytes int `json:"image_bytes,omitempty"` + ImageHeight int `json:"image_height,omitempty"` + ImageWidth int `json:"image_width,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` ServiceName string `json:"service_name,omitempty"` ServiceIcon string `json:"service_icon,omitempty"` diff --git a/vendor/github.com/slack-go/slack/audit.go b/vendor/github.com/slack-go/slack/audit.go index a3ea7ebdfe..135a68d785 100644 --- a/vendor/github.com/slack-go/slack/audit.go +++ b/vendor/github.com/slack-go/slack/audit.go @@ -107,7 +107,8 @@ type AuditLogParameters struct { func (api *Client) auditLogsRequest(ctx context.Context, path string, values url.Values) (*AuditLogResponse, error) { response := &AuditLogResponse{} - err := api.getMethod(ctx, path, api.token, values, response) + // The Audit Logs API uses a different base URL (api.slack.com instead of slack.com/api) + _, err := getResource(ctx, api.httpclient, api.auditEndpoint+path, api.token, values, response, api) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/block.go b/vendor/github.com/slack-go/slack/block.go index 7c4f993085..cc5d80f248 100644 --- a/vendor/github.com/slack-go/slack/block.go +++ b/vendor/github.com/slack-go/slack/block.go @@ -5,18 +5,25 @@ package slack type MessageBlockType string const ( - MBTSection MessageBlockType = "section" - MBTDivider MessageBlockType = "divider" - MBTImage MessageBlockType = "image" - MBTAction MessageBlockType = "actions" - MBTContext MessageBlockType = "context" - MBTFile MessageBlockType = "file" - MBTInput MessageBlockType = "input" - MBTHeader MessageBlockType = "header" - MBTRichText MessageBlockType = "rich_text" - MBTCall MessageBlockType = "call" - MBTVideo MessageBlockType = "video" - MBTMarkdown MessageBlockType = "markdown" + MBTSection MessageBlockType = "section" + MBTDivider MessageBlockType = "divider" + MBTImage MessageBlockType = "image" + MBTAction MessageBlockType = "actions" + MBTContext MessageBlockType = "context" + MBTContextActions MessageBlockType = "context_actions" + MBTFile MessageBlockType = "file" + MBTInput MessageBlockType = "input" + MBTHeader MessageBlockType = "header" + MBTRichText MessageBlockType = "rich_text" + MBTCall MessageBlockType = "call" + MBTVideo MessageBlockType = "video" + MBTMarkdown MessageBlockType = "markdown" + MBTTable MessageBlockType = "table" + MBTTaskCard MessageBlockType = "task_card" + MBTPlan MessageBlockType = "plan" + MBTAlert MessageBlockType = "alert" + MBTCard MessageBlockType = "card" + MBTCarousel MessageBlockType = "carousel" ) // Block defines an interface all block types should implement diff --git a/vendor/github.com/slack-go/slack/block_alert.go b/vendor/github.com/slack-go/slack/block_alert.go new file mode 100644 index 0000000000..ddb151b44a --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_alert.go @@ -0,0 +1,70 @@ +package slack + +// AlertLevel defines the severity for an AlertBlock. +type AlertLevel string + +const ( + AlertLevelDefault AlertLevel = "default" + AlertLevelInfo AlertLevel = "info" + AlertLevelWarning AlertLevel = "warning" + AlertLevelError AlertLevel = "error" + AlertLevelSuccess AlertLevel = "success" +) + +// AlertBlock defines a block of type alert used to surface a notification +// message with an optional severity level. +// +// Surface: modal only. Slack rejects alert blocks sent via chat.postMessage +// or the streaming APIs — use OpenView / UpdateView / PushView with a +// ModalViewRequest whose Blocks include the alert. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/alert-block/ +type AlertBlock struct { + Type MessageBlockType `json:"type"` + Text *TextBlockObject `json:"text"` + Level AlertLevel `json:"level,omitempty"` + BlockID string `json:"block_id,omitempty"` +} + +// BlockType returns the type of the block +func (s AlertBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s AlertBlock) ID() string { + return s.BlockID +} + +// AlertBlockOption allows configuration of options for a new alert block +type AlertBlockOption func(*AlertBlock) + +// AlertBlockOptionLevel sets the severity level for the alert block +func AlertBlockOptionLevel(level AlertLevel) AlertBlockOption { + return func(block *AlertBlock) { + block.Level = level + } +} + +// AlertBlockOptionBlockID sets the block ID for the alert block +func AlertBlockOptionBlockID(blockID string) AlertBlockOption { + return func(block *AlertBlock) { + block.BlockID = blockID + } +} + +// NewAlertBlock returns a new instance of an alert block +func NewAlertBlock(text *TextBlockObject, options ...AlertBlockOption) *AlertBlock { + block := AlertBlock{ + Type: MBTAlert, + Text: text, + } + + for _, option := range options { + if option != nil { + option(&block) + } + } + + return &block +} diff --git a/vendor/github.com/slack-go/slack/block_call.go b/vendor/github.com/slack-go/slack/block_call.go index c81dcb8be1..621a8b6449 100644 --- a/vendor/github.com/slack-go/slack/block_call.go +++ b/vendor/github.com/slack-go/slack/block_call.go @@ -7,6 +7,55 @@ type CallBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` CallID string `json:"call_id"` + // Call is populated by Slack when retrieving messages containing a call block. + // When creating a call block to post, only CallID is required. + // Note: The structure differs from the Call type used in API responses. + Call *CallBlockData `json:"call,omitempty"` + APIDecorationAvailable bool `json:"api_decoration_available,omitempty"` +} + +// CallBlockData represents the call data structure as it appears in CallBlocks. +// This differs from the Call type used in API responses - CallBlock data is nested under V1. +type CallBlockData struct { + V1 *CallBlockDataV1 `json:"v1,omitempty"` + MediaBackendType string `json:"media_backend_type,omitempty"` +} + +// CallBlockDataV1 contains the actual call information within a CallBlock. +type CallBlockDataV1 struct { + ID string `json:"id"` + AppID string `json:"app_id,omitempty"` + AppIconURLs *CallBlockIconURLs `json:"app_icon_urls,omitempty"` + DateStart int64 `json:"date_start"` + DateEnd int64 `json:"date_end"` + ActiveParticipants []CallParticipant `json:"active_participants,omitempty"` + AllParticipants []CallParticipant `json:"all_participants,omitempty"` + DisplayID string `json:"display_id,omitempty"` + JoinURL string `json:"join_url,omitempty"` + DesktopAppJoinURL string `json:"desktop_app_join_url,omitempty"` + Name string `json:"name,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + Channels []string `json:"channels,omitempty"` + IsDMCall bool `json:"is_dm_call"` + WasRejected bool `json:"was_rejected"` + WasMissed bool `json:"was_missed"` + WasAccepted bool `json:"was_accepted"` + HasEnded bool `json:"has_ended"` +} + +// CallBlockIconURLs contains app icon URLs at various sizes for a call integration. +type CallBlockIconURLs struct { + Image32 string `json:"image_32,omitempty"` + Image36 string `json:"image_36,omitempty"` + Image48 string `json:"image_48,omitempty"` + Image64 string `json:"image_64,omitempty"` + Image72 string `json:"image_72,omitempty"` + Image96 string `json:"image_96,omitempty"` + Image128 string `json:"image_128,omitempty"` + Image192 string `json:"image_192,omitempty"` + Image512 string `json:"image_512,omitempty"` + Image1024 string `json:"image_1024,omitempty"` + ImageOriginal string `json:"image_original,omitempty"` } // BlockType returns the type of the block @@ -19,10 +68,26 @@ func (s CallBlock) ID() string { return s.BlockID } +// CallBlockOption allows configuration of options for a new call block +type CallBlockOption func(*CallBlock) + +// CallBlockOptionBlockID sets the block_id for the call block +func CallBlockOptionBlockID(blockID string) CallBlockOption { + return func(block *CallBlock) { + block.BlockID = blockID + } +} + // NewCallBlock returns a new instance of a call block -func NewCallBlock(callID string) *CallBlock { - return &CallBlock{ +func NewCallBlock(callID string, options ...CallBlockOption) *CallBlock { + block := &CallBlock{ Type: MBTCall, CallID: callID, } + + for _, option := range options { + option(block) + } + + return block } diff --git a/vendor/github.com/slack-go/slack/block_card.go b/vendor/github.com/slack-go/slack/block_card.go new file mode 100644 index 0000000000..ccee174204 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_card.go @@ -0,0 +1,90 @@ +package slack + +// CardBlock defines a block of type card used to display a rich, self-contained +// piece of content with an optional hero image, icon, title, subtitle, body, +// and action buttons. Cards can stand alone or be grouped inside a +// CarouselBlock. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/card-block/ +type CardBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + HeroImage *ImageBlockElement `json:"hero_image,omitempty"` + Icon *ImageBlockElement `json:"icon,omitempty"` + Title *TextBlockObject `json:"title,omitempty"` + Subtitle *TextBlockObject `json:"subtitle,omitempty"` + Body *TextBlockObject `json:"body,omitempty"` + Actions *BlockElements `json:"actions,omitempty"` +} + +// BlockType returns the type of the block +func (s CardBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s CardBlock) ID() string { + return s.BlockID +} + +// CardBlockOption allows configuration of options for a new card block +type CardBlockOption func(*CardBlock) + +// CardBlockOptionBlockID sets the block ID for the card block +func CardBlockOptionBlockID(blockID string) CardBlockOption { + return func(block *CardBlock) { + block.BlockID = blockID + } +} + +// NewCardBlock returns a new instance of a card block. Use the chainable +// With* methods or provide options to populate its fields. +func NewCardBlock(options ...CardBlockOption) *CardBlock { + block := CardBlock{ + Type: MBTCard, + } + + for _, option := range options { + if option != nil { + option(&block) + } + } + + return &block +} + +// WithTitle sets the title text for the CardBlock +func (s *CardBlock) WithTitle(title *TextBlockObject) *CardBlock { + s.Title = title + return s +} + +// WithSubtitle sets the subtitle text for the CardBlock +func (s *CardBlock) WithSubtitle(subtitle *TextBlockObject) *CardBlock { + s.Subtitle = subtitle + return s +} + +// WithBody sets the body text for the CardBlock +func (s *CardBlock) WithBody(body *TextBlockObject) *CardBlock { + s.Body = body + return s +} + +// WithIcon sets the icon image for the CardBlock +func (s *CardBlock) WithIcon(icon *ImageBlockElement) *CardBlock { + s.Icon = icon + return s +} + +// WithHeroImage sets the hero image for the CardBlock +func (s *CardBlock) WithHeroImage(heroImage *ImageBlockElement) *CardBlock { + s.HeroImage = heroImage + return s +} + +// WithActions sets the action buttons displayed at the bottom of the card +func (s *CardBlock) WithActions(elements ...BlockElement) *CardBlock { + s.Actions = &BlockElements{ElementSet: elements} + return s +} diff --git a/vendor/github.com/slack-go/slack/block_carousel.go b/vendor/github.com/slack-go/slack/block_carousel.go new file mode 100644 index 0000000000..5d407db9c0 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_carousel.go @@ -0,0 +1,42 @@ +package slack + +// CarouselBlock defines a block of type carousel that displays a scrollable +// list of cards. A carousel must contain between 1 and 10 cards. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/carousel-block/ +type CarouselBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements []*CardBlock `json:"elements"` +} + +// BlockType returns the type of the block +func (s CarouselBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s CarouselBlock) ID() string { + return s.BlockID +} + +// NewCarouselBlock returns a new instance of a carousel block containing the +// given cards. +func NewCarouselBlock(cards ...*CardBlock) *CarouselBlock { + return &CarouselBlock{ + Type: MBTCarousel, + Elements: cards, + } +} + +// WithBlockID sets the block ID for the CarouselBlock +func (s *CarouselBlock) WithBlockID(blockID string) *CarouselBlock { + s.BlockID = blockID + return s +} + +// AddCard appends a card to the carousel +func (s *CarouselBlock) AddCard(card *CardBlock) *CarouselBlock { + s.Elements = append(s.Elements, card) + return s +} diff --git a/vendor/github.com/slack-go/slack/block_context_actions.go b/vendor/github.com/slack-go/slack/block_context_actions.go new file mode 100644 index 0000000000..d1cf532c33 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_context_actions.go @@ -0,0 +1,31 @@ +package slack + +// ContextActionsBlock defines data that is used to hold interactive action elements. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/context-actions-block/ +type ContextActionsBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements *BlockElements `json:"elements"` +} + +// BlockType returns the type of the block +func (s ContextActionsBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s ContextActionsBlock) ID() string { + return s.BlockID +} + +// NewContextActionsBlock returns a new instance of a Context Actions Block +func NewContextActionsBlock(blockID string, elements ...BlockElement) *ContextActionsBlock { + return &ContextActionsBlock{ + Type: MBTContextActions, + BlockID: blockID, + Elements: &BlockElements{ + ElementSet: elements, + }, + } +} diff --git a/vendor/github.com/slack-go/slack/block_conv.go b/vendor/github.com/slack-go/slack/block_conv.go index 7df9b107b2..b83ad83128 100644 --- a/vendor/github.com/slack-go/slack/block_conv.go +++ b/vendor/github.com/slack-go/slack/block_conv.go @@ -53,6 +53,8 @@ func (b *Blocks) UnmarshalJSON(data []byte) error { block = &ActionBlock{} case "context": block = &ContextBlock{} + case "context_actions": + block = &ContextActionsBlock{} case "divider": block = &DividerBlock{} case "file": @@ -75,8 +77,25 @@ func (b *Blocks) UnmarshalJSON(data []byte) error { block = &CallBlock{} case "video": block = &VideoBlock{} + case "table": + block = &TableBlock{} + case "task_card": + block = &TaskCardBlock{} + case "alert": + block = &AlertBlock{} + case "plan": + block = &PlanBlock{} + case "card": + block = &CardBlock{} + case "carousel": + block = &CarouselBlock{} default: - block = &UnknownBlock{} + b := &UnknownBlock{raw: r} + if err = json.Unmarshal(r, b); err != nil { + return err + } + blocks.BlockSet = append(blocks.BlockSet, b) + continue } err = json.Unmarshal(r, block) @@ -141,6 +160,12 @@ func (b *InputBlock) UnmarshalJSON(data []byte) error { e = &NumberInputBlockElement{} case "file_input": e = &FileInputBlockElement{} + case "feedback_buttons": + e = &FeedbackButtonsBlockElement{} + case "icon_button": + e = &IconButtonBlockElement{} + case "workflow_button": + e = &WorkflowButtonBlockElement{} default: return fmt.Errorf("unsupported block element type %v", s.TypeVal) } @@ -219,8 +244,18 @@ func (b *BlockElements) UnmarshalJSON(data []byte) error { blockElement = &RadioButtonsBlockElement{} case "static_select", "external_select", "users_select", "conversations_select", "channels_select": blockElement = &SelectBlockElement{} + case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": + blockElement = &MultiSelectBlockElement{} case "number_input": blockElement = &NumberInputBlockElement{} + case "file_input": + blockElement = &FileInputBlockElement{} + case "feedback_buttons": + blockElement = &FeedbackButtonsBlockElement{} + case "icon_button": + blockElement = &IconButtonBlockElement{} + case "workflow_button": + blockElement = &WorkflowButtonBlockElement{} default: return fmt.Errorf("unsupported block element type %v", blockElementType) } @@ -341,6 +376,12 @@ func (a *Accessory) UnmarshalJSON(data []byte) error { return err } a.CheckboxGroupsBlockElement = element.(*CheckboxGroupsBlockElement) + case "workflow_button": + element, err := unmarshalBlockElement(r, &WorkflowButtonBlockElement{}) + if err != nil { + return err + } + a.WorkflowButtonElement = element.(*WorkflowButtonBlockElement) default: element, err := unmarshalBlockElement(r, &UnknownBlockElement{}) if err != nil { @@ -391,6 +432,12 @@ func toBlockElement(element *Accessory) BlockElement { if element.MultiSelectElement != nil { return element.MultiSelectElement } + if element.RichTextInputElement != nil { + return element.RichTextInputElement + } + if element.WorkflowButtonElement != nil { + return element.WorkflowButtonElement + } return nil } diff --git a/vendor/github.com/slack-go/slack/block_element.go b/vendor/github.com/slack-go/slack/block_element.go index 2b32d331e0..1284934070 100644 --- a/vendor/github.com/slack-go/slack/block_element.go +++ b/vendor/github.com/slack-go/slack/block_element.go @@ -3,20 +3,23 @@ package slack // https://api.slack.com/reference/messaging/block-elements const ( - METCheckboxGroups MessageElementType = "checkboxes" - METImage MessageElementType = "image" - METButton MessageElementType = "button" - METOverflow MessageElementType = "overflow" - METDatepicker MessageElementType = "datepicker" - METTimepicker MessageElementType = "timepicker" - METDatetimepicker MessageElementType = "datetimepicker" - METPlainTextInput MessageElementType = "plain_text_input" - METRadioButtons MessageElementType = "radio_buttons" - METRichTextInput MessageElementType = "rich_text_input" - METEmailTextInput MessageElementType = "email_text_input" - METURLTextInput MessageElementType = "url_text_input" - METNumber MessageElementType = "number_input" - METFileInput MessageElementType = "file_input" + METCheckboxGroups MessageElementType = "checkboxes" + METImage MessageElementType = "image" + METButton MessageElementType = "button" + METOverflow MessageElementType = "overflow" + METDatepicker MessageElementType = "datepicker" + METTimepicker MessageElementType = "timepicker" + METDatetimepicker MessageElementType = "datetimepicker" + METPlainTextInput MessageElementType = "plain_text_input" + METRadioButtons MessageElementType = "radio_buttons" + METRichTextInput MessageElementType = "rich_text_input" + METEmailTextInput MessageElementType = "email_text_input" + METURLTextInput MessageElementType = "url_text_input" + METNumber MessageElementType = "number_input" + METFileInput MessageElementType = "file_input" + METFeedbackButtons MessageElementType = "feedback_buttons" + METIconButton MessageElementType = "icon_button" + METWorkflowButton MessageElementType = "workflow_button" MixedElementImage MixedElementType = "mixed_image" MixedElementText MixedElementType = "mixed_text" @@ -58,34 +61,37 @@ type Accessory struct { SelectElement *SelectBlockElement MultiSelectElement *MultiSelectBlockElement CheckboxGroupsBlockElement *CheckboxGroupsBlockElement + WorkflowButtonElement *WorkflowButtonBlockElement UnknownElement *UnknownBlockElement } // NewAccessory returns a new Accessory for a given block element func NewAccessory(element BlockElement) *Accessory { - switch element.(type) { + switch element := element.(type) { case *ImageBlockElement: - return &Accessory{ImageElement: element.(*ImageBlockElement)} + return &Accessory{ImageElement: element} case *ButtonBlockElement: - return &Accessory{ButtonElement: element.(*ButtonBlockElement)} + return &Accessory{ButtonElement: element} case *OverflowBlockElement: - return &Accessory{OverflowElement: element.(*OverflowBlockElement)} + return &Accessory{OverflowElement: element} case *DatePickerBlockElement: - return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)} + return &Accessory{DatePickerElement: element} case *TimePickerBlockElement: - return &Accessory{TimePickerElement: element.(*TimePickerBlockElement)} + return &Accessory{TimePickerElement: element} case *PlainTextInputBlockElement: - return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)} + return &Accessory{PlainTextInputElement: element} case *RichTextInputBlockElement: - return &Accessory{RichTextInputElement: element.(*RichTextInputBlockElement)} + return &Accessory{RichTextInputElement: element} case *RadioButtonsBlockElement: - return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)} + return &Accessory{RadioButtonsElement: element} case *SelectBlockElement: - return &Accessory{SelectElement: element.(*SelectBlockElement)} + return &Accessory{SelectElement: element} case *MultiSelectBlockElement: - return &Accessory{MultiSelectElement: element.(*MultiSelectBlockElement)} + return &Accessory{MultiSelectElement: element} case *CheckboxGroupsBlockElement: - return &Accessory{CheckboxGroupsBlockElement: element.(*CheckboxGroupsBlockElement)} + return &Accessory{CheckboxGroupsBlockElement: element} + case *WorkflowButtonBlockElement: + return &Accessory{WorkflowButtonElement: element} default: return &Accessory{UnknownElement: element.(*UnknownBlockElement)} } @@ -118,7 +124,7 @@ func (s UnknownBlockElement) ElementType() MessageElementType { // More Information: https://api.slack.com/reference/messaging/block-elements#image type ImageBlockElement struct { Type MessageElementType `json:"type"` - ImageURL string `json:"image_url"` + ImageURL *string `json:"image_url,omitempty"` AltText string `json:"alt_text"` SlackFile *SlackFileObject `json:"slack_file,omitempty"` } @@ -136,7 +142,7 @@ func (s ImageBlockElement) MixedElementType() MixedElementType { func NewImageBlockElement(imageURL, altText string) *ImageBlockElement { return &ImageBlockElement{ Type: METImage, - ImageURL: imageURL, + ImageURL: &imageURL, AltText: altText, } } @@ -242,6 +248,7 @@ type SelectBlockElement struct { Filter *SelectBlockElementFilter `json:"filter,omitempty"` MinQueryLength *int `json:"min_query_length,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // SelectBlockElementFilter allows to filter select element conversation options by type. @@ -333,6 +340,7 @@ type MultiSelectBlockElement struct { MinQueryLength *int `json:"min_query_length,omitempty"` MaxSelectedItems *int `json:"max_selected_items,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -453,6 +461,7 @@ type DatePickerBlockElement struct { Placeholder *TextBlockObject `json:"placeholder,omitempty"` InitialDate string `json:"initial_date,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -480,6 +489,7 @@ type TimePickerBlockElement struct { InitialTime string `json:"initial_time,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` Timezone string `json:"timezone,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -591,6 +601,7 @@ type PlainTextInputBlockElement struct { MinLength int `json:"min_length,omitempty"` MaxLength int `json:"max_length,omitempty"` DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } type DispatchActionConfig struct { @@ -678,6 +689,7 @@ type CheckboxGroupsBlockElement struct { Options []*OptionBlockObject `json:"options"` InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -704,6 +716,7 @@ type RadioButtonsBlockElement struct { Options []*OptionBlockObject `json:"options"` InitialOption *OptionBlockObject `json:"initial_option,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -734,6 +747,7 @@ type NumberInputBlockElement struct { MinValue string `json:"min_value,omitempty"` MaxValue string `json:"max_value,omitempty"` DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` } // ElementType returns the type of the Element @@ -811,3 +825,170 @@ func (s *FileInputBlockElement) WithMaxFiles(maxFiles int) *FileInputBlockElemen s.MaxFiles = maxFiles return s } + +// FeedbackButton defines a button within a feedback buttons element +type FeedbackButton struct { + Text *TextBlockObject `json:"text"` + Value string `json:"value"` + AccessibilityLabel string `json:"accessibility_label,omitempty"` +} + +// NewFeedbackButton returns a new instance of a feedback button +func NewFeedbackButton(text *TextBlockObject, value string) *FeedbackButton { + return &FeedbackButton{ + Text: text, + Value: value, + } +} + +// WithAccessibilityLabel sets the accessibility label for the feedback button +func (fb *FeedbackButton) WithAccessibilityLabel(label string) *FeedbackButton { + fb.AccessibilityLabel = label + return fb +} + +// FeedbackButtonsBlockElement defines an element that provides positive/negative feedback options +// +// More Information: https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element +type FeedbackButtonsBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + PositiveButton *FeedbackButton `json:"positive_button"` + NegativeButton *FeedbackButton `json:"negative_button"` +} + +// ElementType returns the type of the element +func (s FeedbackButtonsBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewFeedbackButtonsBlockElement returns a new instance of a feedback buttons element +func NewFeedbackButtonsBlockElement(actionID string, positiveButton, negativeButton *FeedbackButton) *FeedbackButtonsBlockElement { + return &FeedbackButtonsBlockElement{ + Type: METFeedbackButtons, + ActionID: actionID, + PositiveButton: positiveButton, + NegativeButton: negativeButton, + } +} + +// WithPositiveButton sets the positive button for the feedback buttons element +func (s *FeedbackButtonsBlockElement) WithPositiveButton(button *FeedbackButton) *FeedbackButtonsBlockElement { + s.PositiveButton = button + return s +} + +// WithNegativeButton sets the negative button for the feedback buttons element +func (s *FeedbackButtonsBlockElement) WithNegativeButton(button *FeedbackButton) *FeedbackButtonsBlockElement { + s.NegativeButton = button + return s +} + +// IconButtonBlockElement defines an element that displays icon-based interactive buttons +// +// More Information: https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element +type IconButtonBlockElement struct { + Type MessageElementType `json:"type"` + Icon string `json:"icon"` + Text *TextBlockObject `json:"text"` + ActionID string `json:"action_id,omitempty"` + Value string `json:"value,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + AccessibilityLabel string `json:"accessibility_label,omitempty"` + VisibleToUserIDs []string `json:"visible_to_user_ids,omitempty"` +} + +// ElementType returns the type of the element +func (s IconButtonBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewIconButtonBlockElement returns a new instance of an icon button element +func NewIconButtonBlockElement(icon string, text *TextBlockObject, actionID string) *IconButtonBlockElement { + return &IconButtonBlockElement{ + Type: METIconButton, + Icon: icon, + Text: text, + ActionID: actionID, + } +} + +// WithValue sets the value for the icon button element +func (s *IconButtonBlockElement) WithValue(value string) *IconButtonBlockElement { + s.Value = value + return s +} + +// WithConfirm sets the confirmation dialog for the icon button element +func (s *IconButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *IconButtonBlockElement { + s.Confirm = confirm + return s +} + +// WithAccessibilityLabel sets the accessibility label for the icon button element +func (s *IconButtonBlockElement) WithAccessibilityLabel(label string) *IconButtonBlockElement { + s.AccessibilityLabel = label + return s +} + +// WithVisibleToUserIDs sets the user IDs who can see the icon button element +func (s *IconButtonBlockElement) WithVisibleToUserIDs(userIDs []string) *IconButtonBlockElement { + s.VisibleToUserIDs = userIDs + return s +} + +// WorkflowTrigger defines the workflow to be executed when a workflow button is clicked +type WorkflowTrigger struct { + URL string `json:"url"` + CustomizableInputParameters []CustomizableInputParameter `json:"customizable_input_parameters,omitempty"` +} + +// CustomizableInputParameter defines a parameter that can be passed to a workflow +type CustomizableInputParameter struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Workflow contains the trigger details for a workflow button +type Workflow struct { + Trigger *WorkflowTrigger `json:"trigger"` +} + +// WorkflowButtonBlockElement defines an element that triggers a workflow when clicked +// +// More Information: https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element +type WorkflowButtonBlockElement struct { + Type MessageElementType `json:"type"` + Text *TextBlockObject `json:"text"` + Workflow *Workflow `json:"workflow"` + ActionID string `json:"action_id"` + Style Style `json:"style,omitempty"` + AccessibilityLabel string `json:"accessibility_label,omitempty"` +} + +// ElementType returns the type of the element +func (s WorkflowButtonBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewWorkflowButtonBlockElement returns a new instance of a workflow button element +func NewWorkflowButtonBlockElement(text *TextBlockObject, workflow *Workflow, actionID string) *WorkflowButtonBlockElement { + return &WorkflowButtonBlockElement{ + Type: METWorkflowButton, + Text: text, + Workflow: workflow, + ActionID: actionID, + } +} + +// WithStyle sets the style for the workflow button element +func (s *WorkflowButtonBlockElement) WithStyle(style Style) *WorkflowButtonBlockElement { + s.Style = style + return s +} + +// WithAccessibilityLabel sets the accessibility label for the workflow button element +func (s *WorkflowButtonBlockElement) WithAccessibilityLabel(label string) *WorkflowButtonBlockElement { + s.AccessibilityLabel = label + return s +} diff --git a/vendor/github.com/slack-go/slack/block_header.go b/vendor/github.com/slack-go/slack/block_header.go index 3afb4c95fe..4277057f73 100644 --- a/vendor/github.com/slack-go/slack/block_header.go +++ b/vendor/github.com/slack-go/slack/block_header.go @@ -36,7 +36,9 @@ func NewHeaderBlock(textObj *TextBlockObject, options ...HeaderBlockOption) *Hea } for _, option := range options { - option(&block) + if option != nil { + option(&block) + } } return &block diff --git a/vendor/github.com/slack-go/slack/block_json.go b/vendor/github.com/slack-go/slack/block_json.go new file mode 100644 index 0000000000..43798f51e0 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_json.go @@ -0,0 +1,110 @@ +package slack + +import ( + "encoding/json" + "fmt" +) + +// RawJSONBlock represents a block created from raw JSON that preserves +// the original JSON structure. This is useful for testing new Slack block types +// before the library has full support, or for using blocks copied from Block Kit Builder. +// +// The block stores the original JSON and outputs it unchanged during marshalling, +// ensuring no data is lost through the unmarshal/marshal cycle. +type RawJSONBlock struct { + Type MessageBlockType `json:"-"` + BlockID string `json:"-"` + raw json.RawMessage +} + +// BlockType returns the type of the block +func (r RawJSONBlock) BlockType() MessageBlockType { + return r.Type +} + +// ID returns the block_id of the block +func (r RawJSONBlock) ID() string { + return r.BlockID +} + +// MarshalJSON outputs the original JSON unchanged +func (r RawJSONBlock) MarshalJSON() ([]byte, error) { + return r.raw, nil +} + +// BlockFromJSON creates a RawJSONBlock from a JSON string that preserves +// the original JSON. This is useful for quickly testing blocks from Slack's +// Block Kit Builder or for incorporating new block types before the library +// has full support. +// +// The JSON can be either a single block object or an array of blocks. +// If an array is provided, only the first block is returned. +// +// The returned block stores the original JSON and outputs it unchanged during +// marshalling, ensuring no data is lost. +// +// Returns an error if the JSON is invalid, empty, or missing required fields. +// +// Example: +// +// block, err := slack.BlockFromJSON(`{"type": "section", "text": {"type": "mrkdwn", "text": "Hello"}}`) +// if err != nil { +// return err +// } +// blocks = append(blocks, block) +func BlockFromJSON(jsonStr string) (Block, error) { + var rawJSON json.RawMessage + var isArray bool + + // Try to unmarshal as an array first + var arrayTest []json.RawMessage + if err := json.Unmarshal([]byte(jsonStr), &arrayTest); err == nil && len(arrayTest) > 0 { + rawJSON = arrayTest[0] + isArray = true + } else { + // Try as a single block object + if err := json.Unmarshal([]byte(jsonStr), &rawJSON); err != nil { + return nil, fmt.Errorf("failed to unmarshal block JSON: %w", err) + } + isArray = false + } + + if !isArray && len(rawJSON) == 0 { + return nil, fmt.Errorf("no blocks found in JSON") + } + + // Extract minimal fields for Block interface + var minimal struct { + Type string `json:"type"` + BlockID string `json:"block_id"` + } + if err := json.Unmarshal(rawJSON, &minimal); err != nil { + return nil, fmt.Errorf("failed to extract block type: %w", err) + } + + if minimal.Type == "" { + return nil, fmt.Errorf("block missing required 'type' field") + } + + return RawJSONBlock{ + Type: MessageBlockType(minimal.Type), + BlockID: minimal.BlockID, + raw: rawJSON, + }, nil +} + +// MustBlockFromJSON creates a Block from a JSON string and panics if there's an error. +// This is primarily intended for use in tests or examples where the JSON is known to be valid. +// For production code, use BlockFromJSON which returns an error instead. +// +// Example: +// +// block := slack.MustBlockFromJSON(`{"type": "divider"}`) +// msg := slack.NewBlockMessage(block) +func MustBlockFromJSON(jsonStr string) Block { + block, err := BlockFromJSON(jsonStr) + if err != nil { + panic(fmt.Sprintf("MustBlockFromJSON: %v", err)) + } + return block +} diff --git a/vendor/github.com/slack-go/slack/block_object.go b/vendor/github.com/slack-go/slack/block_object.go index fd73b6c4a4..c1b64cc4c3 100644 --- a/vendor/github.com/slack-go/slack/block_object.go +++ b/vendor/github.com/slack-go/slack/block_object.go @@ -11,7 +11,6 @@ import ( // BlockObject defines an interface that all block object types should // implement. -// @TODO: Is this interface needed? // blockObject object types const ( diff --git a/vendor/github.com/slack-go/slack/block_plan.go b/vendor/github.com/slack-go/slack/block_plan.go new file mode 100644 index 0000000000..c6e4174903 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_plan.go @@ -0,0 +1,55 @@ +package slack + +// PlanBlock defines a block of type plan used by AI agents +// to group multiple task cards under a shared title. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/plan-block/ +type PlanBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Title string `json:"title"` + Tasks []TaskCardBlock `json:"tasks,omitempty"` +} + +// BlockType returns the type of the block +func (s PlanBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s PlanBlock) ID() string { + return s.BlockID +} + +// PlanBlockOption allows configuration of options for a new plan block +type PlanBlockOption func(*PlanBlock) + +// PlanBlockOptionBlockID sets the block ID for the plan block +func PlanBlockOptionBlockID(blockID string) PlanBlockOption { + return func(block *PlanBlock) { + block.BlockID = blockID + } +} + +// NewPlanBlock returns a new instance of a plan block +func NewPlanBlock(title string, options ...PlanBlockOption) *PlanBlock { + block := PlanBlock{ + Type: MBTPlan, + Title: title, + } + + for _, option := range options { + option(&block) + } + + return &block +} + +// WithTasks sets the tasks for the PlanBlock +func (s *PlanBlock) WithTasks(tasks ...*TaskCardBlock) *PlanBlock { + s.Tasks = make([]TaskCardBlock, len(tasks)) + for i, t := range tasks { + s.Tasks[i] = *t + } + return s +} diff --git a/vendor/github.com/slack-go/slack/block_rich_text.go b/vendor/github.com/slack-go/slack/block_rich_text.go index 73233d23ae..3b0ead2eef 100644 --- a/vendor/github.com/slack-go/slack/block_rich_text.go +++ b/vendor/github.com/slack-go/slack/block_rich_text.go @@ -103,6 +103,10 @@ func (u RichTextUnknown) RichTextElementType() RichTextElementType { return u.Type } +func (u RichTextUnknown) MarshalJSON() ([]byte, error) { + return []byte(u.Raw), nil +} + type RichTextListElementType string const ( @@ -129,7 +133,7 @@ func NewRichTextList(style RichTextListElementType, indent int, elements ...Rich } } -// ElementType returns the type of the Element +// RichTextElementType returns the type of the Element func (s RichTextList) RichTextElementType() RichTextElementType { return s.Type } @@ -262,7 +266,9 @@ func (e *RichTextSection) UnmarshalJSON(b []byte) error { return nil } -// NewRichTextSectionBlockElement . +// NewRichTextSection creates a new rich text section from the provided elements. The +// section type will default to "rich_text_section", as it's the only currently supported +// section type. func NewRichTextSection(elements ...RichTextSectionElement) *RichTextSection { return &RichTextSection{ Type: RTESection, @@ -292,10 +298,14 @@ type RichTextSectionElement interface { } type RichTextSectionTextStyle struct { - Bold bool `json:"bold,omitempty"` - Italic bool `json:"italic,omitempty"` - Strike bool `json:"strike,omitempty"` - Code bool `json:"code,omitempty"` + Bold bool `json:"bold,omitempty"` + Italic bool `json:"italic,omitempty"` + Strike bool `json:"strike,omitempty"` + Code bool `json:"code,omitempty"` + Underline bool `json:"underline,omitempty"` + Highlight bool `json:"highlight,omitempty"` + ClientHighlight bool `json:"client_highlight,omitempty"` + Unlink bool `json:"unlink,omitempty"` } type RichTextSectionTextElement struct { @@ -328,7 +338,7 @@ func (r RichTextSectionChannelElement) RichTextSectionElementType() RichTextSect func NewRichTextSectionChannelElement(channelID string, style *RichTextSectionTextStyle) *RichTextSectionChannelElement { return &RichTextSectionChannelElement{ - Type: RTSEText, + Type: RTSEChannel, ChannelID: channelID, Style: style, } @@ -414,6 +424,7 @@ func NewRichTextSectionTeamElement(teamID string, style *RichTextSectionTextStyl type RichTextSectionUserGroupElement struct { Type RichTextSectionElementType `json:"type"` UsergroupID string `json:"usergroup_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` } func (r RichTextSectionUserGroupElement) RichTextSectionElementType() RichTextSectionElementType { @@ -490,8 +501,16 @@ func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSect return r.Type } +func (r RichTextSectionUnknownElement) MarshalJSON() ([]byte, error) { + return []byte(r.Raw), nil +} + // RichTextQuote represents rich_text_quote element type. -type RichTextQuote RichTextSection +type RichTextQuote struct { + Type RichTextElementType `json:"type"` + Elements []RichTextSectionElement `json:"elements"` + Border int `json:"border,omitempty"` +} // RichTextElementType returns the type of the Element func (s *RichTextQuote) RichTextElementType() RichTextElementType { @@ -504,15 +523,26 @@ func (s *RichTextQuote) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(b, &rts); err != nil { return err } - *s = RichTextQuote(rts) - s.Type = RTEQuote + var standalone struct { + Border int `json:"border"` + } + if err := json.Unmarshal(b, &standalone); err != nil { + return err + } + *s = RichTextQuote{ + Type: RTEQuote, + Elements: rts.Elements, + Border: standalone.Border, + } return nil } // RichTextPreformatted represents rich_text_quote element type. type RichTextPreformatted struct { - RichTextSection - Border int `json:"border"` + Type RichTextElementType `json:"type"` + Elements []RichTextSectionElement `json:"elements"` + Border int `json:"border"` + Language string `json:"language,omitempty"` } // RichTextElementType returns the type of the Element @@ -536,15 +566,17 @@ func (s *RichTextPreformatted) UnmarshalJSON(b []byte) error { // original struct, which may become a maintenance burden (i.e. update the // fields in two places, should it ever change). var standalone struct { - Border int `json:"border"` + Border int `json:"border"` + Language string `json:"language"` } if err := json.Unmarshal(b, &standalone); err != nil { return err } *s = RichTextPreformatted{ - RichTextSection: rts, - Border: standalone.Border, + Type: RTEPreformatted, + Elements: rts.Elements, + Border: standalone.Border, + Language: standalone.Language, } - s.Type = RTEPreformatted return nil } diff --git a/vendor/github.com/slack-go/slack/block_table.go b/vendor/github.com/slack-go/slack/block_table.go new file mode 100644 index 0000000000..5c7b0f13a6 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_table.go @@ -0,0 +1,55 @@ +package slack + +// TableBlock defines a block that lets you use a table to display your data. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/table-block/ +type TableBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Rows [][]*RichTextBlock `json:"rows"` + ColumnSettings []ColumnSetting `json:"column_settings,omitempty"` +} + +type ColumnAlignment string + +const ( + ColumnAlignmentLeft ColumnAlignment = "left" + ColumnAlignmentCenter ColumnAlignment = "center" + ColumnAlignmentRight ColumnAlignment = "right" +) + +type ColumnSetting struct { + Align ColumnAlignment `json:"align"` + IsWrapped bool `json:"is_wrapped"` +} + +// BlockType returns the type of the block +func (s TableBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s TableBlock) ID() string { + return s.BlockID +} + +// WithColumnSettings sets the column settings for the Table Block +func (s *TableBlock) WithColumnSettings(columnSettings ...ColumnSetting) *TableBlock { + s.ColumnSettings = columnSettings + return s +} + +// AddRow adds a new row of cells to the Table Block +func (s *TableBlock) AddRow(cells ...*RichTextBlock) *TableBlock { + s.Rows = append(s.Rows, append([]*RichTextBlock{}, cells...)) + return s +} + +// NewTableBlock returns an instance of a Table Block type +func NewTableBlock(blockID string) *TableBlock { + return &TableBlock{ + Type: MBTTable, + BlockID: blockID, + Rows: make([][]*RichTextBlock, 0), + } +} diff --git a/vendor/github.com/slack-go/slack/block_task_card.go b/vendor/github.com/slack-go/slack/block_task_card.go new file mode 100644 index 0000000000..2053a82d6e --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_task_card.go @@ -0,0 +1,101 @@ +package slack + +// TaskCardStatus defines the status of a task card block. +type TaskCardStatus string + +const ( + TaskCardStatusPending TaskCardStatus = "pending" + TaskCardStatusInProgress TaskCardStatus = "in_progress" + TaskCardStatusComplete TaskCardStatus = "complete" + TaskCardStatusError TaskCardStatus = "error" +) + +// TaskCardSource represents a URL reference in a task card block. +type TaskCardSource struct { + Type string `json:"type"` + URL string `json:"url"` + Text string `json:"text"` +} + +// NewTaskCardSource creates a new TaskCardSource with type "url". +func NewTaskCardSource(url, text string) TaskCardSource { + return TaskCardSource{ + Type: "url", + URL: url, + Text: text, + } +} + +// TaskCardBlock defines a block of type task_card used by AI agents +// to display thinking steps and task execution. +// +// More Information: https://docs.slack.dev/reference/block-kit/blocks/task-card-block/ +type TaskCardBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + TaskID string `json:"task_id"` + Title string `json:"title"` + Status TaskCardStatus `json:"status,omitempty"` + Details *RichTextBlock `json:"details,omitempty"` + Output *RichTextBlock `json:"output,omitempty"` + Sources []TaskCardSource `json:"sources,omitempty"` +} + +// BlockType returns the type of the block +func (s TaskCardBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s TaskCardBlock) ID() string { + return s.BlockID +} + +// TaskCardBlockOption allows configuration of options for a new task card block +type TaskCardBlockOption func(*TaskCardBlock) + +// TaskCardBlockOptionBlockID sets the block ID for the task card block +func TaskCardBlockOptionBlockID(blockID string) TaskCardBlockOption { + return func(block *TaskCardBlock) { + block.BlockID = blockID + } +} + +// NewTaskCardBlock returns a new instance of a task card block +func NewTaskCardBlock(taskID, title string, options ...TaskCardBlockOption) *TaskCardBlock { + block := TaskCardBlock{ + Type: MBTTaskCard, + TaskID: taskID, + Title: title, + } + + for _, option := range options { + option(&block) + } + + return &block +} + +// WithStatus sets the status for the TaskCardBlock +func (s *TaskCardBlock) WithStatus(status TaskCardStatus) *TaskCardBlock { + s.Status = status + return s +} + +// WithDetails sets the details rich text block for the TaskCardBlock +func (s *TaskCardBlock) WithDetails(details *RichTextBlock) *TaskCardBlock { + s.Details = details + return s +} + +// WithOutput sets the output rich text block for the TaskCardBlock +func (s *TaskCardBlock) WithOutput(output *RichTextBlock) *TaskCardBlock { + s.Output = output + return s +} + +// WithSources sets the sources for the TaskCardBlock +func (s *TaskCardBlock) WithSources(sources ...TaskCardSource) *TaskCardBlock { + s.Sources = sources + return s +} diff --git a/vendor/github.com/slack-go/slack/block_unknown.go b/vendor/github.com/slack-go/slack/block_unknown.go index 7a49a2c8d1..71b2a90c9d 100644 --- a/vendor/github.com/slack-go/slack/block_unknown.go +++ b/vendor/github.com/slack-go/slack/block_unknown.go @@ -1,10 +1,19 @@ package slack -// UnknownBlock represents a block type that is not yet known. This block type exists to prevent Slack from introducing -// new and unknown block types that break this library. +import "encoding/json" + +// UnknownBlock represents a block type that is not yet known. This block type +// exists to prevent Slack from introducing new and unknown block types that +// break this library. It preserves the raw JSON so that unrecognized blocks +// survive round-trip marshaling. +// +// If you encounter an UnknownBlock for a block type that Slack documents, +// please open an issue at https://github.com/slack-go/slack/issues so we can +// add first-class support for it. type UnknownBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` + raw json.RawMessage } // BlockType returns the type of the block @@ -16,3 +25,12 @@ func (b UnknownBlock) BlockType() MessageBlockType { func (s UnknownBlock) ID() string { return s.BlockID } + +// MarshalJSON returns the original raw JSON if available, preserving all fields +func (b UnknownBlock) MarshalJSON() ([]byte, error) { + if b.raw != nil { + return b.raw, nil + } + type alias UnknownBlock + return json.Marshal(alias(b)) +} diff --git a/vendor/github.com/slack-go/slack/channels.go b/vendor/github.com/slack-go/slack/channels.go index 88d567bff9..d01ce823f4 100644 --- a/vendor/github.com/slack-go/slack/channels.go +++ b/vendor/github.com/slack-go/slack/channels.go @@ -28,7 +28,7 @@ type Channel struct { func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) { response := &channelResponseFull{} - err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api) + _, err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/chat.go b/vendor/github.com/slack-go/slack/chat.go index 85c4848ba4..a58b4e805f 100644 --- a/vendor/github.com/slack-go/slack/chat.go +++ b/vendor/github.com/slack-go/slack/chat.go @@ -119,13 +119,13 @@ func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOptio // ScheduleMessageContext sends a message to a channel with a custom context. // Slack API docs: https://api.slack.com/methods/chat.scheduleMessage func (api *Client) ScheduleMessageContext(ctx context.Context, channelID, postAt string, options ...MsgOption) (string, string, error) { - respChannel, scheduledMessageId, _, err := api.SendMessageContext( + respChannel, scheduledMessageID, _, err := api.SendMessageContext( ctx, channelID, MsgOptionSchedule(postAt), MsgOptionCompose(options...), ) - return respChannel, scheduledMessageId, err + return respChannel, scheduledMessageID, err } // PostMessage sends a message to a channel. @@ -209,6 +209,33 @@ func (api *Client) UnfurlMessageWithAuthURLContext(ctx context.Context, channelI return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlAuthURL(timestamp, userAuthURL), MsgOptionCompose(options...)) } +// UnfurlMessageWorkObject unfurls a message with Work Objects metadata. +// For more details, see UnfurlMessageWorkObjectContext documentation. +func (api *Client) UnfurlMessageWorkObject(channelID, timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata, options ...MsgOption) (string, string, string, error) { + return api.UnfurlMessageWorkObjectContext(context.Background(), channelID, timestamp, unfurls, metadata, options...) +} + +// UnfurlMessageWorkObjectContext unfurls a message with Work Objects metadata with a custom context. +// This enables rich Work Object previews as described in https://docs.slack.dev/messaging/work-objects/ +// unfurls may be nil to send only Work Object metadata (no legacy attachment unfurls). +func (api *Client) UnfurlMessageWorkObjectContext(ctx context.Context, channelID, timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlWorkObject(timestamp, unfurls, metadata), MsgOptionCompose(options...)) +} + +// UnfurlMessageByID unfurls a link in the message composer using unfurl_id and source. +// Use this when Slack sends link_shared with unfurl_id (e.g. before the message is posted). +// For more details, see UnfurlMessageByIDContext documentation. +func (api *Client) UnfurlMessageByID(unfurlID, source string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.UnfurlMessageByIDContext(context.Background(), unfurlID, source, unfurls, options...) +} + +// UnfurlMessageByIDContext unfurls by unfurl_id and source with a custom context. +// Both unfurl_id and source must be provided together (alternative to channel + ts). +// Slack API docs: https://api.slack.com/methods/chat.unfurl +func (api *Client) UnfurlMessageByIDContext(ctx context.Context, unfurlID, source string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, "", MsgOptionUnfurlByID(unfurlID, source, unfurls), MsgOptionCompose(options...)) +} + // SendMessage more flexible method for configuring messages. // For more details, see SendMessageContext documentation. func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) { @@ -217,7 +244,7 @@ func (api *Client) SendMessage(channel string, options ...MsgOption) (string, st // SendMessageContext more flexible method for configuring messages with a custom context. // Slack API docs: https://api.slack.com/methods/chat.postMessage -func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestampOrScheduledMessageId string, _text string, err error) { +func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestampOrScheduledMessageID string, _text string, err error) { var ( req *http.Request parser func(*chatResponseFull) responseParser @@ -237,7 +264,7 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt api.Debugf("Sending request: %s", redactToken(reqBody)) } - if err = doPost(api.httpclient, req, parser(&response), api); err != nil { + if _, err = doPost(api.httpclient, req, parser(&response), api); err != nil { return "", "", "", err } @@ -307,6 +334,9 @@ const ( chatResponse sendMode = "chat.responseURL" chatMeMessage sendMode = "chat.meMessage" chatUnfurl sendMode = "chat.unfurl" + chatStartStream sendMode = "chat.startStream" + chatAppendStream sendMode = "chat.appendStream" + chatStopStream sendMode = "chat.stopStream" ) type sendConfig struct { @@ -345,13 +375,15 @@ func (t sendConfig) BuildRequestContext(ctx context.Context, token, channelID st deleteOriginal: t.deleteOriginal, }.BuildRequestContext(ctx) default: - return formSender{endpoint: t.endpoint, values: t.values}.BuildRequestContext(ctx) + return formSender{endpoint: t.endpoint, values: t.values, attachments: t.attachments, blocks: t.blocks}.BuildRequestContext(ctx) } } type formSender struct { - endpoint string - values url.Values + endpoint string + values url.Values + attachments []Attachment + blocks Blocks } func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { @@ -359,6 +391,22 @@ func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) respo } func (t formSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + if t.attachments != nil { + attachmentBytes, err := json.Marshal(t.attachments) + if err != nil { + return nil, nil, err + } + t.values.Set("attachments", string(attachmentBytes)) + } + + if t.blocks.BlockSet != nil { + blockBytes, err := json.Marshal(t.blocks.BlockSet) + if err != nil { + return nil, nil, err + } + t.values.Set("blocks", string(blockBytes)) + } + req, err := formReq(ctx, t.endpoint, t.values) return req, func(resp *chatResponseFull) responseParser { return newJSONParser(resp) @@ -468,6 +516,51 @@ func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption } } +// MsgOptionUnfurlByID unfurls using unfurl_id and source (e.g. when link is in the composer). +// Use instead of channel+ts when Slack provides unfurl_id in the link_shared event. +// unfurls may be nil; the API expects a JSON object so nil is sent as {}. +func MsgOptionUnfurlByID(unfurlID, source string, unfurls map[string]Attachment) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Del("channel") + config.values.Del("ts") + config.values.Set("unfurl_id", unfurlID) + config.values.Set("source", source) + if unfurls == nil { + unfurls = make(map[string]Attachment) + } + unfurlsStr, err := json.Marshal(unfurls) + if err == nil { + config.values.Set("unfurls", string(unfurlsStr)) + } + return err + } +} + +// MsgOptionUnfurlMetadataOnly sets chat.unfurl endpoint with only Work Object metadata (no unfurls). +func MsgOptionUnfurlMetadataOnly(timestamp string, metadata WorkObjectMetadata) MsgOption { + return MsgOptionCompose( + func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + return nil + }, + MsgOptionWorkObjectMetadata(metadata), + ) +} + +// MsgOptionUnfurlWorkObject unfurls a message with Work Objects metadata. +// When unfurls is nil, only metadata is sent (no legacy attachment unfurls). +func MsgOptionUnfurlWorkObject(timestamp string, unfurls map[string]Attachment, metadata WorkObjectMetadata) MsgOption { + if len(unfurls) > 0 { + return MsgOptionCompose( + MsgOptionUnfurl(timestamp, unfurls), + MsgOptionWorkObjectMetadata(metadata), + ) + } + return MsgOptionUnfurlMetadataOnly(timestamp, metadata) +} + // MsgOptionUnfurlAuthURL unfurls a message using an auth url based on the timestamp. func MsgOptionUnfurlAuthURL(timestamp string, userAuthURL string) MsgOption { return func(config *sendConfig) error { @@ -500,6 +593,23 @@ func MsgOptionUnfurlAuthMessage(timestamp string, msg string) MsgOption { } } +// MsgOptionUnfurlAuthBlocks sets Block Kit blocks for the auth prompt (overrides default buttons). +// See https://docs.slack.com/methods/chat.unfurl for user_auth_blocks. +func MsgOptionUnfurlAuthBlocks(timestamp string, blocks ...Block) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + if len(blocks) == 0 { + return nil + } + blocksStr, err := json.Marshal(blocks) + if err == nil { + config.values.Set("user_auth_blocks", string(blocksStr)) + } + return err + } +} + // MsgOptionResponseURL supplies a url to use as the endpoint. func MsgOptionResponseURL(url string, responseType string) MsgOption { return func(config *sendConfig) error { @@ -534,6 +644,7 @@ func MsgOptionDeleteOriginal(responseURL string) MsgOption { // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { + //lint:ignore S1002 - we want to explicitly check against the constant if b != DEFAULT_MESSAGE_ASUSER { config.values.Set("as_user", "true") } @@ -578,33 +689,24 @@ func MsgOptionAttachments(attachments ...Attachment) MsgOption { config.attachments = attachments - // FIXME: We are setting the attachments on the message twice: above for - // the json version, and below for the html version. The marshalled bytes - // we put into config.values below don't work directly in the Msg version. - - attachmentBytes, err := json.Marshal(attachments) - if err == nil { - config.values.Set("attachments", string(attachmentBytes)) - } - - return err + return nil } } -// MsgOptionBlocks sets blocks for the message +// MsgOptionBlocks sets blocks for the message. +// Calling with no arguments or an empty slice sends "blocks=[]" to clear blocks. +// To skip setting blocks entirely, do not include this option. func MsgOptionBlocks(blocks ...Block) MsgOption { return func(config *sendConfig) error { - if blocks == nil { - return nil + if len(blocks) == 0 { + // Explicitly set to empty slice (not nil) so the sender + // knows to marshal "[]" and clear blocks on the message. + config.blocks.BlockSet = []Block{} + } else { + config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...) } - config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...) - - blocks, err := json.Marshal(blocks) - if err == nil { - config.values.Set("blocks", string(blocks)) - } - return err + return nil } } @@ -710,6 +812,31 @@ func MsgOptionMetadata(metadata SlackMetadata) MsgOption { } } +// MsgOptionWorkObjectMetadata sets Work Objects metadata for unfurls and messages +// This enables Work Objects support as described in https://docs.slack.dev/messaging/work-objects/ +// If metadata.Entities is nil, it is marshaled as [] so the API receives a valid entities array. +func MsgOptionWorkObjectMetadata(metadata WorkObjectMetadata) MsgOption { + return func(config *sendConfig) error { + metaToMarshal := metadata + if metaToMarshal.Entities == nil { + metaToMarshal.Entities = []WorkObjectEntity{} + } + meta, err := json.Marshal(metaToMarshal) + if err == nil { + config.values.Set("metadata", string(meta)) + } + return err + } +} + +// MsgOptionWorkObjectEntity creates Work Objects metadata with a single entity +// This is a convenience function for the common case of unfurling a single Work Object +func MsgOptionWorkObjectEntity(entity WorkObjectEntity) MsgOption { + return MsgOptionWorkObjectMetadata(WorkObjectMetadata{ + Entities: []WorkObjectEntity{entity}, + }) +} + // MsgOptionLinkNames finds and links user groups. Does not support linking individual users func MsgOptionLinkNames(linkName bool) MsgOption { return func(config *sendConfig) error { @@ -735,6 +862,74 @@ func MsgOptionFileIDs(fileIDs []string) MsgOption { } } +// MsgOptionStartStream starts a streaming message. +func MsgOptionStartStream() MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatStartStream) + return nil + } +} + +// MsgOptionAppendStream appends to a streaming message. +func MsgOptionAppendStream(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatAppendStream) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionStopStream stops a streaming message. +func MsgOptionStopStream(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatStopStream) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionRecipientTeamID sets the recipient team ID for streaming messages. +func MsgOptionRecipientTeamID(teamID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("recipient_team_id", teamID) + return nil + } +} + +// MsgOptionRecipientUserID sets the recipient user ID for streaming messages. +func MsgOptionRecipientUserID(userID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("recipient_user_id", userID) + return nil + } +} + +// MsgOptionMarkdownText sets the markdown text for streaming messages. +func MsgOptionMarkdownText(text string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("markdown_text", text) + return nil + } +} + +// TaskDisplayMode controls how task_card / task_update chunks render in a +// streamed message. Used with chat.startStream. +type TaskDisplayMode string + +const ( + TaskDisplayModeTimeline TaskDisplayMode = "timeline" + TaskDisplayModePlan TaskDisplayMode = "plan" +) + +// MsgOptionTaskDisplayMode sets task_display_mode on chat.startStream, +// controlling whether tasks render as a sequential timeline or a grouped plan. +func MsgOptionTaskDisplayMode(mode TaskDisplayMode) MsgOption { + return func(config *sendConfig) error { + config.values.Set("task_display_mode", string(mode)) + return nil + } +} + // UnsafeMsgOptionEndpoint deliver the message to the specified endpoint. // NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option // will be supported by the library, it is subject to change without notice that @@ -769,15 +964,19 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { config.values.Set("link_names", "1") } + //lint:ignore S1002 - we want to explicitly check against the constant if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "true") } // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. + //lint:ignore S1002 - we want to explicitly check against the constants if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "false") } + + //lint:ignore S1002 - we want to explicitly check against the constant if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { config.values.Set("unfurl_media", "false") } @@ -787,6 +986,7 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { config.values.Set("icon_emoji", params.IconEmoji) } + //lint:ignore S1002 - we want to explicitly check against the constant if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { config.values.Set("mrkdwn", "false") } @@ -794,10 +994,17 @@ func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { config.values.Set("thread_ts", params.ThreadTimestamp) } + //lint:ignore S1002 - we want to explicitly check against the constant if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST { config.values.Set("reply_broadcast", "true") } + if params.MetaData.EventType != "" { + if err := MsgOptionMetadata(params.MetaData)(config); err != nil { + return err + } + } + if len(params.FileIDs) > 0 { return MsgOptionFileIDs(params.FileIDs)(config) } @@ -924,3 +1131,57 @@ func (api *Client) DeleteScheduledMessageContext(ctx context.Context, params *De return response.Ok, response.Err() } + +// StartStream starts a streaming message in a channel. +// For more details, see StartStreamContext documentation. +func (api *Client) StartStream(channelID string, options ...MsgOption) (string, string, error) { + return api.StartStreamContext(context.Background(), channelID, options...) +} + +// StartStreamContext starts a streaming message in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.startStream +func (api *Client) StartStreamContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channelID, + MsgOptionStartStream(), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// AppendStream appends text to a streaming message. +// For more details, see AppendStreamContext documentation. +func (api *Client) AppendStream(channelID, timestamp string, options ...MsgOption) (string, string, error) { + return api.AppendStreamContext(context.Background(), channelID, timestamp, options...) +} + +// AppendStreamContext appends text to a streaming message with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.appendStream +func (api *Client) AppendStreamContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channelID, + MsgOptionAppendStream(timestamp), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// StopStream stops a streaming message. +// For more details, see StopStreamContext documentation. +func (api *Client) StopStream(channelID, timestamp string, options ...MsgOption) (string, string, error) { + return api.StopStreamContext(context.Background(), channelID, timestamp, options...) +} + +// StopStreamContext stops a streaming message with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.stopStream +func (api *Client) StopStreamContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, error) { + respChannel, respTimestamp, _, err := api.SendMessageContext( + ctx, + channelID, + MsgOptionStopStream(timestamp), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} diff --git a/vendor/github.com/slack-go/slack/chat_stream_chunks.go b/vendor/github.com/slack-go/slack/chat_stream_chunks.go new file mode 100644 index 0000000000..fbdab3be93 --- /dev/null +++ b/vendor/github.com/slack-go/slack/chat_stream_chunks.go @@ -0,0 +1,95 @@ +package slack + +import ( + "encoding/json" +) + +// StreamChunkType identifies a chunk in the chat.startStream / chat.appendStream +// / chat.stopStream streaming-message protocol. +// +// More information: https://docs.slack.dev/reference/methods/chat.appendStream/ +type StreamChunkType string + +const ( + StreamChunkMarkdownText StreamChunkType = "markdown_text" + StreamChunkTaskUpdate StreamChunkType = "task_update" + StreamChunkPlanUpdate StreamChunkType = "plan_update" + StreamChunkBlocks StreamChunkType = "blocks" +) + +// StreamChunk represents a single chunk in the streaming-message chunks array. +type StreamChunk interface { + ChunkType() StreamChunkType +} + +// MarkdownTextChunk streams markdown-formatted text. +type MarkdownTextChunk struct { + Type StreamChunkType `json:"type"` + Text string `json:"text"` +} + +func (c MarkdownTextChunk) ChunkType() StreamChunkType { return c.Type } + +// NewMarkdownTextChunk returns a markdown_text chunk. +func NewMarkdownTextChunk(text string) MarkdownTextChunk { + return MarkdownTextChunk{Type: StreamChunkMarkdownText, Text: text} +} + +// TaskUpdateChunk streams a task status update that renders as a task card. +type TaskUpdateChunk struct { + Type StreamChunkType `json:"type"` + ID string `json:"id"` + Title string `json:"title"` + Status TaskCardStatus `json:"status,omitempty"` + Details string `json:"details,omitempty"` + Output string `json:"output,omitempty"` + Sources []TaskCardSource `json:"sources,omitempty"` +} + +func (c TaskUpdateChunk) ChunkType() StreamChunkType { return c.Type } + +// NewTaskUpdateChunk returns a task_update chunk with the given id and title. +func NewTaskUpdateChunk(id, title string) TaskUpdateChunk { + return TaskUpdateChunk{Type: StreamChunkTaskUpdate, ID: id, Title: title} +} + +// PlanUpdateChunk streams an update to the current plan's title. +type PlanUpdateChunk struct { + Type StreamChunkType `json:"type"` + Title string `json:"title"` +} + +func (c PlanUpdateChunk) ChunkType() StreamChunkType { return c.Type } + +// NewPlanUpdateChunk returns a plan_update chunk. +func NewPlanUpdateChunk(title string) PlanUpdateChunk { + return PlanUpdateChunk{Type: StreamChunkPlanUpdate, Title: title} +} + +// BlocksChunk streams a group of Block Kit blocks. Up to 50 blocks per chunk. +type BlocksChunk struct { + Type StreamChunkType `json:"type"` + Blocks []Block `json:"blocks"` +} + +func (c BlocksChunk) ChunkType() StreamChunkType { return c.Type } + +// NewBlocksChunk returns a blocks chunk containing the given blocks. +func NewBlocksChunk(blocks ...Block) BlocksChunk { + return BlocksChunk{Type: StreamChunkBlocks, Blocks: blocks} +} + +// MsgOptionChunks sets the `chunks` parameter for the streaming chat methods +// (chat.startStream / chat.appendStream / chat.stopStream). It is the +// transport for Block Kit agent-UI blocks (Alert, Card, Carousel, etc.) which +// chat.postMessage rejects as "Unsupported block type". +func MsgOptionChunks(chunks ...StreamChunk) MsgOption { + return func(config *sendConfig) error { + encoded, err := json.Marshal(chunks) + if err != nil { + return err + } + config.values.Set("chunks", string(encoded)) + return nil + } +} diff --git a/vendor/github.com/slack-go/slack/conversation.go b/vendor/github.com/slack-go/slack/conversation.go index 33eb0ff9be..a0ce707b0e 100644 --- a/vendor/github.com/slack-go/slack/conversation.go +++ b/vendor/github.com/slack-go/slack/conversation.go @@ -7,6 +7,7 @@ import ( "net/url" "strconv" "strings" + "time" ) // Conversation is the foundation for IM and BaseGroupConversation @@ -28,6 +29,7 @@ type Conversation struct { IsPrivate bool `json:"is_private"` IsReadOnly bool `json:"is_read_only"` IsMpIM bool `json:"is_mpim"` + IsUserDeleted bool `json:"is_user_deleted"` Unlinked int `json:"unlinked"` NameNormalized string `json:"name_normalized"` NumMembers int `json:"num_members"` @@ -67,12 +69,13 @@ type Purpose struct { LastSet JSONTime `json:"last_set"` } -// Properties contains the Canvas associated to the channel. +// Properties contains additional fields that appear based on the context of the conversation type Properties struct { - Canvas Canvas `json:"canvas"` - PostingRestrictedTo RestrictedTo `json:"posting_restricted_to"` - Tabs []Tab `json:"tabs"` - ThreadsRestrictedTo RestrictedTo `json:"threads_restricted_to"` + Canvas Canvas `json:"canvas"` + PostingRestrictedTo RestrictedTo `json:"posting_restricted_to"` + Tabs []Tab `json:"tabs"` + ThreadsRestrictedTo RestrictedTo `json:"threads_restricted_to"` + RecordChannel RecordChannel `json:"record_channel"` } type RestrictedTo struct { @@ -92,6 +95,13 @@ type Canvas struct { QuipThreadId string `json:"quip_thread_id"` } +type RecordChannel struct { + RecordID string `json:"record_id"` + RecordType string `json:"record_type"` + RecordLabel string `json:"record_label"` + RecordLabelPlural string `json:"record_label_plural"` +} + type GetUsersInConversationParameters struct { ChannelID string Cursor string @@ -344,12 +354,14 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel return response.Channel, response.Err() } -// The following functions are for inviting users to a channel but setting the `force` -// parameter to true. We have added this so that we don't break the existing API. -// -// IMPORTANT: If we ever get here for _another_ parameter, we should consider refactoring -// this to be more flexible. -// +/********************************************************************************** +The following functions are for inviting users to a channel but setting the `force` +parameter to true. We have added this so that we don't break the existing API. + +IMPORTANT: If we ever get here for _another_ parameter, we should consider refactoring +this to be more flexible. +*/ + // ForceInviteUsersToConversation invites users to a channel but sets the `force` // parameter to true. // @@ -478,7 +490,7 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI "user": {user}, } - response := SlackResponse{} + response := KickUserFromConversationSlackResponse{} err := api.postMethod(ctx, "conversations.kick", values, &response) if err != nil { return err @@ -677,6 +689,150 @@ type GetConversationsParameters struct { TeamID string } +// GetConversationsOption options for the GetAllConversationsContext method call. +type GetConversationsOption func(*ConversationPagination) + +// GetConversationsOptionLimit limit the number of conversations returned +func GetConversationsOptionLimit(n int) GetConversationsOption { + return func(p *ConversationPagination) { + p.limit = n + } +} + +// GetConversationsOptionExcludeArchived exclude archived conversations +func GetConversationsOptionExcludeArchived(exclude bool) GetConversationsOption { + return func(p *ConversationPagination) { + p.excludeArchived = exclude + } +} + +// GetConversationsOptionTypes filter conversations by type +func GetConversationsOptionTypes(types []string) GetConversationsOption { + return func(p *ConversationPagination) { + p.types = types + } +} + +// GetConversationsOptionTeamID include team Id +func GetConversationsOptionTeamID(teamId string) GetConversationsOption { + return func(p *ConversationPagination) { + p.teamId = teamId + } +} + +func newConversationPagination(c *Client, options ...GetConversationsOption) (cp ConversationPagination) { + cp = ConversationPagination{ + c: c, + limit: 200, // per slack api documentation. + } + + for _, opt := range options { + opt(&cp) + } + + return cp +} + +// ConversationPagination allows for paginating over the conversations +type ConversationPagination struct { + Conversations []Channel + limit int + excludeArchived bool + types []string + teamId string + previousResp *ResponseMetadata + c *Client +} + +// Done checks if the pagination has completed +func (ConversationPagination) Done(err error) bool { + return errors.Is(err, errPaginationComplete) +} + +// Failure checks if pagination failed. +func (t ConversationPagination) Failure(err error) error { + if t.Done(err) { + return nil + } + + return err +} + +func (t ConversationPagination) Next(ctx context.Context) (_ ConversationPagination, err error) { + if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { + return t, errPaginationComplete + } + + t.previousResp = t.previousResp.initialize() + + values := url.Values{ + "token": {t.c.token}, + "limit": {strconv.Itoa(t.limit)}, + "cursor": {t.previousResp.Cursor}, + } + if t.excludeArchived { + values.Add("exclude_archived", strconv.FormatBool(t.excludeArchived)) + } + if t.types != nil { + values.Add("types", strings.Join(t.types, ",")) + } + if t.teamId != "" { + values.Add("team_id", t.teamId) + } + + response := struct { + Channels []Channel `json:"channels"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + + err = t.c.postMethod(ctx, "conversations.list", values, &response) + if err != nil { + return t, err + } + + if err := response.Err(); err != nil { + return t, err + } + + t.c.Debugf("GetAllConversationsContext: got %d conversations; cursor %s", len(response.Channels), response.ResponseMetaData.NextCursor) + t.Conversations = response.Channels + t.previousResp = &ResponseMetadata{Cursor: response.ResponseMetaData.NextCursor} + + return t, nil +} + +// GetConversationsPaginated fetches conversations in a paginated fashion, see GetAllConversationsContext for usage. +func (api *Client) GetConversationsPaginated(options ...GetConversationsOption) ConversationPagination { + return newConversationPagination(api, options...) +} + +// GetAllConversations returns the list of all conversations, handling pagination and rate limiting +func (api *Client) GetAllConversations(options ...GetConversationsOption) (results []Channel, err error) { + return api.GetAllConversationsContext(context.Background(), options...) +} + +// GetAllConversationsContext returns the list of all conversations with a custom context, handling pagination and rate limiting +func (api *Client) GetAllConversationsContext(ctx context.Context, options ...GetConversationsOption) (results []Channel, err error) { + results = []Channel{} + p := api.GetConversationsPaginated(options...) + for err == nil { + p, err = p.Next(ctx) + if err == nil { + results = append(results, p.Conversations...) + } else if rateLimitedError, ok := err.(*RateLimitedError); ok { + select { + case <-ctx.Done(): + err = ctx.Err() + case <-time.After(rateLimitedError.RetryAfter): + err = nil + } + } + } + + return results, p.Failure(err) +} + // GetConversations returns the list of channels in a Slack team. // For more details, see GetConversationsContext documentation. func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { @@ -813,13 +969,13 @@ type GetConversationHistoryResponse struct { Messages []Message `json:"messages"` } -// GetConversationHistory joins an existing conversation. +// GetConversationHistory retrieves the message history from the specified conversation. // For more details, see GetConversationHistoryContext documentation. func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { return api.GetConversationHistoryContext(context.Background(), params) } -// GetConversationHistoryContext joins an existing conversation with a custom context. +// GetConversationHistoryContext retrieves the message history from the specified conversation with a custom context. // Slack API docs: https://api.slack.com/methods/conversations.history func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { values := url.Values{"token": {api.token}, "channel": {params.ChannelID}} @@ -880,21 +1036,55 @@ func (api *Client) MarkConversationContext(ctx context.Context, channel, ts stri return response.Err() } +// createChannelCanvasParams contains arguments for CreateChannelCanvas method call. +type createChannelCanvasParams struct { + title string + documentContent *DocumentContent +} + +// CreateChannelCanvasOption options for the CreateChannelCanvas method call. +type CreateChannelCanvasOption func(*createChannelCanvasParams) + +// CreateChannelCanvasOptionTitle sets the title of the canvas. +func CreateChannelCanvasOptionTitle(title string) CreateChannelCanvasOption { + return func(params *createChannelCanvasParams) { + params.title = title + } +} + +// CreateChannelCanvasOptionDocumentContent sets the document content of the canvas. +func CreateChannelCanvasOptionDocumentContent(documentContent DocumentContent) CreateChannelCanvasOption { + return func(params *createChannelCanvasParams) { + params.documentContent = &documentContent + } +} + // 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) +func (api *Client) CreateChannelCanvas(channel string, documentContent DocumentContent, options ...CreateChannelCanvasOption) (string, error) { + return api.CreateChannelCanvasContext(context.Background(), channel, documentContent, options...) } // 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) { +func (api *Client) CreateChannelCanvasContext(ctx context.Context, channel string, documentContent DocumentContent, options ...CreateChannelCanvasOption) (string, error) { + params := createChannelCanvasParams{ + documentContent: &documentContent, + } + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "channel_id": {channel}, } - if documentContent.Type != "" { - documentContentJSON, err := json.Marshal(documentContent) + if params.title != "" { + values.Add("title", params.title) + } + if params.documentContent != nil && params.documentContent.Type != "" { + documentContentJSON, err := json.Marshal(params.documentContent) if err != nil { return "", err } diff --git a/vendor/github.com/slack-go/slack/dialog.go b/vendor/github.com/slack-go/slack/dialog.go index f94113f4d1..4c507fcdf5 100644 --- a/vendor/github.com/slack-go/slack/dialog.go +++ b/vendor/github.com/slack-go/slack/dialog.go @@ -106,8 +106,7 @@ func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dial } response := &DialogOpenResponse{} - endpoint := api.endpoint + "dialog.open" - if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil { + if err := api.postJSONMethod(ctx, "dialog.open", api.token, encoded, response); err != nil { return err } diff --git a/vendor/github.com/slack-go/slack/dnd.go b/vendor/github.com/slack-go/slack/dnd.go index 81eaf5024d..4f6b35a58f 100644 --- a/vendor/github.com/slack-go/slack/dnd.go +++ b/vendor/github.com/slack-go/slack/dnd.go @@ -7,6 +7,14 @@ import ( "strings" ) +// DNDOptionTeamID sets the team_id parameter for DND methods. Required after +// workspace migration when the API returns missing_argument: team_id. +func DNDOptionTeamID(teamID string) ParamOption { + return func(v *url.Values) { + v.Set("team_id", teamID) + } +} + type SnoozeDebug struct { SnoozeEndDate string `json:"snooze_end_date"` } @@ -52,7 +60,7 @@ func (api *Client) EndDND() error { } // EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.endDnd +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.endDnd func (api *Client) EndDNDContext(ctx context.Context) error { values := url.Values{ "token": {api.token}, @@ -74,7 +82,7 @@ func (api *Client) EndSnooze() (*DNDStatus, error) { } // EndSnoozeContext ends the current user's snooze mode with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.endSnooze +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.endSnooze func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, @@ -89,19 +97,22 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { // GetDNDInfo provides information about a user's current Do Not Disturb settings. // For more information see the GetDNDInfoContext documentation. -func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { - return api.GetDNDInfoContext(context.Background(), user) +func (api *Client) GetDNDInfo(user *string, options ...ParamOption) (*DNDStatus, error) { + return api.GetDNDInfoContext(context.Background(), user, options...) } // GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.info -func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.info/ +func (api *Client) GetDNDInfoContext(ctx context.Context, user *string, options ...ParamOption) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, } if user != nil { values.Set("user", *user) } + for _, opt := range options { + opt(&values) + } response, err := api.dndRequest(ctx, "dnd.info", values) if err != nil { @@ -112,17 +123,20 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta // GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. // For more information see the GetDNDTeamInfoContext documentation. -func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { - return api.GetDNDTeamInfoContext(context.Background(), users) +func (api *Client) GetDNDTeamInfo(users []string, options ...ParamOption) (map[string]DNDStatus, error) { + return api.GetDNDTeamInfoContext(context.Background(), users, options...) } // GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context. -// Slack API docs: https://api.slack.com/methods/dnd.teamInfo -func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.teamInfo +func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string, options ...ParamOption) (map[string]DNDStatus, error) { values := url.Values{ "token": {api.token}, "users": {strings.Join(users, ",")}, } + for _, opt := range options { + opt(&values) + } response := &dndTeamInfoResponse{} if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil { @@ -145,7 +159,7 @@ func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { // SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings. // If a snooze session is not already active for the user, invoking this method will // begin one for the specified duration. -// Slack API docs: https://api.slack.com/methods/dnd.setSnooze +// Slack API docs: https://docs.slack.dev/reference/methods/dnd.setSnooze func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, diff --git a/vendor/github.com/slack-go/slack/entity.go b/vendor/github.com/slack-go/slack/entity.go new file mode 100644 index 0000000000..815de4d75c --- /dev/null +++ b/vendor/github.com/slack-go/slack/entity.go @@ -0,0 +1,132 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" +) + +// EntityPresentDetailsParameters contains the parameters for entity.presentDetails API method +type EntityPresentDetailsParameters struct { + TriggerID string `json:"trigger_id"` + Metadata *EntityDetailsMetadata `json:"metadata,omitempty"` + Error *EntityDetailsError `json:"error,omitempty"` + UserAuthRequired bool `json:"user_auth_required,omitempty"` + UserAuthURL string `json:"user_auth_url,omitempty"` + UserAuthMessage string `json:"user_auth_message,omitempty"` +} + +// EntityDetailsMetadata represents the metadata for entity details +type EntityDetailsMetadata struct { + EntityType string `json:"entity_type"` + URL string `json:"url,omitempty"` + ExternalRef WorkObjectExternalRef `json:"external_ref,omitempty"` + EntityPayload map[string]interface{} `json:"entity_payload"` +} + +// EntityDetailsError represents an error response for entity details +type EntityDetailsError struct { + Status string `json:"status"` + CustomTitle string `json:"custom_title,omitempty"` + CustomMessage string `json:"custom_message,omitempty"` + MessageFormat string `json:"message_format,omitempty"` + Actions []EntityDetailsAction `json:"actions,omitempty"` +} + +// EntityDetailsAction represents an action button in entity details error +type EntityDetailsAction struct { + Text string `json:"text"` + ActionID string `json:"action_id"` + Value string `json:"value,omitempty"` + Style string `json:"style,omitempty"` + URL string `json:"url,omitempty"` + ProcessingState *EntityDetailsProcessingState `json:"processing_state,omitempty"` +} + +// EntityDetailsProcessingState represents the processing state of an action +type EntityDetailsProcessingState struct { + Enabled bool `json:"enabled"` +} + +// EntityPresentDetailsResponse represents the response from entity.presentDetails +type EntityPresentDetailsResponse struct { + SlackResponse +} + +// EntityPresentDetails presents entity details in the flexpane +// For more details, see EntityPresentDetailsContext documentation. +func (api *Client) EntityPresentDetails(params EntityPresentDetailsParameters) error { + return api.EntityPresentDetailsContext(context.Background(), params) +} + +// EntityPresentDetailsContext presents entity details in the flexpane with a custom context. +// Slack API docs: https://docs.slack.dev/reference/methods/entity.presentDetails +func (api *Client) EntityPresentDetailsContext(ctx context.Context, params EntityPresentDetailsParameters) error { + values := url.Values{ + "token": {api.token}, + "trigger_id": {params.TriggerID}, + } + + // Add metadata if provided + if params.Metadata != nil { + metadataJSON, err := json.Marshal(params.Metadata) + if err != nil { + return err + } + values.Set("metadata", string(metadataJSON)) + } + + // Add error if provided + if params.Error != nil { + errorJSON, err := json.Marshal(params.Error) + if err != nil { + return err + } + values.Set("error", string(errorJSON)) + } + + // Add user auth parameters if provided + if params.UserAuthRequired { + values.Set("user_auth_required", "true") + } + if params.UserAuthURL != "" { + values.Set("user_auth_url", params.UserAuthURL) + } + if params.UserAuthMessage != "" { + values.Set("user_auth_message", params.UserAuthMessage) + } + + response := &EntityPresentDetailsResponse{} + err := api.postMethod(ctx, "entity.presentDetails", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// EntityPresentDetailsWithMetadata is a convenience method for presenting entity details with metadata +func (api *Client) EntityPresentDetailsWithMetadata(triggerID string, metadata EntityDetailsMetadata) error { + return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{ + TriggerID: triggerID, + Metadata: &metadata, + }) +} + +// EntityPresentDetailsWithError is a convenience method for presenting entity details with an error +func (api *Client) EntityPresentDetailsWithError(triggerID string, errPayload EntityDetailsError) error { + return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{ + TriggerID: triggerID, + Error: &errPayload, + }) +} + +// EntityPresentDetailsWithAuth is a convenience method for presenting entity details with authentication required +func (api *Client) EntityPresentDetailsWithAuth(triggerID, authURL, authMessage string) error { + return api.EntityPresentDetailsContext(context.Background(), EntityPresentDetailsParameters{ + TriggerID: triggerID, + UserAuthRequired: true, + UserAuthURL: authURL, + UserAuthMessage: authMessage, + }) +} diff --git a/vendor/github.com/slack-go/slack/files.go b/vendor/github.com/slack-go/slack/files.go index 810c476b77..974ba0a439 100644 --- a/vendor/github.com/slack-go/slack/files.go +++ b/vendor/github.com/slack-go/slack/files.go @@ -95,6 +95,9 @@ type File struct { From []EmailFileUserInfo `json:"from"` Cc []EmailFileUserInfo `json:"cc"` Headers EmailHeaders `json:"headers"` + + PlainText string `json:"plain_text"` + PreviewPlainText string `json:"preview_plain_text"` } type EmailFileUserInfo struct { @@ -126,24 +129,6 @@ type ShareFileInfo struct { TeamID string `json:"team_id"` } -// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. -// -// There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, -// or provide a local file path in File to upload it from your filesystem. -// -// Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy. -type FileUploadParameters struct { - File string - Content string - Reader io.Reader - Filetype string - Filename string - Title string - InitialComment string - Channels []string - ThreadTimestamp string -} - // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request type GetFilesParameters struct { User string @@ -167,7 +152,7 @@ type ListFilesParameters struct { Cursor string } -type UploadFileV2Parameters struct { +type UploadFileParameters struct { File string FileSize int Content string @@ -325,6 +310,7 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter if params.Page != DEFAULT_FILES_PAGE { values.Add("page", strconv.Itoa(params.Page)) } + //lint:ignore S1002 - we want to explicitly check against the constant if params.ShowHidden != DEFAULT_FILES_SHOW_HIDDEN { values.Add("show_files_hidden_by_limit", strconv.FormatBool(params.ShowHidden)) } @@ -375,73 +361,6 @@ func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParamet return response.Files, ¶ms, nil } -// UploadFile uploads a file. -// -// Deprecated: Use [Client.UploadFileV2] instead. -// -// Per Slack Changelog, specifically [https://api.slack.com/changelog#entry-march_2025_1](this entry), -// this will stop functioning on November 12, 2025. -// -// For more details, see: https://api.slack.com/methods/files.upload#markdown -func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { - return api.UploadFileContext(context.Background(), params) -} - -// UploadFileContext uploads a file and setting a custom context. -// -// Deprecated: Use [Client.UploadFileV2Context] instead. -// -// Per Slack Changelog, specifically [https://api.slack.com/changelog#entry-march_2025_1](this entry), -// this will stop functioning on November 12, 2025. -// -// For more details, see: https://api.slack.com/methods/files.upload#markdown -func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) { - // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More - // investigation needed, but for now this will do. - _, err = api.AuthTestContext(ctx) - if err != nil { - return nil, err - } - response := &fileResponseFull{} - values := url.Values{} - if params.Filetype != "" { - values.Add("filetype", params.Filetype) - } - if params.Filename != "" { - values.Add("filename", params.Filename) - } - if params.Title != "" { - values.Add("title", params.Title) - } - if params.InitialComment != "" { - values.Add("initial_comment", params.InitialComment) - } - if params.ThreadTimestamp != "" { - values.Add("thread_ts", params.ThreadTimestamp) - } - if len(params.Channels) != 0 { - values.Add("channels", strings.Join(params.Channels, ",")) - } - if params.Content != "" { - values.Add("content", params.Content) - values.Add("token", api.token) - err = api.postMethod(ctx, "files.upload", values, response) - } else if params.File != "" { - err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", api.token, values, response, api) - } else if params.Reader != nil { - if params.Filename == "" { - return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader") - } - err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", api.token, values, params.Reader, response, api) - } - - if err != nil { - return nil, err - } - - return &response.File, response.Err() -} - // DeleteFileComment deletes a file's comment. // For more details, see DeleteFileCommentContext documentation. func (api *Client) DeleteFileComment(commentID, fileID string) error { @@ -609,19 +528,19 @@ func (api *Client) CompleteUploadExternalContext(ctx context.Context, params Com return response, nil } -// UploadFileV2 uploads file to a given slack channel using 3 steps. -// For more details, see UploadFileV2Context documentation. -func (api *Client) UploadFileV2(params UploadFileV2Parameters) (*FileSummary, error) { - return api.UploadFileV2Context(context.Background(), params) +// UploadFile uploads file to a given slack channel using 3 steps. +// For more details, see UploadFileContext documentation. +func (api *Client) UploadFile(params UploadFileParameters) (*FileSummary, error) { + return api.UploadFileContext(context.Background(), params) } -// UploadFileV2Context uploads file to a given slack channel using 3 steps - +// UploadFileContext uploads file to a given slack channel using 3 steps - // 1. Get an upload URL using files.getUploadURLExternal API // 2. Send the file as a post to the URL provided by slack // 3. Complete the upload and share it to the specified channel using files.completeUploadExternal // // Slack Docs: https://api.slack.com/messaging/files#uploading_files -func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2Parameters) (file *FileSummary, err error) { +func (api *Client) UploadFileContext(ctx context.Context, params UploadFileParameters) (file *FileSummary, err error) { if params.Filename == "" { return nil, fmt.Errorf("file.upload.v2: filename cannot be empty") } @@ -636,7 +555,7 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P SnippetType: params.SnippetType, }) if err != nil { - return nil, err + return nil, fmt.Errorf("GetUploadURLExternal: %w", err) } err = api.UploadToURL(ctx, UploadToURLParameters{ @@ -647,7 +566,7 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P Filename: params.Filename, }) if err != nil { - return nil, err + return nil, fmt.Errorf("UploadToURL: %w", err) } c, err := api.CompleteUploadExternalContext(ctx, CompleteUploadExternalParameters{ @@ -661,7 +580,7 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P Blocks: params.Blocks, }) if err != nil { - return nil, err + return nil, fmt.Errorf("CompleteUploadExternal: %w", err) } if len(c.Files) != 1 { return nil, fmt.Errorf("file.upload.v2: something went wrong; received %d files instead of 1", len(c.Files)) diff --git a/vendor/github.com/slack-go/slack/function_execute.go b/vendor/github.com/slack-go/slack/function_execute.go index 4ec8f9f4c4..97bc7e150e 100644 --- a/vendor/github.com/slack-go/slack/function_execute.go +++ b/vendor/github.com/slack-go/slack/function_execute.go @@ -43,14 +43,13 @@ func (api *Client) FunctionCompleteSuccessContext(ctx context.Context, functionE option(r) } - endpoint := api.endpoint + "functions.completeSuccess" jsonData, err := json.Marshal(r) if err != nil { return err } response := &SlackResponse{} - if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + if err := api.postJSONMethod(ctx, "functions.completeSuccess", api.token, jsonData, response); err != nil { return err } @@ -74,14 +73,13 @@ func (api *Client) FunctionCompleteErrorContext(ctx context.Context, functionExe } r.Error = errorMessage - endpoint := api.endpoint + "functions.completeError" jsonData, err := json.Marshal(r) if err != nil { return err } response := &SlackResponse{} - if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + if err := api.postJSONMethod(ctx, "functions.completeError", api.token, jsonData, response); err != nil { return err } diff --git a/vendor/github.com/slack-go/slack/huddle.go b/vendor/github.com/slack-go/slack/huddle.go new file mode 100644 index 0000000000..7012364718 --- /dev/null +++ b/vendor/github.com/slack-go/slack/huddle.go @@ -0,0 +1,64 @@ +package slack + +// HuddleRoom represents a Slack huddle room as it appears in message events +// with subtype "huddle_thread". This is different from CallBlock which is used +// for external call integrations (Zoom, etc.). +type HuddleRoom struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + MediaServer string `json:"media_server,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + DateStart int64 `json:"date_start"` + DateEnd int64 `json:"date_end"` + Participants []string `json:"participants,omitempty"` + ParticipantHistory []string `json:"participant_history,omitempty"` + ParticipantsEvents map[string]HuddleParticipantEvent `json:"participants_events,omitempty"` + ParticipantsCameraOn []string `json:"participants_camera_on,omitempty"` + ParticipantsCameraOff []string `json:"participants_camera_off,omitempty"` + ParticipantsScreenshareOn []string `json:"participants_screenshare_on,omitempty"` + ParticipantsScreenshareOff []string `json:"participants_screenshare_off,omitempty"` + CanvasThreadTs string `json:"canvas_thread_ts,omitempty"` + ThreadRootTs string `json:"thread_root_ts,omitempty"` + Channels []string `json:"channels,omitempty"` + IsDMCall bool `json:"is_dm_call"` + WasRejected bool `json:"was_rejected"` + WasMissed bool `json:"was_missed"` + WasAccepted bool `json:"was_accepted"` + HasEnded bool `json:"has_ended"` + BackgroundID string `json:"background_id,omitempty"` + CanvasBackground string `json:"canvas_background,omitempty"` + IsPrewarmed bool `json:"is_prewarmed"` + IsScheduled bool `json:"is_scheduled"` + Recording *HuddleRecording `json:"recording,omitempty"` + Locale string `json:"locale,omitempty"` + AttachedFileIDs []string `json:"attached_file_ids,omitempty"` + MediaBackendType string `json:"media_backend_type,omitempty"` + DisplayID string `json:"display_id,omitempty"` + ExternalUniqueID string `json:"external_unique_id,omitempty"` + AppID string `json:"app_id,omitempty"` + CallFamily string `json:"call_family,omitempty"` + PendingInvitees map[string]any `json:"pending_invitees,omitempty"` + LastInviteStatusByUser map[string]any `json:"last_invite_status_by_user,omitempty"` + Knocks map[string]any `json:"knocks,omitempty"` + HuddleLink string `json:"huddle_link,omitempty"` +} + +// HuddleParticipantEvent tracks a participant's activity in a huddle. +type HuddleParticipantEvent struct { + UserTeam map[string]any `json:"user_team,omitempty"` + Joined bool `json:"joined"` + CameraOn bool `json:"camera_on"` + CameraOff bool `json:"camera_off"` + ScreenshareOn bool `json:"screenshare_on"` + ScreenshareOff bool `json:"screenshare_off"` +} + +// HuddleRecording contains recording status for a huddle. +type HuddleRecording struct { + CanRecordSummary string `json:"can_record_summary,omitempty"` + NoteTaking bool `json:"note_taking,omitempty"` + Summary bool `json:"summary,omitempty"` + SummaryStatus string `json:"summary_status,omitempty"` + Transcript bool `json:"transcript,omitempty"` + RecordingUser string `json:"recording_user,omitempty"` +} diff --git a/vendor/github.com/slack-go/slack/im.go b/vendor/github.com/slack-go/slack/im.go deleted file mode 100644 index 7c4bc25726..0000000000 --- a/vendor/github.com/slack-go/slack/im.go +++ /dev/null @@ -1,21 +0,0 @@ -package slack - -type imChannel struct { - ID string `json:"id"` -} - -type imResponseFull struct { - NoOp bool `json:"no_op"` - AlreadyClosed bool `json:"already_closed"` - AlreadyOpen bool `json:"already_open"` - Channel imChannel `json:"channel"` - IMs []IM `json:"ims"` - History - SlackResponse -} - -// IM contains information related to the Direct Message channel -type IM struct { - Conversation - IsUserDeleted bool `json:"is_user_deleted"` -} diff --git a/vendor/github.com/slack-go/slack/info.go b/vendor/github.com/slack-go/slack/info.go index a026ab4947..d5276f7577 100644 --- a/vendor/github.com/slack-go/slack/info.go +++ b/vendor/github.com/slack-go/slack/info.go @@ -431,7 +431,7 @@ type Team struct { Icons *Icons `json:"icon,omitempty"` } -// Icons XXX: needs further investigation +// Icons contains the image URLs for the team icons in various sizes type Icons struct { Image36 string `json:"image_36,omitempty"` Image48 string `json:"image_48,omitempty"` @@ -452,28 +452,3 @@ type infoResponseFull struct { Info SlackResponse } - -// GetBotByID is deprecated and returns nil -func (info Info) GetBotByID(botID string) *Bot { - return nil -} - -// GetUserByID is deprecated and returns nil -func (info Info) GetUserByID(userID string) *User { - return nil -} - -// GetChannelByID is deprecated and returns nil -func (info Info) GetChannelByID(channelID string) *Channel { - return nil -} - -// GetGroupByID is deprecated and returns nil -func (info Info) GetGroupByID(groupID string) *Group { - return nil -} - -// GetIMByID is deprecated and returns nil -func (info Info) GetIMByID(imID string) *IM { - return nil -} diff --git a/vendor/github.com/slack-go/slack/manifests.go b/vendor/github.com/slack-go/slack/manifests.go index 0a972a25dc..59f6a70bc9 100644 --- a/vendor/github.com/slack-go/slack/manifests.go +++ b/vendor/github.com/slack-go/slack/manifests.go @@ -187,11 +187,11 @@ type Display struct { // 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"` + 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 @@ -269,8 +269,10 @@ type OAuthConfig struct { // 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"` + Bot []string `json:"bot,omitempty" yaml:"bot,omitempty"` + User []string `json:"user,omitempty" yaml:"user,omitempty"` + BotOptional []string `json:"bot_optional,omitempty" yaml:"bot_optional,omitempty"` + UserOptional []string `json:"user_optional,omitempty" yaml:"user_optional,omitempty"` } // ManifestResponse is the response returned by the API for apps.manifest.x endpoints diff --git a/vendor/github.com/slack-go/slack/messages.go b/vendor/github.com/slack-go/slack/messages.go index c53809fdaa..332e15e996 100644 --- a/vendor/github.com/slack-go/slack/messages.go +++ b/vendor/github.com/slack-go/slack/messages.go @@ -88,6 +88,16 @@ type Msg struct { Icons *Icon `json:"icons,omitempty"` BotProfile *BotProfile `json:"bot_profile,omitempty"` + // These tend to be present in some of the messages, especially when triggered through + // a workflow. The API documentation is not clear about which ones are present in + // which messages, so we make them all optional. + // + // I'm adding them here for completeness but none of the Slack official libraries seem + // to support these fields. Be warned that they may be removed in future versions of + // the API, and that they may not be present in all messages. + TriggerID string `json:"trigger_id,omitempty"` + WorkflowID string `json:"workflow_id,omitempty"` + // channel_join, group_join Inviter string `json:"inviter,omitempty"` diff --git a/vendor/github.com/slack-go/slack/metadata.go b/vendor/github.com/slack-go/slack/metadata.go index a8c0650463..a1e34e990d 100644 --- a/vendor/github.com/slack-go/slack/metadata.go +++ b/vendor/github.com/slack-go/slack/metadata.go @@ -2,6 +2,37 @@ package slack // SlackMetadata https://api.slack.com/reference/metadata type SlackMetadata struct { - EventType string `json:"event_type"` - EventPayload map[string]interface{} `json:"event_payload"` + EventType string `json:"event_type"` + EventPayload map[string]any `json:"event_payload"` +} + +// Work Object entity type constants. +// See https://docs.slack.dev/messaging/work-objects/ +const ( + EntityTypeTask = "slack#/entities/task" + EntityTypeFile = "slack#/entities/file" + EntityTypeItem = "slack#/entities/item" + EntityTypeIncident = "slack#/entities/incident" + EntityTypeContentItem = "slack#/entities/content_item" +) + +// WorkObjectExternalRef represents an external reference for a Work Object +type WorkObjectExternalRef struct { + ID string `json:"id"` + Type string `json:"type,omitempty"` +} + +// WorkObjectEntity represents a single Work Object entity +type WorkObjectEntity struct { + AppUnfurlURL string `json:"app_unfurl_url,omitempty"` + URL string `json:"url"` + ExternalRef WorkObjectExternalRef `json:"external_ref"` + EntityType string `json:"entity_type"` + EntityPayload map[string]interface{} `json:"entity_payload"` +} + +// WorkObjectMetadata represents the metadata for Work Objects +// Used in chat.unfurl and chat.postMessage for Work Objects support +type WorkObjectMetadata struct { + Entities []WorkObjectEntity `json:"entities"` } diff --git a/vendor/github.com/slack-go/slack/misc.go b/vendor/github.com/slack-go/slack/misc.go index 48411ee190..46060a7204 100644 --- a/vendor/github.com/slack-go/slack/misc.go +++ b/vendor/github.com/slack-go/slack/misc.go @@ -19,7 +19,7 @@ import ( "time" ) -// Apps Manifest Create Response Errors ("/apps.manifest.create") +// AppsManifestCreateResponseError ("/apps.manifest.create") type AppsManifestCreateResponseError struct { Code string `json:"code,omitempty"` Message string `json:"message"` @@ -27,7 +27,7 @@ type AppsManifestCreateResponseError struct { RelatedComponent string `json:"related_component,omitempty"` } -// Conversations Invite Response Errors ("/conversations.invite") +// ConversationsInviteResponseError ("/conversations.invite") type ConversationsInviteResponseError struct { Error string `json:"error"` Ok bool `json:"ok"` @@ -69,7 +69,7 @@ func (e *SlackResponseErrors) UnmarshalJSON(data []byte) error { } // Try to determine the error type by checking for unique fields - var raw map[string]interface{} + var raw map[string]any if err := json.Unmarshal(data, &raw); err != nil { // If we can't unmarshal as object, try as string (fallback case) // @@ -114,10 +114,127 @@ func (e *SlackResponseErrors) UnmarshalJSON(data []byte) error { type SlackResponse struct { Ok bool `json:"ok"` Error string `json:"error"` + Warning string `json:"warning"` Errors []SlackResponseErrors `json:"errors,omitempty"` ResponseMetadata ResponseMetadata `json:"response_metadata"` } +// Warn returns warning information from the API response, or nil if there +// are no warnings. +func (t SlackResponse) Warn() *Warning { + if t.Warning == "" && len(t.ResponseMetadata.Warnings) == 0 { + return nil + } + return &Warning{ + Codes: strings.Split(t.Warning, ","), + Warnings: t.ResponseMetadata.Warnings, + } +} + +// warner is satisfied by any response type that can report warnings. +type warner interface { + Warn() *Warning +} + +// Warning provides warning information from the web API. +// https://docs.slack.dev/apis/web-api/#responses +type Warning struct { + Codes []string + Warnings []string +} + +// httpHeaderSetter is satisfied by response types that can store HTTP +// response headers. The response parser checks for this interface and +// injects headers before JSON decoding. +type httpHeaderSetter interface { + setHTTPResponseHeaders(http.Header) +} + +// responseHeaders is a mix-in for internal response types that need to +// capture HTTP response headers. Embedded in types like authTestResponseFull +// so the parser can store headers that are then propagated to the public +// response type (e.g. AuthTestResponse.Header). +type responseHeaders struct { + header http.Header +} + +func (r *responseHeaders) setHTTPResponseHeaders(h http.Header) { r.header = h } + +// KickUserFromConversationSlackResponse is a variant of SlackResponse that can handle the case where +// "errors" can be either an empty object {} or an array of errors. +// This addresses issue #1446 where conversations.kick endpoint returns {"ok":true,"errors":{}} +type KickUserFromConversationSlackResponse struct { + Ok bool `json:"ok"` + Error string `json:"error"` + Warning string `json:"warning"` + Errors []SlackResponseErrors `json:"-"` + ResponseMetadata ResponseMetadata `json:"response_metadata"` +} + +// UnmarshalJSON implements custom unmarshaling for KickUserFromConversationSlackResponse to handle +// the case where "errors" can be either an empty object {} or an array of errors +func (s *KickUserFromConversationSlackResponse) UnmarshalJSON(data []byte) error { + // First, unmarshal everything except errors + type Alias KickUserFromConversationSlackResponse + aux := &struct { + *Alias + ErrorsRaw json.RawMessage `json:"errors,omitempty"` + }{ + Alias: (*Alias)(s), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Handle the errors field + if len(aux.ErrorsRaw) > 0 { + // Check if it's an empty object by looking for just "{}" + trimmed := bytes.TrimSpace(aux.ErrorsRaw) + if bytes.Equal(trimmed, []byte("{}")) { + // Empty object, leave errors as nil/empty slice + s.Errors = nil + } else { + // Try to unmarshal as array of errors + var errors []SlackResponseErrors + if err := json.Unmarshal(aux.ErrorsRaw, &errors); err != nil { + return err + } + s.Errors = errors + } + } + + return nil +} + +// Warn returns warning information from the API response, or nil if there +// are no warnings. +func (s KickUserFromConversationSlackResponse) Warn() *Warning { + if s.Warning == "" && len(s.ResponseMetadata.Warnings) == 0 { + return nil + } + return &Warning{ + Codes: strings.Split(s.Warning, ","), + Warnings: s.ResponseMetadata.Warnings, + } +} + +// Err returns any API error present in the response. +func (s KickUserFromConversationSlackResponse) Err() error { + if s.Ok { + return nil + } + + // handle pure text based responses like chat.post + // which while they have a slack response in their data structure + // it doesn't actually get set during parsing. + if strings.TrimSpace(s.Error) == "" { + return nil + } + + return SlackErrorResponse{Err: s.Error, Errors: s.Errors, ResponseMetadata: s.ResponseMetadata} +} + func (t SlackResponse) Err() error { if t.Ok { return nil @@ -203,7 +320,7 @@ func formReq(ctx context.Context, endpoint string, values url.Values) (req *http return req, nil } -func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http.Request, err error) { +func jsonReq(ctx context.Context, endpoint string, body any) (req *http.Request, err error) { buffer := bytes.NewBuffer([]byte{}) if err = json.NewEncoder(buffer).Encode(body); err != nil { return nil, err @@ -217,7 +334,7 @@ func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http. return req, nil } -func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname, token string, values url.Values, intf interface{}, d Debug) error { +func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname, token string, values url.Values, intf any, d Debug) error { fullpath, err := filepath.Abs(fpath) if err != nil { return err @@ -231,7 +348,7 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, meth return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, token, values, file, intf, d) } -func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf interface{}, d Debug) error { +func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf any, d Debug) error { pipeReader, pipeWriter := io.Pipe() wr := multipart.NewWriter(pipeWriter) @@ -300,49 +417,54 @@ func createFormFields(mw *multipart.Writer, values url.Values) error { return nil } -func doPost(client httpClient, req *http.Request, parser responseParser, d Debug) error { +func doPost(client httpClient, req *http.Request, parser responseParser, d Debug) (http.Header, error) { resp, err := client.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() - err = checkStatusCode(resp, d) - if err != nil { - return err + if err = checkStatusCode(resp, d); err != nil { + return nil, err } - return parser(resp) + return resp.Header, parser(resp) } // post JSON. -func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d Debug) error { - reqBody := bytes.NewBuffer(json) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) +func postJSON(ctx context.Context, client httpClient, endpoint, token string, jsonBody []byte, intf any, d Debug) (http.Header, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonBody)) if err != nil { - return err + return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - + // allow retry client to re-send the request body on 429/5xx. + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(jsonBody)), nil + } return doPost(client, req, newJSONParser(intf), d) } // post a url encoded form. -func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d Debug) error { - reqBody := strings.NewReader(values.Encode()) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, reqBody) +func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf any, d Debug) (http.Header, error) { + body := values.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(body)) if err != nil { - return err + return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // allow retry client to re-send the request body on 429/5xx. + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(body)), nil + } return doPost(client, req, newJSONParser(intf), d) } -func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf interface{}, d Debug) error { +func getResource(ctx context.Context, client httpClient, endpoint, token string, values url.Values, intf any, d Debug) (http.Header, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { - return err + return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) @@ -352,9 +474,10 @@ func getResource(ctx context.Context, client httpClient, endpoint, token string, return doPost(client, req, newJSONParser(intf), d) } -func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d Debug) error { +func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf any, d Debug) error { endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix()) - return postForm(ctx, client, endpoint, values, intf, d) + _, err := postForm(ctx, client, endpoint, values, intf, d) + return err } func logResponse(resp *http.Response, d Debug) error { @@ -397,16 +520,19 @@ func checkStatusCode(resp *http.Response, d Debug) error { type responseParser func(*http.Response) error -func newJSONParser(dst interface{}) responseParser { +func newJSONParser(dst any) responseParser { return func(resp *http.Response) error { if dst == nil { return nil } + if hs, ok := dst.(httpHeaderSetter); ok { + hs.setHTTPResponseHeaders(resp.Header.Clone()) + } return json.NewDecoder(resp.Body).Decode(dst) } } -func newTextParser(dst interface{}) responseParser { +func newTextParser(dst any) responseParser { return func(resp *http.Response) error { if dst == nil { return nil @@ -425,7 +551,7 @@ func newTextParser(dst interface{}) responseParser { } } -func newContentTypeParser(dst interface{}) responseParser { +func newContentTypeParser(dst any) responseParser { return func(req *http.Response) (err error) { var ( ctype string @@ -439,6 +565,10 @@ func newContentTypeParser(dst interface{}) responseParser { case "application/json": return newJSONParser(dst)(req) default: + // newTextParser doesn't use dst, so capture headers here. + if hs, ok := dst.(httpHeaderSetter); ok { + hs.setHTTPResponseHeaders(req.Header.Clone()) + } return newTextParser(dst)(req) } } diff --git a/vendor/github.com/slack-go/slack/mise.toml b/vendor/github.com/slack-go/slack/mise.toml new file mode 100644 index 0000000000..912c02c984 --- /dev/null +++ b/vendor/github.com/slack-go/slack/mise.toml @@ -0,0 +1,4 @@ +[tools] +go = "1.25" +golangci-lint = "2.10.1" +"go:honnef.co/go/tools/cmd/staticcheck" = "2026.1" diff --git a/vendor/github.com/slack-go/slack/oauth.go b/vendor/github.com/slack-go/slack/oauth.go index 0c77eca407..f98284d23e 100644 --- a/vendor/github.com/slack-go/slack/oauth.go +++ b/vendor/github.com/slack-go/slack/oauth.go @@ -2,6 +2,9 @@ package slack import ( "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" "net/url" ) @@ -79,16 +82,47 @@ type OpenIDConnectResponse struct { SlackResponse } +type oauthConfig struct { + apiURL string + codeVerifier string +} + +// OAuthOption configures package-level OAuth functions. +type OAuthOption func(*oauthConfig) + +// OAuthOptionAPIURL overrides the default Slack API URL. Useful for testing. +func OAuthOptionAPIURL(url string) OAuthOption { + return func(c *oauthConfig) { c.apiURL = url } +} + +// OAuthOptionCodeVerifier sets the PKCE code_verifier for the OAuth token exchange. +// Use this when your authorization request included a code_challenge. +func OAuthOptionCodeVerifier(verifier string) OAuthOption { + return func(c *oauthConfig) { c.codeVerifier = verifier } +} + +func resolveOAuthConfig(opts []OAuthOption) oauthConfig { + c := oauthConfig{apiURL: APIURL} + for _, o := range opts { + o(&c) + } + return c +} + +func resolveOAuthAPIURL(opts []OAuthOption) string { + return resolveOAuthConfig(opts).apiURL +} + // GetOAuthToken retrieves an AccessToken. // For more details, see GetOAuthTokenContext documentation. -func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { - return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, err error) { + return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOAuthTokenContext retrieves an AccessToken with a custom context. // For more details, see GetOAuthResponseContext documentation. -func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) { - response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI) +func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, err error) { + response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI, opts...) if err != nil { return "", "", err } @@ -97,14 +131,14 @@ func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clie // GetBotOAuthToken retrieves top-level and bot AccessToken - https://api.slack.com/legacy/oauth#bot_user_access_tokens // For more details, see GetBotOAuthTokenContext documentation. -func GetBotOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, bot OAuthResponseBot, err error) { - return GetBotOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetBotOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, bot OAuthResponseBot, err error) { + return GetBotOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetBotOAuthTokenContext retrieves top-level and bot AccessToken with a custom context. // For more details, see GetOAuthResponseContext documentation. -func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, bot OAuthResponseBot, err error) { - response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI) +func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (accessToken string, scope string, bot OAuthResponseBot, err error) { + response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI, opts...) if err != nil { return "", "", OAuthResponseBot{}, err } @@ -113,13 +147,13 @@ func GetBotOAuthTokenContext(ctx context.Context, client httpClient, clientID, c // GetOAuthResponse retrieves OAuth response. // For more details, see GetOAuthResponseContext documentation. -func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { - return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthResponse, err error) { + return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOAuthResponseContext retrieves OAuth response with custom context. // Slack API docs: https://api.slack.com/methods/oauth.access -func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { +func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -127,7 +161,7 @@ func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, c "redirect_uri": {redirectURI}, } response := &OAuthResponse{} - if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"oauth.access", values, response, discard{}); err != nil { return nil, err } return response, response.Err() @@ -135,21 +169,28 @@ func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, c // GetOAuthV2Response gets a V2 OAuth access token response. // For more details, see GetOAuthV2ResponseContext documentation. -func GetOAuthV2Response(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) { - return GetOAuthV2ResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOAuthV2Response(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + return GetOAuthV2ResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOAuthV2ResponseContext with a context, gets a V2 OAuth access token response. +// For PKCE flows, pass OAuthOptionCodeVerifier and an empty clientSecret. // Slack API docs: https://api.slack.com/methods/oauth.v2.access -func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) { +func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + cfg := resolveOAuthConfig(opts) values := url.Values{ - "client_id": {clientID}, - "client_secret": {clientSecret}, - "code": {code}, - "redirect_uri": {redirectURI}, + "client_id": {clientID}, + "code": {code}, + "redirect_uri": {redirectURI}, + } + if clientSecret != "" { + values.Set("client_secret", clientSecret) + } + if cfg.codeVerifier != "" { + values.Set("code_verifier", cfg.codeVerifier) } response := &OAuthV2Response{} - if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, cfg.apiURL+"oauth.v2.access", values, response, discard{}); err != nil { return nil, err } return response, response.Err() @@ -157,21 +198,92 @@ func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, // RefreshOAuthV2Token with a context, gets a V2 OAuth access token response. // For more details, see RefreshOAuthV2TokenContext documentation. -func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) { - return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken) +func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken, opts...) } // RefreshOAuthV2TokenContext with a context, gets a V2 OAuth access token response. +// For PKCE public clients, pass an empty clientSecret. // Slack API docs: https://api.slack.com/methods/oauth.v2.access -func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) { +func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string, opts ...OAuthOption) (resp *OAuthV2Response, err error) { values := url.Values{ "client_id": {clientID}, - "client_secret": {clientSecret}, "refresh_token": {refreshToken}, "grant_type": {"refresh_token"}, } + if clientSecret != "" { + values.Set("client_secret", clientSecret) + } response := &OAuthV2Response{} - if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"oauth.v2.access", values, response, discard{}); err != nil { + return nil, err + } + return response, response.Err() +} + +// OpenIDConnectUserInfoResponse contains the response from openid.connect.userInfo. +// +// Some of the fields in the response to this method are preceded with https://slack.com/. +// These fields are Slack-specific, and they're from the perspective of Slack. +type OpenIDConnectUserInfoResponse struct { + Ok bool `json:"ok"` + + Sub string `json:"sub"` + + UserID string `json:"https://slack.com/user_id"` + TeamID string `json:"https://slack.com/team_id"` + + Email string `json:"email"` + EmailVerified bool `json:"email_verified"` + DateEmailVerified int64 `json:"date_email_verified"` + + Name string `json:"name"` + Picture string `json:"picture"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + Locale string `json:"locale"` + + TeamName string `json:"https://slack.com/team_name"` + TeamDomain string `json:"https://slack.com/team_domain"` + TeamImage34 string `json:"https://slack.com/team_image_34"` + TeamImage44 string `json:"https://slack.com/team_image_44"` + TeamImage68 string `json:"https://slack.com/team_image_68"` + TeamImage88 string `json:"https://slack.com/team_image_88"` + TeamImage102 string `json:"https://slack.com/team_image_102"` + TeamImage132 string `json:"https://slack.com/team_image_132"` + TeamImage230 string `json:"https://slack.com/team_image_230"` + + // `TeamImageDefault` indicates whether the image is a default one (true), or someone + // uploaded their own (false). + TeamImageDefault bool `json:"https://slack.com/team_image_default"` + + UserImage24 string `json:"https://slack.com/user_image_24"` + UserImage32 string `json:"https://slack.com/user_image_32"` + UserImage48 string `json:"https://slack.com/user_image_48"` + UserImage72 string `json:"https://slack.com/user_image_72"` + UserImage192 string `json:"https://slack.com/user_image_192"` + UserImage512 string `json:"https://slack.com/user_image_512"` + UserImage1024 string `json:"https://slack.com/user_image_1024"` + UserImageOriginal string `json:"https://slack.com/user_image_original"` + + SlackResponse +} + +// GetOpenIDConnectUserInfo returns the user info for the token. +// For more details, see GetOpenIDConnectUserInfoContext documentation. +func (api *Client) GetOpenIDConnectUserInfo() (*OpenIDConnectUserInfoResponse, error) { + return api.GetOpenIDConnectUserInfoContext(context.Background()) +} + +// GetOpenIDConnectUserInfoContext returns identity information about the user associated with the token. +// Slack API docs: https://docs.slack.dev/reference/methods/openid.connect.userInfo +func (api *Client) GetOpenIDConnectUserInfoContext(ctx context.Context) (*OpenIDConnectUserInfoResponse, error) { + values := url.Values{ + "token": {api.token}, + } + response := &OpenIDConnectUserInfoResponse{} + err := api.postMethod(ctx, "openid.connect.userInfo", values, response) + if err != nil { return nil, err } return response, response.Err() @@ -179,13 +291,13 @@ func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID // GetOpenIDConnectToken exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. // For more details, see GetOpenIDConnectTokenContext documentation. -func GetOpenIDConnectToken(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) { - return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +func GetOpenIDConnectToken(client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) { + return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } // GetOpenIDConnectTokenContext with a context, gets an access token for Sign in with Slack. // Slack API docs: https://api.slack.com/methods/openid.connect.token -func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) { +func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -193,8 +305,26 @@ func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, client "redirect_uri": {redirectURI}, } response := &OpenIDConnectResponse{} - if err = postForm(ctx, client, APIURL+"openid.connect.token", values, response, discard{}); err != nil { + if _, err = postForm(ctx, client, resolveOAuthAPIURL(opts)+"openid.connect.token", values, response, discard{}); err != nil { return nil, err } return response, response.Err() } + +// GenerateCodeVerifier creates a cryptographically random PKCE code verifier +// string suitable for use with OAuth 2.0 PKCE flows. The returned string is +// 43 characters of URL-safe base64 (no padding). +func GenerateCodeVerifier() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +// GenerateCodeChallenge creates a PKCE code challenge from a code verifier +// using the S256 method (SHA-256 hash, base64url-encoded without padding). +func GenerateCodeChallenge(verifier string) string { + h := sha256.Sum256([]byte(verifier)) + return base64.RawURLEncoding.EncodeToString(h[:]) +} diff --git a/vendor/github.com/slack-go/slack/reactions.go b/vendor/github.com/slack-go/slack/reactions.go index 240f5ba92b..18befa699b 100644 --- a/vendor/github.com/slack-go/slack/reactions.go +++ b/vendor/github.com/slack-go/slack/reactions.go @@ -33,44 +33,53 @@ func NewGetReactionsParameters() GetReactionsParameters { } type getReactionsResponseFull struct { - Type string - M struct { - Reactions []ItemReaction + Type string + Channel string `json:"channel,omitempty"` // channel is at the root level for message types + M struct { + *Message // message structure already contains reactions } `json:"message"` F struct { + *File Reactions []ItemReaction } `json:"file"` FC struct { + *Comment Reactions []ItemReaction } `json:"comment"` SlackResponse } -func (res getReactionsResponseFull) extractReactions() []ItemReaction { - switch res.Type { +func (res getReactionsResponseFull) extractReactedItem() ReactedItem { + item := ReactedItem{} + item.Type = res.Type + + switch item.Type { case "message": - return res.M.Reactions + item.Channel = res.Channel + item.Message = res.M.Message + item.Reactions = res.M.Reactions case "file": - return res.F.Reactions + item.File = res.F.File + item.Reactions = res.F.Reactions case "file_comment": - return res.FC.Reactions + item.File = res.F.File + item.Comment = res.FC.Comment + item.Reactions = res.FC.Reactions } - return []ItemReaction{} + return item } const ( - DEFAULT_REACTIONS_USER = "" - DEFAULT_REACTIONS_COUNT = 100 - DEFAULT_REACTIONS_PAGE = 1 - DEFAULT_REACTIONS_FULL = false + DEFAULT_REACTIONS_USER = "" + DEFAULT_REACTIONS_FULL = false ) // ListReactionsParameters is the inputs to find all reactions by a user. type ListReactionsParameters struct { User string TeamID string - Count int - Page int + Cursor string + Limit int Full bool } @@ -78,10 +87,8 @@ type ListReactionsParameters struct { // performed by a user. func NewListReactionsParameters() ListReactionsParameters { return ListReactionsParameters{ - User: DEFAULT_REACTIONS_USER, - Count: DEFAULT_REACTIONS_COUNT, - Page: DEFAULT_REACTIONS_PAGE, - Full: DEFAULT_REACTIONS_FULL, + User: DEFAULT_REACTIONS_USER, + Full: DEFAULT_REACTIONS_FULL, } } @@ -101,8 +108,8 @@ type listReactionsResponseFull struct { Reactions []ItemReaction } `json:"comment"` } - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { @@ -200,15 +207,15 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item return response.Err() } -// GetReactions returns details about the reactions on an item. +// GetReactions returns item and details about the reactions on an item. // For more details, see GetReactionsContext documentation. -func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) (ReactedItem, error) { return api.GetReactionsContext(context.Background(), item, params) } -// GetReactionsContext returns details about the reactions on an item with a custom context. +// GetReactionsContext returns item and details about the reactions on an item with a custom context. // Slack API docs: https://api.slack.com/methods/reactions.get -func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) (ReactedItem, error) { values := url.Values{ "token": {api.token}, } @@ -224,31 +231,31 @@ func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params if item.Comment != "" { values.Set("file_comment", item.Comment) } - if params.Full != DEFAULT_REACTIONS_FULL { + if params.Full { values.Set("full", strconv.FormatBool(params.Full)) } response := &getReactionsResponseFull{} if err := api.postMethod(ctx, "reactions.get", values, response); err != nil { - return nil, err + return ReactedItem{}, err } if err := response.Err(); err != nil { - return nil, err + return ReactedItem{}, err } - return response.extractReactions(), nil + return response.extractReactedItem(), nil } // ListReactions returns information about the items a user reacted to. // For more details, see ListReactionsContext documentation. -func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, string, error) { return api.ListReactionsContext(context.Background(), params) } // ListReactionsContext returns information about the items a user reacted to with a custom context. // Slack API docs: https://api.slack.com/methods/reactions.list -func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, string, error) { values := url.Values{ "token": {api.token}, } @@ -258,25 +265,25 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction if params.TeamID != "" { values.Add("team_id", params.TeamID) } - if params.Count != DEFAULT_REACTIONS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) } - if params.Page != DEFAULT_REACTIONS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Full != DEFAULT_REACTIONS_FULL { + if params.Full { values.Add("full", strconv.FormatBool(params.Full)) } response := &listReactionsResponseFull{} err := api.postMethod(ctx, "reactions.list", values, response) if err != nil { - return nil, nil, err + return nil, "", err } if err := response.Err(); err != nil { - return nil, nil, err + return nil, "", err } - return response.extractReactedItems(), &response.Paging, nil + return response.extractReactedItems(), response.ResponseMetadata.Cursor, nil } diff --git a/vendor/github.com/slack-go/slack/remotefiles.go b/vendor/github.com/slack-go/slack/remotefiles.go index 42639a178e..51d3e85658 100644 --- a/vendor/github.com/slack-go/slack/remotefiles.go +++ b/vendor/github.com/slack-go/slack/remotefiles.go @@ -56,6 +56,10 @@ type RemoteFile struct { // ExternalID is a user defined GUID, ExternalURL is where the remote file can be accessed, // and Title is the name of the file. // +// PreviewImage is a file path to upload as preview. PreviewImageReader is an io.Reader +// alternative. When using PreviewImageReader, set PreviewImageName to specify the filename +// with proper extension (e.g., "preview.jpg") to preserve image format. +// // For more details: // https://api.slack.com/methods/files.remote.add type RemoteFileParameters struct { @@ -66,6 +70,7 @@ type RemoteFileParameters struct { IndexableFileContents string PreviewImage string PreviewImageReader io.Reader + PreviewImageName string // filename for PreviewImageReader (e.g., "preview.jpg") } // ListRemoteFilesParameters contains arguments for the ListRemoteFiles method. @@ -124,7 +129,11 @@ func (api *Client) AddRemoteFileContext(ctx context.Context, params RemoteFilePa if params.PreviewImage != "" { err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", params.PreviewImage, "preview_image", api.token, values, response, api) } else if params.PreviewImageReader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", "preview.png", "preview_image", api.token, values, params.PreviewImageReader, response, api) + name := params.PreviewImageName + if name == "" { + name = "preview.png" + } + err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.add", name, "preview_image", api.token, values, params.PreviewImageReader, response, api) } else { response, err = api.remoteFileRequest(ctx, "files.remote.add", values) } @@ -214,7 +223,7 @@ func (api *Client) ShareRemoteFile(channels []string, externalID, fileID string) // ShareRemoteFileContext shares a remote file to channels with a custom context. // Slack API docs: https://api.slack.com/methods/files.remote.share func (api *Client) ShareRemoteFileContext(ctx context.Context, channels []string, externalID, fileID string) (file *RemoteFile, err error) { - if channels == nil || len(channels) == 0 { + if len(channels) == 0 { return nil, ErrParametersMissing } if fileID == "" && externalID == "" { @@ -266,8 +275,14 @@ func (api *Client) UpdateRemoteFileContext(ctx context.Context, fileID string, p if params.IndexableFileContents != "" { values.Add("indexable_file_contents", params.IndexableFileContents) } - if params.PreviewImageReader != nil { - err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", "preview.png", "preview_image", api.token, values, params.PreviewImageReader, response, api) + if params.PreviewImage != "" { + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", params.PreviewImage, "preview_image", api.token, values, response, api) + } else if params.PreviewImageReader != nil { + name := params.PreviewImageName + if name == "" { + name = "preview.png" + } + err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", name, "preview_image", api.token, values, params.PreviewImageReader, response, api) } else { values.Add("token", api.token) response, err = api.remoteFileRequest(ctx, "files.remote.update", values) diff --git a/vendor/github.com/slack-go/slack/retry.go b/vendor/github.com/slack-go/slack/retry.go new file mode 100644 index 0000000000..d2f62117dc --- /dev/null +++ b/vendor/github.com/slack-go/slack/retry.go @@ -0,0 +1,307 @@ +package slack + +// Optional HTTP retries improve reliability when Slack is busy or the network is flaky. +// Retries are off by default; use OptionRetry or OptionRetryConfig to turn them on. +// +// Retry behavior is driven by pluggable handlers (parity with the Python SDK: +// https://github.com/slackapi/python-slack-sdk). When Handlers is nil, only rate limit +// (429) is retried (NewRateLimitErrorRetryHandler). Use +// AllBuiltinRetryHandlers(cfg) for connection + 429; ConnectionOnlyRetryHandlers(cfg) for +// connection-only; add NewServerErrorRetryHandler(cfg) to also retry 5xx. +// +// File uploads and other requests that stream the body cannot be retried (the body is sent once). +// Regular API calls (form or JSON) are retried when a handler matches (429, 5xx, or connection error). + +import ( + "context" + "errors" + "fmt" + "io" + "math/rand/v2" + "net/http" + "strconv" + "strings" + "time" + + "github.com/slack-go/slack/internal/backoff" +) + +// minRetryAfter429 is the minimum wait before retrying after 429 when Retry-After is missing +// or zero, to avoid tight retry loops when using a partial RetryConfig. +const minRetryAfter429 = time.Second + +// RetryState holds the current attempt and max retries; passed to handlers. +// Backoff is set by retryClient for use by handlers that want exponential backoff (e.g. connection, server error). +// Handlers may call Backoff.Duration() when retrying; each call advances the backoff for the next retry. +type RetryState struct { + Attempt int // current attempt (0-based) + MaxRetries int + Backoff *backoff.Backoff // optional; used by connection/server handlers for exponential backoff +} + +// RetryHandler decides whether to retry a request and how long to wait. +// The first handler that returns (true, wait) wins. resp may be nil (connection failure); err may be nil (got response). +type RetryHandler interface { + ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (retry bool, wait time.Duration) +} + +// RetryConfig configures HTTP retry behavior. +// When MaxRetries is 0, retries are disabled. +// If Handlers is nil, only rate limit (429) is retried (see DefaultRetryHandlers). +type RetryConfig struct { + // MaxRetries is the maximum number of retry attempts (0 = no retries, 1 = one retry, etc.). + MaxRetries int + // Handlers is the list of handlers to consult; nil means 429 only (DefaultRetryHandlers). + Handlers []RetryHandler + // RetryAfterDuration is used for 429 when the Retry-After header is missing or invalid. + RetryAfterDuration time.Duration + // RetryAfterJitter adds random jitter [0, RetryAfterJitter] to 429 wait to avoid thundering herd (0 = no jitter). + RetryAfterJitter time.Duration + // BackoffInitial is the initial backoff for 5xx and connection errors. + BackoffInitial time.Duration + // BackoffMax caps the backoff duration. + BackoffMax time.Duration + // BackoffJitter adds random jitter [0, BackoffJitter] to backoff to avoid thundering herd (0 to disable). + BackoffJitter time.Duration +} + +// DefaultRetryConfig returns a retry config with sensible defaults. +func DefaultRetryConfig() RetryConfig { + return RetryConfig{ + MaxRetries: 3, + RetryAfterDuration: 60 * time.Second, + RetryAfterJitter: 1 * time.Second, + BackoffInitial: 100 * time.Millisecond, + BackoffMax: 30 * time.Second, + BackoffJitter: 50 * time.Millisecond, + } +} + +// connectionErrorRetryHandler retries on connection errors (e.g. connection reset). +type connectionErrorRetryHandler struct{} + +// NewConnectionErrorRetryHandler returns a handler that retries on connection errors. +func NewConnectionErrorRetryHandler() RetryHandler { + return &connectionErrorRetryHandler{} +} + +func (h *connectionErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) { + if err == nil || resp != nil { + return false, 0 + } + if !isRetryableConnError(err) || !requestRetryable(req) { + return false, 0 + } + if state.Attempt >= state.MaxRetries { + return false, 0 + } + // Backoff is always set by retryClient.Do(). + wait := state.Backoff.Duration() + return true, wait +} + +// rateLimitErrorRetryHandler retries on 429 Too Many Requests using Retry-After or config. +type rateLimitErrorRetryHandler struct { + cfg RetryConfig +} + +// NewRateLimitErrorRetryHandler returns a handler that retries on 429. +func NewRateLimitErrorRetryHandler(cfg RetryConfig) RetryHandler { + return &rateLimitErrorRetryHandler{cfg: cfg} +} + +func (h *rateLimitErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) { + if resp == nil || resp.StatusCode != http.StatusTooManyRequests || !requestRetryable(req) { + return false, 0 + } + if state.Attempt >= state.MaxRetries { + return false, 0 + } + dur := h.cfg.RetryAfterDuration + if s := resp.Header.Get("Retry-After"); s != "" { + // Parsing only integer seconds is appropriate for Slack (API sends seconds; RFC 7231 also allows HTTP-date). + if sec, parseErr := strconv.ParseInt(strings.TrimSpace(s), 10, 64); parseErr == nil { + if sec > 0 { + dur = time.Duration(sec) * time.Second + } else { + dur = minRetryAfter429 // Retry-After: 0 means use minimum delay + } + } + } + dur = max(dur, minRetryAfter429) + if h.cfg.RetryAfterJitter > 0 { + dur += time.Duration(rand.IntN(int(h.cfg.RetryAfterJitter))) + } + return true, dur +} + +// serverErrorRetryHandler retries on 5xx server errors (opt-in). +type serverErrorRetryHandler struct{} + +// NewServerErrorRetryHandler returns a handler that retries on 5xx. Opt-in; not in DefaultRetryHandlers, ConnectionOnlyRetryHandlers, or AllBuiltinRetryHandlers. +func NewServerErrorRetryHandler(cfg RetryConfig) RetryHandler { + return &serverErrorRetryHandler{} +} + +func (h *serverErrorRetryHandler) ShouldRetry(state *RetryState, req *http.Request, resp *http.Response, err error) (bool, time.Duration) { + if resp == nil || resp.StatusCode < http.StatusInternalServerError || !requestRetryable(req) { + return false, 0 + } + if state.Attempt >= state.MaxRetries { + return false, 0 + } + // Backoff is always set by retryClient.Do(). + wait := state.Backoff.Duration() + return true, wait +} + +// DefaultRetryHandlers returns the default handler when retries are on: rate limit (429) only. +// Used when Handlers is nil. Use AllBuiltinRetryHandlers(cfg) for connection + 429. +func DefaultRetryHandlers(cfg RetryConfig) []RetryHandler { + return []RetryHandler{NewRateLimitErrorRetryHandler(cfg)} +} + +// ConnectionOnlyRetryHandlers returns connection-only handlers (no 429 retries). +func ConnectionOnlyRetryHandlers() []RetryHandler { + return []RetryHandler{NewConnectionErrorRetryHandler()} +} + +// AllBuiltinRetryHandlers returns connection + rate limit (429) handlers; no 5xx. +func AllBuiltinRetryHandlers(cfg RetryConfig) []RetryHandler { + return []RetryHandler{ + NewConnectionErrorRetryHandler(), + NewRateLimitErrorRetryHandler(cfg), + } +} + +// retryClient wraps an httpClient and retries according to config.Handlers. +type retryClient struct { + client httpClient + config RetryConfig + debug Debug // optional; when set and Debug() is true, retries are logged +} + +var _ httpClient = (*retryClient)(nil) + +// handlers returns the list of retry handlers. Empty Handlers slice is treated like nil (default 429 only). +func (c *retryClient) handlers() []RetryHandler { + if len(c.config.Handlers) > 0 { + return c.config.Handlers + } + return DefaultRetryHandlers(c.config) +} + +func (c *retryClient) logRetry(attempt int, reason string, detail any) { + if c.debug == nil || !c.debug.Debug() { + return + } + c.debug.Debugf("slack retry: %s (attempt %d/%d), detail: %v", reason, attempt+1, c.config.MaxRetries+1, detail) +} + +// requestRetryable reports whether the request can be safely retried: either it has no body +// (e.g. GET) or the body can be replayed via GetBody (e.g. POST with GetBody set). +// Requests with a non-nil body and nil GetBody (e.g. streaming uploads) must not be retried. +func requestRetryable(req *http.Request) bool { + return req.Body == nil || req.GetBody != nil +} + +// sleepWithContext sleeps for up to d, or until ctx is done. Returns true if the full duration +// elapsed, false if ctx was cancelled (caller should return ctx.Err()). +func sleepWithContext(ctx context.Context, d time.Duration) bool { + if d <= 0 { + return true + } + timer := time.NewTimer(d) + defer timer.Stop() + select { + case <-timer.C: + return true + case <-ctx.Done(): + return false + } +} + +func (c *retryClient) Do(req *http.Request) (*http.Response, error) { + handlers := c.handlers() + bo := &backoff.Backoff{ + Initial: c.config.BackoffInitial, + Max: c.config.BackoffMax, + Jitter: c.config.BackoffJitter, + } + var lastErr error + maxAttempts := c.config.MaxRetries + 1 + + for attempt := range maxAttempts { + state := &RetryState{Attempt: attempt, MaxRetries: c.config.MaxRetries, Backoff: bo} + + // Rewind body for retries (POST/PUT with GetBody). + if attempt > 0 && req.GetBody != nil { + newBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = newBody + } + + resp, err := c.client.Do(req) + if err != nil { + lastErr = err + } + + // Success: got response and status is not 429/5xx. + if err == nil && resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < http.StatusInternalServerError { + return resp, nil + } + + // No retries left or request body cannot be replayed — return now. + if attempt >= c.config.MaxRetries || !requestRetryable(req) { + if err != nil { + return nil, err + } + return resp, nil + } + + // First handler that wants to retry wins. + var wait time.Duration + retry := false + for _, h := range handlers { + if r, w := h.ShouldRetry(state, req, resp, err); r { + retry, wait = true, w + break + } + } + if !retry { + if err != nil { + return nil, err + } + return resp, nil + } + + // Log, discard response body if we have one, sleep, then next attempt. + if err != nil { + c.logRetry(attempt, "connection error", err) + } else { + reason := fmt.Sprintf("%d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + c.logRetry(attempt, reason, wait) + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } + if !sleepWithContext(req.Context(), wait) { + return nil, req.Context().Err() + } + } + + return nil, lastErr +} + +func isRetryableConnError(err error) bool { + for ; err != nil; err = errors.Unwrap(err) { + s := err.Error() + if strings.Contains(s, "connection reset") || + strings.Contains(s, "connection refused") || + strings.Contains(s, "EOF") { + return true + } + } + return false +} diff --git a/vendor/github.com/slack-go/slack/security.go b/vendor/github.com/slack-go/slack/security.go index 4510352937..8124c2c09e 100644 --- a/vendor/github.com/slack-go/slack/security.go +++ b/vendor/github.com/slack-go/slack/security.go @@ -30,6 +30,10 @@ func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifi bsignature []byte ) + if secret == "" { + return SecretsVerifier{}, ErrInvalidConfiguration + } + signature := header.Get(hSignature) stimestamp := header.Get(hTimestamp) @@ -42,7 +46,7 @@ func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifi } hash := hmac.New(sha256.New, []byte(secret)) - if _, err = hash.Write([]byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil { + if _, err = fmt.Fprintf(hash, "v0:%s:", stimestamp); err != nil { return SecretsVerifier{}, err } @@ -95,7 +99,7 @@ func (v SecretsVerifier) Ensure() error { if v.d != nil && v.d.Debug() { v.d.Debugln(fmt.Sprintf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed))) } - return fmt.Errorf("Computed unexpected signature of: %s", hex.EncodeToString(computed)) + return fmt.Errorf("computed unexpected signature of: %s", hex.EncodeToString(computed)) } func abs64(n int64) int64 { diff --git a/vendor/github.com/slack-go/slack/slack.go b/vendor/github.com/slack-go/slack/slack.go index 756106fe4a..45b9cd3bcb 100644 --- a/vendor/github.com/slack-go/slack/slack.go +++ b/vendor/github.com/slack-go/slack/slack.go @@ -12,6 +12,8 @@ import ( const ( // APIURL of the slack api. APIURL = "https://slack.com/api/" + // AuditAPIURL is the base URL for the Audit Logs API. + AuditAPIURL = "https://api.slack.com/" // WEBAPIURLFormat ... WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d" ) @@ -44,27 +46,32 @@ type AuthTestResponse struct { TeamID string `json:"team_id"` UserID string `json:"user_id"` // EnterpriseID is only returned when an enterprise id present - EnterpriseID string `json:"enterprise_id,omitempty"` - BotID string `json:"bot_id"` + EnterpriseID string `json:"enterprise_id,omitempty"` + BotID string `json:"bot_id"` + Header http.Header `json:"-"` } type authTestResponseFull struct { SlackResponse AuthTestResponse + responseHeaders } -// Client for the slack api. type ParamOption func(*url.Values) +// Client for the slack api. type Client struct { token string appLevelToken string configToken string configRefreshToken string endpoint string + auditEndpoint string debug bool log ilogger httpclient httpClient + onWarning func(path string, request any, w *Warning) + onResponseHeaders func(path string, headers http.Header) } // Option defines an option for a Client @@ -91,11 +98,44 @@ func OptionLog(l logger) func(*Client) { } } +// OptionOnWarning sets a callback invoked whenever an API response contains +// warnings. The callback receives the API method path (e.g. +// "conversations.join"), the request payload ([url.Values] for form-encoded +// requests or []byte for JSON requests), and a [Warning] with the warning +// codes and messages. +// +// Example: +// +// api := slack.New("YOUR_TOKEN", +// slack.OptionOnWarning(func(path string, request any, w *slack.Warning) { +// log.Printf("slack warnings for %s: codes=%v warnings=%v", path, w.Codes, w.Warnings) +// }), +// ) +func OptionOnWarning(fn func(path string, request any, w *Warning)) func(*Client) { + return func(c *Client) { + c.onWarning = fn + } +} + +// OptionOnResponseHeaders sets a callback invoked after every API request +// with the API method path and the HTTP response headers. This allows +// accessing headers like X-OAuth-Scopes and X-Ratelimit-* for any request. +func OptionOnResponseHeaders(fn func(path string, headers http.Header)) func(*Client) { + return func(c *Client) { + c.onResponseHeaders = fn + } +} + // OptionAPIURL set the url for the client. only useful for testing. func OptionAPIURL(u string) func(*Client) { return func(c *Client) { c.endpoint = u } } +// OptionAuditAPIURL set the url for the Audit Logs API. only useful for testing. +func OptionAuditAPIURL(u string) func(*Client) { + return func(c *Client) { c.auditEndpoint = u } +} + // OptionAppLevelToken sets an app-level token for the client. func OptionAppLevelToken(token string) func(*Client) { return func(c *Client) { c.appLevelToken = token } @@ -111,13 +151,47 @@ func OptionConfigRefreshToken(token string) func(*Client) { return func(c *Client) { c.configRefreshToken = token } } +// OptionRetry enables HTTP retries for rate limit (429) only; 5xx and connection errors are not retried. +// Uses DefaultRetryHandlers. Use OptionRetryConfig with AllBuiltinRetryHandlers for connection + 429. +// If maxRetries is zero or negative, the client is not wrapped (no retries). +// When using a custom HTTP client, pass OptionRetry after OptionHTTPClient so the retry wrapper is applied to it. +func OptionRetry(maxRetries int) func(*Client) { + return func(c *Client) { + if maxRetries <= 0 { + return + } + cfg := DefaultRetryConfig() + cfg.MaxRetries = maxRetries + cfg.Handlers = DefaultRetryHandlers(cfg) + c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c} + } +} + +// OptionRetryConfig enables HTTP retries with a custom config. +// If config.MaxRetries is 0, the client is not wrapped (no retries). +// If config.Handlers is nil, DefaultRetryHandlers(cfg) is used (429 only). +// When using a custom HTTP client, pass OptionRetryConfig after OptionHTTPClient so the retry wrapper is applied to it. +func OptionRetryConfig(config RetryConfig) func(*Client) { + return func(c *Client) { + if config.MaxRetries <= 0 { + return + } + cfg := config + if cfg.Handlers == nil { + cfg.Handlers = DefaultRetryHandlers(cfg) + } + c.httpclient = &retryClient{client: c.httpclient, config: cfg, debug: c} + } +} + // New builds a slack client from the provided token and options. func New(token string, options ...Option) *Client { s := &Client{ - token: token, - endpoint: APIURL, - httpclient: &http.Client{}, - log: log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile), + token: token, + endpoint: APIURL, + auditEndpoint: AuditAPIURL, + httpclient: &http.Client{}, + log: log.New(os.Stderr, "slack-go/slack", log.LstdFlags|log.Lshortfile), } for _, opt := range options { @@ -141,18 +215,19 @@ func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestRespo return nil, err } + responseFull.AuthTestResponse.Header = responseFull.responseHeaders.header return &responseFull.AuthTestResponse, responseFull.Err() } // Debugf print a formatted debug line. -func (api *Client) Debugf(format string, v ...interface{}) { +func (api *Client) Debugf(format string, v ...any) { if api.debug { api.log.Output(2, fmt.Sprintf(format, v...)) } } // Debugln print a debug line. -func (api *Client) Debugln(v ...interface{}) { +func (api *Client) Debugln(v ...any) { if api.debug { api.log.Output(2, fmt.Sprintln(v...)) } @@ -164,11 +239,42 @@ func (api *Client) Debug() bool { } // post to a slack web method. -func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error { - return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api) +func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf any) error { + headers, err := postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api) + api.checkWarnings(intf, path, values) + api.fireResponseHeaders(path, headers) + return err } // get a slack web method. -func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf interface{}) error { - return getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api) +func (api *Client) getMethod(ctx context.Context, path string, token string, values url.Values, intf any) error { + headers, err := getResource(ctx, api.httpclient, api.endpoint+path, token, values, intf, api) + api.checkWarnings(intf, path, values) + api.fireResponseHeaders(path, headers) + return err +} + +// postJSONMethod posts JSON to a slack web method. +func (api *Client) postJSONMethod(ctx context.Context, path string, token string, jsonBody []byte, intf any) error { + headers, err := postJSON(ctx, api.httpclient, api.endpoint+path, token, jsonBody, intf, api) + api.checkWarnings(intf, path, jsonBody) + api.fireResponseHeaders(path, headers) + return err +} + +func (api *Client) checkWarnings(intf any, path string, request any) { + if api.onWarning == nil { + return + } + if w, ok := intf.(warner); ok { + if warning := w.Warn(); warning != nil { + api.onWarning(path, request, warning) + } + } +} + +func (api *Client) fireResponseHeaders(path string, headers http.Header) { + if api.onResponseHeaders != nil && headers != nil { + api.onResponseHeaders(path, headers) + } } diff --git a/vendor/github.com/slack-go/slack/socket_mode.go b/vendor/github.com/slack-go/slack/socket_mode.go index ea9ea3b7a0..9498cdf734 100644 --- a/vendor/github.com/slack-go/slack/socket_mode.go +++ b/vendor/github.com/slack-go/slack/socket_mode.go @@ -22,7 +22,7 @@ type openResponseFull struct { // To have a fully managed Socket Mode connection, use `socketmode.New()`, and call `Run()` on it. func (api *Client) StartSocketModeContext(ctx context.Context) (info *SocketModeConnection, websocketURL string, err error) { response := &openResponseFull{} - err = postJSON(ctx, api.httpclient, api.endpoint+"apps.connections.open", api.appLevelToken, nil, response, api) + err = api.postJSONMethod(ctx, "apps.connections.open", api.appLevelToken, nil, response) if err != nil { return nil, "", err } @@ -36,7 +36,9 @@ func (api *Client) StartSocketModeContext(ctx context.Context) (info *SocketMode // time significantly shorter (360 seconds). if api.debug { u, _ := url.Parse(response.SocketModeConnection.URL) - u.Query().Add("debug_reconnects", "true") + q := u.Query() + q.Set("debug_reconnects", "true") + u.RawQuery = q.Encode() response.SocketModeConnection.URL = u.String() } return &response.SocketModeConnection, response.SocketModeConnection.URL, response.Err() diff --git a/vendor/github.com/slack-go/slack/stars.go b/vendor/github.com/slack-go/slack/stars.go index 51926854e8..0adb28c536 100644 --- a/vendor/github.com/slack-go/slack/stars.go +++ b/vendor/github.com/slack-go/slack/stars.go @@ -8,31 +8,28 @@ import ( ) const ( - DEFAULT_STARS_USER = "" - DEFAULT_STARS_COUNT = 100 - DEFAULT_STARS_PAGE = 1 + DEFAULT_STARS_USER = "" ) type StarsParameters struct { - User string - Count int - Page int + User string + Cursor string + Limit int + TeamID string } type StarredItem Item type listResponseFull struct { - Items []Item `json:"items"` - Paging `json:"paging"` + Items []Item `json:"items"` SlackResponse + ResponseMetadata `json:"response_metadata"` } // NewStarsParameters initialises StarsParameters with default values func NewStarsParameters() StarsParameters { return StarsParameters{ - User: DEFAULT_STARS_USER, - Count: DEFAULT_STARS_COUNT, - Page: DEFAULT_STARS_PAGE, + User: DEFAULT_STARS_USER, } } @@ -100,37 +97,40 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I // ListStars returns information about the stars a user added. // For more information see the ListStarsContext documentation. -func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { +func (api *Client) ListStars(params StarsParameters) ([]Item, string, error) { return api.ListStarsContext(context.Background(), params) } // ListStarsContext returns information about the stars a user added with a custom context. // Slack API docs: https://api.slack.com/methods/stars.list -func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) { +func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, string, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_STARS_USER { values.Add("user", params.User) } - if params.Count != DEFAULT_STARS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Page != DEFAULT_STARS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.TeamID != "" { + values.Add("team_id", params.TeamID) } response := &listResponseFull{} err := api.postMethod(ctx, "stars.list", values, response) if err != nil { - return nil, nil, err + return nil, "", err } if err := response.Err(); err != nil { - return nil, nil, err + return nil, "", err } - return response.Items, &response.Paging, nil + return response.Items, response.ResponseMetadata.Cursor, nil } // GetStarred returns a list of StarredItem items. @@ -139,31 +139,31 @@ func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) // be looking at according to what is in the Type: // // for _, item := range items { -// switch c.Type { -// case "file_comment": -// log.Println(c.Comment) -// case "file": -// ... +// switch c.Type { +// case "file_comment": +// log.Println(c.Comment) +// case "file": +// ... // } // // This function still exists to maintain backwards compatibility. // I exposed it as returning []StarredItem, so it shall stay as StarredItem. -func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) { +func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, string, error) { return api.GetStarredContext(context.Background(), params) } // GetStarredContext returns a list of StarredItem items with a custom context // For more details see GetStarred -func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, *Paging, error) { - items, paging, err := api.ListStarsContext(ctx, params) +func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, string, error) { + items, nextCursor, err := api.ListStarsContext(ctx, params) if err != nil { - return nil, nil, err + return nil, "", err } starredItems := make([]StarredItem, len(items)) for i, item := range items { starredItems[i] = StarredItem(item) } - return starredItems, paging, nil + return starredItems, nextCursor, nil } type listResponsePaginated struct { diff --git a/vendor/github.com/slack-go/slack/team.go b/vendor/github.com/slack-go/slack/team.go index 35b6992716..55364a9b41 100644 --- a/vendor/github.com/slack-go/slack/team.go +++ b/vendor/github.com/slack-go/slack/team.go @@ -6,11 +6,6 @@ import ( "strconv" ) -const ( - DEFAULT_LOGINS_COUNT = 100 - DEFAULT_LOGINS_PAGE = 1 -) - type TeamResponse struct { Team TeamInfo `json:"team"` SlackResponse @@ -46,8 +41,8 @@ type TeamProfileField struct { type LoginResponse struct { Logins []Login `json:"logins"` - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } type Login struct { @@ -75,16 +70,14 @@ type BillingActive struct { // AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request type AccessLogParameters struct { TeamID string - Count int - Page int + Cursor string + Limit int + Before int } // NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set func NewAccessLogParameters() AccessLogParameters { - return AccessLogParameters{ - Count: DEFAULT_LOGINS_COUNT, - Page: DEFAULT_LOGINS_PAGE, - } + return AccessLogParameters{} } func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) { @@ -193,31 +186,34 @@ func (api *Client) GetTeamProfileContext(ctx context.Context, teamID ...string) // GetAccessLogs retrieves a page of logins according to the parameters given. // For more information see the GetAccessLogsContext documentation. -func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) { +func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, string, error) { return api.GetAccessLogsContext(context.Background(), params) } // GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context. // Slack API docs: https://api.slack.com/methods/team.accessLogs -func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) { +func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, string, error) { values := url.Values{ "token": {api.token}, } if params.TeamID != "" { values.Add("team_id", params.TeamID) } - if params.Count != DEFAULT_LOGINS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Page != DEFAULT_LOGINS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if params.Before != 0 { + values.Add("before", strconv.Itoa(params.Before)) } response, err := api.accessLogsRequest(ctx, "team.accessLogs", values) if err != nil { - return nil, nil, err + return nil, "", err } - return response.Logins, &response.Paging, nil + return response.Logins, response.ResponseMetadata.Cursor, nil } type GetBillableInfoParams struct { diff --git a/vendor/github.com/slack-go/slack/usergroups.go b/vendor/github.com/slack-go/slack/usergroups.go index 616bbf1580..b5c5454570 100644 --- a/vendor/github.com/slack-go/slack/usergroups.go +++ b/vendor/github.com/slack-go/slack/usergroups.go @@ -568,3 +568,17 @@ func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup } return response.UserGroup, nil } + +// UpdateUserGroupMembersList updates the members of an existing user group, +// accepting a slice of user IDs. This is a convenience wrapper around +// UpdateUserGroupMembers for use with APIs that return []string (e.g. +// GetUserGroupMembers). +func (api *Client) UpdateUserGroupMembersList(userGroup string, members []string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + return api.UpdateUserGroupMembersContext(context.Background(), userGroup, strings.Join(members, ","), options...) +} + +// UpdateUserGroupMembersListContext updates the members of an existing user +// group with a custom context, accepting a slice of user IDs. +func (api *Client) UpdateUserGroupMembersListContext(ctx context.Context, userGroup string, members []string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + return api.UpdateUserGroupMembersContext(ctx, userGroup, strings.Join(members, ","), options...) +} diff --git a/vendor/github.com/slack-go/slack/users.go b/vendor/github.com/slack-go/slack/users.go index 91a9a4c7be..541baae786 100644 --- a/vendor/github.com/slack-go/slack/users.go +++ b/vendor/github.com/slack-go/slack/users.go @@ -17,32 +17,41 @@ const ( // UserProfile contains all the information details of a given user type UserProfile struct { - FirstName string `json:"first_name,omitempty"` - LastName string `json:"last_name,omitempty"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - DisplayName string `json:"display_name"` - DisplayNameNormalized string `json:"display_name_normalized"` - AvatarHash string `json:"avatar_hash"` - Email string `json:"email,omitempty"` - Skype string `json:"skyp,omitempty"` - Phone string `json:"phone,omitempty"` - Image24 string `json:"image_24"` - Image32 string `json:"image_32"` - Image48 string `json:"image_48"` - Image72 string `json:"image_72"` - Image192 string `json:"image_192"` - Image512 string `json:"image_512"` - ImageOriginal string `json:"image_original,omitempty"` - Title string `json:"title,omitempty"` - BotID string `json:"bot_id,omitempty"` - ApiAppID string `json:"api_app_id,omitempty"` - StatusText string `json:"status_text,omitempty"` - StatusEmoji string `json:"status_emoji,omitempty"` - StatusEmojiDisplayInfo []UserProfileStatusEmojiDisplayInfo `json:"status_emoji_display_info,omitempty"` - StatusExpiration int `json:"status_expiration,omitempty"` - Team string `json:"team"` - Fields UserProfileCustomFields `json:"fields,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + DisplayName string `json:"display_name"` + DisplayNameNormalized string `json:"display_name_normalized"` + Pronouns string `json:"pronouns,omitempty"` + AvatarHash string `json:"avatar_hash"` + Email string `json:"email,omitempty"` + Skype string `json:"skype,omitempty"` + Phone string `json:"phone,omitempty"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + Image512 string `json:"image_512"` + Image1024 string `json:"image_1024,omitempty"` + ImageOriginal string `json:"image_original,omitempty"` + IsCustomImage bool `json:"is_custom_image,omitempty"` + Title string `json:"title,omitempty"` + BotID string `json:"bot_id,omitempty"` + ApiAppID string `json:"api_app_id,omitempty"` + AlwaysActive bool `json:"always_active,omitempty"` + StatusText string `json:"status_text,omitempty"` + StatusEmoji string `json:"status_emoji,omitempty"` + StatusEmojiDisplayInfo []UserProfileStatusEmojiDisplayInfo `json:"status_emoji_display_info,omitempty"` + StatusExpiration int `json:"status_expiration,omitempty"` + StatusTextCanonical string `json:"status_text_canonical,omitempty"` + HuddleState string `json:"huddle_state,omitempty"` + HuddleStateExpirationTS int `json:"huddle_state_expiration_ts,omitempty"` + StartDate string `json:"start_date,omitempty"` + GuestInvitedBy string `json:"guest_invited_by,omitempty"` + Team string `json:"team"` + Fields UserProfileCustomFields `json:"fields,omitempty"` } type UserProfileStatusEmojiDisplayInfo struct { @@ -72,7 +81,7 @@ func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error { // MarshalJSON is the implementation of the json.Marshaler interface. func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) { if len(fields.fields) == 0 { - return []byte("[]"), nil + return []byte("{}"), nil } return json.Marshal(fields.fields) } @@ -111,33 +120,37 @@ type UserProfileCustomField struct { // User contains all the information of a user type User struct { - ID string `json:"id"` - TeamID string `json:"team_id"` - Name string `json:"name"` - Deleted bool `json:"deleted"` - Color string `json:"color"` - RealName string `json:"real_name"` - TZ string `json:"tz,omitempty"` - TZLabel string `json:"tz_label"` - TZOffset int `json:"tz_offset"` - Profile UserProfile `json:"profile"` - IsBot bool `json:"is_bot"` - IsAdmin bool `json:"is_admin"` - IsOwner bool `json:"is_owner"` - IsPrimaryOwner bool `json:"is_primary_owner"` - IsRestricted bool `json:"is_restricted"` - IsUltraRestricted bool `json:"is_ultra_restricted"` - IsStranger bool `json:"is_stranger"` - IsAppUser bool `json:"is_app_user"` - IsInvitedUser bool `json:"is_invited_user"` - IsEmailConfirmed bool `json:"is_email_confirmed"` - Has2FA bool `json:"has_2fa"` - TwoFactorType *string `json:"two_factor_type"` - HasFiles bool `json:"has_files"` - Presence string `json:"presence"` - Locale string `json:"locale"` - Updated JSONTime `json:"updated"` - Enterprise EnterpriseUser `json:"enterprise_user,omitempty"` + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + Username string `json:"username,omitempty"` + Deleted bool `json:"deleted"` + Color string `json:"color"` + RealName string `json:"real_name"` + TZ string `json:"tz,omitempty"` + TZLabel string `json:"tz_label"` + TZOffset int `json:"tz_offset"` + Profile UserProfile `json:"profile"` + IsBot bool `json:"is_bot"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + IsStranger bool `json:"is_stranger"` + IsAppUser bool `json:"is_app_user"` + IsConnectorBot bool `json:"is_connector_bot"` + IsWorkflowBot bool `json:"is_workflow_bot"` + IsInvitedUser bool `json:"is_invited_user"` + IsEmailConfirmed bool `json:"is_email_confirmed"` + Has2FA *bool `json:"has_2fa,omitempty"` + TwoFactorType *string `json:"two_factor_type"` + HasFiles bool `json:"has_files"` + Presence string `json:"presence"` + Locale string `json:"locale"` + Updated JSONTime `json:"updated"` + WhoCanShareContactCard string `json:"who_can_share_contact_card,omitempty"` + Enterprise EnterpriseUser `json:"enterprise_user,omitempty"` } // UserPresence contains details about a user online status @@ -169,13 +182,14 @@ type UserIdentity struct { } // EnterpriseUser is present when a user is part of Slack Enterprise Grid -// https://api.slack.com/types/user#enterprise_grid_user_objects +// https://docs.slack.dev/reference/objects/user-object/#fields type EnterpriseUser struct { ID string `json:"id"` EnterpriseID string `json:"enterprise_id"` EnterpriseName string `json:"enterprise_name"` IsAdmin bool `json:"is_admin"` IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` Teams []string `json:"teams"` } @@ -316,6 +330,13 @@ func GetUsersOptionTeamID(teamId string) GetUsersOption { } } +// GetUsersOptionCursor set the cursor to the next page of results +func GetUsersOptionCursor(cursor string) GetUsersOption { + return func(p *UserPagination) { + p.Cursor = cursor + } +} + func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) { up = UserPagination{ c: c, @@ -331,12 +352,13 @@ func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) // UserPagination allows for paginating over the users type UserPagination struct { - Users []User - limit int - presence bool - teamId string - previousResp *ResponseMetadata - c *Client + Users []User + Cursor string + limit int + presence bool + teamId string + complete bool + c *Client } // Done checks if the pagination has completed @@ -358,17 +380,15 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) resp *userResponseFull ) - if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { + if t.c == nil || t.complete { return t, errPaginationComplete } - t.previousResp = t.previousResp.initialize() - values := url.Values{ "limit": {strconv.Itoa(t.limit)}, "presence": {strconv.FormatBool(t.presence)}, "token": {t.c.token}, - "cursor": {t.previousResp.Cursor}, + "cursor": {t.Cursor}, "team_id": {t.teamId}, "include_locale": {strconv.FormatBool(true)}, } @@ -379,7 +399,8 @@ func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata) t.Users = resp.Members - t.previousResp = &resp.Metadata + t.Cursor = resp.Metadata.Cursor + t.complete = t.Cursor == "" return t, nil } @@ -585,6 +606,54 @@ func (api *Client) SetUserRealNameContextWithUser(ctx context.Context, user, rea return response.Err() } +// SetUserProfile sets the profile for the provided user. +// For more information see the SetUserProfileContext documentation. +func (api *Client) SetUserProfile(user string, profile *UserProfile) error { + return api.SetUserProfileContext(context.Background(), user, profile) +} + +// SetUserProfileContext sets the profile for the provided user with a custom context. +// +// The profile parameter is serialized as-is. Fields present in the JSON (including +// zero-value fields without an omitempty tag, such as RealName and DisplayName) will +// be updated by Slack. To avoid unintended changes, retrieve the current profile with +// GetUserProfile, modify the desired fields, and pass the result. +// +// For setting individual fields, prefer the targeted methods: SetUserRealName, +// SetUserCustomStatus, SetUserCustomFields. +// +// If a workspace admin has mapped custom profile fields to standard fields (e.g. +// title), the custom field takes precedence. Update the custom field via +// SetUserCustomFields instead. +// +// The user parameter is required when setting another user's profile (admin only, +// paid plans). Pass an empty string to modify the authenticated user's own profile. +// +// Slack API docs: https://docs.slack.dev/reference/methods/users.profile.set/ +func (api *Client) SetUserProfileContext(ctx context.Context, user string, profile *UserProfile) error { + profileJSON, err := json.Marshal(profile) + if err != nil { + return err + } + + values := url.Values{ + "token": {api.token}, + "profile": {string(profileJSON)}, + } + + // optional field. It should not be set if empty + if user != "" { + values["user"] = []string{user} + } + + response := &userResponseFull{} + if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil { + return err + } + + return response.Err() +} + // SetUserCustomFields sets Custom Profile fields on the provided users account. // For more information see the SetUserCustomFieldsContext documentation. func (api *Client) SetUserCustomFields(userID string, customFields map[string]UserProfileCustomField) error { @@ -628,7 +697,7 @@ func (api *Client) SetUserCustomFieldsContext(ctx context.Context, userID string } response := &userResponseFull{} - if err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil { + if _, err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil { return err } @@ -661,16 +730,16 @@ func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji str // // Slack API docs: https://api.slack.com/methods/users.profile.set func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error { - // XXX(theckman): this anonymous struct is for making requests to the Slack - // API for setting and unsetting a User's Custom Status/Emoji. To change - // these values we must provide a JSON document as the profile POST field. + // This anonymous struct is for making requests to the Slack API for setting and + // unsetting a User's Custom Status/Emoji. To change these values we must provide a + // JSON document as the profile POST field. // - // We use an anonymous struct over UserProfile because to unset the values - // on the User's profile we cannot use the `json:"omitempty"` tag. This is - // because an empty string ("") is what's used to unset the values. Check - // out the API docs for more details: + // We use an anonymous struct over UserProfile because to unset the values on the + // User's profile we cannot use the `json:"omitempty"` tag. This is because an empty + // string ("") is what's used to unset the values. Check out the API docs for more + // details: // - // - https://api.slack.com/docs/presence-and-status#custom_status + // - https://docs.slack.dev/apis/web-api/user-presence-and-status/#custom-status profile, err := json.Marshal( &struct { StatusText string `json:"status_text"` diff --git a/vendor/github.com/slack-go/slack/views.go b/vendor/github.com/slack-go/slack/views.go index 5f55b537a2..c16503c0e3 100644 --- a/vendor/github.com/slack-go/slack/views.go +++ b/vendor/github.com/slack-go/slack/views.go @@ -70,12 +70,30 @@ type ViewSubmissionResponse struct { Errors map[string]string `json:"errors,omitempty"` } +// NewClearViewSubmissionResponse closes all open modals in the current stack. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals#closing_views func NewClearViewSubmissionResponse() *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAClear, } } +// NewUpdateViewSubmissionResponse replaces the current modal with a new view. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals#updating_views func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAUpdate, @@ -83,6 +101,15 @@ func NewUpdateViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResp } } +// NewPushViewSubmissionResponse pushes a new view onto the modal stack. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals#pushing_views func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAPush, @@ -90,6 +117,19 @@ func NewPushViewSubmissionResponse(view *ModalViewRequest) *ViewSubmissionRespon } } +// NewErrorsViewSubmissionResponse displays validation errors on form fields. +// +// The errors map keys must be the BlockID of an InputBlock in the view. Keys +// that reference other block types (e.g. SectionBlock) are silently ignored +// by Slack, which shows a generic "trouble connecting" error instead. +// +// For HTTP-based apps, marshal this to JSON and write it as the HTTP response +// body. The response is not sent until the handler returns, so start any slow +// work in a goroutine and return promptly. +// +// For Socket Mode apps, pass this as the payload argument to Ack(). +// +// See https://docs.slack.dev/surfaces/modals/#displaying_errors func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionResponse { return &ViewSubmissionResponse{ ResponseAction: RAErrors, @@ -167,6 +207,9 @@ func ValidateUniqueBlockID(view ModalViewRequest) bool { for _, b := range view.Blocks.BlockSet { if inputBlock, ok := b.(*InputBlock); ok { + if inputBlock.BlockID == "" { + continue + } if _, ok := uniqueBlockID[inputBlock.BlockID]; ok { return false } @@ -178,7 +221,7 @@ func ValidateUniqueBlockID(view ModalViewRequest) bool { } // OpenViewContext opens a view for a user with a custom context. -// Slack API docs: https://api.slack.com/methods/views.open +// Slack API docs: https://docs.slack.dev/reference/methods/views.open func (api *Client) OpenViewContext( ctx context.Context, triggerID string, @@ -200,9 +243,8 @@ func (api *Client) OpenViewContext( if err != nil { return nil, err } - endpoint := api.endpoint + "views.open" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.open", api.token, encoded, resp) if err != nil { return nil, err } @@ -212,11 +254,15 @@ func (api *Client) OpenViewContext( // PublishView publishes a static view for a user. // For more information see the PublishViewContext documentation. func (api *Client) PublishView(userID string, view HomeTabViewRequest, hash string) (*ViewResponse, error) { - return api.PublishViewContext(context.Background(), PublishViewContextRequest{UserID: userID, View: view, Hash: &hash}) + var hashPtr *string + if hash != "" { + hashPtr = &hash + } + return api.PublishViewContext(context.Background(), PublishViewContextRequest{UserID: userID, View: view, Hash: hashPtr}) } // PublishViewContext publishes a static view for a user with a custom context. -// Slack API docs: https://api.slack.com/methods/views.publish +// Slack API docs: https://docs.slack.dev/reference/methods/views.publish func (api *Client) PublishViewContext( ctx context.Context, req PublishViewContextRequest, @@ -228,9 +274,8 @@ func (api *Client) PublishViewContext( if err != nil { return nil, err } - endpoint := api.endpoint + "views.publish" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.publish", api.token, encoded, resp) if err != nil { return nil, err } @@ -244,7 +289,7 @@ func (api *Client) PushView(triggerID string, view ModalViewRequest) (*ViewRespo } // PushViewContext pushes a view onto the stack of a root view with a custom context. -// Slack API docs: https://api.slack.com/methods/views.push +// Slack API docs: https://docs.slack.dev/reference/methods/views.push func (api *Client) PushViewContext( ctx context.Context, triggerID string, @@ -261,9 +306,8 @@ func (api *Client) PushViewContext( if err != nil { return nil, err } - endpoint := api.endpoint + "views.push" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.push", api.token, encoded, resp) if err != nil { return nil, err } @@ -277,7 +321,7 @@ func (api *Client) UpdateView(view ModalViewRequest, externalID, hash, viewID st } // UpdateViewContext updates an existing view with a custom context. -// Slack API docs: https://api.slack.com/methods/views.update +// Slack API docs: https://docs.slack.dev/reference/methods/views.update func (api *Client) UpdateViewContext( ctx context.Context, view ModalViewRequest, @@ -297,9 +341,8 @@ func (api *Client) UpdateViewContext( if err != nil { return nil, err } - endpoint := api.endpoint + "views.update" resp := &ViewResponse{} - err = postJSON(ctx, api.httpclient, endpoint, api.token, encoded, resp, api) + err = api.postJSONMethod(ctx, "views.update", api.token, encoded, resp) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/webhooks.go b/vendor/github.com/slack-go/slack/webhooks.go index 5a854f38b0..729bce401d 100644 --- a/vendor/github.com/slack-go/slack/webhooks.go +++ b/vendor/github.com/slack-go/slack/webhooks.go @@ -23,8 +23,8 @@ type WebhookMessage struct { ReplaceOriginal bool `json:"replace_original"` DeleteOriginal bool `json:"delete_original"` ReplyBroadcast bool `json:"reply_broadcast,omitempty"` - UnfurlLinks bool `json:"unfurl_links,omitempty"` - UnfurlMedia bool `json:"unfurl_media,omitempty"` + UnfurlLinks *bool `json:"unfurl_links,omitempty"` + UnfurlMedia *bool `json:"unfurl_media,omitempty"` } func PostWebhook(url string, msg *WebhookMessage) error { diff --git a/vendor/github.com/slack-go/slack/websocket_groups.go b/vendor/github.com/slack-go/slack/websocket_groups.go index eb88985c45..c35d5f3574 100644 --- a/vendor/github.com/slack-go/slack/websocket_groups.go +++ b/vendor/github.com/slack-go/slack/websocket_groups.go @@ -7,9 +7,6 @@ type GroupCreatedEvent struct { Channel ChannelCreatedInfo `json:"channel"` } -// XXX: Should we really do this? event.Group is probably nicer than event.Channel -// even though the api returns "channel" - // GroupMarkedEvent represents the Group marked event type GroupMarkedEvent ChannelInfoEvent diff --git a/vendor/github.com/slack-go/slack/websocket_managed_conn.go b/vendor/github.com/slack-go/slack/websocket_managed_conn.go index f107b2a47b..da861fa874 100644 --- a/vendor/github.com/slack-go/slack/websocket_managed_conn.go +++ b/vendor/github.com/slack-go/slack/websocket_managed_conn.go @@ -582,7 +582,10 @@ var EventMapping = map[string]interface{}{ "manual_presence_change": ManualPresenceChangeEvent{}, - "user_change": UserChangeEvent{}, + "user_change": UserChangeEvent{}, + "user_status_changed": UserStatusChangedEvent{}, + "user_huddle_changed": UserHuddleChangedEvent{}, + "user_profile_changed": UserProfileChangedEvent{}, "emoji_changed": EmojiChangedEvent{}, @@ -595,6 +598,10 @@ var EventMapping = map[string]interface{}{ "accounts_changed": AccountsChangedEvent{}, + "apps_uninstalled": AppsUninstalledEvent{}, + "activity": ActivityEvent{}, + "badge_counts_updated": BadgeCountsUpdatedEvent{}, + "reconnect_url": ReconnectUrlEvent{}, "member_joined_channel": MemberJoinedChannelEvent{}, @@ -608,4 +615,10 @@ var EventMapping = map[string]interface{}{ "desktop_notification": DesktopNotificationEvent{}, "mobile_in_app_notification": MobileInAppNotificationEvent{}, + + "channel_updated": ChannelUpdatedEvent{}, + + "sh_room_join": SHRoomJoinEvent{}, + "sh_room_leave": SHRoomLeaveEvent{}, + "sh_room_update": SHRoomUpdateEvent{}, } diff --git a/vendor/github.com/slack-go/slack/websocket_misc.go b/vendor/github.com/slack-go/slack/websocket_misc.go index 65a8bb65d4..fb301f5eb2 100644 --- a/vendor/github.com/slack-go/slack/websocket_misc.go +++ b/vendor/github.com/slack-go/slack/websocket_misc.go @@ -71,8 +71,34 @@ type ManualPresenceChangeEvent struct { // UserChangeEvent represents the user change event type UserChangeEvent struct { - Type string `json:"type"` - User User `json:"user"` + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +// UserStatusChangedEvent represents the user status changed event +type UserStatusChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +// UserHuddleChangedEvent represents the user huddle changed event +type UserHuddleChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +// UserProfileChangedEvent represents the user profile changed event +type UserProfileChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` } // EmojiChangedEvent represents the emoji changed event @@ -139,3 +165,115 @@ type MemberLeftChannelEvent struct { ChannelType string `json:"channel_type"` Team string `json:"team"` } + +// ChannelUpdatedEvent is fired when a channel's properties are updated (tabs, meeting +// notes, etc.). +type ChannelUpdatedEvent struct { + Type string `json:"type"` + Updates map[string]any `json:"updates"` + Channel string `json:"channel"` + Channels []string `json:"channels"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// SHRoomRecording holds recording metadata for a Slack Call/Huddle room. +type SHRoomRecording struct { + CanRecordSummary string `json:"can_record_summary,omitempty"` +} + +// SHRoom represents a Slack Huddle/Call room. +type SHRoom struct { + ID string `json:"id"` + Name *string `json:"name"` // nullable in Slack's response + MediaServer string `json:"media_server"` + CreatedBy string `json:"created_by"` + DateStart int64 `json:"date_start"` + DateEnd int64 `json:"date_end"` + Participants []string `json:"participants"` + ParticipantHistory []string `json:"participant_history"` + ParticipantsEvents map[string]map[string]any `json:"participants_events,omitempty"` + ParticipantsCameraOn []string `json:"participants_camera_on"` + ParticipantsCameraOff []string `json:"participants_camera_off"` + ParticipantsScreenshareOn []string `json:"participants_screenshare_on"` + ParticipantsScreenshareOff []string `json:"participants_screenshare_off"` + CanvasThreadTS string `json:"canvas_thread_ts,omitempty"` + ThreadRootTS string `json:"thread_root_ts,omitempty"` + Channels []string `json:"channels"` + IsDMCall bool `json:"is_dm_call"` + WasRejected bool `json:"was_rejected"` + WasMissed bool `json:"was_missed"` + WasAccepted bool `json:"was_accepted"` + HasEnded bool `json:"has_ended"` + BackgroundID string `json:"background_id,omitempty"` + CanvasBackground string `json:"canvas_background,omitempty"` + IsPrewarmed bool `json:"is_prewarmed,omitempty"` + IsScheduled bool `json:"is_scheduled,omitempty"` + Recording *SHRoomRecording `json:"recording,omitempty"` + Locale string `json:"locale,omitempty"` + AttachedFileIDs []string `json:"attached_file_ids,omitempty"` + MediaBackendType string `json:"media_backend_type"` + DisplayID string `json:"display_id,omitempty"` + ExternalUniqueID string `json:"external_unique_id"` + AppID string `json:"app_id"` + CallFamily string `json:"call_family,omitempty"` + HuddleLink string `json:"huddle_link,omitempty"` +} + +// SHRoomHuddle holds the huddle-specific metadata on sh_room events. +type SHRoomHuddle struct { + ChannelID string `json:"channel_id"` +} + +// SHRoomJoinEvent is fired when a user joins a Slack Call/Huddle room. +type SHRoomJoinEvent struct { + Type string `json:"type"` + Room SHRoom `json:"room"` + User string `json:"user"` + Huddle *SHRoomHuddle `json:"huddle,omitempty"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// SHRoomLeaveEvent is fired when a user leaves a Slack Call/Huddle room. +type SHRoomLeaveEvent struct { + Type string `json:"type"` + Room SHRoom `json:"room"` + User string `json:"user"` + Huddle *SHRoomHuddle `json:"huddle,omitempty"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// SHRoomUpdateEvent is fired when a Slack Call/Huddle room is updated. +type SHRoomUpdateEvent struct { + Type string `json:"type"` + Room SHRoom `json:"room"` + User string `json:"user"` + Huddle *SHRoomHuddle `json:"huddle,omitempty"` + EventTS string `json:"event_ts"` + TS string `json:"ts"` +} + +// AppsUninstalledEvent represents the apps_uninstalled event sent via RTM +// when one or more apps are uninstalled from the workspace. +type AppsUninstalledEvent struct { + Type string `json:"type"` +} + +// ActivityEvent represents the activity event sent via RTM. This is an +// internal Slack event that fires during normal workspace usage (e.g. new +// messages, bundle updates). +type ActivityEvent struct { + Type string `json:"type"` + SubType string `json:"subtype"` + Key string `json:"key"` + Entry json.RawMessage `json:"entry"` + EventTimestamp string `json:"event_ts"` +} + +// BadgeCountsUpdatedEvent represents the badge_counts_updated event sent via +// RTM when notification badge counts change. +type BadgeCountsUpdatedEvent struct { + Type string `json:"type"` +} diff --git a/vendor/github.com/slack-go/slack/workflows_featured.go b/vendor/github.com/slack-go/slack/workflows_featured.go new file mode 100644 index 0000000000..08b3398683 --- /dev/null +++ b/vendor/github.com/slack-go/slack/workflows_featured.go @@ -0,0 +1,143 @@ +package slack + +import ( + "context" + "encoding/json" + "fmt" +) + +type ( + FeaturedWorkflowTrigger struct { + ID string `json:"id"` + Title string `json:"title"` + } + + FeaturedWorkflow struct { + ChannelID string `json:"channel_id"` + Triggers []FeaturedWorkflowTrigger `json:"triggers"` + } + + WorkflowsFeaturedAddInput struct { + ChannelID string `json:"channel_id"` + TriggerIDs []string `json:"trigger_ids"` + } + + WorkflowsFeaturedListInput struct { + ChannelIDs []string `json:"channel_ids"` + } + + WorkflowsFeaturedListOutput struct { + FeaturedWorkflows []FeaturedWorkflow `json:"featured_workflows"` + } + + WorkflowsFeaturedRemoveInput struct { + ChannelID string `json:"channel_id"` + TriggerIDs []string `json:"trigger_ids"` + } + + WorkflowsFeaturedSetInput struct { + ChannelID string `json:"channel_id"` + TriggerIDs []string `json:"trigger_ids"` + } +) + +// WorkflowsFeaturedAdd adds featured workflows to a channel. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.add +func (api *Client) WorkflowsFeaturedAdd(ctx context.Context, input *WorkflowsFeaturedAddInput) error { + response := struct { + SlackResponse + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal WorkflowsFeaturedAddInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.add", api.token, jsonPayload, &response) + if err != nil { + return err + } + + if err := response.Err(); err != nil { + return err + } + + return nil +} + +// WorkflowsFeaturedList lists featured workflows for the given channels. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.list +func (api *Client) WorkflowsFeaturedList(ctx context.Context, input *WorkflowsFeaturedListInput) (*WorkflowsFeaturedListOutput, error) { + response := struct { + SlackResponse + *WorkflowsFeaturedListOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsFeaturedListInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.list", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsFeaturedListOutput, nil +} + +// WorkflowsFeaturedRemove removes featured workflows from a channel. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.remove +func (api *Client) WorkflowsFeaturedRemove(ctx context.Context, input *WorkflowsFeaturedRemoveInput) error { + response := struct { + SlackResponse + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal WorkflowsFeaturedRemoveInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.remove", api.token, jsonPayload, &response) + if err != nil { + return err + } + + if err := response.Err(); err != nil { + return err + } + + return nil +} + +// WorkflowsFeaturedSet replaces all featured workflows in a channel with the given triggers. +// +// Slack API Docs:https://api.slack.com/methods/workflows.featured.set +func (api *Client) WorkflowsFeaturedSet(ctx context.Context, input *WorkflowsFeaturedSetInput) error { + response := struct { + SlackResponse + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("failed to marshal WorkflowsFeaturedSetInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.featured.set", api.token, jsonPayload, &response) + if err != nil { + return err + } + + if err := response.Err(); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/slack-go/slack/workflows_triggers.go b/vendor/github.com/slack-go/slack/workflows_triggers.go index f9a7cd90c8..348348061b 100644 --- a/vendor/github.com/slack-go/slack/workflows_triggers.go +++ b/vendor/github.com/slack-go/slack/workflows_triggers.go @@ -84,7 +84,7 @@ func (api *Client) WorkflowsTriggersPermissionsAdd(ctx context.Context, input *W return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsAddInput: %w", err) } - err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.add", api.token, jsonPayload, &response, api) + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.add", api.token, jsonPayload, &response) if err != nil { return nil, err } @@ -111,7 +111,7 @@ func (api *Client) WorkflowsTriggersPermissionsList(ctx context.Context, input * return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsListInput: %w", err) } - err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.list", api.token, jsonPayload, &response, api) + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.list", api.token, jsonPayload, &response) if err != nil { return nil, err } @@ -138,7 +138,7 @@ func (api *Client) WorkflowsTriggersPermissionsRemove(ctx context.Context, input return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsRemoveInput: %w", err) } - err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.remove", api.token, jsonPayload, &response, api) + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.remove", api.token, jsonPayload, &response) if err != nil { return nil, err } @@ -164,7 +164,7 @@ func (api *Client) WorkflowsTriggersPermissionsSet(ctx context.Context, input *W return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsSetInput: %w", err) } - err = postJSON(ctx, api.httpclient, api.endpoint+"workflows.triggers.permissions.set", api.token, jsonPayload, &response, api) + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.set", api.token, jsonPayload, &response) if err != nil { return nil, err } diff --git a/vendor/modules.txt b/vendor/modules.txt index 71bb751bf3..3dcee78011 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -792,8 +792,8 @@ github.com/shirou/gopsutil/v4/process # github.com/sirupsen/logrus v1.9.3 ## explicit; go 1.13 github.com/sirupsen/logrus -# github.com/slack-go/slack v0.17.3 -## explicit; go 1.22 +# github.com/slack-go/slack v0.23.1 +## explicit; go 1.25 github.com/slack-go/slack github.com/slack-go/slack/internal/backoff github.com/slack-go/slack/internal/errorsx