From 83f791c69f234beef7f26fb511c885cb111e8e7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 22:12:35 +0000 Subject: [PATCH] Bump github.com/slack-go/slack from 0.6.4 to 0.23.1 Bumps [github.com/slack-go/slack](https://github.com/slack-go/slack) from 0.6.4 to 0.23.1. - [Release notes](https://github.com/slack-go/slack/releases) - [Changelog](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) - [Commits](https://github.com/slack-go/slack/compare/v0.6.4...v0.23.1) --- updated-dependencies: - dependency-name: github.com/slack-go/slack dependency-version: 0.23.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 26 +- go.sum | 42 +- .../github.com/gorilla/websocket/.gitignore | 2 +- .../github.com/gorilla/websocket/.travis.yml | 19 - vendor/github.com/gorilla/websocket/AUTHORS | 1 + vendor/github.com/gorilla/websocket/README.md | 41 +- vendor/github.com/gorilla/websocket/client.go | 288 ++-- .../gorilla/websocket/client_clone.go | 16 - .../gorilla/websocket/client_clone_legacy.go | 38 - vendor/github.com/gorilla/websocket/conn.go | 355 +++-- .../github.com/gorilla/websocket/conn_read.go | 18 - .../gorilla/websocket/conn_read_legacy.go | 21 - vendor/github.com/gorilla/websocket/doc.go | 105 +- vendor/github.com/gorilla/websocket/join.go | 42 + vendor/github.com/gorilla/websocket/json.go | 11 +- vendor/github.com/gorilla/websocket/mask.go | 2 +- .../github.com/gorilla/websocket/mask_safe.go | 1 + .../github.com/gorilla/websocket/prepared.go | 5 +- vendor/github.com/gorilla/websocket/proxy.go | 77 + vendor/github.com/gorilla/websocket/server.go | 144 +- .../gorilla/websocket/tls_handshake.go | 21 + .../gorilla/websocket/tls_handshake_116.go | 21 + vendor/github.com/gorilla/websocket/util.go | 180 ++- .../gorilla/websocket/x_net_proxy.go | 473 ++++++ vendor/github.com/mattn/go-colorable/go.mod | 3 - vendor/github.com/mattn/go-colorable/go.sum | 4 - vendor/github.com/mattn/go-isatty/go.mod | 3 - vendor/github.com/mattn/go-isatty/go.sum | 2 - .../robinjoseph08/redisqueue/go.mod | 23 - .../robinjoseph08/redisqueue/go.sum | 86 -- vendor/github.com/rs/zerolog/go.mod | 9 - vendor/github.com/rs/zerolog/go.sum | 16 - vendor/github.com/slack-go/slack/.gitignore | 2 + .../github.com/slack-go/slack/.golangci.yml | 35 + .../slack-go/slack/.gometalinter.json | 14 - vendor/github.com/slack-go/slack/.travis.yml | 39 - vendor/github.com/slack-go/slack/CHANGELOG.md | 639 +++++++- .../github.com/slack-go/slack/CONTRIBUTING.md | 40 + vendor/github.com/slack-go/slack/Makefile | 4 +- vendor/github.com/slack-go/slack/README.md | 70 +- vendor/github.com/slack-go/slack/TODO.txt | 3 - vendor/github.com/slack-go/slack/admin.go | 12 +- .../slack-go/slack/admin_conversations.go | 807 ++++++++++ .../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 | 72 + vendor/github.com/slack-go/slack/assistant.go | 377 +++++ .../github.com/slack-go/slack/attachments.go | 16 +- vendor/github.com/slack-go/slack/audit.go | 153 ++ vendor/github.com/slack-go/slack/auth.go | 46 +- vendor/github.com/slack-go/slack/block.go | 76 +- .../github.com/slack-go/slack/block_action.go | 9 +- .../github.com/slack-go/slack/block_alert.go | 70 + .../github.com/slack-go/slack/block_call.go | 93 ++ .../github.com/slack-go/slack/block_card.go | 90 ++ .../slack-go/slack/block_carousel.go | 42 + .../slack-go/slack/block_context.go | 7 +- .../slack-go/slack/block_context_actions.go | 31 + .../github.com/slack-go/slack/block_conv.go | 131 +- .../slack-go/slack/block_divider.go | 6 +- .../slack-go/slack/block_element.go | 685 ++++++++- .../github.com/slack-go/slack/block_file.go | 5 + .../github.com/slack-go/slack/block_header.go | 45 + .../github.com/slack-go/slack/block_image.go | 37 +- .../github.com/slack-go/slack/block_input.go | 33 +- .../github.com/slack-go/slack/block_json.go | 110 ++ .../slack-go/slack/block_markdown.go | 34 + .../github.com/slack-go/slack/block_object.go | 73 +- .../github.com/slack-go/slack/block_plan.go | 55 + .../slack-go/slack/block_rich_text.go | 582 ++++++++ .../slack-go/slack/block_section.go | 15 + .../github.com/slack-go/slack/block_table.go | 55 + .../slack-go/slack/block_task_card.go | 101 ++ .../slack-go/slack/block_unknown.go | 27 +- .../github.com/slack-go/slack/block_video.go | 70 + vendor/github.com/slack-go/slack/bookmarks.go | 169 +++ vendor/github.com/slack-go/slack/bots.go | 25 +- vendor/github.com/slack-go/slack/calls.go | 216 +++ vendor/github.com/slack-go/slack/canvas.go | 264 ++++ vendor/github.com/slack-go/slack/channels.go | 456 +----- vendor/github.com/slack-go/slack/chat.go | 598 ++++++-- .../slack-go/slack/chat_stream_chunks.go | 95 ++ .../github.com/slack-go/slack/conversation.go | 616 +++++++- vendor/github.com/slack-go/slack/dialog.go | 7 +- .../slack-go/slack/dialog_select.go | 14 + .../github.com/slack-go/slack/dialog_text.go | 2 +- vendor/github.com/slack-go/slack/dnd.go | 53 +- vendor/github.com/slack-go/slack/emoji.go | 6 +- vendor/github.com/slack-go/slack/entity.go | 132 ++ vendor/github.com/slack-go/slack/errors.go | 1 + vendor/github.com/slack-go/slack/files.go | 410 +++-- .../slack-go/slack/function_execute.go | 91 ++ vendor/github.com/slack-go/slack/go.mod | 10 - vendor/github.com/slack-go/slack/go.sum | 12 - vendor/github.com/slack-go/slack/groups.go | 369 ----- vendor/github.com/slack-go/slack/huddle.go | 64 + vendor/github.com/slack-go/slack/im.go | 154 -- vendor/github.com/slack-go/slack/info.go | 48 +- .../github.com/slack-go/slack/interactions.go | 145 +- .../slack/{ => internal/backoff}/backoff.go | 15 +- .../slack/internal/errorsx/errorsx.go | 9 + vendor/github.com/slack-go/slack/logger.go | 2 +- vendor/github.com/slack-go/slack/logo.png | Bin 0 -> 52440 bytes vendor/github.com/slack-go/slack/manifests.go | 299 ++++ vendor/github.com/slack-go/slack/messageID.go | 20 +- vendor/github.com/slack-go/slack/messages.go | 87 +- vendor/github.com/slack-go/slack/metadata.go | 38 + vendor/github.com/slack-go/slack/migration.go | 40 + vendor/github.com/slack-go/slack/misc.go | 401 +++-- vendor/github.com/slack-go/slack/mise.toml | 4 + vendor/github.com/slack-go/slack/oauth.go | 267 +++- vendor/github.com/slack-go/slack/pins.go | 14 +- vendor/github.com/slack-go/slack/reactions.go | 99 +- vendor/github.com/slack-go/slack/reminders.go | 85 +- .../github.com/slack-go/slack/remotefiles.go | 324 ++++ vendor/github.com/slack-go/slack/retry.go | 307 ++++ vendor/github.com/slack-go/slack/rtm.go | 6 + vendor/github.com/slack-go/slack/search.go | 14 +- vendor/github.com/slack-go/slack/security.go | 18 +- vendor/github.com/slack-go/slack/slack.go | 163 +- .../slack/slackevents/action_events.go | 7 + .../slack/slackevents/inner_events.go | 1315 ++++++++++++++++- .../slack/slackevents/outer_events.go | 35 +- .../slack-go/slack/slackevents/parsers.go | 77 +- .../slack-go/slack/slackutilsx/slackutilsx.go | 6 +- vendor/github.com/slack-go/slack/slash.go | 64 +- .../github.com/slack-go/slack/socket_mode.go | 45 + vendor/github.com/slack-go/slack/stars.go | 88 +- .../slack-go/slack/status_code_error.go | 28 + vendor/github.com/slack-go/slack/team.go | 157 +- vendor/github.com/slack-go/slack/tokens.go | 52 + .../github.com/slack-go/slack/usergroups.go | 408 ++++- vendor/github.com/slack-go/slack/users.go | 432 ++++-- vendor/github.com/slack-go/slack/views.go | 165 ++- vendor/github.com/slack-go/slack/webhooks.go | 35 + .../slack-go/slack/webhooks_go112.go | 34 - .../slack-go/slack/webhooks_go113.go | 33 - .../slack-go/slack/websocket_groups.go | 3 - .../slack-go/slack/websocket_internals.go | 1 + .../slack-go/slack/websocket_managed_conn.go | 63 +- .../slack-go/slack/websocket_misc.go | 142 +- .../slack-go/slack/websocket_reactions.go | 12 +- .../slack-go/slack/websocket_subteam.go | 4 +- .../slack-go/slack/workflows_featured.go | 143 ++ .../slack-go/slack/workflows_triggers.go | 177 +++ vendor/github.com/urfave/cli/go.mod | 8 - vendor/github.com/urfave/cli/go.sum | 6 - vendor/github.com/valyala/fastjson/go.mod | 3 - vendor/gopkg.in/AlecAivazis/survey.v1/go.mod | 16 - vendor/gopkg.in/AlecAivazis/survey.v1/go.sum | 24 - vendor/gopkg.in/yaml.v2/go.mod | 5 - vendor/modules.txt | 32 +- 154 files changed, 14183 insertions(+), 3120 deletions(-) delete mode 100644 vendor/github.com/gorilla/websocket/.travis.yml delete mode 100644 vendor/github.com/gorilla/websocket/client_clone.go delete mode 100644 vendor/github.com/gorilla/websocket/client_clone_legacy.go delete mode 100644 vendor/github.com/gorilla/websocket/conn_read.go delete mode 100644 vendor/github.com/gorilla/websocket/conn_read_legacy.go create mode 100644 vendor/github.com/gorilla/websocket/join.go create mode 100644 vendor/github.com/gorilla/websocket/proxy.go create mode 100644 vendor/github.com/gorilla/websocket/tls_handshake.go create mode 100644 vendor/github.com/gorilla/websocket/tls_handshake_116.go create mode 100644 vendor/github.com/gorilla/websocket/x_net_proxy.go delete mode 100644 vendor/github.com/mattn/go-colorable/go.mod delete mode 100644 vendor/github.com/mattn/go-colorable/go.sum delete mode 100644 vendor/github.com/mattn/go-isatty/go.mod delete mode 100644 vendor/github.com/mattn/go-isatty/go.sum delete mode 100644 vendor/github.com/robinjoseph08/redisqueue/go.mod delete mode 100644 vendor/github.com/robinjoseph08/redisqueue/go.sum delete mode 100644 vendor/github.com/rs/zerolog/go.mod delete mode 100644 vendor/github.com/rs/zerolog/go.sum create mode 100644 vendor/github.com/slack-go/slack/.golangci.yml delete mode 100644 vendor/github.com/slack-go/slack/.gometalinter.json delete mode 100644 vendor/github.com/slack-go/slack/.travis.yml create mode 100644 vendor/github.com/slack-go/slack/CONTRIBUTING.md delete mode 100644 vendor/github.com/slack-go/slack/TODO.txt create mode 100644 vendor/github.com/slack-go/slack/admin_conversations.go 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/apps.go create mode 100644 vendor/github.com/slack-go/slack/assistant.go create mode 100644 vendor/github.com/slack-go/slack/audit.go create mode 100644 vendor/github.com/slack-go/slack/block_alert.go create mode 100644 vendor/github.com/slack-go/slack/block_call.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_header.go create mode 100644 vendor/github.com/slack-go/slack/block_json.go create mode 100644 vendor/github.com/slack-go/slack/block_markdown.go create mode 100644 vendor/github.com/slack-go/slack/block_plan.go create mode 100644 vendor/github.com/slack-go/slack/block_rich_text.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/block_video.go create mode 100644 vendor/github.com/slack-go/slack/bookmarks.go create mode 100644 vendor/github.com/slack-go/slack/calls.go create mode 100644 vendor/github.com/slack-go/slack/canvas.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/function_execute.go delete mode 100644 vendor/github.com/slack-go/slack/go.mod delete mode 100644 vendor/github.com/slack-go/slack/go.sum create mode 100644 vendor/github.com/slack-go/slack/huddle.go delete mode 100644 vendor/github.com/slack-go/slack/im.go rename vendor/github.com/slack-go/slack/{ => internal/backoff}/backoff.go (79%) create mode 100644 vendor/github.com/slack-go/slack/logo.png create mode 100644 vendor/github.com/slack-go/slack/manifests.go create mode 100644 vendor/github.com/slack-go/slack/metadata.go create mode 100644 vendor/github.com/slack-go/slack/migration.go create mode 100644 vendor/github.com/slack-go/slack/mise.toml create mode 100644 vendor/github.com/slack-go/slack/remotefiles.go create mode 100644 vendor/github.com/slack-go/slack/retry.go create mode 100644 vendor/github.com/slack-go/slack/socket_mode.go create mode 100644 vendor/github.com/slack-go/slack/status_code_error.go create mode 100644 vendor/github.com/slack-go/slack/tokens.go delete mode 100644 vendor/github.com/slack-go/slack/webhooks_go112.go delete mode 100644 vendor/github.com/slack-go/slack/webhooks_go113.go create mode 100644 vendor/github.com/slack-go/slack/workflows_featured.go create mode 100644 vendor/github.com/slack-go/slack/workflows_triggers.go delete mode 100644 vendor/github.com/urfave/cli/go.mod delete mode 100644 vendor/github.com/urfave/cli/go.sum delete mode 100644 vendor/github.com/valyala/fastjson/go.mod delete mode 100644 vendor/gopkg.in/AlecAivazis/survey.v1/go.mod delete mode 100644 vendor/gopkg.in/AlecAivazis/survey.v1/go.sum delete mode 100644 vendor/gopkg.in/yaml.v2/go.mod diff --git a/go.mod b/go.mod index 9a66ff2..c334172 100644 --- a/go.mod +++ b/go.mod @@ -2,19 +2,37 @@ module github.com/gobridge/gopherbot -go 1.14 +go 1.25 require ( github.com/go-redis/redis v6.15.7+incompatible github.com/google/go-cmp v0.4.0 github.com/heroku/x v0.0.22 - github.com/onsi/ginkgo v1.10.1 // indirect - github.com/onsi/gomega v1.7.0 // indirect github.com/robinjoseph08/redisqueue v1.1.0 github.com/rs/zerolog v1.18.0 - github.com/slack-go/slack v0.6.4 + github.com/slack-go/slack v0.23.1 github.com/valyala/fastjson v1.5.1 +) + +require ( + github.com/fatih/color v1.7.0 // indirect + github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/imdario/mergo v0.3.7 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect + github.com/mattn/goveralls v0.0.2 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/onsi/ginkgo v1.10.1 // indirect + github.com/onsi/gomega v1.7.0 // indirect + github.com/pkg/errors v0.8.2-0.20190227000051-27936f6d90f9 // indirect + github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df // indirect + github.com/urfave/cli v1.21.0 // indirect + golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c // indirect + gopkg.in/AlecAivazis/survey.v1 v1.8.5 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/kyokomi/emoji.v1 v1.5.1 // indirect gopkg.in/yaml.v2 v2.2.4 // indirect ) diff --git a/go.sum b/go.sum index 04bdba2..629902b 100644 --- a/go.sum +++ b/go.sum @@ -14,7 +14,6 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -22,7 +21,6 @@ github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fp github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f h1:8l4Aw3Jmx0pLKYMkY+1b6yBPgE+rzRtA5T3vqFyI2Z8= @@ -36,8 +34,8 @@ github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8w github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -47,29 +45,25 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4er github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gops v0.3.8-0.20200229223415-3a98d6d24562/go.mod h1:bj0cwMmX1X4XIJFTjR99R5sCxNssNJ8HebFNvoQlmgY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= -github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s= github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.6/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -121,19 +115,15 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.2-0.20190227000051-27936f6d90f9 h1:PCj9X21C4pet4sEcElTfAi6LSl5ShkjE8doieLc+cbU= github.com/pkg/errors v0.8.2-0.20190227000051-27936f6d90f9/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -152,8 +142,8 @@ github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/slack-go/slack v0.6.4 h1:cxOqFgM5RW6mdEyDqAJutFk3qiORK9oHRKi5bPqkY9o= -github.com/slack-go/slack v0.6.4/go.mod h1:sGRjv3w+ERAUMMMbldHObQPBcNSyVB7KLKYfnwUFBfw= +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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soveran/redisurl v0.0.0-20180322091936-eb325bc7a4b8/go.mod h1:FVJ8jbHu7QrNFs3bZEsv/L5JjearIAY9N0oXh2wk+6Y= @@ -163,14 +153,13 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df h1:Y2l28Jr3vOEeYtxfVbMtVfOdAwuUqWaP9fvNKiBVeXY= github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df/go.mod h1:pnyouUty/nBr/zm3GYwTIt+qFTLWbdjeLjZmJdzJOu8= github.com/unrolled/secure v1.0.1/go.mod h1:R6rugAuzh4TQpbFAq69oqZggyBQxFRFQIewtz5z7Jsc= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE= github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= @@ -186,13 +175,10 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -200,7 +186,6 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -213,7 +198,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -226,9 +210,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20171017063910-8dbc5d05d6ed/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -236,9 +218,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -247,7 +227,6 @@ golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -265,7 +244,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db h1:9hRk1xeL9LTT3yX/941DqeBz87XgHAQuj+TbimYJuiw= golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -298,7 +276,6 @@ google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac gopkg.in/AlecAivazis/survey.v1 v1.8.5 h1:QoEEmn/d5BbuPIL2qvXwzJdttFFhRQFkaq+tEKb7SMI= gopkg.in/AlecAivazis/survey.v1 v1.8.5/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA= gopkg.in/caio/go-tdigest.v2 v2.3.0/go.mod h1:HPfh/CLN8UWDMOC76lqxVeKa5E24ypoVuTj4BLMb9cU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= @@ -313,10 +290,11 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/vendor/github.com/gorilla/websocket/.gitignore b/vendor/github.com/gorilla/websocket/.gitignore index ac71020..cd3fcd1 100644 --- a/vendor/github.com/gorilla/websocket/.gitignore +++ b/vendor/github.com/gorilla/websocket/.gitignore @@ -22,4 +22,4 @@ _testmain.go *.exe .idea/ -*.iml \ No newline at end of file +*.iml diff --git a/vendor/github.com/gorilla/websocket/.travis.yml b/vendor/github.com/gorilla/websocket/.travis.yml deleted file mode 100644 index 3d8d29c..0000000 --- a/vendor/github.com/gorilla/websocket/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: go -sudo: false - -matrix: - include: - - go: 1.4 - - go: 1.5 - - go: 1.6 - - go: 1.7 - - go: 1.8 - - go: tip - allow_failures: - - go: tip - -script: - - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d .) - - go vet $(go list ./... | grep -v /vendor/) - - go test -v -race ./... diff --git a/vendor/github.com/gorilla/websocket/AUTHORS b/vendor/github.com/gorilla/websocket/AUTHORS index b003eca..1931f40 100644 --- a/vendor/github.com/gorilla/websocket/AUTHORS +++ b/vendor/github.com/gorilla/websocket/AUTHORS @@ -4,5 +4,6 @@ # Please keep the list sorted. Gary Burd +Google LLC (https://opensource.google.com/) Joachim Bauch diff --git a/vendor/github.com/gorilla/websocket/README.md b/vendor/github.com/gorilla/websocket/README.md index 33c3d2b..d33ed7f 100644 --- a/vendor/github.com/gorilla/websocket/README.md +++ b/vendor/github.com/gorilla/websocket/README.md @@ -1,14 +1,15 @@ # Gorilla WebSocket +[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket) +[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket) + Gorilla WebSocket is a [Go](http://golang.org/) implementation of the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. -[![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket) -[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket) ### Documentation -* [API Reference](http://godoc.org/github.com/gorilla/websocket) +* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc) * [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) * [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) * [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) @@ -27,38 +28,6 @@ package API is stable. ### Protocol Compliance The Gorilla WebSocket package passes the server tests in the [Autobahn Test -Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn +Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). -### Gorilla WebSocket compared with other packages - - - - - - - - - - - - - - - - - - -
github.com/gorillagolang.org/x/net
RFC 6455 Features
Passes Autobahn Test SuiteYesNo
Receive fragmented messageYesNo, see note 1
Send close messageYesNo
Send pings and receive pongsYesNo
Get the type of a received data messageYesYes, see note 2
Other Features
Compression ExtensionsExperimentalNo
Read message using io.ReaderYesNo, see note 3
Write message using io.WriteCloserYesNo, see note 3
- -Notes: - -1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html). -2. The application can get the type of a received data message by implementing - a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal) - function. -3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. - Read returns when the input buffer is full or a frame boundary is - encountered. Each call to Write sends a single frame message. The Gorilla - io.Reader and io.WriteCloser operate on a single WebSocket message. - diff --git a/vendor/github.com/gorilla/websocket/client.go b/vendor/github.com/gorilla/websocket/client.go index 43a87c7..04fdafe 100644 --- a/vendor/github.com/gorilla/websocket/client.go +++ b/vendor/github.com/gorilla/websocket/client.go @@ -5,15 +5,16 @@ package websocket import ( - "bufio" "bytes" + "context" "crypto/tls" - "encoding/base64" "errors" + "fmt" "io" "io/ioutil" "net" "net/http" + "net/http/httptrace" "net/url" "strings" "time" @@ -48,11 +49,23 @@ func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufS } // A Dialer contains options for connecting to WebSocket server. +// +// It is safe to call Dialer's methods concurrently. type Dialer struct { // NetDial specifies the dial function for creating TCP connections. If // NetDial is nil, net.Dial is used. NetDial func(network, addr string) (net.Conn, error) + // NetDialContext specifies the dial function for creating TCP connections. If + // NetDialContext is nil, NetDial is used. + NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + + // NetDialTLSContext specifies the dial function for creating TLS/TCP connections. If + // NetDialTLSContext is nil, NetDialContext is used. + // If NetDialTLSContext is set, Dial assumes the TLS handshake is done there and + // TLSClientConfig is ignored. + NetDialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error) + // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the // request is aborted with the provided error. @@ -61,16 +74,29 @@ type Dialer struct { // TLSClientConfig specifies the TLS configuration to use with tls.Client. // If nil, the default configuration is used. + // If either NetDialTLS or NetDialTLSContext are set, Dial assumes the TLS handshake + // is done there and TLSClientConfig is ignored. TLSClientConfig *tls.Config // HandshakeTimeout specifies the duration for the handshake to complete. HandshakeTimeout time.Duration - // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer // size is zero, then a useful default size is used. The I/O buffer sizes // do not limit the size of the messages that can be sent or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the client's requested subprotocols. Subprotocols []string @@ -86,52 +112,13 @@ type Dialer struct { Jar http.CookieJar } -var errMalformedURL = errors.New("malformed ws or wss URL") - -// parseURL parses the URL. -// -// This function is a replacement for the standard library url.Parse function. -// In Go 1.4 and earlier, url.Parse loses information from the path. -func parseURL(s string) (*url.URL, error) { - // From the RFC: - // - // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] - // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] - var u url.URL - switch { - case strings.HasPrefix(s, "ws://"): - u.Scheme = "ws" - s = s[len("ws://"):] - case strings.HasPrefix(s, "wss://"): - u.Scheme = "wss" - s = s[len("wss://"):] - default: - return nil, errMalformedURL - } - - if i := strings.Index(s, "?"); i >= 0 { - u.RawQuery = s[i+1:] - s = s[:i] - } - - if i := strings.Index(s, "/"); i >= 0 { - u.Opaque = s[i:] - s = s[:i] - } else { - u.Opaque = "/" - } - - u.Host = s - - if strings.Contains(u.Host, "@") { - // Don't bother parsing user information because user information is - // not allowed in websocket URIs. - return nil, errMalformedURL - } - - return &u, nil +// Dial creates a new client connection by calling DialContext with a background context. +func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + return d.DialContext(context.Background(), urlStr, requestHeader) } +var errMalformedURL = errors.New("malformed ws or wss URL") + func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { hostPort = u.Host hostNoPort = u.Host @@ -150,26 +137,29 @@ func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { return hostPort, hostNoPort } -// DefaultDialer is a dialer with all fields set to the default zero values. +// DefaultDialer is a dialer with all fields set to the default values. var DefaultDialer = &Dialer{ - Proxy: http.ProxyFromEnvironment, + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, } -// Dial creates a new client connection. Use requestHeader to specify the +// nilDialer is dialer to use when receiver is nil. +var nilDialer = *DefaultDialer + +// DialContext creates a new client connection. Use requestHeader to specify the // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). // Use the response.Header to get the selected subprotocol // (Sec-WebSocket-Protocol) and cookies (Set-Cookie). // +// The context will be used in the request and in the Dialer. +// // If the WebSocket handshake fails, ErrBadHandshake is returned along with a // non-nil *http.Response so that callers can handle redirects, authentication, // etcetera. The response body may not contain the entire response and does not // need to be closed by the application. -func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { - +func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { if d == nil { - d = &Dialer{ - Proxy: http.ProxyFromEnvironment, - } + d = &nilDialer } challengeKey, err := generateChallengeKey() @@ -177,7 +167,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re return nil, nil, err } - u, err := parseURL(urlStr) + u, err := url.Parse(urlStr) if err != nil { return nil, nil, err } @@ -197,7 +187,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } req := &http.Request{ - Method: "GET", + Method: http.MethodGet, URL: u, Proto: "HTTP/1.1", ProtoMajor: 1, @@ -205,6 +195,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re Header: make(http.Header), Host: u.Host, } + req = req.WithContext(ctx) // Set the cookies present in the cookie jar of the dialer if d.Jar != nil { @@ -237,48 +228,105 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re k == "Sec-Websocket-Extensions" || (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0): return nil, nil, errors.New("websocket: duplicate header not allowed: " + k) + case k == "Sec-Websocket-Protocol": + req.Header["Sec-WebSocket-Protocol"] = vs default: req.Header[k] = vs } } if d.EnableCompression { - req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") + req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} } - hostPort, hostNoPort := hostPortNoPort(u) + if d.HandshakeTimeout != 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() + } - var proxyURL *url.URL - // Check wether the proxy method has been configured - if d.Proxy != nil { - proxyURL, err = d.Proxy(req) + // Get network dial function. + var netDial func(network, add string) (net.Conn, error) + + switch u.Scheme { + case "http": + if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } + case "https": + if d.NetDialTLSContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialTLSContext(ctx, network, addr) + } + } else if d.NetDialContext != nil { + netDial = func(network, addr string) (net.Conn, error) { + return d.NetDialContext(ctx, network, addr) + } + } else if d.NetDial != nil { + netDial = d.NetDial + } + default: + return nil, nil, errMalformedURL } - if err != nil { - return nil, nil, err + + if netDial == nil { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } } - var targetHostPort string - if proxyURL != nil { - targetHostPort, _ = hostPortNoPort(proxyURL) - } else { - targetHostPort = hostPort + // If needed, wrap the dial function to set the connection deadline. + if deadline, ok := ctx.Deadline(); ok { + forwardDial := netDial + netDial = func(network, addr string) (net.Conn, error) { + c, err := forwardDial(network, addr) + if err != nil { + return nil, err + } + err = c.SetDeadline(deadline) + if err != nil { + c.Close() + return nil, err + } + return c, nil + } } - var deadline time.Time - if d.HandshakeTimeout != 0 { - deadline = time.Now().Add(d.HandshakeTimeout) + // If needed, wrap the dial function to connect through a proxy. + if d.Proxy != nil { + proxyURL, err := d.Proxy(req) + if err != nil { + return nil, nil, err + } + if proxyURL != nil { + dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) + if err != nil { + return nil, nil, err + } + netDial = dialer.Dial + } } - netDial := d.NetDial - if netDial == nil { - netDialer := &net.Dialer{Deadline: deadline} - netDial = netDialer.Dial + hostPort, hostNoPort := hostPortNoPort(u) + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) } - netConn, err := netDial("tcp", targetHostPort) + netConn, err := netDial("tcp", hostPort) if err != nil { return nil, nil, err } + if trace != nil && trace.GotConn != nil { + trace.GotConn(httptrace.GotConnInfo{ + Conn: netConn, + }) + } defer func() { if netConn != nil { @@ -286,67 +334,54 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } }() - if err := netConn.SetDeadline(deadline); err != nil { - return nil, nil, err - } - - if proxyURL != nil { - connectHeader := make(http.Header) - if user := proxyURL.User; user != nil { - proxyUser := user.Username() - if proxyPassword, passwordSet := user.Password(); passwordSet { - credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) - connectHeader.Set("Proxy-Authorization", "Basic "+credential) - } - } - connectReq := &http.Request{ - Method: "CONNECT", - URL: &url.URL{Opaque: hostPort}, - Host: hostPort, - Header: connectHeader, - } - - connectReq.Write(netConn) + if u.Scheme == "https" && d.NetDialTLSContext == nil { + // If NetDialTLSContext is set, assume that the TLS handshake has already been done - // Read response. - // Okay to use and discard buffered reader here, because - // TLS server will not speak until spoken to. - br := bufio.NewReader(netConn) - resp, err := http.ReadResponse(br, connectReq) - if err != nil { - return nil, nil, err - } - if resp.StatusCode != 200 { - f := strings.SplitN(resp.Status, " ", 2) - return nil, nil, errors.New(f[1]) - } - } - - if u.Scheme == "https" { cfg := cloneTLSConfig(d.TLSClientConfig) if cfg.ServerName == "" { cfg.ServerName = hostNoPort } tlsConn := tls.Client(netConn, cfg) netConn = tlsConn - if err := tlsConn.Handshake(); err != nil { - return nil, nil, err + + if trace != nil && trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() } - if !cfg.InsecureSkipVerify { - if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { - return nil, nil, err - } + err := doHandshake(ctx, tlsConn, cfg) + if trace != nil && trace.TLSHandshakeDone != nil { + trace.TLSHandshakeDone(tlsConn.ConnectionState(), err) + } + + if err != nil { + return nil, nil, err } } - conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) + conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil) if err := req.Write(netConn); err != nil { return nil, nil, err } + if trace != nil && trace.GotFirstResponseByte != nil { + if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 { + trace.GotFirstResponseByte() + } + } + resp, err := http.ReadResponse(conn.br, req) if err != nil { + if d.TLSClientConfig != nil { + for _, proto := range d.TLSClientConfig.NextProtos { + if proto != "http/1.1" { + return nil, nil, fmt.Errorf( + "websocket: protocol %q was given but is not supported;"+ + "sharing tls.Config with net/http Transport can cause this error: %w", + proto, err, + ) + } + } + } return nil, nil, err } @@ -357,8 +392,8 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re } if resp.StatusCode != 101 || - !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") || - !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") || + !tokenListContainsValue(resp.Header, "Upgrade", "websocket") || + !tokenListContainsValue(resp.Header, "Connection", "upgrade") || resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) { // Before closing the network connection on return from this // function, slurp up some of the response to aid application @@ -390,3 +425,10 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re netConn = nil // to avoid close in defer. return conn, resp, nil } + +func cloneTLSConfig(cfg *tls.Config) *tls.Config { + if cfg == nil { + return &tls.Config{} + } + return cfg.Clone() +} diff --git a/vendor/github.com/gorilla/websocket/client_clone.go b/vendor/github.com/gorilla/websocket/client_clone.go deleted file mode 100644 index 4f0d943..0000000 --- a/vendor/github.com/gorilla/websocket/client_clone.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.8 - -package websocket - -import "crypto/tls" - -func cloneTLSConfig(cfg *tls.Config) *tls.Config { - if cfg == nil { - return &tls.Config{} - } - return cfg.Clone() -} diff --git a/vendor/github.com/gorilla/websocket/client_clone_legacy.go b/vendor/github.com/gorilla/websocket/client_clone_legacy.go deleted file mode 100644 index babb007..0000000 --- a/vendor/github.com/gorilla/websocket/client_clone_legacy.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.8 - -package websocket - -import "crypto/tls" - -// cloneTLSConfig clones all public fields except the fields -// SessionTicketsDisabled and SessionTicketKey. This avoids copying the -// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a -// config in active use. -func cloneTLSConfig(cfg *tls.Config) *tls.Config { - if cfg == nil { - return &tls.Config{} - } - return &tls.Config{ - Rand: cfg.Rand, - Time: cfg.Time, - Certificates: cfg.Certificates, - NameToCertificate: cfg.NameToCertificate, - GetCertificate: cfg.GetCertificate, - RootCAs: cfg.RootCAs, - NextProtos: cfg.NextProtos, - ServerName: cfg.ServerName, - ClientAuth: cfg.ClientAuth, - ClientCAs: cfg.ClientCAs, - InsecureSkipVerify: cfg.InsecureSkipVerify, - CipherSuites: cfg.CipherSuites, - PreferServerCipherSuites: cfg.PreferServerCipherSuites, - ClientSessionCache: cfg.ClientSessionCache, - MinVersion: cfg.MinVersion, - MaxVersion: cfg.MaxVersion, - CurvePreferences: cfg.CurvePreferences, - } -} diff --git a/vendor/github.com/gorilla/websocket/conn.go b/vendor/github.com/gorilla/websocket/conn.go index 97e1dba..5161ef8 100644 --- a/vendor/github.com/gorilla/websocket/conn.go +++ b/vendor/github.com/gorilla/websocket/conn.go @@ -13,6 +13,7 @@ import ( "math/rand" "net" "strconv" + "strings" "sync" "time" "unicode/utf8" @@ -76,7 +77,7 @@ const ( // is UTF-8 encoded text. PingMessage = 9 - // PongMessage denotes a ping control message. The optional message payload + // PongMessage denotes a pong control message. The optional message payload // is UTF-8 encoded text. PongMessage = 10 ) @@ -100,9 +101,8 @@ func (e *netError) Error() string { return e.msg } func (e *netError) Temporary() bool { return e.temporary } func (e *netError) Timeout() bool { return e.timeout } -// CloseError represents close frame. +// CloseError represents a close message. type CloseError struct { - // Code is defined in RFC 6455, section 11.7. Code int @@ -224,6 +224,20 @@ func isValidReceivedCloseCode(code int) bool { return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) } +// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this +// interface. The type of the value stored in a pool is not specified. +type BufferPool interface { + // Get gets a value from the pool or returns nil if the pool is empty. + Get() interface{} + // Put adds a value to the pool. + Put(interface{}) +} + +// writePoolData is the type added to the write buffer pool. This wrapper is +// used to prevent applications from peeking at and depending on the values +// added to the pool. +type writePoolData struct{ buf []byte } + // The Conn type represents a WebSocket connection. type Conn struct { conn net.Conn @@ -231,8 +245,10 @@ type Conn struct { subprotocol string // Write fields - mu chan bool // used as mutex to protect write to conn - writeBuf []byte // frame is constructed in this buffer. + mu chan struct{} // used as mutex to protect write to conn + writeBuf []byte // frame is constructed in this buffer. + writePool BufferPool + writeBufSize int writeDeadline time.Time writer io.WriteCloser // the current writer returned to the application isWriting bool // for best-effort concurrent write detection @@ -245,10 +261,12 @@ type Conn struct { newCompressionWriter func(io.WriteCloser, int) io.WriteCloser // Read fields - reader io.ReadCloser // the current reader returned to the application - readErr error - br *bufio.Reader - readRemaining int64 // bytes remaining in current frame. + reader io.ReadCloser // the current reader returned to the application + readErr error + br *bufio.Reader + // bytes remaining in current frame. + // set setReadRemaining to safely update this value and prevent overflow + readRemaining int64 readFinal bool // true the current message has more frames. readLength int64 // Message size. readLimit int64 // Maximum message size. @@ -264,64 +282,29 @@ type Conn struct { newDecompressionReader func(io.Reader) io.ReadCloser } -func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { - return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil) -} +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { -type writeHook struct { - p []byte -} - -func (wh *writeHook) Write(p []byte) (int, error) { - wh.p = p - return len(p), nil -} - -func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn { - mu := make(chan bool, 1) - mu <- true - - var br *bufio.Reader - if readBufferSize == 0 && brw != nil && brw.Reader != nil { - // Reuse the supplied bufio.Reader if the buffer has a useful size. - // This code assumes that peek on a reader returns - // bufio.Reader.buf[:0]. - brw.Reader.Reset(conn) - if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 { - br = brw.Reader - } - } if br == nil { if readBufferSize == 0 { readBufferSize = defaultReadBufferSize - } - if readBufferSize < maxControlFramePayloadSize { + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame readBufferSize = maxControlFramePayloadSize } br = bufio.NewReaderSize(conn, readBufferSize) } - var writeBuf []byte - if writeBufferSize == 0 && brw != nil && brw.Writer != nil { - // Use the bufio.Writer's buffer if the buffer has a useful size. This - // code assumes that bufio.Writer.buf[:1] is passed to the - // bufio.Writer's underlying writer. - var wh writeHook - brw.Writer.Reset(&wh) - brw.Writer.WriteByte(0) - brw.Flush() - if cap(wh.p) >= maxFrameHeaderSize+256 { - writeBuf = wh.p[:cap(wh.p)] - } + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize } + writeBufferSize += maxFrameHeaderSize - if writeBuf == nil { - if writeBufferSize == 0 { - writeBufferSize = defaultWriteBufferSize - } - writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize) + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) } + mu := make(chan struct{}, 1) + mu <- struct{}{} c := &Conn{ isServer: isServer, br: br, @@ -329,6 +312,8 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in mu: mu, readFinal: true, writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, enableWriteCompression: true, compressionLevel: defaultCompressionLevel, } @@ -338,12 +323,24 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in return c } +// setReadRemaining tracks the number of bytes remaining on the connection. If n +// overflows, an ErrReadLimit is returned. +func (c *Conn) setReadRemaining(n int64) error { + if n < 0 { + return ErrReadLimit + } + + c.readRemaining = n + return nil +} + // Subprotocol returns the negotiated protocol for the connection. func (c *Conn) Subprotocol() string { return c.subprotocol } -// Close closes the underlying network connection without sending or waiting for a close frame. +// Close closes the underlying network connection without sending or waiting +// for a close message. func (c *Conn) Close() error { return c.conn.Close() } @@ -370,9 +367,18 @@ func (c *Conn) writeFatal(err error) error { return err } -func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { +func (c *Conn) read(n int) ([]byte, error) { + p, err := c.br.Peek(n) + if err == io.EOF { + err = errUnexpectedEOF + } + c.br.Discard(len(p)) + return p, err +} + +func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { <-c.mu - defer func() { c.mu <- true }() + defer func() { c.mu <- struct{}{} }() c.writeErrMu.Lock() err := c.writeErr @@ -382,21 +388,26 @@ func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error { } c.conn.SetWriteDeadline(deadline) - for _, buf := range bufs { - if len(buf) > 0 { - _, err := c.conn.Write(buf) - if err != nil { - return c.writeFatal(err) - } - } + if len(buf1) == 0 { + _, err = c.conn.Write(buf0) + } else { + err = c.writeBufs(buf0, buf1) + } + if err != nil { + return c.writeFatal(err) } - if frameType == CloseMessage { c.writeFatal(ErrCloseSent) } return nil } +func (c *Conn) writeBufs(bufs ...[]byte) error { + b := net.Buffers(bufs) + _, err := b.WriteTo(c.conn) + return err +} + // WriteControl writes a control message with the given deadline. The allowed // message types are CloseMessage, PingMessage and PongMessage. func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error { @@ -425,7 +436,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er maskBytes(key, 0, buf[6:]) } - d := time.Hour * 1000 + d := 1000 * time.Hour if !deadline.IsZero() { d = deadline.Sub(time.Now()) if d < 0 { @@ -440,7 +451,7 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er case <-timer.C: return errWriteTimeout } - defer func() { c.mu <- true }() + defer func() { c.mu <- struct{}{} }() c.writeErrMu.Lock() err := c.writeErr @@ -460,7 +471,8 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er return err } -func (c *Conn) prepWrite(messageType int) error { +// beginMessage prepares a connection and message writer for a new message. +func (c *Conn) beginMessage(mw *messageWriter, messageType int) error { // Close previous writer if not already closed by the application. It's // probably better to return an error in this situation, but we cannot // change this without breaking existing applications. @@ -476,7 +488,23 @@ func (c *Conn) prepWrite(messageType int) error { c.writeErrMu.Lock() err := c.writeErr c.writeErrMu.Unlock() - return err + if err != nil { + return err + } + + mw.c = c + mw.frameType = messageType + mw.pos = maxFrameHeaderSize + + if c.writeBuf == nil { + wpd, ok := c.writePool.Get().(writePoolData) + if ok { + c.writeBuf = wpd.buf + } else { + c.writeBuf = make([]byte, c.writeBufSize) + } + } + return nil } // NextWriter returns a writer for the next message to send. The writer's Close @@ -484,17 +512,15 @@ func (c *Conn) prepWrite(messageType int) error { // // There can be at most one open writer on a connection. NextWriter closes the // previous writer if the application has not already done so. +// +// All message types (TextMessage, BinaryMessage, CloseMessage, PingMessage and +// PongMessage) are supported. func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) { - if err := c.prepWrite(messageType); err != nil { + var mw messageWriter + if err := c.beginMessage(&mw, messageType); err != nil { return nil, err } - - mw := &messageWriter{ - c: c, - frameType: messageType, - pos: maxFrameHeaderSize, - } - c.writer = mw + c.writer = &mw if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) { w := c.newCompressionWriter(c.writer, c.compressionLevel) mw.compress = true @@ -511,10 +537,16 @@ type messageWriter struct { err error } -func (w *messageWriter) fatal(err error) error { +func (w *messageWriter) endMessage(err error) error { if w.err != nil { - w.err = err - w.c.writer = nil + return err + } + c := w.c + w.err = err + c.writer = nil + if c.writePool != nil { + c.writePool.Put(writePoolData{buf: c.writeBuf}) + c.writeBuf = nil } return err } @@ -528,7 +560,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error { // Check for invalid control frames. if isControl(w.frameType) && (!final || length > maxControlFramePayloadSize) { - return w.fatal(errInvalidControlFrame) + return w.endMessage(errInvalidControlFrame) } b0 := byte(w.frameType) @@ -573,7 +605,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error { copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos]) if len(extra) > 0 { - return c.writeFatal(errors.New("websocket: internal error, extra used in client mode")) + return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode"))) } } @@ -594,11 +626,11 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error { c.isWriting = false if err != nil { - return w.fatal(err) + return w.endMessage(err) } if final { - c.writer = nil + w.endMessage(errWriteClosed) return nil } @@ -696,11 +728,7 @@ func (w *messageWriter) Close() error { if w.err != nil { return w.err } - if err := w.flushFrame(true, nil); err != nil { - return err - } - w.err = errWriteClosed - return nil + return w.flushFrame(true, nil) } // WritePreparedMessage writes prepared message into connection. @@ -732,10 +760,10 @@ func (c *Conn) WriteMessage(messageType int, data []byte) error { if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) { // Fast path with no allocations and single frame. - if err := c.prepWrite(messageType); err != nil { + var mw messageWriter + if err := c.beginMessage(&mw, messageType); err != nil { return err } - mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize} n := copy(c.writeBuf[mw.pos:], data) mw.pos += n data = data[n:] @@ -764,7 +792,6 @@ func (c *Conn) SetWriteDeadline(t time.Time) error { // Read methods func (c *Conn) advanceFrame() (int, error) { - // 1. Skip remainder of previous frame. if c.readRemaining > 0 { @@ -774,50 +801,82 @@ func (c *Conn) advanceFrame() (int, error) { } // 2. Read and parse first two bytes of frame header. + // To aid debugging, collect and report all errors in the first two bytes + // of the header. + + var errors []string p, err := c.read(2) if err != nil { return noFrame, err } - final := p[0]&finalBit != 0 frameType := int(p[0] & 0xf) + final := p[0]&finalBit != 0 + rsv1 := p[0]&rsv1Bit != 0 + rsv2 := p[0]&rsv2Bit != 0 + rsv3 := p[0]&rsv3Bit != 0 mask := p[1]&maskBit != 0 - c.readRemaining = int64(p[1] & 0x7f) + c.setReadRemaining(int64(p[1] & 0x7f)) c.readDecompress = false - if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 { - c.readDecompress = true - p[0] &^= rsv1Bit + if rsv1 { + if c.newDecompressionReader != nil { + c.readDecompress = true + } else { + errors = append(errors, "RSV1 set") + } + } + + if rsv2 { + errors = append(errors, "RSV2 set") } - if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 { - return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16)) + if rsv3 { + errors = append(errors, "RSV3 set") } switch frameType { case CloseMessage, PingMessage, PongMessage: if c.readRemaining > maxControlFramePayloadSize { - return noFrame, c.handleProtocolError("control frame length > 125") + errors = append(errors, "len > 125 for control") } if !final { - return noFrame, c.handleProtocolError("control frame not final") + errors = append(errors, "FIN not set on control") } case TextMessage, BinaryMessage: if !c.readFinal { - return noFrame, c.handleProtocolError("message start before final message frame") + errors = append(errors, "data before FIN") } c.readFinal = final case continuationFrame: if c.readFinal { - return noFrame, c.handleProtocolError("continuation after final message frame") + errors = append(errors, "continuation after FIN") } c.readFinal = final default: - return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType)) + errors = append(errors, "bad opcode "+strconv.Itoa(frameType)) + } + + if mask != c.isServer { + errors = append(errors, "bad MASK") + } + + if len(errors) > 0 { + return noFrame, c.handleProtocolError(strings.Join(errors, ", ")) } - // 3. Read and parse frame length. + // 3. Read and parse frame length as per + // https://tools.ietf.org/html/rfc6455#section-5.2 + // + // The length of the "Payload data", in bytes: if 0-125, that is the payload + // length. + // - If 126, the following 2 bytes interpreted as a 16-bit unsigned + // integer are the payload length. + // - If 127, the following 8 bytes interpreted as + // a 64-bit unsigned integer (the most significant bit MUST be 0) are the + // payload length. Multibyte length quantities are expressed in network byte + // order. switch c.readRemaining { case 126: @@ -825,21 +884,23 @@ func (c *Conn) advanceFrame() (int, error) { if err != nil { return noFrame, err } - c.readRemaining = int64(binary.BigEndian.Uint16(p)) + + if err := c.setReadRemaining(int64(binary.BigEndian.Uint16(p))); err != nil { + return noFrame, err + } case 127: p, err := c.read(8) if err != nil { return noFrame, err } - c.readRemaining = int64(binary.BigEndian.Uint64(p)) + + if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil { + return noFrame, err + } } // 4. Handle frame masking. - if mask != c.isServer { - return noFrame, c.handleProtocolError("incorrect mask flag") - } - if mask { c.readMaskPos = 0 p, err := c.read(len(c.readMaskKey)) @@ -854,6 +915,12 @@ func (c *Conn) advanceFrame() (int, error) { if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage { c.readLength += c.readRemaining + // Don't allow readLength to overflow in the presence of a large readRemaining + // counter. + if c.readLength < 0 { + return noFrame, ErrReadLimit + } + if c.readLimit > 0 && c.readLength > c.readLimit { c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) return noFrame, ErrReadLimit @@ -867,7 +934,7 @@ func (c *Conn) advanceFrame() (int, error) { var payload []byte if c.readRemaining > 0 { payload, err = c.read(int(c.readRemaining)) - c.readRemaining = 0 + c.setReadRemaining(0) if err != nil { return noFrame, err } @@ -893,7 +960,7 @@ func (c *Conn) advanceFrame() (int, error) { if len(payload) >= 2 { closeCode = int(binary.BigEndian.Uint16(payload)) if !isValidReceivedCloseCode(closeCode) { - return noFrame, c.handleProtocolError("invalid close code") + return noFrame, c.handleProtocolError("bad close code " + strconv.Itoa(closeCode)) } closeText = string(payload[2:]) if !utf8.ValidString(closeText) { @@ -910,7 +977,11 @@ func (c *Conn) advanceFrame() (int, error) { } func (c *Conn) handleProtocolError(message string) error { - c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait)) + data := FormatCloseMessage(CloseProtocolError, message) + if len(data) > maxControlFramePayloadSize { + data = data[:maxControlFramePayloadSize] + } + c.WriteControl(CloseMessage, data, time.Now().Add(writeWait)) return errors.New("websocket: " + message) } @@ -940,6 +1011,7 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { c.readErr = hideTempErr(err) break } + if frameType == TextMessage || frameType == BinaryMessage { c.messageReader = &messageReader{c} c.reader = c.messageReader @@ -980,7 +1052,9 @@ func (r *messageReader) Read(b []byte) (int, error) { if c.isServer { c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) } - c.readRemaining -= int64(n) + rem := c.readRemaining + rem -= int64(n) + c.setReadRemaining(rem) if c.readRemaining > 0 && c.readErr == io.EOF { c.readErr = errUnexpectedEOF } @@ -1032,8 +1106,8 @@ func (c *Conn) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) } -// SetReadLimit sets the maximum size for a message read from the peer. If a -// message exceeds the limit, the connection sends a close frame to the peer +// SetReadLimit sets the maximum size in bytes for a message read from the peer. If a +// message exceeds the limit, the connection sends a close message to the peer // and returns ErrReadLimit to the application. func (c *Conn) SetReadLimit(limit int64) { c.readLimit = limit @@ -1046,24 +1120,22 @@ func (c *Conn) CloseHandler() func(code int, text string) error { // SetCloseHandler sets the handler for close messages received from the peer. // The code argument to h is the received close code or CloseNoStatusReceived -// if the close message is empty. The default close handler sends a close frame -// back to the peer. +// if the close message is empty. The default close handler sends a close +// message back to the peer. // -// The application must read the connection to process close messages as -// described in the section on Control Frames above. +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// close messages as described in the section on Control Messages above. // -// The connection read methods return a CloseError when a close frame is +// The connection read methods return a CloseError when a close message is // received. Most applications should handle close messages as part of their // normal error handling. Applications should only set a close handler when the -// application must perform some action before sending a close frame back to +// application must perform some action before sending a close message back to // the peer. func (c *Conn) SetCloseHandler(h func(code int, text string) error) { if h == nil { h = func(code int, text string) error { - message := []byte{} - if code != CloseNoStatusReceived { - message = FormatCloseMessage(code, "") - } + message := FormatCloseMessage(code, "") c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) return nil } @@ -1077,11 +1149,12 @@ func (c *Conn) PingHandler() func(appData string) error { } // SetPingHandler sets the handler for ping messages received from the peer. -// The appData argument to h is the PING frame application data. The default +// The appData argument to h is the PING message application data. The default // ping handler sends a pong to the peer. // -// The application must read the connection to process ping messages as -// described in the section on Control Frames above. +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// ping messages as described in the section on Control Messages above. func (c *Conn) SetPingHandler(h func(appData string) error) { if h == nil { h = func(message string) error { @@ -1103,11 +1176,12 @@ func (c *Conn) PongHandler() func(appData string) error { } // SetPongHandler sets the handler for pong messages received from the peer. -// The appData argument to h is the PONG frame application data. The default +// The appData argument to h is the PONG message application data. The default // pong handler does nothing. // -// The application must read the connection to process ping messages as -// described in the section on Control Frames above. +// The handler function is called from the NextReader, ReadMessage and message +// reader Read methods. The application must read the connection to process +// pong messages as described in the section on Control Messages above. func (c *Conn) SetPongHandler(h func(appData string) error) { if h == nil { h = func(string) error { return nil } @@ -1115,8 +1189,16 @@ func (c *Conn) SetPongHandler(h func(appData string) error) { c.handlePong = h } +// NetConn returns the underlying connection that is wrapped by c. +// Note that writing to or reading from this connection directly will corrupt the +// WebSocket connection. +func (c *Conn) NetConn() net.Conn { + return c.conn +} + // UnderlyingConn returns the internal net.Conn. This can be used to further // modifications to connection specific flags. +// Deprecated: Use the NetConn method. func (c *Conn) UnderlyingConn() net.Conn { return c.conn } @@ -1141,7 +1223,14 @@ func (c *Conn) SetCompressionLevel(level int) error { } // FormatCloseMessage formats closeCode and text as a WebSocket close message. +// An empty message is returned for code CloseNoStatusReceived. func FormatCloseMessage(closeCode int, text string) []byte { + if closeCode == CloseNoStatusReceived { + // Return empty message because it's illegal to send + // CloseNoStatusReceived. Return non-nil value in case application + // checks for nil. + return []byte{} + } buf := make([]byte, 2+len(text)) binary.BigEndian.PutUint16(buf, uint16(closeCode)) copy(buf[2:], text) diff --git a/vendor/github.com/gorilla/websocket/conn_read.go b/vendor/github.com/gorilla/websocket/conn_read.go deleted file mode 100644 index 1ea1505..0000000 --- a/vendor/github.com/gorilla/websocket/conn_read.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.5 - -package websocket - -import "io" - -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - c.br.Discard(len(p)) - return p, err -} diff --git a/vendor/github.com/gorilla/websocket/conn_read_legacy.go b/vendor/github.com/gorilla/websocket/conn_read_legacy.go deleted file mode 100644 index 018541c..0000000 --- a/vendor/github.com/gorilla/websocket/conn_read_legacy.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !go1.5 - -package websocket - -import "io" - -func (c *Conn) read(n int) ([]byte, error) { - p, err := c.br.Peek(n) - if err == io.EOF { - err = errUnexpectedEOF - } - if len(p) > 0 { - // advance over the bytes just read - io.ReadFull(c.br, p) - } - return p, err -} diff --git a/vendor/github.com/gorilla/websocket/doc.go b/vendor/github.com/gorilla/websocket/doc.go index e291a95..8db0cef 100644 --- a/vendor/github.com/gorilla/websocket/doc.go +++ b/vendor/github.com/gorilla/websocket/doc.go @@ -6,9 +6,8 @@ // // Overview // -// The Conn type represents a WebSocket connection. A server application uses -// the Upgrade function from an Upgrader object with a HTTP request handler -// to get a pointer to a Conn: +// The Conn type represents a WebSocket connection. A server application calls +// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn: // // var upgrader = websocket.Upgrader{ // ReadBufferSize: 1024, @@ -31,10 +30,12 @@ // for { // messageType, p, err := conn.ReadMessage() // if err != nil { +// log.Println(err) // return // } -// if err = conn.WriteMessage(messageType, p); err != nil { -// return err +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return // } // } // @@ -85,20 +86,26 @@ // and pong. Call the connection WriteControl, WriteMessage or NextWriter // methods to send a control message to the peer. // -// Connections handle received close messages by sending a close message to the -// peer and returning a *CloseError from the the NextReader, ReadMessage or the -// message Read method. +// Connections handle received close messages by calling the handler function +// set with the SetCloseHandler method and by returning a *CloseError from the +// NextReader, ReadMessage or the message Read method. The default close +// handler sends a close message to the peer. // -// Connections handle received ping and pong messages by invoking callback -// functions set with SetPingHandler and SetPongHandler methods. The callback -// functions are called from the NextReader, ReadMessage and the message Read -// methods. +// Connections handle received ping messages by calling the handler function +// set with the SetPingHandler method. The default ping handler sends a pong +// message to the peer. +// +// Connections handle received pong messages by calling the handler function +// set with the SetPongHandler method. The default pong handler does nothing. +// If an application sends ping messages, then the application should set a +// pong handler to receive the corresponding pong. // -// The default ping handler sends a pong to the peer. The application's reading -// goroutine can block for a short time while the handler writes the pong data -// to the connection. +// The control message handler functions are called from the NextReader, +// ReadMessage and message reader Read methods. The default close and ping +// handlers can block these methods for a short time when the handler writes to +// the connection. // -// The application must read the connection to process ping, pong and close +// The application must read the connection to process close, ping and pong // messages sent from the peer. If the application is not otherwise interested // in messages from the peer, then the application should start a goroutine to // read and discard messages from the peer. A simple example is: @@ -137,19 +144,59 @@ // method fails the WebSocket handshake with HTTP status 403. // // If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail -// the handshake if the Origin request header is present and not equal to the -// Host request header. -// -// An application can allow connections from any origin by specifying a -// function that always returns true: -// -// var upgrader = websocket.Upgrader{ -// CheckOrigin: func(r *http.Request) bool { return true }, -// } -// -// The deprecated Upgrade function does not enforce an origin policy. It's the -// application's responsibility to check the Origin header before calling -// Upgrade. +// the handshake if the Origin request header is present and the Origin host is +// not equal to the Host request header. +// +// The deprecated package-level Upgrade function does not perform origin +// checking. The application is responsible for checking the Origin header +// before calling the Upgrade function. +// +// Buffers +// +// Connections buffer network input and output to reduce the number +// of system calls when reading or writing messages. +// +// Write buffers are also used for constructing WebSocket frames. See RFC 6455, +// Section 5 for a discussion of message framing. A WebSocket frame header is +// written to the network each time a write buffer is flushed to the network. +// Decreasing the size of the write buffer can increase the amount of framing +// overhead on the connection. +// +// The buffer sizes in bytes are specified by the ReadBufferSize and +// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default +// size of 4096 when a buffer size field is set to zero. The Upgrader reuses +// buffers created by the HTTP server when a buffer size field is set to zero. +// The HTTP server buffers have a size of 4096 at the time of this writing. +// +// The buffer sizes do not limit the size of a message that can be read or +// written by a connection. +// +// Buffers are held for the lifetime of the connection by default. If the +// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the +// write buffer only when writing a message. +// +// Applications should tune the buffer sizes to balance memory use and +// performance. Increasing the buffer size uses more memory, but can reduce the +// number of system calls to read or write the network. In the case of writing, +// increasing the buffer size can reduce the number of frame headers written to +// the network. +// +// Some guidelines for setting buffer parameters are: +// +// Limit the buffer sizes to the maximum expected message size. Buffers larger +// than the largest message do not provide any benefit. +// +// Depending on the distribution of message sizes, setting the buffer size to +// a value less than the maximum expected message size can greatly reduce memory +// use with a small impact on performance. Here's an example: If 99% of the +// messages are smaller than 256 bytes and the maximum message size is 512 +// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls +// than a buffer size of 512 bytes. The memory savings is 50%. +// +// A write buffer pool is useful when the application has a modest number +// writes over a large number of connections. when buffers are pooled, a larger +// buffer size has a reduced impact on total memory use and has the benefit of +// reducing system calls and frame overhead. // // Compression EXPERIMENTAL // diff --git a/vendor/github.com/gorilla/websocket/join.go b/vendor/github.com/gorilla/websocket/join.go new file mode 100644 index 0000000..c64f8c8 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/join.go @@ -0,0 +1,42 @@ +// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "io" + "strings" +) + +// JoinMessages concatenates received messages to create a single io.Reader. +// The string term is appended to each message. The returned reader does not +// support concurrent calls to the Read method. +func JoinMessages(c *Conn, term string) io.Reader { + return &joinReader{c: c, term: term} +} + +type joinReader struct { + c *Conn + term string + r io.Reader +} + +func (r *joinReader) Read(p []byte) (int, error) { + if r.r == nil { + var err error + _, r.r, err = r.c.NextReader() + if err != nil { + return 0, err + } + if r.term != "" { + r.r = io.MultiReader(r.r, strings.NewReader(r.term)) + } + } + n, err := r.r.Read(p) + if err == io.EOF { + err = nil + r.r = nil + } + return n, err +} diff --git a/vendor/github.com/gorilla/websocket/json.go b/vendor/github.com/gorilla/websocket/json.go index 4f0e368..dc2c1f6 100644 --- a/vendor/github.com/gorilla/websocket/json.go +++ b/vendor/github.com/gorilla/websocket/json.go @@ -9,12 +9,14 @@ import ( "io" ) -// WriteJSON is deprecated, use c.WriteJSON instead. +// WriteJSON writes the JSON encoding of v as a message. +// +// Deprecated: Use c.WriteJSON instead. func WriteJSON(c *Conn, v interface{}) error { return c.WriteJSON(v) } -// WriteJSON writes the JSON encoding of v to the connection. +// WriteJSON writes the JSON encoding of v as a message. // // See the documentation for encoding/json Marshal for details about the // conversion of Go values to JSON. @@ -31,7 +33,10 @@ func (c *Conn) WriteJSON(v interface{}) error { return err2 } -// ReadJSON is deprecated, use c.ReadJSON instead. +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// Deprecated: Use c.ReadJSON instead. func ReadJSON(c *Conn, v interface{}) error { return c.ReadJSON(v) } diff --git a/vendor/github.com/gorilla/websocket/mask.go b/vendor/github.com/gorilla/websocket/mask.go index 6a88bbc..d0742bf 100644 --- a/vendor/github.com/gorilla/websocket/mask.go +++ b/vendor/github.com/gorilla/websocket/mask.go @@ -2,6 +2,7 @@ // this source code is governed by a BSD-style license that can be found in the // LICENSE file. +//go:build !appengine // +build !appengine package websocket @@ -11,7 +12,6 @@ import "unsafe" const wordSize = int(unsafe.Sizeof(uintptr(0))) func maskBytes(key [4]byte, pos int, b []byte) int { - // Mask one byte at a time for small buffers. if len(b) < 2*wordSize { for i := range b { diff --git a/vendor/github.com/gorilla/websocket/mask_safe.go b/vendor/github.com/gorilla/websocket/mask_safe.go index 2aac060..36250ca 100644 --- a/vendor/github.com/gorilla/websocket/mask_safe.go +++ b/vendor/github.com/gorilla/websocket/mask_safe.go @@ -2,6 +2,7 @@ // this source code is governed by a BSD-style license that can be found in the // LICENSE file. +//go:build appengine // +build appengine package websocket diff --git a/vendor/github.com/gorilla/websocket/prepared.go b/vendor/github.com/gorilla/websocket/prepared.go index 1efffbd..c854225 100644 --- a/vendor/github.com/gorilla/websocket/prepared.go +++ b/vendor/github.com/gorilla/websocket/prepared.go @@ -19,7 +19,6 @@ import ( type PreparedMessage struct { messageType int data []byte - err error mu sync.Mutex frames map[prepareKey]*preparedFrame } @@ -74,8 +73,8 @@ func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) { // Prepare a frame using a 'fake' connection. // TODO: Refactor code in conn.go to allow more direct construction of // the frame. - mu := make(chan bool, 1) - mu <- true + mu := make(chan struct{}, 1) + mu <- struct{}{} var nc prepareConn c := &Conn{ conn: &nc, diff --git a/vendor/github.com/gorilla/websocket/proxy.go b/vendor/github.com/gorilla/websocket/proxy.go new file mode 100644 index 0000000..e0f466b --- /dev/null +++ b/vendor/github.com/gorilla/websocket/proxy.go @@ -0,0 +1,77 @@ +// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "encoding/base64" + "errors" + "net" + "net/http" + "net/url" + "strings" +) + +type netDialerFunc func(network, addr string) (net.Conn, error) + +func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { + return fn(network, addr) +} + +func init() { + proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { + return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil + }) +} + +type httpProxyDialer struct { + proxyURL *url.URL + forwardDial func(network, addr string) (net.Conn, error) +} + +func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { + hostPort, _ := hostPortNoPort(hpd.proxyURL) + conn, err := hpd.forwardDial(network, hostPort) + if err != nil { + return nil, err + } + + connectHeader := make(http.Header) + if user := hpd.proxyURL.User; user != nil { + proxyUser := user.Username() + if proxyPassword, passwordSet := user.Password(); passwordSet { + credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword)) + connectHeader.Set("Proxy-Authorization", "Basic "+credential) + } + } + + connectReq := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Opaque: addr}, + Host: addr, + Header: connectHeader, + } + + if err := connectReq.Write(conn); err != nil { + conn.Close() + return nil, err + } + + // Read response. It's OK to use and discard buffered reader here becaue + // the remote server does not speak until spoken to. + br := bufio.NewReader(conn) + resp, err := http.ReadResponse(br, connectReq) + if err != nil { + conn.Close() + return nil, err + } + + if resp.StatusCode != 200 { + conn.Close() + f := strings.SplitN(resp.Status, " ", 2) + return nil, errors.New(f[1]) + } + return conn, nil +} diff --git a/vendor/github.com/gorilla/websocket/server.go b/vendor/github.com/gorilla/websocket/server.go index 3495e0f..bb33597 100644 --- a/vendor/github.com/gorilla/websocket/server.go +++ b/vendor/github.com/gorilla/websocket/server.go @@ -7,7 +7,7 @@ package websocket import ( "bufio" "errors" - "net" + "io" "net/http" "net/url" "strings" @@ -23,20 +23,35 @@ func (e HandshakeError) Error() string { return e.message } // Upgrader specifies parameters for upgrading an HTTP connection to a // WebSocket connection. +// +// It is safe to call Upgrader's methods concurrently. type Upgrader struct { // HandshakeTimeout specifies the duration for the handshake to complete. HandshakeTimeout time.Duration - // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer + // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer // size is zero, then buffers allocated by the HTTP server are used. The // I/O buffer sizes do not limit the size of the messages that can be sent // or received. ReadBufferSize, WriteBufferSize int + // WriteBufferPool is a pool of buffers for write operations. If the value + // is not set, then write buffers are allocated to the connection for the + // lifetime of the connection. + // + // A pool is most useful when the application has a modest volume of writes + // across a large number of connections. + // + // Applications should use a single pool for each unique value of + // WriteBufferSize. + WriteBufferPool BufferPool + // Subprotocols specifies the server's supported protocols in order of - // preference. If this field is set, then the Upgrade method negotiates a + // preference. If this field is not nil, then the Upgrade method negotiates a // subprotocol by selecting the first match in this list with a protocol - // requested by the client. + // requested by the client. If there's no match, then no protocol is + // negotiated (the Sec-Websocket-Protocol header is not included in the + // handshake response). Subprotocols []string // Error specifies the function for generating HTTP error responses. If Error @@ -44,8 +59,12 @@ type Upgrader struct { Error func(w http.ResponseWriter, r *http.Request, status int, reason error) // CheckOrigin returns true if the request Origin header is acceptable. If - // CheckOrigin is nil, the host in the Origin header must not be set or - // must match the host of the request. + // CheckOrigin is nil, then a safe default is used: return false if the + // Origin request header is present and the origin host is not equal to + // request Host header. + // + // A CheckOrigin function should carefully validate the request origin to + // prevent cross-site request forgery. CheckOrigin func(r *http.Request) bool // EnableCompression specify if the server should attempt to negotiate per @@ -76,7 +95,7 @@ func checkSameOrigin(r *http.Request) bool { if err != nil { return false } - return u.Host == r.Host + return equalASCIIFold(u.Host, r.Host) } func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { @@ -98,43 +117,45 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // // The responseHeader is included in the response to the client's upgrade -// request. Use the responseHeader to specify cookies (Set-Cookie) and the -// application negotiated subprotocol (Sec-Websocket-Protocol). +// request. Use the responseHeader to specify cookies (Set-Cookie). To specify +// subprotocols supported by the server, set Upgrader.Subprotocols directly. // // If the upgrade fails, then Upgrade replies to the client with an HTTP error // response. func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) { - if r.Method != "GET" { - return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET") - } - - if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { - return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported") - } + const badHandshake = "websocket: the client is not using the websocket protocol: " if !tokenListContainsValue(r.Header, "Connection", "upgrade") { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header") + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header") } if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header") + return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") + } + + if r.Method != http.MethodGet { + return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET") } if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") { return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header") } + if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { + return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported") + } + checkOrigin := u.CheckOrigin if checkOrigin == nil { checkOrigin = checkSameOrigin } if !checkOrigin(r) { - return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed") + return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin") } challengeKey := r.Header.Get("Sec-Websocket-Key") - if challengeKey == "" { - return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank") + if !isValidChallengeKey(challengeKey) { + return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header must be Base64 encoded value of 16-byte in length") } subprotocol := u.selectSubprotocol(r, responseHeader) @@ -151,17 +172,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade } } - var ( - netConn net.Conn - err error - ) - h, ok := w.(http.Hijacker) if !ok { return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") } var brw *bufio.ReadWriter - netConn, brw, err = h.Hijack() + netConn, brw, err := h.Hijack() if err != nil { return u.returnError(w, r, http.StatusInternalServerError, err.Error()) } @@ -171,7 +187,21 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade return nil, errors.New("websocket: client sent data before handshake is complete") } - c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw) + var br *bufio.Reader + if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { + // Reuse hijacked buffered reader as connection reader. + br = brw.Reader + } + + buf := bufioWriterBuffer(netConn, brw.Writer) + + var writeBuf []byte + if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { + // Reuse hijacked write buffer as connection buffer. + writeBuf = buf + } + + c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf) c.subprotocol = subprotocol if compress { @@ -179,17 +209,23 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade c.newDecompressionReader = decompressNoContextTakeover } - p := c.writeBuf[:0] + // Use larger of hijacked buffer and connection write buffer for header. + p := buf + if len(c.writeBuf) > len(p) { + p = c.writeBuf + } + p = p[:0] + p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) p = append(p, computeAcceptKey(challengeKey)...) p = append(p, "\r\n"...) if c.subprotocol != "" { - p = append(p, "Sec-Websocket-Protocol: "...) + p = append(p, "Sec-WebSocket-Protocol: "...) p = append(p, c.subprotocol...) p = append(p, "\r\n"...) } if compress { - p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) + p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) } for k, vs := range responseHeader { if k == "Sec-Websocket-Protocol" { @@ -230,13 +266,14 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade // Upgrade upgrades the HTTP server connection to the WebSocket protocol. // -// This function is deprecated, use websocket.Upgrader instead. +// Deprecated: Use websocket.Upgrader instead. // -// The application is responsible for checking the request origin before -// calling Upgrade. An example implementation of the same origin policy is: +// Upgrade does not perform origin checking. The application is responsible for +// checking the Origin header before calling Upgrade. An example implementation +// of the same origin policy check is: // // if req.Header.Get("Origin") != "http://"+req.Host { -// http.Error(w, "Origin not allowed", 403) +// http.Error(w, "Origin not allowed", http.StatusForbidden) // return // } // @@ -289,3 +326,40 @@ func IsWebSocketUpgrade(r *http.Request) bool { return tokenListContainsValue(r.Header, "Connection", "upgrade") && tokenListContainsValue(r.Header, "Upgrade", "websocket") } + +// bufioReaderSize size returns the size of a bufio.Reader. +func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { + // This code assumes that peek on a reset reader returns + // bufio.Reader.buf[:0]. + // TODO: Use bufio.Reader.Size() after Go 1.10 + br.Reset(originalReader) + if p, err := br.Peek(0); err == nil { + return cap(p) + } + return 0 +} + +// writeHook is an io.Writer that records the last slice passed to it vio +// io.Writer.Write. +type writeHook struct { + p []byte +} + +func (wh *writeHook) Write(p []byte) (int, error) { + wh.p = p + return len(p), nil +} + +// bufioWriterBuffer grabs the buffer from a bufio.Writer. +func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte { + // This code assumes that bufio.Writer.buf[:1] is passed to the + // bufio.Writer's underlying writer. + var wh writeHook + bw.Reset(&wh) + bw.WriteByte(0) + bw.Flush() + + bw.Reset(originalWriter) + + return wh.p[:cap(wh.p)] +} diff --git a/vendor/github.com/gorilla/websocket/tls_handshake.go b/vendor/github.com/gorilla/websocket/tls_handshake.go new file mode 100644 index 0000000..a62b68c --- /dev/null +++ b/vendor/github.com/gorilla/websocket/tls_handshake.go @@ -0,0 +1,21 @@ +//go:build go1.17 +// +build go1.17 + +package websocket + +import ( + "context" + "crypto/tls" +) + +func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.HandshakeContext(ctx); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/tls_handshake_116.go b/vendor/github.com/gorilla/websocket/tls_handshake_116.go new file mode 100644 index 0000000..e1b2b44 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/tls_handshake_116.go @@ -0,0 +1,21 @@ +//go:build !go1.17 +// +build !go1.17 + +package websocket + +import ( + "context" + "crypto/tls" +) + +func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error { + if err := tlsConn.Handshake(); err != nil { + return err + } + if !cfg.InsecureSkipVerify { + if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/gorilla/websocket/util.go b/vendor/github.com/gorilla/websocket/util.go index 9a4908d..31a5dee 100644 --- a/vendor/github.com/gorilla/websocket/util.go +++ b/vendor/github.com/gorilla/websocket/util.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "strings" + "unicode/utf8" ) var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") @@ -30,68 +31,113 @@ func generateChallengeKey() (string, error) { return base64.StdEncoding.EncodeToString(p), nil } -// Octet types from RFC 2616. -var octetTypes [256]byte - -const ( - isTokenOctet = 1 << iota - isSpaceOctet -) - -func init() { - // From RFC 2616 - // - // OCTET = - // CHAR = - // CTL = - // CR = - // LF = - // SP = - // HT = - // <"> = - // CRLF = CR LF - // LWS = [CRLF] 1*( SP | HT ) - // TEXT = - // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> - // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT - // token = 1* - // qdtext = > - - for c := 0; c < 256; c++ { - var t byte - isCtl := c <= 31 || c == 127 - isChar := 0 <= c && c <= 127 - isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 - if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { - t |= isSpaceOctet - } - if isChar && !isCtl && !isSeparator { - t |= isTokenOctet - } - octetTypes[c] = t - } +// Token octets per RFC 2616. +var isTokenOctet = [256]bool{ + '!': true, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '*': true, + '+': true, + '-': true, + '.': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'W': true, + 'V': true, + 'X': true, + 'Y': true, + 'Z': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '|': true, + '~': true, } +// skipSpace returns a slice of the string s with all leading RFC 2616 linear +// whitespace removed. func skipSpace(s string) (rest string) { i := 0 for ; i < len(s); i++ { - if octetTypes[s[i]]&isSpaceOctet == 0 { + if b := s[i]; b != ' ' && b != '\t' { break } } return s[i:] } +// nextToken returns the leading RFC 2616 token of s and the string following +// the token. func nextToken(s string) (token, rest string) { i := 0 for ; i < len(s); i++ { - if octetTypes[s[i]]&isTokenOctet == 0 { + if !isTokenOctet[s[i]] { break } } return s[:i], s[i:] } +// nextTokenOrQuoted returns the leading token or quoted string per RFC 2616 +// and the string following the token or quoted string. func nextTokenOrQuoted(s string) (value string, rest string) { if !strings.HasPrefix(s, "\"") { return nextToken(s) @@ -111,14 +157,14 @@ func nextTokenOrQuoted(s string) (value string, rest string) { case escape: escape = false p[j] = b - j += 1 + j++ case b == '\\': escape = true case b == '"': return string(p[:j]), s[i+1:] default: p[j] = b - j += 1 + j++ } } return "", "" @@ -127,8 +173,32 @@ func nextTokenOrQuoted(s string) (value string, rest string) { return "", "" } +// equalASCIIFold returns true if s is equal to t with ASCII case folding as +// defined in RFC 4790. +func equalASCIIFold(s, t string) bool { + for s != "" && t != "" { + sr, size := utf8.DecodeRuneInString(s) + s = s[size:] + tr, size := utf8.DecodeRuneInString(t) + t = t[size:] + if sr == tr { + continue + } + if 'A' <= sr && sr <= 'Z' { + sr = sr + 'a' - 'A' + } + if 'A' <= tr && tr <= 'Z' { + tr = tr + 'a' - 'A' + } + if sr != tr { + return false + } + } + return s == t +} + // tokenListContainsValue returns true if the 1#token header with the given -// name contains token. +// name contains a token equal to value with ASCII case folding. func tokenListContainsValue(header http.Header, name string, value string) bool { headers: for _, s := range header[name] { @@ -142,7 +212,7 @@ headers: if s != "" && s[0] != ',' { continue headers } - if strings.EqualFold(t, value) { + if equalASCIIFold(t, value) { return true } if s == "" { @@ -154,9 +224,8 @@ headers: return false } -// parseExtensiosn parses WebSocket extensions from a header. +// parseExtensions parses WebSocket extensions from a header. func parseExtensions(header http.Header) []map[string]string { - // From RFC 6455: // // Sec-WebSocket-Extensions = extension-list @@ -212,3 +281,18 @@ headers: } return result } + +// isValidChallengeKey checks if the argument meets RFC6455 specification. +func isValidChallengeKey(s string) bool { + // From RFC6455: + // + // A |Sec-WebSocket-Key| header field with a base64-encoded (see + // Section 4 of [RFC4648]) value that, when decoded, is 16 bytes in + // length. + + if s == "" { + return false + } + decoded, err := base64.StdEncoding.DecodeString(s) + return err == nil && len(decoded) == 16 +} diff --git a/vendor/github.com/gorilla/websocket/x_net_proxy.go b/vendor/github.com/gorilla/websocket/x_net_proxy.go new file mode 100644 index 0000000..2e668f6 --- /dev/null +++ b/vendor/github.com/gorilla/websocket/x_net_proxy.go @@ -0,0 +1,473 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o x_net_proxy.go golang.org/x/net/proxy + +// Package proxy provides support for a variety of protocols to proxy network +// data. +// + +package websocket + +import ( + "errors" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" +) + +type proxy_direct struct{} + +// Direct is a direct proxy: one that makes network connections directly. +var proxy_Direct = proxy_direct{} + +func (proxy_direct) Dial(network, addr string) (net.Conn, error) { + return net.Dial(network, addr) +} + +// A PerHost directs connections to a default Dialer unless the host name +// requested matches one of a number of exceptions. +type proxy_PerHost struct { + def, bypass proxy_Dialer + + bypassNetworks []*net.IPNet + bypassIPs []net.IP + bypassZones []string + bypassHosts []string +} + +// NewPerHost returns a PerHost Dialer that directs connections to either +// defaultDialer or bypass, depending on whether the connection matches one of +// the configured rules. +func proxy_NewPerHost(defaultDialer, bypass proxy_Dialer) *proxy_PerHost { + return &proxy_PerHost{ + def: defaultDialer, + bypass: bypass, + } +} + +// Dial connects to the address addr on the given network through either +// defaultDialer or bypass. +func (p *proxy_PerHost) Dial(network, addr string) (c net.Conn, err error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + return p.dialerForRequest(host).Dial(network, addr) +} + +func (p *proxy_PerHost) dialerForRequest(host string) proxy_Dialer { + if ip := net.ParseIP(host); ip != nil { + for _, net := range p.bypassNetworks { + if net.Contains(ip) { + return p.bypass + } + } + for _, bypassIP := range p.bypassIPs { + if bypassIP.Equal(ip) { + return p.bypass + } + } + return p.def + } + + for _, zone := range p.bypassZones { + if strings.HasSuffix(host, zone) { + return p.bypass + } + if host == zone[1:] { + // For a zone ".example.com", we match "example.com" + // too. + return p.bypass + } + } + for _, bypassHost := range p.bypassHosts { + if bypassHost == host { + return p.bypass + } + } + return p.def +} + +// AddFromString parses a string that contains comma-separated values +// specifying hosts that should use the bypass proxy. Each value is either an +// IP address, a CIDR range, a zone (*.example.com) or a host name +// (localhost). A best effort is made to parse the string and errors are +// ignored. +func (p *proxy_PerHost) AddFromString(s string) { + hosts := strings.Split(s, ",") + for _, host := range hosts { + host = strings.TrimSpace(host) + if len(host) == 0 { + continue + } + if strings.Contains(host, "/") { + // We assume that it's a CIDR address like 127.0.0.0/8 + if _, net, err := net.ParseCIDR(host); err == nil { + p.AddNetwork(net) + } + continue + } + if ip := net.ParseIP(host); ip != nil { + p.AddIP(ip) + continue + } + if strings.HasPrefix(host, "*.") { + p.AddZone(host[1:]) + continue + } + p.AddHost(host) + } +} + +// AddIP specifies an IP address that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match an IP. +func (p *proxy_PerHost) AddIP(ip net.IP) { + p.bypassIPs = append(p.bypassIPs, ip) +} + +// AddNetwork specifies an IP range that will use the bypass proxy. Note that +// this will only take effect if a literal IP address is dialed. A connection +// to a named host will never match. +func (p *proxy_PerHost) AddNetwork(net *net.IPNet) { + p.bypassNetworks = append(p.bypassNetworks, net) +} + +// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of +// "example.com" matches "example.com" and all of its subdomains. +func (p *proxy_PerHost) AddZone(zone string) { + if strings.HasSuffix(zone, ".") { + zone = zone[:len(zone)-1] + } + if !strings.HasPrefix(zone, ".") { + zone = "." + zone + } + p.bypassZones = append(p.bypassZones, zone) +} + +// AddHost specifies a host name that will use the bypass proxy. +func (p *proxy_PerHost) AddHost(host string) { + if strings.HasSuffix(host, ".") { + host = host[:len(host)-1] + } + p.bypassHosts = append(p.bypassHosts, host) +} + +// A Dialer is a means to establish a connection. +type proxy_Dialer interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, err error) +} + +// Auth contains authentication parameters that specific Dialers may require. +type proxy_Auth struct { + User, Password string +} + +// FromEnvironment returns the dialer specified by the proxy related variables in +// the environment. +func proxy_FromEnvironment() proxy_Dialer { + allProxy := proxy_allProxyEnv.Get() + if len(allProxy) == 0 { + return proxy_Direct + } + + proxyURL, err := url.Parse(allProxy) + if err != nil { + return proxy_Direct + } + proxy, err := proxy_FromURL(proxyURL, proxy_Direct) + if err != nil { + return proxy_Direct + } + + noProxy := proxy_noProxyEnv.Get() + if len(noProxy) == 0 { + return proxy + } + + perHost := proxy_NewPerHost(proxy, proxy_Direct) + perHost.AddFromString(noProxy) + return perHost +} + +// proxySchemes is a map from URL schemes to a function that creates a Dialer +// from a URL with such a scheme. +var proxy_proxySchemes map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error) + +// RegisterDialerType takes a URL scheme and a function to generate Dialers from +// a URL with that scheme and a forwarding Dialer. Registered schemes are used +// by FromURL. +func proxy_RegisterDialerType(scheme string, f func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) { + if proxy_proxySchemes == nil { + proxy_proxySchemes = make(map[string]func(*url.URL, proxy_Dialer) (proxy_Dialer, error)) + } + proxy_proxySchemes[scheme] = f +} + +// FromURL returns a Dialer given a URL specification and an underlying +// Dialer for it to make network requests. +func proxy_FromURL(u *url.URL, forward proxy_Dialer) (proxy_Dialer, error) { + var auth *proxy_Auth + if u.User != nil { + auth = new(proxy_Auth) + auth.User = u.User.Username() + if p, ok := u.User.Password(); ok { + auth.Password = p + } + } + + switch u.Scheme { + case "socks5": + return proxy_SOCKS5("tcp", u.Host, auth, forward) + } + + // If the scheme doesn't match any of the built-in schemes, see if it + // was registered by another package. + if proxy_proxySchemes != nil { + if f, ok := proxy_proxySchemes[u.Scheme]; ok { + return f(u, forward) + } + } + + return nil, errors.New("proxy: unknown scheme: " + u.Scheme) +} + +var ( + proxy_allProxyEnv = &proxy_envOnce{ + names: []string{"ALL_PROXY", "all_proxy"}, + } + proxy_noProxyEnv = &proxy_envOnce{ + names: []string{"NO_PROXY", "no_proxy"}, + } +) + +// envOnce looks up an environment variable (optionally by multiple +// names) once. It mitigates expensive lookups on some platforms +// (e.g. Windows). +// (Borrowed from net/http/transport.go) +type proxy_envOnce struct { + names []string + once sync.Once + val string +} + +func (e *proxy_envOnce) Get() string { + e.once.Do(e.init) + return e.val +} + +func (e *proxy_envOnce) init() { + for _, n := range e.names { + e.val = os.Getenv(n) + if e.val != "" { + return + } + } +} + +// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address +// with an optional username and password. See RFC 1928 and RFC 1929. +func proxy_SOCKS5(network, addr string, auth *proxy_Auth, forward proxy_Dialer) (proxy_Dialer, error) { + s := &proxy_socks5{ + network: network, + addr: addr, + forward: forward, + } + if auth != nil { + s.user = auth.User + s.password = auth.Password + } + + return s, nil +} + +type proxy_socks5 struct { + user, password string + network, addr string + forward proxy_Dialer +} + +const proxy_socks5Version = 5 + +const ( + proxy_socks5AuthNone = 0 + proxy_socks5AuthPassword = 2 +) + +const proxy_socks5Connect = 1 + +const ( + proxy_socks5IP4 = 1 + proxy_socks5Domain = 3 + proxy_socks5IP6 = 4 +) + +var proxy_socks5Errors = []string{ + "", + "general failure", + "connection forbidden", + "network unreachable", + "host unreachable", + "connection refused", + "TTL expired", + "command not supported", + "address type not supported", +} + +// Dial connects to the address addr on the given network via the SOCKS5 proxy. +func (s *proxy_socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network) + } + + conn, err := s.forward.Dial(s.network, s.addr) + if err != nil { + return nil, err + } + if err := s.connect(conn, addr); err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *proxy_socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, proxy_socks5Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, proxy_socks5AuthNone, proxy_socks5AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, proxy_socks5AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + // See RFC 1929 + if buf[1] == proxy_socks5AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, proxy_socks5Version, proxy_socks5Connect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, proxy_socks5IP4) + ip = ip4 + } else { + buf = append(buf, proxy_socks5IP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination host name too long: " + host) + } + buf = append(buf, proxy_socks5Domain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(proxy_socks5Errors) { + failure = proxy_socks5Errors[buf[1]] + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case proxy_socks5IP4: + bytesToDiscard = net.IPv4len + case proxy_socks5IP6: + bytesToDiscard = net.IPv6len + case proxy_socks5Domain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} diff --git a/vendor/github.com/mattn/go-colorable/go.mod b/vendor/github.com/mattn/go-colorable/go.mod deleted file mode 100644 index ef3ca9d..0000000 --- a/vendor/github.com/mattn/go-colorable/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/mattn/go-colorable - -require github.com/mattn/go-isatty v0.0.8 diff --git a/vendor/github.com/mattn/go-colorable/go.sum b/vendor/github.com/mattn/go-colorable/go.sum deleted file mode 100644 index 2c12960..0000000 --- a/vendor/github.com/mattn/go-colorable/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/vendor/github.com/mattn/go-isatty/go.mod b/vendor/github.com/mattn/go-isatty/go.mod deleted file mode 100644 index f310320..0000000 --- a/vendor/github.com/mattn/go-isatty/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/mattn/go-isatty - -require golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 diff --git a/vendor/github.com/mattn/go-isatty/go.sum b/vendor/github.com/mattn/go-isatty/go.sum deleted file mode 100644 index 426c897..0000000 --- a/vendor/github.com/mattn/go-isatty/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/vendor/github.com/robinjoseph08/redisqueue/go.mod b/vendor/github.com/robinjoseph08/redisqueue/go.mod deleted file mode 100644 index 8e69c54..0000000 --- a/vendor/github.com/robinjoseph08/redisqueue/go.mod +++ /dev/null @@ -1,23 +0,0 @@ -module github.com/robinjoseph08/redisqueue - -go 1.12 - -require ( - github.com/fatih/color v1.7.0 // indirect - github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f - github.com/go-redis/redis v6.15.2+incompatible - github.com/imdario/mergo v0.3.7 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/goveralls v0.0.2 - github.com/onsi/ginkgo v1.8.0 // indirect - github.com/onsi/gomega v1.5.0 // indirect - github.com/pborman/uuid v1.2.0 // indirect - github.com/pkg/errors v0.8.1 - github.com/stretchr/testify v1.3.0 - github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df // indirect - github.com/urfave/cli v1.20.0 // indirect - golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db // indirect - gopkg.in/AlecAivazis/survey.v1 v1.8.5 // indirect - gopkg.in/kyokomi/emoji.v1 v1.5.1 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect -) diff --git a/vendor/github.com/robinjoseph08/redisqueue/go.sum b/vendor/github.com/robinjoseph08/redisqueue/go.sum deleted file mode 100644 index 754951c..0000000 --- a/vendor/github.com/robinjoseph08/redisqueue/go.sum +++ /dev/null @@ -1,86 +0,0 @@ -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f h1:8l4Aw3Jmx0pLKYMkY+1b6yBPgE+rzRtA5T3vqFyI2Z8= -github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f/go.mod h1:Dcsy1kii/xFyNad5JqY/d0GO5mu91sungp5xotbm3Yk= -github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= -github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= -github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df h1:Y2l28Jr3vOEeYtxfVbMtVfOdAwuUqWaP9fvNKiBVeXY= -github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df/go.mod h1:pnyouUty/nBr/zm3GYwTIt+qFTLWbdjeLjZmJdzJOu8= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db h1:9hRk1xeL9LTT3yX/941DqeBz87XgHAQuj+TbimYJuiw= -golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -gopkg.in/AlecAivazis/survey.v1 v1.8.5 h1:QoEEmn/d5BbuPIL2qvXwzJdttFFhRQFkaq+tEKb7SMI= -gopkg.in/AlecAivazis/survey.v1 v1.8.5/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/kyokomi/emoji.v1 v1.5.1 h1:beetH5mWDMzFznJ+Qzd5KVHp79YKhVUMcdO8LpRLeGw= -gopkg.in/kyokomi/emoji.v1 v1.5.1/go.mod h1:N9AZ6hi1jHOPn34PsbpufQZUcKftSD7WgS2pgpmH4Lg= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/rs/zerolog/go.mod b/vendor/github.com/rs/zerolog/go.mod deleted file mode 100644 index 8c42ba8..0000000 --- a/vendor/github.com/rs/zerolog/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/rs/zerolog - -require ( - github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e - github.com/pkg/errors v0.8.1 - github.com/rs/xid v1.2.1 - github.com/zenazn/goji v0.9.0 - golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 -) diff --git a/vendor/github.com/rs/zerolog/go.sum b/vendor/github.com/rs/zerolog/go.sum deleted file mode 100644 index b14fd2b..0000000 --- a/vendor/github.com/rs/zerolog/go.sum +++ /dev/null @@ -1,16 +0,0 @@ -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 h1:4cFkmztxtMslUX2SctSl+blCyXfpzhGOy9LhKAqSMA4= -golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/github.com/slack-go/slack/.gitignore b/vendor/github.com/slack-go/slack/.gitignore index ac6f3ee..9b3903c 100644 --- a/vendor/github.com/slack-go/slack/.gitignore +++ b/vendor/github.com/slack-go/slack/.gitignore @@ -1,3 +1,5 @@ *.test *~ .idea/ +/vendor/ +.env* diff --git a/vendor/github.com/slack-go/slack/.golangci.yml b/vendor/github.com/slack-go/slack/.golangci.yml new file mode 100644 index 0000000..0ef0f87 --- /dev/null +++ b/vendor/github.com/slack-go/slack/.golangci.yml @@ -0,0 +1,35 @@ +version: "2" +run: + timeout: 6m + issues-exit-code: 1 +linters: + default: none + enable: + - gocritic + - govet + - misspell + - unconvert + - unused + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +issues: + new: true +formatters: + enable: + - goimports + exclusions: + generated: lax + warn-unused: true + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/slack-go/slack/.gometalinter.json b/vendor/github.com/slack-go/slack/.gometalinter.json deleted file mode 100644 index 5fa629d..0000000 --- a/vendor/github.com/slack-go/slack/.gometalinter.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "DisableAll": true, - "Enable": [ - "structcheck", - "vet", - "misspell", - "unconvert", - "interfacer", - "goimports" - ], - "Vendor": true, - "Exclude": ["vendor"], - "Deadline": "300s" -} diff --git a/vendor/github.com/slack-go/slack/.travis.yml b/vendor/github.com/slack-go/slack/.travis.yml deleted file mode 100644 index 6a96823..0000000 --- a/vendor/github.com/slack-go/slack/.travis.yml +++ /dev/null @@ -1,39 +0,0 @@ -language: go - -env: - - GO111MODULE=on - -install: true - -before_install: - - export PATH=$HOME/gopath/bin:$PATH - # install gometalinter - - curl -L https://git.io/vp6lP | sh - -script: - - PATH=$PWD/bin:$PATH gometalinter ./... - - go test -race -cover ./... - -matrix: - allow_failures: - - go: tip - include: - - go: "1.7.x" - script: go test -v ./... - - go: "1.8.x" - script: go test -v ./... - - go: "1.9.x" - script: go test -v ./... - - go: "1.10.x" - script: go test -v ./... - - go: "1.11.x" - script: go test -v -mod=vendor ./... - - go: "1.12.x" - script: go test -v -mod=vendor ./... - - go: "1.13.x" - script: go test -v -mod=vendor ./... - - go: "tip" - script: go test -v -mod=vendor ./... - -git: - depth: 10 diff --git a/vendor/github.com/slack-go/slack/CHANGELOG.md b/vendor/github.com/slack-go/slack/CHANGELOG.md index 48bcce5..2f9f821 100644 --- a/vendor/github.com/slack-go/slack/CHANGELOG.md +++ b/vendor/github.com/slack-go/slack/CHANGELOG.md @@ -1,59 +1,580 @@ -### v0.6.0 - August 31, 2019 -full differences can be viewed using `git log --oneline --decorate --color v0.5.0..v0.6.0` -thanks to everyone who has contributed since January! - - -#### Breaking Changes: -- Info struct has had fields removed related to deprecated functionality by slack. -- minor adjustments to some structs. -- some internal default values have changed, usually to be more inline with slack defaults or to correct inability to set a particular value. (Message Parse for example.) - -##### Highlights: -- new slacktest package easy mocking for slack client. use, enjoy, please submit PRs for improvements and default behaviours! shamelessly taken from the [slack-test repo](https://github.com/lusis/slack-test) thank you lusis for letting us use it and bring it into the slack repo. -- blocks, blocks, blocks. -- RTM ManagedConnection has undergone a significant cleanup. -in particular handles backoffs gracefully, removed many deadlocks, -and Disconnect is now much more responsive. - -### v0.5.0 - January 20, 2019 -full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0` -- Breaking changes: various old struct fields have been removed or updated to match slack's api. -- deadlock fix in RTM disconnect. - -### v0.4.0 - October 06, 2018 -full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0` -- Breaking Change: renamed ApplyMessageOption, to mark it as unsafe, -this means it may break without warning in the future. -- Breaking: Msg structure files field changed to an array. -- General: implementation for new security headers. -- RTM: deadlock fix between connect/disconnect. -- Events: various new fields added. -- Web: various fixes, new fields exposed, new methods added. -- Interactions: minor additions expect breaking changes in next release for dialogs/button clicks. -- Utils: new methods added. - -### v0.3.0 - July 30, 2018 -full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0` -- slack events initial support added. (still considered experimental and undergoing changes, stability not promised) -- vendored depedencies using dep, ensure using up to date tooling before filing issues. -- RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects). -- bug fixes (various timestamp handling, error handling, RTM locking, etc). - -### v0.2.0 - Feb 10, 2018 - -Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. - -Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0) - -### v0.1.0 - May 28, 2017 - -This is released before adding context support. -As the used context package is the one from Go 1.7 this will be the last -compatible with Go < 1.7. - -Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) - -### v0.0.1 - Jul 26, 2015 - -If you just updated from master and it broke your implementation, please -check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) +# 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/CONTRIBUTING.md b/vendor/github.com/slack-go/slack/CONTRIBUTING.md new file mode 100644 index 0000000..8b92d35 --- /dev/null +++ b/vendor/github.com/slack-go/slack/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Contributing Guide + +Welcome! We are glad that you want to contribute to our project! 💖 + +There are a just a few small guidelines you ask everyone to follow to make things a bit smoother and more consistent. + +## Opening Pull Requests + +1. It's generally best to start by opening a new issue describing the bug or feature you're intending to fix. Even if you think it's relatively minor, it's helpful to know what people are working on. Mention in the initial issue that you are planning to work on that bug or feature so that it can be assigned to you. + +2. Follow the normal process of [forking](https://help.github.com/articles/fork-a-repo) the project, and set up a new branch to work in. It's important that each group of changes be done in separate branches in order to ensure that a pull request only includes the commits related to that bug or feature. + +3. Any significant changes should almost always be accompanied by tests. The project already has some test coverage, so look at some of the existing tests if you're unsure how to go about it. + +4. Run `make pr-prep` to format your code and check that it passes all tests and linters. + +5. Do your best to have [well-formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for each change. This provides consistency throughout the project, and ensures that commit messages are able to be formatted properly by various git tools. _Pull Request Titles_ should generally follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) format to ease the release note process when cutting releases. + +6. Finally, push the commits to your fork and submit a [pull request](https://help.github.com/articles/creating-a-pull-request). NOTE: Please do not use force-push on PRs in this repo, as it makes it more difficult for reviewers to see what has changed since the last code review. We always perform "squash and merge" actions on PRs in this repo, so it doesn't matter how many commits your PR has, as they will end up being a single commit after merging. This is done to make a much cleaner `git log` history and helps to find regressions in the code using existing tools such as `git bisect`. + +## Code Comments + +Every exported method needs to have code comments that follow [Go Doc Comments](https://go.dev/doc/comment). A typical method's comments will look like this: + +```go +// PostMessage sends a message to a channel. +// +// Slack API docs: https://api.dev.slack.com/methods/chat.postMessage +func (api *Client) PostMessage(ctx context.Context, input PostMesssageInput) (PostMesssageOutput, error) { +... +} +``` + +The first line is the name of the method followed by a short description. This could also be a longer description if needed, but there is no need to repeat any details that are documented in Slack's documentation because users are expected to follow the documentation links to learn more. + +After the description comes a link to the Slack API documentation. + +## Other notes on code organization + +Currently, everything is defined in the main `slack` package, with API methods group separate files by the [Slack API Method Groupings](https://api.dev.slack.com/methods). diff --git a/vendor/github.com/slack-go/slack/Makefile b/vendor/github.com/slack-go/slack/Makefile index 7279640..a810401 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 eaf0778..53dd912 100644 --- a/vendor/github.com/slack-go/slack/README.md +++ b/vendor/github.com/slack-go/slack/README.md @@ -1,19 +1,24 @@ -Slack API in Go [![GoDoc](https://godoc.org/github.com/slack-go/slack?status.svg)](https://godoc.org/github.com/slack-go/slack) [![Build Status](https://travis-ci.org/slack-go/slack.svg)](https://travis-ci.org/slack-go/slack) +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 Lopez, transferred to a Github organization. -[![Join the chat at https://gitter.im/go-slack/Lobby](https://badges.gitter.im/go-slack/Lobby.svg)](https://gitter.im/go-slack/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +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") This library supports most if not all of the `api.slack.com` REST calls, as well as the Real-Time Messaging protocol over websocket, in a fully managed way. +## Project Status +There is currently no major version released. +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 -## Changelog - -[CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates. +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 @@ -27,24 +32,24 @@ a fully managed way. ```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.GetGroups(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) + } } ``` @@ -61,15 +66,30 @@ 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 + + ## Minimal RTM usage: +As mentioned in https://api.slack.com/rtm - for most applications, Socket Mode is a better way to communicate with Slack. + See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.go @@ -77,7 +97,13 @@ See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.g See https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go +## Socketmode Event Handler (Experimental) + +When using socket mode, dealing with an event can be pretty lengthy as it requires you to route the event to the right place. + +Instead, you can use `SocketmodeHandler` much like you use an HTTP handler to register which event you would like to listen to and what callback function will process that event when it occurs. +See [./examples/socketmode_handler/socketmode_handler.go](./examples/socketmode_handler/socketmode_handler.go) ## Contributing You are more than welcome to contribute to this project. Fork and diff --git a/vendor/github.com/slack-go/slack/TODO.txt b/vendor/github.com/slack-go/slack/TODO.txt deleted file mode 100644 index 8607960..0000000 --- a/vendor/github.com/slack-go/slack/TODO.txt +++ /dev/null @@ -1,3 +0,0 @@ -- Add more tests!!! -- Add support to have markdown hints - - See section Message Formatting at https://api.slack.com/docs/formatting diff --git a/vendor/github.com/slack-go/slack/admin.go b/vendor/github.com/slack-go/slack/admin.go index d51426b..1b0d217 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 new file mode 100644 index 0000000..eba5d98 --- /dev/null +++ b/vendor/github.com/slack-go/slack/admin_conversations.go @@ -0,0 +1,807 @@ +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 { + ChannelID string + OrgChannel *bool + TargetTeamIDs []string + TeamID *string +} + +// Set the workspaces in an Enterprise Grid organisation that connect to a public or +// private channel. +// See: https://api.slack.com/methods/admin.conversations.setTeams +func (api *Client) AdminConversationsSetTeams(ctx context.Context, params AdminConversationsSetTeamsParams) error { + values := url.Values{ + "token": {api.token}, + "channel_id": {params.ChannelID}, + } + + if params.OrgChannel != nil { + values.Add("org_channel", strconv.FormatBool(*params.OrgChannel)) + } + + if len(params.TargetTeamIDs) > 0 { + values.Add("target_team_ids", strings.Join(params.TargetTeamIDs, ",")) // ["T123", "T456"] - > "T123,T456" + } + + if params.TeamID != nil { + values.Add("team_id", *params.TeamID) + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.setTeams", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// ConversationsConvertToPrivate converts a public channel to a private channel. To do +// this, you must have the admin.conversations:write scope. There are other requirements: +// you should read the Slack documentation for more details. +// See: https://api.slack.com/methods/admin.conversations.convertToPrivate +func (api *Client) AdminConversationsConvertToPrivate(ctx context.Context, channelID string) error { + values := url.Values{ + "token": []string{api.token}, + "channel_id": []string{channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.convertToPrivate", values, response) + if err != nil { + return err + } + + return response.Err() +} + +// ConversationsConvertToPublic converts a private channel to a public channel. To do +// this, you must have the admin.conversations:write scope. There are other requirements: +// you should read the Slack documentation for more details. +// See: https://api.slack.com/methods/admin.conversations.convertToPublic +func (api *Client) AdminConversationsConvertToPublic(ctx context.Context, channelID string) error { + values := url.Values{ + "token": []string{api.token}, + "channel_id": []string{channelID}, + } + + response := &SlackResponse{} + err := api.postMethod(ctx, "admin.conversations.convertToPublic", values, response) + if err != nil { + return err + } + + return response.Err() +} 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 0000000..f4b4e14 --- /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 0000000..3445799 --- /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 0000000..676acf0 --- /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 0000000..17ae3df --- /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 new file mode 100644 index 0000000..c75569f --- /dev/null +++ b/vendor/github.com/slack-go/slack/apps.go @@ -0,0 +1,72 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" +) + +type listEventAuthorizationsResponse struct { + SlackResponse + Authorizations []EventAuthorization `json:"authorizations"` +} + +type EventAuthorization struct { + EnterpriseID string `json:"enterprise_id"` + TeamID string `json:"team_id"` + UserID string `json:"user_id"` + IsBot bool `json:"is_bot"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` +} + +// ListEventAuthorizations lists authed users and teams for the given event_context. +// You must provide an app-level token to the client using OptionAppLevelToken. +// For more details, see ListEventAuthorizationsContext documentation. +func (api *Client) ListEventAuthorizations(eventContext string) ([]EventAuthorization, error) { + return api.ListEventAuthorizationsContext(context.Background(), eventContext) +} + +// ListEventAuthorizationsContext lists authed users and teams for the given event_context with a custom context. +// Slack API docs: https://api.slack.com/methods/apps.event.authorizations.list +func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventContext string) ([]EventAuthorization, error) { + resp := &listEventAuthorizationsResponse{} + + request, _ := json.Marshal(map[string]string{ + "event_context": eventContext, + }) + + err := api.postJSONMethod(ctx, "apps.event.authorizations.list", api.appLevelToken, request, &resp) + + if err != nil { + return nil, err + } + if !resp.Ok { + return nil, resp.Err() + } + + return resp.Authorizations, nil +} + +// UninstallApp uninstalls your app from a workspace. +// For more details, see UninstallAppContext documentation. +func (api *Client) UninstallApp(clientID, clientSecret string) error { + return api.UninstallAppContext(context.Background(), clientID, clientSecret) +} + +// UninstallAppContext uninstalls your app from a workspace with a custom context. +// Slack API docs: https://api.slack.com/methods/apps.uninstall +func (api *Client) UninstallAppContext(ctx context.Context, clientID, clientSecret string) error { + values := url.Values{ + "client_id": {clientID}, + "client_secret": {clientSecret}, + } + + response := SlackResponse{} + + err := api.getMethod(ctx, "apps.uninstall", api.token, values, &response) + if err != nil { + return err + } + + return response.Err() +} diff --git a/vendor/github.com/slack-go/slack/assistant.go b/vendor/github.com/slack-go/slack/assistant.go new file mode 100644 index 0000000..fb82cb6 --- /dev/null +++ b/vendor/github.com/slack-go/slack/assistant.go @@ -0,0 +1,377 @@ +package slack + +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"` + 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 +type AssistantThreadsSetTitleParameters struct { + ChannelID string `json:"channel_id"` + ThreadTS string `json:"thread_ts"` + Title string `json:"title"` +} + +// AssistantThreadSetSuggestedPromptsParameters are the parameters for AssistantThreadSetSuggestedPrompts +type AssistantThreadsSetSuggestedPromptsParameters struct { + Title string `json:"title"` + ChannelID string `json:"channel_id"` + ThreadTS string `json:"thread_ts"` + Prompts []AssistantThreadsPrompt `json:"prompts"` +} + +// AssistantThreadPrompt is a suggested prompt for a thread +type AssistantThreadsPrompt struct { + Title string `json:"title"` + 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{ + Title: title, + Message: message, + }) +} + +// SetAssistantThreadsSugesstedPrompts sets the suggested prompts for a thread +// @see https://api.slack.com/methods/assistant.threads.setSuggestedPrompts +func (api *Client) SetAssistantThreadsSuggestedPrompts(params AssistantThreadsSetSuggestedPromptsParameters) (err error) { + return api.SetAssistantThreadsSuggestedPromptsContext(context.Background(), params) +} + +// SetAssistantThreadSuggestedPromptsContext sets the suggested prompts for a thread with a custom context +// @see https://api.slack.com/methods/assistant.threads.setSuggestedPrompts +func (api *Client) SetAssistantThreadsSuggestedPromptsContext(ctx context.Context, params AssistantThreadsSetSuggestedPromptsParameters) (err error) { + + values := url.Values{ + "token": {api.token}, + } + + if params.ThreadTS != "" { + values.Add("thread_ts", params.ThreadTS) + } + + 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 { + return err + } + + values.Add("prompts", string(prompts)) + + response := struct { + SlackResponse + }{} + + err = api.postMethod(ctx, "assistant.threads.setSuggestedPrompts", values, &response) + if err != nil { + return + } + + return response.Err() +} + +// 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) +} + +// 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) { + + values := url.Values{ + "token": {api.token}, + } + + if params.ThreadTS != "" { + values.Add("thread_ts", params.ThreadTS) + } + + values.Add("channel_id", params.ChannelID) + + // 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 + }{} + + err = api.postMethod(ctx, "assistant.threads.setStatus", values, &response) + if err != nil { + return + } + + return response.Err() +} + +// SetAssistantThreadsTitle sets the title of a thread +// @see https://api.slack.com/methods/assistant.threads.setTitle +func (api *Client) SetAssistantThreadsTitle(params AssistantThreadsSetTitleParameters) (err error) { + return api.SetAssistantThreadsTitleContext(context.Background(), params) +} + +// SetAssistantThreadsTitleContext sets the title of a thread with a custom context +// @see https://api.slack.com/methods/assistant.threads.setTitle +func (api *Client) SetAssistantThreadsTitleContext(ctx context.Context, params AssistantThreadsSetTitleParameters) (err error) { + + values := url.Values{ + "token": {api.token}, + } + + if params.ChannelID != "" { + values.Add("channel_id", params.ChannelID) + } + + if params.ThreadTS != "" { + values.Add("thread_ts", params.ThreadTS) + } + + if params.Title != "" { + values.Add("title", params.Title) + } + + response := struct { + SlackResponse + }{} + + err = api.postMethod(ctx, "assistant.threads.setTitle", values, &response) + if err != nil { + return + } + + 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 b5b79f9..be04e90 100644 --- a/vendor/github.com/slack-go/slack/attachments.go +++ b/vendor/github.com/slack-go/slack/attachments.go @@ -17,7 +17,7 @@ type AttachmentAction struct { Name string `json:"name"` // Required. Text string `json:"text"` // Required. Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger". - Type actionType `json:"type"` // Required. Must be set to "button" or "select". + Type ActionType `json:"type"` // Required. Must be set to "button" or "select". Value string `json:"value,omitempty"` // Optional. DataSource string `json:"data_source,omitempty"` // Optional. MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1. @@ -29,7 +29,7 @@ type AttachmentAction struct { } // actionType returns the type of the action -func (a AttachmentAction) actionType() actionType { +func (a AttachmentAction) actionType() ActionType { return a.Type } @@ -77,8 +77,16 @@ 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"` + FromURL string `json:"from_url,omitempty"` + OriginalURL string `json:"original_url,omitempty"` Fields []AttachmentField `json:"fields,omitempty"` Actions []AttachmentAction `json:"actions,omitempty"` diff --git a/vendor/github.com/slack-go/slack/audit.go b/vendor/github.com/slack-go/slack/audit.go new file mode 100644 index 0000000..135a68d --- /dev/null +++ b/vendor/github.com/slack-go/slack/audit.go @@ -0,0 +1,153 @@ +package slack + +import ( + "context" + "net/url" + "strconv" +) + +type AuditLogResponse struct { + Entries []AuditEntry `json:"entries"` + SlackResponse +} + +type AuditEntry struct { + ID string `json:"id"` + DateCreate int `json:"date_create"` + Action string `json:"action"` + Actor struct { + Type string `json:"type"` + User AuditUser `json:"user"` + } `json:"actor"` + Entity struct { + Type string `json:"type"` + // Only one of the below will be completed, based on the value of Type a user, a channel, a file, an app, a workspace, or an enterprise + User AuditUser `json:"user"` + Channel AuditChannel `json:"channel"` + File AuditFile `json:"file"` + App AuditApp `json:"app"` + Workspace AuditWorkspace `json:"workspace"` + Enterprise AuditEnterprise `json:"enterprise"` + } `json:"entity"` + Context struct { + Location struct { + Type string `json:"type"` + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + } `json:"location"` + UA string `json:"ua"` + IPAddress string `json:"ip_address"` + } `json:"context"` + Details struct { + NewValue interface{} `json:"new_value"` + PreviousValue interface{} `json:"previous_value"` + MobileOnly bool `json:"mobile_only"` + WebOnly bool `json:"web_only"` + NonSSOOnly bool `json:"non_sso_only"` + ExportType string `json:"export_type"` + ExportStart string `json:"export_start_ts"` + ExportEnd string `json:"export_end_ts"` + } `json:"details"` +} + +type AuditUser struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Team string `json:"team"` +} + +type AuditChannel struct { + ID string `json:"id"` + Name string `json:"name"` + Privacy string `json:"privacy"` + IsShared bool `json:"is_shared"` + IsOrgShared bool `json:"is_org_shared"` +} + +type AuditFile struct { + ID string `json:"id"` + Name string `json:"name"` + Filetype string `json:"filetype"` + Title string `json:"title"` +} + +type AuditApp struct { + ID string `json:"id"` + Name string `json:"name"` + IsDistributed bool `json:"is_distributed"` + IsDirectoryApproved bool `json:"is_directory_approved"` + IsWorkflowApp bool `json:"is_workflow_app"` + Scopes []string `json:"scopes"` +} + +type AuditWorkspace struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` +} + +type AuditEnterprise struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` +} + +// AuditLogParameters contains all the parameters necessary (including the optional ones) for a GetAuditLogs() request +type AuditLogParameters struct { + Limit int + Cursor string + Latest int + Oldest int + Action string + Actor string + Entity string +} + +func (api *Client) auditLogsRequest(ctx context.Context, path string, values url.Values) (*AuditLogResponse, error) { + response := &AuditLogResponse{} + // 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 + } + return response, response.Err() +} + +// GetAuditLogs retrieves a page of audit entires according to the parameters given +func (api *Client) GetAuditLogs(params AuditLogParameters) (entries []AuditEntry, nextCursor string, err error) { + return api.GetAuditLogsContext(context.Background(), params) +} + +// GetAuditLogsContext retrieves a page of audit entries according to the parameters given with a custom context +func (api *Client) GetAuditLogsContext(ctx context.Context, params AuditLogParameters) (entries []AuditEntry, nextCursor string, err error) { + values := url.Values{} + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Oldest != 0 { + values.Add("oldest", strconv.Itoa(params.Oldest)) + } + if params.Latest != 0 { + values.Add("latest", strconv.Itoa(params.Latest)) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Action != "" { + values.Add("action", params.Action) + } + if params.Actor != "" { + values.Add("actor", params.Actor) + } + if params.Entity != "" { + values.Add("entity", params.Entity) + } + + response, err := api.auditLogsRequest(ctx, "audit/v1/logs", values) + if err != nil { + return nil, "", err + } + return response.Entries, response.ResponseMetadata.Cursor, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/auth.go b/vendor/github.com/slack-go/slack/auth.go index f4f7f00..972f59e 100644 --- a/vendor/github.com/slack-go/slack/auth.go +++ b/vendor/github.com/slack-go/slack/auth.go @@ -3,6 +3,7 @@ package slack import ( "context" "net/url" + "strconv" ) // AuthRevokeResponse contains our Auth response from the auth.revoke endpoint @@ -22,12 +23,14 @@ func (api *Client) authRequest(ctx context.Context, path string, values url.Valu return response, response.Err() } -// SendAuthRevoke will send a revocation for our token +// SendAuthRevoke will send a revocation for our token. +// For more details, see SendAuthRevokeContext documentation. func (api *Client) SendAuthRevoke(token string) (*AuthRevokeResponse, error) { return api.SendAuthRevokeContext(context.Background(), token) } -// SendAuthRevokeContext will send a revocation request for our token to api.revoke with context +// SendAuthRevokeContext will send a revocation request for our token to api.revoke with a custom context. +// Slack API docs: https://api.slack.com/methods/auth.revoke func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*AuthRevokeResponse, error) { if token == "" { token = api.token @@ -38,3 +41,42 @@ func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*Au return api.authRequest(ctx, "auth.revoke", values) } + +type listTeamsResponse struct { + Teams []Team `json:"teams"` + SlackResponse +} + +type ListTeamsParameters struct { + Limit int + Cursor string + IncludeIcon *bool +} + +// ListTeams returns all workspaces a token can access. +// For more details, see ListTeamsContext documentation. +func (api *Client) ListTeams(params ListTeamsParameters) ([]Team, string, error) { + return api.ListTeamsContext(context.Background(), params) +} + +// ListTeamsContext returns all workspaces a token can access with a custom context. +// Slack API docs: https://api.slack.com/methods/auth.teams.list +func (api *Client) ListTeamsContext(ctx context.Context, params ListTeamsParameters) ([]Team, string, error) { + values := url.Values{ + "token": {api.token}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.IncludeIcon != nil { + values.Add("include_icon", strconv.FormatBool(*params.IncludeIcon)) + } + + response := &listTeamsResponse{} + err := api.postMethod(ctx, "auth.teams.list", values, response) + if err != nil { + return nil, "", err + } + + return response.Teams, response.ResponseMetadata.Cursor, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/block.go b/vendor/github.com/slack-go/slack/block.go index dbc3449..cc5d80f 100644 --- a/vendor/github.com/slack-go/slack/block.go +++ b/vendor/github.com/slack-go/slack/block.go @@ -1,27 +1,36 @@ package slack -// @NOTE: Blocks are in beta and subject to change. - -// More Information: https://api.slack.com/block-kit - // MessageBlockType defines a named string type to define each block type // as a constant for use within the package. 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" + 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 // to ensure consistency between blocks. type Block interface { BlockType() MessageBlockType + ID() string } // Blocks is a convenience struct defined to allow dynamic unmarshalling of @@ -32,27 +41,36 @@ type Blocks struct { // BlockAction is the action callback sent when a block is interacted with type BlockAction struct { - ActionID string `json:"action_id"` - BlockID string `json:"block_id"` - Type actionType `json:"type"` - Text TextBlockObject `json:"text"` - Value string `json:"value"` - ActionTs string `json:"action_ts"` - SelectedOption OptionBlockObject `json:"selected_option"` - SelectedOptions []OptionBlockObject `json:"selected_options"` - SelectedUser string `json:"selected_user"` - SelectedChannel string `json:"selected_channel"` - SelectedConversation string `json:"selected_conversation"` - SelectedDate string `json:"selected_date"` - InitialOption OptionBlockObject `json:"initial_option"` - InitialUser string `json:"initial_user"` - InitialChannel string `json:"initial_channel"` - InitialConversation string `json:"initial_conversation"` - InitialDate string `json:"initial_date"` + ActionID string `json:"action_id"` + BlockID string `json:"block_id"` + Type ActionType `json:"type"` + Text TextBlockObject `json:"text"` + Value string `json:"value"` + Files []File `json:"files"` + ActionTs string `json:"action_ts"` + SelectedOption OptionBlockObject `json:"selected_option"` + SelectedOptions []OptionBlockObject `json:"selected_options"` + SelectedUser string `json:"selected_user"` + SelectedUsers []string `json:"selected_users"` + SelectedChannel string `json:"selected_channel"` + SelectedChannels []string `json:"selected_channels"` + SelectedConversation string `json:"selected_conversation"` + SelectedConversations []string `json:"selected_conversations"` + SelectedDate string `json:"selected_date"` + SelectedTime string `json:"selected_time"` + SelectedDateTime int64 `json:"selected_date_time"` + Timezone string `json:"timezone"` + InitialOption OptionBlockObject `json:"initial_option"` + InitialUser string `json:"initial_user"` + InitialChannel string `json:"initial_channel"` + InitialConversation string `json:"initial_conversation"` + InitialDate string `json:"initial_date"` + InitialTime string `json:"initial_time"` + RichTextValue RichTextBlock `json:"rich_text_value"` } // actionType returns the type of the action -func (b BlockAction) actionType() actionType { +func (b BlockAction) actionType() ActionType { return b.Type } diff --git a/vendor/github.com/slack-go/slack/block_action.go b/vendor/github.com/slack-go/slack/block_action.go index fe46a95..819c0ef 100644 --- a/vendor/github.com/slack-go/slack/block_action.go +++ b/vendor/github.com/slack-go/slack/block_action.go @@ -6,7 +6,7 @@ package slack type ActionBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` - Elements BlockElements `json:"elements"` + Elements *BlockElements `json:"elements"` } // BlockType returns the type of the block @@ -14,12 +14,17 @@ func (s ActionBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s ActionBlock) ID() string { + return s.BlockID +} + // NewActionBlock returns a new instance of an Action Block func NewActionBlock(blockID string, elements ...BlockElement) *ActionBlock { return &ActionBlock{ Type: MBTAction, BlockID: blockID, - Elements: BlockElements{ + Elements: &BlockElements{ ElementSet: elements, }, } 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 0000000..ddb151b --- /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 new file mode 100644 index 0000000..621a8b6 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_call.go @@ -0,0 +1,93 @@ +package slack + +// CallBlock defines data that is used to display a call in slack. +// +// More Information: https://api.slack.com/apis/calls#post_to_channel +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 +func (s CallBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +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, 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 0000000..ccee174 --- /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 0000000..5d407db --- /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.go b/vendor/github.com/slack-go/slack/block_context.go index c37bf27..879ee61 100644 --- a/vendor/github.com/slack-go/slack/block_context.go +++ b/vendor/github.com/slack-go/slack/block_context.go @@ -3,7 +3,7 @@ package slack // ContextBlock defines data that is used to display message context, which can // include both images and text. // -// More Information: https://api.slack.com/reference/messaging/blocks#actions +// More Information: https://api.slack.com/reference/messaging/blocks#context type ContextBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` @@ -15,6 +15,11 @@ func (s ContextBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s ContextBlock) ID() string { + return s.BlockID +} + type ContextElements struct { Elements []MixedElement } 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 0000000..d1cf532 --- /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 00d59c3..b83ad83 100644 --- a/vendor/github.com/slack-go/slack/block_conv.go +++ b/vendor/github.com/slack-go/slack/block_conv.go @@ -2,8 +2,7 @@ package slack import ( "encoding/json" - - "github.com/pkg/errors" + "fmt" ) type sumtype struct { @@ -54,18 +53,49 @@ func (b *Blocks) UnmarshalJSON(data []byte) error { block = &ActionBlock{} case "context": block = &ContextBlock{} + case "context_actions": + block = &ContextActionsBlock{} case "divider": block = &DividerBlock{} case "file": block = &FileBlock{} + case "header": + block = &HeaderBlock{} case "image": block = &ImageBlock{} case "input": block = &InputBlock{} + case "markdown": + block = &MarkdownBlock{} + case "rich_text": + block = &RichTextBlock{} + case "rich_text_input": + block = &RichTextBlock{} case "section": block = &SectionBlock{} + case "call": + 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) @@ -104,14 +134,40 @@ func (b *InputBlock) UnmarshalJSON(data []byte) error { switch s.TypeVal { case "datepicker": e = &DatePickerBlockElement{} + case "timepicker": + e = &TimePickerBlockElement{} + case "datetimepicker": + e = &DateTimePickerBlockElement{} case "plain_text_input": e = &PlainTextInputBlockElement{} + case "rich_text_input": + e = &RichTextInputBlockElement{} + case "email_text_input": + e = &EmailTextInputBlockElement{} + case "url_text_input": + e = &URLTextInputBlockElement{} case "static_select", "external_select", "users_select", "conversations_select", "channels_select": e = &SelectBlockElement{} case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": e = &MultiSelectBlockElement{} + case "checkboxes": + e = &CheckboxGroupsBlockElement{} + case "overflow": + e = &OverflowBlockElement{} + case "radio_buttons": + e = &RadioButtonsBlockElement{} + case "number_input": + e = &NumberInputBlockElement{} + case "file_input": + e = &FileInputBlockElement{} + case "feedback_buttons": + e = &FeedbackButtonsBlockElement{} + case "icon_button": + e = &IconButtonBlockElement{} + case "workflow_button": + e = &WorkflowButtonBlockElement{} default: - return errors.New("unsupported block element type") + return fmt.Errorf("unsupported block element type %v", s.TypeVal) } if err := json.Unmarshal(a.Element, e); err != nil { @@ -170,12 +226,38 @@ func (b *BlockElements) UnmarshalJSON(data []byte) error { blockElement = &OverflowBlockElement{} case "datepicker": blockElement = &DatePickerBlockElement{} + case "timepicker": + blockElement = &TimePickerBlockElement{} + case "datetimepicker": + blockElement = &DateTimePickerBlockElement{} case "plain_text_input": blockElement = &PlainTextInputBlockElement{} + case "rich_text_input": + blockElement = &RichTextInputBlockElement{} + case "email_text_input": + blockElement = &EmailTextInputBlockElement{} + case "url_text_input": + blockElement = &URLTextInputBlockElement{} + case "checkboxes": + blockElement = &CheckboxGroupsBlockElement{} + case "radio_buttons": + 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 errors.New("unsupported block element type") + return fmt.Errorf("unsupported block element type %v", blockElementType) } err = json.Unmarshal(r, blockElement) @@ -203,6 +285,7 @@ func (a *Accessory) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the Unmarshaller interface for Accessory, so that any JSON // unmarshalling is delegated and proper type determination can be made before unmarshal +// Note: datetimepicker is not supported in Accessory func (a *Accessory) UnmarshalJSON(data []byte) error { var r json.RawMessage @@ -251,12 +334,24 @@ func (a *Accessory) UnmarshalJSON(data []byte) error { return err } a.DatePickerElement = element.(*DatePickerBlockElement) + case "timepicker": + element, err := unmarshalBlockElement(r, &TimePickerBlockElement{}) + if err != nil { + return err + } + a.TimePickerElement = element.(*TimePickerBlockElement) case "plain_text_input": element, err := unmarshalBlockElement(r, &PlainTextInputBlockElement{}) if err != nil { return err } a.PlainTextInputElement = element.(*PlainTextInputBlockElement) + case "rich_text_input": + element, err := unmarshalBlockElement(r, &RichTextInputBlockElement{}) + if err != nil { + return err + } + a.RichTextInputElement = element.(*RichTextInputBlockElement) case "radio_buttons": element, err := unmarshalBlockElement(r, &RadioButtonsBlockElement{}) if err != nil { @@ -275,6 +370,18 @@ func (a *Accessory) UnmarshalJSON(data []byte) error { return err } a.MultiSelectElement = element.(*MultiSelectBlockElement) + case "checkboxes": + element, err := unmarshalBlockElement(r, &CheckboxGroupsBlockElement{}) + if err != nil { + 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 { @@ -307,18 +414,30 @@ func toBlockElement(element *Accessory) BlockElement { if element.DatePickerElement != nil { return element.DatePickerElement } + if element.TimePickerElement != nil { + return element.TimePickerElement + } if element.PlainTextInputElement != nil { return element.PlainTextInputElement } if element.RadioButtonsElement != nil { return element.RadioButtonsElement } + if element.CheckboxGroupsBlockElement != nil { + return element.CheckboxGroupsBlockElement + } if element.SelectElement != nil { return element.SelectElement } if element.MultiSelectElement != nil { return element.MultiSelectElement } + if element.RichTextInputElement != nil { + return element.RichTextInputElement + } + if element.WorkflowButtonElement != nil { + return element.WorkflowButtonElement + } return nil } @@ -376,7 +495,7 @@ func (e *ContextElements) UnmarshalJSON(data []byte) error { e.Elements = append(e.Elements, elem.(*ImageBlockElement)) default: - return errors.New("unsupported context element type") + return fmt.Errorf("unsupported context element type %v", contextElementType) } } diff --git a/vendor/github.com/slack-go/slack/block_divider.go b/vendor/github.com/slack-go/slack/block_divider.go index 2d442ba..e10d7b0 100644 --- a/vendor/github.com/slack-go/slack/block_divider.go +++ b/vendor/github.com/slack-go/slack/block_divider.go @@ -13,10 +13,14 @@ func (s DividerBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s DividerBlock) ID() string { + return s.BlockID +} + // NewDividerBlock returns a new instance of a divider block func NewDividerBlock() *DividerBlock { return &DividerBlock{ Type: MBTDivider, } - } diff --git a/vendor/github.com/slack-go/slack/block_element.go b/vendor/github.com/slack-go/slack/block_element.go index 50971bf..1284934 100644 --- a/vendor/github.com/slack-go/slack/block_element.go +++ b/vendor/github.com/slack-go/slack/block_element.go @@ -3,13 +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" - METPlainTextInput MessageElementType = "plain_text_input" - METRadioButtons MessageElementType = "radio_buttons" + 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" @@ -40,36 +50,48 @@ type MixedElement interface { } type Accessory struct { - ImageElement *ImageBlockElement - ButtonElement *ButtonBlockElement - OverflowElement *OverflowBlockElement - DatePickerElement *DatePickerBlockElement - PlainTextInputElement *PlainTextInputBlockElement - RadioButtonsElement *RadioButtonsBlockElement - SelectElement *SelectBlockElement - MultiSelectElement *MultiSelectBlockElement - UnknownElement *UnknownBlockElement + ImageElement *ImageBlockElement + ButtonElement *ButtonBlockElement + OverflowElement *OverflowBlockElement + DatePickerElement *DatePickerBlockElement + TimePickerElement *TimePickerBlockElement + PlainTextInputElement *PlainTextInputBlockElement + RichTextInputElement *RichTextInputBlockElement + RadioButtonsElement *RadioButtonsBlockElement + 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} case *PlainTextInputBlockElement: - return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)} + return &Accessory{PlainTextInputElement: element} + case *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} + case *WorkflowButtonBlockElement: + return &Accessory{WorkflowButtonElement: element} default: return &Accessory{UnknownElement: element.(*UnknownBlockElement)} } @@ -101,9 +123,10 @@ 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"` - AltText string `json:"alt_text"` + Type MessageElementType `json:"type"` + ImageURL *string `json:"image_url,omitempty"` + AltText string `json:"alt_text"` + SlackFile *SlackFileObject `json:"slack_file,omitempty"` } // ElementType returns the type of the Element @@ -119,15 +142,27 @@ func (s ImageBlockElement) MixedElementType() MixedElementType { func NewImageBlockElement(imageURL, altText string) *ImageBlockElement { return &ImageBlockElement{ Type: METImage, - ImageURL: imageURL, + ImageURL: &imageURL, AltText: altText, } } +// NewImageBlockElementSlackFile returns a new instance of an image block element +// TODO: BREAKING CHANGE - This should be combined with the function above +func NewImageBlockElementSlackFile(slackFile *SlackFileObject, altText string) *ImageBlockElement { + return &ImageBlockElement{ + Type: METImage, + SlackFile: slackFile, + AltText: altText, + } +} + +// Style is a style of Button element +// https://api.slack.com/reference/block-kit/block-elements#button__fields type Style string const ( - StyleDefault Style = "default" + StyleDefault Style = "" StylePrimary Style = "primary" StyleDanger Style = "danger" ) @@ -152,9 +187,22 @@ func (s ButtonBlockElement) ElementType() MessageElementType { return s.Type } -// add styling to button object -func (s *ButtonBlockElement) WithStyle(style Style) { +// WithStyle adds styling to the button object and returns the modified ButtonBlockElement +func (s *ButtonBlockElement) WithStyle(style Style) *ButtonBlockElement { s.Style = style + return s +} + +// WithConfirm adds a confirmation dialogue to the button object and returns the modified ButtonBlockElement +func (s *ButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *ButtonBlockElement { + s.Confirm = confirm + return s +} + +// WithURL adds a URL for the button to link to and returns the modified ButtonBlockElement +func (s *ButtonBlockElement) WithURL(url string) *ButtonBlockElement { + s.URL = url + return s } // NewButtonBlockElement returns an instance of a new button element to be used within a block @@ -186,17 +234,30 @@ type OptionGroupsResponse struct { // // More Information: https://api.slack.com/reference/messaging/block-elements#select type SelectBlockElement struct { - Type string `json:"type,omitempty"` - Placeholder *TextBlockObject `json:"placeholder,omitempty"` - ActionID string `json:"action_id,omitempty"` - Options []*OptionBlockObject `json:"options,omitempty"` - OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` - InitialOption *OptionBlockObject `json:"initial_option,omitempty"` - InitialUser string `json:"initial_user,omitempty"` - InitialConversation string `json:"initial_conversation,omitempty"` - InitialChannel string `json:"initial_channel,omitempty"` - MinQueryLength *int `json:"min_query_length,omitempty"` - Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + Type string `json:"type,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options,omitempty"` + OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` + InitialOption *OptionBlockObject `json:"initial_option,omitempty"` + InitialUser string `json:"initial_user,omitempty"` + InitialConversation string `json:"initial_conversation,omitempty"` + InitialChannel string `json:"initial_channel,omitempty"` + DefaultToCurrentConversation bool `json:"default_to_current_conversation,omitempty"` + ResponseURLEnabled bool `json:"response_url_enabled,omitempty"` + 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. +// +// More Information: https://api.slack.com/reference/block-kit/composition-objects#filter_conversations +type SelectBlockElementFilter struct { + Include []string `json:"include,omitempty"` + ExcludeExternalSharedChannels bool `json:"exclude_external_shared_channels,omitempty"` + ExcludeBotUsers bool `json:"exclude_bot_users,omitempty"` } // ElementType returns the type of the Element @@ -215,6 +276,36 @@ func NewOptionsSelectBlockElement(optType string, placeholder *TextBlockObject, } } +// WithInitialOption sets the initial option for the select element +func (s *SelectBlockElement) WithInitialOption(option *OptionBlockObject) *SelectBlockElement { + s.InitialOption = option + return s +} + +// WithInitialUser sets the initial user for the select element +func (s *SelectBlockElement) WithInitialUser(user string) *SelectBlockElement { + s.InitialUser = user + return s +} + +// WithInitialConversation sets the initial conversation for the select element +func (s *SelectBlockElement) WithInitialConversation(conversation string) *SelectBlockElement { + s.InitialConversation = conversation + return s +} + +// WithInitialChannel sets the initial channel for the select element +func (s *SelectBlockElement) WithInitialChannel(channel string) *SelectBlockElement { + s.InitialChannel = channel + return s +} + +// WithConfirm adds a confirmation dialogue to the select element +func (s *SelectBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *SelectBlockElement { + s.Confirm = confirm + return s +} + // NewOptionsGroupSelectBlockElement returns a new instance of SelectBlockElement for use with // the Options object only. func NewOptionsGroupSelectBlockElement( @@ -245,8 +336,11 @@ type MultiSelectBlockElement struct { InitialUsers []string `json:"initial_users,omitempty"` InitialConversations []string `json:"initial_conversations,omitempty"` InitialChannels []string `json:"initial_channels,omitempty"` + Filter *SelectBlockElementFilter `json:"filter,omitempty"` 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 @@ -265,6 +359,48 @@ func NewOptionsMultiSelectBlockElement(optType string, placeholder *TextBlockObj } } +// WithInitialOptions sets the initial options for the multi-select element +func (s *MultiSelectBlockElement) WithInitialOptions(options ...*OptionBlockObject) *MultiSelectBlockElement { + s.InitialOptions = options + return s +} + +// WithInitialUsers sets the initial users for the multi-select element +func (s *MultiSelectBlockElement) WithInitialUsers(users ...string) *MultiSelectBlockElement { + s.InitialUsers = users + return s +} + +// WithInitialConversations sets the initial conversations for the multi-select element +func (s *MultiSelectBlockElement) WithInitialConversations(conversations ...string) *MultiSelectBlockElement { + s.InitialConversations = conversations + return s +} + +// WithInitialChannels sets the initial channels for the multi-select element +func (s *MultiSelectBlockElement) WithInitialChannels(channels ...string) *MultiSelectBlockElement { + s.InitialChannels = channels + return s +} + +// WithConfirm adds a confirmation dialogue to the multi-select element +func (s *MultiSelectBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *MultiSelectBlockElement { + s.Confirm = confirm + return s +} + +// WithMaxSelectedItems sets the maximum number of items that can be selected +func (s *MultiSelectBlockElement) WithMaxSelectedItems(maxSelectedItems int) *MultiSelectBlockElement { + s.MaxSelectedItems = &maxSelectedItems + return s +} + +// WithMinQueryLength sets the minimum query length for the multi-select element +func (s *MultiSelectBlockElement) WithMinQueryLength(minQueryLength int) *MultiSelectBlockElement { + s.MinQueryLength = &minQueryLength + return s +} + // NewOptionsGroupMultiSelectBlockElement returns a new instance of MultiSelectBlockElement for use with // the Options object only. func NewOptionsGroupMultiSelectBlockElement( @@ -308,6 +444,12 @@ func NewOverflowBlockElement(actionID string, options ...*OptionBlockObject) *Ov } } +// WithConfirm adds a confirmation dialogue to the overflow element +func (s *OverflowBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *OverflowBlockElement { + s.Confirm = confirm + return s +} + // DatePickerBlockElement defines an element which lets users easily select a // date from a calendar style UI. Date picker elements can be used inside of // section and actions blocks. @@ -315,10 +457,11 @@ func NewOverflowBlockElement(actionID string, options ...*OptionBlockObject) *Ov // More Information: https://api.slack.com/reference/messaging/block-elements#datepicker type DatePickerBlockElement struct { Type MessageElementType `json:"type"` - ActionID string `json:"action_id"` + ActionID string `json:"action_id,omitempty"` 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 @@ -334,19 +477,135 @@ func NewDatePickerBlockElement(actionID string) *DatePickerBlockElement { } } +// TimePickerBlockElement defines an element which lets users easily select a +// time from nice UI. Time picker elements can be used inside of +// section and actions blocks. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#timepicker +type TimePickerBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + 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 +func (s TimePickerBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewTimePickerBlockElement returns an instance of a date picker element +func NewTimePickerBlockElement(actionID string) *TimePickerBlockElement { + return &TimePickerBlockElement{ + Type: METTimepicker, + ActionID: actionID, + } +} + +// DateTimePickerBlockElement defines an element that allows the selection of both +// a date and a time of day formatted as a UNIX timestamp. +// More Information: https://api.slack.com/reference/messaging/block-elements#datetimepicker +type DateTimePickerBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + InitialDateTime int64 `json:"initial_date_time,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` +} + +// ElementType returns the type of the Element +func (s DateTimePickerBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewDatePickerBlockElement returns an instance of a datetime picker element +func NewDateTimePickerBlockElement(actionID string) *DateTimePickerBlockElement { + return &DateTimePickerBlockElement{ + Type: METDatetimepicker, + ActionID: actionID, + } +} + +// EmailTextInputBlockElement creates a field where a user can enter email +// data. +// email-text-input elements are currently only available in modals. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#email +type EmailTextInputBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialValue string `json:"initial_value,omitempty"` + DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` +} + +// ElementType returns the type of the Element +func (s EmailTextInputBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewEmailTextInputBlockElement returns an instance of a plain-text input +// element +func NewEmailTextInputBlockElement(placeholder *TextBlockObject, actionID string) *EmailTextInputBlockElement { + return &EmailTextInputBlockElement{ + Type: METEmailTextInput, + ActionID: actionID, + Placeholder: placeholder, + } +} + +// URLTextInputBlockElement creates a field where a user can enter url data. +// +// url-text-input elements are currently only available in modals. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#url +type URLTextInputBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialValue string `json:"initial_value,omitempty"` + DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` +} + +// ElementType returns the type of the Element +func (s URLTextInputBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewURLTextInputBlockElement returns an instance of a plain-text input +// element +func NewURLTextInputBlockElement(placeholder *TextBlockObject, actionID string) *URLTextInputBlockElement { + return &URLTextInputBlockElement{ + Type: METURLTextInput, + ActionID: actionID, + Placeholder: placeholder, + } +} + // PlainTextInputBlockElement creates a field where a user can enter freeform // data. // Plain-text input elements are currently only available in modals. // // More Information: https://api.slack.com/reference/block-kit/block-elements#input type PlainTextInputBlockElement struct { - Type MessageElementType `json:"type"` - ActionID string `json:"action_id"` - Placeholder *TextBlockObject `json:"placeholder,omitempty"` - InitialValue string `json:"initial_value,omitempty"` - Multiline bool `json:"multiline,omitempty"` - MinLength int `json:"min_length,omitempty"` - MaxLength int `json:"max_length,omitempty"` + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialValue string `json:"initial_value,omitempty"` + Multiline bool `json:"multiline,omitempty"` + 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 { + TriggerActionsOn []string `json:"trigger_actions_on,omitempty"` } // ElementType returns the type of the Element @@ -364,16 +623,73 @@ func NewPlainTextInputBlockElement(placeholder *TextBlockObject, actionID string } } +// WithInitialValue sets the initial value for the plain-text input element +func (s *PlainTextInputBlockElement) WithInitialValue(initialValue string) *PlainTextInputBlockElement { + s.InitialValue = initialValue + return s +} + +// WithMinLength sets the minimum length for the plain-text input element +func (s *PlainTextInputBlockElement) WithMinLength(minLength int) *PlainTextInputBlockElement { + s.MinLength = minLength + return s +} + +// WithMaxLength sets the maximum length for the plain-text input element +func (s *PlainTextInputBlockElement) WithMaxLength(maxLength int) *PlainTextInputBlockElement { + s.MaxLength = maxLength + return s +} + +// WithMultiline sets the multiline property for the plain-text input element +func (s *PlainTextInputBlockElement) WithMultiline(multiline bool) *PlainTextInputBlockElement { + s.Multiline = multiline + return s +} + +// WithDispatchActionConfig sets the dispatch action config for the plain-text input element +func (s *PlainTextInputBlockElement) WithDispatchActionConfig(config *DispatchActionConfig) *PlainTextInputBlockElement { + s.DispatchActionConfig = config + return s +} + +// RichTextInputBlockElement creates a field where allows users to enter formatted text +// in a WYSIWYG composer, offering the same messaging writing experience as in Slack +// More Information: https://api.slack.com/reference/block-kit/block-elements#rich_text_input +type RichTextInputBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialValue *RichTextBlock `json:"initial_value,omitempty"` + DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` + FocusOnLoad bool `json:"focus_on_load,omitempty"` +} + +// ElementType returns the type of the Element +func (s RichTextInputBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewRichTextInputBlockElement returns an instance of a rich-text input element +func NewRichTextInputBlockElement(placeholder *TextBlockObject, actionID string) *RichTextInputBlockElement { + return &RichTextInputBlockElement{ + Type: METRichTextInput, + ActionID: actionID, + Placeholder: placeholder, + } +} + // CheckboxGroupsBlockElement defines an element which allows users to choose // one or more items from a list of possible options. // // More Information: https://api.slack.com/reference/block-kit/block-elements#checkboxes type CheckboxGroupsBlockElement struct { Type MessageElementType `json:"type"` - ActionID string `json:"action_id"` + ActionID string `json:"action_id,omitempty"` 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 @@ -381,7 +697,7 @@ func (c CheckboxGroupsBlockElement) ElementType() MessageElementType { return c.Type } -// NewRadioButtonsBlockElement returns an instance of a radio block element +// NewCheckboxGroupsBlockElement returns an instance of a checkbox-group block element func NewCheckboxGroupsBlockElement(actionID string, options ...*OptionBlockObject) *CheckboxGroupsBlockElement { return &CheckboxGroupsBlockElement{ Type: METCheckboxGroups, @@ -396,10 +712,11 @@ func NewCheckboxGroupsBlockElement(actionID string, options ...*OptionBlockObjec // More Information: https://api.slack.com/reference/block-kit/block-elements#radio type RadioButtonsBlockElement struct { Type MessageElementType `json:"type"` - ActionID string `json:"action_id"` + ActionID string `json:"action_id,omitempty"` 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 @@ -415,3 +732,263 @@ func NewRadioButtonsBlockElement(actionID string, options ...*OptionBlockObject) Options: options, } } + +// NumberInputBlockElement creates a field where a user can enter number +// data. +// Number input elements are currently only available in modals. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#number +type NumberInputBlockElement struct { + Type MessageElementType `json:"type"` + IsDecimalAllowed bool `json:"is_decimal_allowed"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialValue string `json:"initial_value,omitempty"` + 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 +func (s NumberInputBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewNumberInputBlockElement returns an instance of a number input element +func NewNumberInputBlockElement(placeholder *TextBlockObject, actionID string, isDecimalAllowed bool) *NumberInputBlockElement { + return &NumberInputBlockElement{ + Type: METNumber, + ActionID: actionID, + Placeholder: placeholder, + IsDecimalAllowed: isDecimalAllowed, + } +} + +// WithInitialValue sets the initial value for the number input element +func (s *NumberInputBlockElement) WithInitialValue(initialValue string) *NumberInputBlockElement { + s.InitialValue = initialValue + return s +} + +// WithMinValue sets the minimum value for the number input element +func (s *NumberInputBlockElement) WithMinValue(minValue string) *NumberInputBlockElement { + s.MinValue = minValue + return s +} + +// WithMaxValue sets the maximum value for the number input element +func (s *NumberInputBlockElement) WithMaxValue(maxValue string) *NumberInputBlockElement { + s.MaxValue = maxValue + return s +} + +// WithDispatchActionConfig sets the dispatch action config for the number input element +func (s *NumberInputBlockElement) WithDispatchActionConfig(config *DispatchActionConfig) *NumberInputBlockElement { + s.DispatchActionConfig = config + return s +} + +// FileInputBlockElement creates a field where a user can upload a file. +// +// File input elements are currently only available in modals. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#file_input +type FileInputBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + FileTypes []string `json:"filetypes,omitempty"` + MaxFiles int `json:"max_files,omitempty"` +} + +// ElementType returns the type of the Element +func (s FileInputBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewFileInputBlockElement returns an instance of a file input element +func NewFileInputBlockElement(actionID string) *FileInputBlockElement { + return &FileInputBlockElement{ + Type: METFileInput, + ActionID: actionID, + } +} + +// WithFileTypes sets the file types that can be uploaded +func (s *FileInputBlockElement) WithFileTypes(fileTypes ...string) *FileInputBlockElement { + s.FileTypes = fileTypes + return s +} + +// WithMaxFiles sets the maximum number of files that can be uploaded +func (s *FileInputBlockElement) WithMaxFiles(maxFiles int) *FileInputBlockElement { + 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_file.go b/vendor/github.com/slack-go/slack/block_file.go index ac4453f..2f669b0 100644 --- a/vendor/github.com/slack-go/slack/block_file.go +++ b/vendor/github.com/slack-go/slack/block_file.go @@ -15,6 +15,11 @@ func (s FileBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s FileBlock) ID() string { + return s.BlockID +} + // NewFileBlock returns a new instance of a file block func NewFileBlock(blockID string, externalID string, source string) *FileBlock { return &FileBlock{ diff --git a/vendor/github.com/slack-go/slack/block_header.go b/vendor/github.com/slack-go/slack/block_header.go new file mode 100644 index 0000000..4277057 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_header.go @@ -0,0 +1,45 @@ +package slack + +// HeaderBlock defines a new block of type header +// +// More Information: https://api.slack.com/reference/messaging/blocks#header +type HeaderBlock struct { + Type MessageBlockType `json:"type"` + Text *TextBlockObject `json:"text,omitempty"` + BlockID string `json:"block_id,omitempty"` +} + +// BlockType returns the type of the block +func (s HeaderBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s HeaderBlock) ID() string { + return s.BlockID +} + +// HeaderBlockOption allows configuration of options for a new header block +type HeaderBlockOption func(*HeaderBlock) + +func HeaderBlockOptionBlockID(blockID string) HeaderBlockOption { + return func(block *HeaderBlock) { + block.BlockID = blockID + } +} + +// NewHeaderBlock returns a new instance of a header block to be rendered +func NewHeaderBlock(textObj *TextBlockObject, options ...HeaderBlockOption) *HeaderBlock { + block := HeaderBlock{ + Type: MBTHeader, + Text: textObj, + } + + for _, option := range options { + if option != nil { + option(&block) + } + } + + return &block +} diff --git a/vendor/github.com/slack-go/slack/block_image.go b/vendor/github.com/slack-go/slack/block_image.go index 6de3f63..2a914e5 100644 --- a/vendor/github.com/slack-go/slack/block_image.go +++ b/vendor/github.com/slack-go/slack/block_image.go @@ -4,11 +4,26 @@ package slack // // More Information: https://api.slack.com/reference/messaging/blocks#image type ImageBlock struct { - Type MessageBlockType `json:"type"` - ImageURL string `json:"image_url"` - AltText string `json:"alt_text"` - BlockID string `json:"block_id,omitempty"` - Title *TextBlockObject `json:"title"` + Type MessageBlockType `json:"type"` + ImageURL string `json:"image_url,omitempty"` + AltText string `json:"alt_text"` + BlockID string `json:"block_id,omitempty"` + Title *TextBlockObject `json:"title,omitempty"` + SlackFile *SlackFileObject `json:"slack_file,omitempty"` +} + +// ID returns the ID of the block +func (s ImageBlock) ID() string { + return s.BlockID +} + +// SlackFileObject Defines an object containing Slack file information to be used in an +// image block or image element. +// +// More Information: https://api.slack.com/reference/block-kit/composition-objects#slack_file +type SlackFileObject struct { + ID string `json:"id,omitempty"` + URL string `json:"url,omitempty"` } // BlockType returns the type of the block @@ -26,3 +41,15 @@ func NewImageBlock(imageURL, altText, blockID string, title *TextBlockObject) *I Title: title, } } + +// NewImageBlockSlackFile returns an instance of a new Image Block type +// TODO: BREAKING CHANGE - This should be combined with the function above +func NewImageBlockSlackFile(slackFile *SlackFileObject, altText string, blockID string, title *TextBlockObject) *ImageBlock { + return &ImageBlock{ + Type: MBTImage, + SlackFile: slackFile, + AltText: altText, + BlockID: blockID, + Title: title, + } +} diff --git a/vendor/github.com/slack-go/slack/block_input.go b/vendor/github.com/slack-go/slack/block_input.go index 10638cd..f74eda6 100644 --- a/vendor/github.com/slack-go/slack/block_input.go +++ b/vendor/github.com/slack-go/slack/block_input.go @@ -4,12 +4,13 @@ package slack // // More Information: https://api.slack.com/reference/block-kit/blocks#input type InputBlock struct { - Type MessageBlockType `json:"type"` - BlockID string `json:"block_id,omitempty"` - Label *TextBlockObject `json:"label"` - Element BlockElement `json:"element"` - Hint *TextBlockObject `json:"hint,omitempty"` - Optional bool `json:"optional,omitempty"` + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Label *TextBlockObject `json:"label"` + Element BlockElement `json:"element"` + Hint *TextBlockObject `json:"hint,omitempty"` + Optional bool `json:"optional,omitempty"` + DispatchAction bool `json:"dispatch_action,omitempty"` } // BlockType returns the type of the block @@ -17,12 +18,30 @@ func (s InputBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s InputBlock) ID() string { + return s.BlockID +} + // NewInputBlock returns a new instance of an input block -func NewInputBlock(blockID string, label *TextBlockObject, element BlockElement) *InputBlock { +func NewInputBlock(blockID string, label, hint *TextBlockObject, element BlockElement) *InputBlock { return &InputBlock{ Type: MBTInput, BlockID: blockID, Label: label, Element: element, + Hint: hint, } } + +// WithOptional sets the optional flag on the input block +func (s *InputBlock) WithOptional(optional bool) *InputBlock { + s.Optional = optional + return s +} + +// WithDispatchAction sets the dispatch action flag on the input block +func (s *InputBlock) WithDispatchAction(dispatchAction bool) *InputBlock { + s.DispatchAction = dispatchAction + return s +} 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 0000000..43798f5 --- /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_markdown.go b/vendor/github.com/slack-go/slack/block_markdown.go new file mode 100644 index 0000000..e22a0d1 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_markdown.go @@ -0,0 +1,34 @@ +package slack + +// MarkdownBlock defines a block that lets you use markdown to format your text. +// +// This block can be used with AI apps when you expect a markdown response from an LLM +// that can get lost in translation rendering in Slack. Providing it in a markdown block +// leaves the translating to Slack to ensure your message appears as intended. Note that +// passing a single block may result in multiple blocks after translation. +// +// More Information: https://api.slack.com/reference/block-kit/blocks#markdown +type MarkdownBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Text string `json:"text"` +} + +// BlockType returns the type of the block +func (s MarkdownBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s MarkdownBlock) ID() string { + return s.BlockID +} + +// NewMarkdownBlock returns an instance of a new Markdown Block type +func NewMarkdownBlock(blockID, text string) *MarkdownBlock { + return &MarkdownBlock{ + Type: MBTMarkdown, + BlockID: blockID, + Text: text, + } +} diff --git a/vendor/github.com/slack-go/slack/block_object.go b/vendor/github.com/slack-go/slack/block_object.go index cf3536d..c1b64cc 100644 --- a/vendor/github.com/slack-go/slack/block_object.go +++ b/vendor/github.com/slack-go/slack/block_object.go @@ -2,6 +2,7 @@ package slack import ( "encoding/json" + "errors" ) // Block Objects are also known as Composition Objects @@ -10,7 +11,6 @@ import ( // BlockObject defines an interface that all block object types should // implement. -// @TODO: Is this interface needed? // blockObject object types const ( @@ -121,7 +121,7 @@ func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, e type TextBlockObject struct { Type string `json:"type"` Text string `json:"text"` - Emoji bool `json:"emoji,omitempty"` + Emoji *bool `json:"emoji,omitempty"` Verbatim bool `json:"verbatim,omitempty"` } @@ -135,19 +135,59 @@ func (s TextBlockObject) MixedElementType() MixedElementType { return MixedElementText } +// Validate checks if TextBlockObject has valid values +func (s TextBlockObject) Validate() error { + if s.Type != "plain_text" && s.Type != "mrkdwn" { + return errors.New("type must be either of plain_text or mrkdwn") + } + + if s.Type == "mrkdwn" && s.Emoji != nil { + return errors.New("emoji cannot be set for mrkdwn type") + } + + // https://api.slack.com/reference/block-kit/composition-objects#text__fields + if len(s.Text) == 0 { + return errors.New("text must have a minimum length of 1") + } + + // https://api.slack.com/reference/block-kit/composition-objects#text__fields + if len(s.Text) > 3000 { + return errors.New("text cannot be longer than 3000 characters") + } + + return nil +} + // NewTextBlockObject returns an instance of a new Text Block Object -func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject { +// +// If you want to create a mrkdwn object, you should set the emoji parameter to false. The +// reason is that Slack doesn't accept emoji in mrkdwn. +func NewTextBlockObject(elementType, text string, emoji bool, verbatim bool) *TextBlockObject { + // If we're trying to build a mrkdwn object, we can't send emoji at all. I think the + // right approach here is to be a bit clever, and not break the function interface. + // + // So, here's the plan: + // 1. If the type is mrkdwn, set emoji to nil, regardless of what the user passed in + // 2. Else, set emoji to the value passed in + var emojiPtr *bool + + if elementType == "mrkdwn" { + emojiPtr = nil + } else { + emojiPtr = &emoji + } + return &TextBlockObject{ Type: elementType, Text: text, - Emoji: emoji, + Emoji: emojiPtr, Verbatim: verbatim, } } // BlockType returns the type of the block func (t TextBlockObject) BlockType() MessageBlockType { - if t.Type == "mrkdown" { + if t.Type == "mrkdwn" { return MarkdownType } return PlainTextType @@ -162,7 +202,8 @@ type ConfirmationBlockObject struct { Title *TextBlockObject `json:"title"` Text *TextBlockObject `json:"text"` Confirm *TextBlockObject `json:"confirm"` - Deny *TextBlockObject `json:"deny"` + Deny *TextBlockObject `json:"deny,omitempty"` + Style Style `json:"style,omitempty"` } // validateType enforces block objects for element and block parameters @@ -170,6 +211,12 @@ func (s ConfirmationBlockObject) validateType() MessageObjectType { return motConfirmation } +// WithStyle add styling to confirmation object +func (s *ConfirmationBlockObject) WithStyle(style Style) *ConfirmationBlockObject { + s.Style = style + return s +} + // NewConfirmationBlockObject returns an instance of a new Confirmation Block Object func NewConfirmationBlockObject(title, text, confirm, deny *TextBlockObject) *ConfirmationBlockObject { return &ConfirmationBlockObject{ @@ -184,16 +231,18 @@ func NewConfirmationBlockObject(title, text, confirm, deny *TextBlockObject) *Co // // More Information: https://api.slack.com/reference/messaging/composition-objects#option type OptionBlockObject struct { - Text *TextBlockObject `json:"text"` - Value string `json:"value"` - URL string `json:"url,omitempty"` + Text *TextBlockObject `json:"text"` + Value string `json:"value"` + Description *TextBlockObject `json:"description,omitempty"` + URL string `json:"url,omitempty"` } // NewOptionBlockObject returns an instance of a new Option Block Element -func NewOptionBlockObject(value string, text *TextBlockObject) *OptionBlockObject { +func NewOptionBlockObject(value string, text, description *TextBlockObject) *OptionBlockObject { return &OptionBlockObject{ - Text: text, - Value: value, + Text: text, + Value: value, + Description: description, } } 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 0000000..c6e4174 --- /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 new file mode 100644 index 0000000..3b0ead2 --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_rich_text.go @@ -0,0 +1,582 @@ +package slack + +import ( + "encoding/json" +) + +// RichTextBlock defines a new block of type rich_text. +// More Information: https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less +type RichTextBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements []RichTextElement `json:"elements"` +} + +func (b RichTextBlock) BlockType() MessageBlockType { + return b.Type +} + +// ID returns the ID of the block +func (s RichTextBlock) ID() string { + return s.BlockID +} + +func (e *RichTextBlock) UnmarshalJSON(b []byte) error { + var raw struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id"` + RawElements []json.RawMessage `json:"elements"` + } + if string(b) == "{}" { + return nil + } + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + elems := make([]RichTextElement, 0, len(raw.RawElements)) + for _, r := range raw.RawElements { + var s struct { + Type RichTextElementType `json:"type"` + } + if err := json.Unmarshal(r, &s); err != nil { + return err + } + var elem RichTextElement + switch s.Type { + case RTESection: + elem = &RichTextSection{} + case RTEList: + elem = &RichTextList{} + case RTEQuote: + elem = &RichTextQuote{} + case RTEPreformatted: + elem = &RichTextPreformatted{} + default: + elems = append(elems, &RichTextUnknown{ + Type: s.Type, + Raw: string(r), + }) + continue + } + if err := json.Unmarshal(r, &elem); err != nil { + return err + } + elems = append(elems, elem) + } + *e = RichTextBlock{ + Type: raw.Type, + BlockID: raw.BlockID, + Elements: elems, + } + return nil +} + +// NewRichTextBlock returns a new instance of RichText Block. +func NewRichTextBlock(blockID string, elements ...RichTextElement) *RichTextBlock { + return &RichTextBlock{ + Type: MBTRichText, + BlockID: blockID, + Elements: elements, + } +} + +type RichTextElementType string + +type RichTextElement interface { + RichTextElementType() RichTextElementType +} + +const ( + RTEList RichTextElementType = "rich_text_list" + RTEPreformatted RichTextElementType = "rich_text_preformatted" + RTEQuote RichTextElementType = "rich_text_quote" + RTESection RichTextElementType = "rich_text_section" + RTEUnknown RichTextElementType = "rich_text_unknown" +) + +type RichTextUnknown struct { + Type RichTextElementType + Raw string +} + +func (u RichTextUnknown) RichTextElementType() RichTextElementType { + return u.Type +} + +func (u RichTextUnknown) MarshalJSON() ([]byte, error) { + return []byte(u.Raw), nil +} + +type RichTextListElementType string + +const ( + RTEListOrdered RichTextListElementType = "ordered" + RTEListBullet RichTextListElementType = "bullet" +) + +type RichTextList struct { + Type RichTextElementType `json:"type"` + Elements []RichTextElement `json:"elements"` + Style RichTextListElementType `json:"style"` + Indent int `json:"indent"` + Border int `json:"border"` + Offset int `json:"offset"` +} + +// NewRichTextList returns a new rich text list element. +func NewRichTextList(style RichTextListElementType, indent int, elements ...RichTextElement) *RichTextList { + return &RichTextList{ + Type: RTEList, + Elements: elements, + Style: style, + Indent: indent, + } +} + +// RichTextElementType returns the type of the Element +func (s RichTextList) RichTextElementType() RichTextElementType { + return s.Type +} + +func (e *RichTextList) UnmarshalJSON(b []byte) error { + var raw struct { + RawElements []json.RawMessage `json:"elements"` + Style RichTextListElementType `json:"style"` + Indent int `json:"indent"` + Border int `json:"border"` + Offset int `json:"offset"` + } + if string(b) == "{}" { + return nil + } + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + elems := make([]RichTextElement, 0, len(raw.RawElements)) + for _, r := range raw.RawElements { + var s struct { + Type RichTextElementType `json:"type"` + } + if err := json.Unmarshal(r, &s); err != nil { + return err + } + var elem RichTextElement + switch s.Type { + case RTESection: + elem = &RichTextSection{} + case RTEList: + elem = &RichTextList{} + case RTEQuote: + elem = &RichTextQuote{} + case RTEPreformatted: + elem = &RichTextPreformatted{} + default: + elems = append(elems, &RichTextUnknown{ + Type: s.Type, + Raw: string(r), + }) + continue + } + if err := json.Unmarshal(r, elem); err != nil { + return err + } + elems = append(elems, elem) + } + *e = RichTextList{ + Type: RTEList, + Elements: elems, + Style: raw.Style, + Indent: raw.Indent, + Border: raw.Border, + Offset: raw.Offset, + } + return nil +} + +type RichTextSection struct { + Type RichTextElementType `json:"type"` + Elements []RichTextSectionElement `json:"elements"` +} + +// RichTextElementType returns the type of the Element +func (s RichTextSection) RichTextElementType() RichTextElementType { + return s.Type +} + +func (e *RichTextSection) UnmarshalJSON(b []byte) error { + var raw struct { + RawElements []json.RawMessage `json:"elements"` + Type RichTextElementType `json:"type"` + } + if string(b) == "{}" { + return nil + } + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + elems := make([]RichTextSectionElement, 0, len(raw.RawElements)) + for _, r := range raw.RawElements { + var s struct { + Type RichTextSectionElementType `json:"type"` + } + if err := json.Unmarshal(r, &s); err != nil { + return err + } + var elem RichTextSectionElement + switch s.Type { + case RTSEText: + elem = &RichTextSectionTextElement{} + case RTSEChannel: + elem = &RichTextSectionChannelElement{} + case RTSEUser: + elem = &RichTextSectionUserElement{} + case RTSEEmoji: + elem = &RichTextSectionEmojiElement{} + case RTSELink: + elem = &RichTextSectionLinkElement{} + case RTSETeam: + elem = &RichTextSectionTeamElement{} + case RTSEUserGroup: + elem = &RichTextSectionUserGroupElement{} + case RTSEDate: + elem = &RichTextSectionDateElement{} + case RTSEBroadcast: + elem = &RichTextSectionBroadcastElement{} + case RTSEColor: + elem = &RichTextSectionColorElement{} + default: + elems = append(elems, &RichTextSectionUnknownElement{ + Type: s.Type, + Raw: string(r), + }) + continue + } + if err := json.Unmarshal(r, elem); err != nil { + return err + } + elems = append(elems, elem) + } + if raw.Type == "" { + raw.Type = RTESection + } + *e = RichTextSection{ + Type: raw.Type, + Elements: elems, + } + return nil +} + +// 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, + Elements: elements, + } +} + +type RichTextSectionElementType string + +const ( + RTSEBroadcast RichTextSectionElementType = "broadcast" + RTSEChannel RichTextSectionElementType = "channel" + RTSEColor RichTextSectionElementType = "color" + RTSEDate RichTextSectionElementType = "date" + RTSEEmoji RichTextSectionElementType = "emoji" + RTSELink RichTextSectionElementType = "link" + RTSETeam RichTextSectionElementType = "team" + RTSEText RichTextSectionElementType = "text" + RTSEUser RichTextSectionElementType = "user" + RTSEUserGroup RichTextSectionElementType = "usergroup" + + RTSEUnknown RichTextSectionElementType = "unknown" +) + +type RichTextSectionElement interface { + RichTextSectionElementType() RichTextSectionElementType +} + +type RichTextSectionTextStyle struct { + 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 { + Type RichTextSectionElementType `json:"type"` + Text string `json:"text"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionTextElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionTextElement(text string, style *RichTextSectionTextStyle) *RichTextSectionTextElement { + return &RichTextSectionTextElement{ + Type: RTSEText, + Text: text, + Style: style, + } +} + +type RichTextSectionChannelElement struct { + Type RichTextSectionElementType `json:"type"` + ChannelID string `json:"channel_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionChannelElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionChannelElement(channelID string, style *RichTextSectionTextStyle) *RichTextSectionChannelElement { + return &RichTextSectionChannelElement{ + Type: RTSEChannel, + ChannelID: channelID, + Style: style, + } +} + +type RichTextSectionUserElement struct { + Type RichTextSectionElementType `json:"type"` + UserID string `json:"user_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionUserElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionUserElement(userID string, style *RichTextSectionTextStyle) *RichTextSectionUserElement { + return &RichTextSectionUserElement{ + Type: RTSEUser, + UserID: userID, + Style: style, + } +} + +type RichTextSectionEmojiElement struct { + Type RichTextSectionElementType `json:"type"` + Name string `json:"name"` + SkinTone int `json:"skin_tone,omitempty"` + Unicode string `json:"unicode,omitempty"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionEmojiElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionEmojiElement(name string, skinTone int, style *RichTextSectionTextStyle) *RichTextSectionEmojiElement { + return &RichTextSectionEmojiElement{ + Type: RTSEEmoji, + Name: name, + SkinTone: skinTone, + Style: style, + } +} + +type RichTextSectionLinkElement struct { + Type RichTextSectionElementType `json:"type"` + URL string `json:"url"` + Text string `json:"text,omitempty"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionLinkElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionLinkElement(url, text string, style *RichTextSectionTextStyle) *RichTextSectionLinkElement { + return &RichTextSectionLinkElement{ + Type: RTSELink, + URL: url, + Text: text, + Style: style, + } +} + +type RichTextSectionTeamElement struct { + Type RichTextSectionElementType `json:"type"` + TeamID string `json:"team_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionTeamElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionTeamElement(teamID string, style *RichTextSectionTextStyle) *RichTextSectionTeamElement { + return &RichTextSectionTeamElement{ + Type: RTSETeam, + TeamID: teamID, + Style: style, + } +} + +type RichTextSectionUserGroupElement struct { + Type RichTextSectionElementType `json:"type"` + UsergroupID string `json:"usergroup_id"` + Style *RichTextSectionTextStyle `json:"style,omitempty"` +} + +func (r RichTextSectionUserGroupElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionUserGroupElement(usergroupID string) *RichTextSectionUserGroupElement { + return &RichTextSectionUserGroupElement{ + Type: RTSEUserGroup, + UsergroupID: usergroupID, + } +} + +type RichTextSectionDateElement struct { + Type RichTextSectionElementType `json:"type"` + Timestamp JSONTime `json:"timestamp"` + Format string `json:"format"` + URL *string `json:"url,omitempty"` + Fallback *string `json:"fallback,omitempty"` +} + +func (r RichTextSectionDateElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionDateElement(timestamp int64, format string, url *string, fallback *string) *RichTextSectionDateElement { + return &RichTextSectionDateElement{ + Type: RTSEDate, + Timestamp: JSONTime(timestamp), + Format: format, + URL: url, + Fallback: fallback, + } +} + +type RichTextSectionBroadcastElement struct { + Type RichTextSectionElementType `json:"type"` + Range string `json:"range"` +} + +func (r RichTextSectionBroadcastElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionBroadcastElement(rangeStr string) *RichTextSectionBroadcastElement { + return &RichTextSectionBroadcastElement{ + Type: RTSEBroadcast, + Range: rangeStr, + } +} + +type RichTextSectionColorElement struct { + Type RichTextSectionElementType `json:"type"` + Value string `json:"value"` +} + +func (r RichTextSectionColorElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func NewRichTextSectionColorElement(value string) *RichTextSectionColorElement { + return &RichTextSectionColorElement{ + Type: RTSEColor, + Value: value, + } +} + +type RichTextSectionUnknownElement struct { + Type RichTextSectionElementType `json:"type"` + Raw string +} + +func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSectionElementType { + return r.Type +} + +func (r RichTextSectionUnknownElement) MarshalJSON() ([]byte, error) { + return []byte(r.Raw), nil +} + +// RichTextQuote represents rich_text_quote element type. +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 { + return s.Type +} + +func (s *RichTextQuote) UnmarshalJSON(b []byte) error { + // reusing the RichTextSection struct, as it's the same as RichTextQuote. + var rts RichTextSection + if err := json.Unmarshal(b, &rts); err != nil { + return err + } + 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 { + Type RichTextElementType `json:"type"` + Elements []RichTextSectionElement `json:"elements"` + Border int `json:"border"` + Language string `json:"language,omitempty"` +} + +// RichTextElementType returns the type of the Element +func (s *RichTextPreformatted) RichTextElementType() RichTextElementType { + return s.Type +} + +func (s *RichTextPreformatted) UnmarshalJSON(b []byte) error { + var rts RichTextSection + if err := json.Unmarshal(b, &rts); err != nil { + return err + } + // we define standalone fields because we need to unmarshal the border + // field. We can not directly unmarshal the data into + // RichTextPreformatted because it will cause an infinite loop. We also + // can not define a struct with embedded RichTextSection and Border fields + // because the json package will not unmarshal the data into the + // standalone fields, once it sees UnmarshalJSON method on the embedded + // struct. The drawback is that we have to process the data twice, and + // have to define a standalone struct with the same set of fields as the + // 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"` + Language string `json:"language"` + } + if err := json.Unmarshal(b, &standalone); err != nil { + return err + } + *s = RichTextPreformatted{ + Type: RTEPreformatted, + Elements: rts.Elements, + Border: standalone.Border, + Language: standalone.Language, + } + return nil +} diff --git a/vendor/github.com/slack-go/slack/block_section.go b/vendor/github.com/slack-go/slack/block_section.go index 01ffd5a..0d2352a 100644 --- a/vendor/github.com/slack-go/slack/block_section.go +++ b/vendor/github.com/slack-go/slack/block_section.go @@ -9,6 +9,7 @@ type SectionBlock struct { BlockID string `json:"block_id,omitempty"` Fields []*TextBlockObject `json:"fields,omitempty"` Accessory *Accessory `json:"accessory,omitempty"` + Expand bool `json:"expand,omitempty"` } // BlockType returns the type of the block @@ -16,6 +17,11 @@ func (s SectionBlock) BlockType() MessageBlockType { return s.Type } +// ID returns the ID of the block +func (s SectionBlock) ID() string { + return s.BlockID +} + // SectionBlockOption allows configuration of options for a new section block type SectionBlockOption func(*SectionBlock) @@ -25,6 +31,15 @@ func SectionBlockOptionBlockID(blockID string) SectionBlockOption { } } +// SectionBlockOptionExpand allows long text to be auto-expanded when displaying +// +// @see https://api.slack.com/reference/block-kit/blocks#section +func SectionBlockOptionExpand(shouldExpand bool) SectionBlockOption { + return func(block *SectionBlock) { + block.Expand = shouldExpand + } +} + // NewSectionBlock returns a new instance of a section block to be rendered func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory, options ...SectionBlockOption) *SectionBlock { block := SectionBlock{ 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 0000000..5c7b0f1 --- /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 0000000..2053a82 --- /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 97054c7..71b2a90 100644 --- a/vendor/github.com/slack-go/slack/block_unknown.go +++ b/vendor/github.com/slack-go/slack/block_unknown.go @@ -1,13 +1,36 @@ 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 func (b UnknownBlock) BlockType() MessageBlockType { return b.Type } + +// ID returns the ID of the block +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/block_video.go b/vendor/github.com/slack-go/slack/block_video.go new file mode 100644 index 0000000..4d6739c --- /dev/null +++ b/vendor/github.com/slack-go/slack/block_video.go @@ -0,0 +1,70 @@ +package slack + +// VideoBlock defines data required to display a video as a block element +// +// More Information: https://api.slack.com/reference/block-kit/blocks#video +type VideoBlock struct { + Type MessageBlockType `json:"type"` + VideoURL string `json:"video_url"` + ThumbnailURL string `json:"thumbnail_url"` + AltText string `json:"alt_text"` + Title *TextBlockObject `json:"title"` + BlockID string `json:"block_id,omitempty"` + TitleURL string `json:"title_url,omitempty"` + AuthorName string `json:"author_name,omitempty"` + ProviderName string `json:"provider_name,omitempty"` + ProviderIconURL string `json:"provider_icon_url,omitempty"` + Description *TextBlockObject `json:"description,omitempty"` +} + +// BlockType returns the type of the block +func (s VideoBlock) BlockType() MessageBlockType { + return s.Type +} + +// ID returns the ID of the block +func (s VideoBlock) ID() string { + return s.BlockID +} + +// NewVideoBlock returns an instance of a new Video Block type +func NewVideoBlock(videoURL, thumbnailURL, altText, blockID string, title *TextBlockObject) *VideoBlock { + return &VideoBlock{ + Type: MBTVideo, + VideoURL: videoURL, + ThumbnailURL: thumbnailURL, + AltText: altText, + BlockID: blockID, + Title: title, + } +} + +// WithAuthorName sets the author name for the VideoBlock +func (s *VideoBlock) WithAuthorName(authorName string) *VideoBlock { + s.AuthorName = authorName + return s +} + +// WithTitleURL sets the title URL for the VideoBlock +func (s *VideoBlock) WithTitleURL(titleURL string) *VideoBlock { + s.TitleURL = titleURL + return s +} + +// WithDescription sets the description for the VideoBlock +func (s *VideoBlock) WithDescription(description *TextBlockObject) *VideoBlock { + s.Description = description + return s +} + +// WithProviderIconURL sets the provider icon URL for the VideoBlock +func (s *VideoBlock) WithProviderIconURL(providerIconURL string) *VideoBlock { + s.ProviderIconURL = providerIconURL + return s +} + +// WithProviderName sets the provider name for the VideoBlock +func (s *VideoBlock) WithProviderName(providerName string) *VideoBlock { + s.ProviderName = providerName + return s +} diff --git a/vendor/github.com/slack-go/slack/bookmarks.go b/vendor/github.com/slack-go/slack/bookmarks.go new file mode 100644 index 0000000..1f07e59 --- /dev/null +++ b/vendor/github.com/slack-go/slack/bookmarks.go @@ -0,0 +1,169 @@ +package slack + +import ( + "context" + "net/url" +) + +type Bookmark struct { + ID string `json:"id"` + ChannelID string `json:"channel_id"` + Title string `json:"title"` + Link string `json:"link"` + Emoji string `json:"emoji"` + IconURL string `json:"icon_url"` + Type string `json:"type"` + Created JSONTime `json:"date_created"` + Updated JSONTime `json:"date_updated"` + Rank string `json:"rank"` + + LastUpdatedByUserID string `json:"last_updated_by_user_id"` + LastUpdatedByTeamID string `json:"last_updated_by_team_id"` + + ShortcutID string `json:"shortcut_id"` + EntityID string `json:"entity_id"` + AppID string `json:"app_id"` +} + +type AddBookmarkParameters struct { + Title string // A required title for the bookmark + Type string // A required type for the bookmark + Link string // URL required for type:link + Emoji string // An optional emoji + EntityID string + ParentID string +} + +type EditBookmarkParameters struct { + Title *string // Change the title. Set to "" to clear + Emoji *string // Change the emoji. Set to "" to clear + Link string // Change the link +} + +type addBookmarkResponse struct { + Bookmark Bookmark `json:"bookmark"` + SlackResponse +} + +type editBookmarkResponse struct { + Bookmark Bookmark `json:"bookmark"` + SlackResponse +} + +type listBookmarksResponse struct { + Bookmarks []Bookmark `json:"bookmarks"` + SlackResponse +} + +// AddBookmark adds a bookmark in a channel. +// For more details, see AddBookmarkContext documentation. +func (api *Client) AddBookmark(channelID string, params AddBookmarkParameters) (Bookmark, error) { + return api.AddBookmarkContext(context.Background(), channelID, params) +} + +// AddBookmarkContext adds a bookmark in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/bookmarks.add +func (api *Client) AddBookmarkContext(ctx context.Context, channelID string, params AddBookmarkParameters) (Bookmark, error) { + values := url.Values{ + "channel_id": {channelID}, + "token": {api.token}, + "title": {params.Title}, + "type": {params.Type}, + } + if params.Link != "" { + values.Set("link", params.Link) + } + if params.Emoji != "" { + values.Set("emoji", params.Emoji) + } + if params.EntityID != "" { + values.Set("entity_id", params.EntityID) + } + if params.ParentID != "" { + values.Set("parent_id", params.ParentID) + } + + response := &addBookmarkResponse{} + if err := api.postMethod(ctx, "bookmarks.add", values, response); err != nil { + return Bookmark{}, err + } + + return response.Bookmark, response.Err() +} + +// RemoveBookmark removes a bookmark from a channel. +// For more details, see RemoveBookmarkContext documentation. +func (api *Client) RemoveBookmark(channelID, bookmarkID string) error { + return api.RemoveBookmarkContext(context.Background(), channelID, bookmarkID) +} + +// RemoveBookmarkContext removes a bookmark from a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/bookmarks.remove +func (api *Client) RemoveBookmarkContext(ctx context.Context, channelID, bookmarkID string) error { + values := url.Values{ + "channel_id": {channelID}, + "token": {api.token}, + "bookmark_id": {bookmarkID}, + } + + response := &SlackResponse{} + if err := api.postMethod(ctx, "bookmarks.remove", values, response); err != nil { + return err + } + + return response.Err() +} + +// ListBookmarks returns all bookmarks for a channel. +// For more details, see ListBookmarksContext documentation. +func (api *Client) ListBookmarks(channelID string) ([]Bookmark, error) { + return api.ListBookmarksContext(context.Background(), channelID) +} + +// ListBookmarksContext returns all bookmarks for a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/bookmarks.edit +func (api *Client) ListBookmarksContext(ctx context.Context, channelID string) ([]Bookmark, error) { + values := url.Values{ + "channel_id": {channelID}, + "token": {api.token}, + } + + response := &listBookmarksResponse{} + err := api.postMethod(ctx, "bookmarks.list", values, response) + if err != nil { + return nil, err + } + return response.Bookmarks, response.Err() +} + +// EditBookmark edits a bookmark in a channel. +// For more details, see EditBookmarkContext documentation. +func (api *Client) EditBookmark(channelID, bookmarkID string, params EditBookmarkParameters) (Bookmark, error) { + return api.EditBookmarkContext(context.Background(), channelID, bookmarkID, params) +} + +// EditBookmarkContext edits a bookmark in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/bookmarks.edit +func (api *Client) EditBookmarkContext(ctx context.Context, channelID, bookmarkID string, params EditBookmarkParameters) (Bookmark, error) { + values := url.Values{ + "channel_id": {channelID}, + "token": {api.token}, + "bookmark_id": {bookmarkID}, + } + if params.Link != "" { + values.Set("link", params.Link) + } + if params.Emoji != nil { + values.Set("emoji", *params.Emoji) + } + if params.Title != nil { + values.Set("title", *params.Title) + } + + response := &editBookmarkResponse{} + if err := api.postMethod(ctx, "bookmarks.edit", values, response); err != nil { + return Bookmark{}, err + } + + return response.Bookmark, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/bots.go b/vendor/github.com/slack-go/slack/bots.go index da21ba0..1ab9469 100644 --- a/vendor/github.com/slack-go/slack/bots.go +++ b/vendor/github.com/slack-go/slack/bots.go @@ -35,19 +35,30 @@ func (api *Client) botRequest(ctx context.Context, path string, values url.Value return response, nil } -// GetBotInfo will retrieve the complete bot information -func (api *Client) GetBotInfo(bot string) (*Bot, error) { - return api.GetBotInfoContext(context.Background(), bot) +type GetBotInfoParameters struct { + Bot string + TeamID string } -// GetBotInfoContext will retrieve the complete bot information using a custom context -func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { +// GetBotInfo will retrieve the complete bot information. +// For more details, see GetBotInfoContext documentation. +func (api *Client) GetBotInfo(parameters GetBotInfoParameters) (*Bot, error) { + return api.GetBotInfoContext(context.Background(), parameters) +} + +// GetBotInfoContext will retrieve the complete bot information using a custom context. +// Slack API docs: https://api.slack.com/methods/bots.info +func (api *Client) GetBotInfoContext(ctx context.Context, parameters GetBotInfoParameters) (*Bot, error) { values := url.Values{ "token": {api.token}, } - if bot != "" { - values.Add("bot", bot) + if parameters.Bot != "" { + values.Add("bot", parameters.Bot) + } + + if parameters.TeamID != "" { + values.Add("team_id", parameters.TeamID) } response, err := api.botRequest(ctx, "bots.info", values) diff --git a/vendor/github.com/slack-go/slack/calls.go b/vendor/github.com/slack-go/slack/calls.go new file mode 100644 index 0000000..2d6e91f --- /dev/null +++ b/vendor/github.com/slack-go/slack/calls.go @@ -0,0 +1,216 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" + "strconv" + "time" +) + +type Call struct { + ID string `json:"id"` + Title string `json:"title"` + DateStart JSONTime `json:"date_start"` + DateEnd JSONTime `json:"date_end"` + ExternalUniqueID string `json:"external_unique_id"` + JoinURL string `json:"join_url"` + DesktopAppJoinURL string `json:"desktop_app_join_url"` + ExternalDisplayID string `json:"external_display_id"` + Participants []CallParticipant `json:"users"` + Channels []string `json:"channels"` +} + +// CallParticipant is a thin user representation which has a SlackID, ExternalID, or both. +// +// See: https://api.slack.com/apis/calls#users +type CallParticipant struct { + SlackID string `json:"slack_id,omitempty"` + ExternalID string `json:"external_id,omitempty"` + DisplayName string `json:"display_name,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` +} + +// Valid checks if the CallUser has a is valid with a SlackID or ExternalID or both. +func (u CallParticipant) Valid() bool { + return u.SlackID != "" || u.ExternalID != "" +} + +type AddCallParameters struct { + JoinURL string // Required + ExternalUniqueID string // Required + CreatedBy string // Required if using a bot token + Title string + DesktopAppJoinURL string + ExternalDisplayID string + DateStart JSONTime + Participants []CallParticipant +} + +type UpdateCallParameters struct { + Title string + DesktopAppJoinURL string + JoinURL string +} + +type EndCallParameters struct { + // Duration is the duration of the call in seconds. Omitted if 0. + Duration time.Duration +} + +type callResponse struct { + Call Call `json:"call"` + SlackResponse +} + +// AddCall adds a new Call to the Slack API. +func (api *Client) AddCall(params AddCallParameters) (Call, error) { + return api.AddCallContext(context.Background(), params) +} + +// AddCallContext adds a new Call to the Slack API. +func (api *Client) AddCallContext(ctx context.Context, params AddCallParameters) (Call, error) { + values := url.Values{ + "token": {api.token}, + "join_url": {params.JoinURL}, + "external_unique_id": {params.ExternalUniqueID}, + } + if params.CreatedBy != "" { + values.Set("created_by", params.CreatedBy) + } + if params.DateStart != 0 { + values.Set("date_start", strconv.FormatInt(int64(params.DateStart), 10)) + } + if params.DesktopAppJoinURL != "" { + values.Set("desktop_app_join_url", params.DesktopAppJoinURL) + } + if params.ExternalDisplayID != "" { + values.Set("external_display_id", params.ExternalDisplayID) + } + if params.Title != "" { + values.Set("title", params.Title) + } + if len(params.Participants) > 0 { + data, err := json.Marshal(params.Participants) + if err != nil { + return Call{}, err + } + values.Set("users", string(data)) + } + + response := &callResponse{} + if err := api.postMethod(ctx, "calls.add", values, response); err != nil { + return Call{}, err + } + + return response.Call, response.Err() +} + +// GetCallInfo returns information about a Call. +func (api *Client) GetCall(callID string) (Call, error) { + return api.GetCallContext(context.Background(), callID) +} + +// GetCallInfoContext returns information about a Call. +func (api *Client) GetCallContext(ctx context.Context, callID string) (Call, error) { + values := url.Values{ + "token": {api.token}, + "id": {callID}, + } + + response := &callResponse{} + if err := api.postMethod(ctx, "calls.info", values, response); err != nil { + return Call{}, err + } + return response.Call, response.Err() +} + +func (api *Client) UpdateCall(callID string, params UpdateCallParameters) (Call, error) { + return api.UpdateCallContext(context.Background(), callID, params) +} + +// UpdateCallContext updates a Call with the given parameters. +func (api *Client) UpdateCallContext(ctx context.Context, callID string, params UpdateCallParameters) (Call, error) { + values := url.Values{ + "token": {api.token}, + "id": {callID}, + } + + if params.DesktopAppJoinURL != "" { + values.Set("desktop_app_join_url", params.DesktopAppJoinURL) + } + if params.JoinURL != "" { + values.Set("join_url", params.JoinURL) + } + if params.Title != "" { + values.Set("title", params.Title) + } + + response := &callResponse{} + if err := api.postMethod(ctx, "calls.update", values, response); err != nil { + return Call{}, err + } + return response.Call, response.Err() +} + +// EndCall ends a Call. +func (api *Client) EndCall(callID string, params EndCallParameters) error { + return api.EndCallContext(context.Background(), callID, params) +} + +// EndCallContext ends a Call. +func (api *Client) EndCallContext(ctx context.Context, callID string, params EndCallParameters) error { + values := url.Values{ + "token": {api.token}, + "id": {callID}, + } + + if params.Duration != 0 { + values.Set("duration", strconv.FormatInt(int64(params.Duration.Seconds()), 10)) + } + + response := &SlackResponse{} + if err := api.postMethod(ctx, "calls.end", values, response); err != nil { + return err + } + return response.Err() +} + +// CallAddParticipants adds users to a Call. +func (api *Client) CallAddParticipants(callID string, participants []CallParticipant) error { + return api.CallAddParticipantsContext(context.Background(), callID, participants) +} + +// CallAddParticipantsContext adds users to a Call. +func (api *Client) CallAddParticipantsContext(ctx context.Context, callID string, participants []CallParticipant) error { + return api.setCallParticipants(ctx, "calls.participants.add", callID, participants) +} + +// CallRemoveParticipants removes users from a Call. +func (api *Client) CallRemoveParticipants(callID string, participants []CallParticipant) error { + return api.CallRemoveParticipantsContext(context.Background(), callID, participants) +} + +// CallRemoveParticipantsContext removes users from a Call. +func (api *Client) CallRemoveParticipantsContext(ctx context.Context, callID string, participants []CallParticipant) error { + return api.setCallParticipants(ctx, "calls.participants.remove", callID, participants) +} + +func (api *Client) setCallParticipants(ctx context.Context, method, callID string, participants []CallParticipant) error { + values := url.Values{ + "token": {api.token}, + "id": {callID}, + } + + data, err := json.Marshal(participants) + if err != nil { + return err + } + values.Set("users", string(data)) + + response := &SlackResponse{} + if err := api.postMethod(ctx, method, values, response); err != nil { + return err + } + return response.Err() +} diff --git a/vendor/github.com/slack-go/slack/canvas.go b/vendor/github.com/slack-go/slack/canvas.go new file mode 100644 index 0000000..5225afa --- /dev/null +++ b/vendor/github.com/slack-go/slack/canvas.go @@ -0,0 +1,264 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" +) + +type CanvasDetails struct { + CanvasID string `json:"canvas_id"` +} + +type DocumentContent struct { + Type string `json:"type"` + Markdown string `json:"markdown,omitempty"` +} + +type CanvasChange struct { + Operation string `json:"operation"` + SectionID string `json:"section_id,omitempty"` + DocumentContent DocumentContent `json:"document_content"` +} + +type EditCanvasParams struct { + CanvasID string `json:"canvas_id"` + Changes []CanvasChange `json:"changes"` +} + +type SetCanvasAccessParams struct { + CanvasID string `json:"canvas_id"` + AccessLevel string `json:"access_level"` + ChannelIDs []string `json:"channel_ids,omitempty"` + UserIDs []string `json:"user_ids,omitempty"` +} + +type DeleteCanvasAccessParams struct { + CanvasID string `json:"canvas_id"` + ChannelIDs []string `json:"channel_ids,omitempty"` + UserIDs []string `json:"user_ids,omitempty"` +} + +type LookupCanvasSectionsCriteria struct { + SectionTypes []string `json:"section_types,omitempty"` + ContainsText string `json:"contains_text,omitempty"` +} + +type LookupCanvasSectionsParams struct { + CanvasID string `json:"canvas_id"` + Criteria LookupCanvasSectionsCriteria `json:"criteria"` +} + +type CanvasSection struct { + ID string `json:"id"` +} + +type LookupCanvasSectionsResponse struct { + SlackResponse + Sections []CanvasSection `json:"sections"` +} + +// CreateCanvas creates a new canvas. +// For more details, see CreateCanvasContext documentation. +func (api *Client) CreateCanvas(title string, documentContent DocumentContent) (string, error) { + return api.CreateCanvasContext(context.Background(), title, documentContent) +} + +// CreateCanvasContext creates a new canvas with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.create +func (api *Client) CreateCanvasContext(ctx context.Context, title string, documentContent DocumentContent) (string, error) { + values := url.Values{ + "token": {api.token}, + } + if title != "" { + values.Add("title", title) + } + if documentContent.Type != "" { + documentContentJSON, err := json.Marshal(documentContent) + if err != nil { + return "", err + } + values.Add("document_content", string(documentContentJSON)) + } + + response := struct { + SlackResponse + CanvasID string `json:"canvas_id"` + }{} + + err := api.postMethod(ctx, "canvases.create", values, &response) + if err != nil { + return "", err + } + + return response.CanvasID, response.Err() +} + +// DeleteCanvas deletes an existing canvas. +// For more details, see DeleteCanvasContext documentation. +func (api *Client) DeleteCanvas(canvasID string) error { + return api.DeleteCanvasContext(context.Background(), canvasID) +} + +// DeleteCanvasContext deletes an existing canvas with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.delete +func (api *Client) DeleteCanvasContext(ctx context.Context, canvasID string) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {canvasID}, + } + + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "canvases.delete", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// EditCanvas edits an existing canvas. +// For more details, see EditCanvasContext documentation. +func (api *Client) EditCanvas(params EditCanvasParams) error { + return api.EditCanvasContext(context.Background(), params) +} + +// EditCanvasContext edits an existing canvas with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.edit +func (api *Client) EditCanvasContext(ctx context.Context, params EditCanvasParams) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + } + + changesJSON, err := json.Marshal(params.Changes) + if err != nil { + return err + } + values.Add("changes", string(changesJSON)) + + response := struct { + SlackResponse + }{} + + err = api.postMethod(ctx, "canvases.edit", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// SetCanvasAccess sets the access level to a canvas for specified entities. +// For more details, see SetCanvasAccessContext documentation. +func (api *Client) SetCanvasAccess(params SetCanvasAccessParams) error { + return api.SetCanvasAccessContext(context.Background(), params) +} + +// SetCanvasAccessContext sets the access level to a canvas for specified entities with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.access.set +func (api *Client) SetCanvasAccessContext(ctx context.Context, params SetCanvasAccessParams) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + "access_level": {params.AccessLevel}, + } + if len(params.ChannelIDs) > 0 { + channelIDsJSON, err := json.Marshal(params.ChannelIDs) + if err != nil { + return err + } + values.Add("channel_ids", string(channelIDsJSON)) + } + if len(params.UserIDs) > 0 { + userIDsJSON, err := json.Marshal(params.UserIDs) + if err != nil { + return err + } + values.Add("user_ids", string(userIDsJSON)) + } + + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "canvases.access.set", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// DeleteCanvasAccess removes access to a canvas for specified entities. +// For more details, see DeleteCanvasAccessContext documentation. +func (api *Client) DeleteCanvasAccess(params DeleteCanvasAccessParams) error { + return api.DeleteCanvasAccessContext(context.Background(), params) +} + +// DeleteCanvasAccessContext removes access to a canvas for specified entities with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.access.delete +func (api *Client) DeleteCanvasAccessContext(ctx context.Context, params DeleteCanvasAccessParams) error { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + } + if len(params.ChannelIDs) > 0 { + channelIDsJSON, err := json.Marshal(params.ChannelIDs) + if err != nil { + return err + } + values.Add("channel_ids", string(channelIDsJSON)) + } + if len(params.UserIDs) > 0 { + userIDsJSON, err := json.Marshal(params.UserIDs) + if err != nil { + return err + } + values.Add("user_ids", string(userIDsJSON)) + } + + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "canvases.access.delete", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// LookupCanvasSections finds sections matching the provided criteria. +// For more details, see LookupCanvasSectionsContext documentation. +func (api *Client) LookupCanvasSections(params LookupCanvasSectionsParams) ([]CanvasSection, error) { + return api.LookupCanvasSectionsContext(context.Background(), params) +} + +// LookupCanvasSectionsContext finds sections matching the provided criteria with a custom context. +// Slack API docs: https://api.slack.com/methods/canvases.sections.lookup +func (api *Client) LookupCanvasSectionsContext(ctx context.Context, params LookupCanvasSectionsParams) ([]CanvasSection, error) { + values := url.Values{ + "token": {api.token}, + "canvas_id": {params.CanvasID}, + } + + criteriaJSON, err := json.Marshal(params.Criteria) + if err != nil { + return nil, err + } + values.Add("criteria", string(criteriaJSON)) + + response := LookupCanvasSectionsResponse{} + + err = api.postMethod(ctx, "canvases.sections.lookup", values, &response) + if err != nil { + return nil, err + } + + return response.Sections, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/channels.go b/vendor/github.com/slack-go/slack/channels.go index a90d238..d01ce82 100644 --- a/vendor/github.com/slack-go/slack/channels.go +++ b/vendor/github.com/slack-go/slack/channels.go @@ -3,8 +3,6 @@ package slack import ( "context" "net/url" - "strconv" - "time" ) type channelResponseFull struct { @@ -21,461 +19,19 @@ type channelResponseFull struct { // Channel contains information about the channel type Channel struct { GroupConversation - IsChannel bool `json:"is_channel"` - IsGeneral bool `json:"is_general"` - IsMember bool `json:"is_member"` - Locale string `json:"locale"` + IsChannel bool `json:"is_channel"` + IsGeneral bool `json:"is_general"` + IsMember bool `json:"is_member"` + Locale string `json:"locale"` + Properties *Properties `json:"properties"` } 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 } return response, response.Err() } - -// GetChannelsOption option provided when getting channels. -type GetChannelsOption func(*ChannelPagination) error - -// GetChannelsOptionExcludeMembers excludes the members collection from each channel. -func GetChannelsOptionExcludeMembers() GetChannelsOption { - return func(p *ChannelPagination) error { - p.excludeMembers = true - return nil - } -} - -// GetChannelsOptionExcludeArchived excludes archived channels from results. -func GetChannelsOptionExcludeArchived() GetChannelsOption { - return func(p *ChannelPagination) error { - p.excludeArchived = true - return nil - } -} - -// ArchiveChannel archives the given channel -// see https://api.slack.com/methods/channels.archive -func (api *Client) ArchiveChannel(channelID string) error { - return api.ArchiveChannelContext(context.Background(), channelID) -} - -// ArchiveChannelContext archives the given channel with a custom context -// see https://api.slack.com/methods/channels.archive -func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - - _, err = api.channelRequest(ctx, "channels.archive", values) - return err -} - -// UnarchiveChannel unarchives the given channel -// see https://api.slack.com/methods/channels.unarchive -func (api *Client) UnarchiveChannel(channelID string) error { - return api.UnarchiveChannelContext(context.Background(), channelID) -} - -// UnarchiveChannelContext unarchives the given channel with a custom context -// see https://api.slack.com/methods/channels.unarchive -func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - - _, err = api.channelRequest(ctx, "channels.unarchive", values) - return err -} - -// CreateChannel creates a channel with the given name and returns a *Channel -// see https://api.slack.com/methods/channels.create -func (api *Client) CreateChannel(channelName string) (*Channel, error) { - return api.CreateChannelContext(context.Background(), channelName) -} - -// CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context -// see https://api.slack.com/methods/channels.create -func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "name": {channelName}, - } - - response, err := api.channelRequest(ctx, "channels.create", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// GetChannelHistory retrieves the channel history -// see https://api.slack.com/methods/channels.history -func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) (*History, error) { - return api.GetChannelHistoryContext(context.Background(), channelID, params) -} - -// GetChannelHistoryContext retrieves the channel history with a custom context -// see https://api.slack.com/methods/channels.history -func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - if params.Latest != DEFAULT_HISTORY_LATEST { - values.Add("latest", params.Latest) - } - if params.Oldest != DEFAULT_HISTORY_OLDEST { - values.Add("oldest", params.Oldest) - } - if params.Count != DEFAULT_HISTORY_COUNT { - values.Add("count", strconv.Itoa(params.Count)) - } - if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { - if params.Inclusive { - values.Add("inclusive", "1") - } else { - values.Add("inclusive", "0") - } - } - - if params.Unreads != DEFAULT_HISTORY_UNREADS { - if params.Unreads { - values.Add("unreads", "1") - } else { - values.Add("unreads", "0") - } - } - - response, err := api.channelRequest(ctx, "channels.history", values) - if err != nil { - return nil, err - } - return &response.History, nil -} - -// GetChannelInfo retrieves the given channel -// see https://api.slack.com/methods/channels.info -func (api *Client) GetChannelInfo(channelID string) (*Channel, error) { - return api.GetChannelInfoContext(context.Background(), channelID) -} - -// GetChannelInfoContext retrieves the given channel with a custom context -// see https://api.slack.com/methods/channels.info -func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "include_locale": {strconv.FormatBool(true)}, - } - - response, err := api.channelRequest(ctx, "channels.info", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// InviteUserToChannel invites a user to a given channel and returns a *Channel -// see https://api.slack.com/methods/channels.invite -func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) { - return api.InviteUserToChannelContext(context.Background(), channelID, user) -} - -// InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context -// see https://api.slack.com/methods/channels.invite -func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "user": {user}, - } - - response, err := api.channelRequest(ctx, "channels.invite", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// JoinChannel joins the currently authenticated user to a channel -// see https://api.slack.com/methods/channels.join -func (api *Client) JoinChannel(channelName string) (*Channel, error) { - return api.JoinChannelContext(context.Background(), channelName) -} - -// JoinChannelContext joins the currently authenticated user to a channel with a custom context -// see https://api.slack.com/methods/channels.join -func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "name": {channelName}, - } - - response, err := api.channelRequest(ctx, "channels.join", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// LeaveChannel makes the authenticated user leave the given channel -// see https://api.slack.com/methods/channels.leave -func (api *Client) LeaveChannel(channelID string) (bool, error) { - return api.LeaveChannelContext(context.Background(), channelID) -} - -// LeaveChannelContext makes the authenticated user leave the given channel with a custom context -// see https://api.slack.com/methods/channels.leave -func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - } - - response, err := api.channelRequest(ctx, "channels.leave", values) - if err != nil { - return false, err - } - - return response.NotInChannel, nil -} - -// KickUserFromChannel kicks a user from a given channel -// see https://api.slack.com/methods/channels.kick -func (api *Client) KickUserFromChannel(channelID, user string) error { - return api.KickUserFromChannelContext(context.Background(), channelID, user) -} - -// KickUserFromChannelContext kicks a user from a given channel with a custom context -// see https://api.slack.com/methods/channels.kick -func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "user": {user}, - } - - _, err = api.channelRequest(ctx, "channels.kick", values) - return err -} - -func newChannelPagination(c *Client, options ...GetChannelsOption) (cp ChannelPagination) { - cp = ChannelPagination{ - c: c, - limit: 200, // per slack api documentation. - } - - for _, opt := range options { - opt(&cp) - } - - return cp -} - -// ChannelPagination allows for paginating over the channels -type ChannelPagination struct { - Channels []Channel - limit int - excludeArchived bool - excludeMembers bool - previousResp *ResponseMetadata - c *Client -} - -// Done checks if the pagination has completed -func (ChannelPagination) Done(err error) bool { - return err == errPaginationComplete -} - -// Failure checks if pagination failed. -func (t ChannelPagination) Failure(err error) error { - if t.Done(err) { - return nil - } - - return err -} - -func (t ChannelPagination) Next(ctx context.Context) (_ ChannelPagination, err error) { - var ( - resp *channelResponseFull - ) - - if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") { - return t, errPaginationComplete - } - - t.previousResp = t.previousResp.initialize() - - values := url.Values{ - "limit": {strconv.Itoa(t.limit)}, - "exclude_archived": {strconv.FormatBool(t.excludeArchived)}, - "exclude_members": {strconv.FormatBool(t.excludeMembers)}, - "token": {t.c.token}, - "cursor": {t.previousResp.Cursor}, - } - - if resp, err = t.c.channelRequest(ctx, "channels.list", values); err != nil { - return t, err - } - - t.c.Debugf("GetChannelsContext: got %d channels; metadata %v", len(resp.Channels), resp.Metadata) - t.Channels = resp.Channels - t.previousResp = &resp.Metadata - - return t, nil -} - -// GetChannelsPaginated fetches channels in a paginated fashion, see GetChannelsContext for usage. -func (api *Client) GetChannelsPaginated(options ...GetChannelsOption) ChannelPagination { - return newChannelPagination(api, options...) -} - -// GetChannels retrieves all the channels -// see https://api.slack.com/methods/channels.list -func (api *Client) GetChannels(excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) { - return api.GetChannelsContext(context.Background(), excludeArchived, options...) -} - -// GetChannelsContext retrieves all the channels with a custom context -// see https://api.slack.com/methods/channels.list -func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) (results []Channel, err error) { - if excludeArchived { - options = append(options, GetChannelsOptionExcludeArchived()) - } - - p := api.GetChannelsPaginated(options...) - for err == nil { - p, err = p.Next(ctx) - if err == nil { - results = append(results, p.Channels...) - } 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) -} - -// SetChannelReadMark sets the read mark of a given channel to a specific point -// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a -// timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls -// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A -// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. -// see https://api.slack.com/methods/channels.mark -func (api *Client) SetChannelReadMark(channelID, ts string) error { - return api.SetChannelReadMarkContext(context.Background(), channelID, ts) -} - -// SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context -// For more details see SetChannelReadMark documentation -// see https://api.slack.com/methods/channels.mark -func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "ts": {ts}, - } - - _, err = api.channelRequest(ctx, "channels.mark", values) - return err -} - -// RenameChannel renames a given channel -// see https://api.slack.com/methods/channels.rename -func (api *Client) RenameChannel(channelID, name string) (*Channel, error) { - return api.RenameChannelContext(context.Background(), channelID, name) -} - -// RenameChannelContext renames a given channel with a custom context -// see https://api.slack.com/methods/channels.rename -func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "name": {name}, - } - - // XXX: the created entry in this call returns a string instead of a number - // so I may have to do some workaround to solve it. - response, err := api.channelRequest(ctx, "channels.rename", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set -// see https://api.slack.com/methods/channels.setPurpose -func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) { - return api.SetChannelPurposeContext(context.Background(), channelID, purpose) -} - -// SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context -// see https://api.slack.com/methods/channels.setPurpose -func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "purpose": {purpose}, - } - - response, err := api.channelRequest(ctx, "channels.setPurpose", values) - if err != nil { - return "", err - } - return response.Purpose, nil -} - -// SetChannelTopic sets the channel topic and returns the topic that was successfully set -// see https://api.slack.com/methods/channels.setTopic -func (api *Client) SetChannelTopic(channelID, topic string) (string, error) { - return api.SetChannelTopicContext(context.Background(), channelID, topic) -} - -// SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context -// see https://api.slack.com/methods/channels.setTopic -func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "topic": {topic}, - } - - response, err := api.channelRequest(ctx, "channels.setTopic", values) - if err != nil { - return "", err - } - return response.Topic, nil -} - -// GetChannelReplies gets an entire thread (a message plus all the messages in reply to it). -// see https://api.slack.com/methods/channels.replies -func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, error) { - return api.GetChannelRepliesContext(context.Background(), channelID, thread_ts) -} - -// GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context -// see https://api.slack.com/methods/channels.replies -func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "thread_ts": {thread_ts}, - } - response, err := api.channelRequest(ctx, "channels.replies", values) - if err != nil { - return nil, err - } - return response.History.Messages, nil -} diff --git a/vendor/github.com/slack-go/slack/chat.go b/vendor/github.com/slack-go/slack/chat.go index c5b524c..a58b4e8 100644 --- a/vendor/github.com/slack-go/slack/chat.go +++ b/vendor/github.com/slack-go/slack/chat.go @@ -1,10 +1,13 @@ package slack import ( + "bytes" "context" "encoding/json" + "io" "net/http" "net/url" + "regexp" "strconv" "github.com/slack-go/slack/slackutilsx" @@ -27,14 +30,14 @@ const ( type chatResponseFull struct { Channel string `json:"channel"` - Timestamp string `json:"ts"` //Regular message timestamp - MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp - ScheduledMessageID string `json:"scheduled_message_id,omitempty"` //Scheduled message id + Timestamp string `json:"ts"` // Regular message timestamp + MessageTimeStamp string `json:"message_ts"` // Ephemeral message timestamp + ScheduledMessageID string `json:"scheduled_message_id,omitempty"` // Scheduled message id Text string `json:"text"` SlackResponse } -// getMessageTimestamp will inspect the `chatResponseFull` to ruturn a timestamp value +// getMessageTimestamp will inspect the `chatResponseFull` to return a timestamp value // in `chat.postMessage` its under `ts` // in `chat.postEphemeral` its under `message_ts` func (c chatResponseFull) getMessageTimestamp() string { @@ -62,6 +65,12 @@ type PostMessageParameters struct { // chat.postEphemeral support Channel string `json:"channel"` User string `json:"user"` + + // chat metadata support + MetaData SlackMetadata `json:"metadata"` + + // file_ids support + FileIDs []string `json:"file_ids,omitempty"` } // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set @@ -82,17 +91,14 @@ func NewPostMessageParameters() PostMessageParameters { } } -// DeleteMessage deletes a message in a channel +// DeleteMessage deletes a message in a channel. +// For more details, see DeleteMessageContext documentation. func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), - channel, - MsgOptionDelete(messageTimestamp), - ) - return respChannel, respTimestamp, err + return api.DeleteMessageContext(context.Background(), channel, messageTimestamp) } -// DeleteMessageContext deletes a message in a channel with a custom context +// DeleteMessageContext deletes a message in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.delete func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( ctx, @@ -105,31 +111,33 @@ func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTim // ScheduleMessage sends a message to a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +// For more details, see ScheduleMessageContext documentation. func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOption) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), + return api.ScheduleMessageContext(context.Background(), channelID, postAt, options...) +} + +// 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( + ctx, channelID, MsgOptionSchedule(postAt), MsgOptionCompose(options...), ) - return respChannel, respTimestamp, err + return respChannel, scheduledMessageID, err } // PostMessage sends a message to a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +// For more details, see PostMessageContext documentation. func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) { - respChannel, respTimestamp, _, err := api.SendMessageContext( - context.Background(), - channelID, - MsgOptionPost(), - MsgOptionCompose(options...), - ) - return respChannel, respTimestamp, err + return api.PostMessageContext(context.Background(), channelID, options...) } -// PostMessageContext sends a message to a channel with a custom context -// For more details, see PostMessage documentation. +// PostMessageContext sends a message to a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.postMessage func (api *Client) PostMessageContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( ctx, @@ -143,17 +151,13 @@ func (api *Client) PostMessageContext(ctx context.Context, channelID string, opt // PostEphemeral sends an ephemeral message to a user in a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +// For more details, see PostEphemeralContext documentation. func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { - return api.PostEphemeralContext( - context.Background(), - channelID, - userID, - options..., - ) + return api.PostEphemeralContext(context.Background(), channelID, userID, options...) } -// PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context -// For more details, see PostEphemeral documentation +// PostEphemeralContext sends an ephemeral message to a user in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.postEphemeral func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) { _, timestamp, _, err = api.SendMessageContext( ctx, @@ -164,17 +168,14 @@ func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID s return timestamp, err } -// UpdateMessage updates a message in a channel +// UpdateMessage updates a message in a channel. +// For more details, see UpdateMessageContext documentation. func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) { - return api.SendMessageContext( - context.Background(), - channelID, - MsgOptionUpdate(timestamp), - MsgOptionCompose(options...), - ) + return api.UpdateMessageContext(context.Background(), channelID, timestamp, options...) } -// UpdateMessageContext updates a message in a channel +// UpdateMessageContext updates a message in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.update func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, string, error) { return api.SendMessageContext( ctx, @@ -184,33 +185,108 @@ func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestam ) } -// UnfurlMessage unfurls a message in a channel +// UnfurlMessage unfurls a message in a channel. +// For more details, see UnfurlMessageContext documentation. func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { - return api.SendMessageContext(context.Background(), channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...)) + return api.UnfurlMessageContext(context.Background(), channelID, timestamp, unfurls, options...) +} + +// UnfurlMessageContext unfurls a message in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.unfurl +func (api *Client) UnfurlMessageContext(ctx context.Context, channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { + return api.SendMessageContext(ctx, channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...)) +} + +// UnfurlMessageWithAuthURL sends an unfurl request containing an authentication URL. +// For more details, see UnfurlMessageWithAuthURLContext documentation. +func (api *Client) UnfurlMessageWithAuthURL(channelID, timestamp string, userAuthURL string, options ...MsgOption) (string, string, string, error) { + return api.UnfurlMessageWithAuthURLContext(context.Background(), channelID, timestamp, userAuthURL, options...) +} + +// UnfurlMessageWithAuthURLContext sends an unfurl request containing an authentication URL with a custom context. +// For more details see: https://api.slack.com/reference/messaging/link-unfurling#authenticated_unfurls +func (api *Client) UnfurlMessageWithAuthURLContext(ctx context.Context, channelID, timestamp string, userAuthURL string, options ...MsgOption) (string, string, string, error) { + 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) { return api.SendMessageContext(context.Background(), channel, options...) } // SendMessageContext more flexible method for configuring messages with a custom context. -func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) { +// 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) { var ( req *http.Request parser func(*chatResponseFull) responseParser response chatResponseFull ) - if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil { + if req, parser, err = buildSender(api.endpoint, options...).BuildRequestContext(ctx, api.token, channelID); err != nil { return "", "", "", err } - if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil { + if api.Debug() { + reqBody, err := io.ReadAll(req.Body) + if err != nil { + return "", "", "", err + } + req.Body = io.NopCloser(bytes.NewBuffer(reqBody)) + api.Debugf("Sending request: %s", redactToken(reqBody)) + } + + if _, err = doPost(api.httpclient, req, parser(&response), api); err != nil { return "", "", "", err } - return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() + if response.ScheduledMessageID != "" { + return response.Channel, response.ScheduledMessageID, response.Text, response.Err() + } else { + return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() + } +} + +func redactToken(b []byte) []byte { + // See https://api.slack.com/authentication/token-types + // and https://api.slack.com/authentication/rotation + re, err := regexp.Compile(`(token=x[a-z.]+)-[0-9A-Za-z-]+`) + if err != nil { + // The regular expression above should never result in errors, + // but just in case, do no harm. + return b + } + // Keep "token=" and the first element of the token, which identifies its type + // (this could be useful for debugging, e.g. when using a wrong token). + return re.ReplaceAll(b, []byte("$1-REDACTED")) } // UnsafeApplyMsgOptions utility function for debugging/testing chat requests. @@ -258,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 { @@ -267,6 +346,7 @@ type sendConfig struct { endpoint string values url.Values attachments []Attachment + metadata SlackMetadata blocks Blocks responseType string replaceOriginal bool @@ -274,6 +354,10 @@ type sendConfig struct { } func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { + return t.BuildRequestContext(context.Background(), token, channelID) +} + +func (t sendConfig) BuildRequestContext(ctx context.Context, token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil { return nil, nil, err } @@ -284,23 +368,46 @@ func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ endpoint: t.endpoint, values: t.values, attachments: t.attachments, + metadata: t.metadata, blocks: t.blocks, responseType: t.responseType, replaceOriginal: t.replaceOriginal, deleteOriginal: t.deleteOriginal, - }.BuildRequest() + }.BuildRequestContext(ctx) default: - return formSender{endpoint: t.endpoint, values: t.values}.BuildRequest() + 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) { - req, err := formReq(t.endpoint, t.values) + return t.BuildRequestContext(context.Background()) +} + +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) }, err @@ -310,6 +417,7 @@ type responseURLSender struct { endpoint string values url.Values attachments []Attachment + metadata SlackMetadata blocks Blocks responseType string replaceOriginal bool @@ -317,11 +425,17 @@ type responseURLSender struct { } func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { - req, err := jsonReq(t.endpoint, Msg{ + return t.BuildRequestContext(context.Background()) +} + +func (t responseURLSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := jsonReq(ctx, t.endpoint, Msg{ Text: t.values.Get("text"), Timestamp: t.values.Get("ts"), + ThreadTimestamp: t.values.Get("thread_ts"), Attachments: t.attachments, Blocks: t.blocks, + Metadata: t.metadata, ResponseType: t.responseType, ReplaceOriginal: t.replaceOriginal, DeleteOriginal: t.deleteOriginal, @@ -402,6 +516,100 @@ 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 { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + config.values.Add("user_auth_url", userAuthURL) + return nil + } +} + +// MsgOptionUnfurlAuthRequired requests that the user installs the +// Slack app for unfurling. +func MsgOptionUnfurlAuthRequired(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + config.values.Add("user_auth_required", "true") + return nil + } +} + +// MsgOptionUnfurlAuthMessage attaches a message inviting the user to +// authenticate. +func MsgOptionUnfurlAuthMessage(timestamp string, msg string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + config.values.Add("user_auth_message", msg) + return nil + } +} + +// 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 { @@ -436,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") } @@ -480,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 } } @@ -560,9 +760,9 @@ func MsgOptionBroadcast() MsgOption { // MsgOptionCompose combines multiple options into a single option. func MsgOptionCompose(options ...MsgOption) MsgOption { - return func(c *sendConfig) error { + return func(config *sendConfig) error { for _, opt := range options { - if err := opt(c); err != nil { + if err := opt(config); err != nil { return err } } @@ -572,30 +772,160 @@ func MsgOptionCompose(options ...MsgOption) MsgOption { // MsgOptionParse set parse option. func MsgOptionParse(b bool) MsgOption { - return func(c *sendConfig) error { + return func(config *sendConfig) error { var v string if b { v = "full" } else { v = "none" } - c.values.Set("parse", v) + config.values.Set("parse", v) return nil } } // MsgOptionIconURL sets an icon URL func MsgOptionIconURL(iconURL string) MsgOption { - return func(c *sendConfig) error { - c.values.Set("icon_url", iconURL) + return func(config *sendConfig) error { + config.values.Set("icon_url", iconURL) return nil } } // MsgOptionIconEmoji sets an icon emoji func MsgOptionIconEmoji(iconEmoji string) MsgOption { - return func(c *sendConfig) error { - c.values.Set("icon_emoji", iconEmoji) + return func(config *sendConfig) error { + config.values.Set("icon_emoji", iconEmoji) + return nil + } +} + +// MsgOptionMetadata sets message metadata +func MsgOptionMetadata(metadata SlackMetadata) MsgOption { + return func(config *sendConfig) error { + config.metadata = metadata + meta, err := json.Marshal(metadata) + if err == nil { + config.values.Set("metadata", string(meta)) + } + return err + } +} + +// 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 { + config.values.Set("link_names", strconv.FormatBool(linkName)) + return nil + } +} + +// MsgOptionFileIDs sets file IDs for the message +func MsgOptionFileIDs(fileIDs []string) MsgOption { + return func(config *sendConfig) error { + if len(fileIDs) == 0 { + return nil + } + + fileIDsBytes, err := json.Marshal(fileIDs) + if err != nil { + return err + } + + config.values.Set("file_ids", string(fileIDsBytes)) + return nil + } +} + +// 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 } } @@ -634,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") } @@ -652,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") } @@ -659,33 +994,42 @@ 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) + } + return nil } } -// PermalinkParameters are the parameters required to get a permalink to a -// message. Slack documentation can be found here: -// https://api.slack.com/methods/chat.getPermalink +// PermalinkParameters are the parameters required to get a permalink to a message. type PermalinkParameters struct { Channel string Ts string } -// GetPermalink returns the permalink for a message. It takes -// PermalinkParameters and returns a string containing the permalink. It -// returns an error if unable to retrieve the permalink. +// GetPermalink returns the permalink for a message. It takes PermalinkParameters and returns a string containing the +// permalink. It returns an error if unable to retrieve the permalink. +// For more details, see GetPermalinkContext documentation. func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) { return api.GetPermalinkContext(context.Background(), params) } // GetPermalinkContext returns the permalink for a message using a custom context. +// Slack API docs: https://api.slack.com/methods/chat.getPermalink func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) { values := url.Values{ - "token": {api.token}, "channel": {params.Channel}, "message_ts": {params.Ts}, } @@ -695,7 +1039,7 @@ func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkPar Permalink string `json:"permalink"` SlackResponse }{} - err := api.getMethod(ctx, "chat.getPermalink", values, &response) + err := api.getMethod(ctx, "chat.getPermalink", api.token, values, &response) if err != nil { return "", err } @@ -704,25 +1048,31 @@ func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkPar type GetScheduledMessagesParameters struct { Channel string + TeamID string Cursor string Latest string Limit int Oldest string } -// GetScheduledMessages returns the list of scheduled messages based on params -func (api *Client) GetScheduledMessages(params *GetScheduledMessagesParameters) (channels []Message, nextCursor string, err error) { +// GetScheduledMessages returns the list of scheduled messages based on params. +// For more details, see GetScheduledMessagesContext documentation. +func (api *Client) GetScheduledMessages(params *GetScheduledMessagesParameters) (channels []ScheduledMessage, nextCursor string, err error) { return api.GetScheduledMessagesContext(context.Background(), params) } -// GetScheduledMessagesContext returns the list of scheduled messages in a Slack team with a custom context -func (api *Client) GetScheduledMessagesContext(ctx context.Context, params *GetScheduledMessagesParameters) (channels []Message, nextCursor string, err error) { +// GetScheduledMessagesContext returns the list of scheduled messages based on params with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.getScheduledMessages.list +func (api *Client) GetScheduledMessagesContext(ctx context.Context, params *GetScheduledMessagesParameters) (channels []ScheduledMessage, nextCursor string, err error) { values := url.Values{ "token": {api.token}, } if params.Channel != "" { values.Add("channel", params.Channel) } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } if params.Cursor != "" { values.Add("cursor", params.Cursor) } @@ -736,8 +1086,8 @@ func (api *Client) GetScheduledMessagesContext(ctx context.Context, params *GetS values.Add("oldest", params.Oldest) } response := struct { - Messages []Message `json:"scheduled_messages"` - ResponseMetaData responseMetaData `json:"response_metadata"` + Messages []ScheduledMessage `json:"scheduled_messages"` + ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} @@ -755,12 +1105,14 @@ type DeleteScheduledMessageParameters struct { AsUser bool } -// DeleteScheduledMessage returns the list of scheduled messages based on params +// DeleteScheduledMessage deletes a pending scheduled message. +// For more details, see DeleteScheduledMessageContext documentation. func (api *Client) DeleteScheduledMessage(params *DeleteScheduledMessageParameters) (bool, error) { return api.DeleteScheduledMessageContext(context.Background(), params) } -// DeleteScheduledMessageContext returns the list of scheduled messages in a Slack team with a custom context +// DeleteScheduledMessageContext deletes a pending scheduled message with a custom context. +// Slack API docs: https://api.slack.com/methods/chat.deleteScheduledMessage func (api *Client) DeleteScheduledMessageContext(ctx context.Context, params *DeleteScheduledMessageParameters) (bool, error) { values := url.Values{ "token": {api.token}, @@ -779,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 0000000..fbdab3b --- /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 1e4a61f..a0ce707 100644 --- a/vendor/github.com/slack-go/slack/conversation.go +++ b/vendor/github.com/slack-go/slack/conversation.go @@ -2,9 +2,12 @@ package slack import ( "context" + "encoding/json" + "errors" "net/url" "strconv" "strings" + "time" ) // Conversation is the foundation for IM and BaseGroupConversation @@ -21,17 +24,24 @@ type Conversation struct { IsIM bool `json:"is_im"` IsExtShared bool `json:"is_ext_shared"` IsOrgShared bool `json:"is_org_shared"` + IsGlobalShared bool `json:"is_global_shared"` IsPendingExtShared bool `json:"is_pending_ext_shared"` 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"` Priority float64 `json:"priority"` User string `json:"user"` - - // TODO support pending_shared - // TODO support previous_names + ConnectedTeamIDs []string `json:"connected_team_ids,omitempty"` + SharedTeamIDs []string `json:"shared_team_ids,omitempty"` + InternalTeamIDs []string `json:"internal_team_ids,omitempty"` + ContextTeamID string `json:"context_team_id,omitempty"` + ConversationHostID string `json:"conversation_host_id,omitempty"` + PreviousNames []string `json:"previous_names,omitempty"` + PendingShared []string `json:"pending_shared,omitempty"` } // GroupConversation is the foundation for Group and Channel @@ -59,6 +69,39 @@ type Purpose struct { LastSet JSONTime `json:"last_set"` } +// 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"` + RecordChannel RecordChannel `json:"record_channel"` +} + +type RestrictedTo struct { + Type []string `json:"type"` + User []string `json:"user"` +} + +type Tab struct { + ID string `json:"id"` + Label string `json:"label"` + Type string `json:"type"` +} + +type Canvas struct { + FileId string `json:"file_id"` + IsEmpty bool `json:"is_empty"` + 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 @@ -71,18 +114,21 @@ type GetConversationsForUserParameters struct { Types []string Limit int ExcludeArchived bool + TeamID string } type responseMetaData struct { NextCursor string `json:"next_cursor"` } -// GetUsersInConversation returns the list of users in a conversation +// GetUsersInConversation returns the list of users in a conversation. +// For more details, see GetUsersInConversationContext documentation. func (api *Client) GetUsersInConversation(params *GetUsersInConversationParameters) ([]string, string, error) { return api.GetUsersInConversationContext(context.Background(), params) } -// GetUsersInConversationContext returns the list of users in a conversation with a custom context +// GetUsersInConversationContext returns the list of users in a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.members func (api *Client) GetUsersInConversationContext(ctx context.Context, params *GetUsersInConversationParameters) ([]string, string, error) { values := url.Values{ "token": {api.token}, @@ -112,12 +158,14 @@ func (api *Client) GetUsersInConversationContext(ctx context.Context, params *Ge return response.Members, response.ResponseMetaData.NextCursor, nil } -// GetConversationsForUser returns the list conversations for a given user +// GetConversationsForUser returns the list conversations for a given user. +// For more details, see GetConversationsForUserContext documentation. func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { return api.GetConversationsForUserContext(context.Background(), params) } // GetConversationsForUserContext returns the list conversations for a given user with a custom context +// Slack API docs: https://api.slack.com/methods/users.conversations func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { values := url.Values{ "token": {api.token}, @@ -137,6 +185,10 @@ func (api *Client) GetConversationsForUserContext(ctx context.Context, params *G if params.ExcludeArchived { values.Add("exclude_archived", "true") } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response := struct { Channels []Channel `json:"channels"` ResponseMetaData responseMetaData `json:"response_metadata"` @@ -150,12 +202,14 @@ func (api *Client) GetConversationsForUserContext(ctx context.Context, params *G return response.Channels, response.ResponseMetaData.NextCursor, response.Err() } -// ArchiveConversation archives a conversation +// ArchiveConversation archives a conversation. +// For more details, see ArchiveConversationContext documentation. func (api *Client) ArchiveConversation(channelID string) error { return api.ArchiveConversationContext(context.Background(), channelID) } -// ArchiveConversationContext archives a conversation with a custom context +// ArchiveConversationContext archives a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.archive func (api *Client) ArchiveConversationContext(ctx context.Context, channelID string) error { values := url.Values{ "token": {api.token}, @@ -171,12 +225,14 @@ func (api *Client) ArchiveConversationContext(ctx context.Context, channelID str return response.Err() } -// UnArchiveConversation reverses conversation archival +// UnArchiveConversation reverses conversation archival. +// For more details, see UnArchiveConversationContext documentation. func (api *Client) UnArchiveConversation(channelID string) error { return api.UnArchiveConversationContext(context.Background(), channelID) } -// UnArchiveConversationContext reverses conversation archival with a custom context +// UnArchiveConversationContext reverses conversation archival with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.unarchive func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID string) error { values := url.Values{ "token": {api.token}, @@ -191,12 +247,14 @@ func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID s return response.Err() } -// SetTopicOfConversation sets the topic for a conversation +// SetTopicOfConversation sets the topic for a conversation. +// For more details, see SetTopicOfConversationContext documentation. func (api *Client) SetTopicOfConversation(channelID, topic string) (*Channel, error) { return api.SetTopicOfConversationContext(context.Background(), channelID, topic) } -// SetTopicOfConversationContext sets the topic for a conversation with a custom context +// SetTopicOfConversationContext sets the topic for a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.setTopic func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, topic string) (*Channel, error) { values := url.Values{ "token": {api.token}, @@ -215,12 +273,14 @@ func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, return response.Channel, response.Err() } -// SetPurposeOfConversation sets the purpose for a conversation +// SetPurposeOfConversation sets the purpose for a conversation. +// For more details, see SetPurposeOfConversationContext documentation. func (api *Client) SetPurposeOfConversation(channelID, purpose string) (*Channel, error) { return api.SetPurposeOfConversationContext(context.Background(), channelID, purpose) } -// SetPurposeOfConversationContext sets the purpose for a conversation with a custom context +// SetPurposeOfConversationContext sets the purpose for a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.setPurpose func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelID, purpose string) (*Channel, error) { values := url.Values{ "token": {api.token}, @@ -240,12 +300,14 @@ func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelI return response.Channel, response.Err() } -// RenameConversation renames a conversation +// RenameConversation renames a conversation. +// For more details, see RenameConversationContext documentation. func (api *Client) RenameConversation(channelID, channelName string) (*Channel, error) { return api.RenameConversationContext(context.Background(), channelID, channelName) } -// RenameConversationContext renames a conversation with a custom context +// RenameConversationContext renames a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.rename func (api *Client) RenameConversationContext(ctx context.Context, channelID, channelName string) (*Channel, error) { values := url.Values{ "token": {api.token}, @@ -265,12 +327,14 @@ func (api *Client) RenameConversationContext(ctx context.Context, channelID, cha return response.Channel, response.Err() } -// InviteUsersToConversation invites users to a channel +// InviteUsersToConversation invites users to a channel. +// For more details, see InviteUsersToConversation documentation. func (api *Client) InviteUsersToConversation(channelID string, users ...string) (*Channel, error) { return api.InviteUsersToConversationContext(context.Background(), channelID, users...) } -// InviteUsersToConversationContext invites users to a channel with a custom context +// InviteUsersToConversationContext invites users to a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.invite func (api *Client) InviteUsersToConversationContext(ctx context.Context, channelID string, users ...string) (*Channel, error) { values := url.Values{ "token": {api.token}, @@ -290,12 +354,135 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel return response.Channel, response.Err() } -// KickUserFromConversation removes a user from a conversation +/********************************************************************************** +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. +// +// For more details, see ForceInviteUsersToConversationContext documentation. +func (api *Client) ForceInviteUsersToConversation(channelID string, users ...string) (*Channel, error) { + return api.ForceInviteUsersToConversationContext(context.Background(), channelID, users...) +} + +// ForceInviteUsersToConversationContext invites users to a channel with a custom context +// while setting the `force` argument to true. +// +// Slack API docs: https://api.slack.com/methods/conversations.invite +func (api *Client) ForceInviteUsersToConversationContext(ctx context.Context, channelID string, users ...string) (*Channel, error) { + values := url.Values{ + "token": {api.token}, + "channel": {channelID}, + "users": {strings.Join(users, ",")}, + "force": {"true"}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + + err := api.postMethod(ctx, "conversations.invite", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// InviteSharedEmailsToConversation invites users to a shared channels by email. +// For more details, see InviteSharedToConversationContext documentation. +func (api *Client) InviteSharedEmailsToConversation(channelID string, emails ...string) (string, bool, error) { + return api.InviteSharedToConversationContext(context.Background(), InviteSharedToConversationParams{ + ChannelID: channelID, + Emails: emails, + }) +} + +// InviteSharedEmailsToConversationContext invites users to a shared channels by email using context. +// For more details, see InviteSharedToConversationContext documentation. +func (api *Client) InviteSharedEmailsToConversationContext(ctx context.Context, channelID string, emails ...string) (string, bool, error) { + return api.InviteSharedToConversationContext(ctx, InviteSharedToConversationParams{ + ChannelID: channelID, + Emails: emails, + }) +} + +// InviteSharedUserIDsToConversation invites users to a shared channels by user id. +// For more details, see InviteSharedToConversationContext documentation. +func (api *Client) InviteSharedUserIDsToConversation(channelID string, userIDs ...string) (string, bool, error) { + return api.InviteSharedToConversationContext(context.Background(), InviteSharedToConversationParams{ + ChannelID: channelID, + UserIDs: userIDs, + }) +} + +// InviteSharedUserIDsToConversationContext invites users to a shared channels by user id with context. +// For more details, see InviteSharedToConversationContext documentation. +func (api *Client) InviteSharedUserIDsToConversationContext(ctx context.Context, channelID string, userIDs ...string) (string, bool, error) { + return api.InviteSharedToConversationContext(ctx, InviteSharedToConversationParams{ + ChannelID: channelID, + UserIDs: userIDs, + }) +} + +// InviteSharedToConversationParams defines the parameters for the InviteSharedToConversation and InviteSharedToConversationContext functions. +type InviteSharedToConversationParams struct { + ChannelID string + Emails []string + UserIDs []string + ExternalLimited *bool +} + +// InviteSharedToConversation invites emails or userIDs to a channel. +// For more details, see InviteSharedToConversationContext documentation. +func (api *Client) InviteSharedToConversation(params InviteSharedToConversationParams) (string, bool, error) { + return api.InviteSharedToConversationContext(context.Background(), params) +} + +// InviteSharedToConversationContext invites emails or userIDs to a channel with a custom context. +// This is a helper function for InviteSharedEmailsToConversation and InviteSharedUserIDsToConversation. +// It accepts either emails or userIDs, but not both. +// Slack API docs: https://api.slack.com/methods/conversations.inviteShared +func (api *Client) InviteSharedToConversationContext(ctx context.Context, params InviteSharedToConversationParams) (string, bool, error) { + values := url.Values{ + "token": {api.token}, + "channel": {params.ChannelID}, + } + if len(params.Emails) > 0 { + values.Add("emails", strings.Join(params.Emails, ",")) + } else if len(params.UserIDs) > 0 { + values.Add("user_ids", strings.Join(params.UserIDs, ",")) + } + if params.ExternalLimited != nil { + values.Add("external_limited", strconv.FormatBool(*params.ExternalLimited)) + } + response := struct { + SlackResponse + InviteID string `json:"invite_id"` + IsLegacySharedChannel bool `json:"is_legacy_shared_channel"` + }{} + + err := api.postMethod(ctx, "conversations.inviteShared", values, &response) + if err != nil { + return "", false, err + } + + return response.InviteID, response.IsLegacySharedChannel, response.Err() +} + +// KickUserFromConversation removes a user from a conversation. +// For more details, see KickUserFromConversationContext documentation. func (api *Client) KickUserFromConversation(channelID string, user string) error { return api.KickUserFromConversationContext(context.Background(), channelID, user) } -// KickUserFromConversationContext removes a user from a conversation with a custom context +// KickUserFromConversationContext removes a user from a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.kick func (api *Client) KickUserFromConversationContext(ctx context.Context, channelID string, user string) error { values := url.Values{ "token": {api.token}, @@ -303,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 @@ -312,12 +499,14 @@ func (api *Client) KickUserFromConversationContext(ctx context.Context, channelI return response.Err() } -// CloseConversation closes a direct message or multi-person direct message +// CloseConversation closes a direct message or multi-person direct message. +// For more details, see CloseConversationContext documentation. func (api *Client) CloseConversation(channelID string) (noOp bool, alreadyClosed bool, err error) { return api.CloseConversationContext(context.Background(), channelID) } -// CloseConversationContext closes a direct message or multi-person direct message with a custom context +// CloseConversationContext closes a direct message or multi-person direct message with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.close func (api *Client) CloseConversationContext(ctx context.Context, channelID string) (noOp bool, alreadyClosed bool, err error) { values := url.Values{ "token": {api.token}, @@ -337,17 +526,28 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin return response.NoOp, response.AlreadyClosed, response.Err() } -// CreateConversation initiates a public or private channel-based conversation -func (api *Client) CreateConversation(channelName string, isPrivate bool) (*Channel, error) { - return api.CreateConversationContext(context.Background(), channelName, isPrivate) +type CreateConversationParams struct { + ChannelName string + IsPrivate bool + TeamID string } -// CreateConversationContext initiates a public or private channel-based conversation with a custom context -func (api *Client) CreateConversationContext(ctx context.Context, channelName string, isPrivate bool) (*Channel, error) { +// CreateConversation initiates a public or private channel-based conversation. +// For more details, see CreateConversationContext documentation. +func (api *Client) CreateConversation(params CreateConversationParams) (*Channel, error) { + return api.CreateConversationContext(context.Background(), params) +} + +// CreateConversationContext initiates a public or private channel-based conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.create +func (api *Client) CreateConversationContext(ctx context.Context, params CreateConversationParams) (*Channel, error) { values := url.Values{ "token": {api.token}, - "name": {channelName}, - "is_private": {strconv.FormatBool(isPrivate)}, + "name": {params.ChannelName}, + "is_private": {strconv.FormatBool(params.IsPrivate)}, + } + if params.TeamID != "" { + values.Set("team_id", params.TeamID) } response, err := api.channelRequest(ctx, "conversations.create", values) if err != nil { @@ -357,17 +557,35 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st return &response.Channel, nil } -// GetConversationInfo retrieves information about a conversation -func (api *Client) GetConversationInfo(channelID string, includeLocale bool) (*Channel, error) { - return api.GetConversationInfoContext(context.Background(), channelID, includeLocale) +// GetConversationInfoInput Defines the parameters of a GetConversationInfo and GetConversationInfoContext function +type GetConversationInfoInput struct { + ChannelID string + IncludeLocale bool + IncludeNumMembers bool } -// GetConversationInfoContext retrieves information about a conversation with a custom context -func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool) (*Channel, error) { +// GetConversationInfo retrieves information about a conversation. +// For more details, see GetConversationInfoContext documentation. +func (api *Client) GetConversationInfo(input *GetConversationInfoInput) (*Channel, error) { + return api.GetConversationInfoContext(context.Background(), input) +} + +// GetConversationInfoContext retrieves information about a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.info +func (api *Client) GetConversationInfoContext(ctx context.Context, input *GetConversationInfoInput) (*Channel, error) { + if input == nil { + return nil, errors.New("GetConversationInfoInput must not be nil") + } + + if input.ChannelID == "" { + return nil, errors.New("ChannelID must be defined") + } + values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "include_locale": {strconv.FormatBool(includeLocale)}, + "token": {api.token}, + "channel": {input.ChannelID}, + "include_locale": {strconv.FormatBool(input.IncludeLocale)}, + "include_num_members": {strconv.FormatBool(input.IncludeNumMembers)}, } response, err := api.channelRequest(ctx, "conversations.info", values) if err != nil { @@ -377,12 +595,14 @@ func (api *Client) GetConversationInfoContext(ctx context.Context, channelID str return &response.Channel, response.Err() } -// LeaveConversation leaves a conversation +// LeaveConversation leaves a conversation. +// For more details, see LeaveConversationContext documentation. func (api *Client) LeaveConversation(channelID string) (bool, error) { return api.LeaveConversationContext(context.Background(), channelID) } -// LeaveConversationContext leaves a conversation with a custom context +// LeaveConversationContext leaves a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.leave func (api *Client) LeaveConversationContext(ctx context.Context, channelID string) (bool, error) { values := url.Values{ "token": {api.token}, @@ -398,21 +618,24 @@ func (api *Client) LeaveConversationContext(ctx context.Context, channelID strin } type GetConversationRepliesParameters struct { - ChannelID string - Timestamp string - Cursor string - Inclusive bool - Latest string - Limit int - Oldest string + ChannelID string + Timestamp string + Cursor string + Inclusive bool + Latest string + Limit int + Oldest string + IncludeAllMetadata bool } -// GetConversationReplies retrieves a thread of messages posted to a conversation +// GetConversationReplies retrieves a thread of messages posted to a conversation. +// For more details, see GetConversationRepliesContext documentation. func (api *Client) GetConversationReplies(params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { return api.GetConversationRepliesContext(context.Background(), params) } -// GetConversationRepliesContext retrieves a thread of messages posted to a conversation with a custom context +// GetConversationRepliesContext retrieves a thread of messages posted to a conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.replies func (api *Client) GetConversationRepliesContext(ctx context.Context, params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { values := url.Values{ "token": {api.token}, @@ -436,6 +659,11 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge } else { values.Add("inclusive", "0") } + if params.IncludeAllMetadata { + values.Add("include_all_metadata", "1") + } else { + values.Add("include_all_metadata", "0") + } response := struct { SlackResponse HasMore bool `json:"has_more"` @@ -455,21 +683,167 @@ func (api *Client) GetConversationRepliesContext(ctx context.Context, params *Ge type GetConversationsParameters struct { Cursor string - ExcludeArchived string + ExcludeArchived bool Limit int Types []string + TeamID string } -// GetConversations returns the list of channels in a Slack team +// 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) { return api.GetConversationsContext(context.Background(), params) } -// GetConversationsContext returns the list of channels in a Slack team with a custom context +// GetConversationsContext returns the list of channels in a Slack team with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.list func (api *Client) GetConversationsContext(ctx context.Context, params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { values := url.Values{ - "token": {api.token}, - "exclude_archived": {params.ExcludeArchived}, + "token": {api.token}, } if params.Cursor != "" { values.Add("cursor", params.Cursor) @@ -480,6 +854,13 @@ func (api *Client) GetConversationsContext(ctx context.Context, params *GetConve if params.Types != nil { values.Add("types", strings.Join(params.Types, ",")) } + if params.ExcludeArchived { + values.Add("exclude_archived", strconv.FormatBool(params.ExcludeArchived)) + } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response := struct { Channels []Channel `json:"channels"` ResponseMetaData responseMetaData `json:"response_metadata"` @@ -500,12 +881,14 @@ type OpenConversationParameters struct { Users []string } -// OpenConversation opens or resumes a direct message or multi-person direct message +// OpenConversation opens or resumes a direct message or multi-person direct message. +// For more details, see OpenConversationContext documentation. func (api *Client) OpenConversation(params *OpenConversationParameters) (*Channel, bool, bool, error) { return api.OpenConversationContext(context.Background(), params) } -// OpenConversationContext opens or resumes a direct message or multi-person direct message with a custom context +// OpenConversationContext opens or resumes a direct message or multi-person direct message with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.open func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConversationParameters) (*Channel, bool, bool, error) { values := url.Values{ "token": {api.token}, @@ -532,12 +915,14 @@ func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConv return response.Channel, response.NoOp, response.AlreadyOpen, response.Err() } -// JoinConversation joins an existing conversation +// JoinConversation joins an existing conversation. +// For more details, see JoinConversationContext documentation. func (api *Client) JoinConversation(channelID string) (*Channel, string, []string, error) { return api.JoinConversationContext(context.Background(), channelID) } -// JoinConversationContext joins an existing conversation with a custom context +// JoinConversationContext joins an existing conversation with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.join func (api *Client) JoinConversationContext(ctx context.Context, channelID string) (*Channel, string, []string, error) { values := url.Values{"token": {api.token}, "channel": {channelID}} response := struct { @@ -564,12 +949,13 @@ func (api *Client) JoinConversationContext(ctx context.Context, channelID string } type GetConversationHistoryParameters struct { - ChannelID string - Cursor string - Inclusive bool - Latest string - Limit int - Oldest string + ChannelID string + Cursor string + Inclusive bool + Latest string + Limit int + Oldest string + IncludeAllMetadata bool } type GetConversationHistoryResponse struct { @@ -583,12 +969,14 @@ 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}} if params.Cursor != "" { @@ -608,6 +996,11 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge if params.Oldest != "" { values.Add("oldest", params.Oldest) } + if params.IncludeAllMetadata { + values.Add("include_all_metadata", "1") + } else { + values.Add("include_all_metadata", "0") + } response := GetConversationHistoryResponse{} @@ -618,3 +1011,94 @@ func (api *Client) GetConversationHistoryContext(ctx context.Context, params *Ge return &response, response.Err() } + +// MarkConversation sets the read mark of a conversation to a specific point. +// For more details, see MarkConversationContext documentation. +func (api *Client) MarkConversation(channel, ts string) (err error) { + return api.MarkConversationContext(context.Background(), channel, ts) +} + +// MarkConversationContext sets the read mark of a conversation to a specific point with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.mark +func (api *Client) MarkConversationContext(ctx context.Context, channel, ts string) error { + values := url.Values{ + "token": {api.token}, + "channel": {channel}, + "ts": {ts}, + } + + response := &SlackResponse{} + + err := api.postMethod(ctx, "conversations.mark", values, response) + if err != nil { + return err + } + 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, 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, options ...CreateChannelCanvasOption) (string, error) { + params := createChannelCanvasParams{ + documentContent: &documentContent, + } + + for _, opt := range options { + opt(¶ms) + } + + values := url.Values{ + "token": {api.token}, + "channel_id": {channel}, + } + 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 + } + values.Add("document_content", string(documentContentJSON)) + } + + response := struct { + SlackResponse + CanvasID string `json:"canvas_id"` + }{} + err := api.postMethod(ctx, "conversations.canvases.create", values, &response) + if err != nil { + return "", err + } + + return response.CanvasID, response.Err() +} diff --git a/vendor/github.com/slack-go/slack/dialog.go b/vendor/github.com/slack-go/slack/dialog.go index 376cd9e..4c507fc 100644 --- a/vendor/github.com/slack-go/slack/dialog.go +++ b/vendor/github.com/slack-go/slack/dialog.go @@ -54,7 +54,9 @@ type DialogCallback InteractionCallback // DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog type DialogSubmissionCallback struct { - State string `json:"state,omitempty"` + // NOTE: State is only used with the dialog_submission type. + // You should use InteractionCallback.BlockActionsState for block_actions type. + State string `json:"-"` Submission map[string]string `json:"submission"` } @@ -104,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/dialog_select.go b/vendor/github.com/slack-go/slack/dialog_select.go index 385cef6..3d6be98 100644 --- a/vendor/github.com/slack-go/slack/dialog_select.go +++ b/vendor/github.com/slack-go/slack/dialog_select.go @@ -54,6 +54,20 @@ func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption } } +// NewExternalSelectDialogInput constructor for a `external` datasource menu input +func NewExternalSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + Optional: true, + }, + DataSource: DialogDataSourceExternal, + Options: options, + } +} + // NewGroupedSelectDialogInput creates grouped options select input for Dialogs. func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect { return &DialogInputSelect{ diff --git a/vendor/github.com/slack-go/slack/dialog_text.go b/vendor/github.com/slack-go/slack/dialog_text.go index da06bd6..25fa1b6 100644 --- a/vendor/github.com/slack-go/slack/dialog_text.go +++ b/vendor/github.com/slack-go/slack/dialog_text.go @@ -18,7 +18,7 @@ const ( ) // TextInputElement subtype of DialogInput -// https://api.slack.com/dialogs#option_element_attributes#text_element_attributes +// https://api.slack.com/dialogs#option_element_attributes#text_element_attributes type TextInputElement struct { DialogInput MaxLength int `json:"max_length,omitempty"` diff --git a/vendor/github.com/slack-go/slack/dnd.go b/vendor/github.com/slack-go/slack/dnd.go index a3aa680..4f6b35a 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"` } @@ -45,12 +53,14 @@ func (api *Client) dndRequest(ctx context.Context, path string, values url.Value return response, response.Err() } -// EndDND ends the user's scheduled Do Not Disturb session +// EndDND ends the user's scheduled Do Not Disturb session. +// For more information see the EndDNDContext documentation. func (api *Client) EndDND() error { return api.EndDNDContext(context.Background()) } -// EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context +// EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context. +// 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}, @@ -65,12 +75,14 @@ func (api *Client) EndDNDContext(ctx context.Context) error { return response.Err() } -// EndSnooze ends the current user's snooze mode +// EndSnooze ends the current user's snooze mode. +// For more information see the EndSnoozeContext documentation. func (api *Client) EndSnooze() (*DNDStatus, error) { return api.EndSnoozeContext(context.Background()) } -// EndSnoozeContext ends the current user's snooze mode with a custom context +// EndSnoozeContext ends the current user's snooze mode with a custom context. +// 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}, @@ -84,18 +96,23 @@ func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { } // GetDNDInfo provides information about a user's current Do Not Disturb settings. -func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { - return api.GetDNDInfoContext(context.Background(), user) +// For more information see the GetDNDInfoContext documentation. +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. -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 { @@ -105,16 +122,21 @@ func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDSta } // GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. -func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { - return api.GetDNDTeamInfoContext(context.Background(), users) +// For more information see the GetDNDTeamInfoContext documentation. +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. -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 { @@ -128,15 +150,16 @@ func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (m return response.Users, nil } -// SetSnooze 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. +// SetSnooze adjusts the snooze duration for a user's Do Not Disturb settings. +// For more information see the SetSnoozeContext documentation. func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { return api.SetSnoozeContext(context.Background(), minutes) } -// SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings with a custom context. -// For more information see the SetSnooze docs +// 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://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/emoji.go b/vendor/github.com/slack-go/slack/emoji.go index b2b0c6c..139df0f 100644 --- a/vendor/github.com/slack-go/slack/emoji.go +++ b/vendor/github.com/slack-go/slack/emoji.go @@ -10,12 +10,14 @@ type emojiResponseFull struct { SlackResponse } -// GetEmoji retrieves all the emojis +// GetEmoji retrieves all the emojis. +// For more details see GetEmojiContext documentation. func (api *Client) GetEmoji() (map[string]string, error) { return api.GetEmojiContext(context.Background()) } -// GetEmojiContext retrieves all the emojis with a custom context +// GetEmojiContext retrieves all the emojis with a custom context. +// Slack API docs: https://api.slack.com/methods/emoji.list func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, 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 0000000..815de4d --- /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/errors.go b/vendor/github.com/slack-go/slack/errors.go index a1dfec2..8be22a6 100644 --- a/vendor/github.com/slack-go/slack/errors.go +++ b/vendor/github.com/slack-go/slack/errors.go @@ -9,6 +9,7 @@ const ( ErrRTMGoodbye = errorsx.String("goodbye detected") ErrRTMDeadman = errorsx.String("deadman switch triggered") ErrParametersMissing = errorsx.String("received empty parameters") + ErrBlockIDNotUnique = errorsx.String("Block ID needs to be unique") ErrInvalidConfiguration = errorsx.String("invalid configuration") ErrMissingHeaders = errorsx.String("missing headers") ErrExpiredTimestamp = errorsx.String("timestamp is too old") diff --git a/vendor/github.com/slack-go/slack/files.go b/vendor/github.com/slack-go/slack/files.go index 3a7363d..974ba0a 100644 --- a/vendor/github.com/slack-go/slack/files.go +++ b/vendor/github.com/slack-go/slack/files.go @@ -2,6 +2,7 @@ package slack import ( "context" + "encoding/json" "fmt" "io" "net/url" @@ -10,14 +11,15 @@ import ( ) const ( - // Add here the defaults in the siten - DEFAULT_FILES_USER = "" - DEFAULT_FILES_CHANNEL = "" - DEFAULT_FILES_TS_FROM = 0 - DEFAULT_FILES_TS_TO = -1 - DEFAULT_FILES_TYPES = "all" - DEFAULT_FILES_COUNT = 100 - DEFAULT_FILES_PAGE = 1 + // Add here the defaults in the site + DEFAULT_FILES_USER = "" + DEFAULT_FILES_CHANNEL = "" + DEFAULT_FILES_TS_FROM = 0 + DEFAULT_FILES_TS_TO = -1 + DEFAULT_FILES_TYPES = "all" + DEFAULT_FILES_COUNT = 100 + DEFAULT_FILES_PAGE = 1 + DEFAULT_FILES_SHOW_HIDDEN = false ) // File contains all the information for a file @@ -87,6 +89,28 @@ type File struct { NumStars int `json:"num_stars"` IsStarred bool `json:"is_starred"` Shares Share `json:"shares"` + + Subject string `json:"subject"` + To []EmailFileUserInfo `json:"to"` + 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 { + Address string `json:"address"` + Name string `json:"name"` + Original string `json:"original"` +} + +type EmailHeaders struct { + Date string `json:"date"` + InReplyTo string `json:"in_reply_to"` + ReplyTo string `json:"reply_to"` + MessageID string `json:"message_id"` } type Share struct { @@ -105,33 +129,17 @@ 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 Channel string + TeamID string TimestampFrom JSONTime TimestampTo JSONTime Types string Count int Page int + ShowHidden bool } // ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request @@ -139,10 +147,65 @@ type ListFilesParameters struct { Limit int User string Channel string + TeamID string Types string Cursor string } +type UploadFileParameters struct { + File string + FileSize int + Content string + Reader io.Reader + Filename string + Title string + InitialComment string + Blocks Blocks + Channel string + ThreadTimestamp string + AltTxt string + SnippetType string +} + +type GetUploadURLExternalParameters struct { + AltTxt string + FileSize int + FileName string + SnippetType string +} + +type GetUploadURLExternalResponse struct { + UploadURL string `json:"upload_url"` + FileID string `json:"file_id"` + SlackResponse +} + +type UploadToURLParameters struct { + UploadURL string + Reader io.Reader + File string + Content string + Filename string +} + +type FileSummary struct { + ID string `json:"id"` + Title string `json:"title"` +} + +type CompleteUploadExternalParameters struct { + Files []FileSummary + Blocks Blocks + Channel string + InitialComment string + ThreadTimestamp string +} + +type CompleteUploadExternalResponse struct { + SlackResponse + Files []FileSummary `json:"files"` +} + type fileResponseFull struct { File `json:"file"` Paging `json:"paging"` @@ -163,6 +226,7 @@ func NewGetFilesParameters() GetFilesParameters { Types: DEFAULT_FILES_TYPES, Count: DEFAULT_FILES_COUNT, Page: DEFAULT_FILES_PAGE, + ShowHidden: DEFAULT_FILES_SHOW_HIDDEN, } } @@ -176,12 +240,14 @@ func (api *Client) fileRequest(ctx context.Context, path string, values url.Valu return response, response.Err() } -// GetFileInfo retrieves a file and related comments +// GetFileInfo retrieves a file and related comments. +// For more details, see GetFileInfoContext documentation. func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { return api.GetFileInfoContext(context.Background(), fileID, count, page) } -// GetFileInfoContext retrieves a file and related comments with a custom context +// GetFileInfoContext retrieves a file and related comments with a custom context. +// Slack API docs: https://api.slack.com/methods/files.info func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.token}, @@ -197,51 +263,25 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, return &response.File, response.Comments, &response.Paging, nil } -// GetFile retreives a given file from its private download URL +// GetFile retrieves a given file from its private download URL. func (api *Client) GetFile(downloadURL string, writer io.Writer) error { - return downloadFile(api.httpclient, api.token, downloadURL, writer, api) + return api.GetFileContext(context.Background(), downloadURL, writer) } -// GetFiles retrieves all files according to the parameters given -func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { - return api.GetFilesContext(context.Background(), params) +// GetFileContext retrieves a given file from its private download URL with a custom context. +// For more details, see GetFile documentation. +func (api *Client) GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error { + return downloadFile(ctx, api.httpclient, api.token, downloadURL, writer, api) } -// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. -func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { - return api.ListFilesContext(context.Background(), params) -} - -// ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination. -func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { - values := url.Values{ - "token": {api.token}, - } - - if params.User != DEFAULT_FILES_USER { - values.Add("user", params.User) - } - if params.Channel != DEFAULT_FILES_CHANNEL { - values.Add("channel", params.Channel) - } - if params.Limit != DEFAULT_FILES_COUNT { - values.Add("limit", strconv.Itoa(params.Limit)) - } - if params.Cursor != "" { - values.Add("cursor", params.Cursor) - } - - response, err := api.fileRequest(ctx, "files.list", values) - if err != nil { - return nil, nil, err - } - - params.Cursor = response.Metadata.Cursor - - return response.Files, ¶ms, nil +// GetFiles retrieves all files according to the parameters given. +// For more details, see GetFilesContext documentation. +func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { + return api.GetFilesContext(context.Background(), params) } -// GetFilesContext retrieves all files according to the parameters given with a custom context +// GetFilesContext retrieves all files according to the parameters given with a custom context. +// Slack API docs: https://api.slack.com/methods/files.list func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ "token": {api.token}, @@ -252,6 +292,9 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter if params.Channel != DEFAULT_FILES_CHANNEL { values.Add("channel", params.Channel) } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } if params.TimestampFrom != DEFAULT_FILES_TS_FROM { values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) } @@ -267,6 +310,10 @@ 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)) + } response, err := api.fileRequest(ctx, "files.list", values) if err != nil { @@ -275,66 +322,53 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter return response.Files, &response.Paging, nil } -// UploadFile uploads a file -func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { - return api.UploadFileContext(context.Background(), params) +// ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. +// For more details, see ListFilesContext documentation. +func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { + return api.ListFilesContext(context.Background(), params) } -// UploadFileContext uploads a file and setting a custom context -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.AuthTest() - if err != nil { - return nil, err - } - response := &fileResponseFull{} +// ListFilesContext retrieves all files according to the parameters given with a custom context. +// Slack API docs: https://api.slack.com/methods/files.list +func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { values := url.Values{ "token": {api.token}, } - 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.User != DEFAULT_FILES_USER { + values.Add("user", params.User) } - if params.InitialComment != "" { - values.Add("initial_comment", params.InitialComment) + if params.Channel != DEFAULT_FILES_CHANNEL { + values.Add("channel", params.Channel) } - if params.ThreadTimestamp != "" { - values.Add("thread_ts", params.ThreadTimestamp) + if params.TeamID != "" { + values.Add("team_id", params.TeamID) } - if len(params.Channels) != 0 { - values.Add("channels", strings.Join(params.Channels, ",")) + if params.Limit != DEFAULT_FILES_COUNT { + values.Add("limit", strconv.Itoa(params.Limit)) } - if params.Content != "" { - values.Add("content", params.Content) - err = api.postMethod(ctx, "files.upload", values, response) - } else if params.File != "" { - err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", 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", values, params.Reader, response, api) + if params.Cursor != "" { + values.Add("cursor", params.Cursor) } + response, err := api.fileRequest(ctx, "files.list", values) if err != nil { - return nil, err + return nil, nil, err } - return &response.File, response.Err() + params.Cursor = response.Metadata.Cursor + + return response.Files, ¶ms, nil } -// DeleteFileComment deletes a file's comment +// DeleteFileComment deletes a file's comment. +// For more details, see DeleteFileCommentContext documentation. func (api *Client) DeleteFileComment(commentID, fileID string) error { return api.DeleteFileCommentContext(context.Background(), fileID, commentID) } -// DeleteFileCommentContext deletes a file's comment with a custom context +// DeleteFileCommentContext deletes a file's comment with a custom context. +// Slack API docs: https://api.slack.com/methods/files.comments.delete func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) { if fileID == "" || commentID == "" { return ErrParametersMissing @@ -349,12 +383,14 @@ func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, comment return err } -// DeleteFile deletes a file +// DeleteFile deletes a file. +// For more details, see DeleteFileContext documentation. func (api *Client) DeleteFile(fileID string) error { return api.DeleteFileContext(context.Background(), fileID) } -// DeleteFileContext deletes a file with a custom context +// DeleteFileContext deletes a file with a custom context. +// Slack API docs: https://api.slack.com/methods/files.delete func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) { values := url.Values{ "token": {api.token}, @@ -365,12 +401,14 @@ func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err er return err } -// RevokeFilePublicURL disables public/external sharing for a file +// RevokeFilePublicURL disables public/external sharing for a file. +// For more details, see RevokeFilePublicURLContext documentation. func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { return api.RevokeFilePublicURLContext(context.Background(), fileID) } -// RevokeFilePublicURLContext disables public/external sharing for a file with a custom context +// RevokeFilePublicURLContext disables public/external sharing for a file with a custom context. +// Slack API docs: https://api.slack.com/methods/files.revokePublicURL func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { values := url.Values{ "token": {api.token}, @@ -384,12 +422,14 @@ func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string return &response.File, nil } -// ShareFilePublicURL enabled public/external sharing for a file +// ShareFilePublicURL enabled public/external sharing for a file. +// For more details, see ShareFilePublicURLContext documentation. func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { return api.ShareFilePublicURLContext(context.Background(), fileID) } -// ShareFilePublicURLContext enabled public/external sharing for a file with a custom context +// ShareFilePublicURLContext enabled public/external sharing for a file with a custom context. +// Slack API docs: https://api.slack.com/methods/files.sharedPublicURL func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.token}, @@ -402,3 +442,149 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) } return &response.File, response.Comments, &response.Paging, nil } + +// GetUploadURLExternalContext gets a URL and fileID from slack which can later be used to upload a file. +// Slack API docs: https://api.slack.com/methods/files.getUploadURLExternal +func (api *Client) GetUploadURLExternalContext(ctx context.Context, params GetUploadURLExternalParameters) (*GetUploadURLExternalResponse, error) { + if params.FileName == "" { + return nil, fmt.Errorf("FileName cannot be empty") + } + if params.FileSize == 0 { + return nil, fmt.Errorf("FileSize cannot be 0") + } + + values := url.Values{ + "token": {api.token}, + "filename": {params.FileName}, + "length": {strconv.Itoa(params.FileSize)}, + } + if params.AltTxt != "" { + values.Add("alt_txt", params.AltTxt) + } + if params.SnippetType != "" { + values.Add("snippet_type", params.SnippetType) + } + response := &GetUploadURLExternalResponse{} + err := api.postMethod(ctx, "files.getUploadURLExternal", values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// UploadToURL uploads the file to the provided URL using post method +// This is not a Slack API method, but a helper function to upload files to the URL +func (api *Client) UploadToURL(ctx context.Context, params UploadToURLParameters) (err error) { + values := url.Values{} + if params.Content != "" { + contentReader := strings.NewReader(params.Content) + err = postWithMultipartResponse(ctx, api.httpclient, params.UploadURL, params.Filename, "file", api.token, values, contentReader, nil, api) + } else if params.File != "" { + err = postLocalWithMultipartResponse(ctx, api.httpclient, params.UploadURL, params.File, "file", api.token, values, nil, api) + } else if params.Reader != nil { + err = postWithMultipartResponse(ctx, api.httpclient, params.UploadURL, params.Filename, "file", api.token, values, params.Reader, nil, api) + } + return err +} + +// CompleteUploadExternalContext once files are uploaded, this completes the upload and shares it to the specified channel +// Slack API docs: https://api.slack.com/methods/files.completeUploadExternal +func (api *Client) CompleteUploadExternalContext(ctx context.Context, params CompleteUploadExternalParameters) (file *CompleteUploadExternalResponse, err error) { + filesBytes, err := json.Marshal(params.Files) + if err != nil { + return nil, err + } + + values := url.Values{ + "token": {api.token}, + "files": {string(filesBytes)}, + } + + if params.Channel != "" { + values.Add("channel_id", params.Channel) + } + if params.InitialComment != "" { + values.Add("initial_comment", params.InitialComment) + } + if params.Blocks.BlockSet != nil && params.InitialComment == "" { + blocksBytes, err := json.Marshal(params.Blocks) + if err != nil { + return nil, err + } + values.Add("blocks", string(blocksBytes)) + } + if params.ThreadTimestamp != "" { + values.Add("thread_ts", params.ThreadTimestamp) + } + response := &CompleteUploadExternalResponse{} + err = api.postMethod(ctx, "files.completeUploadExternal", values, response) + if err != nil { + return nil, err + } + if response.Err() != nil { + return nil, response.Err() + } + return response, nil +} + +// 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) +} + +// 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) UploadFileContext(ctx context.Context, params UploadFileParameters) (file *FileSummary, err error) { + if params.Filename == "" { + return nil, fmt.Errorf("file.upload.v2: filename cannot be empty") + } + if params.FileSize == 0 { + return nil, fmt.Errorf("file.upload.v2: file size cannot be 0") + } + + u, err := api.GetUploadURLExternalContext(ctx, GetUploadURLExternalParameters{ + AltTxt: params.AltTxt, + FileName: params.Filename, + FileSize: params.FileSize, + SnippetType: params.SnippetType, + }) + if err != nil { + return nil, fmt.Errorf("GetUploadURLExternal: %w", err) + } + + err = api.UploadToURL(ctx, UploadToURLParameters{ + UploadURL: u.UploadURL, + Reader: params.Reader, + File: params.File, + Content: params.Content, + Filename: params.Filename, + }) + if err != nil { + return nil, fmt.Errorf("UploadToURL: %w", err) + } + + c, err := api.CompleteUploadExternalContext(ctx, CompleteUploadExternalParameters{ + Files: []FileSummary{{ + ID: u.FileID, + Title: params.Title, + }}, + Channel: params.Channel, + InitialComment: params.InitialComment, + ThreadTimestamp: params.ThreadTimestamp, + Blocks: params.Blocks, + }) + if err != nil { + 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)) + } + + return &c.Files[0], nil +} diff --git a/vendor/github.com/slack-go/slack/function_execute.go b/vendor/github.com/slack-go/slack/function_execute.go new file mode 100644 index 0000000..97bc7e1 --- /dev/null +++ b/vendor/github.com/slack-go/slack/function_execute.go @@ -0,0 +1,91 @@ +package slack + +import ( + "context" + "encoding/json" +) + +type ( + FunctionCompleteSuccessRequest struct { + FunctionExecutionID string `json:"function_execution_id"` + Outputs map[string]string `json:"outputs"` + } + + FunctionCompleteErrorRequest struct { + FunctionExecutionID string `json:"function_execution_id"` + Error string `json:"error"` + } +) + +type FunctionCompleteSuccessRequestOption func(opt *FunctionCompleteSuccessRequest) error + +func FunctionCompleteSuccessRequestOptionOutput(outputs map[string]string) FunctionCompleteSuccessRequestOption { + return func(opt *FunctionCompleteSuccessRequest) error { + if len(outputs) > 0 { + opt.Outputs = outputs + } + return nil + } +} + +// FunctionCompleteSuccess indicates function is completed +func (api *Client) FunctionCompleteSuccess(functionExecutionId string, options ...FunctionCompleteSuccessRequestOption) error { + return api.FunctionCompleteSuccessContext(context.Background(), functionExecutionId, options...) +} + +// FunctionCompleteSuccess indicates function is completed +func (api *Client) FunctionCompleteSuccessContext(ctx context.Context, functionExecutionId string, options ...FunctionCompleteSuccessRequestOption) error { + // More information: https://api.slack.com/methods/functions.completeSuccess + r := &FunctionCompleteSuccessRequest{ + FunctionExecutionID: functionExecutionId, + } + for _, option := range options { + option(r) + } + + jsonData, err := json.Marshal(r) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := api.postJSONMethod(ctx, "functions.completeSuccess", api.token, jsonData, response); err != nil { + return err + } + + if !response.Ok { + return response.Err() + } + + return nil +} + +// FunctionCompleteError indicates function is completed with error +func (api *Client) FunctionCompleteError(functionExecutionID string, errorMessage string) error { + return api.FunctionCompleteErrorContext(context.Background(), functionExecutionID, errorMessage) +} + +// FunctionCompleteErrorContext indicates function is completed with error +func (api *Client) FunctionCompleteErrorContext(ctx context.Context, functionExecutionID string, errorMessage string) error { + // More information: https://api.slack.com/methods/functions.completeError + r := FunctionCompleteErrorRequest{ + FunctionExecutionID: functionExecutionID, + } + r.Error = errorMessage + + jsonData, err := json.Marshal(r) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := api.postJSONMethod(ctx, "functions.completeError", api.token, jsonData, response); err != nil { + return err + } + + if !response.Ok { + return response.Err() + } + + return nil +} diff --git a/vendor/github.com/slack-go/slack/go.mod b/vendor/github.com/slack-go/slack/go.mod deleted file mode 100644 index 2107e61..0000000 --- a/vendor/github.com/slack-go/slack/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module github.com/slack-go/slack - -require ( - github.com/go-test/deep v1.0.4 - github.com/gorilla/websocket v1.2.0 - github.com/pkg/errors v0.8.0 - github.com/stretchr/testify v1.2.2 -) - -go 1.13 diff --git a/vendor/github.com/slack-go/slack/go.sum b/vendor/github.com/slack-go/slack/go.sum deleted file mode 100644 index 7a0ae46..0000000 --- a/vendor/github.com/slack-go/slack/go.sum +++ /dev/null @@ -1,12 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= -github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= -github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/vendor/github.com/slack-go/slack/groups.go b/vendor/github.com/slack-go/slack/groups.go index 6ea1b13..b77f909 100644 --- a/vendor/github.com/slack-go/slack/groups.go +++ b/vendor/github.com/slack-go/slack/groups.go @@ -1,376 +1,7 @@ package slack -import ( - "context" - "net/url" - "strconv" -) - // Group contains all the information for a group type Group struct { GroupConversation IsGroup bool `json:"is_group"` } - -type groupResponseFull struct { - Group Group `json:"group"` - Groups []Group `json:"groups"` - Purpose string `json:"purpose"` - Topic string `json:"topic"` - NotInGroup bool `json:"not_in_group"` - NoOp bool `json:"no_op"` - AlreadyClosed bool `json:"already_closed"` - AlreadyOpen bool `json:"already_open"` - AlreadyInGroup bool `json:"already_in_group"` - Channel Channel `json:"channel"` - History - SlackResponse -} - -func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) { - response := &groupResponseFull{} - err := api.postMethod(ctx, path, values, response) - if err != nil { - return nil, err - } - - return response, response.Err() -} - -// ArchiveGroup archives a private group -func (api *Client) ArchiveGroup(group string) error { - return api.ArchiveGroupContext(context.Background(), group) -} - -// ArchiveGroupContext archives a private group -func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - _, err := api.groupRequest(ctx, "groups.archive", values) - return err -} - -// UnarchiveGroup unarchives a private group -func (api *Client) UnarchiveGroup(group string) error { - return api.UnarchiveGroupContext(context.Background(), group) -} - -// UnarchiveGroupContext unarchives a private group -func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - _, err := api.groupRequest(ctx, "groups.unarchive", values) - return err -} - -// CreateGroup creates a private group -func (api *Client) CreateGroup(group string) (*Group, error) { - return api.CreateGroupContext(context.Background(), group) -} - -// CreateGroupContext creates a private group -func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) { - values := url.Values{ - "token": {api.token}, - "name": {group}, - } - - response, err := api.groupRequest(ctx, "groups.create", values) - if err != nil { - return nil, err - } - return &response.Group, nil -} - -// CreateChildGroup creates a new private group archiving the old one -// This method takes an existing private group and performs the following steps: -// 1. Renames the existing group (from "example" to "example-archived"). -// 2. Archives the existing group. -// 3. Creates a new group with the name of the existing group. -// 4. Adds all members of the existing group to the new group. -func (api *Client) CreateChildGroup(group string) (*Group, error) { - return api.CreateChildGroupContext(context.Background(), group) -} - -// CreateChildGroupContext creates a new private group archiving the old one with a custom context -// For more information see CreateChildGroup -func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - response, err := api.groupRequest(ctx, "groups.createChild", values) - if err != nil { - return nil, err - } - return &response.Group, nil -} - -// GetGroupHistory fetches all the history for a private group -func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { - return api.GetGroupHistoryContext(context.Background(), group, params) -} - -// GetGroupHistoryContext fetches all the history for a private group with a custom context -func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - if params.Latest != DEFAULT_HISTORY_LATEST { - values.Add("latest", params.Latest) - } - if params.Oldest != DEFAULT_HISTORY_OLDEST { - values.Add("oldest", params.Oldest) - } - if params.Count != DEFAULT_HISTORY_COUNT { - values.Add("count", strconv.Itoa(params.Count)) - } - if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { - if params.Inclusive { - values.Add("inclusive", "1") - } else { - values.Add("inclusive", "0") - } - } - if params.Unreads != DEFAULT_HISTORY_UNREADS { - if params.Unreads { - values.Add("unreads", "1") - } else { - values.Add("unreads", "0") - } - } - - response, err := api.groupRequest(ctx, "groups.history", values) - if err != nil { - return nil, err - } - return &response.History, nil -} - -// InviteUserToGroup invites a specific user to a private group -func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { - return api.InviteUserToGroupContext(context.Background(), group, user) -} - -// InviteUserToGroupContext invites a specific user to a private group with a custom context -func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "user": {user}, - } - - response, err := api.groupRequest(ctx, "groups.invite", values) - if err != nil { - return nil, false, err - } - return &response.Group, response.AlreadyInGroup, nil -} - -// LeaveGroup makes authenticated user leave the group -func (api *Client) LeaveGroup(group string) error { - return api.LeaveGroupContext(context.Background(), group) -} - -// LeaveGroupContext makes authenticated user leave the group with a custom context -func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - _, err = api.groupRequest(ctx, "groups.leave", values) - return err -} - -// KickUserFromGroup kicks a user from a group -func (api *Client) KickUserFromGroup(group, user string) error { - return api.KickUserFromGroupContext(context.Background(), group, user) -} - -// KickUserFromGroupContext kicks a user from a group with a custom context -func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "user": {user}, - } - - _, err = api.groupRequest(ctx, "groups.kick", values) - return err -} - -// GetGroups retrieves all groups -func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { - return api.GetGroupsContext(context.Background(), excludeArchived) -} - -// GetGroupsContext retrieves all groups with a custom context -func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) { - values := url.Values{ - "token": {api.token}, - } - if excludeArchived { - values.Add("exclude_archived", "1") - } - - response, err := api.groupRequest(ctx, "groups.list", values) - if err != nil { - return nil, err - } - return response.Groups, nil -} - -// GetGroupInfo retrieves the given group -func (api *Client) GetGroupInfo(group string) (*Group, error) { - return api.GetGroupInfoContext(context.Background(), group) -} - -// GetGroupInfoContext retrieves the given group with a custom context -func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "include_locale": {strconv.FormatBool(true)}, - } - - response, err := api.groupRequest(ctx, "groups.info", values) - if err != nil { - return nil, err - } - return &response.Group, nil -} - -// SetGroupReadMark sets the read mark on a private group -// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a -// timer before making the call. In this way, any further updates needed during the timeout will not generate extra -// calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live -// channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. -func (api *Client) SetGroupReadMark(group, ts string) error { - return api.SetGroupReadMarkContext(context.Background(), group, ts) -} - -// SetGroupReadMarkContext sets the read mark on a private group with a custom context -// For more details see SetGroupReadMark -func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) (err error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "ts": {ts}, - } - - _, err = api.groupRequest(ctx, "groups.mark", values) - return err -} - -// OpenGroup opens a private group -func (api *Client) OpenGroup(group string) (bool, bool, error) { - return api.OpenGroupContext(context.Background(), group) -} - -// OpenGroupContext opens a private group with a custom context -func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - } - - response, err := api.groupRequest(ctx, "groups.open", values) - if err != nil { - return false, false, err - } - return response.NoOp, response.AlreadyOpen, nil -} - -// RenameGroup renames a group -// XXX: They return a channel, not a group. What is this crap? :( -// Inconsistent api it seems. -func (api *Client) RenameGroup(group, name string) (*Channel, error) { - return api.RenameGroupContext(context.Background(), group, name) -} - -// RenameGroupContext renames a group with a custom context -func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "name": {name}, - } - - // XXX: the created entry in this call returns a string instead of a number - // so I may have to do some workaround to solve it. - response, err := api.groupRequest(ctx, "groups.rename", values) - if err != nil { - return nil, err - } - return &response.Channel, nil -} - -// SetGroupPurpose sets the group purpose -func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { - return api.SetGroupPurposeContext(context.Background(), group, purpose) -} - -// SetGroupPurposeContext sets the group purpose with a custom context -func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "purpose": {purpose}, - } - - response, err := api.groupRequest(ctx, "groups.setPurpose", values) - if err != nil { - return "", err - } - return response.Purpose, nil -} - -// SetGroupTopic sets the group topic -func (api *Client) SetGroupTopic(group, topic string) (string, error) { - return api.SetGroupTopicContext(context.Background(), group, topic) -} - -// SetGroupTopicContext sets the group topic with a custom context -func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) { - values := url.Values{ - "token": {api.token}, - "channel": {group}, - "topic": {topic}, - } - - response, err := api.groupRequest(ctx, "groups.setTopic", values) - if err != nil { - return "", err - } - return response.Topic, nil -} - -// GetGroupReplies gets an entire thread (a message plus all the messages in reply to it). -// see https://api.slack.com/methods/groups.replies -func (api *Client) GetGroupReplies(channelID, thread_ts string) ([]Message, error) { - return api.GetGroupRepliesContext(context.Background(), channelID, thread_ts) -} - -// GetGroupRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context -// see https://api.slack.com/methods/groups.replies -func (api *Client) GetGroupRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channelID}, - "thread_ts": {thread_ts}, - } - response, err := api.groupRequest(ctx, "groups.replies", values) - if err != nil { - return nil, err - } - return response.History.Messages, nil -} 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 0000000..7012364 --- /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 ee784fe..0000000 --- a/vendor/github.com/slack-go/slack/im.go +++ /dev/null @@ -1,154 +0,0 @@ -package slack - -import ( - "context" - "net/url" - "strconv" -) - -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"` -} - -func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) { - response := &imResponseFull{} - err := api.postMethod(ctx, path, values, response) - if err != nil { - return nil, err - } - - return response, response.Err() -} - -// CloseIMChannel closes the direct message channel -func (api *Client) CloseIMChannel(channel string) (bool, bool, error) { - return api.CloseIMChannelContext(context.Background(), channel) -} - -// CloseIMChannelContext closes the direct message channel with a custom context -func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channel}, - } - - response, err := api.imRequest(ctx, "im.close", values) - if err != nil { - return false, false, err - } - return response.NoOp, response.AlreadyClosed, nil -} - -// OpenIMChannel opens a direct message channel to the user provided as argument -// Returns some status and the channel ID -func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { - return api.OpenIMChannelContext(context.Background(), user) -} - -// OpenIMChannelContext opens a direct message channel to the user provided as argument with a custom context -// Returns some status and the channel ID -func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) { - values := url.Values{ - "token": {api.token}, - "user": {user}, - } - - response, err := api.imRequest(ctx, "im.open", values) - if err != nil { - return false, false, "", err - } - return response.NoOp, response.AlreadyOpen, response.Channel.ID, nil -} - -// MarkIMChannel sets the read mark of a direct message channel to a specific point -func (api *Client) MarkIMChannel(channel, ts string) (err error) { - return api.MarkIMChannelContext(context.Background(), channel, ts) -} - -// MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context -func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) error { - values := url.Values{ - "token": {api.token}, - "channel": {channel}, - "ts": {ts}, - } - - _, err := api.imRequest(ctx, "im.mark", values) - return err -} - -// GetIMHistory retrieves the direct message channel history -func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) { - return api.GetIMHistoryContext(context.Background(), channel, params) -} - -// GetIMHistoryContext retrieves the direct message channel history with a custom context -func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) { - values := url.Values{ - "token": {api.token}, - "channel": {channel}, - } - if params.Latest != DEFAULT_HISTORY_LATEST { - values.Add("latest", params.Latest) - } - if params.Oldest != DEFAULT_HISTORY_OLDEST { - values.Add("oldest", params.Oldest) - } - if params.Count != DEFAULT_HISTORY_COUNT { - values.Add("count", strconv.Itoa(params.Count)) - } - if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { - if params.Inclusive { - values.Add("inclusive", "1") - } else { - values.Add("inclusive", "0") - } - } - if params.Unreads != DEFAULT_HISTORY_UNREADS { - if params.Unreads { - values.Add("unreads", "1") - } else { - values.Add("unreads", "0") - } - } - - response, err := api.imRequest(ctx, "im.history", values) - if err != nil { - return nil, err - } - return &response.History, nil -} - -// GetIMChannels returns the list of direct message channels -func (api *Client) GetIMChannels() ([]IM, error) { - return api.GetIMChannelsContext(context.Background()) -} - -// GetIMChannelsContext returns the list of direct message channels with a custom context -func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) { - values := url.Values{ - "token": {api.token}, - } - - response, err := api.imRequest(ctx, "im.list", values) - if err != nil { - return nil, err - } - return response.IMs, nil -} diff --git a/vendor/github.com/slack-go/slack/info.go b/vendor/github.com/slack-go/slack/info.go index ec70624..d5276f7 100644 --- a/vendor/github.com/slack-go/slack/info.go +++ b/vendor/github.com/slack-go/slack/info.go @@ -321,10 +321,13 @@ type UserPrefs struct { } func (api *Client) GetUserPrefs() (*UserPrefsCarrier, error) { - values := url.Values{"token": {api.token}} + return api.GetUserPrefsContext(context.Background()) +} + +func (api *Client) GetUserPrefsContext(ctx context.Context) (*UserPrefsCarrier, error) { response := UserPrefsCarrier{} - err := api.getMethod(context.Background(), "users.prefs.get", values, &response) + err := api.getMethod(ctx, "users.prefs.get", api.token, url.Values{}, &response) if err != nil { return nil, err } @@ -406,6 +409,11 @@ func (t JSONTime) Time() time.Time { func (t *JSONTime) UnmarshalJSON(buf []byte) error { s := bytes.Trim(buf, `"`) + if bytes.EqualFold(s, []byte("null")) { + *t = JSONTime(0) + return nil + } + v, err := strconv.Atoi(string(s)) if err != nil { return err @@ -420,13 +428,16 @@ type Team struct { ID string `json:"id"` Name string `json:"name"` Domain string `json:"domain"` + 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"` - Image72 string `json:"image_72,omitempty"` + Image36 string `json:"image_36,omitempty"` + Image48 string `json:"image_48,omitempty"` + Image72 string `json:"image_72,omitempty"` + Image132 string `json:"image_132,omitempty"` + Image230 string `json:"image_230,omitempty"` } // Info contains various details about the authenticated user and team. @@ -441,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/interactions.go b/vendor/github.com/slack-go/slack/interactions.go index ec662ee..f658a72 100644 --- a/vendor/github.com/slack-go/slack/interactions.go +++ b/vendor/github.com/slack-go/slack/interactions.go @@ -3,17 +3,19 @@ package slack import ( "bytes" "encoding/json" + "errors" + "net/http" ) // InteractionType type of interactions type InteractionType string // ActionType type represents the type of action (attachment, block, etc.) -type actionType string +type ActionType string // action is an interface that should be implemented by all callback action types type action interface { - actionType() actionType + actionType() ActionType } // Types of interactions that can be received. @@ -28,39 +30,130 @@ const ( InteractionTypeViewSubmission = InteractionType("view_submission") InteractionTypeViewClosed = InteractionType("view_closed") InteractionTypeShortcut = InteractionType("shortcut") + InteractionTypeWorkflowStepEdit = InteractionType("workflow_step_edit") ) // InteractionCallback is sent from slack when a user interactions with a button or dialog. type InteractionCallback struct { - Type InteractionType `json:"type"` - Token string `json:"token"` - CallbackID string `json:"callback_id"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` - ActionTs string `json:"action_ts"` - Team Team `json:"team"` - Channel Channel `json:"channel"` - User User `json:"user"` - OriginalMessage Message `json:"original_message"` - Message Message `json:"message"` - Name string `json:"name"` - Value string `json:"value"` - MessageTs string `json:"message_ts"` - AttachmentID string `json:"attachment_id"` - ActionCallback ActionCallbacks `json:"actions"` - View View `json:"view"` - ActionID string `json:"action_id"` - APIAppID string `json:"api_app_id"` - BlockID string `json:"block_id"` - Container Container `json:"container"` + Type InteractionType `json:"type"` + Token string `json:"token"` + CallbackID string `json:"callback_id"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` + ActionTs string `json:"action_ts"` + Team Team `json:"team"` + Channel Channel `json:"channel"` + User User `json:"user"` + OriginalMessage Message `json:"original_message"` + Message Message `json:"message"` + Name string `json:"name"` + Value string `json:"value"` + MessageTs string `json:"message_ts"` + AttachmentID string `json:"attachment_id"` + ActionCallback ActionCallbacks `json:"actions"` + View View `json:"view"` + ActionID string `json:"action_id"` + APIAppID string `json:"api_app_id"` + BlockID string `json:"block_id"` + Container Container `json:"container"` + Enterprise Enterprise `json:"enterprise"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` DialogSubmissionCallback ViewSubmissionCallback ViewClosedCallback + + // FIXME(kanata2): just workaround for backward-compatibility. + // See also https://github.com/slack-go/slack/issues/816 + RawState json.RawMessage `json:"state,omitempty"` + + // BlockActionState stands for the `state` field in block_actions type. + // NOTE: InteractionCallback.State has a role for the state of dialog_submission type, + // so we cannot use this field for backward-compatibility for now. + BlockActionState *BlockActionStates `json:"-"` +} + +type BlockActionStates struct { + Values map[string]map[string]BlockAction `json:"values"` +} + +// InteractionCallbackParse parses the HTTP form value "payload" from r, unmarshals +// it as JSON into an InteractionCallback, and returns the result. +// It returns an error if the payload is missing or cannot be decoded. +// +// See https://github.com/slack-go/slack/issues/660 for context. +func InteractionCallbackParse(r *http.Request) (InteractionCallback, error) { + payload := r.FormValue("payload") + if len(payload) == 0 { + return InteractionCallback{}, errors.New("payload is empty") + } + + var ic InteractionCallback + if err := json.Unmarshal([]byte(payload), &ic); err != nil { + return InteractionCallback{}, err + } + return ic, nil +} + +func (ic *InteractionCallback) MarshalJSON() ([]byte, error) { + type alias InteractionCallback + tmp := alias(*ic) + if tmp.Type == InteractionTypeBlockActions { + if tmp.BlockActionState == nil { + tmp.RawState = []byte(`{}`) + } else { + state, err := json.Marshal(tmp.BlockActionState.Values) + if err != nil { + return nil, err + } + tmp.RawState = []byte(`{"values":` + string(state) + `}`) + } + } else if ic.Type == InteractionTypeDialogSubmission { + tmp.RawState = []byte(tmp.State) + } + // Use pointer for go1.7 + return json.Marshal(&tmp) +} + +func (ic *InteractionCallback) UnmarshalJSON(b []byte) error { + type alias InteractionCallback + tmp := struct { + Type InteractionType `json:"type"` + *alias + }{ + alias: (*alias)(ic), + } + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + *ic = InteractionCallback(*tmp.alias) + ic.Type = tmp.Type + if ic.Type == InteractionTypeBlockActions { + if len(ic.RawState) > 0 { + err := json.Unmarshal(ic.RawState, &ic.BlockActionState) + if err != nil { + return err + } + } + } else if ic.Type == InteractionTypeDialogSubmission { + ic.State = string(ic.RawState) + } + return nil } type Container struct { - Type string `json:"type"` - ViewID string `json:"view_id"` + Type string `json:"type"` + ViewID string `json:"view_id"` + MessageTs string `json:"message_ts"` + ThreadTs string `json:"thread_ts,omitempty"` + AttachmentID json.Number `json:"attachment_id"` + ChannelID string `json:"channel_id"` + IsEphemeral bool `json:"is_ephemeral"` + IsAppUnfurl bool `json:"is_app_unfurl"` +} + +type Enterprise struct { + ID string `json:"id"` + Name string `json:"name"` } // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of @@ -135,7 +228,7 @@ func (a *ActionCallbacks) UnmarshalJSON(data []byte) error { } a.BlockActions = append(a.BlockActions, action.(*BlockAction)) - return nil + continue } action, err := unmarshalAction(r, &AttachmentAction{}) diff --git a/vendor/github.com/slack-go/slack/backoff.go b/vendor/github.com/slack-go/slack/internal/backoff/backoff.go similarity index 79% rename from vendor/github.com/slack-go/slack/backoff.go rename to vendor/github.com/slack-go/slack/internal/backoff/backoff.go index 2ba697e..833e9f2 100644 --- a/vendor/github.com/slack-go/slack/backoff.go +++ b/vendor/github.com/slack-go/slack/internal/backoff/backoff.go @@ -1,4 +1,4 @@ -package slack +package backoff import ( "math/rand" @@ -11,7 +11,7 @@ import ( // call to Duration() it is multiplied by Factor. It is capped at // Max. It returns to Min on every call to Reset(). Used in // conjunction with the time package. -type backoff struct { +type Backoff struct { attempts int // Initial value to scale out Initial time.Duration @@ -23,7 +23,7 @@ type backoff struct { // Returns the current value of the counter and then multiplies it // Factor -func (b *backoff) Duration() (dur time.Duration) { +func (b *Backoff) Duration() (dur time.Duration) { // Zero-values are nonsensical, so we use // them to apply defaults if b.Max == 0 { @@ -51,7 +51,12 @@ func (b *backoff) Duration() (dur time.Duration) { return dur } -//Resets the current value of the counter back to Min -func (b *backoff) Reset() { +// Resets the current value of the counter back to Min +func (b *Backoff) Reset() { b.attempts = 0 } + +// Attempts returns the number of attempts that we had done so far +func (b *Backoff) Attempts() int { + return b.attempts +} diff --git a/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go b/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go index cb85057..0182ec6 100644 --- a/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go +++ b/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go @@ -6,3 +6,12 @@ type String string func (t String) Error() string { return string(t) } + +// Is reports whether String matches with the target error +func (t String) Is(target error) bool { + if target == nil { + return false + } + + return t.Error() == target.Error() +} diff --git a/vendor/github.com/slack-go/slack/logger.go b/vendor/github.com/slack-go/slack/logger.go index 6a3533a..90cb3ca 100644 --- a/vendor/github.com/slack-go/slack/logger.go +++ b/vendor/github.com/slack-go/slack/logger.go @@ -18,7 +18,7 @@ type ilogger interface { Println(...interface{}) } -type debug interface { +type Debug interface { Debug() bool // Debugf print a formatted debug line. diff --git a/vendor/github.com/slack-go/slack/logo.png b/vendor/github.com/slack-go/slack/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9bd143459bfe55bb6e01e721623c51a367be7190 GIT binary patch literal 52440 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

G+=^K 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 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(t.Error) == "" { + if strings.TrimSpace(s.Error) == "" { return nil } - return errors.New(t.Error) + return SlackErrorResponse{Err: s.Error, Errors: s.Errors, ResponseMetadata: s.ResponseMetadata} } -// StatusCodeError represents an http response error. -// type httpStatusCode interface { HTTPStatusCode() int } to handle it. -type statusCodeError struct { - Code int - Status string -} +func (t SlackResponse) Err() error { + if t.Ok { + return nil + } -func (t statusCodeError) Error() string { - return fmt.Sprintf("slack server error: %s", t.Status) -} + // 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(t.Error) == "" { + return nil + } -func (t statusCodeError) HTTPStatusCode() int { - return t.Code + return SlackErrorResponse{Err: t.Error, Errors: t.Errors, ResponseMetadata: t.ResponseMetadata} } -func (t statusCodeError) Retryable() bool { - if t.Code >= 500 || t.Code == http.StatusTooManyRequests { - return true - } - return false +// SlackErrorResponse brings along the metadata of errors returned by the Slack API. +type SlackErrorResponse struct { + Err string + Errors []SlackResponseErrors + ResponseMetadata ResponseMetadata } -// RateLimitedError represents the rate limit respond from slack +func (r SlackErrorResponse) Error() string { return r.Err } + +// RateLimitedError represents the rate limit response from slack type RateLimitedError struct { RetryAfter time.Duration } @@ -76,30 +272,27 @@ func (e *RateLimitedError) Retryable() bool { return true } -func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) { - req, err := http.NewRequest("POST", path, r) +func fileUploadReq(ctx context.Context, path string, r io.Reader) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, r) if err != nil { return nil, err } - req = req.WithContext(ctx) - req.URL.RawQuery = (values).Encode() return req, nil } -func downloadFile(client httpClient, token string, downloadURL string, writer io.Writer, d debug) error { +func downloadFile(ctx context.Context, client httpClient, token string, downloadURL string, writer io.Writer, d Debug) error { if downloadURL == "" { return fmt.Errorf("received empty download URL") } - req, err := http.NewRequest("GET", downloadURL, &bytes.Buffer{}) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, &bytes.Buffer{}) if err != nil { return err } var bearer = "Bearer " + token req.Header.Add("Authorization", bearer) - req.WithContext(context.Background()) resp, err := client.Do(req) if err != nil { @@ -118,8 +311,8 @@ func downloadFile(client httpClient, token string, downloadURL string, writer io return err } -func formReq(endpoint string, values url.Values) (req *http.Request, err error) { - if req, err = http.NewRequest("POST", endpoint, strings.NewReader(values.Encode())); err != nil { +func formReq(ctx context.Context, endpoint string, values url.Values) (req *http.Request, err error) { + if req, err = http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(values.Encode())); err != nil { return nil, err } @@ -127,13 +320,13 @@ func formReq(endpoint string, values url.Values) (req *http.Request, err error) return req, nil } -func jsonReq(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 } - if req, err = http.NewRequest("POST", endpoint, buffer); err != nil { + if req, err = http.NewRequestWithContext(ctx, http.MethodPost, endpoint, buffer); err != nil { return nil, err } @@ -141,20 +334,7 @@ func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) { return req, nil } -func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error { - response, err := ioutil.ReadAll(body) - if err != nil { - return err - } - - if d.Debug() { - d.Debugln("parseResponseBody", string(response)) - } - - return json.Unmarshal(response, intf) -} - -func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname 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 @@ -165,15 +345,22 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, meth } defer file.Close() - return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, values, file, intf, d) + return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, token, values, file, intf, d) } -func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname 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) + errc := make(chan error) go func() { defer pipeWriter.Close() + defer wr.Close() + err := createFormFields(wr, values) + if err != nil { + errc <- err + return + } ioWriter, err := wr.CreateFormFile(fieldname, name) if err != nil { errc <- err @@ -189,12 +376,13 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam return } }() - req, err := fileUploadReq(ctx, path, values, pipeReader) + + req, err := fileUploadReq(ctx, path, pipeReader) if err != nil { return err } req.Header.Add("Content-Type", wr.FormDataContentType()) - req = req.WithContext(ctx) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) resp, err := client.Do(req) if err != nil { @@ -215,63 +403,84 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam } } -func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d debug) error { - req = req.WithContext(ctx) +func createFormFields(mw *multipart.Writer, values url.Values) error { + for key, value := range values { + writer, err := mw.CreateFormField(key) + if err != nil { + return err + } + _, err = writer.Write([]byte(value[0])) + if err != nil { + return err + } + } + return nil +} + +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.NewRequest("POST", 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)) - - return doPost(ctx, client, req, newJSONParser(intf), d) + // 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.NewRequest("POST", 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") - return doPost(ctx, client, req, newJSONParser(intf), d) + // 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 string, values url.Values, intf interface{}, d debug) error { - req, err := http.NewRequest("GET", endpoint, nil) +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)) + req.URL.RawQuery = values.Encode() - return doPost(ctx, client, req, newJSONParser(intf), d) + 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 { +func logResponse(resp *http.Response, d Debug) error { if d.Debug() { text, err := httputil.DumpResponse(resp, true) if err != nil { @@ -291,16 +500,8 @@ func okJSONHandler(rw http.ResponseWriter, r *http.Request) { rw.Write(response) } -// timerReset safely reset a timer, see time.Timer.Reset for details. -func timerReset(t *time.Timer, d time.Duration) { - if !t.Stop() { - <-t.C - } - t.Reset(d) -} - -func checkStatusCode(resp *http.Response, d debug) error { - if resp.StatusCode == http.StatusTooManyRequests { +func checkStatusCode(resp *http.Response, d Debug) error { + if resp.StatusCode == http.StatusTooManyRequests && resp.Header.Get("Retry-After") != "" { retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64) if err != nil { return err @@ -311,7 +512,7 @@ func checkStatusCode(resp *http.Response, d debug) error { // Slack seems to send an HTML body along with 5xx error codes. Don't parse it. if resp.StatusCode != http.StatusOK { logResponse(resp, d) - return statusCodeError{Code: resp.StatusCode, Status: resp.Status} + return StatusCodeError{Code: resp.StatusCode, Status: resp.Status} } return nil @@ -319,15 +520,25 @@ 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 { - b, err := ioutil.ReadAll(resp.Body) + if dst == nil { + return nil + } + + b, err := io.ReadAll(resp.Body) if err != nil { return err } @@ -340,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 @@ -354,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 0000000..912c02c --- /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 4313976..f98284d 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" ) @@ -33,14 +36,18 @@ type OAuthResponse struct { // OAuthV2Response ... type OAuthV2Response struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - Scope string `json:"scope"` - BotUserID string `json:"bot_user_id"` - AppID string `json:"app_id"` - Team OAuthV2ResponseTeam `json:"team"` - Enterprise OAuthV2ResponseEnterprise `json:"enterprise"` - AuthedUser OAuthV2ResponseAuthedUser `json:"authed_user"` + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + BotUserID string `json:"bot_user_id"` + AppID string `json:"app_id"` + Team OAuthV2ResponseTeam `json:"team"` + IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` + Enterprise OAuthV2ResponseEnterprise `json:"enterprise"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` + AuthedUser OAuthV2ResponseAuthedUser `json:"authed_user"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` SlackResponse } @@ -58,31 +65,95 @@ type OAuthV2ResponseEnterprise struct { // OAuthV2ResponseAuthedUser ... type OAuthV2ResponseAuthedUser struct { - ID string `json:"id"` - Scope string `json:"scope"` + ID string `json:"id"` + Scope string `json:"scope"` + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` +} + +// OpenIDConnectResponse ... +type OpenIDConnectResponse struct { + Ok bool `json:"ok"` AccessToken string `json:"access_token"` TokenType string `json:"token_type"` + IdToken string `json:"id_token"` + 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 } } -// GetOAuthToken retrieves an AccessToken -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 resolveOAuthConfig(opts []OAuthOption) oauthConfig { + c := oauthConfig{apiURL: APIURL} + for _, o := range opts { + o(&c) + } + return c } -// GetOAuthTokenContext retrieves an AccessToken with a custom context -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 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, 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, opts ...OAuthOption) (accessToken string, scope string, err error) { + response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI, opts...) if err != nil { return "", "", err } return response.AccessToken, response.Scope, nil } -func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { - return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +// 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, 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, 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 + } + return response.AccessToken, response.Scope, response.Bot, nil +} + +// GetOAuthResponse retrieves OAuth response. +// For more details, see GetOAuthResponseContext documentation. +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...) } -func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) { +// 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, opts ...OAuthOption) (resp *OAuthResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, @@ -90,28 +161,170 @@ 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() +} + +// GetOAuthV2Response gets a V2 OAuth access token response. +// For more details, see GetOAuthV2ResponseContext documentation. +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, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + cfg := resolveOAuthConfig(opts) + values := url.Values{ + "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, cfg.apiURL+"oauth.v2.access", values, response, discard{}); err != nil { + return nil, err + } + return response, response.Err() +} + +// 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, 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, opts ...OAuthOption) (resp *OAuthV2Response, err error) { + values := url.Values{ + "client_id": {clientID}, + "refresh_token": {refreshToken}, + "grant_type": {"refresh_token"}, + } + if clientSecret != "" { + values.Set("client_secret", clientSecret) + } + response := &OAuthV2Response{} + 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() } -// GetOAuthV2Response gets a V2 OAuth access token response - https://api.slack.com/methods/oauth.v2.access -func GetOAuthV2Response(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) { - return GetOAuthV2ResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI) +// 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, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) { + return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI, opts...) } -// GetOAuthV2ResponseContext with a context, gets a V2 OAuth access token response -func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthV2Response, err error) { +// 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, opts ...OAuthOption) (resp *OpenIDConnectResponse, err error) { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, "code": {code}, "redirect_uri": {redirectURI}, } - response := &OAuthV2Response{} - if err = postForm(ctx, client, APIURL+"oauth.v2.access", values, response, discard{}); err != nil { + response := &OpenIDConnectResponse{} + 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/pins.go b/vendor/github.com/slack-go/slack/pins.go index ef97c8d..5e6cf0c 100644 --- a/vendor/github.com/slack-go/slack/pins.go +++ b/vendor/github.com/slack-go/slack/pins.go @@ -12,12 +12,14 @@ type listPinsResponseFull struct { SlackResponse } -// AddPin pins an item in a channel +// AddPin pins an item in a channel. +// For more details, see AddPinContext documentation. func (api *Client) AddPin(channel string, item ItemRef) error { return api.AddPinContext(context.Background(), channel, item) } -// AddPinContext pins an item in a channel with a custom context +// AddPinContext pins an item in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/pins.add func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, @@ -41,12 +43,14 @@ func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemR return response.Err() } -// RemovePin un-pins an item from a channel +// RemovePin un-pins an item from a channel. +// For more details, see RemovePinContext documentation. func (api *Client) RemovePin(channel string, item ItemRef) error { return api.RemovePinContext(context.Background(), channel, item) } -// RemovePinContext un-pins an item from a channel with a custom context +// RemovePinContext un-pins an item from a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/pins.remove func (api *Client) RemovePinContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, @@ -71,11 +75,13 @@ func (api *Client) RemovePinContext(ctx context.Context, channel string, item It } // ListPins returns information about the items a user reacted to. +// For more details, see ListPinsContext documentation. func (api *Client) ListPins(channel string) ([]Item, *Paging, error) { return api.ListPinsContext(context.Background(), channel) } // ListPinsContext returns information about the items a user reacted to with a custom context. +// Slack API docs: https://api.slack.com/methods/pins.list func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, *Paging, error) { values := url.Values{ "channel": {channel}, diff --git a/vendor/github.com/slack-go/slack/reactions.go b/vendor/github.com/slack-go/slack/reactions.go index 2a9bd42..18befa6 100644 --- a/vendor/github.com/slack-go/slack/reactions.go +++ b/vendor/github.com/slack-go/slack/reactions.go @@ -33,54 +33,62 @@ 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 - Count int - Page int - Full bool + User string + TeamID string + Cursor string + Limit int + Full bool } // NewListReactionsParameters initializes the inputs to find all reactions // 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, } } @@ -100,8 +108,8 @@ type listReactionsResponseFull struct { Reactions []ItemReaction } `json:"comment"` } - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { @@ -128,11 +136,13 @@ func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { } // AddReaction adds a reaction emoji to a message, file or file comment. +// For more details, see AddReactionContext documentation. func (api *Client) AddReaction(name string, item ItemRef) error { return api.AddReactionContext(context.Background(), name, item) } // AddReactionContext adds a reaction emoji to a message, file or file comment with a custom context. +// Slack API docs: https://api.slack.com/methods/reactions.add func (api *Client) AddReactionContext(ctx context.Context, name string, item ItemRef) error { values := url.Values{ "token": {api.token}, @@ -162,11 +172,13 @@ func (api *Client) AddReactionContext(ctx context.Context, name string, item Ite } // RemoveReaction removes a reaction emoji from a message, file or file comment. +// For more details, see RemoveReactionContext documentation. func (api *Client) RemoveReaction(name string, item ItemRef) error { return api.RemoveReactionContext(context.Background(), name, item) } // RemoveReactionContext removes a reaction emoji from a message, file or file comment with a custom context. +// Slack API docs: https://api.slack.com/methods/reactions.remove func (api *Client) RemoveReactionContext(ctx context.Context, name string, item ItemRef) error { values := url.Values{ "token": {api.token}, @@ -195,13 +207,15 @@ func (api *Client) RemoveReactionContext(ctx context.Context, name string, item return response.Err() } -// GetReactions returns details about the reactions on an item. -func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +// 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) (ReactedItem, error) { return api.GetReactionsContext(context.Background(), item, params) } -// GetReactionsContext returns details about the reactions on an item with a custom context -func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { +// 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) (ReactedItem, error) { values := url.Values{ "token": {api.token}, } @@ -217,54 +231,59 @@ 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. -func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +// For more details, see ListReactionsContext documentation. +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. -func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) { +// Slack API docs: https://api.slack.com/methods/reactions.list +func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, string, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_REACTIONS_USER { values.Add("user", params.User) } - if params.Count != DEFAULT_REACTIONS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + 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/reminders.go b/vendor/github.com/slack-go/slack/reminders.go index 9b90538..e025bc9 100644 --- a/vendor/github.com/slack-go/slack/reminders.go +++ b/vendor/github.com/slack-go/slack/reminders.go @@ -3,17 +3,16 @@ package slack import ( "context" "net/url" - "time" ) type Reminder struct { - ID string `json:"id"` - Creator string `json:"creator"` - User string `json:"user"` - Text string `json:"text"` - Recurring bool `json:"recurring"` - Time time.Time `json:"time"` - CompleteTS int `json:"complete_ts"` + ID string `json:"id"` + Creator string `json:"creator"` + User string `json:"user"` + Text string `json:"text"` + Recurring bool `json:"recurring"` + Time int `json:"time"` + CompleteTS int `json:"complete_ts"` } type reminderResp struct { @@ -21,6 +20,11 @@ type reminderResp struct { Reminder Reminder `json:"reminder"` } +type remindersResp struct { + SlackResponse + Reminders []*Reminder `json:"reminders"` +} + func (api *Client) doReminder(ctx context.Context, path string, values url.Values) (*Reminder, error) { response := &reminderResp{} if err := api.postMethod(ctx, path, values, response); err != nil { @@ -29,46 +33,85 @@ func (api *Client) doReminder(ctx context.Context, path string, values url.Value return &response.Reminder, response.Err() } +func (api *Client) doReminders(ctx context.Context, path string, values url.Values) ([]*Reminder, error) { + response := &remindersResp{} + if err := api.postMethod(ctx, path, values, response); err != nil { + return nil, err + } + + // create an array of pointers to reminders + var reminders = make([]*Reminder, 0, len(response.Reminders)) + reminders = append(reminders, response.Reminders...) + return reminders, response.Err() +} + +// ListReminders lists all the reminders created by or for the authenticated user +// For more details, see ListRemindersContext documentation. +func (api *Client) ListReminders() ([]*Reminder, error) { + return api.ListRemindersContext(context.Background()) +} + +// ListRemindersContext lists all the reminders created by or for the authenticated user with a custom context. +// Slack API docs: https://api.slack.com/methods/reminders.list +func (api *Client) ListRemindersContext(ctx context.Context) ([]*Reminder, error) { + values := url.Values{ + "token": {api.token}, + } + return api.doReminders(ctx, "reminders.list", values) +} + // AddChannelReminder adds a reminder for a channel. -// -// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set -// reminders on a channel is currently undocumented but has been tested to -// work) +// For more details, see AddChannelReminderContext documentation. func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, error) { + return api.AddChannelReminderContext(context.Background(), channelID, text, time) +} + +// AddChannelReminderContext adds a reminder for a channel with a custom context +// NOTE: the ability to set reminders on a channel is currently undocumented but has been tested to work. +// Slack API docs: https://api.slack.com/methods/reminders.add +func (api *Client) AddChannelReminderContext(ctx context.Context, channelID, text, time string) (*Reminder, error) { values := url.Values{ "token": {api.token}, "text": {text}, "time": {time}, "channel": {channelID}, } - return api.doReminder(context.Background(), "reminders.add", values) + return api.doReminder(ctx, "reminders.add", values) } // AddUserReminder adds a reminder for a user. -// -// See https://api.slack.com/methods/reminders.add (NOTE: the ability to set -// reminders on a channel is currently undocumented but has been tested to -// work) +// For more details, see AddUserReminderContext documentation. func (api *Client) AddUserReminder(userID, text, time string) (*Reminder, error) { + return api.AddUserReminderContext(context.Background(), userID, text, time) +} + +// AddUserReminderContext adds a reminder for a user with a custom context +// Slack API docs: https://api.slack.com/methods/reminders.add +func (api *Client) AddUserReminderContext(ctx context.Context, userID, text, time string) (*Reminder, error) { values := url.Values{ "token": {api.token}, "text": {text}, "time": {time}, "user": {userID}, } - return api.doReminder(context.Background(), "reminders.add", values) + return api.doReminder(ctx, "reminders.add", values) } // DeleteReminder deletes an existing reminder. -// -// See https://api.slack.com/methods/reminders.delete +// For more details, see DeleteReminderContext documentation. func (api *Client) DeleteReminder(id string) error { + return api.DeleteReminderContext(context.Background(), id) +} + +// DeleteReminderContext deletes an existing reminder with a custom context +// Slack API docs: https://api.slack.com/methods/reminders.delete +func (api *Client) DeleteReminderContext(ctx context.Context, id string) error { values := url.Values{ "token": {api.token}, "reminder": {id}, } response := &SlackResponse{} - if err := api.postMethod(context.Background(), "reminders.delete", values, response); err != nil { + if err := api.postMethod(ctx, "reminders.delete", values, response); err != nil { return err } return response.Err() diff --git a/vendor/github.com/slack-go/slack/remotefiles.go b/vendor/github.com/slack-go/slack/remotefiles.go new file mode 100644 index 0000000..51d3e85 --- /dev/null +++ b/vendor/github.com/slack-go/slack/remotefiles.go @@ -0,0 +1,324 @@ +package slack + +import ( + "context" + "fmt" + "io" + "net/url" + "strconv" + "strings" +) + +const ( + DEFAULT_REMOTE_FILES_CHANNEL = "" + DEFAULT_REMOTE_FILES_TS_FROM = 0 + DEFAULT_REMOTE_FILES_TS_TO = -1 + DEFAULT_REMOTE_FILES_COUNT = 100 +) + +// RemoteFile contains all the information for a remote file +// For more details: +// https://api.slack.com/messaging/files/remote +type RemoteFile struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + Timestamp JSONTime `json:"timestamp"` + Name string `json:"name"` + Title string `json:"title"` + Mimetype string `json:"mimetype"` + Filetype string `json:"filetype"` + PrettyType string `json:"pretty_type"` + User string `json:"user"` + Editable bool `json:"editable"` + Size int `json:"size"` + Mode string `json:"mode"` + IsExternal bool `json:"is_external"` + ExternalType string `json:"external_type"` + IsPublic bool `json:"is_public"` + PublicURLShared bool `json:"public_url_shared"` + DisplayAsBot bool `json:"display_as_bot"` + Username string `json:"username"` + URLPrivate string `json:"url_private"` + Permalink string `json:"permalink"` + CommentsCount int `json:"comments_count"` + IsStarred bool `json:"is_starred"` + Shares Share `json:"shares"` + Channels []string `json:"channels"` + Groups []string `json:"groups"` + IMs []string `json:"ims"` + ExternalID string `json:"external_id"` + ExternalURL string `json:"external_url"` + HasRichPreview bool `json:"has_rich_preview"` +} + +// RemoteFileParameters contains required and optional parameters for a remote file. +// +// 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 { + ExternalID string // required + ExternalURL string // required + Title string // required + Filetype string + IndexableFileContents string + PreviewImage string + PreviewImageReader io.Reader + PreviewImageName string // filename for PreviewImageReader (e.g., "preview.jpg") +} + +// ListRemoteFilesParameters contains arguments for the ListRemoteFiles method. +// For more details: +// https://api.slack.com/methods/files.remote.list +type ListRemoteFilesParameters struct { + Channel string + Cursor string + Limit int + TimestampFrom JSONTime + TimestampTo JSONTime +} + +type remoteFileResponseFull struct { + RemoteFile `json:"file"` + Paging `json:"paging"` + Files []RemoteFile `json:"files"` + SlackResponse +} + +func (api *Client) remoteFileRequest(ctx context.Context, path string, values url.Values) (*remoteFileResponseFull, error) { + response := &remoteFileResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// AddRemoteFile adds a remote file. Unlike regular files, remote files must be explicitly shared. +// For more details see the AddRemoteFileContext documentation. +func (api *Client) AddRemoteFile(params RemoteFileParameters) (*RemoteFile, error) { + return api.AddRemoteFileContext(context.Background(), params) +} + +// AddRemoteFileContext adds a remote file and setting a custom context +// Slack API docs: https://api.slack.com/methods/files.remote.add +func (api *Client) AddRemoteFileContext(ctx context.Context, params RemoteFileParameters) (remotefile *RemoteFile, err error) { + if params.ExternalID == "" || params.ExternalURL == "" || params.Title == "" { + return nil, ErrParametersMissing + } + response := &remoteFileResponseFull{} + values := url.Values{ + "token": {api.token}, + "external_id": {params.ExternalID}, + "external_url": {params.ExternalURL}, + "title": {params.Title}, + } + if params.Filetype != "" { + values.Add("filetype", params.Filetype) + } + if params.IndexableFileContents != "" { + values.Add("indexable_file_contents", params.IndexableFileContents) + } + 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 { + 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) + } + + if err != nil { + return nil, err + } + + return &response.RemoteFile, response.Err() +} + +// ListRemoteFiles retrieves all remote files according to the parameters given. Uses cursor based pagination. +// For more details see the ListRemoteFilesContext documentation. +func (api *Client) ListRemoteFiles(params ListRemoteFilesParameters) ([]RemoteFile, error) { + return api.ListRemoteFilesContext(context.Background(), params) +} + +// ListRemoteFilesContext retrieves all remote files according to the parameters given with a custom context. Uses cursor based pagination. +// Slack API docs: https://api.slack.com/methods/files.remote.list +func (api *Client) ListRemoteFilesContext(ctx context.Context, params ListRemoteFilesParameters) ([]RemoteFile, error) { + values := url.Values{ + "token": {api.token}, + } + if params.Channel != DEFAULT_REMOTE_FILES_CHANNEL { + values.Add("channel", params.Channel) + } + if params.TimestampFrom != DEFAULT_REMOTE_FILES_TS_FROM { + values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) + } + if params.TimestampTo != DEFAULT_REMOTE_FILES_TS_TO { + values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) + } + if params.Limit != DEFAULT_REMOTE_FILES_COUNT { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + + response, err := api.remoteFileRequest(ctx, "files.remote.list", values) + if err != nil { + return nil, err + } + + params.Cursor = response.SlackResponse.ResponseMetadata.Cursor + + return response.Files, nil +} + +// GetRemoteFileInfo retrieves the complete remote file information. +// For more details see the GetRemoteFileInfoContext documentation. +func (api *Client) GetRemoteFileInfo(externalID, fileID string) (remotefile *RemoteFile, err error) { + return api.GetRemoteFileInfoContext(context.Background(), externalID, fileID) +} + +// GetRemoteFileInfoContext retrieves the complete remote file information given with a custom context. +// Slack API docs: https://api.slack.com/methods/files.remote.info +func (api *Client) GetRemoteFileInfoContext(ctx context.Context, externalID, fileID string) (remotefile *RemoteFile, err error) { + if fileID == "" && externalID == "" { + return nil, fmt.Errorf("either externalID or fileID is required") + } + if fileID != "" && externalID != "" { + return nil, fmt.Errorf("don't provide both externalID and fileID") + } + values := url.Values{ + "token": {api.token}, + } + if fileID != "" { + values.Add("file", fileID) + } + if externalID != "" { + values.Add("external_id", externalID) + } + response, err := api.remoteFileRequest(ctx, "files.remote.info", values) + if err != nil { + return nil, err + } + return &response.RemoteFile, err +} + +// ShareRemoteFile shares a remote file to channels. +// For more details see the ShareRemoteFileContext documentation. +func (api *Client) ShareRemoteFile(channels []string, externalID, fileID string) (file *RemoteFile, err error) { + return api.ShareRemoteFileContext(context.Background(), channels, externalID, fileID) +} + +// 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 len(channels) == 0 { + return nil, ErrParametersMissing + } + if fileID == "" && externalID == "" { + return nil, fmt.Errorf("either externalID or fileID is required") + } + values := url.Values{ + "token": {api.token}, + "channels": {strings.Join(channels, ",")}, + } + if fileID != "" { + values.Add("file", fileID) + } + if externalID != "" { + values.Add("external_id", externalID) + } + response, err := api.remoteFileRequest(ctx, "files.remote.share", values) + if err != nil { + return nil, err + } + return &response.RemoteFile, err +} + +// UpdateRemoteFile updates a remote file. +// For more details see the UpdateRemoteFileContext documentation. +func (api *Client) UpdateRemoteFile(fileID string, params RemoteFileParameters) (remotefile *RemoteFile, err error) { + return api.UpdateRemoteFileContext(context.Background(), fileID, params) +} + +// UpdateRemoteFileContext updates a remote file with a custom context. +// Slack API docs: https://api.slack.com/methods/files.remote.update +func (api *Client) UpdateRemoteFileContext(ctx context.Context, fileID string, params RemoteFileParameters) (remotefile *RemoteFile, err error) { + response := &remoteFileResponseFull{} + values := url.Values{} + if fileID != "" { + values.Add("file", fileID) + } + if params.ExternalID != "" { + values.Add("external_id", params.ExternalID) + } + if params.ExternalURL != "" { + values.Add("external_url", params.ExternalURL) + } + if params.Title != "" { + values.Add("title", params.Title) + } + if params.Filetype != "" { + values.Add("filetype", params.Filetype) + } + if params.IndexableFileContents != "" { + values.Add("indexable_file_contents", params.IndexableFileContents) + } + 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) + } + + if err != nil { + return nil, err + } + + return &response.RemoteFile, response.Err() +} + +// RemoveRemoteFile removes a remote file. +// For more information see the RemoveRemoteFileContext documentation. +func (api *Client) RemoveRemoteFile(externalID, fileID string) (err error) { + return api.RemoveRemoteFileContext(context.Background(), externalID, fileID) +} + +// RemoveRemoteFileContext removes a remote file with a custom context +// Slack API docs: https://api.slack.com/methods/files.remote.remove +func (api *Client) RemoveRemoteFileContext(ctx context.Context, externalID, fileID string) (err error) { + if fileID == "" && externalID == "" { + return fmt.Errorf("either externalID or fileID is required") + } + if fileID != "" && externalID != "" { + return fmt.Errorf("don't provide both externalID and fileID") + } + values := url.Values{ + "token": {api.token}, + } + if fileID != "" { + values.Add("file", fileID) + } + if externalID != "" { + values.Add("external_id", externalID) + } + _, err = api.remoteFileRequest(ctx, "files.remote.remove", values) + return err +} 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 0000000..d2f6211 --- /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/rtm.go b/vendor/github.com/slack-go/slack/rtm.go index ef6ba34..9b30eb3 100644 --- a/vendor/github.com/slack-go/slack/rtm.go +++ b/vendor/github.com/slack-go/slack/rtm.go @@ -25,6 +25,9 @@ const ( // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. +// +// Deprecated: Use [ConnectRTM] instead. +// For more details, see: https://api.slack.com/changelog/2021-10-rtm-start-to-stop func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout) defer cancel() @@ -35,6 +38,9 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { // StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context. // // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it. +// +// Deprecated: Use [ConnectRTMContext] instead. +// For more details, see: https://api.slack.com/changelog/2021-10-rtm-start-to-stop func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) { response := &infoResponseFull{} err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response) diff --git a/vendor/github.com/slack-go/slack/search.go b/vendor/github.com/slack-go/slack/search.go index de6b40a..9df4335 100644 --- a/vendor/github.com/slack-go/slack/search.go +++ b/vendor/github.com/slack-go/slack/search.go @@ -15,6 +15,7 @@ const ( ) type SearchParameters struct { + TeamID string Sort string SortDirection string Highlight bool @@ -88,18 +89,21 @@ func NewSearchParameters() SearchParameters { } } -func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) { +func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters) (response *searchResponseFull, error error) { values := url.Values{ "token": {api.token}, "query": {query}, } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } if params.Sort != DEFAULT_SEARCH_SORT { values.Add("sort", params.Sort) } if params.SortDirection != DEFAULT_SEARCH_SORT_DIR { values.Add("sort_dir", params.SortDirection) } - if params.Highlight != DEFAULT_SEARCH_HIGHLIGHT { + if params.Highlight { values.Add("highlight", strconv.Itoa(1)) } if params.Count != DEFAULT_SEARCH_COUNT { @@ -124,7 +128,7 @@ func (api *Client) Search(query string, params SearchParameters) (*SearchMessage } func (api *Client) SearchContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) { - response, err := api._search(ctx, "search.all", query, params, true, true) + response, err := api._search(ctx, "search.all", query, params) if err != nil { return nil, nil, err } @@ -136,7 +140,7 @@ func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFi } func (api *Client) SearchFilesContext(ctx context.Context, query string, params SearchParameters) (*SearchFiles, error) { - response, err := api._search(ctx, "search.files", query, params, true, false) + response, err := api._search(ctx, "search.files", query, params) if err != nil { return nil, err } @@ -148,7 +152,7 @@ func (api *Client) SearchMessages(query string, params SearchParameters) (*Searc } func (api *Client) SearchMessagesContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, error) { - response, err := api._search(ctx, "search.messages", query, params, false, true) + response, err := api._search(ctx, "search.messages", query, params) if err != nil { return nil, err } diff --git a/vendor/github.com/slack-go/slack/security.go b/vendor/github.com/slack-go/slack/security.go index dbe8fb2..8124c2c 100644 --- a/vendor/github.com/slack-go/slack/security.go +++ b/vendor/github.com/slack-go/slack/security.go @@ -20,6 +20,7 @@ const ( // SecretsVerifier contains the information needed to verify that the request comes from Slack type SecretsVerifier struct { + d Debug signature []byte hmac hash.Hash } @@ -29,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) @@ -41,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 } @@ -75,6 +80,11 @@ func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, return sv, err } +func (v *SecretsVerifier) WithDebug(d Debug) *SecretsVerifier { + v.d = d + return v +} + func (v *SecretsVerifier) Write(body []byte) (n int, err error) { return v.hmac.Write(body) } @@ -86,8 +96,10 @@ func (v SecretsVerifier) Ensure() error { if hmac.Equal(computed, v.signature) { return nil } - - return fmt.Errorf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed)) + 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)) } 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 d342a3e..45b9cd3 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" ) @@ -23,7 +25,9 @@ type httpClient interface { // ResponseMetadata holds pagination metadata type ResponseMetadata struct { - Cursor string `json:"next_cursor"` + Cursor string `json:"next_cursor"` + Messages []string `json:"messages"` + Warnings []string `json:"warnings"` } func (t *ResponseMetadata) initialize() *ResponseMetadata { @@ -42,23 +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"` + 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 - endpoint string - debug bool - log ilogger - httpclient httpClient + 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 @@ -85,18 +98,100 @@ 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 } +} + +// OptionConfigToken sets a configuration token for the client. +func OptionConfigToken(token string) func(*Client) { + return func(c *Client) { c.configToken = token } +} + +// OptionConfigRefreshToken sets a configuration refresh token for the client. +func OptionConfigRefreshToken(token string) func(*Client) { + return func(c *Client) { c.configRefreshToken = token } +} + +// 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 { @@ -120,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...)) } @@ -143,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, values url.Values, intf interface{}) error { - return getResource(ctx, api.httpclient, api.endpoint+path, 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/slackevents/action_events.go b/vendor/github.com/slack-go/slack/slackevents/action_events.go index c6016f1..e7e4ed1 100644 --- a/vendor/github.com/slack-go/slack/slackevents/action_events.go +++ b/vendor/github.com/slack-go/slack/slackevents/action_events.go @@ -6,18 +6,25 @@ import ( "github.com/slack-go/slack" ) +// Deprecated: MessageActionResponse is associated with [MessageAction] which cannot +// handle block_actions. Use [slack.InteractionCallback] instead. type MessageActionResponse struct { ResponseType string `json:"response_type"` ReplaceOriginal bool `json:"replace_original"` Text string `json:"text"` } +// Deprecated: MessageActionEntity is associated with [MessageAction] which cannot +// handle block_actions. Use [slack.InteractionCallback] instead. type MessageActionEntity struct { ID string `json:"id"` Domain string `json:"domain"` Name string `json:"name"` } +// Deprecated: MessageAction cannot represent block_actions payloads. Use +// [slack.InteractionCallback] instead, which handles all interaction types. +// See [slack.InteractionCallbackParse] for parsing from an HTTP request. type MessageAction struct { Type string `json:"type"` Actions []slack.AttachmentAction `json:"actions"` diff --git a/vendor/github.com/slack-go/slack/slackevents/inner_events.go b/vendor/github.com/slack-go/slack/slackevents/inner_events.go index 5524320..676a005 100644 --- a/vendor/github.com/slack-go/slack/slackevents/inner_events.go +++ b/vendor/github.com/slack-go/slack/slackevents/inner_events.go @@ -14,19 +14,68 @@ type EventsAPIInnerEvent struct { Data interface{} } +// AssistantThreadMessageEvent is an (inner) EventsAPI subscribable event. +type AssistantThreadStartedEvent struct { + Type string `json:"type"` + AssistantThread AssistantThread `json:"assistant_thread"` + EventTimestamp string `json:"event_ts"` +} + +// AssistantThreadChangedEvent is an (inner) EventsAPI subscribable event. +type AssistantThreadContextChangedEvent struct { + Type string `json:"type"` + AssistantThread AssistantThread `json:"assistant_thread"` + EventTimestamp string `json:"event_ts"` +} + +// AssistantThread is an object that represents a thread of messages between a user and an assistant. +type AssistantThread struct { + UserID string `json:"user_id"` + Context AssistantThreadContext `json:"context"` + ChannelID string `json:"channel_id"` + ThreadTimeStamp string `json:"thread_ts"` +} + +// AssistantThreadActionToken contains the action token for Data Access API queries +type AssistantThreadActionToken struct { + ActionToken string `json:"action_token"` +} + +// AssistantThreadContext is an object that represents the context of an assistant thread. +type AssistantThreadContext struct { + ChannelID string `json:"channel_id"` + TeamID string `json:"team_id"` + EnterpriseID string `json:"enterprise_id"` +} + // AppMentionEvent is an (inner) EventsAPI subscribable event. type AppMentionEvent struct { - Type string `json:"type"` - User string `json:"user"` - Text string `json:"text"` - TimeStamp string `json:"ts"` - ThreadTimeStamp string `json:"thread_ts"` - Channel string `json:"channel"` - EventTimeStamp json.Number `json:"event_ts"` + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + TimeStamp string `json:"ts"` + ThreadTimeStamp string `json:"thread_ts"` + Channel string `json:"channel"` + EventTimeStamp string `json:"event_ts"` // When Message comes from a channel that is shared between workspaces UserTeam string `json:"user_team,omitempty"` SourceTeam string `json:"source_team,omitempty"` + + // BotID is filled out when a bot triggers the app_mention event + BotID string `json:"bot_id,omitempty"` + + // Fields shared with message events + Blocks slack.Blocks `json:"blocks,omitempty"` + Attachments []slack.Attachment `json:"attachments,omitempty"` + Files []slack.File `json:"files,omitempty"` + Upload bool `json:"upload,omitempty"` + + // When the app is mentioned in the edited message + Edited *Edited `json:"edited,omitempty"` + + // AssistantThread contains action token for Data Access API queries when app is mentioned + AssistantThread *AssistantThreadActionToken `json:"assistant_thread,omitempty"` } // AppHomeOpenedEvent Your Slack app home was opened. @@ -34,9 +83,9 @@ type AppHomeOpenedEvent struct { Type string `json:"type"` User string `json:"user"` Channel string `json:"channel"` - EventTimeStamp json.Number `json:"event_ts"` + EventTimeStamp string `json:"event_ts"` Tab string `json:"tab"` - View slack.View `json:"view"` + View *slack.View `json:"view,omitempty"` } // AppUninstalledEvent Your Slack app was uninstalled. @@ -44,6 +93,166 @@ type AppUninstalledEvent struct { Type string `json:"type"` } +// ChannelCreatedEvent represents the Channel created event +type ChannelCreatedEvent struct { + Type string `json:"type"` + Channel ChannelCreatedInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelDeletedEvent represents the Channel deleted event +type ChannelDeletedEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelArchiveEvent represents the Channel archive event +type ChannelArchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelUnarchiveEvent represents the Channel unarchive event +type ChannelUnarchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelLeftEvent represents the Channel left event +type ChannelLeftEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelRenameEvent represents the Channel rename event +type ChannelRenameEvent struct { + Type string `json:"type"` + Channel ChannelRenameInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelIDChangedEvent represents the Channel identifier changed event +type ChannelIDChangedEvent struct { + Type string `json:"type"` + OldChannelID string `json:"old_channel_id"` + NewChannelID string `json:"new_channel_id"` + EventTimestamp string `json:"event_ts"` +} + +// ChannelCreatedInfo represents the information associated with the Channel created event +type ChannelCreatedInfo struct { + ID string `json:"id"` + IsChannel bool `json:"is_channel"` + Name string `json:"name"` + Created int `json:"created"` + Creator string `json:"creator"` +} + +// ChannelRenameInfo represents the information associated with the Channel rename event +type ChannelRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created int `json:"created"` +} + +// ChannelUnsharedEvent represents a channel has been unshared with an external workspace event +type ChannelUnsharedEvent struct { + Type string `json:"type"` + PreviouslyConnectedTeamID string `json:"previously_connected_team_id"` + Channel string `json:"channel"` + IsExtShared bool `json:"is_ext_shared"` + EventTimestamp string `json:"event_ts"` +} + +// GroupDeletedEvent represents the Group deleted event +type GroupDeletedEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupArchiveEvent represents the Group archive event +type GroupArchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupUnarchiveEvent represents the Group unarchive event +type GroupUnarchiveEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupLeftEvent represents the Group left event +type GroupLeftEvent struct { + Type string `json:"type"` + Channel string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupRenameEvent represents the Group rename event +type GroupRenameEvent struct { + Type string `json:"type"` + Channel GroupRenameInfo `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + +// GroupRenameInfo represents the information associated with the Group rename event +type GroupRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created int `json:"created"` +} + +// FileChangeEvent represents the information associated with the File change +// event. +type FileChangeEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File FileEventFile `json:"file"` +} + +// FileDeletedEvent represents the information associated with the File deleted +// event. +type FileDeletedEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + EventTimestamp string `json:"event_ts"` +} + +// FileSharedEvent represents the information associated with the File shared +// event. +type FileSharedEvent struct { + Type string `json:"type"` + ChannelID string `json:"channel_id"` + FileID string `json:"file_id"` + UserID string `json:"user_id"` + File FileEventFile `json:"file"` + EventTimestamp string `json:"event_ts"` +} + +// FileUnsharedEvent represents the information associated with the File +// unshared event. +type FileUnsharedEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File FileEventFile `json:"file"` +} + +// FileEventFile represents information on the specific file being shared in a +// file-related Slack event. +type FileEventFile struct { + ID string `json:"id"` +} + // GridMigrationFinishedEvent An enterprise grid migration has finished on this workspace. type GridMigrationFinishedEvent struct { Type string `json:"type"` @@ -58,66 +267,144 @@ type GridMigrationStartedEvent struct { // LinkSharedEvent A message was posted containing one or more links relevant to your application type LinkSharedEvent struct { - Type string `json:"type"` - User string `json:"user"` - TimeStamp string `json:"ts"` - Channel string `json:"channel"` - MessageTimeStamp json.Number `json:"message_ts"` - Links []sharedLinks `json:"links"` + Type string `json:"type"` + User string `json:"user"` + TimeStamp string `json:"ts"` + Channel string `json:"channel"` + // MessageTimeStamp can be both a numeric timestamp if the LinkSharedEvent corresponds to a sent + // message and (contrary to the field name) a uuid if the LinkSharedEvent is generated in the + // compose text area. + MessageTimeStamp string `json:"message_ts"` + ThreadTimeStamp string `json:"thread_ts"` + Links []SharedLinks `json:"links"` + EventTimestamp string `json:"event_ts"` } -type sharedLinks struct { +type SharedLinks struct { Domain string `json:"domain"` URL string `json:"url"` } +const ( + ChannelTypeChannel = "channel" // Public channel message + ChannelTypeGroup = "group" // Private channel message + ChannelTypeIM = "im" // Direct message + ChannelTypeMPIM = "mpim" // Multiparty direct message +) + // MessageEvent occurs when a variety of types of messages has been posted. // Parse ChannelType to see which // if ChannelType = "group", this is a private channel message // if ChannelType = "channel", this message was sent to a channel // if ChannelType = "im", this is a private message -// if ChannelType = "mim", A message was posted in a multiparty direct message channel -// TODO: Improve this so that it is not required to manually parse ChannelType +// if ChannelType = "mpim", A message was posted in a multiparty direct message channel type MessageEvent struct { // Basic Message Event - https://api.slack.com/events/message - Type string `json:"type"` - User string `json:"user"` - Text string `json:"text"` - ThreadTimeStamp string `json:"thread_ts"` - TimeStamp string `json:"ts"` - Channel string `json:"channel"` - ChannelType string `json:"channel_type"` - EventTimeStamp json.Number `json:"event_ts"` + ClientMsgID string `json:"client_msg_id"` + Type string `json:"type"` + User string `json:"user"` + Text string `json:"text"` + Blocks slack.Blocks `json:"blocks,omitempty"` + ThreadTimeStamp string `json:"thread_ts"` + TimeStamp string `json:"ts"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + EventTimeStamp string `json:"event_ts"` // When Message comes from a channel that is shared between workspaces UserTeam string `json:"user_team,omitempty"` SourceTeam string `json:"source_team,omitempty"` + // When we get a 'message' event with no subtype, i.e. telling us about a new + // message, the message information is stored at the top level. But when we get + // a 'message_changed' event, the message information is stored in + // the Message property. This is really hard to represent nicely in Go, so we use + // a custom JSON unmarshaller to populate the Message field in both cases. + Message *slack.Msg `json:"message,omitempty"` + // Root is set if the SubType is `thread_broadcast`. + Root *slack.Msg `json:"root,omitempty"` // Edited Message - Message *MessageEvent `json:"message,omitempty"` - PreviousMessage *MessageEvent `json:"previous_message,omitempty"` - Edited *Edited `json:"edited,omitempty"` + PreviousMessage *slack.Msg `json:"previous_message,omitempty"` + + // Deleted Message + DeletedTimeStamp string `json:"deleted_ts,omitempty"` // Message Subtypes SubType string `json:"subtype,omitempty"` // bot_message (https://api.slack.com/events/message/bot_message) - BotID string `json:"bot_id,omitempty"` - Username string `json:"username,omitempty"` - Icons *Icon `json:"icons,omitempty"` + BotID string `json:"bot_id,omitempty"` + Username string `json:"username,omitempty"` + Icons *Icon `json:"icons,omitempty"` + WorkflowID string `json:"workflow_id,omitempty"` + + // AssistantThread contains action token for Data Access API queries in message events + AssistantThread *AssistantThreadActionToken `json:"assistant_thread,omitempty"` + + // Huddle-related fields (subtype "huddle_thread") + Room *slack.HuddleRoom `json:"room,omitempty"` + NoNotifications bool `json:"no_notifications,omitempty"` + Permalink string `json:"permalink,omitempty"` +} + +func (e *MessageEvent) IsIM() bool { return e.ChannelType == ChannelTypeIM } +func (e *MessageEvent) IsChannel() bool { return e.ChannelType == ChannelTypeChannel } +func (e *MessageEvent) IsGroup() bool { return e.ChannelType == ChannelTypeGroup } +func (e *MessageEvent) IsMpIM() bool { return e.ChannelType == ChannelTypeMPIM } + +// UnmarshalJSON implements the json.Unmarshaler interface for MessageEvent. +// This custom unmarshaler handles both regular messages and message_changed events +// by normalizing the message data into the Message field. +func (e *MessageEvent) UnmarshalJSON(data []byte) error { + // First, unmarshal into an anonymous struct to avoid infinite recursion + // when calling json.Unmarshal on the MessageEvent type itself + type MessageEventAlias MessageEvent + alias := struct { + MessageEventAlias + }{} + + if err := json.Unmarshal(data, &alias.MessageEventAlias); err != nil { + return err + } - Upload bool `json:"upload"` - Files []File `json:"files"` + // Copy all fields from alias to the original struct + *e = MessageEvent(alias.MessageEventAlias) + + // Now check if there's no Message field (which would happen for regular messages) + if e.Message == nil { + // For regular messages, the message content is at the top level, + // so we need to unmarshal the data again into a slack.Msg + msg := &slack.Msg{} + if err := json.Unmarshal(data, msg); err != nil { + return err + } + + // Set the Message field to the unmarshaled msg + e.Message = msg + } + + return nil } -// MemberJoinedChannelEvent A member join a channel +// MemberJoinedChannelEvent A member joined a public or private channel type MemberJoinedChannelEvent struct { - Type string `json:"type"` - User string `json:"user"` - Channel string `json:"channel"` - ChannelType string `json:"channel_type"` - Team string `json:"team"` - Inviter string `json:"inviter"` + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` + Inviter string `json:"inviter"` + EventTimestamp string `json:"event_ts"` +} + +// MemberLeftChannelEvent A member left a public or private channel +type MemberLeftChannelEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` + ChannelType string `json:"channel_type"` + Team string `json:"team"` + EventTimestamp string `json:"event_ts"` } type pinEvent struct { @@ -155,10 +442,79 @@ type tokens struct { Bot []string `json:"bot"` } -// TokensRevokedEvent APP's API tokes are revoked - https://api.slack.com/events/tokens_revoked +// TeamJoinEvent A new member joined a workspace - https://api.slack.com/events/team_join +type TeamJoinEvent struct { + Type string `json:"type"` + User *slack.User `json:"user"` + EventTimestamp string `json:"event_ts"` +} + +// TokensRevokedEvent APP's API tokens are revoked - https://api.slack.com/events/tokens_revoked type TokensRevokedEvent struct { - Type string `json:"type"` - Tokens tokens `json:"tokens"` + Type string `json:"type"` + Tokens tokens `json:"tokens"` + EventTimestamp string `json:"event_ts"` +} + +// EmojiChangedEvent is the event of custom emoji has been added or changed +type EmojiChangedEvent struct { + Type string `json:"type"` + Subtype string `json:"subtype"` + EventTimeStamp string `json:"event_ts"` + + // filled out when custom emoji added + Name string `json:"name,omitempty"` + + // filled out when custom emoji removed + Names []string `json:"names,omitempty"` + + // filled out when custom emoji renamed + OldName string `json:"old_name,omitempty"` + NewName string `json:"new_name,omitempty"` + + // filled out when custom emoji added or renamed + Value string `json:"value,omitempty"` +} + +// MessageMetadataPostedEvent is sent, if a message with metadata is posted +type MessageMetadataPostedEvent struct { + Type string `json:"type"` + AppId string `json:"app_id"` + BotId string `json:"bot_id"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + ChannelId string `json:"channel_id"` + Metadata *slack.SlackMetadata `json:"metadata"` + MessageTimestamp string `json:"message_ts"` + EventTimestamp string `json:"event_ts"` +} + +// MessageMetadataUpdatedEvent is sent, if a message with metadata is deleted +type MessageMetadataUpdatedEvent struct { + Type string `json:"type"` + ChannelId string `json:"channel_id"` + EventTimestamp string `json:"event_ts"` + PreviousMetadata *slack.SlackMetadata `json:"previous_metadata"` + AppId string `json:"app_id"` + BotId string `json:"bot_id"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + MessageTimestamp string `json:"message_ts"` + Metadata *slack.SlackMetadata `json:"metadata"` +} + +// MessageMetadataDeletedEvent is sent, if a message with metadata is deleted +type MessageMetadataDeletedEvent struct { + Type string `json:"type"` + ChannelId string `json:"channel_id"` + EventTimestamp string `json:"event_ts"` + PreviousMetadata *slack.SlackMetadata `json:"previous_metadata"` + AppId string `json:"app_id"` + BotId string `json:"bot_id"` + UserId string `json:"user_id"` + TeamId string `json:"team_id"` + MessageTimestamp string `json:"message_ts"` + DeletedTimestamp string `json:"deleted_ts"` } // JSONTime exists so that we can have a String method converting the date @@ -194,6 +550,7 @@ type File struct { DisplayAsBot bool `json:"display_as_bot"` Username string `json:"username"` URLPrivate string `json:"url_private"` + FileAccess string `json:"file_access"` URLPrivateDownload string `json:"url_private_download"` Thumb64 string `json:"thumb_64"` Thumb80 string `json:"thumb_80"` @@ -262,50 +619,862 @@ func (e MessageEvent) IsEdited() bool { e.Message.Edited != nil } +// TeamAccessGrantedEvent is sent if access to teams was granted for your org-wide app. +type TeamAccessGrantedEvent struct { + Type string `json:"type"` + TeamIDs []string `json:"team_ids"` +} + +// TeamAccessRevokedEvent is sent if access to teams was revoked for your org-wide app. +type TeamAccessRevokedEvent struct { + Type string `json:"type"` + TeamIDs []string `json:"team_ids"` +} + +// UserProfileChangedEvent is sent if access to teams was revoked for your org-wide app. +type UserProfileChangedEvent struct { + User *slack.User `json:"user"` + CacheTs int `json:"cache_ts"` + Type string `json:"type"` + EventTs string `json:"event_ts"` +} + +// SharedChannelInviteApprovedEvent is sent if your invitation has been approved +type SharedChannelInviteApprovedEvent struct { + Type string `json:"type"` + Invite *SharedInvite `json:"invite"` + Channel *slack.Conversation `json:"channel"` + ApprovingTeamID string `json:"approving_team_id"` + TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"` + ApprovingUser *SlackEventUser `json:"approving_user"` + EventTs string `json:"event_ts"` +} + +// SharedChannelInviteAcceptedEvent is sent if external org accepts a Slack Connect channel invite +type SharedChannelInviteAcceptedEvent struct { + Type string `json:"type"` + ApprovalRequired bool `json:"approval_required"` + Invite *SharedInvite `json:"invite"` + Channel *SharedChannel `json:"channel"` + TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"` + AcceptingUser *SlackEventUser `json:"accepting_user"` + EventTs string `json:"event_ts"` + RequiresSponsorship bool `json:"requires_sponsorship,omitempty"` +} + +// SharedChannelInviteDeclinedEvent is sent if external or internal org declines the Slack Connect invite +type SharedChannelInviteDeclinedEvent struct { + Type string `json:"type"` + Invite *SharedInvite `json:"invite"` + Channel *SharedChannel `json:"channel"` + DecliningTeamID string `json:"declining_team_id"` + TeamsInChannel []*SlackEventTeam `json:"teams_in_channel"` + DecliningUser *SlackEventUser `json:"declining_user"` + EventTs string `json:"event_ts"` +} + +// SharedChannelInviteReceivedEvent is sent if a bot or app is invited to a Slack Connect channel +type SharedChannelInviteReceivedEvent struct { + Type string `json:"type"` + Invite *SharedInvite `json:"invite"` + Channel *SharedChannel `json:"channel"` + EventTs string `json:"event_ts"` +} + +// SlackEventTeam is a struct for teams in ShareChannel events +type SlackEventTeam struct { + ID string `json:"id"` + Name string `json:"name"` + Icon *SlackEventIcon `json:"icon,omitempty"` + AvatarBaseURL string `json:"avatar_base_url,omitempty"` + IsVerified bool `json:"is_verified"` + Domain string `json:"domain"` + DateCreated int `json:"date_created"` + RequiresSponsorship bool `json:"requires_sponsorship,omitempty"` + // TeamID string `json:"team_id,omitempty"` +} + +// SlackEventIcon is a struct for icons in ShareChannel events +type SlackEventIcon struct { + ImageDefault bool `json:"image_default,omitempty"` + Image34 string `json:"image_34,omitempty"` + Image44 string `json:"image_44,omitempty"` + Image68 string `json:"image_68,omitempty"` + Image88 string `json:"image_88,omitempty"` + Image102 string `json:"image_102,omitempty"` + Image132 string `json:"image_132,omitempty"` + Image230 string `json:"image_230,omitempty"` +} + +// SlackEventUser is a struct for users in ShareChannel events +type SlackEventUser struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + Name string `json:"name"` + Updated int `json:"updated,omitempty"` + Profile *slack.UserProfile `json:"profile,omitempty"` + WhoCanShareContactCard string `json:"who_can_share_contact_card,omitempty"` +} + +// SharedChannel is a struct for shared channels in ShareChannel events +type SharedChannel struct { + ID string `json:"id"` + IsPrivate bool `json:"is_private"` + IsIm bool `json:"is_im"` + Name string `json:"name,omitempty"` +} + +// SharedInvite is a struct for shared invites in ShareChannel events +type SharedInvite struct { + ID string `json:"id"` + DateCreated int `json:"date_created"` + DateInvalid int `json:"date_invalid"` + InvitingTeam *SlackEventTeam `json:"inviting_team,omitempty"` + InvitingUser *SlackEventUser `json:"inviting_user,omitempty"` + RecipientEmail string `json:"recipient_email,omitempty"` + RecipientUserID string `json:"recipient_user_id,omitempty"` + IsSponsored bool `json:"is_sponsored,omitempty"` + IsExternalLimited bool `json:"is_external_limited,omitempty"` +} + +type ChannelHistoryChangedEvent struct { + Type string `json:"type"` + Latest string `json:"latest"` + Ts string `json:"ts"` + EventTs string `json:"event_ts"` +} + +type CommandsChangedEvent struct { + Type string `json:"type"` + EventTs string `json:"event_ts"` +} + +type DndUpdatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + DndStatus struct { + DndEnabled bool `json:"dnd_enabled"` + NextDndStartTs int64 `json:"next_dnd_start_ts"` + NextDndEndTs int64 `json:"next_dnd_end_ts"` + SnoozeEnabled bool `json:"snooze_enabled"` + SnoozeEndtime int64 `json:"snooze_endtime"` + } `json:"dnd_status"` +} + +type DndUpdatedUserEvent struct { + Type string `json:"type"` + User string `json:"user"` + DndStatus struct { + DndEnabled bool `json:"dnd_enabled"` + NextDndStartTs int64 `json:"next_dnd_start_ts"` + NextDndEndTs int64 `json:"next_dnd_end_ts"` + } `json:"dnd_status"` +} + +type EmailDomainChangedEvent struct { + Type string `json:"type"` + EmailDomain string `json:"email_domain"` + EventTs string `json:"event_ts"` +} + +type GroupCloseEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type GroupHistoryChangedEvent struct { + Type string `json:"type"` + Latest string `json:"latest"` + Ts string `json:"ts"` + EventTs string `json:"event_ts"` +} + +type GroupOpenEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type ImCloseEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type ImCreatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel struct { + ID string `json:"id"` + } `json:"channel"` +} + +type ImHistoryChangedEvent struct { + Type string `json:"type"` + Latest string `json:"latest"` + Ts string `json:"ts"` + EventTs string `json:"event_ts"` +} + +type ImOpenEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +type SubTeam struct { + ID string `json:"id"` + TeamID string `json:"team_id"` + IsUsergroup bool `json:"is_usergroup"` + Name string `json:"name"` + Description string `json:"description"` + Handle string `json:"handle"` + IsExternal bool `json:"is_external"` + DateCreate int64 `json:"date_create"` + DateUpdate int64 `json:"date_update"` + DateDelete int64 `json:"date_delete"` + AutoType string `json:"auto_type"` + CreatedBy string `json:"created_by"` + UpdatedBy string `json:"updated_by"` + DeletedBy string `json:"deleted_by"` + Prefs struct { + Channels []string `json:"channels"` + Groups []string `json:"groups"` + } `json:"prefs"` + Users []string `json:"users"` + UserCount int `json:"user_count"` +} + +type SubteamCreatedEvent struct { + Type string `json:"type"` + Subteam SubTeam `json:"subteam"` +} + +type SubteamMembersChangedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` + TeamID string `json:"team_id"` + DatePreviousUpdate int `json:"date_previous_update"` + DateUpdate int64 `json:"date_update"` + AddedUsers []string `json:"added_users"` + AddedUsersCount int `json:"added_users_count"` + RemovedUsers []string `json:"removed_users"` + RemovedUsersCount int `json:"removed_users_count"` +} + +type SubteamSelfAddedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` +} + +type SubteamSelfRemovedEvent struct { + Type string `json:"type"` + SubteamID string `json:"subteam_id"` +} + +type SubteamUpdatedEvent struct { + Type string `json:"type"` + Subteam SubTeam `json:"subteam"` +} + +type TeamDomainChangeEvent struct { + Type string `json:"type"` + URL string `json:"url"` + Domain string `json:"domain"` + TeamID string `json:"team_id"` +} + +type TeamRenameEvent struct { + Type string `json:"type"` + Name string `json:"name"` + TeamID string `json:"team_id"` +} + +type UserChangeEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +type AppDeletedEvent struct { + Type string `json:"type"` + AppID string `json:"app_id"` + AppName string `json:"app_name"` + AppOwnerID string `json:"app_owner_id"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EventTs string `json:"event_ts"` +} + +type AppInstalledEvent struct { + Type string `json:"type"` + AppID string `json:"app_id"` + AppName string `json:"app_name"` + AppOwnerID string `json:"app_owner_id"` + UserID string `json:"user_id"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EventTs string `json:"event_ts"` +} + +type AppRequestedEvent struct { + Type string `json:"type"` + AppRequest struct { + ID string `json:"id"` + App struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + HelpURL string `json:"help_url"` + PrivacyPolicyURL string `json:"privacy_policy_url"` + AppHomepageURL string `json:"app_homepage_url"` + AppDirectoryURL string `json:"app_directory_url"` + IsAppDirectoryApproved bool `json:"is_app_directory_approved"` + IsInternal bool `json:"is_internal"` + AdditionalInfo string `json:"additional_info"` + } `json:"app"` + PreviousResolution struct { + Status string `json:"status"` + Scopes []struct { + Name string `json:"name"` + Description string `json:"description"` + IsSensitive bool `json:"is_sensitive"` + TokenType string `json:"token_type"` + } `json:"scopes"` + } `json:"previous_resolution"` + User struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + } `json:"user"` + Team struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + } `json:"team"` + Enterprise interface{} `json:"enterprise"` + Scopes []struct { + Name string `json:"name"` + Description string `json:"description"` + IsSensitive bool `json:"is_sensitive"` + TokenType string `json:"token_type"` + } `json:"scopes"` + Message string `json:"message"` + } `json:"app_request"` +} + +type AppUninstalledTeamEvent struct { + Type string `json:"type"` + AppID string `json:"app_id"` + AppName string `json:"app_name"` + AppOwnerID string `json:"app_owner_id"` + UserID string `json:"user_id"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EventTs string `json:"event_ts"` +} + +type CallRejectedEvent struct { + Token string `json:"token"` + TeamID string `json:"team_id"` + APIAppID string `json:"api_app_id"` + Event struct { + Type string `json:"type"` + CallID string `json:"call_id"` + UserID string `json:"user_id"` + ChannelID string `json:"channel_id"` + ExternalUniqueID string `json:"external_unique_id"` + } `json:"event"` + Type string `json:"type"` + EventID string `json:"event_id"` + AuthedUsers []string `json:"authed_users"` +} + +type ChannelSharedEvent struct { + Type string `json:"type"` + ConnectedTeamID string `json:"connected_team_id"` + Channel string `json:"channel"` + EventTs string `json:"event_ts"` +} + +type FileCreatedEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File struct { + ID string `json:"id"` + } `json:"file"` +} + +type FilePublicEvent struct { + Type string `json:"type"` + FileID string `json:"file_id"` + File struct { + ID string `json:"id"` + } `json:"file"` +} + +type FunctionExecutedEvent struct { + Type string `json:"type"` + Function struct { + ID string `json:"id"` + CallbackID string `json:"callback_id"` + Title string `json:"title"` + Description string `json:"description"` + Type string `json:"type"` + InputParameters []struct { + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + Title string `json:"title"` + IsRequired bool `json:"is_required"` + } `json:"input_parameters"` + OutputParameters []struct { + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + Title string `json:"title"` + IsRequired bool `json:"is_required"` + } `json:"output_parameters"` + AppID string `json:"app_id"` + DateCreated int64 `json:"date_created"` + DateUpdated int64 `json:"date_updated"` + DateDeleted int64 `json:"date_deleted"` + } `json:"function"` + Inputs map[string]interface{} `json:"inputs"` + FunctionExecutionID string `json:"function_execution_id"` + WorkflowExecutionID string `json:"workflow_execution_id"` + EventTs string `json:"event_ts"` + BotAccessToken string `json:"bot_access_token"` +} + +type InviteRequestedEvent struct { + Type string `json:"type"` + InviteRequest struct { + ID string `json:"id"` + Email string `json:"email"` + DateCreated int64 `json:"date_created"` + RequesterIDs []string `json:"requester_ids"` + ChannelIDs []string `json:"channel_ids"` + InviteType string `json:"invite_type"` + RealName string `json:"real_name"` + DateExpire int64 `json:"date_expire"` + RequestReason string `json:"request_reason"` + Team struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` + } `json:"team"` + } `json:"invite_request"` +} + +type StarAddedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item struct { + } `json:"item"` + EventTS string `json:"event_ts"` +} + +type StarRemovedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item struct { + } `json:"item"` + EventTS string `json:"event_ts"` +} + +type UserHuddleChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +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"` + TZLabel string `json:"tz_label"` + TZOffset int `json:"tz_offset"` + Profile Profile `json:"profile"` + 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"` + IsBot bool `json:"is_bot"` + IsAppUser bool `json:"is_app_user"` + Updated int64 `json:"updated"` + IsEmailConfirmed bool `json:"is_email_confirmed"` + WhoCanShareContactCard string `json:"who_can_share_contact_card"` + Locale string `json:"locale"` +} + +type Profile struct { + Title string `json:"title"` + Phone string `json:"phone"` + Skype string `json:"skype"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + DisplayName string `json:"display_name"` + DisplayNameNormalized string `json:"display_name_normalized"` + Fields map[string]interface{} `json:"fields"` + StatusText string `json:"status_text"` + StatusEmoji string `json:"status_emoji"` + StatusEmojiDisplayInfo []interface{} `json:"status_emoji_display_info"` + StatusExpiration int `json:"status_expiration"` + AvatarHash string `json:"avatar_hash"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + 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"` + StatusTextCanonical string `json:"status_text_canonical"` + Team string `json:"team"` +} + +type UserStatusChangedEvent struct { + Type string `json:"type"` + User User `json:"user"` + CacheTS int64 `json:"cache_ts"` + EventTS string `json:"event_ts"` +} + +// EntityDetailsRequestedEvent is sent when entity details are requested +// This event is fired when a user clicks on a Work Object link and Slack requests +// details about the entity from your app. +type EntityDetailsRequestedEvent struct { + Type string `json:"type"` + User string `json:"user"` + ExternalRef EntityDetailsRequestedExternalRef `json:"external_ref"` + EntityURL string `json:"entity_url"` + Link EntityDetailsRequestedLink `json:"link"` + AppUnfurlURL string `json:"app_unfurl_url"` + EventTS string `json:"event_ts"` + TriggerID string `json:"trigger_id"` + UserLocale string `json:"user_locale"` + Channel string `json:"channel,omitempty"` + MessageTs string `json:"message_ts,omitempty"` + ThreadTs string `json:"thread_ts,omitempty"` +} + +// EntityDetailsRequestedExternalRef represents the external reference in entity_details_requested event +type EntityDetailsRequestedExternalRef struct { + ID string `json:"id"` + Type string `json:"type,omitempty"` +} + +// EntityDetailsRequestedLink represents the link information in entity_details_requested event +type EntityDetailsRequestedLink struct { + URL string `json:"url"` + Domain string `json:"domain"` +} + +type Actor struct { + ID string `json:"id"` + Name string `json:"name"` + IsBot bool `json:"is_bot"` + TeamID string `json:"team_id"` + Timezone string `json:"timezone"` + RealName string `json:"real_name"` + DisplayName string `json:"display_name"` +} + +type TargetUser struct { + Email string `json:"email"` + InviteID string `json:"invite_id"` +} + +type TeamIcon struct { + Image34 string `json:"image_34"` + ImageDefault bool `json:"image_default"` +} + +type Team struct { + ID string `json:"id"` + Icon TeamIcon `json:"icon"` + Name string `json:"name"` + Domain string `json:"domain"` + IsVerified bool `json:"is_verified"` + DateCreated int64 `json:"date_created"` + AvatarBaseURL string `json:"avatar_base_url"` + RequiresSponsorship bool `json:"requires_sponsorship"` +} + +type SharedChannelInviteRequestedEvent struct { + Actor Actor `json:"actor"` + ChannelID string `json:"channel_id"` + EventType string `json:"event_type"` + ChannelName string `json:"channel_name"` + ChannelType string `json:"channel_type"` + TargetUsers []TargetUser `json:"target_users"` + TeamsInChannel []Team `json:"teams_in_channel"` + IsExternalLimited bool `json:"is_external_limited"` + ChannelDateCreated int64 `json:"channel_date_created"` + ChannelMessageLatestCounted int64 `json:"channel_message_latest_counted_timestamp"` +} + +type EventsAPIType string + const ( - // AppMention is an Events API subscribable event - AppMention = "app_mention" + // AppDeleted is an event when an app is deleted from a workspace + AppDeleted = EventsAPIType("app_deleted") // AppHomeOpened Your Slack app home was opened - AppHomeOpened = "app_home_opened" + AppHomeOpened = EventsAPIType("app_home_opened") + // AppInstalled is an event when an app is installed to a workspace + AppInstalled = EventsAPIType("app_installed") + // AppMention is an Events API subscribable event + AppMention = EventsAPIType("app_mention") + // AppRequested is an event when a user requests to install an app to a workspace + AppRequested = EventsAPIType("app_requested") // AppUninstalled Your Slack app was uninstalled. - AppUninstalled = "app_uninstalled" + AppUninstalled = EventsAPIType("app_uninstalled") + // AppUninstalledTeam is an event when an app is uninstalled from a team + AppUninstalledTeam = EventsAPIType("app_uninstalled_team") + // AssistantThreadContextChanged Your Slack AI Assistant has changed the context of a thread + AssistantThreadContextChanged = EventsAPIType("assistant_thread_context_changed") + // AssistantThreadStarted Your Slack AI Assistant has started a new thread + AssistantThreadStarted = EventsAPIType("assistant_thread_started") + // CallRejected is an event when a Slack call is rejected + CallRejected = EventsAPIType("call_rejected") + // ChannelArchive is sent when a channel is archived. + ChannelArchive = EventsAPIType("channel_archive") + // ChannelCreated is sent when a new channel is created. + ChannelCreated = EventsAPIType("channel_created") + // ChannelDeleted is sent when a channel is deleted. + ChannelDeleted = EventsAPIType("channel_deleted") + // ChannelHistoryChanged The history of a channel changed + ChannelHistoryChanged = EventsAPIType("channel_history_changed") + // ChannelIDChanged is sent when a channel identifier is changed. + ChannelIDChanged = EventsAPIType("channel_id_changed") + // ChannelLeft is sent when a channel is left. + ChannelLeft = EventsAPIType("channel_left") + // ChannelRename is sent when a channel is rename. + ChannelRename = EventsAPIType("channel_rename") + // ChannelShared is an event when a channel is shared with another workspace + ChannelShared = EventsAPIType("channel_shared") + // ChannelUnarchive is sent when a channel is unarchived. + ChannelUnarchive = EventsAPIType("channel_unarchive") + // ChannelUnshared is sent when a channel is unshared. + ChannelUnshared = EventsAPIType("channel_unshared") + // CommandsChanged A command was changed + CommandsChanged = EventsAPIType("commands_changed") + // DndUpdated Do Not Disturb settings were updated + DndUpdated = EventsAPIType("dnd_updated") + // DndUpdatedUser Do Not Disturb settings for a user were updated + DndUpdatedUser = EventsAPIType("dnd_updated_user") + // EmailDomainChanged The email domain changed + EmailDomainChanged = EventsAPIType("email_domain_changed") + // EmojiChanged A custom emoji has been added or changed + EmojiChanged = EventsAPIType("emoji_changed") + // FileChange is sent when a file is changed. + FileChange = EventsAPIType("file_change") + // FileCreated is an event when a file is created in a workspace + FileCreated = EventsAPIType("file_created") + // FileDeleted is sent when a file is deleted. + FileDeleted = EventsAPIType("file_deleted") + // FilePublic is an event when a file is made public in a workspace + FilePublic = EventsAPIType("file_public") + // FileShared is sent when a file is shared. + FileShared = EventsAPIType("file_shared") + // FileUnshared is sent when a file is unshared. + FileUnshared = EventsAPIType("file_unshared") + // FunctionExecuted is an event when a Slack function is executed + FunctionExecuted = EventsAPIType("function_executed") // GridMigrationFinished An enterprise grid migration has finished on this workspace. - GridMigrationFinished = "grid_migration_finished" + GridMigrationFinished = EventsAPIType("grid_migration_finished") // GridMigrationStarted An enterprise grid migration has started on this workspace. - GridMigrationStarted = "grid_migration_started" + GridMigrationStarted = EventsAPIType("grid_migration_started") + // GroupArchive is sent when a group is archived. + GroupArchive = EventsAPIType("group_archive") + // GroupClose A group was closed + GroupClose = EventsAPIType("group_close") + // GroupDeleted is sent when a group is deleted. + GroupDeleted = EventsAPIType("group_deleted") + // GroupHistoryChanged The history of a group changed + GroupHistoryChanged = EventsAPIType("group_history_changed") + // GroupLeft is sent when a group is left. + GroupLeft = EventsAPIType("group_left") + // GroupOpen A group was opened + GroupOpen = EventsAPIType("group_open") + // GroupRename is sent when a group is renamed. + GroupRename = EventsAPIType("group_rename") + // GroupUnarchive is sent when a group is unarchived. + GroupUnarchive = EventsAPIType("group_unarchive") + // ImClose An instant message channel was closed + ImClose = EventsAPIType("im_close") + // ImCreated An instant message channel was created + ImCreated = EventsAPIType("im_created") + // ImHistoryChanged The history of an instant message channel changed + ImHistoryChanged = EventsAPIType("im_history_changed") + // ImOpen An instant message channel was opened + ImOpen = EventsAPIType("im_open") + // InviteRequested is an event when a user requests an invite to a workspace + InviteRequested = EventsAPIType("invite_requested") // LinkShared A message was posted containing one or more links relevant to your application - LinkShared = "link_shared" + LinkShared = EventsAPIType("link_shared") + // MemberJoinedChannel is sent if a member joined a channel. + MemberJoinedChannel = EventsAPIType("member_joined_channel") + // MemberLeftChannel is sent if a member left a channel. + MemberLeftChannel = EventsAPIType("member_left_channel") // Message A message was posted to a channel, private channel (group), im, or mim - Message = "message" - // Member Joined Channel - MemberJoinedChannel = "member_joined_channel" + Message = EventsAPIType("message") + // MessageMetadataDeleted A message with metadata was deleted + MessageMetadataDeleted = EventsAPIType("message_metadata_deleted") + // MessageMetadataPosted A message with metadata was posted + MessageMetadataPosted = EventsAPIType("message_metadata_posted") + // MessageMetadataUpdated A message with metadata was updated + MessageMetadataUpdated = EventsAPIType("message_metadata_updated") // PinAdded An item was pinned to a channel - PinAdded = "pin_added" + PinAdded = EventsAPIType("pin_added") // PinRemoved An item was unpinned from a channel - PinRemoved = "pin_removed" + PinRemoved = EventsAPIType("pin_removed") // ReactionAdded An reaction was added to a message - ReactionAdded = "reaction_added" + ReactionAdded = EventsAPIType("reaction_added") // ReactionRemoved An reaction was removed from a message - ReactionRemoved = "reaction_removed" + ReactionRemoved = EventsAPIType("reaction_removed") + // SharedChannelInviteAccepted Slack connect channel invite accepted by an end user + SharedChannelInviteAccepted = EventsAPIType("shared_channel_invite_accepted") + // SharedChannelInviteApproved Slack connect channel invite approved + SharedChannelInviteApproved = EventsAPIType("shared_channel_invite_approved") + // SharedChannelInviteDeclined Slack connect channel invite declined + SharedChannelInviteDeclined = EventsAPIType("shared_channel_invite_declined") + // SharedChannelInviteReceived Slack connect app or bot invite received + SharedChannelInviteReceived = EventsAPIType("shared_channel_invite_received") + // SharedChannelInviteRequested is an event when an invitation to share a channel is requested + SharedChannelInviteRequested = EventsAPIType("shared_channel_invite_requested") + // StarAdded is an event when a star is added to a message or file + StarAdded = EventsAPIType("star_added") + // StarRemoved is an event when a star is removed from a message or file + StarRemoved = EventsAPIType("star_removed") + // SubteamCreated A subteam was created + SubteamCreated = EventsAPIType("subteam_created") + // SubteamMembersChanged The members of a subteam changed + SubteamMembersChanged = EventsAPIType("subteam_members_changed") + // SubteamSelfAdded The current user was added to a subteam + SubteamSelfAdded = EventsAPIType("subteam_self_added") + // SubteamSelfRemoved The current user was removed from a subteam + SubteamSelfRemoved = EventsAPIType("subteam_self_removed") + // SubteamUpdated A subteam was updated + SubteamUpdated = EventsAPIType("subteam_updated") + // TeamAccessGranted is sent if access to teams was granted for your org-wide app. + TeamAccessGranted = EventsAPIType("team_access_granted") + // TeamAccessRevoked is sent if access to teams was revoked for your org-wide app. + TeamAccessRevoked = EventsAPIType("team_access_revoked") + // TeamDomainChange The team's domain changed + TeamDomainChange = EventsAPIType("team_domain_change") + // TeamJoin A new user joined the workspace + TeamJoin = EventsAPIType("team_join") + // TeamRename The team was renamed + TeamRename = EventsAPIType("team_rename") // TokensRevoked APP's API tokes are revoked - TokensRevoked = "tokens_revoked" + TokensRevoked = EventsAPIType("tokens_revoked") + // UserChange A user object has changed + UserChange = EventsAPIType("user_change") + // UserHuddleChanged is an event when a user's huddle status changes + UserHuddleChanged = EventsAPIType("user_huddle_changed") + // UserProfileChanged is sent if a user's profile information has changed. + UserProfileChanged = EventsAPIType("user_profile_changed") + // UserStatusChanged is an event when a user's status changes + UserStatusChanged = EventsAPIType("user_status_changed") + // WorkflowStepExecute Happens, if a workflow step of your app is invoked + WorkflowStepExecute = EventsAPIType("workflow_step_execute") + // EntityDetailsRequested is sent when entity details are requested + EntityDetailsRequested = EventsAPIType("entity_details_requested") ) // EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct // implementations. The structs should be instances of the unmarshalling // target for the matching event type. -var EventsAPIInnerEventMapping = map[string]interface{}{ - AppMention: AppMentionEvent{}, - AppHomeOpened: AppHomeOpenedEvent{}, - AppUninstalled: AppUninstalledEvent{}, - GridMigrationFinished: GridMigrationFinishedEvent{}, - GridMigrationStarted: GridMigrationStartedEvent{}, - LinkShared: LinkSharedEvent{}, - Message: MessageEvent{}, - MemberJoinedChannel: MemberJoinedChannelEvent{}, - PinAdded: PinAddedEvent{}, - PinRemoved: PinRemovedEvent{}, - ReactionAdded: ReactionAddedEvent{}, - ReactionRemoved: ReactionRemovedEvent{}, - TokensRevoked: TokensRevokedEvent{}, +var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{ + AppDeleted: AppDeletedEvent{}, + AppHomeOpened: AppHomeOpenedEvent{}, + AppInstalled: AppInstalledEvent{}, + AppMention: AppMentionEvent{}, + AppRequested: AppRequestedEvent{}, + AppUninstalled: AppUninstalledEvent{}, + AppUninstalledTeam: AppUninstalledTeamEvent{}, + AssistantThreadContextChanged: AssistantThreadContextChangedEvent{}, + AssistantThreadStarted: AssistantThreadStartedEvent{}, + CallRejected: CallRejectedEvent{}, + ChannelArchive: ChannelArchiveEvent{}, + ChannelCreated: ChannelCreatedEvent{}, + ChannelDeleted: ChannelDeletedEvent{}, + ChannelHistoryChanged: ChannelHistoryChangedEvent{}, + ChannelIDChanged: ChannelIDChangedEvent{}, + ChannelLeft: ChannelLeftEvent{}, + ChannelRename: ChannelRenameEvent{}, + ChannelShared: ChannelSharedEvent{}, + ChannelUnarchive: ChannelUnarchiveEvent{}, + ChannelUnshared: ChannelUnsharedEvent{}, + CommandsChanged: CommandsChangedEvent{}, + DndUpdated: DndUpdatedEvent{}, + DndUpdatedUser: DndUpdatedUserEvent{}, + EmailDomainChanged: EmailDomainChangedEvent{}, + EmojiChanged: EmojiChangedEvent{}, + FileChange: FileChangeEvent{}, + FileCreated: FileCreatedEvent{}, + FileDeleted: FileDeletedEvent{}, + FilePublic: FilePublicEvent{}, + FileShared: FileSharedEvent{}, + FileUnshared: FileUnsharedEvent{}, + FunctionExecuted: FunctionExecutedEvent{}, + GridMigrationFinished: GridMigrationFinishedEvent{}, + GridMigrationStarted: GridMigrationStartedEvent{}, + GroupArchive: GroupArchiveEvent{}, + GroupClose: GroupCloseEvent{}, + GroupDeleted: GroupDeletedEvent{}, + GroupHistoryChanged: GroupHistoryChangedEvent{}, + GroupLeft: GroupLeftEvent{}, + GroupOpen: GroupOpenEvent{}, + GroupRename: GroupRenameEvent{}, + GroupUnarchive: GroupUnarchiveEvent{}, + ImClose: ImCloseEvent{}, + ImCreated: ImCreatedEvent{}, + ImHistoryChanged: ImHistoryChangedEvent{}, + ImOpen: ImOpenEvent{}, + InviteRequested: InviteRequestedEvent{}, + LinkShared: LinkSharedEvent{}, + MemberJoinedChannel: MemberJoinedChannelEvent{}, + MemberLeftChannel: MemberLeftChannelEvent{}, + Message: MessageEvent{}, + MessageMetadataDeleted: MessageMetadataDeletedEvent{}, + MessageMetadataPosted: MessageMetadataPostedEvent{}, + MessageMetadataUpdated: MessageMetadataUpdatedEvent{}, + PinAdded: PinAddedEvent{}, + PinRemoved: PinRemovedEvent{}, + ReactionAdded: ReactionAddedEvent{}, + ReactionRemoved: ReactionRemovedEvent{}, + SharedChannelInviteAccepted: SharedChannelInviteAcceptedEvent{}, + SharedChannelInviteApproved: SharedChannelInviteApprovedEvent{}, + SharedChannelInviteDeclined: SharedChannelInviteDeclinedEvent{}, + SharedChannelInviteReceived: SharedChannelInviteReceivedEvent{}, + SharedChannelInviteRequested: SharedChannelInviteRequestedEvent{}, + StarAdded: StarAddedEvent{}, + StarRemoved: StarRemovedEvent{}, + SubteamCreated: SubteamCreatedEvent{}, + SubteamMembersChanged: SubteamMembersChangedEvent{}, + SubteamSelfAdded: SubteamSelfAddedEvent{}, + SubteamSelfRemoved: SubteamSelfRemovedEvent{}, + SubteamUpdated: SubteamUpdatedEvent{}, + TeamAccessGranted: TeamAccessGrantedEvent{}, + TeamAccessRevoked: TeamAccessRevokedEvent{}, + TeamDomainChange: TeamDomainChangeEvent{}, + TeamJoin: TeamJoinEvent{}, + TeamRename: TeamRenameEvent{}, + TokensRevoked: TokensRevokedEvent{}, + UserChange: UserChangeEvent{}, + UserHuddleChanged: UserHuddleChangedEvent{}, + UserProfileChanged: UserProfileChangedEvent{}, + UserStatusChanged: UserStatusChangedEvent{}, + EntityDetailsRequested: EntityDetailsRequestedEvent{}, } diff --git a/vendor/github.com/slack-go/slack/slackevents/outer_events.go b/vendor/github.com/slack-go/slack/slackevents/outer_events.go index b5e9526..bcc85f3 100644 --- a/vendor/github.com/slack-go/slack/slackevents/outer_events.go +++ b/vendor/github.com/slack-go/slack/slackevents/outer_events.go @@ -8,12 +8,13 @@ import ( // EventsAPIEvent is the base EventsAPIEvent type EventsAPIEvent struct { - Token string `json:"token"` - TeamID string `json:"team_id"` - Type string `json:"type"` - APIAppID string `json:"api_app_id"` - Data interface{} - InnerEvent EventsAPIInnerEvent + Token string `json:"token"` + TeamID string `json:"team_id"` + Type string `json:"type"` + APIAppID string `json:"api_app_id"` + EnterpriseID string `json:"enterprise_id"` + Data interface{} + InnerEvent EventsAPIInnerEvent } // EventsAPIURLVerificationEvent received when configuring a EventsAPI driven app @@ -30,15 +31,17 @@ type ChallengeResponse struct { // EventsAPICallbackEvent is the main (outer) EventsAPI event. type EventsAPICallbackEvent struct { - Type string `json:"type"` - Token string `json:"token"` - TeamID string `json:"team_id"` - APIAppID string `json:"api_app_id"` - InnerEvent *json.RawMessage `json:"event"` - AuthedUsers []string `json:"authed_users"` - AuthedTeams []string `json:"authed_teams"` - EventID string `json:"event_id"` - EventTime int `json:"event_time"` + Type string `json:"type"` + Token string `json:"token"` + TeamID string `json:"team_id"` + APIAppID string `json:"api_app_id"` + EnterpriseID string `json:"enterprise_id"` + InnerEvent *json.RawMessage `json:"event"` + AuthedUsers []string `json:"authed_users"` + AuthedTeams []string `json:"authed_teams"` + EventID string `json:"event_id"` + EventTime int `json:"event_time"` + EventContext string `json:"event_context"` } // EventsAPIAppRateLimited indicates your app's event subscriptions are being rate limited @@ -59,7 +62,7 @@ const ( AppRateLimited = "app_rate_limited" ) -// EventsAPIEventMap maps OUTTER Event API events to their corresponding struct +// EventsAPIEventMap maps OUTER Event API events to their corresponding struct // implementations. The structs should be instances of the unmarshalling // target for the matching event type. var EventsAPIEventMap = map[string]interface{}{ diff --git a/vendor/github.com/slack-go/slack/slackevents/parsers.go b/vendor/github.com/slack-go/slack/slackevents/parsers.go index 07f296a..80308ea 100644 --- a/vendor/github.com/slack-go/slack/slackevents/parsers.go +++ b/vendor/github.com/slack-go/slack/slackevents/parsers.go @@ -10,23 +10,16 @@ import ( "github.com/slack-go/slack" ) -// eventsMap checks both slack.EventsMapping and -// and slackevents.EventsAPIInnerEventMapping. If the event -// exists, returns the the unmarshalled struct instance of -// target for the matching event type. -// TODO: Consider moving all events into its own package? +// eventsMap checks both slackevents.EventsAPIInnerEventMapping and slack.EventMapping +// (RTM). EventsAPI mapping is checked first because both define a "message" type, and +// EventsAPI's MessageEvent is the correct choice for Events API payloads. func eventsMap(t string) (interface{}, bool) { - // Must parse EventsAPI FIRST as both RTM and EventsAPI - // have a type: "Message" event. - // TODO: Handle these cases more explicitly. - v, exists := EventsAPIInnerEventMapping[t] + // EventsAPI mapping takes precedence over RTM mapping. + v, exists := EventsAPIInnerEventMapping[EventsAPIType(t)] if exists { return v, exists } v, exists = slack.EventMapping[t] - if exists { - return v, exists - } return v, exists } @@ -39,6 +32,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -52,6 +46,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -61,6 +56,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { e.TeamID, e.Type, e.APIAppID, + e.EnterpriseID, cbEvent, EventsAPIInnerEvent{}, }, nil @@ -73,6 +69,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -82,6 +79,7 @@ func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) { e.TeamID, e.Type, e.APIAppID, + e.EnterpriseID, urlVE, EventsAPIInnerEvent{}, }, nil @@ -97,6 +95,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, "unmarshalling_error", e.APIAppID, + e.EnterpriseID, &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -108,9 +107,10 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, iE.Type, e.APIAppID, + e.EnterpriseID, nil, EventsAPIInnerEvent{}, - }, fmt.Errorf("Inner Event does not exist! %s", iE.Type) + }, fmt.Errorf("inner Event does not exist! %s", iE.Type) } t := reflect.TypeOf(v) recvEvent := reflect.New(t).Interface() @@ -121,6 +121,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, "unmarshalling_error", e.APIAppID, + e.EnterpriseID, &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -130,6 +131,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) { e.TeamID, e.Type, e.APIAppID, + e.EnterpriseID, e, EventsAPIInnerEvent{iE.Type, recvEvent}, }, nil @@ -167,7 +169,7 @@ func (c TokenComparator) Verify(t string) bool { return subtle.ConstantTimeCompare([]byte(c.VerificationToken), []byte(t)) == 1 } -// ParseEvent parses the outter and inner events (if applicable) of an events +// ParseEvent parses the outer and inner events (if applicable) of an events // api event returning a EventsAPIEvent type. If the event is a url_verification event, // the inner event is empty. func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error) { @@ -183,7 +185,7 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error } if !cfg.TokenVerified { - return EventsAPIEvent{}, errors.New("Invalid verification token") + return EventsAPIEvent{}, errors.New("invalid verification token") } if e.Type == CallbackEvent { @@ -196,12 +198,39 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err } return innerEvent, nil } + + if e.Type == AppRateLimited { + appRateLimitedEvent := &EventsAPIAppRateLimited{} + err = json.Unmarshal(rawEvent, appRateLimitedEvent) + if err != nil { + return EventsAPIEvent{ + "", + "", + "unmarshalling_error", + "", + "", + &slack.UnmarshallingErrorEvent{ErrorObj: err}, + EventsAPIInnerEvent{}, + }, err + } + return EventsAPIEvent{ + e.Token, + e.TeamID, + e.Type, + e.APIAppID, + e.EnterpriseID, + appRateLimitedEvent, + EventsAPIInnerEvent{}, + }, nil + } + urlVerificationEvent := &EventsAPIURLVerificationEvent{} err = json.Unmarshal(rawEvent, urlVerificationEvent) if err != nil { @@ -210,6 +239,7 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error "", "unmarshalling_error", "", + "", &slack.UnmarshallingErrorEvent{ErrorObj: err}, EventsAPIInnerEvent{}, }, err @@ -219,11 +249,28 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error e.TeamID, e.Type, e.APIAppID, + e.EnterpriseID, urlVerificationEvent, EventsAPIInnerEvent{}, }, nil } +// Deprecated: ParseActionEvent cannot parse block_actions payloads and will return an +// unmarshalling error for them. Use [slack.InteractionCallback] with [json.Unmarshal] +// instead, or [slack.InteractionCallbackParse] to parse directly from an HTTP request. +// InteractionCallback handles all interaction types (block_actions, interactive_message, +// view_submission, etc.). +// +// Migration example: +// +// // Before (broken for block_actions): +// action, err := slackevents.ParseActionEvent(payload, slackevents.OptionNoVerifyToken()) +// +// // After (handles all interaction types): +// var ic slack.InteractionCallback +// err := json.Unmarshal([]byte(payload), &ic) +// // Use ic.ActionCallback.BlockActions for block actions +// // Use ic.ActionCallback.AttachmentActions for legacy attachment actions func ParseActionEvent(payloadString string, opts ...Option) (MessageAction, error) { byteString := []byte(payloadString) action := MessageAction{} diff --git a/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go b/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go index 1f7b2b8..d6c9c07 100644 --- a/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go +++ b/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go @@ -50,10 +50,12 @@ func DetectChannelType(channelID string) ChannelType { } } +// initialize replacer only once (if needed) +var escapeReplacer = strings.NewReplacer("&", "&", "<", "<", ">", ">") + // EscapeMessage text func EscapeMessage(message string) string { - replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") - return replacer.Replace(message) + return escapeReplacer.Replace(message) } // Retryable errors return true. diff --git a/vendor/github.com/slack-go/slack/slash.go b/vendor/github.com/slack-go/slack/slash.go index f62065a..fd46abf 100644 --- a/vendor/github.com/slack-go/slack/slash.go +++ b/vendor/github.com/slack-go/slack/slash.go @@ -1,24 +1,29 @@ package slack import ( + "encoding/json" + "fmt" "net/http" + "strconv" ) // SlashCommand contains information about a request of the slash command type SlashCommand struct { - Token string `json:"token"` - TeamID string `json:"team_id"` - TeamDomain string `json:"team_domain"` - EnterpriseID string `json:"enterprise_id,omitempty"` - EnterpriseName string `json:"enterprise_name,omitempty"` - ChannelID string `json:"channel_id"` - ChannelName string `json:"channel_name"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - Command string `json:"command"` - Text string `json:"text"` - ResponseURL string `json:"response_url"` - TriggerID string `json:"trigger_id"` + Token string `json:"token"` + TeamID string `json:"team_id"` + TeamDomain string `json:"team_domain"` + EnterpriseID string `json:"enterprise_id,omitempty"` + EnterpriseName string `json:"enterprise_name,omitempty"` + IsEnterpriseInstall bool `json:"is_enterprise_install"` + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + Command string `json:"command"` + Text string `json:"text"` + ResponseURL string `json:"response_url"` + TriggerID string `json:"trigger_id"` + APIAppID string `json:"api_app_id"` } // SlashCommandParse will parse the request of the slash command @@ -31,6 +36,7 @@ func SlashCommandParse(r *http.Request) (s SlashCommand, err error) { s.TeamDomain = r.PostForm.Get("team_domain") s.EnterpriseID = r.PostForm.Get("enterprise_id") s.EnterpriseName = r.PostForm.Get("enterprise_name") + s.IsEnterpriseInstall = r.PostForm.Get("is_enterprise_install") == "true" s.ChannelID = r.PostForm.Get("channel_id") s.ChannelName = r.PostForm.Get("channel_name") s.UserID = r.PostForm.Get("user_id") @@ -39,6 +45,7 @@ func SlashCommandParse(r *http.Request) (s SlashCommand, err error) { s.Text = r.PostForm.Get("text") s.ResponseURL = r.PostForm.Get("response_url") s.TriggerID = r.PostForm.Get("trigger_id") + s.APIAppID = r.PostForm.Get("api_app_id") return s, nil } @@ -51,3 +58,34 @@ func (s SlashCommand) ValidateToken(verificationTokens ...string) bool { } return false } + +// UnmarshalJSON handles is_enterprise_install being either a boolean or a +// string when parsing JSON from various payloads +func (s *SlashCommand) UnmarshalJSON(data []byte) error { + type SlashCommandCopy SlashCommand + scopy := &struct { + *SlashCommandCopy + IsEnterpriseInstall interface{} `json:"is_enterprise_install"` + }{ + SlashCommandCopy: (*SlashCommandCopy)(s), + } + + if err := json.Unmarshal(data, scopy); err != nil { + return err + } + + switch rawValue := scopy.IsEnterpriseInstall.(type) { + case string: + b, err := strconv.ParseBool(rawValue) + if err != nil { + return fmt.Errorf("parsing boolean for is_enterprise_install: %w", err) + } + s.IsEnterpriseInstall = b + case bool: + s.IsEnterpriseInstall = rawValue + default: + return fmt.Errorf("wrong data type for is_enterprise_install: %T", scopy.IsEnterpriseInstall) + } + + return nil +} diff --git a/vendor/github.com/slack-go/slack/socket_mode.go b/vendor/github.com/slack-go/slack/socket_mode.go new file mode 100644 index 0000000..9498cdf --- /dev/null +++ b/vendor/github.com/slack-go/slack/socket_mode.go @@ -0,0 +1,45 @@ +package slack + +import ( + "context" + "net/url" +) + +// SocketModeConnection contains various details about the SocketMode connection. +// It is returned by an "apps.connections.open" API call. +type SocketModeConnection struct { + URL string `json:"url,omitempty"` + Data map[string]interface{} `json:"-"` +} + +type openResponseFull struct { + SlackResponse + SocketModeConnection +} + +// StartSocketModeContext calls the "apps.connections.open" endpoint and returns the provided URL and the full Info block with a custom context. +// +// 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 = api.postJSONMethod(ctx, "apps.connections.open", api.appLevelToken, nil, response) + if err != nil { + return nil, "", err + } + + if response.Err() == nil { + api.Debugln("Using URL:", response.SocketModeConnection.URL) + } + + // According to the API documentation at https://api.slack.com/apis/socket-mode, we + // can add a query parameter `debug_reconnects=true` to the URL to make the connection + // time significantly shorter (360 seconds). + if api.debug { + u, _ := url.Parse(response.SocketModeConnection.URL) + 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 5296760..0adb28c 100644 --- a/vendor/github.com/slack-go/slack/stars.go +++ b/vendor/github.com/slack-go/slack/stars.go @@ -8,40 +8,39 @@ 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, } } -// AddStar stars an item in a channel +// AddStar stars an item in a channel. +// For more information see the AddStarContext documentation. func (api *Client) AddStar(channel string, item ItemRef) error { return api.AddStarContext(context.Background(), channel, item) } -// AddStarContext stars an item in a channel with a custom context +// AddStarContext stars an item in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/stars.add func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, @@ -65,12 +64,14 @@ func (api *Client) AddStarContext(ctx context.Context, channel string, item Item return response.Err() } -// RemoveStar removes a starred item from a channel +// RemoveStar removes a starred item from a channel. +// For more information see the RemoveStarContext documentation. func (api *Client) RemoveStar(channel string, item ItemRef) error { return api.RemoveStarContext(context.Background(), channel, item) } -// RemoveStarContext removes a starred item from a channel with a custom context +// RemoveStarContext removes a starred item from a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/stars.remove func (api *Client) RemoveStarContext(ctx context.Context, channel string, item ItemRef) error { values := url.Values{ "channel": {channel}, @@ -94,70 +95,75 @@ func (api *Client) RemoveStarContext(ctx context.Context, channel string, item I return response.Err() } -// ListStars returns information about the stars a user added -func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { +// ListStars returns information about the stars a user added. +// For more information see the ListStarsContext documentation. +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 -func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) { +// 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, 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. // // The user then has to iterate over them and figure out what they should -// 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": -// ... +// 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": +// ... +// } // -// } // 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) { +// I exposed it as returning []StarredItem, so it shall stay as StarredItem. +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/status_code_error.go b/vendor/github.com/slack-go/slack/status_code_error.go new file mode 100644 index 0000000..7347137 --- /dev/null +++ b/vendor/github.com/slack-go/slack/status_code_error.go @@ -0,0 +1,28 @@ +package slack + +import ( + "fmt" + "net/http" +) + +// StatusCodeError represents an http response error. +// type httpStatusCode interface { HTTPStatusCode() int } to handle it. +type StatusCodeError struct { + Code int + Status string +} + +func (t StatusCodeError) Error() string { + return fmt.Sprintf("slack server error: %s", t.Status) +} + +func (t StatusCodeError) HTTPStatusCode() int { + return t.Code +} + +func (t StatusCodeError) Retryable() bool { + if t.Code >= 500 || t.Code == http.StatusTooManyRequests { + return true + } + return false +} diff --git a/vendor/github.com/slack-go/slack/team.go b/vendor/github.com/slack-go/slack/team.go index 029e2b5..55364a9 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 @@ -24,10 +19,30 @@ type TeamInfo struct { Icon map[string]interface{} `json:"icon"` } +type TeamProfileResponse struct { + Profile TeamProfile `json:"profile"` + SlackResponse +} + +type TeamProfile struct { + Fields []TeamProfileField `json:"fields"` +} + +type TeamProfileField struct { + ID string `json:"id"` + Ordering int `json:"ordering"` + Label string `json:"label"` + Hint string `json:"hint"` + Type string `json:"type"` + PossibleValues []string `json:"possible_values"` + IsHidden bool `json:"is_hidden"` + Options map[string]bool `json:"options"` +} + type LoginResponse struct { Logins []Login `json:"logins"` - Paging `json:"paging"` SlackResponse + ResponseMetadata `json:"response_metadata"` } type Login struct { @@ -54,16 +69,15 @@ type BillingActive struct { // AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request type AccessLogParameters struct { - Count int - Page int + TeamID string + 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) { @@ -95,12 +109,46 @@ func (api *Client) accessLogsRequest(ctx context.Context, path string, values ur return response, response.Err() } -// GetTeamInfo gets the Team Information of the user +func (api *Client) teamProfileRequest(ctx context.Context, path string, values url.Values) (*TeamProfileResponse, error) { + response := &TeamProfileResponse{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + return response, response.Err() +} + +// GetTeamInfo gets the Team Information of the user. +// For more information see the GetTeamInfoContext documentation. func (api *Client) GetTeamInfo() (*TeamInfo, error) { return api.GetTeamInfoContext(context.Background()) } -// GetTeamInfoContext gets the Team Information of the user with a custom context +// GetOtherTeamInfoContext gets Team information for any team with a custom context. +// Slack API docs: https://api.slack.com/methods/team.info +func (api *Client) GetOtherTeamInfoContext(ctx context.Context, team string) (*TeamInfo, error) { + if team == "" { + return api.GetTeamInfoContext(ctx) + } + values := url.Values{ + "token": {api.token}, + } + values.Add("team", team) + response, err := api.teamRequest(ctx, "team.info", values) + if err != nil { + return nil, err + } + return &response.Team, nil +} + +// GetOtherTeamInfo gets Team information for any team. +// For more information see the GetOtherTeamInfoContext documentation. +func (api *Client) GetOtherTeamInfo(team string) (*TeamInfo, error) { + return api.GetOtherTeamInfoContext(context.Background(), team) +} + +// GetTeamInfoContext gets the Team Information of the user with a custom context. +// Slack API docs: https://api.slack.com/methods/team.info func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) { values := url.Values{ "token": {api.token}, @@ -113,55 +161,86 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) { return &response.Team, nil } -// GetAccessLogs retrieves a page of logins according to the parameters given -func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) { - return api.GetAccessLogsContext(context.Background(), params) +// GetTeamProfile gets the Team Profile settings of the user. +// For more information see the GetTeamProfileContext documentation. +func (api *Client) GetTeamProfile(teamID ...string) (*TeamProfile, error) { + return api.GetTeamProfileContext(context.Background(), teamID...) } -// GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context -func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) { +// GetTeamProfileContext gets the Team Profile settings of the user with a custom context. +// Slack API docs: https://api.slack.com/methods/team.profile.get +func (api *Client) GetTeamProfileContext(ctx context.Context, teamID ...string) (*TeamProfile, error) { values := url.Values{ "token": {api.token}, } - if params.Count != DEFAULT_LOGINS_COUNT { - values.Add("count", strconv.Itoa(params.Count)) - } - if params.Page != DEFAULT_LOGINS_PAGE { - values.Add("page", strconv.Itoa(params.Page)) + if len(teamID) > 0 { + values["team_id"] = teamID } - response, err := api.accessLogsRequest(ctx, "team.accessLogs", values) + response, err := api.teamProfileRequest(ctx, "team.profile.get", values) if err != nil { - return nil, nil, err + return nil, err } - return response.Logins, &response.Paging, nil + return &response.Profile, nil } -// GetBillableInfo ... -func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) { - return api.GetBillableInfoContext(context.Background(), user) +// 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, string, error) { + return api.GetAccessLogsContext(context.Background(), params) } -// GetBillableInfoContext ... -func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) { +// 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, string, error) { values := url.Values{ "token": {api.token}, - "user": {user}, + } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Before != 0 { + values.Add("before", strconv.Itoa(params.Before)) } - return api.billableInfoRequest(ctx, "team.billableInfo", values) + response, err := api.accessLogsRequest(ctx, "team.accessLogs", values) + if err != nil { + return nil, "", err + } + return response.Logins, response.ResponseMetadata.Cursor, nil } -// GetBillableInfoForTeam returns the billing_active status of all users on the team. -func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) { - return api.GetBillableInfoForTeamContext(context.Background()) +type GetBillableInfoParams struct { + User string + TeamID string } -// GetBillableInfoForTeamContext returns the billing_active status of all users on the team with a custom context -func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) { +// GetBillableInfo gets the billable users information of the team. +// For more information see the GetBillableInfoContext documentation. +func (api *Client) GetBillableInfo(params GetBillableInfoParams) (map[string]BillingActive, error) { + return api.GetBillableInfoContext(context.Background(), params) +} + +// GetBillableInfoContext gets the billable users information of the team with a custom context. +// Slack API docs: https://api.slack.com/methods/team.billableInfo +func (api *Client) GetBillableInfoContext(ctx context.Context, params GetBillableInfoParams) (map[string]BillingActive, error) { values := url.Values{ "token": {api.token}, } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + + if params.User != "" { + values.Add("user", params.User) + } + return api.billableInfoRequest(ctx, "team.billableInfo", values) } diff --git a/vendor/github.com/slack-go/slack/tokens.go b/vendor/github.com/slack-go/slack/tokens.go new file mode 100644 index 0000000..49bbde9 --- /dev/null +++ b/vendor/github.com/slack-go/slack/tokens.go @@ -0,0 +1,52 @@ +package slack + +import ( + "context" + "net/url" +) + +// RotateTokens exchanges a refresh token for a new app configuration token. +// For more information see the RotateTokensContext documentation. +func (api *Client) RotateTokens(configToken string, refreshToken string) (*TokenResponse, error) { + return api.RotateTokensContext(context.Background(), configToken, refreshToken) +} + +// RotateTokensContext exchanges a refresh token for a new app configuration token with a custom context. +// Slack API docs: https://api.slack.com/methods/tooling.tokens.rotate +func (api *Client) RotateTokensContext(ctx context.Context, configToken string, refreshToken string) (*TokenResponse, error) { + if configToken == "" { + configToken = api.configToken + } + + if refreshToken == "" { + refreshToken = api.configRefreshToken + } + + values := url.Values{ + "refresh_token": {refreshToken}, + } + + response := &TokenResponse{} + err := api.getMethod(ctx, "tooling.tokens.rotate", configToken, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// UpdateConfigTokens replaces the configuration tokens in the client with those returned by the API +func (api *Client) UpdateConfigTokens(response *TokenResponse) { + api.configToken = response.Token + api.configRefreshToken = response.RefreshToken +} + +type TokenResponse struct { + Token string `json:"token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + TeamId string `json:"team_id,omitempty"` + UserId string `json:"user_id,omitempty"` + IssuedAt uint64 `json:"iat,omitempty"` + ExpiresAt uint64 `json:"exp,omitempty"` + SlackResponse +} diff --git a/vendor/github.com/slack-go/slack/usergroups.go b/vendor/github.com/slack-go/slack/usergroups.go index 9417f81..b5c5454 100644 --- a/vendor/github.com/slack-go/slack/usergroups.go +++ b/vendor/github.com/slack-go/slack/usergroups.go @@ -3,6 +3,7 @@ package slack import ( "context" "net/url" + "strconv" "strings" ) @@ -50,18 +51,61 @@ func (api *Client) userGroupRequest(ctx context.Context, path string, values url return response, response.Err() } -// CreateUserGroup creates a new user group -func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) { - return api.CreateUserGroupContext(context.Background(), userGroup) +// createUserGroupParams contains arguments for CreateUserGroup method call +type createUserGroupParams struct { + enableSection bool + includeCount bool } -// CreateUserGroupContext creates a new user group with a custom context -func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { +// CreateUserGroupOption options for the CreateUserGroup method call. +type CreateUserGroupOption func(*createUserGroupParams) + +// CreateUserGroupOptionEnableSection enable the section for the user group (default: false) +func CreateUserGroupOptionEnableSection(enableSection bool) CreateUserGroupOption { + return func(params *createUserGroupParams) { + params.enableSection = enableSection + } +} + +// CreateUserGroupOptionIncludeCount include the number of users in each User Group +func CreateUserGroupOptionIncludeCount(includeCount bool) CreateUserGroupOption { + return func(params *createUserGroupParams) { + params.includeCount = includeCount + } +} + +// CreateUserGroup creates a new user group. +// For more information see the CreateUserGroupContext documentation. +func (api *Client) CreateUserGroup(userGroup UserGroup, options ...CreateUserGroupOption) (UserGroup, error) { + return api.CreateUserGroupContext(context.Background(), userGroup, options...) +} + +// CreateUserGroupContext creates a new user group with a custom context. +// Slack API docs: https://api.slack.com/methods/usergroups.create +func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup, options ...CreateUserGroupOption) (UserGroup, error) { + params := createUserGroupParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "name": {userGroup.Name}, } + if params.enableSection { + values["enable_section"] = []string{strconv.FormatBool(params.enableSection)} + } + + if params.includeCount { + values["include_count"] = []string{strconv.FormatBool(params.includeCount)} + } + + if userGroup.TeamID != "" { + values["team_id"] = []string{userGroup.TeamID} + } + if userGroup.Handle != "" { values["handle"] = []string{userGroup.Handle} } @@ -81,18 +125,57 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro return response.UserGroup, nil } -// DisableUserGroup disables an existing user group -func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) { - return api.DisableUserGroupContext(context.Background(), userGroup) +// DisableUserGroupParams contains arguments for DisableUserGroup method calls. +type DisableUserGroupParams struct { + IncludeCount bool + TeamID string } -// DisableUserGroupContext disables an existing user group with a custom context -func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { +// DisableUserGroupOption options for the DisableUserGroup method calls. +type DisableUserGroupOption func(*DisableUserGroupParams) + +// DisableUserGroupOptionIncludeCount include the count of User Groups (default: false) +func DisableUserGroupOptionIncludeCount(b bool) DisableUserGroupOption { + return func(params *DisableUserGroupParams) { + params.IncludeCount = b + } +} + +// DisableUserGroupOptionTeamID include team Id +func DisableUserGroupOptionTeamID(teamID string) DisableUserGroupOption { + return func(params *DisableUserGroupParams) { + params.TeamID = teamID + } +} + +// DisableUserGroup disables an existing user group. +// For more information see the DisableUserGroupContext documentation. +func (api *Client) DisableUserGroup(userGroup string, options ...DisableUserGroupOption) (UserGroup, error) { + return api.DisableUserGroupContext(context.Background(), userGroup, options...) +} + +// DisableUserGroupContext disables an existing user group with a custom context. +// Slack API docs: https://api.slack.com/methods/usergroups.disable +func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string, options ...DisableUserGroupOption) (UserGroup, error) { + params := DisableUserGroupParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response, err := api.userGroupRequest(ctx, "usergroups.disable", values) if err != nil { return UserGroup{}, err @@ -100,18 +183,57 @@ func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string return response.UserGroup, nil } -// EnableUserGroup enables an existing user group -func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) { - return api.EnableUserGroupContext(context.Background(), userGroup) +// EnableUserGroupParams contains arguments for EnableUserGroup method calls. +type EnableUserGroupParams struct { + IncludeCount bool + TeamID string } -// EnableUserGroupContext enables an existing user group with a custom context -func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) { +// EnableUserGroupOption options for the EnableUserGroup method calls. +type EnableUserGroupOption func(*EnableUserGroupParams) + +// EnableUserGroupOptionIncludeCount include the count of User Groups (default: false) +func EnableUserGroupOptionIncludeCount(b bool) EnableUserGroupOption { + return func(params *EnableUserGroupParams) { + params.IncludeCount = b + } +} + +// EnableUserGroupOptionTeamID include team Id +func EnableUserGroupOptionTeamID(teamID string) EnableUserGroupOption { + return func(params *EnableUserGroupParams) { + params.TeamID = teamID + } +} + +// EnableUserGroup enables an existing user group. +// For more information see the EnableUserGroupContext documentation. +func (api *Client) EnableUserGroup(userGroup string, options ...EnableUserGroupOption) (UserGroup, error) { + return api.EnableUserGroupContext(context.Background(), userGroup, options...) +} + +// EnableUserGroupContext enables an existing user group with a custom context. +// Slack API docs: https://api.slack.com/methods/usergroups.enable +func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string, options ...EnableUserGroupOption) (UserGroup, error) { + params := EnableUserGroupParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response, err := api.userGroupRequest(ctx, "usergroups.enable", values) if err != nil { return UserGroup{}, err @@ -122,6 +244,17 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) // GetUserGroupsOption options for the GetUserGroups method call. type GetUserGroupsOption func(*GetUserGroupsParams) +// Deprecated: GetUserGroupsOptionWithTeamID is deprecated, use GetUserGroupsOptionTeamID instead +func GetUserGroupsOptionWithTeamID(teamID string) GetUserGroupsOption { + return GetUserGroupsOptionTeamID(teamID) +} + +func GetUserGroupsOptionTeamID(teamID string) GetUserGroupsOption { + return func(params *GetUserGroupsParams) { + params.TeamID = teamID + } +} + // GetUserGroupsOptionIncludeCount include the number of users in each User Group (default: false) func GetUserGroupsOptionIncludeCount(b bool) GetUserGroupsOption { return func(params *GetUserGroupsParams) { @@ -145,17 +278,20 @@ func GetUserGroupsOptionIncludeUsers(b bool) GetUserGroupsOption { // GetUserGroupsParams contains arguments for GetUserGroups method call type GetUserGroupsParams struct { + TeamID string IncludeCount bool IncludeDisabled bool IncludeUsers bool } -// GetUserGroups returns a list of user groups for the team +// GetUserGroups returns a list of user groups for the team. +// For more information see the GetUserGroupsContext documentation. func (api *Client) GetUserGroups(options ...GetUserGroupsOption) ([]UserGroup, error) { return api.GetUserGroupsContext(context.Background(), options...) } -// GetUserGroupsContext returns a list of user groups for the team with a custom context +// GetUserGroupsContext returns a list of user groups for the team with a custom context. +// Slack API docs: https://api.slack.com/methods/usergroups.list func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserGroupsOption) ([]UserGroup, error) { params := GetUserGroupsParams{} @@ -166,6 +302,9 @@ func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserG values := url.Values{ "token": {api.token}, } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } if params.IncludeCount { values.Add("include_count", "true") } @@ -183,32 +322,103 @@ func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserG return response.UserGroups, nil } -// UpdateUserGroup will update an existing user group -func (api *Client) UpdateUserGroup(userGroup UserGroup) (UserGroup, error) { - return api.UpdateUserGroupContext(context.Background(), userGroup) +// UpdateUserGroupsOption options for the UpdateUserGroup method call. +type UpdateUserGroupsOption func(*UpdateUserGroupsParams) + +// UpdateUserGroupsOptionName change the name of the User Group (default: empty, so it's no-op) +func UpdateUserGroupsOptionName(name string) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.Name = name + } +} + +// UpdateUserGroupsOptionHandle change the handle of the User Group (default: empty, so it's no-op) +func UpdateUserGroupsOptionHandle(handle string) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.Handle = handle + } +} + +// UpdateUserGroupsOptionDescription change the description of the User Group. (default: nil, so it's no-op) +func UpdateUserGroupsOptionDescription(description *string) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.Description = description + } +} + +// UpdateUserGroupsOptionChannels change the default channels of the User Group. (default: unspecified, so it's no-op) +func UpdateUserGroupsOptionChannels(channels []string) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.Channels = &channels + } +} + +// UpdateUserGroupsOptionEnableSection enable the section for the user group (default: false) +func UpdateUserGroupsOptionEnableSection(enableSection bool) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.EnableSection = enableSection + } +} + +// UpdateUserGroupsOptionTeamID specify the team id for the User Group. (default: nil, so it's no-op) +func UpdateUserGroupsOptionTeamID(teamID string) UpdateUserGroupsOption { + return func(params *UpdateUserGroupsParams) { + params.TeamID = teamID + } } -// UpdateUserGroupContext will update an existing user group with a custom context -func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) { +// UpdateUserGroupsParams contains arguments for UpdateUserGroup method call +type UpdateUserGroupsParams struct { + Name string + Handle string + Description *string + Channels *[]string + EnableSection bool + TeamID string +} + +// UpdateUserGroup will update an existing user group. +// For more information see the UpdateUserGroupContext documentation. +func (api *Client) UpdateUserGroup(userGroupID string, options ...UpdateUserGroupsOption) (UserGroup, error) { + return api.UpdateUserGroupContext(context.Background(), userGroupID, options...) +} + +// UpdateUserGroupContext will update an existing user group with a custom context. +// Slack API docs: https://api.slack.com/methods/usergroups.update +func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroupID string, options ...UpdateUserGroupsOption) (UserGroup, error) { + params := UpdateUserGroupsParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, - "usergroup": {userGroup.ID}, + "usergroup": {userGroupID}, } - if userGroup.Name != "" { - values["name"] = []string{userGroup.Name} + if params.Name != "" { + values["name"] = []string{params.Name} } - if userGroup.Handle != "" { - values["handle"] = []string{userGroup.Handle} + if params.Handle != "" { + values["handle"] = []string{params.Handle} } - if userGroup.Description != "" { - values["description"] = []string{userGroup.Description} + if params.Description != nil { + values["description"] = []string{*params.Description} } - if len(userGroup.Prefs.Channels) > 0 { - values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")} + if params.Channels != nil { + values["channels"] = []string{strings.Join(*params.Channels, ",")} + } + + if params.EnableSection { + values["enable_section"] = []string{strconv.FormatBool(params.EnableSection)} + } + + if params.TeamID != "" { + values["team_id"] = []string{params.TeamID} } response, err := api.userGroupRequest(ctx, "usergroups.update", values) @@ -218,18 +428,57 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGro return response.UserGroup, nil } -// GetUserGroupMembers will retrieve the current list of users in a group -func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) { - return api.GetUserGroupMembersContext(context.Background(), userGroup) +// GetUserGroupMembersOption options for the GetUserGroupMembers method call. +type GetUserGroupMembersOption func(*GetUserGroupMembersParams) + +// GetUserGroupMembersParams contains arguments for GetUserGroupMembers method call +type GetUserGroupMembersParams struct { + IncludeDisabled bool + TeamID string +} + +// GetUserGroupMembersOptionIncludeDisabled include disabled User Groups (default: false) +func GetUserGroupMembersOptionIncludeDisabled(b bool) GetUserGroupMembersOption { + return func(params *GetUserGroupMembersParams) { + params.IncludeDisabled = b + } +} + +// GetUserGroupMembersOptionTeamID include team Id +func GetUserGroupMembersOptionTeamID(teamID string) GetUserGroupMembersOption { + return func(params *GetUserGroupMembersParams) { + params.TeamID = teamID + } } -// GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context -func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) { +// GetUserGroupMembers will retrieve the current list of users in a group. +// For more information see the GetUserGroupMembersContext documentation. +func (api *Client) GetUserGroupMembers(userGroup string, options ...GetUserGroupMembersOption) ([]string, error) { + return api.GetUserGroupMembersContext(context.Background(), userGroup, options...) +} + +// GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context. +// Slack API docs: https://api.slack.com/methods/usergroups.users.list +func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string, options ...GetUserGroupMembersOption) ([]string, error) { + params := GetUserGroupMembersParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, } + if params.IncludeDisabled { + values.Add("include_disabled", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + response, err := api.userGroupRequest(ctx, "usergroups.users.list", values) if err != nil { return []string{}, err @@ -237,22 +486,99 @@ func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup str return response.Users, nil } -// UpdateUserGroupMembers will update the members of an existing user group -func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (UserGroup, error) { - return api.UpdateUserGroupMembersContext(context.Background(), userGroup, members) +// UpdateUserGroupMembersOption options for the UpdateUserGroupMembers method call. +type UpdateUserGroupMembersOption func(*UpdateUserGroupMembersParams) + +// UpdateUserGroupMembersParams contains arguments for UpdateUserGroupMembers method call +type UpdateUserGroupMembersParams struct { + AdditionalChannels []string + IncludeCount bool + IsShared bool + TeamID string +} + +// UpdateUserGroupMembersOptionAdditionalChannels include additional channels +func UpdateUserGroupMembersOptionAdditionalChannels(channels []string) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.AdditionalChannels = channels + } +} + +// UpdateUserGroupMembersOptionIsShared include the count of User Groups (default: false) +func UpdateUserGroupMembersOptionIsShared(b bool) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.IsShared = b + } +} + +// UpdateUserGroupMembersOptionIncludeCount include the count of User Groups (default: false) +func UpdateUserGroupMembersOptionIncludeCount(b bool) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.IncludeCount = b + } +} + +// UpdateUserGroupMembersOptionTeamID include team Id +func UpdateUserGroupMembersOptionTeamID(teamID string) UpdateUserGroupMembersOption { + return func(params *UpdateUserGroupMembersParams) { + params.TeamID = teamID + } +} + +// UpdateUserGroupMembers will update the members of an existing user group. +// For more information see the UpdateUserGroupMembersContext documentation. +func (api *Client) UpdateUserGroupMembers(userGroup string, members string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + return api.UpdateUserGroupMembersContext(context.Background(), userGroup, members, options...) } -// UpdateUserGroupMembersContext will update the members of an existing user group with a custom context -func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) { +// UpdateUserGroupMembersContext will update the members of an existing user group with a custom context. +// Slack API docs: https://api.slack.com/methods/usergroups.update +func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string, options ...UpdateUserGroupMembersOption) (UserGroup, error) { + params := UpdateUserGroupMembersParams{} + + for _, opt := range options { + opt(¶ms) + } + values := url.Values{ "token": {api.token}, "usergroup": {userGroup}, "users": {members}, } + if params.IncludeCount { + values.Add("include_count", "true") + } + + if params.IsShared { + values.Add("is_shared", "true") + } + + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + + if len(params.AdditionalChannels) > 0 { + values["additional_channels"] = []string{strings.Join(params.AdditionalChannels, ",")} + } + response, err := api.userGroupRequest(ctx, "usergroups.users.update", values) if err != nil { return UserGroup{}, err } 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 364958e..541baae 100644 --- a/vendor/github.com/slack-go/slack/users.go +++ b/vendor/github.com/slack-go/slack/users.go @@ -17,29 +17,48 @@ const ( // UserProfile contains all the information details of a given user type UserProfile struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - RealName string `json:"real_name"` - RealNameNormalized string `json:"real_name_normalized"` - DisplayName string `json:"display_name"` - DisplayNameNormalized string `json:"display_name_normalized"` - Email string `json:"email"` - Skype string `json:"skype"` - Phone string `json:"phone"` - 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"` - ImageOriginal string `json:"image_original"` - Title string `json:"title"` - 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"` - StatusExpiration int `json:"status_expiration"` - Team string `json:"team"` - Fields UserProfileCustomFields `json:"fields"` + 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 { + EmojiName string `json:"emoji_name"` + DisplayAlias string `json:"display_alias,omitempty"` + DisplayURL string `json:"display_url,omitempty"` + Unicode string `json:"unicode,omitempty"` } // UserProfileCustomFields represents user profile's custom fields. @@ -62,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) } @@ -101,31 +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"` - Has2FA bool `json:"has_2fa"` - 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 @@ -157,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"` } @@ -216,11 +242,13 @@ func (api *Client) userRequest(ctx context.Context, path string, values url.Valu } // GetUserPresence will retrieve the current presence status of given user. +// For more information see the GetUserPresenceContext documentation. func (api *Client) GetUserPresence(user string) (*UserPresence, error) { return api.GetUserPresenceContext(context.Background(), user) } // GetUserPresenceContext will retrieve the current presence status of given user with a custom context. +// Slack API docs: https://api.slack.com/methods/users.getPresence func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) { values := url.Values{ "token": {api.token}, @@ -234,12 +262,14 @@ func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*Us return &response.UserPresence, nil } -// GetUserInfo will retrieve the complete user information +// GetUserInfo will retrieve the complete user information. +// For more information see the GetUserInfoContext documentation. func (api *Client) GetUserInfo(user string) (*User, error) { return api.GetUserInfoContext(context.Background(), user) } -// GetUserInfoContext will retrieve the complete user information with a custom context +// GetUserInfoContext will retrieve the complete user information with a custom context. +// Slack API docs: https://api.slack.com/methods/users.info func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) { values := url.Values{ "token": {api.token}, @@ -254,12 +284,14 @@ func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, return &response.User, nil } -// GetUsersInfo will retrieve the complete multi-users information +// GetUsersInfo will retrieve the complete multi-users information. +// For more information see the GetUsersInfoContext documentation. func (api *Client) GetUsersInfo(users ...string) (*[]User, error) { return api.GetUsersInfoContext(context.Background(), users...) } -// GetUsersInfoContext will retrieve the complete multi-users information with a custom context +// GetUsersInfoContext will retrieve the complete multi-users information with a custom context. +// Slack API docs: https://api.slack.com/methods/users.info func (api *Client) GetUsersInfoContext(ctx context.Context, users ...string) (*[]User, error) { values := url.Values{ "token": {api.token}, @@ -291,6 +323,20 @@ func GetUsersOptionPresence(n bool) GetUsersOption { } } +// GetUsersOptionTeamID include team Id +func GetUsersOptionTeamID(teamId string) GetUsersOption { + return func(p *UserPagination) { + p.teamId = teamId + } +} + +// 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, @@ -306,11 +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 - 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 @@ -332,17 +380,16 @@ 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)}, } @@ -352,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 } @@ -363,13 +411,13 @@ func (api *Client) GetUsersPaginated(options ...GetUsersOption) UserPagination { } // GetUsers returns the list of users (with their detailed information) -func (api *Client) GetUsers() ([]User, error) { - return api.GetUsersContext(context.Background()) +func (api *Client) GetUsers(options ...GetUsersOption) ([]User, error) { + return api.GetUsersContext(context.Background(), options...) } // GetUsersContext returns the list of users (with their detailed information) with a custom context -func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) { - p := api.GetUsersPaginated() +func (api *Client) GetUsersContext(ctx context.Context, options ...GetUsersOption) (results []User, err error) { + p := api.GetUsersPaginated(options...) for err == nil { p, err = p.Next(ctx) if err == nil { @@ -387,12 +435,14 @@ func (api *Client) GetUsersContext(ctx context.Context) (results []User, err err return results, p.Failure(err) } -// GetUserByEmail will retrieve the complete user information by email +// GetUserByEmail will retrieve the complete user information by email. +// For more information see the GetUserByEmailContext documentation. func (api *Client) GetUserByEmail(email string) (*User, error) { return api.GetUserByEmailContext(context.Background(), email) } -// GetUserByEmailContext will retrieve the complete user information by email with a custom context +// GetUserByEmailContext will retrieve the complete user information by email with a custom context. +// Slack API docs: https://api.slack.com/methods/users.lookupByEmail func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*User, error) { values := url.Values{ "token": {api.token}, @@ -405,12 +455,14 @@ func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*Us return &response.User, nil } -// SetUserAsActive marks the currently authenticated user as active +// SetUserAsActive marks the currently authenticated user as active. +// For more information see the SetUserAsActiveContext documentation. func (api *Client) SetUserAsActive() error { return api.SetUserAsActiveContext(context.Background()) } -// SetUserAsActiveContext marks the currently authenticated user as active with a custom context +// SetUserAsActiveContext marks the currently authenticated user as active with a custom context. +// Slack API docs: https://api.slack.com/methods/users.setActive func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { values := url.Values{ "token": {api.token}, @@ -420,12 +472,14 @@ func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) { return err } -// SetUserPresence changes the currently authenticated user presence +// SetUserPresence changes the currently authenticated user presence. +// For more information see the SetUserPresenceContext documentation. func (api *Client) SetUserPresence(presence string) error { return api.SetUserPresenceContext(context.Background(), presence) } -// SetUserPresenceContext changes the currently authenticated user presence with a custom context +// SetUserPresenceContext changes the currently authenticated user presence with a custom context. +// Slack API docs: https://api.slack.com/methods/users.setPresence func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error { values := url.Values{ "token": {api.token}, @@ -436,12 +490,14 @@ func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) return err } -// GetUserIdentity will retrieve user info available per identity scopes +// GetUserIdentity will retrieve user info available per identity scopes. +// For more information see the GetUserIdentityContext documentation. func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) { return api.GetUserIdentityContext(context.Background()) } -// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context +// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context. +// Slack API docs: https://api.slack.com/methods/users.identity func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) { values := url.Values{ "token": {api.token}, @@ -460,17 +516,17 @@ func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserId return response, nil } -// SetUserPhoto changes the currently authenticated user's profile image +// SetUserPhoto changes the currently authenticated user's profile image. +// For more information see the SetUserPhotoContext documentation. func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error { return api.SetUserPhotoContext(context.Background(), image, params) } -// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context +// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context. +// Slack API docs: https://api.slack.com/methods/users.setPhoto func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) { response := &SlackResponse{} - values := url.Values{ - "token": {api.token}, - } + values := url.Values{} if params.CropX != DEFAULT_USER_PHOTO_CROP_X { values.Add("crop_x", strconv.Itoa(params.CropX)) } @@ -481,7 +537,7 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params values.Add("crop_w", strconv.Itoa(params.CropW)) } - err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api) + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", api.token, values, response, api) if err != nil { return err } @@ -489,12 +545,14 @@ func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params return response.Err() } -// DeleteUserPhoto deletes the current authenticated user's profile image +// DeleteUserPhoto deletes the current authenticated user's profile image. +// For more information see the DeleteUserPhotoContext documentation. func (api *Client) DeleteUserPhoto() error { return api.DeleteUserPhotoContext(context.Background()) } -// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context +// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context. +// Slack API docs: https://api.slack.com/methods/users.deletePhoto func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) { response := &SlackResponse{} values := url.Values{ @@ -509,43 +567,179 @@ func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) { return response.Err() } -// SetUserCustomStatus will set a custom status and emoji for the currently -// authenticated user. If statusEmoji is "" and statusText is not, the Slack API -// will automatically set it to ":speech_balloon:". Otherwise, if both are "" -// the Slack API will unset the custom status/emoji. If statusExpiration is set to 0 -// the status will not expire. +// SetUserRealName changes the currently authenticated user's realName +// For more information see the SetUserRealNameContextWithUser documentation. +func (api *Client) SetUserRealName(realName string) error { + return api.SetUserRealNameContextWithUser(context.Background(), "", realName) +} + +// SetUserRealNameContextWithUser will set a real name for the provided user with a custom context. +// Slack API docs: https://api.slack.com/methods/users.profile.set +func (api *Client) SetUserRealNameContextWithUser(ctx context.Context, user, realName string) error { + profile, err := json.Marshal( + &struct { + RealName string `json:"real_name"` + }{ + RealName: realName, + }, + ) + + if err != nil { + return err + } + + values := url.Values{ + "token": {api.token}, + "profile": {string(profile)}, + } + + // 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() +} + +// 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 { + return api.SetUserCustomFieldsContext(context.Background(), userID, customFields) +} + +// SetUserCustomFieldsContext sets Custom Profile fields on the provided users account. +// Due to the non-repeating elements within the request, a map fields is required. +// The key in the map signifies the field that will be updated. +// +// Note: You may need to change the way the custom field is populated within the Profile section of the Admin Console +// from SCIM or User Entered to API. +// +// See GetTeamProfile for information to retrieve possible fields for your account. +// +// Slack API docs: https://api.slack.com/methods/users.profile.set +func (api *Client) SetUserCustomFieldsContext(ctx context.Context, userID string, customFields map[string]UserProfileCustomField) error { + + // Convert data to data type with custom marshall / unmarshall + // For more information, see UserProfileCustomFields definition. + updateFields := UserProfileCustomFields{} + updateFields.SetMap(customFields) + + // This anonymous struct is needed to set the fields level of the request data. The base struct for + // UserProfileCustomFields has an unexported variable named fields that does not contain a struct tag, + // which has resulted in this configuration. + profile, err := json.Marshal(&struct { + Fields UserProfileCustomFields `json:"fields"` + }{ + Fields: updateFields, + }) + + if err != nil { + return err + } + + values := url.Values{ + "token": {api.token}, + "user": {userID}, + "profile": {string(profile)}, + } + + response := &userResponseFull{} + if _, err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil { + return err + } + + return response.Err() + +} + +// SetUserCustomStatus will set a custom status and emoji for the currently authenticated user. +// For more information see the SetUserCustomStatusContext documentation. func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error { return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration) } -// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context -// -// For more information see SetUserCustomStatus +// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context. +// For more information see the SetUserCustomStatusContextWithUser documentation. func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error { - return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration) + return api.SetUserCustomStatusContextWithUser(ctx, "", statusText, statusEmoji, statusExpiration) } // SetUserCustomStatusWithUser will set a custom status and emoji for the provided user. -// -// For more information see SetUserCustomStatus +// For more information see the SetUserCustomStatusContextWithUser documentation. func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji string, statusExpiration int64) error { return api.SetUserCustomStatusContextWithUser(context.Background(), user, statusText, statusEmoji, statusExpiration) } -// SetUserCustomStatusContextWithUser will set a custom status and emoji for the provided user with a custom context +// SetUserCustomStatusContextWithUser will set a custom status and emoji for the currently authenticated user. +// If statusEmoji is "" and statusText is not, the Slack API will automatically set it to ":speech_balloon:". +// Otherwise, if both are "" the Slack API will unset the custom status/emoji. If statusExpiration is set to 0 +// the status will not expire. // -// For more information see SetUserCustomStatus +// 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"` @@ -563,11 +757,15 @@ func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, } values := url.Values{ - "user": {user}, "token": {api.token}, "profile": {string(profile)}, } + // 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 @@ -588,9 +786,16 @@ func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error { return api.SetUserCustomStatusContext(ctx, "", "", 0) } +// GetUserProfileParameters are the parameters required to get user profile +type GetUserProfileParameters struct { + UserID string + IncludeLabels bool +} + // GetUserProfile retrieves a user's profile information. -func (api *Client) GetUserProfile(userID string, includeLabels bool) (*UserProfile, error) { - return api.GetUserProfileContext(context.Background(), userID, includeLabels) +// For more information see the GetUserProfileContext documentation. +func (api *Client) GetUserProfile(params *GetUserProfileParameters) (*UserProfile, error) { + return api.GetUserProfileContext(context.Background(), params) } type getUserProfileResponse struct { @@ -599,9 +804,14 @@ type getUserProfileResponse struct { } // GetUserProfileContext retrieves a user's profile information with a context. -func (api *Client) GetUserProfileContext(ctx context.Context, userID string, includeLabels bool) (*UserProfile, error) { - values := url.Values{"token": {api.token}, "user": {userID}} - if includeLabels { +// Slack API docs: https://api.slack.com/methods/users.profile.get +func (api *Client) GetUserProfileContext(ctx context.Context, params *GetUserProfileParameters) (*UserProfile, error) { + values := url.Values{"token": {api.token}} + + if params.UserID != "" { + values.Add("user", params.UserID) + } + if params.IncludeLabels { values.Add("include_labels", "true") } resp := &getUserProfileResponse{} diff --git a/vendor/github.com/slack-go/slack/views.go b/vendor/github.com/slack-go/slack/views.go index c34feec..c16503c 100644 --- a/vendor/github.com/slack-go/slack/views.go +++ b/vendor/github.com/slack-go/slack/views.go @@ -18,28 +18,37 @@ type ViewState struct { type View struct { SlackResponse - ID string `json:"id"` - TeamID string `json:"team_id"` - Type ViewType `json:"type"` - Title *TextBlockObject `json:"title"` - Close *TextBlockObject `json:"close"` - Submit *TextBlockObject `json:"submit"` - Blocks Blocks `json:"blocks"` - PrivateMetadata string `json:"private_metadata"` - CallbackID string `json:"callback_id"` - State *ViewState `json:"state"` - Hash string `json:"hash"` - ClearOnClose bool `json:"clear_on_close"` - NotifyOnClose bool `json:"notify_on_close"` - RootViewID string `json:"root_view_id"` - PreviousViewID string `json:"previous_view_id"` - AppID string `json:"app_id"` - ExternalID string `json:"external_id"` - BotID string `json:"bot_id"` + ID string `json:"id"` + TeamID string `json:"team_id"` + Type ViewType `json:"type"` + Title *TextBlockObject `json:"title"` + Close *TextBlockObject `json:"close"` + Submit *TextBlockObject `json:"submit"` + Blocks Blocks `json:"blocks"` + PrivateMetadata string `json:"private_metadata"` + CallbackID string `json:"callback_id"` + State *ViewState `json:"state"` + Hash string `json:"hash"` + ClearOnClose bool `json:"clear_on_close"` + NotifyOnClose bool `json:"notify_on_close"` + RootViewID string `json:"root_view_id"` + PreviousViewID string `json:"previous_view_id"` + AppID string `json:"app_id"` + ExternalID string `json:"external_id"` + BotID string `json:"bot_id"` + AppInstalledTeamID string `json:"app_installed_team_id"` +} + +type ViewSubmissionCallbackResponseURL struct { + BlockID string `json:"block_id"` + ActionID string `json:"action_id"` + ChannelID string `json:"channel_id"` + ResponseURL string `json:"response_url"` } type ViewSubmissionCallback struct { - Hash string `json:"hash"` + Hash string `json:"hash"` + ResponseURLs []ViewSubmissionCallbackResponseURL `json:"response_urls,omitempty"` } type ViewClosedCallback struct { @@ -61,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, @@ -74,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, @@ -81,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, @@ -90,7 +139,7 @@ func NewErrorsViewSubmissionResponse(errors map[string]string) *ViewSubmissionRe type ModalViewRequest struct { Type ViewType `json:"type"` - Title *TextBlockObject `json:"title"` + Title *TextBlockObject `json:"title,omitempty"` Blocks Blocks `json:"blocks"` Close *TextBlockObject `json:"close,omitempty"` Submit *TextBlockObject `json:"submit,omitempty"` @@ -101,6 +150,12 @@ type ModalViewRequest struct { ExternalID string `json:"external_id,omitempty"` } +type PublishViewContextRequest struct { + UserID string `json:"user_id"` + View HomeTabViewRequest `json:"view"` + Hash *string `json:"hash,omitempty"` +} + func (v *ModalViewRequest) ViewType() ViewType { return v.Type } @@ -122,12 +177,6 @@ type openViewRequest struct { View ModalViewRequest `json:"view"` } -type publishViewRequest struct { - UserID string `json:"user_id"` - View HomeTabViewRequest `json:"view"` - Hash string `json:"hash,omitempty"` -} - type pushViewRequest struct { TriggerID string `json:"trigger_id"` View ModalViewRequest `json:"view"` @@ -146,11 +195,33 @@ type ViewResponse struct { } // OpenView opens a view for a user. +// For more information see the OpenViewContext documentation. func (api *Client) OpenView(triggerID string, view ModalViewRequest) (*ViewResponse, error) { return api.OpenViewContext(context.Background(), triggerID, view) } +// ValidateUniqueBlockID will verify if each input block has a unique block ID if set +func ValidateUniqueBlockID(view ModalViewRequest) bool { + + uniqueBlockID := map[string]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 + } + uniqueBlockID[inputBlock.BlockID] = true + } + } + + return true +} + // OpenViewContext opens a view for a user with a custom context. +// Slack API docs: https://docs.slack.dev/reference/methods/views.open func (api *Client) OpenViewContext( ctx context.Context, triggerID string, @@ -159,6 +230,11 @@ func (api *Client) OpenViewContext( if triggerID == "" { return nil, ErrParametersMissing } + + if !ValidateUniqueBlockID(view) { + return nil, ErrBlockIDNotUnique + } + req := openViewRequest{ TriggerID: triggerID, View: view, @@ -167,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 } @@ -177,32 +252,30 @@ 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(), userID, view, 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://docs.slack.dev/reference/methods/views.publish func (api *Client) PublishViewContext( ctx context.Context, - userID string, - view HomeTabViewRequest, - hash string, + req PublishViewContextRequest, ) (*ViewResponse, error) { - if userID == "" { + if req.UserID == "" { return nil, ErrParametersMissing } - req := publishViewRequest{ - UserID: userID, - View: view, - Hash: hash, - } encoded, err := json.Marshal(req) 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 } @@ -210,11 +283,13 @@ func (api *Client) PublishViewContext( } // PushView pushes a view onto the stack of a root view. +// For more information see the PushViewContext documentation. func (api *Client) PushView(triggerID string, view ModalViewRequest) (*ViewResponse, error) { return api.PushViewContext(context.Background(), triggerID, view) } -// PublishViewContext pushes a view onto the stack of a root view with a custom context. +// PushViewContext pushes a view onto the stack of a root view with a custom context. +// Slack API docs: https://docs.slack.dev/reference/methods/views.push func (api *Client) PushViewContext( ctx context.Context, triggerID string, @@ -231,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 } @@ -241,11 +315,13 @@ func (api *Client) PushViewContext( } // UpdateView updates an existing view. +// For more information see the UpdateViewContext documentation. func (api *Client) UpdateView(view ModalViewRequest, externalID, hash, viewID string) (*ViewResponse, error) { return api.UpdateViewContext(context.Background(), view, externalID, hash, viewID) } // UpdateViewContext updates an existing view with a custom context. +// Slack API docs: https://docs.slack.dev/reference/methods/views.update func (api *Client) UpdateViewContext( ctx context.Context, view ModalViewRequest, @@ -265,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 1016cb4..729bce4 100644 --- a/vendor/github.com/slack-go/slack/webhooks.go +++ b/vendor/github.com/slack-go/slack/webhooks.go @@ -1,7 +1,11 @@ package slack import ( + "bytes" "context" + "encoding/json" + "fmt" + "io" "net/http" ) @@ -14,6 +18,13 @@ type WebhookMessage struct { Text string `json:"text,omitempty"` Attachments []Attachment `json:"attachments,omitempty"` Parse string `json:"parse,omitempty"` + Blocks *Blocks `json:"blocks,omitempty"` + ResponseType string `json:"response_type,omitempty"` + 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"` } func PostWebhook(url string, msg *WebhookMessage) error { @@ -27,3 +38,27 @@ func PostWebhookContext(ctx context.Context, url string, msg *WebhookMessage) er func PostWebhookCustomHTTP(url string, httpClient *http.Client, msg *WebhookMessage) error { return PostWebhookCustomHTTPContext(context.Background(), url, httpClient, msg) } + +func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { + raw, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("marshal failed: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) + if err != nil { + return fmt.Errorf("failed new request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to post webhook: %w", err) + } + defer func() { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + }() + + return checkStatusCode(resp, discard{}) +} diff --git a/vendor/github.com/slack-go/slack/webhooks_go112.go b/vendor/github.com/slack-go/slack/webhooks_go112.go deleted file mode 100644 index 4e0db0e..0000000 --- a/vendor/github.com/slack-go/slack/webhooks_go112.go +++ /dev/null @@ -1,34 +0,0 @@ -// +build !go1.13 - -package slack - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - - "github.com/pkg/errors" -) - -func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { - raw, err := json.Marshal(msg) - if err != nil { - return errors.Wrap(err, "marshal failed") - } - - req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(raw)) - if err != nil { - return errors.Wrap(err, "failed new request") - } - req = req.WithContext(ctx) - req.Header.Set("Content-Type", "application/json") - - resp, err := httpClient.Do(req) - if err != nil { - return errors.Wrap(err, "failed to post webhook") - } - defer resp.Body.Close() - - return checkStatusCode(resp, discard{}) -} diff --git a/vendor/github.com/slack-go/slack/webhooks_go113.go b/vendor/github.com/slack-go/slack/webhooks_go113.go deleted file mode 100644 index 99c243f..0000000 --- a/vendor/github.com/slack-go/slack/webhooks_go113.go +++ /dev/null @@ -1,33 +0,0 @@ -// +build go1.13 - -package slack - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - - "github.com/pkg/errors" -) - -func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *http.Client, msg *WebhookMessage) error { - raw, err := json.Marshal(msg) - if err != nil { - return errors.Wrap(err, "marshal failed") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) - if err != nil { - return errors.Wrap(err, "failed new request") - } - req.Header.Set("Content-Type", "application/json") - - resp, err := httpClient.Do(req) - if err != nil { - return errors.Wrap(err, "failed to post webhook") - } - defer resp.Body.Close() - - return checkStatusCode(resp, discard{}) -} diff --git a/vendor/github.com/slack-go/slack/websocket_groups.go b/vendor/github.com/slack-go/slack/websocket_groups.go index eb88985..c35d5f3 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_internals.go b/vendor/github.com/slack-go/slack/websocket_internals.go index 3e1906e..454a213 100644 --- a/vendor/github.com/slack-go/slack/websocket_internals.go +++ b/vendor/github.com/slack-go/slack/websocket_internals.go @@ -94,6 +94,7 @@ func (i *IncomingEventError) Error() string { // AckErrorEvent i type AckErrorEvent struct { ErrorObj error + ReplyTo int } func (a *AckErrorEvent) Error() string { 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 8607b3a..da861fa 100644 --- a/vendor/github.com/slack-go/slack/websocket_managed_conn.go +++ b/vendor/github.com/slack-go/slack/websocket_managed_conn.go @@ -10,10 +10,37 @@ import ( "time" "github.com/gorilla/websocket" + + "github.com/slack-go/slack/internal/backoff" "github.com/slack-go/slack/internal/errorsx" "github.com/slack-go/slack/internal/timex" ) +// UnmappedError represents error occurred when there is no mapping between given event name +// and corresponding Go struct. +type UnmappedError struct { + // EventType returns event type name. + EventType string + // RawEvent returns raw event body. + RawEvent json.RawMessage + + ctxMsg string +} + +// NewUnmappedError returns new UnmappedError instance. +func NewUnmappedError(ctxMsg, eventType string, raw json.RawMessage) *UnmappedError { + return &UnmappedError{ + ctxMsg: ctxMsg, + EventType: eventType, + RawEvent: raw, + } +} + +// Error returns human-readable error message. +func (u UnmappedError) Error() string { + return fmt.Sprintf("%s: Received unmapped event %q", u.ctxMsg, u.EventType) +} + // ManageConnection can be called on a Slack RTM instance returned by the // NewRTM method. It will connect to the slack RTM API and handle all incoming // and outgoing events. If a connection fails then it will attempt to reconnect @@ -88,11 +115,12 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke errInvalidAuth = "invalid_auth" errInactiveAccount = "account_inactive" errMissingAuthToken = "not_authed" + errTokenRevoked = "token_revoked" ) // used to provide exponential backoff wait time with jitter before trying // to connect to slack again - boff := &backoff{ + boff := &backoff.Backoff{ Max: 5 * time.Minute, } @@ -103,7 +131,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // send connecting event rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{ - Attempt: boff.attempts + 1, + Attempt: boff.Attempts() + 1, ConnectionCount: connectionCount, }} @@ -115,7 +143,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // check for fatal errors switch err.Error() { - case errInvalidAuth, errInactiveAccount, errMissingAuthToken: + case errInvalidAuth, errInactiveAccount, errMissingAuthToken, errTokenRevoked: rtm.Debugf("invalid auth when connecting with RTM: %s", err) rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} return nil, nil, err @@ -123,7 +151,7 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke } switch actual := err.(type) { - case statusCodeError: + case StatusCodeError: if actual.Code == http.StatusNotFound { rtm.Debugf("invalid auth when connecting with RTM: %s", err) rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} @@ -138,13 +166,13 @@ func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocke // any other errors are treated as recoverable and we try again after // sending the event along the IncomingEvents channel rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{ - Attempt: boff.attempts, + Attempt: boff.Attempts(), Backoff: backoff, ErrorObj: err, }} // get time we should wait before attempting to connect again - rtm.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.attempts, err, backoff) + rtm.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.Attempts(), err, backoff) // wait for one of the following to occur, // backoff duration has elapsed, killChannel is signalled, or @@ -435,10 +463,10 @@ func (rtm *RTM) handleAck(event json.RawMessage) { if ack.RTMResponse.Error.Code == -1 && ack.RTMResponse.Error.Msg == "slow down, too many messages..." { rtm.IncomingEvents <- RTMEvent{"ack_error", &RateLimitEvent{}} } else { - rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}} + rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error, ack.ReplyTo}} } } else { - rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{fmt.Errorf("ack decode failure")}} + rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ErrorObj: fmt.Errorf("ack decode failure")}} } } @@ -471,7 +499,7 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { v, exists := EventMapping[typeStr] if !exists { rtm.Debugf("RTM Error - received unmapped event %q: %s\n", typeStr, string(event)) - err := fmt.Errorf("RTM Error: Received unmapped event %q: %s", typeStr, string(event)) + err := NewUnmappedError("RTM Error", typeStr, event) rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} return } @@ -480,7 +508,7 @@ func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { err := json.Unmarshal(event, recvEvent) if err != nil { rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event)) - err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Could not unmarshall event %q", typeStr) rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} return } @@ -554,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{}, @@ -567,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{}, @@ -580,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 65a8bb6..fb301f5 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/websocket_reactions.go b/vendor/github.com/slack-go/slack/websocket_reactions.go index e497387..6098a6c 100644 --- a/vendor/github.com/slack-go/slack/websocket_reactions.go +++ b/vendor/github.com/slack-go/slack/websocket_reactions.go @@ -1,7 +1,7 @@ package slack -// reactionItem is a lighter-weight item than is returned by the reactions list. -type reactionItem struct { +// ReactionItem is a lighter-weight item than is returned by the reactions list. +type ReactionItem struct { Type string `json:"type"` Channel string `json:"channel,omitempty"` File string `json:"file,omitempty"` @@ -9,17 +9,17 @@ type reactionItem struct { Timestamp string `json:"ts,omitempty"` } -type reactionEvent struct { +type ReactionEvent struct { Type string `json:"type"` User string `json:"user"` ItemUser string `json:"item_user"` - Item reactionItem `json:"item"` + Item ReactionItem `json:"item"` Reaction string `json:"reaction"` EventTimestamp string `json:"event_ts"` } // ReactionAddedEvent represents the Reaction added event -type ReactionAddedEvent reactionEvent +type ReactionAddedEvent ReactionEvent // ReactionRemovedEvent represents the Reaction removed event -type ReactionRemovedEvent reactionEvent +type ReactionRemovedEvent ReactionEvent diff --git a/vendor/github.com/slack-go/slack/websocket_subteam.go b/vendor/github.com/slack-go/slack/websocket_subteam.go index a23b274..2f61873 100644 --- a/vendor/github.com/slack-go/slack/websocket_subteam.go +++ b/vendor/github.com/slack-go/slack/websocket_subteam.go @@ -14,9 +14,9 @@ type SubteamMembersChangedEvent struct { DatePreviousUpdate JSONTime `json:"date_previous_update"` DateUpdate JSONTime `json:"date_update"` AddedUsers []string `json:"added_users"` - AddedUsersCount string `json:"added_users_count"` + AddedUsersCount int `json:"added_users_count"` RemovedUsers []string `json:"removed_users"` - RemovedUsersCount string `json:"removed_users_count"` + RemovedUsersCount int `json:"removed_users_count"` } // SubteamSelfAddedEvent represents an event of you have been added to a User Group 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 0000000..08b3398 --- /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 new file mode 100644 index 0000000..3483480 --- /dev/null +++ b/vendor/github.com/slack-go/slack/workflows_triggers.go @@ -0,0 +1,177 @@ +package slack + +import ( + "context" + "encoding/json" + "fmt" +) + +type ( + WorkflowsTriggersPermissionsAddInput struct { + TriggerId string `json:"trigger_id"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsAddOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsListInput struct { + TriggerId string `json:"trigger_id"` + } + + WorkflowsTriggersPermissionsListOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsRemoveInput struct { + TriggerId string `json:"trigger_id"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsRemoveOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsSetInput struct { + PermissionType string `json:"permission_type"` + TriggerId string `json:"trigger_id"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } + + WorkflowsTriggersPermissionsSetOutput struct { + PermissionType string `json:"permission_type"` + ChannelIds []string `json:"channel_ids,omitempty"` + OrgIds []string `json:"org_ids,omitempty"` + TeamIds []string `json:"team_ids,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + } +) + +// WorkflowsTriggersPermissionsAdd allows users to run a trigger that has its permission +// type set to named_entities. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.add +func (api *Client) WorkflowsTriggersPermissionsAdd(ctx context.Context, input *WorkflowsTriggersPermissionsAddInput) (*WorkflowsTriggersPermissionsAddOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsAddOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsAddInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.add", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsAddOutput, nil +} + +// WorkflowsTriggersPermissionsList returns the permission type of a trigger and if +// applicable, includes the entities that have been granted access. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.list +func (api *Client) WorkflowsTriggersPermissionsList(ctx context.Context, input *WorkflowsTriggersPermissionsListInput) (*WorkflowsTriggersPermissionsListOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsListOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsListInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.list", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsListOutput, nil +} + +// WorkflowsTriggersPermissionsRemove revoke an entity's access to a trigger that has its +// permission type set to named_entities. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.remove +func (api *Client) WorkflowsTriggersPermissionsRemove(ctx context.Context, input *WorkflowsTriggersPermissionsRemoveInput) (*WorkflowsTriggersPermissionsRemoveOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsRemoveOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsRemoveInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.remove", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsRemoveOutput, nil +} + +// WorkflowsTriggersPermissionsSet sets the permission type for who can run a trigger. +// +// Slack API Docs:https://api.slack.com/methods/workflows.triggers.permissions.set +func (api *Client) WorkflowsTriggersPermissionsSet(ctx context.Context, input *WorkflowsTriggersPermissionsSetInput) (*WorkflowsTriggersPermissionsSetOutput, error) { + response := struct { + SlackResponse + *WorkflowsTriggersPermissionsSetOutput + }{} + + jsonPayload, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal WorkflowsTriggersPermissionsSetInput: %w", err) + } + + err = api.postJSONMethod(ctx, "workflows.triggers.permissions.set", api.token, jsonPayload, &response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response.WorkflowsTriggersPermissionsSetOutput, nil +} diff --git a/vendor/github.com/urfave/cli/go.mod b/vendor/github.com/urfave/cli/go.mod deleted file mode 100644 index d5b55c4..0000000 --- a/vendor/github.com/urfave/cli/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/urfave/cli - -go 1.12 - -require ( - github.com/BurntSushi/toml v0.3.1 - gopkg.in/yaml.v2 v2.2.2 -) diff --git a/vendor/github.com/urfave/cli/go.sum b/vendor/github.com/urfave/cli/go.sum deleted file mode 100644 index a2ea702..0000000 --- a/vendor/github.com/urfave/cli/go.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/valyala/fastjson/go.mod b/vendor/github.com/valyala/fastjson/go.mod deleted file mode 100644 index c85c656..0000000 --- a/vendor/github.com/valyala/fastjson/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/valyala/fastjson - -go 1.12 diff --git a/vendor/gopkg.in/AlecAivazis/survey.v1/go.mod b/vendor/gopkg.in/AlecAivazis/survey.v1/go.mod deleted file mode 100644 index 42917d1..0000000 --- a/vendor/gopkg.in/AlecAivazis/survey.v1/go.mod +++ /dev/null @@ -1,16 +0,0 @@ -module gopkg.in/AlecAivazis/survey.v1 - -require ( - github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 - github.com/davecgh/go-spew v1.1.0 // indirect - github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/kr/pty v1.1.1 // indirect - github.com/mattn/go-colorable v0.0.9 // indirect - github.com/mattn/go-isatty v0.0.3 - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.1 - golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b // indirect - golang.org/x/sys v0.0.0-20180606202747-9527bec2660b // indirect -) diff --git a/vendor/gopkg.in/AlecAivazis/survey.v1/go.sum b/vendor/gopkg.in/AlecAivazis/survey.v1/go.sum deleted file mode 100644 index fec8360..0000000 --- a/vendor/gopkg.in/AlecAivazis/survey.v1/go.sum +++ /dev/null @@ -1,24 +0,0 @@ -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= -github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= -github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b h1:Elez2XeF2p9uyVj0yEUDqQ56NFcDtcBNkYP7yv8YbUE= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/sys v0.0.0-20180606202747-9527bec2660b h1:5rOiLYVqtE+JehJPVJTXQJaP8aT3cpJC1Iy22+5WLFU= -golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/vendor/gopkg.in/yaml.v2/go.mod b/vendor/gopkg.in/yaml.v2/go.mod deleted file mode 100644 index 1934e87..0000000 --- a/vendor/gopkg.in/yaml.v2/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module "gopkg.in/yaml.v2" - -require ( - "gopkg.in/check.v1" v0.0.0-20161208181325-20d25e280405 -) diff --git a/vendor/modules.txt b/vendor/modules.txt index 030362e..c38b9f8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,8 @@ # github.com/fatih/color v1.7.0 +## explicit github.com/fatih/color # github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f +## explicit github.com/git-chglog/git-chglog github.com/git-chglog/git-chglog/cmd/git-chglog # github.com/go-redis/redis v6.15.7+incompatible @@ -13,71 +15,85 @@ github.com/go-redis/redis/internal/pool github.com/go-redis/redis/internal/proto github.com/go-redis/redis/internal/util # github.com/google/go-cmp v0.4.0 -## explicit +## explicit; go 1.8 github.com/google/go-cmp/cmp github.com/google/go-cmp/cmp/internal/diff github.com/google/go-cmp/cmp/internal/flags github.com/google/go-cmp/cmp/internal/function github.com/google/go-cmp/cmp/internal/value -# github.com/gorilla/websocket v1.2.0 +# github.com/gorilla/websocket v1.5.3 +## explicit; go 1.12 github.com/gorilla/websocket # github.com/heroku/x v0.0.22 -## explicit +## explicit; go 1.12 github.com/heroku/x/hmetrics github.com/heroku/x/hmetrics/onload # github.com/imdario/mergo v0.3.7 +## explicit github.com/imdario/mergo # github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 +## explicit github.com/kballard/go-shellquote # github.com/mattn/go-colorable v0.1.2 +## explicit github.com/mattn/go-colorable # github.com/mattn/go-isatty v0.0.8 +## explicit github.com/mattn/go-isatty # github.com/mattn/goveralls v0.0.2 +## explicit github.com/mattn/goveralls # github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b +## explicit github.com/mgutz/ansi # github.com/onsi/ginkgo v1.10.1 ## explicit # github.com/onsi/gomega v1.7.0 ## explicit # github.com/pkg/errors v0.8.2-0.20190227000051-27936f6d90f9 +## explicit github.com/pkg/errors # github.com/robinjoseph08/redisqueue v1.1.0 -## explicit +## explicit; go 1.12 github.com/robinjoseph08/redisqueue # github.com/rs/zerolog v1.18.0 ## explicit github.com/rs/zerolog github.com/rs/zerolog/internal/cbor github.com/rs/zerolog/internal/json -# github.com/slack-go/slack v0.6.4 -## explicit +# 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 github.com/slack-go/slack/internal/timex github.com/slack-go/slack/slackevents github.com/slack-go/slack/slackutilsx # github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df +## explicit github.com/tsuyoshiwada/go-gitcmd # github.com/urfave/cli v1.21.0 +## explicit; go 1.12 github.com/urfave/cli # github.com/valyala/fastjson v1.5.1 -## explicit +## explicit; go 1.12 github.com/valyala/fastjson github.com/valyala/fastjson/fastfloat # golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 +## explicit; go 1.12 golang.org/x/sys/unix # golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c -## explicit +## explicit; go 1.11 golang.org/x/tools/cover # gopkg.in/AlecAivazis/survey.v1 v1.8.5 +## explicit gopkg.in/AlecAivazis/survey.v1 gopkg.in/AlecAivazis/survey.v1/core gopkg.in/AlecAivazis/survey.v1/terminal # gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 ## explicit # gopkg.in/kyokomi/emoji.v1 v1.5.1 +## explicit gopkg.in/kyokomi/emoji.v1 # gopkg.in/yaml.v2 v2.2.4 ## explicit