From 0feab9dd060a2ea792434f1acc72d9fadd5f299b Mon Sep 17 00:00:00 2001 From: Spyros Date: Mon, 18 Aug 2025 21:54:33 +0100 Subject: [PATCH 1/3] make the Slack reporter start threads and report on threads with templates exactly like the Discord component would. -- Dependencies --- components/reporters/slack/go.mod | 8 +- components/reporters/slack/go.sum | 12 +- .../github.com/cenkalti/backoff/v5/.gitignore | 25 + .../cenkalti/backoff/v5/CHANGELOG.md | 29 + .../github.com/cenkalti/backoff/v5/LICENSE | 20 + .../github.com/cenkalti/backoff/v5/README.md | 31 + .../github.com/cenkalti/backoff/v5/backoff.go | 66 + .../github.com/cenkalti/backoff/v5/error.go | 46 + .../cenkalti/backoff/v5/exponential.go | 125 ++ .../github.com/cenkalti/backoff/v5/retry.go | 139 ++ .../github.com/cenkalti/backoff/v5/ticker.go | 83 ++ .../github.com/cenkalti/backoff/v5/timer.go | 35 + .../github.com/gorilla/websocket/.gitignore | 25 + .../github.com/gorilla/websocket/AUTHORS | 9 + .../github.com/gorilla/websocket/LICENSE | 22 + .../github.com/gorilla/websocket/README.md | 33 + .../github.com/gorilla/websocket/client.go | 434 ++++++ .../gorilla/websocket/compression.go | 148 ++ .../github.com/gorilla/websocket/conn.go | 1238 +++++++++++++++++ .../github.com/gorilla/websocket/doc.go | 227 +++ .../github.com/gorilla/websocket/join.go | 42 + .../github.com/gorilla/websocket/json.go | 60 + .../github.com/gorilla/websocket/mask.go | 55 + .../github.com/gorilla/websocket/mask_safe.go | 16 + .../github.com/gorilla/websocket/prepared.go | 102 ++ .../github.com/gorilla/websocket/proxy.go | 77 + .../github.com/gorilla/websocket/server.go | 365 +++++ .../gorilla/websocket/tls_handshake.go | 21 + .../gorilla/websocket/tls_handshake_116.go | 21 + .../github.com/gorilla/websocket/util.go | 298 ++++ .../gorilla/websocket/x_net_proxy.go | 473 +++++++ .../github.com/slack-go/slack/.gitignore | 5 + .../github.com/slack-go/slack/.golangci.yml | 35 + .../github.com/slack-go/slack/CONTRIBUTING.md | 40 + .../vendor/github.com/slack-go/slack/LICENSE | 23 + .../vendor/github.com/slack-go/slack/Makefile | 36 + .../github.com/slack-go/slack/README.md | 111 ++ .../vendor/github.com/slack-go/slack/admin.go | 207 +++ .../slack-go/slack/admin_conversations.go | 85 ++ .../vendor/github.com/slack-go/slack/apps.go | 72 + .../github.com/slack-go/slack/assistant.go | 157 +++ .../github.com/slack-go/slack/attachments.go | 98 ++ .../vendor/github.com/slack-go/slack/audit.go | 152 ++ .../vendor/github.com/slack-go/slack/auth.go | 82 ++ .../vendor/github.com/slack-go/slack/block.go | 85 ++ .../github.com/slack-go/slack/block_action.go | 31 + .../github.com/slack-go/slack/block_call.go | 28 + .../slack-go/slack/block_context.go | 37 + .../github.com/slack-go/slack/block_conv.go | 456 ++++++ .../slack-go/slack/block_divider.go | 26 + .../slack-go/slack/block_element.go | 813 +++++++++++ .../github.com/slack-go/slack/block_file.go | 31 + .../github.com/slack-go/slack/block_header.go | 43 + .../github.com/slack-go/slack/block_image.go | 55 + .../github.com/slack-go/slack/block_input.go | 47 + .../slack-go/slack/block_markdown.go | 34 + .../github.com/slack-go/slack/block_object.go | 274 ++++ .../slack-go/slack/block_rich_text.go | 550 ++++++++ .../slack-go/slack/block_section.go | 57 + .../slack-go/slack/block_unknown.go | 18 + .../github.com/slack-go/slack/block_video.go | 70 + .../github.com/slack-go/slack/bookmarks.go | 169 +++ .../vendor/github.com/slack-go/slack/bots.go | 69 + .../vendor/github.com/slack-go/slack/calls.go | 216 +++ .../github.com/slack-go/slack/canvas.go | 264 ++++ .../github.com/slack-go/slack/channels.go | 37 + .../vendor/github.com/slack-go/slack/chat.go | 926 ++++++++++++ .../github.com/slack-go/slack/comment.go | 10 + .../github.com/slack-go/slack/conversation.go | 914 ++++++++++++ .../github.com/slack-go/slack/dialog.go | 120 ++ .../slack-go/slack/dialog_select.go | 115 ++ .../github.com/slack-go/slack/dialog_text.go | 59 + .../vendor/github.com/slack-go/slack/dnd.go | 160 +++ .../vendor/github.com/slack-go/slack/emoji.go | 37 + .../github.com/slack-go/slack/errors.go | 21 + .../vendor/github.com/slack-go/slack/files.go | 671 +++++++++ .../slack-go/slack/function_execute.go | 93 ++ .../github.com/slack-go/slack/groups.go | 7 + .../github.com/slack-go/slack/history.go | 37 + .../vendor/github.com/slack-go/slack/im.go | 21 + .../vendor/github.com/slack-go/slack/info.go | 479 +++++++ .../github.com/slack-go/slack/interactions.go | 250 ++++ .../slack/internal/backoff/backoff.go | 62 + .../slack/internal/errorsx/errorsx.go | 17 + .../slack-go/slack/internal/timex/timex.go | 18 + .../vendor/github.com/slack-go/slack/item.go | 75 + .../github.com/slack-go/slack/logger.go | 60 + .../vendor/github.com/slack-go/slack/logo.png | Bin 0 -> 52440 bytes .../github.com/slack-go/slack/manifests.go | 297 ++++ .../github.com/slack-go/slack/messageID.go | 30 + .../github.com/slack-go/slack/messages.go | 264 ++++ .../github.com/slack-go/slack/metadata.go | 7 + .../github.com/slack-go/slack/migration.go | 40 + .../vendor/github.com/slack-go/slack/misc.go | 445 ++++++ .../vendor/github.com/slack-go/slack/oauth.go | 200 +++ .../github.com/slack-go/slack/pagination.go | 20 + .../vendor/github.com/slack-go/slack/pins.go | 100 ++ .../github.com/slack-go/slack/reactions.go | 282 ++++ .../github.com/slack-go/slack/reminders.go | 118 ++ .../github.com/slack-go/slack/remotefiles.go | 309 ++++ .../vendor/github.com/slack-go/slack/rtm.go | 137 ++ .../github.com/slack-go/slack/search.go | 160 +++ .../github.com/slack-go/slack/security.go | 108 ++ .../vendor/github.com/slack-go/slack/slack.go | 174 +++ .../slack-go/slack/slackutilsx/slackutilsx.go | 64 + .../vendor/github.com/slack-go/slack/slash.go | 91 ++ .../github.com/slack-go/slack/socket_mode.go | 43 + .../vendor/github.com/slack-go/slack/stars.go | 269 ++++ .../slack-go/slack/status_code_error.go | 28 + .../vendor/github.com/slack-go/slack/team.go | 250 ++++ .../github.com/slack-go/slack/tokens.go | 52 + .../github.com/slack-go/slack/usergroups.go | 570 ++++++++ .../vendor/github.com/slack-go/slack/users.go | 760 ++++++++++ .../vendor/github.com/slack-go/slack/views.go | 307 ++++ .../github.com/slack-go/slack/webhooks.go | 64 + .../github.com/slack-go/slack/websocket.go | 103 ++ .../slack-go/slack/websocket_channels.go | 72 + .../slack/websocket_desktop_notification.go | 19 + .../github.com/slack-go/slack/websocket_dm.go | 23 + .../slack-go/slack/websocket_dnd.go | 8 + .../slack-go/slack/websocket_files.go | 49 + .../slack-go/slack/websocket_groups.go | 49 + .../slack-go/slack/websocket_internals.go | 102 ++ .../slack-go/slack/websocket_managed_conn.go | 611 ++++++++ .../slack-go/slack/websocket_misc.go | 141 ++ .../websocket_mobile_in_app_notification.go | 20 + .../slack-go/slack/websocket_pins.go | 16 + .../slack-go/slack/websocket_reactions.go | 25 + .../slack-go/slack/websocket_stars.go | 14 + .../slack-go/slack/websocket_subteam.go | 35 + .../slack-go/slack/websocket_teams.go | 33 + .../slack-go/slack/workflows_triggers.go | 177 +++ .../smithy-security/pkg/retry/LICENSE | 21 + .../smithy-security/pkg/retry/README.md | 38 + .../smithy-security/pkg/retry/logger.go | 11 + .../smithy-security/pkg/retry/retry.go | 227 +++ .../vendor/go.uber.org/mock/gomock/call.go | 506 +++++++ .../vendor/go.uber.org/mock/gomock/callset.go | 164 +++ .../go.uber.org/mock/gomock/controller.go | 326 +++++ .../vendor/go.uber.org/mock/gomock/doc.go | 60 + .../go.uber.org/mock/gomock/matchers.go | 447 ++++++ .../vendor/go.uber.org/mock/gomock/string.go | 36 + components/reporters/slack/vendor/modules.txt | 17 + 143 files changed, 21485 insertions(+), 4 deletions(-) create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/.gitignore create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/CHANGELOG.md create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/LICENSE create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/README.md create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/backoff.go create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/error.go create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/exponential.go create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/retry.go create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/ticker.go create mode 100644 components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/timer.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/.gitignore create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/AUTHORS create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/LICENSE create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/README.md create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/client.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/compression.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/conn.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/doc.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/join.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/json.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/mask.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/mask_safe.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/prepared.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/proxy.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/server.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/tls_handshake.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/tls_handshake_116.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/util.go create mode 100644 components/reporters/slack/vendor/github.com/gorilla/websocket/x_net_proxy.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/.gitignore create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/.golangci.yml create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/CONTRIBUTING.md create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/LICENSE create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/Makefile create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/README.md create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/admin.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/admin_conversations.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/apps.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/assistant.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/attachments.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/audit.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/auth.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_action.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_call.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_context.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_conv.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_divider.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_element.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_file.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_header.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_image.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_input.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_markdown.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_object.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_rich_text.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_section.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_unknown.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/block_video.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/bookmarks.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/bots.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/calls.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/canvas.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/channels.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/chat.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/comment.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/conversation.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/dialog.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/dialog_select.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/dialog_text.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/dnd.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/emoji.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/errors.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/files.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/function_execute.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/groups.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/history.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/im.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/info.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/interactions.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/internal/backoff/backoff.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/internal/timex/timex.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/item.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/logger.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/logo.png create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/manifests.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/messageID.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/messages.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/metadata.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/migration.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/misc.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/oauth.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/pagination.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/pins.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/reactions.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/reminders.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/remotefiles.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/rtm.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/search.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/security.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/slack.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/slackutilsx/slackutilsx.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/slash.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/socket_mode.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/stars.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/status_code_error.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/team.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/tokens.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/usergroups.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/users.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/views.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/webhooks.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_channels.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_desktop_notification.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_dm.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_dnd.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_files.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_groups.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_internals.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_managed_conn.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_misc.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_mobile_in_app_notification.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_pins.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_reactions.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_stars.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_subteam.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/websocket_teams.go create mode 100644 components/reporters/slack/vendor/github.com/slack-go/slack/workflows_triggers.go create mode 100644 components/reporters/slack/vendor/github.com/smithy-security/pkg/retry/LICENSE create mode 100644 components/reporters/slack/vendor/github.com/smithy-security/pkg/retry/README.md create mode 100644 components/reporters/slack/vendor/github.com/smithy-security/pkg/retry/logger.go create mode 100644 components/reporters/slack/vendor/github.com/smithy-security/pkg/retry/retry.go create mode 100644 components/reporters/slack/vendor/go.uber.org/mock/gomock/call.go create mode 100644 components/reporters/slack/vendor/go.uber.org/mock/gomock/callset.go create mode 100644 components/reporters/slack/vendor/go.uber.org/mock/gomock/controller.go create mode 100644 components/reporters/slack/vendor/go.uber.org/mock/gomock/doc.go create mode 100644 components/reporters/slack/vendor/go.uber.org/mock/gomock/matchers.go create mode 100644 components/reporters/slack/vendor/go.uber.org/mock/gomock/string.go diff --git a/components/reporters/slack/go.mod b/components/reporters/slack/go.mod index 65a900464..787a624f1 100644 --- a/components/reporters/slack/go.mod +++ b/components/reporters/slack/go.mod @@ -4,9 +4,13 @@ go 1.23.4 require ( github.com/go-errors/errors v1.5.1 + github.com/slack-go/slack v0.17.3 github.com/smithy-security/pkg/env v0.0.3 + github.com/smithy-security/pkg/retry v0.0.3 + github.com/smithy-security/pkg/utils v0.0.2 github.com/smithy-security/smithy/sdk v0.0.19-alpha github.com/stretchr/testify v1.10.0 + go.uber.org/mock v0.5.0 google.golang.org/protobuf v1.36.5 ) @@ -20,12 +24,14 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/inflect v0.21.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/hcl/v2 v2.23.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -43,14 +49,12 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/smithy-security/pkg/utils v0.0.2 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/sqlc-dev/sqlc v1.28.0 // indirect github.com/urfave/cli/v2 v2.27.6 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/zclconf/go-cty v1.16.2 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect - go.uber.org/mock v0.5.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.37.0 // indirect diff --git a/components/reporters/slack/go.sum b/components/reporters/slack/go.sum index 820785f98..1582aff50 100644 --- a/components/reporters/slack/go.sum +++ b/components/reporters/slack/go.sum @@ -28,6 +28,8 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oM github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= @@ -53,8 +55,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/inflect v0.21.2 h1:0gClGlGcxifcJR56zwvhaOulnNgnhc4qTAkob5ObnSM= github.com/go-openapi/inflect v0.21.2/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= -github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= -github.com/go-test/deep v1.0.3/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/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= @@ -67,6 +69,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -127,8 +131,12 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g= +github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk= github.com/smithy-security/pkg/env v0.0.3 h1:eZYRzzFAzWkAJ2OMMhEQr0xSL2mk6cOGpEqcR28QWXM= github.com/smithy-security/pkg/env v0.0.3/go.mod h1:VIJfDqeAbQQcmohaXcZI6grjeJC9Y8CmqR4ITpdngZE= +github.com/smithy-security/pkg/retry v0.0.3 h1:Zcea0m13C7tO+OehN3bU9Spz4wW6P0Ok6pqNi52qCg4= +github.com/smithy-security/pkg/retry v0.0.3/go.mod h1:etMizy7PyMKk6EFDRAjjTEwqCEriuNmIrhV/aSs6Xho= github.com/smithy-security/pkg/utils v0.0.2 h1:r1Gz5eki8xUJXShw4i5ZaizkiKgZlYNYtKE2PDwpoHQ= github.com/smithy-security/pkg/utils v0.0.2/go.mod h1:bzCtRv/q9BdCrALRkcWWW3y8DzugbZrEQPwgZ/iepig= github.com/smithy-security/smithy/sdk v0.0.19-alpha h1:c+DKDLMNmv6dMu2QQyLUou/Rxx+4c73aO8jmEEK16Pw= diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/.gitignore b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/.gitignore new file mode 100644 index 000000000..50d95c548 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +# IDEs +.idea/ diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/CHANGELOG.md b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/CHANGELOG.md new file mode 100644 index 000000000..658c37436 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/CHANGELOG.md @@ -0,0 +1,29 @@ +# 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). + +## [5.0.0] - 2024-12-19 + +### Added + +- RetryAfterError can be returned from an operation to indicate how long to wait before the next retry. + +### Changed + +- Retry function now accepts additional options for specifying max number of tries and max elapsed time. +- Retry function now accepts a context.Context. +- Operation function signature changed to return result (any type) and error. + +### Removed + +- RetryNotify* and RetryWithData functions. Only single Retry function remains. +- Optional arguments from ExponentialBackoff constructor. +- Clock and Timer interfaces. + +### Fixed + +- The original error is returned from Retry if there's a PermanentError. (#144) +- The Retry function respects the wrapped PermanentError. (#140) diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/LICENSE b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/LICENSE new file mode 100644 index 000000000..89b817996 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Cenk Altı + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/README.md b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/README.md new file mode 100644 index 000000000..4611b1d17 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/README.md @@ -0,0 +1,31 @@ +# Exponential Backoff [![GoDoc][godoc image]][godoc] + +This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. + +[Exponential backoff][exponential backoff wiki] +is an algorithm that uses feedback to multiplicatively decrease the rate of some process, +in order to gradually find an acceptable rate. +The retries exponentially increase and stop increasing when a certain threshold is met. + +## Usage + +Import path is `github.com/cenkalti/backoff/v5`. Please note the version part at the end. + +For most cases, use `Retry` function. See [example_test.go][example] for an example. + +If you have specific needs, copy `Retry` function (from [retry.go][retry-src]) into your code and modify it as needed. + +## Contributing + +* I would like to keep this library as small as possible. +* Please don't send a PR without opening an issue and discussing it first. +* If proposed change is not a common use case, I will probably not accept it. + +[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v5 +[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png + +[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java +[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff + +[retry-src]: https://github.com/cenkalti/backoff/blob/v5/retry.go +[example]: https://github.com/cenkalti/backoff/blob/v5/example_test.go diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/backoff.go b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/backoff.go new file mode 100644 index 000000000..dd2b24ca7 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/backoff.go @@ -0,0 +1,66 @@ +// Package backoff implements backoff algorithms for retrying operations. +// +// Use Retry function for retrying operations that may fail. +// If Retry does not meet your needs, +// copy/paste the function into your project and modify as you wish. +// +// There is also Ticker type similar to time.Ticker. +// You can use it if you need to work with channels. +// +// See Examples section below for usage examples. +package backoff + +import "time" + +// BackOff is a backoff policy for retrying an operation. +type BackOff interface { + // NextBackOff returns the duration to wait before retrying the operation, + // backoff.Stop to indicate that no more retries should be made. + // + // Example usage: + // + // duration := backoff.NextBackOff() + // if duration == backoff.Stop { + // // Do not retry operation. + // } else { + // // Sleep for duration and retry operation. + // } + // + NextBackOff() time.Duration + + // Reset to initial state. + Reset() +} + +// Stop indicates that no more retries should be made for use in NextBackOff(). +const Stop time.Duration = -1 + +// ZeroBackOff is a fixed backoff policy whose backoff time is always zero, +// meaning that the operation is retried immediately without waiting, indefinitely. +type ZeroBackOff struct{} + +func (b *ZeroBackOff) Reset() {} + +func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 } + +// StopBackOff is a fixed backoff policy that always returns backoff.Stop for +// NextBackOff(), meaning that the operation should never be retried. +type StopBackOff struct{} + +func (b *StopBackOff) Reset() {} + +func (b *StopBackOff) NextBackOff() time.Duration { return Stop } + +// ConstantBackOff is a backoff policy that always returns the same backoff delay. +// This is in contrast to an exponential backoff policy, +// which returns a delay that grows longer as you call NextBackOff() over and over again. +type ConstantBackOff struct { + Interval time.Duration +} + +func (b *ConstantBackOff) Reset() {} +func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval } + +func NewConstantBackOff(d time.Duration) *ConstantBackOff { + return &ConstantBackOff{Interval: d} +} diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/error.go b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/error.go new file mode 100644 index 000000000..beb2b38a2 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/error.go @@ -0,0 +1,46 @@ +package backoff + +import ( + "fmt" + "time" +) + +// PermanentError signals that the operation should not be retried. +type PermanentError struct { + Err error +} + +// Permanent wraps the given err in a *PermanentError. +func Permanent(err error) error { + if err == nil { + return nil + } + return &PermanentError{ + Err: err, + } +} + +// Error returns a string representation of the Permanent error. +func (e *PermanentError) Error() string { + return e.Err.Error() +} + +// Unwrap returns the wrapped error. +func (e *PermanentError) Unwrap() error { + return e.Err +} + +// RetryAfterError signals that the operation should be retried after the given duration. +type RetryAfterError struct { + Duration time.Duration +} + +// RetryAfter returns a RetryAfter error that specifies how long to wait before retrying. +func RetryAfter(seconds int) error { + return &RetryAfterError{Duration: time.Duration(seconds) * time.Second} +} + +// Error returns a string representation of the RetryAfter error. +func (e *RetryAfterError) Error() string { + return fmt.Sprintf("retry after %s", e.Duration) +} diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/exponential.go b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/exponential.go new file mode 100644 index 000000000..c1f3e442d --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/exponential.go @@ -0,0 +1,125 @@ +package backoff + +import ( + "math/rand" + "time" +) + +/* +ExponentialBackOff is a backoff implementation that increases the backoff +period for each retry attempt using a randomization function that grows exponentially. + +NextBackOff() is calculated using the following formula: + + randomized interval = + RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) + +In other words NextBackOff() will range between the randomization factor +percentage below and above the retry interval. + +For example, given the following parameters: + + RetryInterval = 2 + RandomizationFactor = 0.5 + Multiplier = 2 + +the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, +multiplied by the exponential, that is, between 2 and 6 seconds. + +Note: MaxInterval caps the RetryInterval and not the randomized interval. + +If the time elapsed since an ExponentialBackOff instance is created goes past the +MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop. + +The elapsed time can be reset by calling Reset(). + +Example: Given the following default arguments, for 10 tries the sequence will be, +and assuming we go over the MaxElapsedTime on the 10th try: + + Request # RetryInterval (seconds) Randomized Interval (seconds) + + 1 0.5 [0.25, 0.75] + 2 0.75 [0.375, 1.125] + 3 1.125 [0.562, 1.687] + 4 1.687 [0.8435, 2.53] + 5 2.53 [1.265, 3.795] + 6 3.795 [1.897, 5.692] + 7 5.692 [2.846, 8.538] + 8 8.538 [4.269, 12.807] + 9 12.807 [6.403, 19.210] + 10 19.210 backoff.Stop + +Note: Implementation is not thread-safe. +*/ +type ExponentialBackOff struct { + InitialInterval time.Duration + RandomizationFactor float64 + Multiplier float64 + MaxInterval time.Duration + + currentInterval time.Duration +} + +// Default values for ExponentialBackOff. +const ( + DefaultInitialInterval = 500 * time.Millisecond + DefaultRandomizationFactor = 0.5 + DefaultMultiplier = 1.5 + DefaultMaxInterval = 60 * time.Second +) + +// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. +func NewExponentialBackOff() *ExponentialBackOff { + return &ExponentialBackOff{ + InitialInterval: DefaultInitialInterval, + RandomizationFactor: DefaultRandomizationFactor, + Multiplier: DefaultMultiplier, + MaxInterval: DefaultMaxInterval, + } +} + +// Reset the interval back to the initial retry interval and restarts the timer. +// Reset must be called before using b. +func (b *ExponentialBackOff) Reset() { + b.currentInterval = b.InitialInterval +} + +// NextBackOff calculates the next backoff interval using the formula: +// +// Randomized interval = RetryInterval * (1 ± RandomizationFactor) +func (b *ExponentialBackOff) NextBackOff() time.Duration { + if b.currentInterval == 0 { + b.currentInterval = b.InitialInterval + } + + next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) + b.incrementCurrentInterval() + return next +} + +// Increments the current interval by multiplying it with the multiplier. +func (b *ExponentialBackOff) incrementCurrentInterval() { + // Check for overflow, if overflow is detected set the current interval to the max interval. + if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { + b.currentInterval = b.MaxInterval + } else { + b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) + } +} + +// Returns a random value from the following interval: +// +// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval]. +func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { + if randomizationFactor == 0 { + return currentInterval // make sure no randomness is used when randomizationFactor is 0. + } + var delta = randomizationFactor * float64(currentInterval) + var minInterval = float64(currentInterval) - delta + var maxInterval = float64(currentInterval) + delta + + // Get a random value from the range [minInterval, maxInterval]. + // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then + // we want a 33% chance for selecting either 1, 2 or 3. + return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) +} diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/retry.go b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/retry.go new file mode 100644 index 000000000..e43f47fb8 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/retry.go @@ -0,0 +1,139 @@ +package backoff + +import ( + "context" + "errors" + "time" +) + +// DefaultMaxElapsedTime sets a default limit for the total retry duration. +const DefaultMaxElapsedTime = 15 * time.Minute + +// Operation is a function that attempts an operation and may be retried. +type Operation[T any] func() (T, error) + +// Notify is a function called on operation error with the error and backoff duration. +type Notify func(error, time.Duration) + +// retryOptions holds configuration settings for the retry mechanism. +type retryOptions struct { + BackOff BackOff // Strategy for calculating backoff periods. + Timer timer // Timer to manage retry delays. + Notify Notify // Optional function to notify on each retry error. + MaxTries uint // Maximum number of retry attempts. + MaxElapsedTime time.Duration // Maximum total time for all retries. +} + +type RetryOption func(*retryOptions) + +// WithBackOff configures a custom backoff strategy. +func WithBackOff(b BackOff) RetryOption { + return func(args *retryOptions) { + args.BackOff = b + } +} + +// withTimer sets a custom timer for managing delays between retries. +func withTimer(t timer) RetryOption { + return func(args *retryOptions) { + args.Timer = t + } +} + +// WithNotify sets a notification function to handle retry errors. +func WithNotify(n Notify) RetryOption { + return func(args *retryOptions) { + args.Notify = n + } +} + +// WithMaxTries limits the number of retry attempts. +func WithMaxTries(n uint) RetryOption { + return func(args *retryOptions) { + args.MaxTries = n + } +} + +// WithMaxElapsedTime limits the total duration for retry attempts. +func WithMaxElapsedTime(d time.Duration) RetryOption { + return func(args *retryOptions) { + args.MaxElapsedTime = d + } +} + +// Retry attempts the operation until success, a permanent error, or backoff completion. +// It ensures the operation is executed at least once. +// +// Returns the operation result or error if retries are exhausted or context is cancelled. +func Retry[T any](ctx context.Context, operation Operation[T], opts ...RetryOption) (T, error) { + // Initialize default retry options. + args := &retryOptions{ + BackOff: NewExponentialBackOff(), + Timer: &defaultTimer{}, + MaxElapsedTime: DefaultMaxElapsedTime, + } + + // Apply user-provided options to the default settings. + for _, opt := range opts { + opt(args) + } + + defer args.Timer.Stop() + + startedAt := time.Now() + args.BackOff.Reset() + for numTries := uint(1); ; numTries++ { + // Execute the operation. + res, err := operation() + if err == nil { + return res, nil + } + + // Stop retrying if maximum tries exceeded. + if args.MaxTries > 0 && numTries >= args.MaxTries { + return res, err + } + + // Handle permanent errors without retrying. + var permanent *PermanentError + if errors.As(err, &permanent) { + return res, err + } + + // Stop retrying if context is cancelled. + if cerr := context.Cause(ctx); cerr != nil { + return res, cerr + } + + // Calculate next backoff duration. + next := args.BackOff.NextBackOff() + if next == Stop { + return res, err + } + + // Reset backoff if RetryAfterError is encountered. + var retryAfter *RetryAfterError + if errors.As(err, &retryAfter) { + next = retryAfter.Duration + args.BackOff.Reset() + } + + // Stop retrying if maximum elapsed time exceeded. + if args.MaxElapsedTime > 0 && time.Since(startedAt)+next > args.MaxElapsedTime { + return res, err + } + + // Notify on error if a notifier function is provided. + if args.Notify != nil { + args.Notify(err, next) + } + + // Wait for the next backoff period or context cancellation. + args.Timer.Start(next) + select { + case <-args.Timer.C(): + case <-ctx.Done(): + return res, context.Cause(ctx) + } + } +} diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/ticker.go b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/ticker.go new file mode 100644 index 000000000..f0d4b2ae7 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/ticker.go @@ -0,0 +1,83 @@ +package backoff + +import ( + "sync" + "time" +) + +// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff. +// +// Ticks will continue to arrive when the previous operation is still running, +// so operations that take a while to fail could run in quick succession. +type Ticker struct { + C <-chan time.Time + c chan time.Time + b BackOff + timer timer + stop chan struct{} + stopOnce sync.Once +} + +// NewTicker returns a new Ticker containing a channel that will send +// the time at times specified by the BackOff argument. Ticker is +// guaranteed to tick at least once. The channel is closed when Stop +// method is called or BackOff stops. It is not safe to manipulate the +// provided backoff policy (notably calling NextBackOff or Reset) +// while the ticker is running. +func NewTicker(b BackOff) *Ticker { + c := make(chan time.Time) + t := &Ticker{ + C: c, + c: c, + b: b, + timer: &defaultTimer{}, + stop: make(chan struct{}), + } + t.b.Reset() + go t.run() + return t +} + +// Stop turns off a ticker. After Stop, no more ticks will be sent. +func (t *Ticker) Stop() { + t.stopOnce.Do(func() { close(t.stop) }) +} + +func (t *Ticker) run() { + c := t.c + defer close(c) + + // Ticker is guaranteed to tick at least once. + afterC := t.send(time.Now()) + + for { + if afterC == nil { + return + } + + select { + case tick := <-afterC: + afterC = t.send(tick) + case <-t.stop: + t.c = nil // Prevent future ticks from being sent to the channel. + return + } + } +} + +func (t *Ticker) send(tick time.Time) <-chan time.Time { + select { + case t.c <- tick: + case <-t.stop: + return nil + } + + next := t.b.NextBackOff() + if next == Stop { + t.Stop() + return nil + } + + t.timer.Start(next) + return t.timer.C() +} diff --git a/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/timer.go b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/timer.go new file mode 100644 index 000000000..a89530974 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/cenkalti/backoff/v5/timer.go @@ -0,0 +1,35 @@ +package backoff + +import "time" + +type timer interface { + Start(duration time.Duration) + Stop() + C() <-chan time.Time +} + +// defaultTimer implements Timer interface using time.Timer +type defaultTimer struct { + timer *time.Timer +} + +// C returns the timers channel which receives the current time when the timer fires. +func (t *defaultTimer) C() <-chan time.Time { + return t.timer.C +} + +// Start starts the timer to fire after the given duration +func (t *defaultTimer) Start(duration time.Duration) { + if t.timer == nil { + t.timer = time.NewTimer(duration) + } else { + t.timer.Reset(duration) + } +} + +// Stop is called when the timer is not used anymore and resources may be freed. +func (t *defaultTimer) Stop() { + if t.timer != nil { + t.timer.Stop() + } +} diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/.gitignore b/components/reporters/slack/vendor/github.com/gorilla/websocket/.gitignore new file mode 100644 index 000000000..cd3fcd1ef --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +.idea/ +*.iml diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/AUTHORS b/components/reporters/slack/vendor/github.com/gorilla/websocket/AUTHORS new file mode 100644 index 000000000..1931f4006 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/AUTHORS @@ -0,0 +1,9 @@ +# This is the official list of Gorilla WebSocket authors for copyright +# purposes. +# +# Please keep the list sorted. + +Gary Burd +Google LLC (https://opensource.google.com/) +Joachim Bauch + diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/LICENSE b/components/reporters/slack/vendor/github.com/gorilla/websocket/LICENSE new file mode 100644 index 000000000..9171c9722 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/README.md b/components/reporters/slack/vendor/github.com/gorilla/websocket/README.md new file mode 100644 index 000000000..d33ed7fdd --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/README.md @@ -0,0 +1,33 @@ +# 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. + + +### Documentation + +* [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) +* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) + +### Status + +The Gorilla WebSocket package provides a complete and tested implementation of +the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The +package API is stable. + +### Installation + + go get github.com/gorilla/websocket + +### Protocol Compliance + +The Gorilla WebSocket package passes the server tests in the [Autobahn Test +Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn +subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). + diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/client.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/client.go new file mode 100644 index 000000000..04fdafee1 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/client.go @@ -0,0 +1,434 @@ +// 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. + +package websocket + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httptrace" + "net/url" + "strings" + "time" +) + +// ErrBadHandshake is returned when the server response to opening handshake is +// invalid. +var ErrBadHandshake = errors.New("websocket: bad handshake") + +var errInvalidCompression = errors.New("websocket: invalid compression negotiation") + +// NewClient creates a new client connection using the given net connection. +// The URL u specifies the host and request URI. 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). +// +// If the WebSocket handshake fails, ErrBadHandshake is returned along with a +// non-nil *http.Response so that callers can handle redirects, authentication, +// etc. +// +// Deprecated: Use Dialer instead. +func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) { + d := Dialer{ + ReadBufferSize: readBufSize, + WriteBufferSize: writeBufSize, + NetDial: func(net, addr string) (net.Conn, error) { + return netConn, nil + }, + } + return d.Dial(u.String(), requestHeader) +} + +// 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. + // If Proxy is nil or returns a nil *URL, no proxy is used. + Proxy func(*http.Request) (*url.URL, error) + + // 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 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 + + // EnableCompression specifies if the client should attempt to negotiate + // per message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool + + // Jar specifies the cookie jar. + // If Jar is nil, cookies are not sent in requests and ignored + // in responses. + Jar http.CookieJar +} + +// 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 + if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") { + hostNoPort = hostNoPort[:i] + } else { + switch u.Scheme { + case "wss": + hostPort += ":443" + case "https": + hostPort += ":443" + default: + hostPort += ":80" + } + } + return hostPort, hostNoPort +} + +// DefaultDialer is a dialer with all fields set to the default values. +var DefaultDialer = &Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, +} + +// 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) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { + if d == nil { + d = &nilDialer + } + + challengeKey, err := generateChallengeKey() + if err != nil { + return nil, nil, err + } + + u, err := url.Parse(urlStr) + if err != nil { + return nil, nil, err + } + + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, nil, errMalformedURL + } + + if u.User != nil { + // User name and password are not allowed in websocket URIs. + return nil, nil, errMalformedURL + } + + req := &http.Request{ + Method: http.MethodGet, + URL: u, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + 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 { + for _, cookie := range d.Jar.Cookies(u) { + req.AddCookie(cookie) + } + } + + // Set the request headers using the capitalization for names and values in + // RFC examples. Although the capitalization shouldn't matter, there are + // servers that depend on it. The Header.Set method is not used because the + // method canonicalizes the header names. + req.Header["Upgrade"] = []string{"websocket"} + req.Header["Connection"] = []string{"Upgrade"} + req.Header["Sec-WebSocket-Key"] = []string{challengeKey} + req.Header["Sec-WebSocket-Version"] = []string{"13"} + if len(d.Subprotocols) > 0 { + req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")} + } + for k, vs := range requestHeader { + switch { + case k == "Host": + if len(vs) > 0 { + req.Host = vs[0] + } + case k == "Upgrade" || + k == "Connection" || + k == "Sec-Websocket-Key" || + k == "Sec-Websocket-Version" || + 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["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"} + } + + if d.HandshakeTimeout != 0 { + var cancel func() + ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout) + defer cancel() + } + + // 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 netDial == nil { + netDialer := &net.Dialer{} + netDial = func(network, addr string) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } + } + + // 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 + } + } + + // 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 + } + } + + hostPort, hostNoPort := hostPortNoPort(u) + trace := httptrace.ContextClientTrace(ctx) + if trace != nil && trace.GetConn != nil { + trace.GetConn(hostPort) + } + + 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 { + netConn.Close() + } + }() + + if u.Scheme == "https" && d.NetDialTLSContext == nil { + // If NetDialTLSContext is set, assume that the TLS handshake has already been done + + cfg := cloneTLSConfig(d.TLSClientConfig) + if cfg.ServerName == "" { + cfg.ServerName = hostNoPort + } + tlsConn := tls.Client(netConn, cfg) + netConn = tlsConn + + if trace != nil && trace.TLSHandshakeStart != nil { + trace.TLSHandshakeStart() + } + 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, 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 + } + + if d.Jar != nil { + if rc := resp.Cookies(); len(rc) > 0 { + d.Jar.SetCookies(u, rc) + } + } + + if resp.StatusCode != 101 || + !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 + // debugging. + buf := make([]byte, 1024) + n, _ := io.ReadFull(resp.Body, buf) + resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) + return nil, resp, ErrBadHandshake + } + + for _, ext := range parseExtensions(resp.Header) { + if ext[""] != "permessage-deflate" { + continue + } + _, snct := ext["server_no_context_takeover"] + _, cnct := ext["client_no_context_takeover"] + if !snct || !cnct { + return nil, resp, errInvalidCompression + } + conn.newCompressionWriter = compressNoContextTakeover + conn.newDecompressionReader = decompressNoContextTakeover + break + } + + resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") + + netConn.SetDeadline(time.Time{}) + 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/components/reporters/slack/vendor/github.com/gorilla/websocket/compression.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/compression.go new file mode 100644 index 000000000..813ffb1e8 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/compression.go @@ -0,0 +1,148 @@ +// 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 ( + "compress/flate" + "errors" + "io" + "strings" + "sync" +) + +const ( + minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6 + maxCompressionLevel = flate.BestCompression + defaultCompressionLevel = 1 +) + +var ( + flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool + flateReaderPool = sync.Pool{New: func() interface{} { + return flate.NewReader(nil) + }} +) + +func decompressNoContextTakeover(r io.Reader) io.ReadCloser { + const tail = + // Add four bytes as specified in RFC + "\x00\x00\xff\xff" + + // Add final block to squelch unexpected EOF error from flate reader. + "\x01\x00\x00\xff\xff" + + fr, _ := flateReaderPool.Get().(io.ReadCloser) + fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil) + return &flateReadWrapper{fr} +} + +func isValidCompressionLevel(level int) bool { + return minCompressionLevel <= level && level <= maxCompressionLevel +} + +func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser { + p := &flateWriterPools[level-minCompressionLevel] + tw := &truncWriter{w: w} + fw, _ := p.Get().(*flate.Writer) + if fw == nil { + fw, _ = flate.NewWriter(tw, level) + } else { + fw.Reset(tw) + } + return &flateWriteWrapper{fw: fw, tw: tw, p: p} +} + +// truncWriter is an io.Writer that writes all but the last four bytes of the +// stream to another io.Writer. +type truncWriter struct { + w io.WriteCloser + n int + p [4]byte +} + +func (w *truncWriter) Write(p []byte) (int, error) { + n := 0 + + // fill buffer first for simplicity. + if w.n < len(w.p) { + n = copy(w.p[w.n:], p) + p = p[n:] + w.n += n + if len(p) == 0 { + return n, nil + } + } + + m := len(p) + if m > len(w.p) { + m = len(w.p) + } + + if nn, err := w.w.Write(w.p[:m]); err != nil { + return n + nn, err + } + + copy(w.p[:], w.p[m:]) + copy(w.p[len(w.p)-m:], p[len(p)-m:]) + nn, err := w.w.Write(p[:len(p)-m]) + return n + nn, err +} + +type flateWriteWrapper struct { + fw *flate.Writer + tw *truncWriter + p *sync.Pool +} + +func (w *flateWriteWrapper) Write(p []byte) (int, error) { + if w.fw == nil { + return 0, errWriteClosed + } + return w.fw.Write(p) +} + +func (w *flateWriteWrapper) Close() error { + if w.fw == nil { + return errWriteClosed + } + err1 := w.fw.Flush() + w.p.Put(w.fw) + w.fw = nil + if w.tw.p != [4]byte{0, 0, 0xff, 0xff} { + return errors.New("websocket: internal error, unexpected bytes at end of flate stream") + } + err2 := w.tw.w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +type flateReadWrapper struct { + fr io.ReadCloser +} + +func (r *flateReadWrapper) Read(p []byte) (int, error) { + if r.fr == nil { + return 0, io.ErrClosedPipe + } + n, err := r.fr.Read(p) + if err == io.EOF { + // Preemptively place the reader back in the pool. This helps with + // scenarios where the application does not call NextReader() soon after + // this final read. + r.Close() + } + return n, err +} + +func (r *flateReadWrapper) Close() error { + if r.fr == nil { + return io.ErrClosedPipe + } + err := r.fr.Close() + flateReaderPool.Put(r.fr) + r.fr = nil + return err +} diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/conn.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/conn.go new file mode 100644 index 000000000..5161ef81f --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/conn.go @@ -0,0 +1,1238 @@ +// 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. + +package websocket + +import ( + "bufio" + "encoding/binary" + "errors" + "io" + "io/ioutil" + "math/rand" + "net" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" +) + +const ( + // Frame header byte 0 bits from Section 5.2 of RFC 6455 + finalBit = 1 << 7 + rsv1Bit = 1 << 6 + rsv2Bit = 1 << 5 + rsv3Bit = 1 << 4 + + // Frame header byte 1 bits from Section 5.2 of RFC 6455 + maskBit = 1 << 7 + + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maxControlFramePayloadSize = 125 + + writeWait = time.Second + + defaultReadBufferSize = 4096 + defaultWriteBufferSize = 4096 + + continuationFrame = 0 + noFrame = -1 +) + +// Close codes defined in RFC 6455, section 11.7. +const ( + CloseNormalClosure = 1000 + CloseGoingAway = 1001 + CloseProtocolError = 1002 + CloseUnsupportedData = 1003 + CloseNoStatusReceived = 1005 + CloseAbnormalClosure = 1006 + CloseInvalidFramePayloadData = 1007 + ClosePolicyViolation = 1008 + CloseMessageTooBig = 1009 + CloseMandatoryExtension = 1010 + CloseInternalServerErr = 1011 + CloseServiceRestart = 1012 + CloseTryAgainLater = 1013 + CloseTLSHandshake = 1015 +) + +// The message types are defined in RFC 6455, section 11.8. +const ( + // TextMessage denotes a text data message. The text message payload is + // interpreted as UTF-8 encoded text data. + TextMessage = 1 + + // BinaryMessage denotes a binary data message. + BinaryMessage = 2 + + // CloseMessage denotes a close control message. The optional message + // payload contains a numeric code and text. Use the FormatCloseMessage + // function to format a close message payload. + CloseMessage = 8 + + // PingMessage denotes a ping control message. The optional message payload + // is UTF-8 encoded text. + PingMessage = 9 + + // PongMessage denotes a pong control message. The optional message payload + // is UTF-8 encoded text. + PongMessage = 10 +) + +// ErrCloseSent is returned when the application writes a message to the +// connection after sending a close message. +var ErrCloseSent = errors.New("websocket: close sent") + +// ErrReadLimit is returned when reading a message that is larger than the +// read limit set for the connection. +var ErrReadLimit = errors.New("websocket: read limit exceeded") + +// netError satisfies the net Error interface. +type netError struct { + msg string + temporary bool + timeout bool +} + +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 a close message. +type CloseError struct { + // Code is defined in RFC 6455, section 11.7. + Code int + + // Text is the optional text payload. + Text string +} + +func (e *CloseError) Error() string { + s := []byte("websocket: close ") + s = strconv.AppendInt(s, int64(e.Code), 10) + switch e.Code { + case CloseNormalClosure: + s = append(s, " (normal)"...) + case CloseGoingAway: + s = append(s, " (going away)"...) + case CloseProtocolError: + s = append(s, " (protocol error)"...) + case CloseUnsupportedData: + s = append(s, " (unsupported data)"...) + case CloseNoStatusReceived: + s = append(s, " (no status)"...) + case CloseAbnormalClosure: + s = append(s, " (abnormal closure)"...) + case CloseInvalidFramePayloadData: + s = append(s, " (invalid payload data)"...) + case ClosePolicyViolation: + s = append(s, " (policy violation)"...) + case CloseMessageTooBig: + s = append(s, " (message too big)"...) + case CloseMandatoryExtension: + s = append(s, " (mandatory extension missing)"...) + case CloseInternalServerErr: + s = append(s, " (internal server error)"...) + case CloseTLSHandshake: + s = append(s, " (TLS handshake error)"...) + } + if e.Text != "" { + s = append(s, ": "...) + s = append(s, e.Text...) + } + return string(s) +} + +// IsCloseError returns boolean indicating whether the error is a *CloseError +// with one of the specified codes. +func IsCloseError(err error, codes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range codes { + if e.Code == code { + return true + } + } + } + return false +} + +// IsUnexpectedCloseError returns boolean indicating whether the error is a +// *CloseError with a code not in the list of expected codes. +func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { + if e, ok := err.(*CloseError); ok { + for _, code := range expectedCodes { + if e.Code == code { + return false + } + } + return true + } + return false +} + +var ( + errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true} + errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()} + errBadWriteOpCode = errors.New("websocket: bad write message type") + errWriteClosed = errors.New("websocket: write closed") + errInvalidControlFrame = errors.New("websocket: invalid control frame") +) + +func newMaskKey() [4]byte { + n := rand.Uint32() + return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} +} + +func hideTempErr(err error) error { + if e, ok := err.(net.Error); ok && e.Temporary() { + err = &netError{msg: e.Error(), timeout: e.Timeout()} + } + return err +} + +func isControl(frameType int) bool { + return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage +} + +func isData(frameType int) bool { + return frameType == TextMessage || frameType == BinaryMessage +} + +var validReceivedCloseCodes = map[int]bool{ + // see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number + + CloseNormalClosure: true, + CloseGoingAway: true, + CloseProtocolError: true, + CloseUnsupportedData: true, + CloseNoStatusReceived: false, + CloseAbnormalClosure: false, + CloseInvalidFramePayloadData: true, + ClosePolicyViolation: true, + CloseMessageTooBig: true, + CloseMandatoryExtension: true, + CloseInternalServerErr: true, + CloseServiceRestart: true, + CloseTryAgainLater: true, + CloseTLSHandshake: false, +} + +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 + isServer bool + subprotocol string + + // Write fields + 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 + + writeErrMu sync.Mutex + writeErr error + + enableWriteCompression bool + compressionLevel int + newCompressionWriter func(io.WriteCloser, int) io.WriteCloser + + // Read fields + 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. + readMaskPos int + readMaskKey [4]byte + handlePong func(string) error + handlePing func(string) error + handleClose func(int, string) error + readErrCount int + messageReader *messageReader // the current low-level reader + + readDecompress bool // whether last read frame had RSV1 set + newDecompressionReader func(io.Reader) io.ReadCloser +} + +func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn { + + if br == nil { + if readBufferSize == 0 { + readBufferSize = defaultReadBufferSize + } else if readBufferSize < maxControlFramePayloadSize { + // must be large enough for control frame + readBufferSize = maxControlFramePayloadSize + } + br = bufio.NewReaderSize(conn, readBufferSize) + } + + if writeBufferSize <= 0 { + writeBufferSize = defaultWriteBufferSize + } + writeBufferSize += maxFrameHeaderSize + + if writeBuf == nil && writeBufferPool == nil { + writeBuf = make([]byte, writeBufferSize) + } + + mu := make(chan struct{}, 1) + mu <- struct{}{} + c := &Conn{ + isServer: isServer, + br: br, + conn: conn, + mu: mu, + readFinal: true, + writeBuf: writeBuf, + writePool: writeBufferPool, + writeBufSize: writeBufferSize, + enableWriteCompression: true, + compressionLevel: defaultCompressionLevel, + } + c.SetCloseHandler(nil) + c.SetPingHandler(nil) + c.SetPongHandler(nil) + 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 message. +func (c *Conn) Close() error { + return c.conn.Close() +} + +// LocalAddr returns the local network address. +func (c *Conn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// RemoteAddr returns the remote network address. +func (c *Conn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +// Write methods + +func (c *Conn) writeFatal(err error) error { + err = hideTempErr(err) + c.writeErrMu.Lock() + if c.writeErr == nil { + c.writeErr = err + } + c.writeErrMu.Unlock() + return err +} + +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 <- struct{}{} }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + 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 { + if !isControl(messageType) { + return errBadWriteOpCode + } + if len(data) > maxControlFramePayloadSize { + return errInvalidControlFrame + } + + b0 := byte(messageType) | finalBit + b1 := byte(len(data)) + if !c.isServer { + b1 |= maskBit + } + + buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize) + buf = append(buf, b0, b1) + + if c.isServer { + buf = append(buf, data...) + } else { + key := newMaskKey() + buf = append(buf, key[:]...) + buf = append(buf, data...) + maskBytes(key, 0, buf[6:]) + } + + d := 1000 * time.Hour + if !deadline.IsZero() { + d = deadline.Sub(time.Now()) + if d < 0 { + return errWriteTimeout + } + } + + timer := time.NewTimer(d) + select { + case <-c.mu: + timer.Stop() + case <-timer.C: + return errWriteTimeout + } + defer func() { c.mu <- struct{}{} }() + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + if err != nil { + return err + } + + c.conn.SetWriteDeadline(deadline) + _, err = c.conn.Write(buf) + if err != nil { + return c.writeFatal(err) + } + if messageType == CloseMessage { + c.writeFatal(ErrCloseSent) + } + return err +} + +// 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. + if c.writer != nil { + c.writer.Close() + c.writer = nil + } + + if !isControl(messageType) && !isData(messageType) { + return errBadWriteOpCode + } + + c.writeErrMu.Lock() + err := c.writeErr + c.writeErrMu.Unlock() + 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 +// method flushes the complete message to the network. +// +// 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) { + var mw messageWriter + if err := c.beginMessage(&mw, messageType); err != nil { + return nil, err + } + c.writer = &mw + if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) { + w := c.newCompressionWriter(c.writer, c.compressionLevel) + mw.compress = true + c.writer = w + } + return c.writer, nil +} + +type messageWriter struct { + c *Conn + compress bool // whether next call to flushFrame should set RSV1 + pos int // end of data in writeBuf. + frameType int // type of the current frame. + err error +} + +func (w *messageWriter) endMessage(err error) error { + if w.err != 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 +} + +// flushFrame writes buffered data and extra as a frame to the network. The +// final argument indicates that this is the last frame in the message. +func (w *messageWriter) flushFrame(final bool, extra []byte) error { + c := w.c + length := w.pos - maxFrameHeaderSize + len(extra) + + // Check for invalid control frames. + if isControl(w.frameType) && + (!final || length > maxControlFramePayloadSize) { + return w.endMessage(errInvalidControlFrame) + } + + b0 := byte(w.frameType) + if final { + b0 |= finalBit + } + if w.compress { + b0 |= rsv1Bit + } + w.compress = false + + b1 := byte(0) + if !c.isServer { + b1 |= maskBit + } + + // Assume that the frame starts at beginning of c.writeBuf. + framePos := 0 + if c.isServer { + // Adjust up if mask not included in the header. + framePos = 4 + } + + switch { + case length >= 65536: + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 127 + binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length)) + case length > 125: + framePos += 6 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | 126 + binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length)) + default: + framePos += 8 + c.writeBuf[framePos] = b0 + c.writeBuf[framePos+1] = b1 | byte(length) + } + + if !c.isServer { + key := newMaskKey() + copy(c.writeBuf[maxFrameHeaderSize-4:], key[:]) + maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos]) + if len(extra) > 0 { + return w.endMessage(c.writeFatal(errors.New("websocket: internal error, extra used in client mode"))) + } + } + + // Write the buffers to the connection with best-effort detection of + // concurrent writes. See the concurrency section in the package + // documentation for more info. + + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + + err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra) + + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + + if err != nil { + return w.endMessage(err) + } + + if final { + w.endMessage(errWriteClosed) + return nil + } + + // Setup for next frame. + w.pos = maxFrameHeaderSize + w.frameType = continuationFrame + return nil +} + +func (w *messageWriter) ncopy(max int) (int, error) { + n := len(w.c.writeBuf) - w.pos + if n <= 0 { + if err := w.flushFrame(false, nil); err != nil { + return 0, err + } + n = len(w.c.writeBuf) - w.pos + } + if n > max { + n = max + } + return n, nil +} + +func (w *messageWriter) Write(p []byte) (int, error) { + if w.err != nil { + return 0, w.err + } + + if len(p) > 2*len(w.c.writeBuf) && w.c.isServer { + // Don't buffer large messages. + err := w.flushFrame(false, p) + if err != nil { + return 0, err + } + return len(p), nil + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) WriteString(p string) (int, error) { + if w.err != nil { + return 0, w.err + } + + nn := len(p) + for len(p) > 0 { + n, err := w.ncopy(len(p)) + if err != nil { + return 0, err + } + copy(w.c.writeBuf[w.pos:], p[:n]) + w.pos += n + p = p[n:] + } + return nn, nil +} + +func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) { + if w.err != nil { + return 0, w.err + } + for { + if w.pos == len(w.c.writeBuf) { + err = w.flushFrame(false, nil) + if err != nil { + break + } + } + var n int + n, err = r.Read(w.c.writeBuf[w.pos:]) + w.pos += n + nn += int64(n) + if err != nil { + if err == io.EOF { + err = nil + } + break + } + } + return nn, err +} + +func (w *messageWriter) Close() error { + if w.err != nil { + return w.err + } + return w.flushFrame(true, nil) +} + +// WritePreparedMessage writes prepared message into connection. +func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error { + frameType, frameData, err := pm.frame(prepareKey{ + isServer: c.isServer, + compress: c.newCompressionWriter != nil && c.enableWriteCompression && isData(pm.messageType), + compressionLevel: c.compressionLevel, + }) + if err != nil { + return err + } + if c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = true + err = c.write(frameType, c.writeDeadline, frameData, nil) + if !c.isWriting { + panic("concurrent write to websocket connection") + } + c.isWriting = false + return err +} + +// WriteMessage is a helper method for getting a writer using NextWriter, +// writing the message and closing the writer. +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. + + var mw messageWriter + if err := c.beginMessage(&mw, messageType); err != nil { + return err + } + n := copy(c.writeBuf[mw.pos:], data) + mw.pos += n + data = data[n:] + return mw.flushFrame(true, data) + } + + w, err := c.NextWriter(messageType) + if err != nil { + return err + } + if _, err = w.Write(data); err != nil { + return err + } + return w.Close() +} + +// SetWriteDeadline sets the write deadline on the underlying network +// connection. After a write has timed out, the websocket state is corrupt and +// all future writes will return an error. A zero value for t means writes will +// not time out. +func (c *Conn) SetWriteDeadline(t time.Time) error { + c.writeDeadline = t + return nil +} + +// Read methods + +func (c *Conn) advanceFrame() (int, error) { + // 1. Skip remainder of previous frame. + + if c.readRemaining > 0 { + if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { + return noFrame, err + } + } + + // 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 + } + + 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.setReadRemaining(int64(p[1] & 0x7f)) + + c.readDecompress = false + if rsv1 { + if c.newDecompressionReader != nil { + c.readDecompress = true + } else { + errors = append(errors, "RSV1 set") + } + } + + if rsv2 { + errors = append(errors, "RSV2 set") + } + + if rsv3 { + errors = append(errors, "RSV3 set") + } + + switch frameType { + case CloseMessage, PingMessage, PongMessage: + if c.readRemaining > maxControlFramePayloadSize { + errors = append(errors, "len > 125 for control") + } + if !final { + errors = append(errors, "FIN not set on control") + } + case TextMessage, BinaryMessage: + if !c.readFinal { + errors = append(errors, "data before FIN") + } + c.readFinal = final + case continuationFrame: + if c.readFinal { + errors = append(errors, "continuation after FIN") + } + c.readFinal = final + default: + 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 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: + p, err := c.read(2) + if err != nil { + return noFrame, err + } + + 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 + } + + if err := c.setReadRemaining(int64(binary.BigEndian.Uint64(p))); err != nil { + return noFrame, err + } + } + + // 4. Handle frame masking. + + if mask { + c.readMaskPos = 0 + p, err := c.read(len(c.readMaskKey)) + if err != nil { + return noFrame, err + } + copy(c.readMaskKey[:], p) + } + + // 5. For text and binary messages, enforce read limit and return. + + 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 + } + + return frameType, nil + } + + // 6. Read control frame payload. + + var payload []byte + if c.readRemaining > 0 { + payload, err = c.read(int(c.readRemaining)) + c.setReadRemaining(0) + if err != nil { + return noFrame, err + } + if c.isServer { + maskBytes(c.readMaskKey, 0, payload) + } + } + + // 7. Process control frame payload. + + switch frameType { + case PongMessage: + if err := c.handlePong(string(payload)); err != nil { + return noFrame, err + } + case PingMessage: + if err := c.handlePing(string(payload)); err != nil { + return noFrame, err + } + case CloseMessage: + closeCode := CloseNoStatusReceived + closeText := "" + if len(payload) >= 2 { + closeCode = int(binary.BigEndian.Uint16(payload)) + if !isValidReceivedCloseCode(closeCode) { + return noFrame, c.handleProtocolError("bad close code " + strconv.Itoa(closeCode)) + } + closeText = string(payload[2:]) + if !utf8.ValidString(closeText) { + return noFrame, c.handleProtocolError("invalid utf8 payload in close frame") + } + } + if err := c.handleClose(closeCode, closeText); err != nil { + return noFrame, err + } + return noFrame, &CloseError{Code: closeCode, Text: closeText} + } + + return frameType, nil +} + +func (c *Conn) handleProtocolError(message string) error { + data := FormatCloseMessage(CloseProtocolError, message) + if len(data) > maxControlFramePayloadSize { + data = data[:maxControlFramePayloadSize] + } + c.WriteControl(CloseMessage, data, time.Now().Add(writeWait)) + return errors.New("websocket: " + message) +} + +// NextReader returns the next data message received from the peer. The +// returned messageType is either TextMessage or BinaryMessage. +// +// There can be at most one open reader on a connection. NextReader discards +// the previous message if the application has not already consumed it. +// +// Applications must break out of the application's read loop when this method +// returns a non-nil error value. Errors returned from this method are +// permanent. Once this method returns a non-nil error, all subsequent calls to +// this method return the same error. +func (c *Conn) NextReader() (messageType int, r io.Reader, err error) { + // Close previous reader, only relevant for decompression. + if c.reader != nil { + c.reader.Close() + c.reader = nil + } + + c.messageReader = nil + c.readLength = 0 + + for c.readErr == nil { + frameType, err := c.advanceFrame() + if err != nil { + c.readErr = hideTempErr(err) + break + } + + if frameType == TextMessage || frameType == BinaryMessage { + c.messageReader = &messageReader{c} + c.reader = c.messageReader + if c.readDecompress { + c.reader = c.newDecompressionReader(c.reader) + } + return frameType, c.reader, nil + } + } + + // Applications that do handle the error returned from this method spin in + // tight loop on connection failure. To help application developers detect + // this error, panic on repeated reads to the failed connection. + c.readErrCount++ + if c.readErrCount >= 1000 { + panic("repeated read on failed websocket connection") + } + + return noFrame, nil, c.readErr +} + +type messageReader struct{ c *Conn } + +func (r *messageReader) Read(b []byte) (int, error) { + c := r.c + if c.messageReader != r { + return 0, io.EOF + } + + for c.readErr == nil { + + if c.readRemaining > 0 { + if int64(len(b)) > c.readRemaining { + b = b[:c.readRemaining] + } + n, err := c.br.Read(b) + c.readErr = hideTempErr(err) + if c.isServer { + c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) + } + rem := c.readRemaining + rem -= int64(n) + c.setReadRemaining(rem) + if c.readRemaining > 0 && c.readErr == io.EOF { + c.readErr = errUnexpectedEOF + } + return n, c.readErr + } + + if c.readFinal { + c.messageReader = nil + return 0, io.EOF + } + + frameType, err := c.advanceFrame() + switch { + case err != nil: + c.readErr = hideTempErr(err) + case frameType == TextMessage || frameType == BinaryMessage: + c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") + } + } + + err := c.readErr + if err == io.EOF && c.messageReader == r { + err = errUnexpectedEOF + } + return 0, err +} + +func (r *messageReader) Close() error { + return nil +} + +// ReadMessage is a helper method for getting a reader using NextReader and +// reading from that reader to a buffer. +func (c *Conn) ReadMessage() (messageType int, p []byte, err error) { + var r io.Reader + messageType, r, err = c.NextReader() + if err != nil { + return messageType, nil, err + } + p, err = ioutil.ReadAll(r) + return messageType, p, err +} + +// SetReadDeadline sets the read deadline on the underlying network connection. +// After a read has timed out, the websocket connection state is corrupt and +// all future reads will return an error. A zero value for t means reads will +// not time out. +func (c *Conn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// 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 +} + +// CloseHandler returns the current close handler +func (c *Conn) CloseHandler() func(code int, text string) error { + return c.handleClose +} + +// 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 +// message back to the peer. +// +// 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 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 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 := FormatCloseMessage(code, "") + c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) + return nil + } + } + c.handleClose = h +} + +// PingHandler returns the current ping handler +func (c *Conn) PingHandler() func(appData string) error { + return c.handlePing +} + +// SetPingHandler sets the handler for ping messages received from the peer. +// The appData argument to h is the PING message application data. The default +// ping handler sends a pong to the peer. +// +// 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 { + err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) + if err == ErrCloseSent { + return nil + } else if e, ok := err.(net.Error); ok && e.Temporary() { + return nil + } + return err + } + } + c.handlePing = h +} + +// PongHandler returns the current pong handler +func (c *Conn) PongHandler() func(appData string) error { + return c.handlePong +} + +// SetPongHandler sets the handler for pong messages received from the peer. +// The appData argument to h is the PONG message application data. The default +// pong handler does nothing. +// +// 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 } + } + 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 +} + +// EnableWriteCompression enables and disables write compression of +// subsequent text and binary messages. This function is a noop if +// compression was not negotiated with the peer. +func (c *Conn) EnableWriteCompression(enable bool) { + c.enableWriteCompression = enable +} + +// SetCompressionLevel sets the flate compression level for subsequent text and +// binary messages. This function is a noop if compression was not negotiated +// with the peer. See the compress/flate package for a description of +// compression levels. +func (c *Conn) SetCompressionLevel(level int) error { + if !isValidCompressionLevel(level) { + return errors.New("websocket: invalid compression level") + } + c.compressionLevel = level + return nil +} + +// 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) + return buf +} diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/doc.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/doc.go new file mode 100644 index 000000000..8db0cef95 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/doc.go @@ -0,0 +1,227 @@ +// 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. + +// Package websocket implements the WebSocket protocol defined in RFC 6455. +// +// Overview +// +// 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, +// WriteBufferSize: 1024, +// } +// +// func handler(w http.ResponseWriter, r *http.Request) { +// conn, err := upgrader.Upgrade(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// ... Use conn to send and receive messages. +// } +// +// Call the connection's WriteMessage and ReadMessage methods to send and +// receive messages as a slice of bytes. This snippet of code shows how to echo +// messages using these methods: +// +// for { +// messageType, p, err := conn.ReadMessage() +// if err != nil { +// log.Println(err) +// return +// } +// if err := conn.WriteMessage(messageType, p); err != nil { +// log.Println(err) +// return +// } +// } +// +// In above snippet of code, p is a []byte and messageType is an int with value +// websocket.BinaryMessage or websocket.TextMessage. +// +// An application can also send and receive messages using the io.WriteCloser +// and io.Reader interfaces. To send a message, call the connection NextWriter +// method to get an io.WriteCloser, write the message to the writer and close +// the writer when done. To receive a message, call the connection NextReader +// method to get an io.Reader and read until io.EOF is returned. This snippet +// shows how to echo messages using the NextWriter and NextReader methods: +// +// for { +// messageType, r, err := conn.NextReader() +// if err != nil { +// return +// } +// w, err := conn.NextWriter(messageType) +// if err != nil { +// return err +// } +// if _, err := io.Copy(w, r); err != nil { +// return err +// } +// if err := w.Close(); err != nil { +// return err +// } +// } +// +// Data Messages +// +// The WebSocket protocol distinguishes between text and binary data messages. +// Text messages are interpreted as UTF-8 encoded text. The interpretation of +// binary messages is left to the application. +// +// This package uses the TextMessage and BinaryMessage integer constants to +// identify the two data message types. The ReadMessage and NextReader methods +// return the type of the received message. The messageType argument to the +// WriteMessage and NextWriter methods specifies the type of a sent message. +// +// It is the application's responsibility to ensure that text messages are +// valid UTF-8 encoded text. +// +// Control Messages +// +// The WebSocket protocol defines three types of control messages: close, ping +// and pong. Call the connection WriteControl, WriteMessage or NextWriter +// methods to send a control message to the peer. +// +// 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 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 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 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: +// +// func readLoop(c *websocket.Conn) { +// for { +// if _, _, err := c.NextReader(); err != nil { +// c.Close() +// break +// } +// } +// } +// +// Concurrency +// +// Connections support one concurrent reader and one concurrent writer. +// +// Applications are responsible for ensuring that no more than one goroutine +// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage, +// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and +// that no more than one goroutine calls the read methods (NextReader, +// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler) +// concurrently. +// +// The Close and WriteControl methods can be called concurrently with all other +// methods. +// +// Origin Considerations +// +// Web browsers allow Javascript applications to open a WebSocket connection to +// any host. It's up to the server to enforce an origin policy using the Origin +// request header sent by the browser. +// +// The Upgrader calls the function specified in the CheckOrigin field to check +// the origin. If the CheckOrigin function returns false, then the Upgrade +// 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 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 +// +// Per message compression extensions (RFC 7692) are experimentally supported +// by this package in a limited capacity. Setting the EnableCompression option +// to true in Dialer or Upgrader will attempt to negotiate per message deflate +// support. +// +// var upgrader = websocket.Upgrader{ +// EnableCompression: true, +// } +// +// If compression was successfully negotiated with the connection's peer, any +// message received in compressed form will be automatically decompressed. +// All Read methods will return uncompressed bytes. +// +// Per message compression of messages written to a connection can be enabled +// or disabled by calling the corresponding Conn method: +// +// conn.EnableWriteCompression(false) +// +// Currently this package does not support compression with "context takeover". +// This means that messages must be compressed and decompressed in isolation, +// without retaining sliding window or dictionary state across messages. For +// more details refer to RFC 7692. +// +// Use of compression is experimental and may result in decreased performance. +package websocket diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/join.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/join.go new file mode 100644 index 000000000..c64f8c829 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/gorilla/websocket/json.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/json.go new file mode 100644 index 000000000..dc2c1f641 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/json.go @@ -0,0 +1,60 @@ +// 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. + +package websocket + +import ( + "encoding/json" + "io" +) + +// 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 as a message. +// +// See the documentation for encoding/json Marshal for details about the +// conversion of Go values to JSON. +func (c *Conn) WriteJSON(v interface{}) error { + w, err := c.NextWriter(TextMessage) + if err != nil { + return err + } + err1 := json.NewEncoder(w).Encode(v) + err2 := w.Close() + if err1 != nil { + return err1 + } + return err2 +} + +// 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) +} + +// ReadJSON reads the next JSON-encoded message from the connection and stores +// it in the value pointed to by v. +// +// See the documentation for the encoding/json Unmarshal function for details +// about the conversion of JSON to a Go value. +func (c *Conn) ReadJSON(v interface{}) error { + _, r, err := c.NextReader() + if err != nil { + return err + } + err = json.NewDecoder(r).Decode(v) + if err == io.EOF { + // One value is expected in the message. + err = io.ErrUnexpectedEOF + } + return err +} diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/mask.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/mask.go new file mode 100644 index 000000000..d0742bf2a --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/mask.go @@ -0,0 +1,55 @@ +// 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. + +//go:build !appengine +// +build !appengine + +package websocket + +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 { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 + } + + // Mask one byte at a time to word boundary. + if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 { + n = wordSize - n + for i := range b[:n] { + b[i] ^= key[pos&3] + pos++ + } + b = b[n:] + } + + // Create aligned word size key. + var k [wordSize]byte + for i := range k { + k[i] = key[(pos+i)&3] + } + kw := *(*uintptr)(unsafe.Pointer(&k)) + + // Mask one word at a time. + n := (len(b) / wordSize) * wordSize + for i := 0; i < n; i += wordSize { + *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw + } + + // Mask one byte at a time for remaining bytes. + b = b[n:] + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + + return pos & 3 +} diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/mask_safe.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/mask_safe.go new file mode 100644 index 000000000..36250ca7c --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/mask_safe.go @@ -0,0 +1,16 @@ +// 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. + +//go:build appengine +// +build appengine + +package websocket + +func maskBytes(key [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= key[pos&3] + pos++ + } + return pos & 3 +} diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/prepared.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/prepared.go new file mode 100644 index 000000000..c854225e9 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/prepared.go @@ -0,0 +1,102 @@ +// 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 ( + "bytes" + "net" + "sync" + "time" +) + +// PreparedMessage caches on the wire representations of a message payload. +// Use PreparedMessage to efficiently send a message payload to multiple +// connections. PreparedMessage is especially useful when compression is used +// because the CPU and memory expensive compression operation can be executed +// once for a given set of compression options. +type PreparedMessage struct { + messageType int + data []byte + mu sync.Mutex + frames map[prepareKey]*preparedFrame +} + +// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage. +type prepareKey struct { + isServer bool + compress bool + compressionLevel int +} + +// preparedFrame contains data in wire representation. +type preparedFrame struct { + once sync.Once + data []byte +} + +// NewPreparedMessage returns an initialized PreparedMessage. You can then send +// it to connection using WritePreparedMessage method. Valid wire +// representation will be calculated lazily only once for a set of current +// connection options. +func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) { + pm := &PreparedMessage{ + messageType: messageType, + frames: make(map[prepareKey]*preparedFrame), + data: data, + } + + // Prepare a plain server frame. + _, frameData, err := pm.frame(prepareKey{isServer: true, compress: false}) + if err != nil { + return nil, err + } + + // To protect against caller modifying the data argument, remember the data + // copied to the plain server frame. + pm.data = frameData[len(frameData)-len(data):] + return pm, nil +} + +func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) { + pm.mu.Lock() + frame, ok := pm.frames[key] + if !ok { + frame = &preparedFrame{} + pm.frames[key] = frame + } + pm.mu.Unlock() + + var err error + frame.once.Do(func() { + // Prepare a frame using a 'fake' connection. + // TODO: Refactor code in conn.go to allow more direct construction of + // the frame. + mu := make(chan struct{}, 1) + mu <- struct{}{} + var nc prepareConn + c := &Conn{ + conn: &nc, + mu: mu, + isServer: key.isServer, + compressionLevel: key.compressionLevel, + enableWriteCompression: true, + writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize), + } + if key.compress { + c.newCompressionWriter = compressNoContextTakeover + } + err = c.WriteMessage(pm.messageType, pm.data) + frame.data = nc.buf.Bytes() + }) + return pm.messageType, frame.data, err +} + +type prepareConn struct { + buf bytes.Buffer + net.Conn +} + +func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) } +func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/components/reporters/slack/vendor/github.com/gorilla/websocket/proxy.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/proxy.go new file mode 100644 index 000000000..e0f466b72 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/gorilla/websocket/server.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/server.go new file mode 100644 index 000000000..bb3359743 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/server.go @@ -0,0 +1,365 @@ +// 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. + +package websocket + +import ( + "bufio" + "errors" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +// HandshakeError describes an error with the handshake from the peer. +type HandshakeError struct { + message string +} + +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 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 not nil, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // 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 + // is nil, then http.Error is used to generate the HTTP response. + 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, 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 + // message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool +} + +func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) { + err := HandshakeError{reason} + if u.Error != nil { + u.Error(w, r, status, err) + } else { + w.Header().Set("Sec-Websocket-Version", "13") + http.Error(w, http.StatusText(status), status) + } + return nil, err +} + +// checkSameOrigin returns true if the origin is not set or is equal to the request host. +func checkSameOrigin(r *http.Request) bool { + origin := r.Header["Origin"] + if len(origin) == 0 { + return true + } + u, err := url.Parse(origin[0]) + if err != nil { + return false + } + return equalASCIIFold(u.Host, r.Host) +} + +func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { + if u.Subprotocols != nil { + clientProtocols := Subprotocols(r) + for _, serverProtocol := range u.Subprotocols { + for _, clientProtocol := range clientProtocols { + if clientProtocol == serverProtocol { + return clientProtocol + } + } + } + } else if responseHeader != nil { + return responseHeader.Get("Sec-Websocket-Protocol") + } + return "" +} + +// 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). 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) { + const badHandshake = "websocket: the client is not using the websocket protocol: " + + if !tokenListContainsValue(r.Header, "Connection", "upgrade") { + 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, 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: request origin not allowed by Upgrader.CheckOrigin") + } + + challengeKey := r.Header.Get("Sec-Websocket-Key") + 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) + + // Negotiate PMCE + var compress bool + if u.EnableCompression { + for _, ext := range parseExtensions(r.Header) { + if ext[""] != "permessage-deflate" { + continue + } + compress = true + break + } + } + + 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() + if err != nil { + return u.returnError(w, r, http.StatusInternalServerError, err.Error()) + } + + if brw.Reader.Buffered() > 0 { + netConn.Close() + return nil, errors.New("websocket: client sent data before handshake is complete") + } + + 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 { + c.newCompressionWriter = compressNoContextTakeover + c.newDecompressionReader = decompressNoContextTakeover + } + + // 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, 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"...) + } + for k, vs := range responseHeader { + if k == "Sec-Websocket-Protocol" { + continue + } + for _, v := range vs { + p = append(p, k...) + p = append(p, ": "...) + for i := 0; i < len(v); i++ { + b := v[i] + if b <= 31 { + // prevent response splitting. + b = ' ' + } + p = append(p, b) + } + p = append(p, "\r\n"...) + } + } + p = append(p, "\r\n"...) + + // Clear deadlines set by HTTP server. + netConn.SetDeadline(time.Time{}) + + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) + } + if _, err = netConn.Write(p); err != nil { + netConn.Close() + return nil, err + } + if u.HandshakeTimeout > 0 { + netConn.SetWriteDeadline(time.Time{}) + } + + return c, nil +} + +// Upgrade upgrades the HTTP server connection to the WebSocket protocol. +// +// Deprecated: Use websocket.Upgrader instead. +// +// 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", http.StatusForbidden) +// return +// } +// +// If the endpoint supports subprotocols, then the application is responsible +// for negotiating the protocol used on the connection. Use the Subprotocols() +// function to get the subprotocols requested by the client. Use the +// Sec-Websocket-Protocol response header to specify the subprotocol selected +// by the application. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// negotiated subprotocol (Sec-Websocket-Protocol). +// +// The connection buffers IO to the underlying network connection. The +// readBufSize and writeBufSize parameters specify the size of the buffers to +// use. Messages can be larger than the buffers. +// +// If the request is not a valid WebSocket handshake, then Upgrade returns an +// error of type HandshakeError. Applications should handle this error by +// replying to the client with an HTTP error response. +func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) { + u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize} + u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) { + // don't return errors to maintain backwards compatibility + } + u.CheckOrigin = func(r *http.Request) bool { + // allow all connections by default + return true + } + return u.Upgrade(w, r, responseHeader) +} + +// Subprotocols returns the subprotocols requested by the client in the +// Sec-Websocket-Protocol header. +func Subprotocols(r *http.Request) []string { + h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol")) + if h == "" { + return nil + } + protocols := strings.Split(h, ",") + for i := range protocols { + protocols[i] = strings.TrimSpace(protocols[i]) + } + return protocols +} + +// IsWebSocketUpgrade returns true if the client requested upgrade to the +// WebSocket protocol. +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/components/reporters/slack/vendor/github.com/gorilla/websocket/tls_handshake.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/tls_handshake.go new file mode 100644 index 000000000..a62b68ccb --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/gorilla/websocket/tls_handshake_116.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/tls_handshake_116.go new file mode 100644 index 000000000..e1b2b44f6 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/gorilla/websocket/util.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/util.go new file mode 100644 index 000000000..31a5dee64 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/gorilla/websocket/util.go @@ -0,0 +1,298 @@ +// 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. + +package websocket + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "io" + "net/http" + "strings" + "unicode/utf8" +) + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func generateChallengeKey() (string, error) { + p := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, p); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(p), nil +} + +// 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 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 !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) + } + s = s[1:] + for i := 0; i < len(s); i++ { + switch s[i] { + case '"': + return s[:i], s[i+1:] + case '\\': + p := make([]byte, len(s)-1) + j := copy(p, s[:i]) + escape := true + for i = i + 1; i < len(s); i++ { + b := s[i] + switch { + case escape: + escape = false + p[j] = b + j++ + case b == '\\': + escape = true + case b == '"': + return string(p[:j]), s[i+1:] + default: + p[j] = b + j++ + } + } + return "", "" + } + } + 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 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] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + s = skipSpace(s) + if s != "" && s[0] != ',' { + continue headers + } + if equalASCIIFold(t, value) { + return true + } + if s == "" { + continue headers + } + s = s[1:] + } + } + return false +} + +// parseExtensions parses WebSocket extensions from a header. +func parseExtensions(header http.Header) []map[string]string { + // From RFC 6455: + // + // Sec-WebSocket-Extensions = extension-list + // extension-list = 1#extension + // extension = extension-token *( ";" extension-param ) + // extension-token = registered-token + // registered-token = token + // extension-param = token [ "=" (token | quoted-string) ] + // ;When using the quoted-string syntax variant, the value + // ;after quoted-string unescaping MUST conform to the + // ;'token' ABNF. + + var result []map[string]string +headers: + for _, s := range header["Sec-Websocket-Extensions"] { + for { + var t string + t, s = nextToken(skipSpace(s)) + if t == "" { + continue headers + } + ext := map[string]string{"": t} + for { + s = skipSpace(s) + if !strings.HasPrefix(s, ";") { + break + } + var k string + k, s = nextToken(skipSpace(s[1:])) + if k == "" { + continue headers + } + s = skipSpace(s) + var v string + if strings.HasPrefix(s, "=") { + v, s = nextTokenOrQuoted(skipSpace(s[1:])) + s = skipSpace(s) + } + if s != "" && s[0] != ',' && s[0] != ';' { + continue headers + } + ext[k] = v + } + if s != "" && s[0] != ',' { + continue headers + } + result = append(result, ext) + if s == "" { + continue headers + } + s = s[1:] + } + } + 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/components/reporters/slack/vendor/github.com/gorilla/websocket/x_net_proxy.go b/components/reporters/slack/vendor/github.com/gorilla/websocket/x_net_proxy.go new file mode 100644 index 000000000..2e668f6b8 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/.gitignore b/components/reporters/slack/vendor/github.com/slack-go/slack/.gitignore new file mode 100644 index 000000000..9b3903cd2 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/.gitignore @@ -0,0 +1,5 @@ +*.test +*~ +.idea/ +/vendor/ +.env* diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/.golangci.yml b/components/reporters/slack/vendor/github.com/slack-go/slack/.golangci.yml new file mode 100644 index 000000000..0ef0f87ec --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/CONTRIBUTING.md b/components/reporters/slack/vendor/github.com/slack-go/slack/CONTRIBUTING.md new file mode 100644 index 000000000..8b92d3531 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/LICENSE b/components/reporters/slack/vendor/github.com/slack-go/slack/LICENSE new file mode 100644 index 000000000..5145171f1 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Norberto Lopes +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/Makefile b/components/reporters/slack/vendor/github.com/slack-go/slack/Makefile new file mode 100644 index 000000000..727964016 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/Makefile @@ -0,0 +1,36 @@ +.PHONY: help deps fmt lint test test-race test-integration + +help: + @echo "" + @echo "Welcome to slack-go/slack make." + @echo "The following commands are available:" + @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 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" + @echo "" + @echo " make pr-prep : Run this before making a PR to run fmt, lint and tests" + @echo "" + +deps: + @go mod tidy + +fmt: + @go fmt . + +lint: + @go vet . + +test: + @go test -v -count=1 -timeout 300s -short ./... + +test-race: + @go test -v -count=1 -timeout 300s -short -race ./... + +test-integration: + @go test -v -count=1 -timeout 600s ./... + +pr-prep: fmt lint test-race test-integration diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/README.md b/components/reporters/slack/vendor/github.com/slack-go/slack/README.md new file mode 100644 index 000000000..127ae253c --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/README.md @@ -0,0 +1,111 @@ +Slack API in Go [![Go Reference](https://pkg.go.dev/badge/github.com/slack-go/slack.svg)](https://pkg.go.dev/github.com/slack-go/slack) [![CI](https://github.com/slack-go/slack/actions/workflows/test.yml/badge.svg)](https://github.com/slack-go/slack/actions/workflows/test.yml) +=============== + +This is the original Slack library for Go created by Norberto Lopes, transferred to a GitHub organization. + +You can also chat with us on the #slack-go, #slack-go-ja Slack channel on the Gophers Slack. + +![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. + +## Installing + +### *go get* + + $ go get -u github.com/slack-go/slack + +## Example + +### Getting all groups + +```golang +import ( + "fmt" + + "github.com/slack-go/slack" +) + +func main() { + api := slack.New("YOUR_TOKEN_HERE") + // If you set debugging, it will log all requests to the console + // Useful when encountering issues + // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) + groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false)) + if err != nil { + fmt.Printf("%s\n", err) + return + } + for _, group := range groups { + fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) + } +} +``` + +### Getting User Information + +```golang +import ( + "fmt" + + "github.com/slack-go/slack" +) + +func main() { + api := slack.New("YOUR_TOKEN_HERE") + user, err := api.GetUserInfo("U023BECGF") + if err != nil { + fmt.Printf("%s\n", err) + return + } + fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) +} +``` + +## 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 + + +## Minimal EventsAPI usage: + +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 +make a Pull Request, or create an Issue if you see any problem. + +Before making any Pull Request please run the following: + +``` +make pr-prep +``` + +This will check/update code formatting, linting and then run all tests + +## License + +BSD 2 Clause license diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/admin.go b/components/reporters/slack/vendor/github.com/slack-go/slack/admin.go new file mode 100644 index 000000000..d51426b56 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/admin.go @@ -0,0 +1,207 @@ +package slack + +import ( + "context" + "fmt" + "net/url" + "strings" +) + +func (api *Client) adminRequest(ctx context.Context, method string, teamName string, values url.Values) error { + resp := &SlackResponse{} + err := parseAdminResponse(ctx, api.httpclient, method, teamName, values, resp, api) + if err != nil { + return err + } + + return resp.Err() +} + +// DisableUser disabled a user account, given a user ID +func (api *Client) DisableUser(teamName string, uid string) error { + return api.DisableUserContext(context.Background(), teamName, uid) +} + +// DisableUserContext disabled a user account, given a user ID with a custom context +func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error { + values := url.Values{ + "user": {uid}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + if err := api.adminRequest(ctx, "setInactive", teamName, values); err != nil { + return fmt.Errorf("failed to disable user with id '%s': %s", uid, err) + } + + return nil +} + +// InviteGuest invites a user to Slack as a single-channel guest +func (api *Client) InviteGuest(teamName, channel, firstName, lastName, emailAddress string) error { + return api.InviteGuestContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) +} + +// InviteGuestContext invites a user to Slack as a single-channel guest with a custom context +func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "ultra_restricted": {"1"}, + "token": {api.token}, + "resend": {"true"}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "invite", teamName, values) + if err != nil { + return fmt.Errorf("Failed to invite single-channel guest: %s", err) + } + + return nil +} + +// InviteRestricted invites a user to Slack as a restricted account +func (api *Client) InviteRestricted(teamName, channel, firstName, lastName, emailAddress string) error { + return api.InviteRestrictedContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) +} + +// InviteRestrictedContext invites a user to Slack as a restricted account with a custom context +func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "restricted": {"1"}, + "token": {api.token}, + "resend": {"true"}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "invite", teamName, values) + if err != nil { + return fmt.Errorf("Failed to restricted account: %s", err) + } + + return nil +} + +// InviteToTeam invites a user to a Slack team +func (api *Client) InviteToTeam(teamName, firstName, lastName, emailAddress string) error { + return api.InviteToTeamContext(context.Background(), teamName, firstName, lastName, emailAddress) +} + +// InviteToTeamContext invites a user to a Slack team with a custom context +func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, lastName, emailAddress string) error { + values := url.Values{ + "email": {emailAddress}, + "first_name": {firstName}, + "last_name": {lastName}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "invite", teamName, values) + if err != nil { + return fmt.Errorf("Failed to invite to team: %s", err) + } + + return nil +} + +// SetRegular enables the specified user +func (api *Client) SetRegular(teamName, user string) error { + return api.SetRegularContext(context.Background(), teamName, user) +} + +// SetRegularContext enables the specified user with a custom context +func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + 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 nil +} + +// SendSSOBindingEmail sends an SSO binding email to the specified user +func (api *Client) SendSSOBindingEmail(teamName, user string) error { + return api.SendSSOBindingEmailContext(context.Background(), teamName, user) +} + +// SendSSOBindingEmailContext sends an SSO binding email to the specified user with a custom context +func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + 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 nil +} + +// SetUltraRestricted converts a user into a single-channel guest +func (api *Client) SetUltraRestricted(teamName, uid, channel string) error { + return api.SetUltraRestrictedContext(context.Background(), teamName, uid, channel) +} + +// SetUltraRestrictedContext converts a user into a single-channel guest with a custom context +func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, channel string) error { + values := url.Values{ + "user": {uid}, + "channel": {channel}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + err := api.adminRequest(ctx, "setUltraRestricted", teamName, values) + if err != nil { + return fmt.Errorf("Failed to ultra-restrict account: %s", err) + } + + return nil +} + +// SetRestricted converts a user into a restricted account +func (api *Client) SetRestricted(teamName, uid string, channelIds ...string) error { + return api.SetRestrictedContext(context.Background(), teamName, uid, channelIds...) +} + +// SetRestrictedContext converts a user into a restricted account with a custom context +func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string, channelIds ...string) error { + values := url.Values{ + "user": {uid}, + "token": {api.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + "channels": {strings.Join(channelIds, ",")}, + } + + err := api.adminRequest(ctx, "setRestricted", teamName, values) + if err != nil { + return fmt.Errorf("failed to restrict account: %s", err) + } + + return nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/admin_conversations.go b/components/reporters/slack/vendor/github.com/slack-go/slack/admin_conversations.go new file mode 100644 index 000000000..6f76568ba --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/admin_conversations.go @@ -0,0 +1,85 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +// 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/components/reporters/slack/vendor/github.com/slack-go/slack/apps.go b/components/reporters/slack/vendor/github.com/slack-go/slack/apps.go new file mode 100644 index 000000000..7322b15b9 --- /dev/null +++ b/components/reporters/slack/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 := postJSON(ctx, api.httpclient, api.endpoint+"apps.event.authorizations.list", api.appLevelToken, request, &resp, api) + + 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/components/reporters/slack/vendor/github.com/slack-go/slack/assistant.go b/components/reporters/slack/vendor/github.com/slack-go/slack/assistant.go new file mode 100644 index 000000000..8432f89b3 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/assistant.go @@ -0,0 +1,157 @@ +package slack + +import ( + "context" + "encoding/json" + "net/url" +) + +// AssistantThreadSetStatusParameters are the parameters for AssistantThreadSetStatus +type AssistantThreadsSetStatusParameters struct { + ChannelID string `json:"channel_id"` + Status string `json:"status"` + ThreadTS string `json:"thread_ts"` +} + +// 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"` +} + +// 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) + + // 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() +} + +// SetAssistantThreadStatus sets the status of a thread +// @see https://api.slack.com/methods/assistant.threads.setStatus +func (api *Client) SetAssistantThreadsStatus(params AssistantThreadsSetStatusParameters) (err error) { + return api.SetAssistantThreadsStatusContext(context.Background(), params) +} + +// SetAssistantThreadStatusContext sets the status of a thread with a custom context +// @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) + + 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() + +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/attachments.go b/components/reporters/slack/vendor/github.com/slack-go/slack/attachments.go new file mode 100644 index 000000000..f4eb9b932 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/attachments.go @@ -0,0 +1,98 @@ +package slack + +import "encoding/json" + +// AttachmentField contains information for an attachment field +// An Attachment can contain multiple of these +type AttachmentField struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// AttachmentAction is a button or menu to be included in the attachment. Required when +// using message buttons or menus and otherwise not useful. A maximum of 5 actions may be +// provided per attachment. +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". + 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. + Options []AttachmentActionOption `json:"options,omitempty"` // Optional. Maximum of 100 options can be provided in each menu. + SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu. + OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional. + Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional. + URL string `json:"url,omitempty"` // Optional. +} + +// actionType returns the type of the action +func (a AttachmentAction) actionType() ActionType { + return a.Type +} + +// AttachmentActionOption the individual option to appear in action menu. +type AttachmentActionOption struct { + Text string `json:"text"` // Required. + Value string `json:"value"` // Required. + Description string `json:"description,omitempty"` // Optional. Up to 30 characters. +} + +// AttachmentActionOptionGroup is a semi-hierarchal way to list available options to appear in action menu. +type AttachmentActionOptionGroup struct { + Text string `json:"text"` // Required. + Options []AttachmentActionOption `json:"options"` // Required. +} + +// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction) +// DEPRECATED: use InteractionCallback +type AttachmentActionCallback InteractionCallback + +// ConfirmationField are used to ask users to confirm actions +type ConfirmationField struct { + Title string `json:"title,omitempty"` // Optional. + Text string `json:"text"` // Required. + OkText string `json:"ok_text,omitempty"` // Optional. Defaults to "Okay" + DismissText string `json:"dismiss_text,omitempty"` // Optional. Defaults to "Cancel" +} + +// Attachment contains all the information for an attachment +type Attachment struct { + Color string `json:"color,omitempty"` + Fallback string `json:"fallback,omitempty"` + + CallbackID string `json:"callback_id,omitempty"` + ID int `json:"id,omitempty"` + + AuthorID string `json:"author_id,omitempty"` + AuthorName string `json:"author_name,omitempty"` + AuthorSubname string `json:"author_subname,omitempty"` + AuthorLink string `json:"author_link,omitempty"` + AuthorIcon string `json:"author_icon,omitempty"` + + Title string `json:"title,omitempty"` + TitleLink string `json:"title_link,omitempty"` + Pretext string `json:"pretext,omitempty"` + Text string `json:"text,omitempty"` + + ImageURL string `json:"image_url,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"` + MarkdownIn []string `json:"mrkdwn_in,omitempty"` + + Blocks Blocks `json:"blocks,omitempty"` + + Footer string `json:"footer,omitempty"` + FooterIcon string `json:"footer_icon,omitempty"` + + Ts json.Number `json:"ts,omitempty"` +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/audit.go b/components/reporters/slack/vendor/github.com/slack-go/slack/audit.go new file mode 100644 index 000000000..a3ea7ebdf --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/audit.go @@ -0,0 +1,152 @@ +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{} + err := api.getMethod(ctx, path, api.token, values, response) + 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/components/reporters/slack/vendor/github.com/slack-go/slack/auth.go b/components/reporters/slack/vendor/github.com/slack-go/slack/auth.go new file mode 100644 index 000000000..972f59ea6 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/auth.go @@ -0,0 +1,82 @@ +package slack + +import ( + "context" + "net/url" + "strconv" +) + +// AuthRevokeResponse contains our Auth response from the auth.revoke endpoint +type AuthRevokeResponse struct { + SlackResponse // Contains the "ok", and "Error", if any + Revoked bool `json:"revoked,omitempty"` +} + +// authRequest sends the actual request, and unmarshals the response +func (api *Client) authRequest(ctx context.Context, path string, values url.Values) (*AuthRevokeResponse, error) { + response := &AuthRevokeResponse{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// 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 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 + } + values := url.Values{ + "token": {token}, + } + + 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/components/reporters/slack/vendor/github.com/slack-go/slack/block.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block.go new file mode 100644 index 000000000..7c4f99308 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block.go @@ -0,0 +1,85 @@ +package slack + +// 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" + MBTHeader MessageBlockType = "header" + MBTRichText MessageBlockType = "rich_text" + MBTCall MessageBlockType = "call" + MBTVideo MessageBlockType = "video" + MBTMarkdown MessageBlockType = "markdown" +) + +// 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 +// the "blocks" value in Slack's JSON response, which varies depending on block type +type Blocks struct { + BlockSet []Block `json:"blocks,omitempty"` +} + +// 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"` + 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 { + return b.Type +} + +// NewBlockMessage creates a new Message that contains one or more blocks to be displayed +func NewBlockMessage(blocks ...Block) Message { + return Message{ + Msg: Msg{ + Blocks: Blocks{ + BlockSet: blocks, + }, + }, + } +} + +// AddBlockMessage appends a block to the end of the existing list of blocks +func AddBlockMessage(message Message, newBlk Block) Message { + message.Msg.Blocks.BlockSet = append(message.Msg.Blocks.BlockSet, newBlk) + return message +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_action.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_action.go new file mode 100644 index 000000000..819c0ef0d --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_action.go @@ -0,0 +1,31 @@ +package slack + +// ActionBlock defines data that is used to hold interactive elements. +// +// More Information: https://api.slack.com/reference/messaging/blocks#actions +type ActionBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + Elements *BlockElements `json:"elements"` +} + +// BlockType returns the type of the block +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{ + ElementSet: elements, + }, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_call.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_call.go new file mode 100644 index 000000000..c81dcb8be --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_call.go @@ -0,0 +1,28 @@ +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"` +} + +// 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 +} + +// NewCallBlock returns a new instance of a call block +func NewCallBlock(callID string) *CallBlock { + return &CallBlock{ + Type: MBTCall, + CallID: callID, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_context.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_context.go new file mode 100644 index 000000000..879ee61ee --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_context.go @@ -0,0 +1,37 @@ +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#context +type ContextBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + ContextElements ContextElements `json:"elements"` +} + +// BlockType returns the type of the block +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 +} + +// NewContextBlock returns a new instance of a context block +func NewContextBlock(blockID string, mixedElements ...MixedElement) *ContextBlock { + elements := ContextElements{ + Elements: mixedElements, + } + return &ContextBlock{ + Type: MBTContext, + BlockID: blockID, + ContextElements: elements, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_conv.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_conv.go new file mode 100644 index 000000000..7df9b107b --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_conv.go @@ -0,0 +1,456 @@ +package slack + +import ( + "encoding/json" + "fmt" +) + +type sumtype struct { + TypeVal string `json:"type"` +} + +// MarshalJSON implements the Marshaller interface for Blocks so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (b Blocks) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(b.BlockSet) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// UnmarshalJSON implements the Unmarshaller interface for Blocks, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *Blocks) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + + if string(data) == "{}" { + return nil + } + + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + var blocks Blocks + for _, r := range raw { + s := sumtype{} + err := json.Unmarshal(r, &s) + if err != nil { + return err + } + + var blockType string + if s.TypeVal != "" { + blockType = s.TypeVal + } + + var block Block + switch blockType { + case "actions": + block = &ActionBlock{} + case "context": + block = &ContextBlock{} + 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{} + default: + block = &UnknownBlock{} + } + + err = json.Unmarshal(r, block) + if err != nil { + return err + } + + blocks.BlockSet = append(blocks.BlockSet, block) + } + + *b = blocks + return nil +} + +// UnmarshalJSON implements the Unmarshaller interface for InputBlock, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *InputBlock) UnmarshalJSON(data []byte) error { + type alias InputBlock + a := struct { + Element json.RawMessage `json:"element"` + *alias + }{ + alias: (*alias)(b), + } + + if err := json.Unmarshal(data, &a); err != nil { + return err + } + + s := sumtype{} + if err := json.Unmarshal(a.Element, &s); err != nil { + return nil + } + + var e BlockElement + 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{} + default: + return fmt.Errorf("unsupported block element type %v", s.TypeVal) + } + + if err := json.Unmarshal(a.Element, e); err != nil { + return err + } + b.Element = e + + return nil +} + +// MarshalJSON implements the Marshaller interface for BlockElements so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (b *BlockElements) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(b.ElementSet) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// UnmarshalJSON implements the Unmarshaller interface for BlockElements, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *BlockElements) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + + if string(data) == "{}" { + return nil + } + + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + var blockElements BlockElements + for _, r := range raw { + s := sumtype{} + err := json.Unmarshal(r, &s) + if err != nil { + return err + } + + var blockElementType string + if s.TypeVal != "" { + blockElementType = s.TypeVal + } + + var blockElement BlockElement + switch blockElementType { + case "image": + blockElement = &ImageBlockElement{} + case "button": + blockElement = &ButtonBlockElement{} + case "overflow": + 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 "number_input": + blockElement = &NumberInputBlockElement{} + default: + return fmt.Errorf("unsupported block element type %v", blockElementType) + } + + err = json.Unmarshal(r, blockElement) + if err != nil { + return err + } + + blockElements.ElementSet = append(blockElements.ElementSet, blockElement) + } + + *b = blockElements + return nil +} + +// MarshalJSON implements the Marshaller interface for Accessory so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (a *Accessory) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(toBlockElement(a)) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// 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 + + if string(data) == "{\"accessory\":null}" { + return nil + } + + err := json.Unmarshal(data, &r) + if err != nil { + return err + } + + s := sumtype{} + err = json.Unmarshal(r, &s) + if err != nil { + return err + } + + var blockElementType string + if s.TypeVal != "" { + blockElementType = s.TypeVal + } + + switch blockElementType { + case "image": + element, err := unmarshalBlockElement(r, &ImageBlockElement{}) + if err != nil { + return err + } + a.ImageElement = element.(*ImageBlockElement) + case "button": + element, err := unmarshalBlockElement(r, &ButtonBlockElement{}) + if err != nil { + return err + } + a.ButtonElement = element.(*ButtonBlockElement) + case "overflow": + element, err := unmarshalBlockElement(r, &OverflowBlockElement{}) + if err != nil { + return err + } + a.OverflowElement = element.(*OverflowBlockElement) + case "datepicker": + element, err := unmarshalBlockElement(r, &DatePickerBlockElement{}) + if err != nil { + 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 { + return err + } + a.RadioButtonsElement = element.(*RadioButtonsBlockElement) + case "static_select", "external_select", "users_select", "conversations_select", "channels_select": + element, err := unmarshalBlockElement(r, &SelectBlockElement{}) + if err != nil { + return err + } + a.SelectElement = element.(*SelectBlockElement) + case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": + element, err := unmarshalBlockElement(r, &MultiSelectBlockElement{}) + if err != nil { + return err + } + a.MultiSelectElement = element.(*MultiSelectBlockElement) + case "checkboxes": + element, err := unmarshalBlockElement(r, &CheckboxGroupsBlockElement{}) + if err != nil { + return err + } + a.CheckboxGroupsBlockElement = element.(*CheckboxGroupsBlockElement) + default: + element, err := unmarshalBlockElement(r, &UnknownBlockElement{}) + if err != nil { + return err + } + a.UnknownElement = element.(*UnknownBlockElement) + } + + return nil +} + +func unmarshalBlockElement(r json.RawMessage, element BlockElement) (BlockElement, error) { + err := json.Unmarshal(r, element) + if err != nil { + return nil, err + } + return element, nil +} + +func toBlockElement(element *Accessory) BlockElement { + if element.ImageElement != nil { + return element.ImageElement + } + if element.ButtonElement != nil { + return element.ButtonElement + } + if element.OverflowElement != nil { + return element.OverflowElement + } + 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 + } + + return nil +} + +// MarshalJSON implements the Marshaller interface for ContextElements so that any JSON +// marshalling is delegated and proper type determination can be made before marshal +func (e *ContextElements) MarshalJSON() ([]byte, error) { + bytes, err := json.Marshal(e.Elements) + if err != nil { + return nil, err + } + + return bytes, nil +} + +// UnmarshalJSON implements the Unmarshaller interface for ContextElements, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (e *ContextElements) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + + if string(data) == "{\"elements\":null}" { + return nil + } + + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + for _, r := range raw { + s := sumtype{} + err := json.Unmarshal(r, &s) + if err != nil { + return err + } + + var contextElementType string + if s.TypeVal != "" { + contextElementType = s.TypeVal + } + + switch contextElementType { + case PlainTextType, MarkdownType: + elem, err := unmarshalBlockObject(r, &TextBlockObject{}) + if err != nil { + return err + } + + e.Elements = append(e.Elements, elem.(*TextBlockObject)) + case "image": + elem, err := unmarshalBlockElement(r, &ImageBlockElement{}) + if err != nil { + return err + } + + e.Elements = append(e.Elements, elem.(*ImageBlockElement)) + default: + return fmt.Errorf("unsupported context element type %v", contextElementType) + } + } + + return nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_divider.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_divider.go new file mode 100644 index 000000000..e10d7b053 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_divider.go @@ -0,0 +1,26 @@ +package slack + +// DividerBlock for displaying a divider line between blocks (similar to
tag in html) +// +// More Information: https://api.slack.com/reference/messaging/blocks#divider +type DividerBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` +} + +// BlockType returns the type of the block +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/components/reporters/slack/vendor/github.com/slack-go/slack/block_element.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_element.go new file mode 100644 index 000000000..2b32d331e --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_element.go @@ -0,0 +1,813 @@ +package slack + +// https://api.slack.com/reference/messaging/block-elements + +const ( + METCheckboxGroups MessageElementType = "checkboxes" + METImage MessageElementType = "image" + METButton MessageElementType = "button" + METOverflow MessageElementType = "overflow" + METDatepicker MessageElementType = "datepicker" + METTimepicker MessageElementType = "timepicker" + METDatetimepicker MessageElementType = "datetimepicker" + METPlainTextInput MessageElementType = "plain_text_input" + METRadioButtons MessageElementType = "radio_buttons" + METRichTextInput MessageElementType = "rich_text_input" + METEmailTextInput MessageElementType = "email_text_input" + METURLTextInput MessageElementType = "url_text_input" + METNumber MessageElementType = "number_input" + METFileInput MessageElementType = "file_input" + + MixedElementImage MixedElementType = "mixed_image" + MixedElementText MixedElementType = "mixed_text" + + OptTypeStatic string = "static_select" + OptTypeExternal string = "external_select" + OptTypeUser string = "users_select" + OptTypeConversations string = "conversations_select" + OptTypeChannels string = "channels_select" + + MultiOptTypeStatic string = "multi_static_select" + MultiOptTypeExternal string = "multi_external_select" + MultiOptTypeUser string = "multi_users_select" + MultiOptTypeConversations string = "multi_conversations_select" + MultiOptTypeChannels string = "multi_channels_select" +) + +type MessageElementType string +type MixedElementType string + +// BlockElement defines an interface that all block element types should implement. +type BlockElement interface { + ElementType() MessageElementType +} + +type MixedElement interface { + MixedElementType() MixedElementType +} + +type Accessory struct { + ImageElement *ImageBlockElement + ButtonElement *ButtonBlockElement + OverflowElement *OverflowBlockElement + DatePickerElement *DatePickerBlockElement + TimePickerElement *TimePickerBlockElement + PlainTextInputElement *PlainTextInputBlockElement + RichTextInputElement *RichTextInputBlockElement + RadioButtonsElement *RadioButtonsBlockElement + SelectElement *SelectBlockElement + MultiSelectElement *MultiSelectBlockElement + CheckboxGroupsBlockElement *CheckboxGroupsBlockElement + UnknownElement *UnknownBlockElement +} + +// NewAccessory returns a new Accessory for a given block element +func NewAccessory(element BlockElement) *Accessory { + switch element.(type) { + case *ImageBlockElement: + return &Accessory{ImageElement: element.(*ImageBlockElement)} + case *ButtonBlockElement: + return &Accessory{ButtonElement: element.(*ButtonBlockElement)} + case *OverflowBlockElement: + return &Accessory{OverflowElement: element.(*OverflowBlockElement)} + case *DatePickerBlockElement: + return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)} + case *TimePickerBlockElement: + return &Accessory{TimePickerElement: element.(*TimePickerBlockElement)} + case *PlainTextInputBlockElement: + return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)} + case *RichTextInputBlockElement: + return &Accessory{RichTextInputElement: element.(*RichTextInputBlockElement)} + case *RadioButtonsBlockElement: + return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)} + case *SelectBlockElement: + return &Accessory{SelectElement: element.(*SelectBlockElement)} + case *MultiSelectBlockElement: + return &Accessory{MultiSelectElement: element.(*MultiSelectBlockElement)} + case *CheckboxGroupsBlockElement: + return &Accessory{CheckboxGroupsBlockElement: element.(*CheckboxGroupsBlockElement)} + default: + return &Accessory{UnknownElement: element.(*UnknownBlockElement)} + } +} + +// BlockElements is a convenience struct defined to allow dynamic unmarshalling of +// the "elements" value in Slack's JSON response, which varies depending on BlockElement type +type BlockElements struct { + ElementSet []BlockElement `json:"elements,omitempty"` +} + +// UnknownBlockElement any block element that this library does not directly support. +// See the "Rich Elements" section at the following URL: +// https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less +// New block element types may be introduced by Slack at any time; this is a catch-all for any such block elements. +type UnknownBlockElement struct { + Type MessageElementType `json:"type"` + Elements BlockElements +} + +// ElementType returns the type of the Element +func (s UnknownBlockElement) ElementType() MessageElementType { + return s.Type +} + +// ImageBlockElement An element to insert an image - this element can be used +// in section and context blocks only. If you want a block with only an image +// in it, you're looking for the image block. +// +// 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"` + SlackFile *SlackFileObject `json:"slack_file,omitempty"` +} + +// ElementType returns the type of the Element +func (s ImageBlockElement) ElementType() MessageElementType { + return s.Type +} + +func (s ImageBlockElement) MixedElementType() MixedElementType { + return MixedElementImage +} + +// NewImageBlockElement returns a new instance of an image block element +func NewImageBlockElement(imageURL, altText string) *ImageBlockElement { + return &ImageBlockElement{ + Type: METImage, + 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 = "" + StylePrimary Style = "primary" + StyleDanger Style = "danger" +) + +// ButtonBlockElement defines an interactive element that inserts a button. The +// button can be a trigger for anything from opening a simple link to starting +// a complex workflow. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#button +type ButtonBlockElement struct { + Type MessageElementType `json:"type,omitempty"` + Text *TextBlockObject `json:"text"` + ActionID string `json:"action_id,omitempty"` + URL string `json:"url,omitempty"` + Value string `json:"value,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` + Style Style `json:"style,omitempty"` +} + +// ElementType returns the type of the element +func (s ButtonBlockElement) ElementType() MessageElementType { + return s.Type +} + +// 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 +func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *ButtonBlockElement { + return &ButtonBlockElement{ + Type: METButton, + ActionID: actionID, + Text: text, + Value: value, + } +} + +// OptionsResponse defines the response used for select block typahead. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select +type OptionsResponse struct { + Options []*OptionBlockObject `json:"options,omitempty"` +} + +// OptionGroupsResponse defines the response used for select block typahead. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select +type OptionGroupsResponse struct { + OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` +} + +// SelectBlockElement defines the simplest form of select menu, with a static list +// of options passed in when defining the element. +// +// 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"` + 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"` +} + +// 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 +func (s SelectBlockElement) ElementType() MessageElementType { + return MessageElementType(s.Type) +} + +// NewOptionsSelectBlockElement returns a new instance of SelectBlockElement for use with +// the Options object only. +func NewOptionsSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *SelectBlockElement { + return &SelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + Options: options, + } +} + +// 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( + optType string, + placeholder *TextBlockObject, + actionID string, + optGroups ...*OptionGroupBlockObject, +) *SelectBlockElement { + return &SelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + OptionGroups: optGroups, + } +} + +// MultiSelectBlockElement defines a multiselect menu, with a static list +// of options passed in when defining the element. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#multi_select +type MultiSelectBlockElement 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"` + InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` + 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"` +} + +// ElementType returns the type of the Element +func (s MultiSelectBlockElement) ElementType() MessageElementType { + return MessageElementType(s.Type) +} + +// NewOptionsMultiSelectBlockElement returns a new instance of SelectBlockElement for use with +// the Options object only. +func NewOptionsMultiSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *MultiSelectBlockElement { + return &MultiSelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + Options: options, + } +} + +// 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( + optType string, + placeholder *TextBlockObject, + actionID string, + optGroups ...*OptionGroupBlockObject, +) *MultiSelectBlockElement { + return &MultiSelectBlockElement{ + Type: optType, + Placeholder: placeholder, + ActionID: actionID, + OptionGroups: optGroups, + } +} + +// OverflowBlockElement defines the fields needed to use an overflow element. +// And Overflow Element is like a cross between a button and a select menu - +// when a user clicks on this overflow button, they will be presented with a +// list of options to choose from. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#overflow +type OverflowBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s OverflowBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewOverflowBlockElement returns an instance of a new Overflow Block Element +func NewOverflowBlockElement(actionID string, options ...*OptionBlockObject) *OverflowBlockElement { + return &OverflowBlockElement{ + Type: METOverflow, + ActionID: actionID, + Options: options, + } +} + +// 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. +// +// More Information: https://api.slack.com/reference/messaging/block-elements#datepicker +type DatePickerBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Placeholder *TextBlockObject `json:"placeholder,omitempty"` + InitialDate string `json:"initial_date,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s DatePickerBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewDatePickerBlockElement returns an instance of a date picker element +func NewDatePickerBlockElement(actionID string) *DatePickerBlockElement { + return &DatePickerBlockElement{ + Type: METDatepicker, + ActionID: actionID, + } +} + +// 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"` +} + +// 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,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"` +} + +type DispatchActionConfig struct { + TriggerActionsOn []string `json:"trigger_actions_on,omitempty"` +} + +// ElementType returns the type of the Element +func (s PlainTextInputBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewPlainTextInputBlockElement returns an instance of a plain-text input +// element +func NewPlainTextInputBlockElement(placeholder *TextBlockObject, actionID string) *PlainTextInputBlockElement { + return &PlainTextInputBlockElement{ + Type: METPlainTextInput, + ActionID: actionID, + Placeholder: placeholder, + } +} + +// 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,omitempty"` + Options []*OptionBlockObject `json:"options"` + InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (c CheckboxGroupsBlockElement) ElementType() MessageElementType { + return c.Type +} + +// NewCheckboxGroupsBlockElement returns an instance of a checkbox-group block element +func NewCheckboxGroupsBlockElement(actionID string, options ...*OptionBlockObject) *CheckboxGroupsBlockElement { + return &CheckboxGroupsBlockElement{ + Type: METCheckboxGroups, + ActionID: actionID, + Options: options, + } +} + +// RadioButtonsBlockElement defines an element which lets users choose one item +// from a list of possible options. +// +// More Information: https://api.slack.com/reference/block-kit/block-elements#radio +type RadioButtonsBlockElement struct { + Type MessageElementType `json:"type"` + ActionID string `json:"action_id,omitempty"` + Options []*OptionBlockObject `json:"options"` + InitialOption *OptionBlockObject `json:"initial_option,omitempty"` + Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` +} + +// ElementType returns the type of the Element +func (s RadioButtonsBlockElement) ElementType() MessageElementType { + return s.Type +} + +// NewRadioButtonsBlockElement returns an instance of a radio buttons element. +func NewRadioButtonsBlockElement(actionID string, options ...*OptionBlockObject) *RadioButtonsBlockElement { + return &RadioButtonsBlockElement{ + Type: METRadioButtons, + ActionID: actionID, + 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"` +} + +// 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 +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_file.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_file.go new file mode 100644 index 000000000..2f669b02a --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_file.go @@ -0,0 +1,31 @@ +package slack + +// FileBlock defines data that is used to display a remote file. +// +// More Information: https://api.slack.com/reference/block-kit/blocks#file +type FileBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` + ExternalID string `json:"external_id"` + Source string `json:"source"` +} + +// BlockType returns the type of the block +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{ + Type: MBTFile, + BlockID: blockID, + ExternalID: externalID, + Source: source, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_header.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_header.go new file mode 100644 index 000000000..3afb4c95f --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_header.go @@ -0,0 +1,43 @@ +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 { + option(&block) + } + + return &block +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_image.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_image.go new file mode 100644 index 000000000..2a914e5a0 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_image.go @@ -0,0 +1,55 @@ +package slack + +// ImageBlock defines data required to display an image as a block element +// +// More Information: https://api.slack.com/reference/messaging/blocks#image +type ImageBlock struct { + 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 +func (s ImageBlock) BlockType() MessageBlockType { + return s.Type +} + +// NewImageBlock returns an instance of a new Image Block type +func NewImageBlock(imageURL, altText, blockID string, title *TextBlockObject) *ImageBlock { + return &ImageBlock{ + Type: MBTImage, + ImageURL: imageURL, + AltText: altText, + BlockID: blockID, + 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/components/reporters/slack/vendor/github.com/slack-go/slack/block_input.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_input.go new file mode 100644 index 000000000..f74eda6d2 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_input.go @@ -0,0 +1,47 @@ +package slack + +// InputBlock defines data that is used to display user input fields. +// +// 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"` + DispatchAction bool `json:"dispatch_action,omitempty"` +} + +// BlockType returns the type of the block +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, 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/components/reporters/slack/vendor/github.com/slack-go/slack/block_markdown.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_markdown.go new file mode 100644 index 000000000..e22a0d16d --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/block_object.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_object.go new file mode 100644 index 000000000..fd73b6c4a --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_object.go @@ -0,0 +1,274 @@ +package slack + +import ( + "encoding/json" + "errors" +) + +// Block Objects are also known as Composition Objects +// +// For more information: https://api.slack.com/reference/messaging/composition-objects + +// BlockObject defines an interface that all block object types should +// implement. +// @TODO: Is this interface needed? + +// blockObject object types +const ( + MarkdownType = "mrkdwn" + PlainTextType = "plain_text" + // The following objects don't actually have types and their corresponding + // const values are just for internal use + motConfirmation = "confirm" + motOption = "option" + motOptionGroup = "option_group" +) + +type MessageObjectType string + +type blockObject interface { + validateType() MessageObjectType +} + +type BlockObjects struct { + TextObjects []*TextBlockObject + ConfirmationObjects []*ConfirmationBlockObject + OptionObjects []*OptionBlockObject + OptionGroupObjects []*OptionGroupBlockObject +} + +// UnmarshalJSON implements the Unmarshaller interface for BlockObjects, so that any JSON +// unmarshalling is delegated and proper type determination can be made before unmarshal +func (b *BlockObjects) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + for _, r := range raw { + var obj map[string]interface{} + err := json.Unmarshal(r, &obj) + if err != nil { + return err + } + + blockObjectType := getBlockObjectType(obj) + + switch blockObjectType { + case PlainTextType, MarkdownType: + object, err := unmarshalBlockObject(r, &TextBlockObject{}) + if err != nil { + return err + } + b.TextObjects = append(b.TextObjects, object.(*TextBlockObject)) + case motConfirmation: + object, err := unmarshalBlockObject(r, &ConfirmationBlockObject{}) + if err != nil { + return err + } + b.ConfirmationObjects = append(b.ConfirmationObjects, object.(*ConfirmationBlockObject)) + case motOption: + object, err := unmarshalBlockObject(r, &OptionBlockObject{}) + if err != nil { + return err + } + b.OptionObjects = append(b.OptionObjects, object.(*OptionBlockObject)) + case motOptionGroup: + object, err := unmarshalBlockObject(r, &OptionGroupBlockObject{}) + if err != nil { + return err + } + b.OptionGroupObjects = append(b.OptionGroupObjects, object.(*OptionGroupBlockObject)) + + } + } + + return nil +} + +// Ideally would have a better way to identify the block objects for +// type casting at time of unmarshalling, should be adapted if possible +// to accomplish in a more reliable manner. +func getBlockObjectType(obj map[string]interface{}) string { + if t, ok := obj["type"].(string); ok { + return t + } + if _, ok := obj["confirm"].(string); ok { + return "confirm" + } + if _, ok := obj["options"].(string); ok { + return "option_group" + } + if _, ok := obj["text"].(string); ok { + if _, ok := obj["value"].(string); ok { + return "option" + } + } + return "" +} + +func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, error) { + err := json.Unmarshal(r, object) + if err != nil { + return nil, err + } + return object, nil +} + +// TextBlockObject defines a text element object to be used with blocks +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#text +type TextBlockObject struct { + Type string `json:"type"` + Text string `json:"text"` + Emoji *bool `json:"emoji,omitempty"` + Verbatim bool `json:"verbatim,omitempty"` +} + +// validateType enforces block objects for element and block parameters +func (s TextBlockObject) validateType() MessageObjectType { + return MessageObjectType(s.Type) +} + +// validateType enforces block objects for element and block parameters +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 +// +// 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: emojiPtr, + Verbatim: verbatim, + } +} + +// BlockType returns the type of the block +func (t TextBlockObject) BlockType() MessageBlockType { + if t.Type == "mrkdwn" { + return MarkdownType + } + return PlainTextType +} + +// ConfirmationBlockObject defines a dialog that provides a confirmation step to +// any interactive element. This dialog will ask the user to confirm their action by +// offering a confirm and deny buttons. +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#confirm +type ConfirmationBlockObject struct { + Title *TextBlockObject `json:"title"` + Text *TextBlockObject `json:"text"` + Confirm *TextBlockObject `json:"confirm"` + Deny *TextBlockObject `json:"deny,omitempty"` + Style Style `json:"style,omitempty"` +} + +// validateType enforces block objects for element and block parameters +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{ + Title: title, + Text: text, + Confirm: confirm, + Deny: deny, + } +} + +// OptionBlockObject represents a single selectable item in a select menu +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#option +type OptionBlockObject struct { + 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, description *TextBlockObject) *OptionBlockObject { + return &OptionBlockObject{ + Text: text, + Value: value, + Description: description, + } +} + +// validateType enforces block objects for element and block parameters +func (s OptionBlockObject) validateType() MessageObjectType { + return motOption +} + +// OptionGroupBlockObject Provides a way to group options in a select menu. +// +// More Information: https://api.slack.com/reference/messaging/composition-objects#option-group +type OptionGroupBlockObject struct { + Label *TextBlockObject `json:"label,omitempty"` + Options []*OptionBlockObject `json:"options"` +} + +// validateType enforces block objects for element and block parameters +func (s OptionGroupBlockObject) validateType() MessageObjectType { + return motOptionGroup +} + +// NewOptionGroupBlockElement returns an instance of a new option group block element +func NewOptionGroupBlockElement(label *TextBlockObject, options ...*OptionBlockObject) *OptionGroupBlockObject { + return &OptionGroupBlockObject{ + Label: label, + Options: options, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_rich_text.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_rich_text.go new file mode 100644 index 000000000..73233d23a --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_rich_text.go @@ -0,0 +1,550 @@ +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 +} + +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, + } +} + +// ElementType 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 +} + +// NewRichTextSectionBlockElement . +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"` +} + +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: RTSEText, + 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"` +} + +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 +} + +// RichTextQuote represents rich_text_quote element type. +type RichTextQuote RichTextSection + +// 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 + } + *s = RichTextQuote(rts) + s.Type = RTEQuote + return nil +} + +// RichTextPreformatted represents rich_text_quote element type. +type RichTextPreformatted struct { + RichTextSection + Border int `json:"border"` +} + +// 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"` + } + if err := json.Unmarshal(b, &standalone); err != nil { + return err + } + *s = RichTextPreformatted{ + RichTextSection: rts, + Border: standalone.Border, + } + s.Type = RTEPreformatted + return nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_section.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_section.go new file mode 100644 index 000000000..0d2352a03 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_section.go @@ -0,0 +1,57 @@ +package slack + +// SectionBlock defines a new block of type section +// +// More Information: https://api.slack.com/reference/messaging/blocks#section +type SectionBlock struct { + Type MessageBlockType `json:"type"` + Text *TextBlockObject `json:"text,omitempty"` + 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 +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) + +func SectionBlockOptionBlockID(blockID string) SectionBlockOption { + return func(block *SectionBlock) { + block.BlockID = blockID + } +} + +// 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{ + Type: MBTSection, + Text: textObj, + Fields: fields, + Accessory: accessory, + } + + for _, option := range options { + option(&block) + } + + return &block +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_unknown.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_unknown.go new file mode 100644 index 000000000..7a49a2c8d --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/block_unknown.go @@ -0,0 +1,18 @@ +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. +type UnknownBlock struct { + Type MessageBlockType `json:"type"` + BlockID string `json:"block_id,omitempty"` +} + +// 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 +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/block_video.go b/components/reporters/slack/vendor/github.com/slack-go/slack/block_video.go new file mode 100644 index 000000000..4d6739c46 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/bookmarks.go b/components/reporters/slack/vendor/github.com/slack-go/slack/bookmarks.go new file mode 100644 index 000000000..1f07e59b0 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/bots.go b/components/reporters/slack/vendor/github.com/slack-go/slack/bots.go new file mode 100644 index 000000000..1ab946962 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/bots.go @@ -0,0 +1,69 @@ +package slack + +import ( + "context" + "net/url" +) + +// Bot contains information about a bot +type Bot struct { + ID string `json:"id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + UserID string `json:"user_id"` + AppID string `json:"app_id"` + Updated JSONTime `json:"updated"` + Icons Icons `json:"icons"` +} + +type botResponseFull struct { + Bot `json:"bot,omitempty"` // GetBotInfo + SlackResponse +} + +func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) { + response := &botResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + if err := response.Err(); err != nil { + return nil, err + } + + return response, nil +} + +type GetBotInfoParameters struct { + Bot string + TeamID string +} + +// 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 parameters.Bot != "" { + values.Add("bot", parameters.Bot) + } + + if parameters.TeamID != "" { + values.Add("team_id", parameters.TeamID) + } + + response, err := api.botRequest(ctx, "bots.info", values) + if err != nil { + return nil, err + } + return &response.Bot, nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/calls.go b/components/reporters/slack/vendor/github.com/slack-go/slack/calls.go new file mode 100644 index 000000000..2d6e91f16 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/canvas.go b/components/reporters/slack/vendor/github.com/slack-go/slack/canvas.go new file mode 100644 index 000000000..5225afa35 --- /dev/null +++ b/components/reporters/slack/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/components/reporters/slack/vendor/github.com/slack-go/slack/channels.go b/components/reporters/slack/vendor/github.com/slack-go/slack/channels.go new file mode 100644 index 000000000..88d567bff --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/channels.go @@ -0,0 +1,37 @@ +package slack + +import ( + "context" + "net/url" +) + +type channelResponseFull struct { + Channel Channel `json:"channel"` + Channels []Channel `json:"channels"` + Purpose string `json:"purpose"` + Topic string `json:"topic"` + NotInChannel bool `json:"not_in_channel"` + History + SlackResponse + Metadata ResponseMetadata `json:"response_metadata"` +} + +// 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"` + 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) + if err != nil { + return nil, err + } + + return response, response.Err() +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/chat.go b/components/reporters/slack/vendor/github.com/slack-go/slack/chat.go new file mode 100644 index 000000000..85c4848ba --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/chat.go @@ -0,0 +1,926 @@ +package slack + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "regexp" + "strconv" + + "github.com/slack-go/slack/slackutilsx" +) + +const ( + DEFAULT_MESSAGE_USERNAME = "" + DEFAULT_MESSAGE_REPLY_BROADCAST = false + DEFAULT_MESSAGE_ASUSER = false + DEFAULT_MESSAGE_PARSE = "" + DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" + DEFAULT_MESSAGE_LINK_NAMES = 0 + DEFAULT_MESSAGE_UNFURL_LINKS = false + DEFAULT_MESSAGE_UNFURL_MEDIA = true + DEFAULT_MESSAGE_ICON_URL = "" + DEFAULT_MESSAGE_ICON_EMOJI = "" + DEFAULT_MESSAGE_MARKDOWN = true + DEFAULT_MESSAGE_ESCAPE_TEXT = true +) + +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 + Text string `json:"text"` + SlackResponse +} + +// 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 { + if len(c.Timestamp) > 0 { + return c.Timestamp + } + return c.MessageTimeStamp +} + +// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request +type PostMessageParameters struct { + Username string `json:"username"` + AsUser bool `json:"as_user"` + Parse string `json:"parse"` + ThreadTimestamp string `json:"thread_ts"` + ReplyBroadcast bool `json:"reply_broadcast"` + LinkNames int `json:"link_names"` + UnfurlLinks bool `json:"unfurl_links"` + UnfurlMedia bool `json:"unfurl_media"` + IconURL string `json:"icon_url"` + IconEmoji string `json:"icon_emoji"` + Markdown bool `json:"mrkdwn,omitempty"` + EscapeText bool `json:"escape_text"` + + // 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 +func NewPostMessageParameters() PostMessageParameters { + return PostMessageParameters{ + Username: DEFAULT_MESSAGE_USERNAME, + User: DEFAULT_MESSAGE_USERNAME, + AsUser: DEFAULT_MESSAGE_ASUSER, + Parse: DEFAULT_MESSAGE_PARSE, + ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP, + LinkNames: DEFAULT_MESSAGE_LINK_NAMES, + UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, + UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, + IconURL: DEFAULT_MESSAGE_ICON_URL, + IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, + Markdown: DEFAULT_MESSAGE_MARKDOWN, + EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, + } +} + +// DeleteMessage deletes a message in a channel. +// For more details, see DeleteMessageContext documentation. +func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { + return api.DeleteMessageContext(context.Background(), channel, messageTimestamp) +} + +// 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, + channel, + MsgOptionDelete(messageTimestamp), + ) + return respChannel, respTimestamp, err +} + +// 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) { + 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, 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) { + return api.PostMessageContext(context.Background(), channelID, options...) +} + +// 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, + channelID, + MsgOptionPost(), + MsgOptionCompose(options...), + ) + return respChannel, respTimestamp, err +} + +// 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...) +} + +// 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, + channelID, + MsgOptionPostEphemeral(userID), + MsgOptionCompose(options...), + ) + return timestamp, err +} + +// 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.UpdateMessageContext(context.Background(), channelID, timestamp, options...) +} + +// 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, + channelID, + MsgOptionUpdate(timestamp), + MsgOptionCompose(options...), + ) +} + +// 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.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...)) +} + +// 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. +// 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...).BuildRequestContext(ctx, api.token, channelID); err != nil { + return "", "", "", err + } + + 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 + } + + 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. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function +// will be supported by the library. +func UnsafeApplyMsgOptions(token, channel, apiurl string, options ...MsgOption) (string, url.Values, error) { + config, err := applyMsgOptions(token, channel, apiurl, options...) + return config.endpoint, config.values, err +} + +func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendConfig, error) { + config := sendConfig{ + apiurl: apiurl, + endpoint: apiurl + string(chatPostMessage), + values: url.Values{ + "token": {token}, + "channel": {channel}, + }, + } + + for _, opt := range options { + if err := opt(&config); err != nil { + return config, err + } + } + + return config, nil +} + +func buildSender(apiurl string, options ...MsgOption) sendConfig { + return sendConfig{ + apiurl: apiurl, + options: options, + } +} + +type sendMode string + +const ( + chatUpdate sendMode = "chat.update" + chatPostMessage sendMode = "chat.postMessage" + chatScheduleMessage sendMode = "chat.scheduleMessage" + chatDelete sendMode = "chat.delete" + chatPostEphemeral sendMode = "chat.postEphemeral" + chatResponse sendMode = "chat.responseURL" + chatMeMessage sendMode = "chat.meMessage" + chatUnfurl sendMode = "chat.unfurl" +) + +type sendConfig struct { + apiurl string + options []MsgOption + mode sendMode + endpoint string + values url.Values + attachments []Attachment + metadata SlackMetadata + blocks Blocks + responseType string + replaceOriginal bool + deleteOriginal bool +} + +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 + } + + switch t.mode { + case chatResponse: + return responseURLSender{ + endpoint: t.endpoint, + values: t.values, + attachments: t.attachments, + metadata: t.metadata, + blocks: t.blocks, + responseType: t.responseType, + replaceOriginal: t.replaceOriginal, + deleteOriginal: t.deleteOriginal, + }.BuildRequestContext(ctx) + default: + return formSender{endpoint: t.endpoint, values: t.values}.BuildRequestContext(ctx) + } +} + +type formSender struct { + endpoint string + values url.Values +} + +func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { + return t.BuildRequestContext(context.Background()) +} + +func (t formSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { + req, err := formReq(ctx, t.endpoint, t.values) + return req, func(resp *chatResponseFull) responseParser { + return newJSONParser(resp) + }, err +} + +type responseURLSender struct { + endpoint string + values url.Values + attachments []Attachment + metadata SlackMetadata + blocks Blocks + responseType string + replaceOriginal bool + deleteOriginal bool +} + +func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { + 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, + }) + return req, func(resp *chatResponseFull) responseParser { + return newContentTypeParser(resp) + }, err +} + +// MsgOption option provided when sending a message. +type MsgOption func(*sendConfig) error + +// MsgOptionSchedule schedules a messages. +func MsgOptionSchedule(postAt string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatScheduleMessage) + config.values.Add("post_at", postAt) + return nil + } +} + +// MsgOptionPost posts a messages, this is the default. +func MsgOptionPost() MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatPostMessage) + config.values.Del("ts") + return nil + } +} + +// MsgOptionPostEphemeral - posts an ephemeral message to the provided user. +func MsgOptionPostEphemeral(userID string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatPostEphemeral) + MsgOptionUser(userID)(config) + config.values.Del("ts") + + return nil + } +} + +// MsgOptionMeMessage posts a "me message" type from the calling user +func MsgOptionMeMessage() MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatMeMessage) + return nil + } +} + +// MsgOptionUpdate updates a message based on the timestamp. +func MsgOptionUpdate(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUpdate) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionDelete deletes a message based on the timestamp. +func MsgOptionDelete(timestamp string) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatDelete) + config.values.Add("ts", timestamp) + return nil + } +} + +// MsgOptionUnfurl unfurls a message based on the timestamp. +func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption { + return func(config *sendConfig) error { + config.endpoint = config.apiurl + string(chatUnfurl) + config.values.Add("ts", timestamp) + unfurlsStr, err := json.Marshal(unfurls) + if err == nil { + config.values.Add("unfurls", string(unfurlsStr)) + } + return err + } +} + +// 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 + } +} + +// MsgOptionResponseURL supplies a url to use as the endpoint. +func MsgOptionResponseURL(url string, responseType string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = url + config.responseType = responseType + config.values.Del("ts") + return nil + } +} + +// MsgOptionReplaceOriginal replaces original message with response url +func MsgOptionReplaceOriginal(responseURL string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = responseURL + config.replaceOriginal = true + return nil + } +} + +// MsgOptionDeleteOriginal deletes original message with response url +func MsgOptionDeleteOriginal(responseURL string) MsgOption { + return func(config *sendConfig) error { + config.mode = chatResponse + config.endpoint = responseURL + config.deleteOriginal = true + return nil + } +} + +// MsgOptionAsUser whether or not to send the message as the user. +func MsgOptionAsUser(b bool) MsgOption { + return func(config *sendConfig) error { + if b != DEFAULT_MESSAGE_ASUSER { + config.values.Set("as_user", "true") + } + return nil + } +} + +// MsgOptionUser set the user for the message. +func MsgOptionUser(userID string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("user", userID) + return nil + } +} + +// MsgOptionUsername set the username for the message. +func MsgOptionUsername(username string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("username", username) + return nil + } +} + +// MsgOptionText provide the text for the message, optionally escape the provided +// text. +func MsgOptionText(text string, escape bool) MsgOption { + return func(config *sendConfig) error { + if escape { + text = slackutilsx.EscapeMessage(text) + } + config.values.Add("text", text) + return nil + } +} + +// MsgOptionAttachments provide attachments for the message. +func MsgOptionAttachments(attachments ...Attachment) MsgOption { + return func(config *sendConfig) error { + if attachments == nil { + return nil + } + + 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 + } +} + +// MsgOptionBlocks sets blocks for the message +func MsgOptionBlocks(blocks ...Block) MsgOption { + return func(config *sendConfig) error { + if blocks == nil { + return nil + } + + config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...) + + blocks, err := json.Marshal(blocks) + if err == nil { + config.values.Set("blocks", string(blocks)) + } + return err + } +} + +// MsgOptionEnableLinkUnfurl enables link unfurling +func MsgOptionEnableLinkUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_links", "true") + return nil + } +} + +// MsgOptionDisableLinkUnfurl disables link unfurling +func MsgOptionDisableLinkUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_links", "false") + return nil + } +} + +// MsgOptionDisableMediaUnfurl disables media unfurling. +func MsgOptionDisableMediaUnfurl() MsgOption { + return func(config *sendConfig) error { + config.values.Set("unfurl_media", "false") + return nil + } +} + +// MsgOptionDisableMarkdown disables markdown. +func MsgOptionDisableMarkdown() MsgOption { + return func(config *sendConfig) error { + config.values.Set("mrkdwn", "false") + return nil + } +} + +// MsgOptionTS sets the thread TS of the message to enable creating or replying to a thread +func MsgOptionTS(ts string) MsgOption { + return func(config *sendConfig) error { + config.values.Set("thread_ts", ts) + return nil + } +} + +// MsgOptionBroadcast sets reply_broadcast to true +func MsgOptionBroadcast() MsgOption { + return func(config *sendConfig) error { + config.values.Set("reply_broadcast", "true") + return nil + } +} + +// MsgOptionCompose combines multiple options into a single option. +func MsgOptionCompose(options ...MsgOption) MsgOption { + return func(config *sendConfig) error { + for _, opt := range options { + if err := opt(config); err != nil { + return err + } + } + return nil + } +} + +// MsgOptionParse set parse option. +func MsgOptionParse(b bool) MsgOption { + return func(config *sendConfig) error { + var v string + if b { + v = "full" + } else { + v = "none" + } + config.values.Set("parse", v) + return nil + } +} + +// MsgOptionIconURL sets an icon URL +func MsgOptionIconURL(iconURL string) MsgOption { + 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(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 + } +} + +// 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 + } +} + +// UnsafeMsgOptionEndpoint deliver the message to the specified endpoint. +// NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option +// will be supported by the library, it is subject to change without notice that +// may result in compilation errors or runtime behaviour changes. +func UnsafeMsgOptionEndpoint(endpoint string, update func(url.Values)) MsgOption { + return func(config *sendConfig) error { + config.endpoint = endpoint + update(config.values) + return nil + } +} + +// MsgOptionPostMessageParameters maintain backwards compatibility. +func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { + return func(config *sendConfig) error { + if params.Username != DEFAULT_MESSAGE_USERNAME { + config.values.Set("username", params.Username) + } + + // chat.postEphemeral support + if params.User != DEFAULT_MESSAGE_USERNAME { + config.values.Set("user", params.User) + } + + // never generates an error. + MsgOptionAsUser(params.AsUser)(config) + + if params.Parse != DEFAULT_MESSAGE_PARSE { + config.values.Set("parse", params.Parse) + } + if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { + config.values.Set("link_names", "1") + } + + 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. + if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { + config.values.Set("unfurl_links", "false") + } + if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { + config.values.Set("unfurl_media", "false") + } + if params.IconURL != DEFAULT_MESSAGE_ICON_URL { + config.values.Set("icon_url", params.IconURL) + } + if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { + config.values.Set("icon_emoji", params.IconEmoji) + } + if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { + config.values.Set("mrkdwn", "false") + } + + if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { + config.values.Set("thread_ts", params.ThreadTimestamp) + } + if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST { + config.values.Set("reply_broadcast", "true") + } + + if len(params.FileIDs) > 0 { + return MsgOptionFileIDs(params.FileIDs)(config) + } + + return nil + } +} + +// 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. +// 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{ + "channel": {params.Channel}, + "message_ts": {params.Ts}, + } + + response := struct { + Channel string `json:"channel"` + Permalink string `json:"permalink"` + SlackResponse + }{} + err := api.getMethod(ctx, "chat.getPermalink", api.token, values, &response) + if err != nil { + return "", err + } + return response.Permalink, response.Err() +} + +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. +// 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 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) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + response := struct { + Messages []ScheduledMessage `json:"scheduled_messages"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + + err = api.postMethod(ctx, "chat.scheduledMessages.list", values, &response) + if err != nil { + return nil, "", err + } + + return response.Messages, response.ResponseMetaData.NextCursor, response.Err() +} + +type DeleteScheduledMessageParameters struct { + Channel string + ScheduledMessageID string + AsUser bool +} + +// 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 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}, + "channel": {params.Channel}, + "scheduled_message_id": {params.ScheduledMessageID}, + "as_user": {strconv.FormatBool(params.AsUser)}, + } + response := struct { + SlackResponse + }{} + + err := api.postMethod(ctx, "chat.deleteScheduledMessage", values, &response) + if err != nil { + return false, err + } + + return response.Ok, response.Err() +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/comment.go b/components/reporters/slack/vendor/github.com/slack-go/slack/comment.go new file mode 100644 index 000000000..7d1c0d4eb --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/comment.go @@ -0,0 +1,10 @@ +package slack + +// Comment contains all the information relative to a comment +type Comment struct { + ID string `json:"id,omitempty"` + Created JSONTime `json:"created,omitempty"` + Timestamp JSONTime `json:"timestamp,omitempty"` + User string `json:"user,omitempty"` + Comment string `json:"comment,omitempty"` +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/conversation.go b/components/reporters/slack/vendor/github.com/slack-go/slack/conversation.go new file mode 100644 index 000000000..33eb0ff9b --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/conversation.go @@ -0,0 +1,914 @@ +package slack + +import ( + "context" + "encoding/json" + "errors" + "net/url" + "strconv" + "strings" +) + +// Conversation is the foundation for IM and BaseGroupConversation +type Conversation struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + IsOpen bool `json:"is_open"` + LastRead string `json:"last_read,omitempty"` + Latest *Message `json:"latest,omitempty"` + UnreadCount int `json:"unread_count,omitempty"` + UnreadCountDisplay int `json:"unread_count_display,omitempty"` + IsGroup bool `json:"is_group"` + IsShared bool `json:"is_shared"` + 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"` + Unlinked int `json:"unlinked"` + NameNormalized string `json:"name_normalized"` + NumMembers int `json:"num_members"` + Priority float64 `json:"priority"` + User string `json:"user"` + 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 +type GroupConversation struct { + Conversation + Name string `json:"name"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + Members []string `json:"members"` + Topic Topic `json:"topic"` + Purpose Purpose `json:"purpose"` +} + +// Topic contains information about the topic +type Topic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} + +// Purpose contains information about the purpose +type Purpose struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} + +// Properties contains the Canvas associated to the channel. +type Properties struct { + Canvas Canvas `json:"canvas"` + PostingRestrictedTo RestrictedTo `json:"posting_restricted_to"` + Tabs []Tab `json:"tabs"` + ThreadsRestrictedTo RestrictedTo `json:"threads_restricted_to"` +} + +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 GetUsersInConversationParameters struct { + ChannelID string + Cursor string + Limit int +} + +type GetConversationsForUserParameters struct { + UserID string + Cursor string + 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. +// 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. +// 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}, + "channel": {params.ChannelID}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + response := struct { + Members []string `json:"members"` + ResponseMetaData responseMetaData `json:"response_metadata"` + SlackResponse + }{} + + err := api.postMethod(ctx, "conversations.members", values, &response) + if err != nil { + return nil, "", err + } + + if err := response.Err(); err != nil { + return nil, "", err + } + + return response.Members, response.ResponseMetaData.NextCursor, nil +} + +// 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}, + } + if params.UserID != "" { + values.Add("user", params.UserID) + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Types != nil { + values.Add("types", strings.Join(params.Types, ",")) + } + 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"` + SlackResponse + }{} + err = api.postMethod(ctx, "users.conversations", values, &response) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + } + + response := SlackResponse{} + err := api.postMethod(ctx, "conversations.archive", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + } + response := SlackResponse{} + err := api.postMethod(ctx, "conversations.unarchive", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + "topic": {topic}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + err := api.postMethod(ctx, "conversations.setTopic", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + "purpose": {purpose}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + + err := api.postMethod(ctx, "conversations.setPurpose", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + "name": {channelName}, + } + response := struct { + SlackResponse + Channel *Channel `json:"channel"` + }{} + + err := api.postMethod(ctx, "conversations.rename", values, &response) + if err != nil { + return nil, err + } + + return response.Channel, response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + "users": {strings.Join(users, ",")}, + } + 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() +} + +// 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. +// 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}, + "channel": {channelID}, + "user": {user}, + } + + response := SlackResponse{} + err := api.postMethod(ctx, "conversations.kick", values, &response) + if err != nil { + return err + } + + return response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + } + response := struct { + SlackResponse + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + }{} + + err = api.postMethod(ctx, "conversations.close", values, &response) + if err != nil { + return false, false, err + } + + return response.NoOp, response.AlreadyClosed, response.Err() +} + +type CreateConversationParams struct { + ChannelName string + IsPrivate bool + TeamID string +} + +// 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": {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 { + return nil, err + } + + return &response.Channel, nil +} + +// GetConversationInfoInput Defines the parameters of a GetConversationInfo and GetConversationInfoContext function +type GetConversationInfoInput struct { + ChannelID string + IncludeLocale bool + IncludeNumMembers bool +} + +// 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": {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 { + return nil, err + } + + return &response.Channel, response.Err() +} + +// 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. +// 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}, + "channel": {channelID}, + } + + response, err := api.channelRequest(ctx, "conversations.leave", values) + if err != nil { + return false, err + } + + return response.NotInChannel, err +} + +type GetConversationRepliesParameters struct { + 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. +// 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. +// 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}, + "channel": {params.ChannelID}, + "ts": {params.Timestamp}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + if params.Oldest != "" { + values.Add("oldest", params.Oldest) + } + if params.Inclusive { + values.Add("inclusive", "1") + } 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"` + ResponseMetaData struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` + Messages []Message `json:"messages"` + }{} + + err = api.postMethod(ctx, "conversations.replies", values, &response) + if err != nil { + return nil, false, "", err + } + + return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err() +} + +type GetConversationsParameters struct { + Cursor string + ExcludeArchived bool + Limit int + Types []string + TeamID string +} + +// 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. +// 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}, + } + if params.Cursor != "" { + values.Add("cursor", params.Cursor) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + 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"` + SlackResponse + }{} + + err = api.postMethod(ctx, "conversations.list", values, &response) + if err != nil { + return nil, "", err + } + + return response.Channels, response.ResponseMetaData.NextCursor, response.Err() +} + +type OpenConversationParameters struct { + ChannelID string + ReturnIM bool + Users []string +} + +// 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. +// 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}, + "return_im": {strconv.FormatBool(params.ReturnIM)}, + } + if params.ChannelID != "" { + values.Add("channel", params.ChannelID) + } + if params.Users != nil { + values.Add("users", strings.Join(params.Users, ",")) + } + response := struct { + Channel *Channel `json:"channel"` + NoOp bool `json:"no_op"` + AlreadyOpen bool `json:"already_open"` + SlackResponse + }{} + + err := api.postMethod(ctx, "conversations.open", values, &response) + if err != nil { + return nil, false, false, err + } + + return response.Channel, response.NoOp, response.AlreadyOpen, response.Err() +} + +// 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. +// 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 { + Channel *Channel `json:"channel"` + Warning string `json:"warning"` + ResponseMetaData *struct { + Warnings []string `json:"warnings"` + } `json:"response_metadata"` + SlackResponse + }{} + + err := api.postMethod(ctx, "conversations.join", values, &response) + if err != nil { + return nil, "", nil, err + } + if response.Err() != nil { + return nil, "", nil, response.Err() + } + var warnings []string + if response.ResponseMetaData != nil { + warnings = response.ResponseMetaData.Warnings + } + return response.Channel, response.Warning, warnings, nil +} + +type GetConversationHistoryParameters struct { + ChannelID string + Cursor string + Inclusive bool + Latest string + Limit int + Oldest string + IncludeAllMetadata bool +} + +type GetConversationHistoryResponse struct { + SlackResponse + HasMore bool `json:"has_more"` + PinCount int `json:"pin_count"` + Latest string `json:"latest"` + ResponseMetaData struct { + NextCursor string `json:"next_cursor"` + } `json:"response_metadata"` + Messages []Message `json:"messages"` +} + +// GetConversationHistory joins an existing 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. +// 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 != "" { + values.Add("cursor", params.Cursor) + } + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + if params.Latest != "" { + values.Add("latest", params.Latest) + } + if params.Limit != 0 { + values.Add("limit", strconv.Itoa(params.Limit)) + } + 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{} + + err := api.postMethod(ctx, "conversations.history", values, &response) + if err != nil { + return nil, err + } + + 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() +} + +// CreateChannelCanvas creates a new canvas in a channel. +// For more details, see CreateChannelCanvasContext documentation. +func (api *Client) CreateChannelCanvas(channel string, documentContent DocumentContent) (string, error) { + return api.CreateChannelCanvasContext(context.Background(), channel, documentContent) +} + +// CreateChannelCanvasContext creates a new canvas in a channel with a custom context. +// Slack API docs: https://api.slack.com/methods/conversations.canvases.create +func (api *Client) CreateChannelCanvasContext(ctx context.Context, channel string, documentContent DocumentContent) (string, error) { + values := url.Values{ + "token": {api.token}, + "channel_id": {channel}, + } + if documentContent.Type != "" { + documentContentJSON, err := json.Marshal(documentContent) + if err != nil { + return "", err + } + values.Add("document_content", string(documentContentJSON)) + } + + response := struct { + SlackResponse + CanvasID string `json:"canvas_id"` + }{} + err := api.postMethod(ctx, "conversations.canvases.create", values, &response) + if err != nil { + return "", err + } + + return response.CanvasID, response.Err() +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/dialog.go b/components/reporters/slack/vendor/github.com/slack-go/slack/dialog.go new file mode 100644 index 000000000..f94113f4d --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/dialog.go @@ -0,0 +1,120 @@ +package slack + +import ( + "context" + "encoding/json" + "strings" +) + +// InputType is the type of the dialog input type +type InputType string + +const ( + // InputTypeText textfield input + InputTypeText InputType = "text" + // InputTypeTextArea textarea input + InputTypeTextArea InputType = "textarea" + // InputTypeSelect select menus input + InputTypeSelect InputType = "select" +) + +// DialogInput for dialogs input type text or menu +type DialogInput struct { + Type InputType `json:"type"` + Label string `json:"label"` + Name string `json:"name"` + Placeholder string `json:"placeholder"` + Optional bool `json:"optional"` + Hint string `json:"hint"` +} + +// DialogTrigger ... +type DialogTrigger struct { + TriggerID string `json:"trigger_id"` //Required. Must respond within 3 seconds. + Dialog Dialog `json:"dialog"` //Required. +} + +// Dialog as in Slack dialogs +// https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes +type Dialog struct { + TriggerID string `json:"trigger_id"` // Required + CallbackID string `json:"callback_id"` // Required + State string `json:"state,omitempty"` // Optional + Title string `json:"title"` + SubmitLabel string `json:"submit_label,omitempty"` + NotifyOnCancel bool `json:"notify_on_cancel"` + Elements []DialogElement `json:"elements"` +} + +// DialogElement abstract type for dialogs. +type DialogElement interface{} + +// DialogCallback DEPRECATED use InteractionCallback +type DialogCallback InteractionCallback + +// DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog +type DialogSubmissionCallback struct { + // 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"` +} + +// DialogOpenResponse response from `dialog.open` +type DialogOpenResponse struct { + SlackResponse + DialogResponseMetadata DialogResponseMetadata `json:"response_metadata"` +} + +// DialogResponseMetadata lists the error messages +type DialogResponseMetadata struct { + Messages []string `json:"messages"` +} + +// DialogInputValidationError is an error when user inputs incorrect value to form from within a dialog +type DialogInputValidationError struct { + Name string `json:"name"` + Error string `json:"error"` +} + +// DialogInputValidationErrors lists the name of field and that error messages +type DialogInputValidationErrors struct { + Errors []DialogInputValidationError `json:"errors"` +} + +// OpenDialog opens a dialog window where the triggerID originated from. +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) { + return api.OpenDialogContext(context.Background(), triggerID, dialog) +} + +// OpenDialogContext opens a dialog window where the triggerId originated from with a custom context +// EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. +func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) { + if triggerID == "" { + return ErrParametersMissing + } + + req := DialogTrigger{ + TriggerID: triggerID, + Dialog: dialog, + } + + encoded, err := json.Marshal(req) + if err != nil { + return err + } + + response := &DialogOpenResponse{} + endpoint := api.endpoint + "dialog.open" + if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil { + return err + } + + if len(response.DialogResponseMetadata.Messages) > 0 { + response.Ok = false + response.Error += "\n" + strings.Join(response.DialogResponseMetadata.Messages, "\n") + } + + return response.Err() +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/dialog_select.go b/components/reporters/slack/vendor/github.com/slack-go/slack/dialog_select.go new file mode 100644 index 000000000..3d6be989e --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/dialog_select.go @@ -0,0 +1,115 @@ +package slack + +// SelectDataSource types of select datasource +type SelectDataSource string + +const ( + // DialogDataSourceStatic menu with static Options/OptionGroups + DialogDataSourceStatic SelectDataSource = "static" + // DialogDataSourceExternal dynamic datasource + DialogDataSourceExternal SelectDataSource = "external" + // DialogDataSourceConversations provides a list of conversations + DialogDataSourceConversations SelectDataSource = "conversations" + // DialogDataSourceChannels provides a list of channels + DialogDataSourceChannels SelectDataSource = "channels" + // DialogDataSourceUsers provides a list of users + DialogDataSourceUsers SelectDataSource = "users" +) + +// DialogInputSelect dialog support for select boxes. +type DialogInputSelect struct { + DialogInput + Value string `json:"value,omitempty"` //Optional. + DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". + SelectedOptions []DialogSelectOption `json:"selected_options,omitempty"` //Optional. May hold at most one element, for use with "external" only. + Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required. + OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options. + MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent. + Hint string `json:"hint,omitempty"` //Optional. Additional hint text. +} + +// DialogSelectOption is an option for the user to select from the menu +type DialogSelectOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// DialogOptionGroup is a collection of options for creating a segmented table +type DialogOptionGroup struct { + Label string `json:"label"` + Options []DialogSelectOption `json:"options"` +} + +// NewStaticSelectDialogInput constructor for a `static` datasource menu input +func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + Optional: true, + }, + DataSource: DialogDataSourceStatic, + Options: options, + } +} + +// 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{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Name: name, + Label: label, + }, + DataSource: DialogDataSourceStatic, + OptionGroups: options} +} + +// NewDialogOptionGroup creates a DialogOptionGroup from several select options +func NewDialogOptionGroup(label string, options ...DialogSelectOption) DialogOptionGroup { + return DialogOptionGroup{ + Label: label, + Options: options, + } +} + +// NewConversationsSelect returns a `Conversations` select +func NewConversationsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceConversations) +} + +// NewChannelsSelect returns a `Channels` select +func NewChannelsSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceChannels) +} + +// NewUsersSelect returns a `Users` select +func NewUsersSelect(name, label string) *DialogInputSelect { + return newPresetSelect(name, label, DialogDataSourceUsers) +} + +func newPresetSelect(name, label string, dataSourceType SelectDataSource) *DialogInputSelect { + return &DialogInputSelect{ + DialogInput: DialogInput{ + Type: InputTypeSelect, + Label: label, + Name: name, + }, + DataSource: dataSourceType, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/dialog_text.go b/components/reporters/slack/vendor/github.com/slack-go/slack/dialog_text.go new file mode 100644 index 000000000..25fa1b693 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/dialog_text.go @@ -0,0 +1,59 @@ +package slack + +// TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype. +type TextInputSubtype string + +// TextInputOption handle to extra inputs options. +type TextInputOption func(*TextInputElement) + +const ( + // InputSubtypeEmail email keyboard + InputSubtypeEmail TextInputSubtype = "email" + // InputSubtypeNumber numeric keyboard + InputSubtypeNumber TextInputSubtype = "number" + // InputSubtypeTel Phone keyboard + InputSubtypeTel TextInputSubtype = "tel" + // InputSubtypeURL Phone keyboard + InputSubtypeURL TextInputSubtype = "url" +) + +// TextInputElement subtype of DialogInput +// https://api.slack.com/dialogs#option_element_attributes#text_element_attributes +type TextInputElement struct { + DialogInput + MaxLength int `json:"max_length,omitempty"` + MinLength int `json:"min_length,omitempty"` + Hint string `json:"hint,omitempty"` + Subtype TextInputSubtype `json:"subtype"` + Value string `json:"value"` +} + +// NewTextInput constructor for a `text` input +func NewTextInput(name, label, text string, options ...TextInputOption) *TextInputElement { + t := &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeText, + Name: name, + Label: label, + }, + Value: text, + } + + for _, opt := range options { + opt(t) + } + + return t +} + +// NewTextAreaInput constructor for a `textarea` input +func NewTextAreaInput(name, label, text string) *TextInputElement { + return &TextInputElement{ + DialogInput: DialogInput{ + Type: InputTypeTextArea, + Name: name, + Label: label, + }, + Value: text, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/dnd.go b/components/reporters/slack/vendor/github.com/slack-go/slack/dnd.go new file mode 100644 index 000000000..81eaf5024 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/dnd.go @@ -0,0 +1,160 @@ +package slack + +import ( + "context" + "net/url" + "strconv" + "strings" +) + +type SnoozeDebug struct { + SnoozeEndDate string `json:"snooze_end_date"` +} + +type SnoozeInfo struct { + SnoozeEnabled bool `json:"snooze_enabled,omitempty"` + SnoozeEndTime int `json:"snooze_endtime,omitempty"` + SnoozeRemaining int `json:"snooze_remaining,omitempty"` + SnoozeDebug SnoozeDebug `json:"snooze_debug,omitempty"` +} + +type DNDStatus struct { + Enabled bool `json:"dnd_enabled"` + NextStartTimestamp int `json:"next_dnd_start_ts"` + NextEndTimestamp int `json:"next_dnd_end_ts"` + SnoozeInfo +} + +type dndResponseFull struct { + DNDStatus + SlackResponse +} + +type dndTeamInfoResponse struct { + Users map[string]DNDStatus `json:"users"` + SlackResponse +} + +func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) { + response := &dndResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// 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. +// Slack API docs: https://api.slack.com/methods/dnd.endDnd +func (api *Client) EndDNDContext(ctx context.Context) error { + values := url.Values{ + "token": {api.token}, + } + + response := &SlackResponse{} + + if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil { + return err + } + + return response.Err() +} + +// 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. +// Slack API docs: https://api.slack.com/methods/dnd.endSnooze +func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + } + + response, err := api.dndRequest(ctx, "dnd.endSnooze", values) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} + +// GetDNDInfo provides information about a user's current Do Not Disturb settings. +// For more information see the GetDNDInfoContext documentation. +func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { + return api.GetDNDInfoContext(context.Background(), user) +} + +// GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context. +// Slack API docs: https://api.slack.com/methods/dnd.info +func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + } + if user != nil { + values.Set("user", *user) + } + + response, err := api.dndRequest(ctx, "dnd.info", values) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} + +// GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. +// For more information see the GetDNDTeamInfoContext documentation. +func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { + return api.GetDNDTeamInfoContext(context.Background(), users) +} + +// GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context. +// Slack API docs: https://api.slack.com/methods/dnd.teamInfo +func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + "users": {strings.Join(users, ",")}, + } + response := &dndTeamInfoResponse{} + + if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil { + return nil, err + } + + if response.Err() != nil { + return nil, response.Err() + } + + return response.Users, nil +} + +// 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. +// If a snooze session is not already active for the user, invoking this method will +// begin one for the specified duration. +// Slack API docs: https://api.slack.com/methods/dnd.setSnooze +func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { + values := url.Values{ + "token": {api.token}, + "num_minutes": {strconv.Itoa(minutes)}, + } + + response, err := api.dndRequest(ctx, "dnd.setSnooze", values) + if err != nil { + return nil, err + } + return &response.DNDStatus, nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/emoji.go b/components/reporters/slack/vendor/github.com/slack-go/slack/emoji.go new file mode 100644 index 000000000..139df0fd2 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/emoji.go @@ -0,0 +1,37 @@ +package slack + +import ( + "context" + "net/url" +) + +type emojiResponseFull struct { + Emoji map[string]string `json:"emoji"` + SlackResponse +} + +// 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. +// 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}, + } + response := &emojiResponseFull{} + + err := api.postMethod(ctx, "emoji.list", values, response) + if err != nil { + return nil, err + } + + if response.Err() != nil { + return nil, response.Err() + } + + return response.Emoji, nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/errors.go b/components/reporters/slack/vendor/github.com/slack-go/slack/errors.go new file mode 100644 index 000000000..8be22a659 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/errors.go @@ -0,0 +1,21 @@ +package slack + +import "github.com/slack-go/slack/internal/errorsx" + +// Errors returned by various methods. +const ( + ErrAlreadyDisconnected = errorsx.String("Invalid call to Disconnect - Slack API is already disconnected") + ErrRTMDisconnected = errorsx.String("disconnect received while trying to connect") + 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") +) + +// internal errors +const ( + errPaginationComplete = errorsx.String("pagination complete") +) diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/files.go b/components/reporters/slack/vendor/github.com/slack-go/slack/files.go new file mode 100644 index 000000000..810c476b7 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/files.go @@ -0,0 +1,671 @@ +package slack + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/url" + "strconv" + "strings" +) + +const ( + // 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 +type File 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"` + ImageExifRotation int `json:"image_exif_rotation"` + Filetype string `json:"filetype"` + PrettyType string `json:"pretty_type"` + User string `json:"user"` + + Mode string `json:"mode"` + Editable bool `json:"editable"` + IsExternal bool `json:"is_external"` + ExternalType string `json:"external_type"` + + Size int `json:"size"` + + URL string `json:"url"` // Deprecated - never set + URLDownload string `json:"url_download"` // Deprecated - never set + URLPrivate string `json:"url_private"` + URLPrivateDownload string `json:"url_private_download"` + + OriginalH int `json:"original_h"` + OriginalW int `json:"original_w"` + Thumb64 string `json:"thumb_64"` + Thumb80 string `json:"thumb_80"` + Thumb160 string `json:"thumb_160"` + Thumb360 string `json:"thumb_360"` + Thumb360Gif string `json:"thumb_360_gif"` + Thumb360W int `json:"thumb_360_w"` + Thumb360H int `json:"thumb_360_h"` + Thumb480 string `json:"thumb_480"` + Thumb480W int `json:"thumb_480_w"` + Thumb480H int `json:"thumb_480_h"` + Thumb720 string `json:"thumb_720"` + Thumb720W int `json:"thumb_720_w"` + Thumb720H int `json:"thumb_720_h"` + Thumb960 string `json:"thumb_960"` + Thumb960W int `json:"thumb_960_w"` + Thumb960H int `json:"thumb_960_h"` + Thumb1024 string `json:"thumb_1024"` + Thumb1024W int `json:"thumb_1024_w"` + Thumb1024H int `json:"thumb_1024_h"` + + Permalink string `json:"permalink"` + PermalinkPublic string `json:"permalink_public"` + + EditLink string `json:"edit_link"` + Preview string `json:"preview"` + PreviewHighlight string `json:"preview_highlight"` + Lines int `json:"lines"` + LinesMore int `json:"lines_more"` + + IsPublic bool `json:"is_public"` + PublicURLShared bool `json:"public_url_shared"` + Channels []string `json:"channels"` + Groups []string `json:"groups"` + IMs []string `json:"ims"` + InitialComment Comment `json:"initial_comment"` + CommentsCount int `json:"comments_count"` + 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"` +} + +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 { + Public map[string][]ShareFileInfo `json:"public"` + Private map[string][]ShareFileInfo `json:"private"` +} + +type ShareFileInfo struct { + ReplyUsers []string `json:"reply_users"` + ReplyUsersCount int `json:"reply_users_count"` + ReplyCount int `json:"reply_count"` + Ts string `json:"ts"` + ThreadTs string `json:"thread_ts"` + LatestReply string `json:"latest_reply"` + ChannelName string `json:"channel_name"` + 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 +type ListFilesParameters struct { + Limit int + User string + Channel string + TeamID string + Types string + Cursor string +} + +type UploadFileV2Parameters 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"` + Comments []Comment `json:"comments"` + Files []File `json:"files"` + Metadata ResponseMetadata `json:"response_metadata"` + + SlackResponse +} + +// NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set +func NewGetFilesParameters() GetFilesParameters { + return GetFilesParameters{ + User: DEFAULT_FILES_USER, + Channel: DEFAULT_FILES_CHANNEL, + TimestampFrom: DEFAULT_FILES_TS_FROM, + TimestampTo: DEFAULT_FILES_TS_TO, + Types: DEFAULT_FILES_TYPES, + Count: DEFAULT_FILES_COUNT, + Page: DEFAULT_FILES_PAGE, + ShowHidden: DEFAULT_FILES_SHOW_HIDDEN, + } +} + +func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) { + response := &fileResponseFull{} + err := api.postMethod(ctx, path, values, response) + if err != nil { + return nil, err + } + + return response, response.Err() +} + +// 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. +// 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}, + "file": {fileID}, + "count": {strconv.Itoa(count)}, + "page": {strconv.Itoa(page)}, + } + + response, err := api.fileRequest(ctx, "files.info", values) + if err != nil { + return nil, nil, nil, err + } + return &response.File, response.Comments, &response.Paging, nil +} + +// GetFile retrieves a given file from its private download URL. +func (api *Client) GetFile(downloadURL string, writer io.Writer) error { + return api.GetFileContext(context.Background(), downloadURL, writer) +} + +// 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) +} + +// 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. +// 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}, + } + if params.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + 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)) + } + if params.TimestampTo != DEFAULT_FILES_TS_TO { + values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) + } + if params.Types != DEFAULT_FILES_TYPES { + values.Add("types", params.Types) + } + if params.Count != DEFAULT_FILES_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_FILES_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + 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 { + return nil, nil, err + } + return response.Files, &response.Paging, nil +} + +// 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) +} + +// 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.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + if params.Channel != DEFAULT_FILES_CHANNEL { + values.Add("channel", params.Channel) + } + if params.TeamID != "" { + values.Add("team_id", params.TeamID) + } + 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 +} + +// UploadFile uploads a file. +// +// Deprecated: Use [Client.UploadFileV2] instead. +// +// Per Slack Changelog, specifically [https://api.slack.com/changelog#entry-march_2025_1](this entry), +// this will stop functioning on November 12, 2025. +// +// For more details, see: https://api.slack.com/methods/files.upload#markdown +func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { + return api.UploadFileContext(context.Background(), params) +} + +// UploadFileContext uploads a file and setting a custom context. +// +// Deprecated: Use [Client.UploadFileV2Context] instead. +// +// Per Slack Changelog, specifically [https://api.slack.com/changelog#entry-march_2025_1](this entry), +// this will stop functioning on November 12, 2025. +// +// For more details, see: https://api.slack.com/methods/files.upload#markdown +func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) { + // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More + // investigation needed, but for now this will do. + _, err = api.AuthTestContext(ctx) + if err != nil { + return nil, err + } + response := &fileResponseFull{} + values := url.Values{} + if params.Filetype != "" { + values.Add("filetype", params.Filetype) + } + if params.Filename != "" { + values.Add("filename", params.Filename) + } + if params.Title != "" { + values.Add("title", params.Title) + } + if params.InitialComment != "" { + values.Add("initial_comment", params.InitialComment) + } + if params.ThreadTimestamp != "" { + values.Add("thread_ts", params.ThreadTimestamp) + } + if len(params.Channels) != 0 { + values.Add("channels", strings.Join(params.Channels, ",")) + } + if params.Content != "" { + values.Add("content", params.Content) + values.Add("token", api.token) + err = api.postMethod(ctx, "files.upload", values, response) + } else if params.File != "" { + err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", api.token, values, response, api) + } else if params.Reader != nil { + if params.Filename == "" { + return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader") + } + err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", api.token, values, params.Reader, response, api) + } + + if err != nil { + return nil, err + } + + return &response.File, response.Err() +} + +// DeleteFileComment deletes a file's comment. +// For more details, see DeleteFileCommentContext documentation. +func (api *Client) DeleteFileComment(commentID, fileID string) error { + return api.DeleteFileCommentContext(context.Background(), fileID, commentID) +} + +// 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 + } + + values := url.Values{ + "token": {api.token}, + "file": {fileID}, + "id": {commentID}, + } + _, err = api.fileRequest(ctx, "files.comments.delete", values) + return err +} + +// 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. +// 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}, + "file": {fileID}, + } + + _, err = api.fileRequest(ctx, "files.delete", values) + return err +} + +// 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. +// 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}, + "file": {fileID}, + } + + response, err := api.fileRequest(ctx, "files.revokePublicURL", values) + if err != nil { + return nil, err + } + return &response.File, nil +} + +// 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. +// 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}, + "file": {fileID}, + } + + response, err := api.fileRequest(ctx, "files.sharedPublicURL", values) + if err != nil { + return nil, nil, nil, err + } + 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 +} + +// UploadFileV2 uploads file to a given slack channel using 3 steps. +// For more details, see UploadFileV2Context documentation. +func (api *Client) UploadFileV2(params UploadFileV2Parameters) (*FileSummary, error) { + return api.UploadFileV2Context(context.Background(), params) +} + +// UploadFileV2Context uploads file to a given slack channel using 3 steps - +// 1. Get an upload URL using files.getUploadURLExternal API +// 2. Send the file as a post to the URL provided by slack +// 3. Complete the upload and share it to the specified channel using files.completeUploadExternal +// +// Slack Docs: https://api.slack.com/messaging/files#uploading_files +func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2Parameters) (file *FileSummary, err error) { + 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, 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, 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, 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/components/reporters/slack/vendor/github.com/slack-go/slack/function_execute.go b/components/reporters/slack/vendor/github.com/slack-go/slack/function_execute.go new file mode 100644 index 000000000..4ec8f9f4c --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/function_execute.go @@ -0,0 +1,93 @@ +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) + } + + endpoint := api.endpoint + "functions.completeSuccess" + jsonData, err := json.Marshal(r) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + 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 + + endpoint := api.endpoint + "functions.completeError" + jsonData, err := json.Marshal(r) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + return err + } + + if !response.Ok { + return response.Err() + } + + return nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/groups.go b/components/reporters/slack/vendor/github.com/slack-go/slack/groups.go new file mode 100644 index 000000000..b77f909db --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/groups.go @@ -0,0 +1,7 @@ +package slack + +// Group contains all the information for a group +type Group struct { + GroupConversation + IsGroup bool `json:"is_group"` +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/history.go b/components/reporters/slack/vendor/github.com/slack-go/slack/history.go new file mode 100644 index 000000000..49dfe3540 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/history.go @@ -0,0 +1,37 @@ +package slack + +const ( + DEFAULT_HISTORY_LATEST = "" + DEFAULT_HISTORY_OLDEST = "0" + DEFAULT_HISTORY_COUNT = 100 + DEFAULT_HISTORY_INCLUSIVE = false + DEFAULT_HISTORY_UNREADS = false +) + +// HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs +type HistoryParameters struct { + Latest string + Oldest string + Count int + Inclusive bool + Unreads bool +} + +// History contains message history information needed to navigate a Channel / Group / DM history +type History struct { + Latest string `json:"latest"` + Messages []Message `json:"messages"` + HasMore bool `json:"has_more"` + Unread int `json:"unread_count_display"` +} + +// NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set +func NewHistoryParameters() HistoryParameters { + return HistoryParameters{ + Latest: DEFAULT_HISTORY_LATEST, + Oldest: DEFAULT_HISTORY_OLDEST, + Count: DEFAULT_HISTORY_COUNT, + Inclusive: DEFAULT_HISTORY_INCLUSIVE, + Unreads: DEFAULT_HISTORY_UNREADS, + } +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/im.go b/components/reporters/slack/vendor/github.com/slack-go/slack/im.go new file mode 100644 index 000000000..7c4bc2572 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/im.go @@ -0,0 +1,21 @@ +package slack + +type imChannel struct { + ID string `json:"id"` +} + +type imResponseFull struct { + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + AlreadyOpen bool `json:"already_open"` + Channel imChannel `json:"channel"` + IMs []IM `json:"ims"` + History + SlackResponse +} + +// IM contains information related to the Direct Message channel +type IM struct { + Conversation + IsUserDeleted bool `json:"is_user_deleted"` +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/info.go b/components/reporters/slack/vendor/github.com/slack-go/slack/info.go new file mode 100644 index 000000000..a026ab494 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/info.go @@ -0,0 +1,479 @@ +package slack + +import ( + "bytes" + "context" + "fmt" + "net/url" + "strconv" + "strings" + "time" +) + +type UserPrefsCarrier struct { + SlackResponse + UserPrefs *UserPrefs `json:"prefs"` +} + +// UserPrefs carries a bunch of user settings including some unknown types +type UserPrefs struct { + UserColors string `json:"user_colors,omitempty"` + ColorNamesInList bool `json:"color_names_in_list,omitempty"` + // Keyboard UnknownType `json:"keyboard"` + EmailAlerts string `json:"email_alerts,omitempty"` + EmailAlertsSleepUntil int `json:"email_alerts_sleep_until,omitempty"` + EmailTips bool `json:"email_tips,omitempty"` + EmailWeekly bool `json:"email_weekly,omitempty"` + EmailOffers bool `json:"email_offers,omitempty"` + EmailResearch bool `json:"email_research,omitempty"` + EmailDeveloper bool `json:"email_developer,omitempty"` + WelcomeMessageHidden bool `json:"welcome_message_hidden,omitempty"` + SearchSort string `json:"search_sort,omitempty"` + SearchFileSort string `json:"search_file_sort,omitempty"` + SearchChannelSort string `json:"search_channel_sort,omitempty"` + SearchPeopleSort string `json:"search_people_sort,omitempty"` + ExpandInlineImages bool `json:"expand_inline_images,omitempty"` + ExpandInternalInlineImages bool `json:"expand_internal_inline_images,omitempty"` + ExpandSnippets bool `json:"expand_snippets,omitempty"` + PostsFormattingGuide bool `json:"posts_formatting_guide,omitempty"` + SeenWelcome2 bool `json:"seen_welcome_2,omitempty"` + SeenSSBPrompt bool `json:"seen_ssb_prompt,omitempty"` + SpacesNewXpBannerDismissed bool `json:"spaces_new_xp_banner_dismissed,omitempty"` + SearchOnlyMyChannels bool `json:"search_only_my_channels,omitempty"` + SearchOnlyCurrentTeam bool `json:"search_only_current_team,omitempty"` + SearchHideMyChannels bool `json:"search_hide_my_channels,omitempty"` + SearchOnlyShowOnline bool `json:"search_only_show_online,omitempty"` + SearchHideDeactivatedUsers bool `json:"search_hide_deactivated_users,omitempty"` + EmojiMode string `json:"emoji_mode,omitempty"` + EmojiUse string `json:"emoji_use,omitempty"` + HasInvited bool `json:"has_invited,omitempty"` + HasUploaded bool `json:"has_uploaded,omitempty"` + HasCreatedChannel bool `json:"has_created_channel,omitempty"` + HasSearched bool `json:"has_searched,omitempty"` + SearchExcludeChannels string `json:"search_exclude_channels,omitempty"` + MessagesTheme string `json:"messages_theme,omitempty"` + WebappSpellcheck bool `json:"webapp_spellcheck,omitempty"` + NoJoinedOverlays bool `json:"no_joined_overlays,omitempty"` + NoCreatedOverlays bool `json:"no_created_overlays,omitempty"` + DropboxEnabled bool `json:"dropbox_enabled,omitempty"` + SeenDomainInviteReminder bool `json:"seen_domain_invite_reminder,omitempty"` + SeenMemberInviteReminder bool `json:"seen_member_invite_reminder,omitempty"` + MuteSounds bool `json:"mute_sounds,omitempty"` + ArrowHistory bool `json:"arrow_history,omitempty"` + TabUIReturnSelects bool `json:"tab_ui_return_selects,omitempty"` + ObeyInlineImgLimit bool `json:"obey_inline_img_limit,omitempty"` + RequireAt bool `json:"require_at,omitempty"` + SsbSpaceWindow string `json:"ssb_space_window,omitempty"` + MacSsbBounce string `json:"mac_ssb_bounce,omitempty"` + MacSsbBullet bool `json:"mac_ssb_bullet,omitempty"` + ExpandNonMediaAttachments bool `json:"expand_non_media_attachments,omitempty"` + ShowTyping bool `json:"show_typing,omitempty"` + PagekeysHandled bool `json:"pagekeys_handled,omitempty"` + LastSnippetType string `json:"last_snippet_type,omitempty"` + DisplayRealNamesOverride int `json:"display_real_names_override,omitempty"` + DisplayDisplayNames bool `json:"display_display_names,omitempty"` + Time24 bool `json:"time24,omitempty"` + EnterIsSpecialInTbt bool `json:"enter_is_special_in_tbt,omitempty"` + MsgInputSendBtn bool `json:"msg_input_send_btn,omitempty"` + MsgInputSendBtnAutoSet bool `json:"msg_input_send_btn_auto_set,omitempty"` + MsgInputStickyComposer bool `json:"msg_input_sticky_composer,omitempty"` + GraphicEmoticons bool `json:"graphic_emoticons,omitempty"` + ConvertEmoticons bool `json:"convert_emoticons,omitempty"` + SsEmojis bool `json:"ss_emojis,omitempty"` + SeenOnboardingStart bool `json:"seen_onboarding_start,omitempty"` + OnboardingCancelled bool `json:"onboarding_cancelled,omitempty"` + SeenOnboardingSlackbotConversation bool `json:"seen_onboarding_slackbot_conversation,omitempty"` + SeenOnboardingChannels bool `json:"seen_onboarding_channels,omitempty"` + SeenOnboardingDirectMessages bool `json:"seen_onboarding_direct_messages,omitempty"` + SeenOnboardingInvites bool `json:"seen_onboarding_invites,omitempty"` + SeenOnboardingSearch bool `json:"seen_onboarding_search,omitempty"` + SeenOnboardingRecentMentions bool `json:"seen_onboarding_recent_mentions,omitempty"` + SeenOnboardingStarredItems bool `json:"seen_onboarding_starred_items,omitempty"` + SeenOnboardingPrivateGroups bool `json:"seen_onboarding_private_groups,omitempty"` + SeenOnboardingBanner bool `json:"seen_onboarding_banner,omitempty"` + OnboardingSlackbotConversationStep int `json:"onboarding_slackbot_conversation_step,omitempty"` + SetTzAutomatically bool `json:"set_tz_automatically,omitempty"` + SuppressLinkWarning bool `json:"suppress_link_warning,omitempty"` + DndEnabled bool `json:"dnd_enabled,omitempty"` + DndStartHour string `json:"dnd_start_hour,omitempty"` + DndEndHour string `json:"dnd_end_hour,omitempty"` + DndBeforeMonday string `json:"dnd_before_monday,omitempty"` + DndAfterMonday string `json:"dnd_after_monday,omitempty"` + DndEnabledMonday string `json:"dnd_enabled_monday,omitempty"` + DndBeforeTuesday string `json:"dnd_before_tuesday,omitempty"` + DndAfterTuesday string `json:"dnd_after_tuesday,omitempty"` + DndEnabledTuesday string `json:"dnd_enabled_tuesday,omitempty"` + DndBeforeWednesday string `json:"dnd_before_wednesday,omitempty"` + DndAfterWednesday string `json:"dnd_after_wednesday,omitempty"` + DndEnabledWednesday string `json:"dnd_enabled_wednesday,omitempty"` + DndBeforeThursday string `json:"dnd_before_thursday,omitempty"` + DndAfterThursday string `json:"dnd_after_thursday,omitempty"` + DndEnabledThursday string `json:"dnd_enabled_thursday,omitempty"` + DndBeforeFriday string `json:"dnd_before_friday,omitempty"` + DndAfterFriday string `json:"dnd_after_friday,omitempty"` + DndEnabledFriday string `json:"dnd_enabled_friday,omitempty"` + DndBeforeSaturday string `json:"dnd_before_saturday,omitempty"` + DndAfterSaturday string `json:"dnd_after_saturday,omitempty"` + DndEnabledSaturday string `json:"dnd_enabled_saturday,omitempty"` + DndBeforeSunday string `json:"dnd_before_sunday,omitempty"` + DndAfterSunday string `json:"dnd_after_sunday,omitempty"` + DndEnabledSunday string `json:"dnd_enabled_sunday,omitempty"` + DndDays string `json:"dnd_days,omitempty"` + DndCustomNewBadgeSeen bool `json:"dnd_custom_new_badge_seen,omitempty"` + DndNotificationScheduleNewBadgeSeen bool `json:"dnd_notification_schedule_new_badge_seen,omitempty"` + // UnreadCollapsedChannels unknownType `json:"unread_collapsed_channels,omitempty"` + SidebarBehavior string `json:"sidebar_behavior,omitempty"` + ChannelSort string `json:"channel_sort,omitempty"` + SeparatePrivateChannels bool `json:"separate_private_channels,omitempty"` + SeparateSharedChannels bool `json:"separate_shared_channels,omitempty"` + SidebarTheme string `json:"sidebar_theme,omitempty"` + SidebarThemeCustomValues string `json:"sidebar_theme_custom_values,omitempty"` + NoInvitesWidgetInSidebar bool `json:"no_invites_widget_in_sidebar,omitempty"` + NoOmniboxInChannels bool `json:"no_omnibox_in_channels,omitempty"` + + KKeyOmniboxAutoHideCount int `json:"k_key_omnibox_auto_hide_count,omitempty"` + ShowSidebarQuickswitcherButton bool `json:"show_sidebar_quickswitcher_button,omitempty"` + EntOrgWideChannelsSidebar bool `json:"ent_org_wide_channels_sidebar,omitempty"` + MarkMsgsReadImmediately bool `json:"mark_msgs_read_immediately,omitempty"` + StartScrollAtOldest bool `json:"start_scroll_at_oldest,omitempty"` + SnippetEditorWrapLongLines bool `json:"snippet_editor_wrap_long_lines,omitempty"` + LsDisabled bool `json:"ls_disabled,omitempty"` + FKeySearch bool `json:"f_key_search,omitempty"` + KKeyOmnibox bool `json:"k_key_omnibox,omitempty"` + PromptedForEmailDisabling bool `json:"prompted_for_email_disabling,omitempty"` + NoMacelectronBanner bool `json:"no_macelectron_banner,omitempty"` + NoMacssb1Banner bool `json:"no_macssb1_banner,omitempty"` + NoMacssb2Banner bool `json:"no_macssb2_banner,omitempty"` + NoWinssb1Banner bool `json:"no_winssb1_banner,omitempty"` + HideUserGroupInfoPane bool `json:"hide_user_group_info_pane,omitempty"` + MentionsExcludeAtUserGroups bool `json:"mentions_exclude_at_user_groups,omitempty"` + MentionsExcludeReactions bool `json:"mentions_exclude_reactions,omitempty"` + PrivacyPolicySeen bool `json:"privacy_policy_seen,omitempty"` + EnterpriseMigrationSeen bool `json:"enterprise_migration_seen,omitempty"` + LastTosAcknowledged string `json:"last_tos_acknowledged,omitempty"` + SearchExcludeBots bool `json:"search_exclude_bots,omitempty"` + LoadLato2 bool `json:"load_lato_2,omitempty"` + FullerTimestamps bool `json:"fuller_timestamps,omitempty"` + LastSeenAtChannelWarning int `json:"last_seen_at_channel_warning,omitempty"` + EmojiAutocompleteBig bool `json:"emoji_autocomplete_big,omitempty"` + TwoFactorAuthEnabled bool `json:"two_factor_auth_enabled,omitempty"` + // TwoFactorType unknownType `json:"two_factor_type,omitempty"` + // TwoFactorBackupType unknownType `json:"two_factor_backup_type,omitempty"` + HideHexSwatch bool `json:"hide_hex_swatch,omitempty"` + ShowJumperScores bool `json:"show_jumper_scores,omitempty"` + EnterpriseMdmCustomMsg string `json:"enterprise_mdm_custom_msg,omitempty"` + // EnterpriseExcludedAppTeams unknownType `json:"enterprise_excluded_app_teams,omitempty"` + ClientLogsPri string `json:"client_logs_pri,omitempty"` + FlannelServerPool string `json:"flannel_server_pool,omitempty"` + MentionsExcludeAtChannels bool `json:"mentions_exclude_at_channels,omitempty"` + ConfirmClearAllUnreads bool `json:"confirm_clear_all_unreads,omitempty"` + ConfirmUserMarkedAway bool `json:"confirm_user_marked_away,omitempty"` + BoxEnabled bool `json:"box_enabled,omitempty"` + SeenSingleEmojiMsg bool `json:"seen_single_emoji_msg,omitempty"` + ConfirmShCallStart bool `json:"confirm_sh_call_start,omitempty"` + PreferredSkinTone string `json:"preferred_skin_tone,omitempty"` + ShowAllSkinTones bool `json:"show_all_skin_tones,omitempty"` + WhatsNewRead int `json:"whats_new_read,omitempty"` + // FrecencyJumper unknownType `json:"frecency_jumper,omitempty"` + FrecencyEntJumper string `json:"frecency_ent_jumper,omitempty"` + FrecencyEntJumperBackup string `json:"frecency_ent_jumper_backup,omitempty"` + Jumbomoji bool `json:"jumbomoji,omitempty"` + NewxpSeenLastMessage int `json:"newxp_seen_last_message,omitempty"` + ShowMemoryInstrument bool `json:"show_memory_instrument,omitempty"` + EnableUnreadView bool `json:"enable_unread_view,omitempty"` + SeenUnreadViewCoachmark bool `json:"seen_unread_view_coachmark,omitempty"` + EnableReactEmojiPicker bool `json:"enable_react_emoji_picker,omitempty"` + SeenCustomStatusBadge bool `json:"seen_custom_status_badge,omitempty"` + SeenCustomStatusCallout bool `json:"seen_custom_status_callout,omitempty"` + SeenCustomStatusExpirationBadge bool `json:"seen_custom_status_expiration_badge,omitempty"` + UsedCustomStatusKbShortcut bool `json:"used_custom_status_kb_shortcut,omitempty"` + SeenGuestAdminSlackbotAnnouncement bool `json:"seen_guest_admin_slackbot_announcement,omitempty"` + SeenThreadsNotificationBanner bool `json:"seen_threads_notification_banner,omitempty"` + SeenNameTaggingCoachmark bool `json:"seen_name_tagging_coachmark,omitempty"` + AllUnreadsSortOrder string `json:"all_unreads_sort_order,omitempty"` + Locale string `json:"locale,omitempty"` + SeenIntlChannelNamesCoachmark bool `json:"seen_intl_channel_names_coachmark,omitempty"` + SeenP2LocaleChangeMessage int `json:"seen_p2_locale_change_message,omitempty"` + SeenLocaleChangeMessage int `json:"seen_locale_change_message,omitempty"` + SeenJapaneseLocaleChangeMessage bool `json:"seen_japanese_locale_change_message,omitempty"` + SeenSharedChannelsCoachmark bool `json:"seen_shared_channels_coachmark,omitempty"` + SeenSharedChannelsOptInChangeMessage bool `json:"seen_shared_channels_opt_in_change_message,omitempty"` + HasRecentlySharedaChannel bool `json:"has_recently_shared_a_channel,omitempty"` + SeenChannelBrowserAdminCoachmark bool `json:"seen_channel_browser_admin_coachmark,omitempty"` + SeenAdministrationMenu bool `json:"seen_administration_menu,omitempty"` + SeenDraftsSectionCoachmark bool `json:"seen_drafts_section_coachmark,omitempty"` + SeenEmojiUpdateOverlayCoachmark bool `json:"seen_emoji_update_overlay_coachmark,omitempty"` + SeenSonicDeluxeToast int `json:"seen_sonic_deluxe_toast,omitempty"` + SeenWysiwygDeluxeToast bool `json:"seen_wysiwyg_deluxe_toast,omitempty"` + SeenMarkdownPasteToast int `json:"seen_markdown_paste_toast,omitempty"` + SeenMarkdownPasteShortcut int `json:"seen_markdown_paste_shortcut,omitempty"` + SeenIaEducation bool `json:"seen_ia_education,omitempty"` + PlainTextMode bool `json:"plain_text_mode,omitempty"` + ShowSharedChannelsEducationBanner bool `json:"show_shared_channels_education_banner,omitempty"` + AllowCallsToSetCurrentStatus bool `json:"allow_calls_to_set_current_status,omitempty"` + InInteractiveMasMigrationFlow bool `json:"in_interactive_mas_migration_flow,omitempty"` + SunsetInteractiveMessageViews int `json:"sunset_interactive_message_views,omitempty"` + ShdepPromoCodeSubmitted bool `json:"shdep_promo_code_submitted,omitempty"` + SeenShdepSlackbotMessage bool `json:"seen_shdep_slackbot_message,omitempty"` + SeenCallsInteractiveCoachmark bool `json:"seen_calls_interactive_coachmark,omitempty"` + AllowCmdTabIss bool `json:"allow_cmd_tab_iss,omitempty"` + SeenWorkflowBuilderDeluxeToast bool `json:"seen_workflow_builder_deluxe_toast,omitempty"` + WorkflowBuilderIntroModalClickedThrough bool `json:"workflow_builder_intro_modal_clicked_through,omitempty"` + // WorkflowBuilderCoachmarks unknownType `json:"workflow_builder_coachmarks,omitempty"` + SeenGdriveCoachmark bool `json:"seen_gdrive_coachmark,omitempty"` + OverloadedMessageEnabled bool `json:"overloaded_message_enabled,omitempty"` + SeenHighlightsCoachmark bool `json:"seen_highlights_coachmark,omitempty"` + SeenHighlightsArrowsCoachmark bool `json:"seen_highlights_arrows_coachmark,omitempty"` + SeenHighlightsWarmWelcome bool `json:"seen_highlights_warm_welcome,omitempty"` + SeenNewSearchUi bool `json:"seen_new_search_ui,omitempty"` + SeenChannelSearch bool `json:"seen_channel_search,omitempty"` + SeenPeopleSearch bool `json:"seen_people_search,omitempty"` + SeenPeopleSearchCount int `json:"seen_people_search_count,omitempty"` + DismissedScrollSearchTooltipCount int `json:"dismissed_scroll_search_tooltip_count,omitempty"` + LastDismissedScrollSearchTooltipTimestamp int `json:"last_dismissed_scroll_search_tooltip_timestamp,omitempty"` + HasUsedQuickswitcherShortcut bool `json:"has_used_quickswitcher_shortcut,omitempty"` + SeenQuickswitcherShortcutTipCount int `json:"seen_quickswitcher_shortcut_tip_count,omitempty"` + BrowsersDismissedChannelsLowResultsEducation bool `json:"browsers_dismissed_channels_low_results_education,omitempty"` + BrowsersSeenInitialChannelsEducation bool `json:"browsers_seen_initial_channels_education,omitempty"` + BrowsersDismissedPeopleLowResultsEducation bool `json:"browsers_dismissed_people_low_results_education,omitempty"` + BrowsersSeenInitialPeopleEducation bool `json:"browsers_seen_initial_people_education,omitempty"` + BrowsersDismissedUserGroupsLowResultsEducation bool `json:"browsers_dismissed_user_groups_low_results_education,omitempty"` + BrowsersSeenInitialUserGroupsEducation bool `json:"browsers_seen_initial_user_groups_education,omitempty"` + BrowsersDismissedFilesLowResultsEducation bool `json:"browsers_dismissed_files_low_results_education,omitempty"` + BrowsersSeenInitialFilesEducation bool `json:"browsers_seen_initial_files_education,omitempty"` + A11yAnimations bool `json:"a11y_animations,omitempty"` + SeenKeyboardShortcutsCoachmark bool `json:"seen_keyboard_shortcuts_coachmark,omitempty"` + NeedsInitialPasswordSet bool `json:"needs_initial_password_set,omitempty"` + LessonsEnabled bool `json:"lessons_enabled,omitempty"` + TractorEnabled bool `json:"tractor_enabled,omitempty"` + TractorExperimentGroup string `json:"tractor_experiment_group,omitempty"` + OpenedSlackbotDm bool `json:"opened_slackbot_dm,omitempty"` + NewxpSuggestedChannels string `json:"newxp_suggested_channels,omitempty"` + OnboardingComplete bool `json:"onboarding_complete,omitempty"` + WelcomePlaceState string `json:"welcome_place_state,omitempty"` + // OnboardingRoleApps unknownType `json:"onboarding_role_apps,omitempty"` + HasReceivedThreadedMessage bool `json:"has_received_threaded_message,omitempty"` + SendYourFirstMessageBannerEnabled bool `json:"send_your_first_message_banner_enabled,omitempty"` + WhocanseethisDmMpdmBadge bool `json:"whocanseethis_dm_mpdm_badge,omitempty"` + HighlightWords string `json:"highlight_words,omitempty"` + ThreadsEverything bool `json:"threads_everything,omitempty"` + NoTextInNotifications bool `json:"no_text_in_notifications,omitempty"` + PushShowPreview bool `json:"push_show_preview,omitempty"` + GrowlsEnabled bool `json:"growls_enabled,omitempty"` + AllChannelsLoud bool `json:"all_channels_loud,omitempty"` + PushDmAlert bool `json:"push_dm_alert,omitempty"` + PushMentionAlert bool `json:"push_mention_alert,omitempty"` + PushEverything bool `json:"push_everything,omitempty"` + PushIdleWait int `json:"push_idle_wait,omitempty"` + PushSound string `json:"push_sound,omitempty"` + NewMsgSnd string `json:"new_msg_snd,omitempty"` + PushLoudChannels string `json:"push_loud_channels,omitempty"` + PushMentionChannels string `json:"push_mention_channels,omitempty"` + PushLoudChannelsSet string `json:"push_loud_channels_set,omitempty"` + LoudChannels string `json:"loud_channels,omitempty"` + NeverChannels string `json:"never_channels,omitempty"` + LoudChannelsSet string `json:"loud_channels_set,omitempty"` + AtChannelSuppressedChannels string `json:"at_channel_suppressed_channels,omitempty"` + PushAtChannelSuppressedChannels string `json:"push_at_channel_suppressed_channels,omitempty"` + MutedChannels string `json:"muted_channels,omitempty"` + // AllNotificationsPrefs unknownType `json:"all_notifications_prefs,omitempty"` + GrowthMsgLimitApproachingCtaCount int `json:"growth_msg_limit_approaching_cta_count,omitempty"` + GrowthMsgLimitApproachingCtaTs int `json:"growth_msg_limit_approaching_cta_ts,omitempty"` + GrowthMsgLimitReachedCtaCount int `json:"growth_msg_limit_reached_cta_count,omitempty"` + GrowthMsgLimitReachedCtaLastTs int `json:"growth_msg_limit_reached_cta_last_ts,omitempty"` + GrowthMsgLimitLongReachedCtaCount int `json:"growth_msg_limit_long_reached_cta_count,omitempty"` + GrowthMsgLimitLongReachedCtaLastTs int `json:"growth_msg_limit_long_reached_cta_last_ts,omitempty"` + GrowthMsgLimitSixtyDayBannerCtaCount int `json:"growth_msg_limit_sixty_day_banner_cta_count,omitempty"` + GrowthMsgLimitSixtyDayBannerCtaLastTs int `json:"growth_msg_limit_sixty_day_banner_cta_last_ts,omitempty"` + // GrowthAllBannersPrefs unknownType `json:"growth_all_banners_prefs,omitempty"` + AnalyticsUpsellCoachmarkSeen bool `json:"analytics_upsell_coachmark_seen,omitempty"` + SeenAppSpaceCoachmark bool `json:"seen_app_space_coachmark,omitempty"` + SeenAppSpaceTutorial bool `json:"seen_app_space_tutorial,omitempty"` + DismissedAppLauncherWelcome bool `json:"dismissed_app_launcher_welcome,omitempty"` + DismissedAppLauncherLimit bool `json:"dismissed_app_launcher_limit,omitempty"` + Purchaser bool `json:"purchaser,omitempty"` + ShowEntOnboarding bool `json:"show_ent_onboarding,omitempty"` + FoldersEnabled bool `json:"folders_enabled,omitempty"` + // FolderData unknownType `json:"folder_data,omitempty"` + SeenCorporateExportAlert bool `json:"seen_corporate_export_alert,omitempty"` + ShowAutocompleteHelp int `json:"show_autocomplete_help,omitempty"` + DeprecationToastLastSeen int `json:"deprecation_toast_last_seen,omitempty"` + DeprecationModalLastSeen int `json:"deprecation_modal_last_seen,omitempty"` + Iap1Lab int `json:"iap1_lab,omitempty"` + IaTopNavTheme string `json:"ia_top_nav_theme,omitempty"` + IaPlatformActionsLab int `json:"ia_platform_actions_lab,omitempty"` + ActivityView string `json:"activity_view,omitempty"` + FailoverProxyCheckCompleted int `json:"failover_proxy_check_completed,omitempty"` + EdgeUploadProxyCheckCompleted int `json:"edge_upload_proxy_check_completed,omitempty"` + AppSubdomainCheckCompleted int `json:"app_subdomain_check_completed,omitempty"` + AddAppsPromptDismissed bool `json:"add_apps_prompt_dismissed,omitempty"` + AddChannelPromptDismissed bool `json:"add_channel_prompt_dismissed,omitempty"` + ChannelSidebarHideInvite bool `json:"channel_sidebar_hide_invite,omitempty"` + InProdSurveysEnabled bool `json:"in_prod_surveys_enabled,omitempty"` + DismissedInstalledAppDmSuggestions string `json:"dismissed_installed_app_dm_suggestions,omitempty"` + SeenContextualMessageShortcutsModal bool `json:"seen_contextual_message_shortcuts_modal,omitempty"` + SeenMessageNavigationEducationalToast bool `json:"seen_message_navigation_educational_toast,omitempty"` + ContextualMessageShortcutsModalWasSeen bool `json:"contextual_message_shortcuts_modal_was_seen,omitempty"` + MessageNavigationToastWasSeen bool `json:"message_navigation_toast_was_seen,omitempty"` + UpToBrowseKbShortcut bool `json:"up_to_browse_kb_shortcut,omitempty"` + ChannelSections string `json:"channel_sections,omitempty"` + TZ string `json:"tz,omitempty"` +} + +func (api *Client) GetUserPrefs() (*UserPrefsCarrier, error) { + return api.GetUserPrefsContext(context.Background()) +} + +func (api *Client) GetUserPrefsContext(ctx context.Context) (*UserPrefsCarrier, error) { + response := UserPrefsCarrier{} + + err := api.getMethod(ctx, "users.prefs.get", api.token, url.Values{}, &response) + if err != nil { + return nil, err + } + + return &response, response.Err() +} + +func (api *Client) MuteChat(channelID string) (*UserPrefsCarrier, error) { + prefs, err := api.GetUserPrefs() + if err != nil { + return nil, err + } + chnls := strings.Split(prefs.UserPrefs.MutedChannels, ",") + for _, chn := range chnls { + if chn == channelID { + return nil, nil // noop + } + } + newChnls := prefs.UserPrefs.MutedChannels + "," + channelID + values := url.Values{"token": {api.token}, "muted_channels": {newChnls}, "reason": {"update-muted-channels"}} + response := UserPrefsCarrier{} + + err = api.postMethod(context.Background(), "users.prefs.set", values, &response) + if err != nil { + return nil, err + } + + return &response, response.Err() +} + +func (api *Client) UnMuteChat(channelID string) (*UserPrefsCarrier, error) { + prefs, err := api.GetUserPrefs() + if err != nil { + return nil, err + } + chnls := strings.Split(prefs.UserPrefs.MutedChannels, ",") + newChnls := make([]string, len(chnls)-1) + for i, chn := range chnls { + if chn == channelID { + return nil, nil // noop + } + newChnls[i] = chn + } + values := url.Values{"token": {api.token}, "muted_channels": {strings.Join(newChnls, ",")}, "reason": {"update-muted-channels"}} + response := UserPrefsCarrier{} + + err = api.postMethod(context.Background(), "users.prefs.set", values, &response) + if err != nil { + return nil, err + } + + return &response, response.Err() +} + +// UserDetails contains user details coming in the initial response from StartRTM +type UserDetails struct { + ID string `json:"id"` + Name string `json:"name"` + Created JSONTime `json:"created"` + ManualPresence string `json:"manual_presence"` + Prefs UserPrefs `json:"prefs"` +} + +// JSONTime exists so that we can have a String method converting the date +type JSONTime int64 + +// String converts the unix timestamp into a string +func (t JSONTime) String() string { + tm := t.Time() + return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) +} + +// Time returns a `time.Time` representation of this value. +func (t JSONTime) Time() time.Time { + return time.Unix(int64(t), 0) +} + +// UnmarshalJSON will unmarshal both string and int JSON values +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 + } + + *t = JSONTime(int64(v)) + return nil +} + +// Team contains details about a team +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 +type Icons struct { + 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. +// It is returned by StartRTM or included in the "ConnectedEvent" RTM event. +type Info struct { + URL string `json:"url,omitempty"` + User *UserDetails `json:"self,omitempty"` + Team *Team `json:"team,omitempty"` +} + +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/components/reporters/slack/vendor/github.com/slack-go/slack/interactions.go b/components/reporters/slack/vendor/github.com/slack-go/slack/interactions.go new file mode 100644 index 000000000..f658a72c2 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/interactions.go @@ -0,0 +1,250 @@ +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 + +// action is an interface that should be implemented by all callback action types +type action interface { + actionType() ActionType +} + +// Types of interactions that can be received. +const ( + InteractionTypeDialogCancellation = InteractionType("dialog_cancellation") + InteractionTypeDialogSubmission = InteractionType("dialog_submission") + InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion") + InteractionTypeInteractionMessage = InteractionType("interactive_message") + InteractionTypeMessageAction = InteractionType("message_action") + InteractionTypeBlockActions = InteractionType("block_actions") + InteractionTypeBlockSuggestion = InteractionType("block_suggestion") + 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"` + 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"` + 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 +// the "actions" value in Slack's JSON response, which varies depending on block type +type ActionCallbacks struct { + AttachmentActions []*AttachmentAction + BlockActions []*BlockAction +} + +// MarshalJSON implements the Marshaller interface in order to combine both +// action callback types back into a single array, like how the api responds. +// This makes Marshaling and Unmarshaling an InteractionCallback symmetrical +func (a ActionCallbacks) MarshalJSON() ([]byte, error) { + count := 0 + length := len(a.AttachmentActions) + len(a.BlockActions) + buffer := bytes.NewBufferString("[") + + f := func(obj interface{}) error { + js, err := json.Marshal(obj) + if err != nil { + return err + } + _, err = buffer.Write(js) + if err != nil { + return err + } + + count++ + if count < length { + _, err = buffer.WriteString(",") + return err + } + return nil + } + + for _, act := range a.AttachmentActions { + err := f(act) + if err != nil { + return nil, err + } + } + for _, blk := range a.BlockActions { + err := f(blk) + if err != nil { + return nil, err + } + } + buffer.WriteString("]") + return buffer.Bytes(), nil +} + +// UnmarshalJSON implements the Marshaller interface in order to delegate +// marshalling and allow for proper type assertion when decoding the response +func (a *ActionCallbacks) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + err := json.Unmarshal(data, &raw) + if err != nil { + return err + } + + for _, r := range raw { + var obj map[string]interface{} + err := json.Unmarshal(r, &obj) + if err != nil { + return err + } + + if _, ok := obj["block_id"].(string); ok { + action, err := unmarshalAction(r, &BlockAction{}) + if err != nil { + return err + } + + a.BlockActions = append(a.BlockActions, action.(*BlockAction)) + continue + } + + action, err := unmarshalAction(r, &AttachmentAction{}) + if err != nil { + return err + } + a.AttachmentActions = append(a.AttachmentActions, action.(*AttachmentAction)) + } + + return nil +} + +func unmarshalAction(r json.RawMessage, callbackAction action) (action, error) { + err := json.Unmarshal(r, callbackAction) + if err != nil { + return nil, err + } + return callbackAction, nil +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/internal/backoff/backoff.go b/components/reporters/slack/vendor/github.com/slack-go/slack/internal/backoff/backoff.go new file mode 100644 index 000000000..833e9f2b6 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/internal/backoff/backoff.go @@ -0,0 +1,62 @@ +package backoff + +import ( + "math/rand" + "time" +) + +// This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go + +// Backoff is a time.Duration counter. It starts at Min. After every +// 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 { + attempts int + // Initial value to scale out + Initial time.Duration + // Jitter value randomizes an additional delay between 0 and Jitter + Jitter time.Duration + // Max maximum values of the backoff + Max time.Duration +} + +// Returns the current value of the counter and then multiplies it +// Factor +func (b *Backoff) Duration() (dur time.Duration) { + // Zero-values are nonsensical, so we use + // them to apply defaults + if b.Max == 0 { + b.Max = 10 * time.Second + } + + if b.Initial == 0 { + b.Initial = 100 * time.Millisecond + } + + // calculate this duration + if dur = time.Duration(1 << uint(b.attempts)); dur > 0 { + dur = dur * b.Initial + } else { + dur = b.Max + } + + if b.Jitter > 0 { + dur = dur + time.Duration(rand.Intn(int(b.Jitter))) + } + + // bump attempts count + b.attempts++ + + return dur +} + +// 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/components/reporters/slack/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go b/components/reporters/slack/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go new file mode 100644 index 000000000..0182ec68c --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/internal/errorsx/errorsx.go @@ -0,0 +1,17 @@ +package errorsx + +// String representing an error, useful for declaring string constants as errors. +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/components/reporters/slack/vendor/github.com/slack-go/slack/internal/timex/timex.go b/components/reporters/slack/vendor/github.com/slack-go/slack/internal/timex/timex.go new file mode 100644 index 000000000..40063f738 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/internal/timex/timex.go @@ -0,0 +1,18 @@ +package timex + +import "time" + +// Max returns the maximum duration +func Max(values ...time.Duration) time.Duration { + var ( + max time.Duration + ) + + for _, v := range values { + if v > max { + max = v + } + } + + return max +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/item.go b/components/reporters/slack/vendor/github.com/slack-go/slack/item.go new file mode 100644 index 000000000..89af4eb15 --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/item.go @@ -0,0 +1,75 @@ +package slack + +const ( + TYPE_MESSAGE = "message" + TYPE_FILE = "file" + TYPE_FILE_COMMENT = "file_comment" + TYPE_CHANNEL = "channel" + TYPE_IM = "im" + TYPE_GROUP = "group" +) + +// Item is any type of slack message - message, file, or file comment. +type Item struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + Message *Message `json:"message,omitempty"` + File *File `json:"file,omitempty"` + Comment *Comment `json:"comment,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// NewMessageItem turns a message on a channel into a typed message struct. +func NewMessageItem(ch string, m *Message) Item { + return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m} +} + +// NewFileItem turns a file into a typed file struct. +func NewFileItem(f *File) Item { + return Item{Type: TYPE_FILE, File: f} +} + +// NewFileCommentItem turns a file and comment into a typed file_comment struct. +func NewFileCommentItem(f *File, c *Comment) Item { + return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c} +} + +// NewChannelItem turns a channel id into a typed channel struct. +func NewChannelItem(ch string) Item { + return Item{Type: TYPE_CHANNEL, Channel: ch} +} + +// NewIMItem turns a channel id into a typed im struct. +func NewIMItem(ch string) Item { + return Item{Type: TYPE_IM, Channel: ch} +} + +// NewGroupItem turns a channel id into a typed group struct. +func NewGroupItem(ch string) Item { + return Item{Type: TYPE_GROUP, Channel: ch} +} + +// ItemRef is a reference to a message of any type. One of FileID, +// CommentId, or the combination of ChannelId and Timestamp must be +// specified. +type ItemRef struct { + Channel string `json:"channel"` + Timestamp string `json:"timestamp"` + File string `json:"file"` + Comment string `json:"file_comment"` +} + +// NewRefToMessage initializes a reference to to a message. +func NewRefToMessage(channel, timestamp string) ItemRef { + return ItemRef{Channel: channel, Timestamp: timestamp} +} + +// NewRefToFile initializes a reference to a file. +func NewRefToFile(file string) ItemRef { + return ItemRef{File: file} +} + +// NewRefToComment initializes a reference to a file comment. +func NewRefToComment(comment string) ItemRef { + return ItemRef{Comment: comment} +} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/logger.go b/components/reporters/slack/vendor/github.com/slack-go/slack/logger.go new file mode 100644 index 000000000..90cb3caab --- /dev/null +++ b/components/reporters/slack/vendor/github.com/slack-go/slack/logger.go @@ -0,0 +1,60 @@ +package slack + +import ( + "fmt" +) + +// logger is a logger interface compatible with both stdlib and some +// 3rd party loggers. +type logger interface { + Output(int, string) error +} + +// ilogger represents the internal logging api we use. +type ilogger interface { + logger + Print(...interface{}) + Printf(string, ...interface{}) + Println(...interface{}) +} + +type Debug interface { + Debug() bool + + // Debugf print a formatted debug line. + Debugf(format string, v ...interface{}) + // Debugln print a debug line. + Debugln(v ...interface{}) +} + +// internalLog implements the additional methods used by our internal logging. +type internalLog struct { + logger +} + +// Println replicates the behaviour of the standard logger. +func (t internalLog) Println(v ...interface{}) { + t.Output(2, fmt.Sprintln(v...)) +} + +// Printf replicates the behaviour of the standard logger. +func (t internalLog) Printf(format string, v ...interface{}) { + t.Output(2, fmt.Sprintf(format, v...)) +} + +// Print replicates the behaviour of the standard logger. +func (t internalLog) Print(v ...interface{}) { + t.Output(2, fmt.Sprint(v...)) +} + +type discard struct{} + +func (t discard) Debug() bool { + return false +} + +// Debugf print a formatted debug line. +func (t discard) Debugf(format string, v ...interface{}) {} + +// Debugln print a debug line. +func (t discard) Debugln(v ...interface{}) {} diff --git a/components/reporters/slack/vendor/github.com/slack-go/slack/logo.png b/components/reporters/slack/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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